Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
211 lines
8.2 KiB
Plaintext
211 lines
8.2 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="<%= lang || 'id' %>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><%= t('admin.print_vouchers.title', 'Cetak Voucher Batch') %> #<%= batch.id %> — <%= company %></title>
|
|
<link rel="stylesheet" href="/css/admin.css">
|
|
<!-- no-admin-theme -->
|
|
<style>
|
|
@page {
|
|
size: A4 portrait;
|
|
margin: 6mm 8mm;
|
|
}
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
-webkit-print-color-adjust: exact !important;
|
|
print-color-adjust: exact !important;
|
|
}
|
|
body {
|
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
color: #0f172a;
|
|
background: #f8fafc;
|
|
padding: 12px;
|
|
}
|
|
.tpl { width: 100%; }
|
|
.tpl .page-break { break-after: page; page-break-after: always; }
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
}
|
|
|
|
.card {
|
|
border: 1.5px solid #334155;
|
|
border-radius: 8px;
|
|
padding: 8px 10px 24px 10px;
|
|
min-height: 112px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: stretch;
|
|
background: #ffffff;
|
|
break-inside: avoid !important;
|
|
page-break-inside: avoid !important;
|
|
}
|
|
|
|
.card::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, rgba(59,130,246,.04), rgba(16,185,129,.03));
|
|
}
|
|
|
|
.card > * { position: relative; }
|
|
.left { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; }
|
|
.right { width: 48%; display: flex; flex-direction: column; justify-content: center; align-items: center; }
|
|
.row { display: flex; justify-content: space-between; gap: 6px; align-items: flex-start; }
|
|
.co { font-weight: 900; font-size: 11px; letter-spacing: .2px; line-height: 1.1; color: #0f172a; text-transform: uppercase; }
|
|
.wa-btm { position: absolute; left: 50%; bottom: 4px; transform: translateX(-50%); font-size: 8.5px; color: #475569; white-space: nowrap; z-index: 4; font-weight: 500; }
|
|
.pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px; border-radius: 4px; background: rgba(2,6,23,.06); font-size: 9.5px; color: #0f172a; white-space: nowrap; line-height: 1.1; font-weight: 600; }
|
|
.pills { display: flex; gap: 4px; align-items: center; margin-top: 4px; }
|
|
.price-pill { background: rgba(16,185,129,.15); border: 1px solid rgba(16,185,129,.4); color: #065f46; font-weight: 700; }
|
|
.right-meta { margin-top: 4px; font-size: 9.5px; font-weight: 600; color: #334155; }
|
|
.price-top { margin-bottom: 6px; font-size: 10.5px; }
|
|
.code { font-weight: 900; font-size: 24px; letter-spacing: 1.5px; text-align: center; margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: #0f172a; }
|
|
.foot { font-size: 9px; color: #475569; margin-top: 4px; line-height: 1.15; }
|
|
.muted { color: #64748b; }
|
|
|
|
/* Floating Controls */
|
|
.print-controls {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
display: flex;
|
|
gap: 10px;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.btn-action {
|
|
background: #1e3a8a;
|
|
color: #ffffff;
|
|
border: none;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.btn-close {
|
|
background: #475569;
|
|
}
|
|
|
|
@media print {
|
|
body {
|
|
background: #ffffff !important;
|
|
padding: 0 !important;
|
|
}
|
|
.no-print, .print-controls {
|
|
display: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
<!-- MWRAP (admin.css dipindah ke atas agar style halaman tidak ketimpa) -->
|
|
<style>
|
|
@media screen { body { padding: 0 !important; } .print-controls { display: none !important; } .tb-title, .tb-title i { color: var(--text) !important; } .sidebar { color: var(--text); } }
|
|
@media print {
|
|
.topbar, .sidebar, .bottom-nav, .hb-menu { display: none !important; }
|
|
.page { padding: 0 !important; margin: 0 !important; }
|
|
.mw { display: block !important; height: auto !important; overflow: visible !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<%- include('partials/sidebar', { activePage: 'vouchers', company }) %>
|
|
<div class="mw">
|
|
<div class="topbar">
|
|
<div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-printer"></i> <%= t('admin.print_vouchers.title', 'Cetak Voucher Batch') %> #<%= batch.id %></div>
|
|
<div class="tb-right">
|
|
<a href="/admin/vouchers/batches/<%= batch.id %>/pdf" target="_blank" class="btn btn-p btn-sm"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
|
<a href="/admin/vouchers" class="btn btn-g btn-sm"><i class="bi bi-arrow-left"></i> <%= t('common.back', 'Kembali') %></a>
|
|
<a href="/admin/logout" class="btn btn-g btn-sm"><i class="bi bi-box-arrow-left"></i></a>
|
|
</div>
|
|
</div>
|
|
<div class="page">
|
|
<%
|
|
function formatValidity(v) {
|
|
if (!v) return '-';
|
|
const s = String(v).trim();
|
|
const mDay = s.match(/^(\d+)\s*d$/i);
|
|
if (mDay) {
|
|
const n = Number(mDay[1]);
|
|
if ((lang || 'id') === 'en') return n + (n === 1 ? ' day' : ' days');
|
|
return n + ' ' + t('admin.print_vouchers.days', 'hari');
|
|
}
|
|
const mHour = s.match(/^(\d+)\s*h$/i);
|
|
if (mHour) {
|
|
return mHour[1] + ' Jam';
|
|
}
|
|
return s;
|
|
}
|
|
%>
|
|
<% if (templateError) { %>
|
|
<div class="no-print" style="padding:10px 12px;background:#fff7ed;border:1px solid #fb923c;margin:8px;border-radius:8px;font-size:12px">
|
|
<b>Template Print:</b> <%= templateError %>
|
|
</div>
|
|
<% } %>
|
|
<% if (style === 'template' && renderedHtml) { %>
|
|
<div class="tpl"><%- renderedHtml %></div>
|
|
<% } else { %>
|
|
<div class="grid">
|
|
<% vouchers.forEach(v => { %>
|
|
<div class="card">
|
|
<div class="left">
|
|
<div class="row">
|
|
<div class="co"><%= settings.company_header || company %></div>
|
|
</div>
|
|
<div class="pills">
|
|
<div class="pill"><i class="bi bi-wifi"></i> <%= batch.package_name || 'Voucher Hotspot' %></div>
|
|
</div>
|
|
<% if (v.code !== v.password) { %>
|
|
<div class="foot">
|
|
<div class="muted"><%= t('admin.print_vouchers.hint_user_pass', 'User & Pass') %></div>
|
|
</div>
|
|
<% } else { %>
|
|
<div class="foot">
|
|
<div class="muted">Login Hotspot</div>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
<div class="right">
|
|
<div class="pill price-pill price-top">Rp <%= Number(batch.price || 0).toLocaleString('id-ID') %></div>
|
|
<% if (v.code === v.password) { %>
|
|
<div class="code"><%= v.code %></div>
|
|
<div class="right-meta"><i class="bi bi-hourglass-split"></i> <%= formatValidity(batch.validity) %></div>
|
|
<% } else { %>
|
|
<div class="code" style="font-size:15px; letter-spacing:1px">U: <%= v.code %></div>
|
|
<div class="code" style="font-size:15px; letter-spacing:1px; margin-top:2px">P: <%= v.password %></div>
|
|
<div class="right-meta"><i class="bi bi-hourglass-split"></i> <%= formatValidity(batch.validity) %></div>
|
|
<% } %>
|
|
</div>
|
|
<div class="wa-btm">CS/WA: <%= (settings.whatsapp_admin_numbers && settings.whatsapp_admin_numbers.length > 0) ? '+' + settings.whatsapp_admin_numbers[0] : (settings.company_phone || '-') %></div>
|
|
</div>
|
|
<% }) %>
|
|
</div>
|
|
<% } %>
|
|
|
|
<div class="print-controls">
|
|
<button class="btn-action" onclick="window.print()"><i class="bi bi-printer-fill"></i> Cetak Voucher</button>
|
|
<button class="btn-action btn-close" onclick="window.close()"><i class="bi bi-x-lg"></i> <%= t('common.close','Tutup') %></button>
|
|
</div>
|
|
|
|
<script>
|
|
window.onload = function() {
|
|
setTimeout(() => {
|
|
if (!window.location.search.includes('noprint')) {
|
|
window.print();
|
|
}
|
|
}, 400);
|
|
}
|
|
</script>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|