i18n (Phase C batch 6): settings(JS), mikrotik, discounts, collector_payments, attendance ID/EN

This commit is contained in:
oxpoe committed 2026-10-09 07:32:48 +07:00
1 parent fd13f3fa1b
commit 6ed6945970
7 files changed
+204 -72

No files matched your search

+67 -1
View File
@@ -1781,7 +1781,73 @@
"mayar_webhook": "Mayar Webhook (Mode 1 — Gateway)", "mayar_webhook": "Mayar Webhook (Mode 1 — Gateway)",
"save": "Save Settings", "save": "Save Settings",
"pay_mode": "Mode", "pay_mode": "Mode",
"qris_note": "Note" "qris_note": "Note",
"no_data": "No data yet.",
"ew_not_tx": "Not a transaction",
"ew_paid": "Paid",
"ew_unmatched": "Not matched"
},
"mt": {
"updating": "Updating...",
"online_list": "Online PPPoE Users",
"offline_list": "Offline PPPoE Users",
"limit_uptime": "Uptime Limit (e.g. 1h)"
},
"disc": {
"title": "Invoice Discount",
"settings_title": "Discount Settings",
"opt_selected": "Selected customers (batch)",
"opt_single": "Single customer (1 by 1)",
"opt_all": "All active customers",
"pick_customer": "Select Customer",
"ph_pick_customer": "-- select customer --",
"disc_type": "Discount Type",
"opt_percent": "Percent (%)",
"opt_amount": "Amount (Rp)",
"disc_value": "Discount Value",
"ph_value": "e.g. 10 (percent) / 20000 (Rp)",
"disc_hint": "The percentage is calculated from the base price (before VAT/USO).",
"period_month": "Billing Period — Month",
"year": "Year",
"note_opt": "Note (optional)",
"ph_note": "e.g. independence month promo",
"apply_hint": "The discount applies to <b>unpaid</b> invoices in the selected period. Paid/void invoices are unchanged. The QRIS unique code is regenerated from the new amount.",
"apply": "Apply Discount",
"selected": "selected",
"history": "Discount History (last 100)",
"th_note": "Note"
},
"cpay": {
"title": "Collector Payment Approval",
"list": "Request List",
"opt_all": "All Processes",
"opt_pending": "Needs Approval",
"opt_auto": "Collector Auto",
"opt_admin": "Approved by Admin",
"opt_cashier": "Approved by Cashier",
"opt_rejected": "Manually Rejected",
"opt_manual": "Processed Manually",
"th_invoice": "Invoice",
"th_process": "Process",
"approve": "Approve",
"reject": "Reject"
},
"att": {
"late_checkin": "Late Check-in",
"not_checkout": "Not Checked-out",
"all_attendance": "All Attendance",
"total_staff": "Total Staff",
"today": "Today",
"working": "Currently Working",
"checked_out": "Checked Out",
"late": "Late",
"after_0830": "After 08:30",
"technician": "Technician",
"cashier": "Cashier",
"checkin_time": "Check-in Time",
"duration": "Duration",
"location": "Location",
"photo": "Photo"
} }
}, },
"tech": { "tech": {
+67 -1
View File
@@ -1782,7 +1782,73 @@
"mayar_webhook": "Webhook Mayar (Mode 1 — Gateway)", "mayar_webhook": "Webhook Mayar (Mode 1 — Gateway)",
"save": "Simpan Pengaturan", "save": "Simpan Pengaturan",
"pay_mode": "Mode", "pay_mode": "Mode",
"qris_note": "Catatan" "qris_note": "Catatan",
"no_data": "Belum ada data.",
"ew_not_tx": "Bukan transaksi",
"ew_paid": "Lunas",
"ew_unmatched": "Belum cocok"
},
"mt": {
"updating": "Memperbarui...",
"online_list": "Daftar User PPPoE Online",
"offline_list": "Daftar User PPPoE Offline",
"limit_uptime": "Limit Uptime (e.g. 1h)"
},
"disc": {
"title": "Diskon Tagihan",
"settings_title": "Pengaturan Diskon",
"opt_selected": "Pelanggan terpilih (batch)",
"opt_single": "Satu pelanggan (1 per 1)",
"opt_all": "Semua pelanggan aktif",
"pick_customer": "Pilih Pelanggan",
"ph_pick_customer": "-- pilih pelanggan --",
"disc_type": "Tipe Diskon",
"opt_percent": "Persen (%)",
"opt_amount": "Nominal (Rp)",
"disc_value": "Nilai Diskon",
"ph_value": "mis. 10 (persen) / 20000 (Rp)",
"disc_hint": "Persen dihitung dari harga dasar (sebelum PPN/USO).",
"period_month": "Periode Tagihan — Bulan",
"year": "Tahun",
"note_opt": "Catatan (opsional)",
"ph_note": "mis. promo bulan kemerdekaan",
"apply_hint": "Diskon berlaku ke tagihan <b>belum bayar</b> pada periode terpilih. Tagihan lunas/hangus tidak diubah. Kode unik QRIS akan digenerate ulang dari nominal baru.",
"apply": "Terapkan Diskon",
"selected": "dipilih",
"history": "Riwayat Diskon (100 terakhir)",
"th_note": "Catatan"
},
"cpay": {
"title": "Approval Pembayaran Kolektor",
"list": "Daftar Request",
"opt_all": "Semua Proses",
"opt_pending": "Butuh Approval",
"opt_auto": "Auto Kolektor",
"opt_admin": "Disetujui Admin",
"opt_cashier": "Disetujui Kasir",
"opt_rejected": "Ditolak Manual",
"opt_manual": "Diproses Manual",
"th_invoice": "Tagihan",
"th_process": "Proses",
"approve": "Setujui",
"reject": "Tolak"
},
"att": {
"late_checkin": "Terlambat Check-in",
"not_checkout": "Belum Check-out",
"all_attendance": "Semua Absensi",
"total_staff": "Total Karyawan",
"today": "Hari ini",
"working": "Sedang Bekerja",
"checked_out": "Sudah Pulang",
"late": "Terlambat",
"after_0830": "Setelah 08:30",
"technician": "Teknisi",
"cashier": "Kasir",
"checkin_time": "Waktu Check-in",
"duration": "Durasi",
"location": "Lokasi",
"photo": "Foto"
} }
}, },
"tech": { "tech": {
+20 -20
View File
@@ -131,34 +131,34 @@
<div class="stat-grid" style="grid-template-columns:repeat(4,1fr)"> <div class="stat-grid" style="grid-template-columns:repeat(4,1fr)">
<div class="sc p"> <div class="sc p">
<i class="bi bi-people sc-icon"></i> <i class="bi bi-people sc-icon"></i>
<div class="sc-label">Total Karyawan</div> <div class="sc-label"><%= t('admin.att.total_staff','Total Karyawan') %></div>
<div class="sc-val"><%= totalAll %></div> <div class="sc-val"><%= totalAll %></div>
<div class="sc-sub">Hari ini</div> <div class="sc-sub"><%= t('admin.att.today','Hari ini') %></div>
</div> </div>
<div class="sc s"> <div class="sc s">
<i class="bi bi-check-circle sc-icon"></i> <i class="bi bi-check-circle sc-icon"></i>
<div class="sc-label">Sedang Bekerja</div> <div class="sc-label"><%= t('admin.att.working','Sedang Bekerja') %></div>
<div class="sc-val"><%= checkedInAll %></div> <div class="sc-val"><%= checkedInAll %></div>
<div class="sc-sub">Check-in</div> <div class="sc-sub">Check-in</div>
</div> </div>
<div class="sc p"> <div class="sc p">
<i class="bi bi-door-open sc-icon"></i> <i class="bi bi-door-open sc-icon"></i>
<div class="sc-label">Sudah Pulang</div> <div class="sc-label"><%= t('admin.att.checked_out','Sudah Pulang') %></div>
<div class="sc-val"><%= checkedOutAll %></div> <div class="sc-val"><%= checkedOutAll %></div>
<div class="sc-sub">Check-out</div> <div class="sc-sub">Check-out</div>
</div> </div>
<div class="sc d"> <div class="sc d">
<i class="bi bi-clock-history sc-icon"></i> <i class="bi bi-clock-history sc-icon"></i>
<div class="sc-label">Terlambat</div> <div class="sc-label"><%= t('admin.att.late','Terlambat') %></div>
<div class="sc-val"><%= lateCheckIns.length %></div> <div class="sc-val"><%= lateCheckIns.length %></div>
<div class="sc-sub">Setelah 08:30</div> <div class="sc-sub"><%= t('admin.att.after_0830','Setelah 08:30') %></div>
</div> </div>
</div> </div>
<!-- Breakdown by Type --> <!-- Breakdown by Type -->
<div class="stat-grid" style="grid-template-columns:repeat(4,1fr);margin-top:16px"> <div class="stat-grid" style="grid-template-columns:repeat(4,1fr);margin-top:16px">
<div class="sc" style="border-left:3px solid #3b82f6"> <div class="sc" style="border-left:3px solid #3b82f6">
<div class="sc-label"><i class="bi bi-person-workspace"></i> Teknisi</div> <div class="sc-label"><i class="bi bi-person-workspace"></i> <%= t('admin.att.technician','Teknisi') %></div>
<div class="sc-val"><%= techStats.total || 0 %></div> <div class="sc-val"><%= techStats.total || 0 %></div>
<div class="sc-sub"> <div class="sc-sub">
<span style="color:#22c55e"><%= techStats.checked_in || 0 %> aktif</span> • <span style="color:#22c55e"><%= techStats.checked_in || 0 %> aktif</span> •
@@ -174,7 +174,7 @@
</div> </div>
</div> </div>
<div class="sc" style="border-left:3px solid #22c55e"> <div class="sc" style="border-left:3px solid #22c55e">
<div class="sc-label"><i class="bi bi-cash-coin"></i> Kasir</div> <div class="sc-label"><i class="bi bi-cash-coin"></i> <%= t('admin.att.cashier','Kasir') %></div>
<div class="sc-val"><%= cashierStats.total || 0 %></div> <div class="sc-val"><%= cashierStats.total || 0 %></div>
<div class="sc-sub"> <div class="sc-sub">
<span style="color:#22c55e"><%= cashierStats.checked_in || 0 %> aktif</span> • <span style="color:#22c55e"><%= cashierStats.checked_in || 0 %> aktif</span> •
@@ -195,7 +195,7 @@
<% if (lateCheckIns.length > 0) { %> <% if (lateCheckIns.length > 0) { %>
<div class="card" style="margin-top:20px;border-left:3px solid #ef4444"> <div class="card" style="margin-top:20px;border-left:3px solid #ef4444">
<div class="card-hd"> <div class="card-hd">
<h6><i class="bi bi-exclamation-triangle"></i> Terlambat Check-in (<%= lateCheckIns.length %>)</h6> <h6><i class="bi bi-exclamation-triangle"></i> <%= t('admin.att.late_checkin','Terlambat Check-in') %> (<%= lateCheckIns.length %>)</h6>
</div> </div>
<div class="tbl-wrap"> <div class="tbl-wrap">
<table class="dtbl"> <table class="dtbl">
@@ -203,8 +203,8 @@
<tr> <tr>
<th><%= t('pwa.field.name','Nama') %></th> <th><%= t('pwa.field.name','Nama') %></th>
<th><%= t('common.type','Tipe') %></th> <th><%= t('common.type','Tipe') %></th>
<th>Waktu Check-in</th> <th><%= t('admin.att.checkin_time','Waktu Check-in') %></th>
<th>Terlambat</th> <th><%= t('admin.att.late','Terlambat') %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -231,7 +231,7 @@
<% if (notCheckedOut.length > 0) { %> <% if (notCheckedOut.length > 0) { %>
<div class="card" style="margin-top:20px;border-left:3px solid #f59e0b"> <div class="card" style="margin-top:20px;border-left:3px solid #f59e0b">
<div class="card-hd"> <div class="card-hd">
<h6><i class="bi bi-bell"></i> Belum Check-out (<%= notCheckedOut.length %>)</h6> <h6><i class="bi bi-bell"></i> <%= t('admin.att.not_checkout','Belum Check-out') %> (<%= notCheckedOut.length %>)</h6>
</div> </div>
<div class="tbl-wrap"> <div class="tbl-wrap">
<table class="dtbl"> <table class="dtbl">
@@ -240,7 +240,7 @@
<th><%= t('pwa.field.name','Nama') %></th> <th><%= t('pwa.field.name','Nama') %></th>
<th><%= t('common.type','Tipe') %></th> <th><%= t('common.type','Tipe') %></th>
<th>Check-in</th> <th>Check-in</th>
<th>Durasi</th> <th><%= t('admin.att.duration','Durasi') %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -267,7 +267,7 @@
<!-- All Attendance Records --> <!-- All Attendance Records -->
<div class="card" style="margin-top:20px"> <div class="card" style="margin-top:20px">
<div class="card-hd"> <div class="card-hd">
<h6><i class="bi bi-calendar-check"></i> Semua Absensi - <%= parseDateInTimezone(selectedDate).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) %></h6> <h6><i class="bi bi-calendar-check"></i> <%= t('admin.att.all_attendance','Semua Absensi') %> - <%= parseDateInTimezone(selectedDate).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) %></h6>
</div> </div>
<% if (attendances.length > 0) { %> <% if (attendances.length > 0) { %>
@@ -279,10 +279,10 @@
<th><%= t('common.type','Tipe') %></th> <th><%= t('common.type','Tipe') %></th>
<th>Check-in</th> <th>Check-in</th>
<th>Check-out</th> <th>Check-out</th>
<th>Durasi</th> <th><%= t('admin.att.duration','Durasi') %></th>
<th><%= t('common.status','Status') %></th> <th><%= t('common.status','Status') %></th>
<th>Lokasi</th> <th><%= t('admin.att.location','Lokasi') %></th>
<th>Foto</th> <th><%= t('admin.att.photo','Foto') %></th>
<th><%= t('common.actions','Aksi') %></th> <th><%= t('common.actions','Aksi') %></th>
</tr> </tr>
</thead> </thead>
@@ -336,13 +336,13 @@
<div class="photo-stack"> <div class="photo-stack">
<% if (att.check_in_photo) { %> <% if (att.check_in_photo) { %>
<a href="<%= att.check_in_photo %>" target="_blank" class="photo-item"> <a href="<%= att.check_in_photo %>" target="_blank" class="photo-item">
<img src="<%= att.check_in_photo %>" alt="Foto check-in" class="photo-thumb"> <img src="<%= att.check_in_photo %>" alt="<%= t('admin.att.photo','Foto') %> check-in" class="photo-thumb">
In In
</a> </a>
<% } %> <% } %>
<% if (att.check_out_photo) { %> <% if (att.check_out_photo) { %>
<a href="<%= att.check_out_photo %>" target="_blank" class="photo-item"> <a href="<%= att.check_out_photo %>" target="_blank" class="photo-item">
<img src="<%= att.check_out_photo %>" alt="Foto check-out" class="photo-thumb"> <img src="<%= att.check_out_photo %>" alt="<%= t('admin.att.photo','Foto') %> check-out" class="photo-thumb">
Out Out
</a> </a>
<% } %> <% } %>
@@ -402,7 +402,7 @@
} }
function editAttendance(id) { function editAttendance(id) {
const checkIn = prompt('Waktu Check-in (YYYY-MM-DD HH:MM:SS):'); const checkIn = prompt('<%= t('admin.att.checkin_time','Waktu Check-in') %> (YYYY-MM-DD HH:MM:SS):');
if (!checkIn) return; if (!checkIn) return;
const checkOut = prompt('Waktu Check-out (YYYY-MM-DD HH:MM:SS) - kosongkan jika belum:'); const checkOut = prompt('Waktu Check-out (YYYY-MM-DD HH:MM:SS) - kosongkan jika belum:');
+14 -14
View File
@@ -2,7 +2,7 @@
<html lang="<%= lang || 'id' %>"> <html lang="<%= lang || 'id' %>">
<head> <head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Approval Pembayaran Kolektor | <%= company %></title> <title><%= t('admin.cpay.title','Approval Pembayaran Kolektor') %> | <%= company %></title>
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css"> <link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/admin.css"> <link rel="stylesheet" href="/css/admin.css">
</head> </head>
@@ -14,7 +14,7 @@
<div class="tb-title"> <div class="tb-title">
<button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button> <button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button>
<i class="bi bi-check2-square"></i> <i class="bi bi-check2-square"></i>
<span>Approval Pembayaran Kolektor</span> <span><%= t('admin.cpay.title','Approval Pembayaran Kolektor') %></span>
</div> </div>
<div class="tb-right"> <div class="tb-right">
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session.isCashier) ? t('admin.role.cashier', 'Kasir') : t('admin.role.admin', 'Admin') %></span> <span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session.isCashier) ? t('admin.role.cashier', 'Kasir') : t('admin.role.admin', 'Admin') %></span>
@@ -31,20 +31,20 @@
<div class="card"> <div class="card">
<div class="card-hd"> <div class="card-hd">
<h6><i class="bi bi-list-ul"></i> Daftar Request (<%= (rows || []).length %>)</h6> <h6><i class="bi bi-list-ul"></i> <%= t('admin.cpay.list','Daftar Request') %> (<%= (rows || []).length %>)</h6>
</div> </div>
<div class="hint" style="padding:12px 16px 0 16px;font-size:12px;color:var(--muted)"> <div class="hint" style="padding:12px 16px 0 16px;font-size:12px;color:var(--muted)">
Label proses membantu membedakan request yang memang menunggu approval, request yang otomatis langsung lunas oleh kolektor, dan request yang sudah diproses admin atau kasir. Label proses membantu membedakan request yang memang menunggu approval, request yang otomatis langsung lunas oleh kolektor, dan request yang sudah diproses admin atau kasir.
</div> </div>
<div style="padding:12px 16px 0 16px;display:flex;gap:10px;flex-wrap:wrap"> <div style="padding:12px 16px 0 16px;display:flex;gap:10px;flex-wrap:wrap">
<select id="processFilter" class="fs" style="max-width:220px"> <select id="processFilter" class="fs" style="max-width:220px">
<option value="all">Semua Proses</option> <option value="all"><%= t('admin.cpay.opt_all','Semua Proses') %></option>
<option value="pending">Butuh Approval</option> <option value="pending"><%= t('admin.cpay.opt_pending','Butuh Approval') %></option>
<option value="auto">Auto Kolektor</option> <option value="auto"><%= t('admin.cpay.opt_auto','Auto Kolektor') %></option>
<option value="admin">Disetujui Admin</option> <option value="admin"><%= t('admin.cpay.opt_admin','Disetujui Admin') %></option>
<option value="cashier">Disetujui Kasir</option> <option value="cashier"><%= t('admin.cpay.opt_cashier','Disetujui Kasir') %></option>
<option value="rejected">Ditolak Manual</option> <option value="rejected"><%= t('admin.cpay.opt_rejected','Ditolak Manual') %></option>
<option value="manual">Diproses Manual</option> <option value="manual"><%= t('admin.cpay.opt_manual','Diproses Manual') %></option>
</select> </select>
</div> </div>
<div class="tbl-wrap"> <div class="tbl-wrap">
@@ -54,9 +54,9 @@
<th><%= t('collector.col_time','Waktu') %></th> <th><%= t('collector.col_time','Waktu') %></th>
<th><%= t('collector.role','Kolektor') %></th> <th><%= t('collector.role','Kolektor') %></th>
<th><%= t('common.customer','Pelanggan') %></th> <th><%= t('common.customer','Pelanggan') %></th>
<th>Tagihan</th> <th><%= t('admin.cpay.th_invoice','Tagihan') %></th>
<th class="num"><%= t('collector.col_amount','Nominal') %></th> <th class="num"><%= t('collector.col_amount','Nominal') %></th>
<th>Proses</th> <th><%= t('admin.cpay.th_process','Proses') %></th>
<th><%= t('common.status','Status') %></th> <th><%= t('common.status','Status') %></th>
<th><%= t('common.actions','Aksi') %></th> <th><%= t('common.actions','Aksi') %></th>
</tr> </tr>
@@ -119,12 +119,12 @@
<div class="act-menu" style="min-width:270px"> <div class="act-menu" style="min-width:270px">
<form method="POST" action="/admin/collector-payments/<%= r.id %>/approve" style="display:flex;gap:6px;padding:4px 4px 6px"> <form method="POST" action="/admin/collector-payments/<%= r.id %>/approve" style="display:flex;gap:6px;padding:4px 4px 6px">
<input class="fc" name="decided_note" placeholder="Catatan (opsional)" style="font-size:12px"> <input class="fc" name="decided_note" placeholder="Catatan (opsional)" style="font-size:12px">
<button class="btn btn-s btn-sm" type="submit" title="Approve"><i class="bi bi-check-lg"></i> Setujui</button> <button class="btn btn-s btn-sm" type="submit" title="Approve"><i class="bi bi-check-lg"></i> <%= t('admin.cpay.approve','Setujui') %></button>
</form> </form>
<div class="act-sep"></div> <div class="act-sep"></div>
<form method="POST" action="/admin/collector-payments/<%= r.id %>/reject" style="display:flex;gap:6px;padding:4px"> <form method="POST" action="/admin/collector-payments/<%= r.id %>/reject" style="display:flex;gap:6px;padding:4px">
<input class="fc" name="decided_note" placeholder="Alasan (opsional)" style="font-size:12px"> <input class="fc" name="decided_note" placeholder="Alasan (opsional)" style="font-size:12px">
<button class="btn btn-d btn-sm" type="submit" title="Reject"><i class="bi bi-x-lg"></i> Tolak</button> <button class="btn btn-d btn-sm" type="submit" title="Reject"><i class="bi bi-x-lg"></i> <%= t('admin.cpay.reject','Tolak') %></button>
</form> </form>
</div> </div>
</div> </div>
+27 -27
View File
@@ -2,7 +2,7 @@
<html lang="<%= lang || 'id' %>"> <html lang="<%= lang || 'id' %>">
<head> <head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Diskon Tagihan | <%= company %></title> <title><%= t('admin.disc.title','Diskon Tagihan') %> | <%= company %></title>
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css"> <link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/admin.css"> <link rel="stylesheet" href="/css/admin.css">
<style> <style>
@@ -16,7 +16,7 @@
<%- include('partials/sidebar', { activePage: 'discounts', company }) %> <%- include('partials/sidebar', { activePage: 'discounts', company }) %>
<div class="mw"> <div class="mw">
<div class="topbar"> <div class="topbar">
<div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-percent"></i> Diskon Tagihan</div> <div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-percent"></i> <%= t('admin.disc.title','Diskon Tagihan') %></div>
<div class="tb-right"> <div class="tb-right">
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session.isCashier) ? 'Kasir' : 'Admin' %></span> <span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session.isCashier) ? 'Kasir' : 'Admin' %></span>
<a href="/admin/logout" class="btn btn-g btn-sm"><i class="bi bi-box-arrow-left"></i></a> <a href="/admin/logout" class="btn btn-g btn-sm"><i class="bi bi-box-arrow-left"></i></a>
@@ -33,42 +33,42 @@
<form method="POST" action="/admin/discounts/apply" id="discForm"> <form method="POST" action="/admin/discounts/apply" id="discForm">
<!-- Konfigurasi diskon --> <!-- Konfigurasi diskon -->
<div class="card mb16"> <div class="card mb16">
<div class="card-hd"><h6><i class="bi bi-sliders"></i> Pengaturan Diskon</h6></div> <div class="card-hd"><h6><i class="bi bi-sliders"></i> <%= t('admin.disc.settings_title','Pengaturan Diskon') %></h6></div>
<div class="card-bd"> <div class="card-bd">
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Target</label> <label class="fl">Target</label>
<select class="fs" name="apply_mode" id="applyMode"> <select class="fs" name="apply_mode" id="applyMode">
<option value="selected" selected>Pelanggan terpilih (batch)</option> <option value="selected" selected><%= t('admin.disc.opt_selected','Pelanggan terpilih (batch)') %></option>
<option value="single">Satu pelanggan (1 per 1)</option> <option value="single"><%= t('admin.disc.opt_single','Satu pelanggan (1 per 1)') %></option>
<option value="all">Semua pelanggan aktif</option> <option value="all"><%= t('admin.disc.opt_all','Semua pelanggan aktif') %></option>
</select> </select>
</div> </div>
<div class="fg" id="singleWrap" style="display:none"> <div class="fg" id="singleWrap" style="display:none">
<label class="fl">Pilih Pelanggan</label> <label class="fl"><%= t('admin.disc.pick_customer','Pilih Pelanggan') %></label>
<select class="fs" name="customer_id"> <select class="fs" name="customer_id">
<option value="">-- pilih pelanggan --</option> <option value=""><%= t('admin.disc.ph_pick_customer','-- pilih pelanggan --') %></option>
<% customers.forEach(function(c){ %> <% customers.forEach(function(c){ %>
<option value="<%= c.id %>"><%= c.customer_code || c.id %> — <%= c.name %><%= c.area ? ' ('+c.area+')' : '' %></option> <option value="<%= c.id %>"><%= c.customer_code || c.id %> — <%= c.name %><%= c.area ? ' ('+c.area+')' : '' %></option>
<% }); %> <% }); %>
</select> </select>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Tipe Diskon</label> <label class="fl"><%= t('admin.disc.disc_type','Tipe Diskon') %></label>
<select class="fs" name="discount_type" id="discType"> <select class="fs" name="discount_type" id="discType">
<option value="percent">Persen (%)</option> <option value="percent"><%= t('admin.disc.opt_percent','Persen (%)') %></option>
<option value="amount">Nominal (Rp)</option> <option value="amount"><%= t('admin.disc.opt_amount','Nominal (Rp)') %></option>
</select> </select>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Nilai Diskon</label> <label class="fl"><%= t('admin.disc.disc_value','Nilai Diskon') %></label>
<input class="fc" type="number" name="discount_value" id="discValue" min="1" step="1" placeholder="mis. 10 (persen) / 20000 (Rp)" required> <input class="fc" type="number" name="discount_value" id="discValue" min="1" step="1" placeholder="<%= t('admin.disc.ph_value','mis. 10 (persen) / 20000 (Rp)') %>" required>
<div class="hint" id="discHint">Persen dihitung dari harga dasar (sebelum PPN/USO).</div> <div class="hint" id="discHint"><%= t('admin.disc.disc_hint','Persen dihitung dari harga dasar (sebelum PPN/USO).') %></div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Periode Tagihan — Bulan</label> <label class="fl"><%= t('admin.disc.period_month','Periode Tagihan — Bulan') %></label>
<select class="fs" name="period_month"> <select class="fs" name="period_month">
<% const mNames=['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember']; <% const mNames=['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
for(let m=1;m<=12;m++){ %> for(let m=1;m<=12;m++){ %>
@@ -77,7 +77,7 @@
</select> </select>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Tahun</label> <label class="fl"><%= t('admin.disc.year','Tahun') %></label>
<select class="fs" name="period_year"> <select class="fs" name="period_year">
<% for(let y=(timeInfo.year||new Date().getFullYear())+1; y>=2023; y--){ %> <% for(let y=(timeInfo.year||new Date().getFullYear())+1; y>=2023; y--){ %>
<option value="<%= y %>" <%= (timeInfo.year===y)?'selected':'' %>><%= y %></option> <option value="<%= y %>" <%= (timeInfo.year===y)?'selected':'' %>><%= y %></option>
@@ -85,13 +85,13 @@
</select> </select>
</div> </div>
<div class="fg" style="flex:2"> <div class="fg" style="flex:2">
<label class="fl">Catatan (opsional)</label> <label class="fl"><%= t('admin.disc.note_opt','Catatan (opsional)') %></label>
<input class="fc" name="note" placeholder="mis. promo bulan kemerdekaan"> <input class="fc" name="note" placeholder="<%= t('admin.disc.ph_note','mis. promo bulan kemerdekaan') %>">
</div> </div>
</div> </div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px"> <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px">
<div class="hint"><i class="bi bi-info-circle"></i> Diskon berlaku ke tagihan <b>belum bayar</b> pada periode terpilih. Tagihan lunas/hangus tidak diubah. Kode unik QRIS akan digenerate ulang dari nominal baru.</div> <div class="hint"><i class="bi bi-info-circle"></i> <%- t('admin.disc.apply_hint','Diskon berlaku ke tagihan <b>belum bayar</b> pada periode terpilih. Tagihan lunas/hangus tidak diubah. Kode unik QRIS akan digenerate ulang dari nominal baru.') %></div>
<button type="submit" class="btn btn-p" id="applyBtn"><i class="bi bi-percent"></i> Terapkan Diskon</button> <button type="submit" class="btn btn-p" id="applyBtn"><i class="bi bi-percent"></i> <%= t('admin.disc.apply','Terapkan Diskon') %></button>
</div> </div>
</div> </div>
</div> </div>
@@ -100,9 +100,9 @@
<div class="card mb16" id="custCard"> <div class="card mb16" id="custCard">
<div class="card-hd"> <div class="card-hd">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;width:100%"> <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;width:100%">
<h6 style="margin:0"><i class="bi bi-people"></i> Pilih Pelanggan (<%= customers.length %>)</h6> <h6 style="margin:0"><i class="bi bi-people"></i> <%= t('admin.disc.pick_customer','Pilih Pelanggan') %> (<%= customers.length %>)</h6>
<div class="disc-filter"> <div class="disc-filter">
<span class="disc-badge" id="selCount" title="Jumlah pelanggan terpilih">0 dipilih</span> <span class="disc-badge" id="selCount" title="Jumlah pelanggan terpilih">0 <%= t('admin.disc.selected','dipilih') %></span>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer"> <label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer">
<input type="checkbox" id="selectAll" style="width:16px;height:16px"> Pilih semua yang tampil <input type="checkbox" id="selectAll" style="width:16px;height:16px"> Pilih semua yang tampil
</label> </label>
@@ -146,7 +146,7 @@
<!-- Riwayat --> <!-- Riwayat -->
<div class="card"> <div class="card">
<div class="card-hd"><h6><i class="bi bi-clock-history"></i> Riwayat Diskon (100 terakhir)</h6></div> <div class="card-hd"><h6><i class="bi bi-clock-history"></i> <%= t('admin.disc.history','Riwayat Diskon (100 terakhir)') %></h6></div>
<div class="tbl-wrap"> <div class="tbl-wrap">
<table class="dtbl"> <table class="dtbl">
<thead> <thead>
@@ -158,7 +158,7 @@
<th class="num" style="width:120px">Potongan</th> <th class="num" style="width:120px">Potongan</th>
<th class="num" style="width:120px">Awal → Akhir</th> <th class="num" style="width:120px">Awal → Akhir</th>
<th style="width:110px">Oleh</th> <th style="width:110px">Oleh</th>
<th>Catatan</th> <th><%= t('admin.disc.th_note','Catatan') %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -201,7 +201,7 @@
function syncHint(){ function syncHint(){
hint.textContent = (type.value === 'percent') hint.textContent = (type.value === 'percent')
? 'Persen dihitung dari harga dasar (sebelum PPN/USO). Contoh: 10 = potong 10%.' ? '<%= t('admin.disc.disc_hint','Persen dihitung dari harga dasar (sebelum PPN/USO).') %> Contoh: 10 = potong 10%.'
: 'Nominal potongan langsung dalam Rupiah. Contoh: 20000 = potong Rp 20.000.'; : 'Nominal potongan langsung dalam Rupiah. Contoh: 20000 = potong Rp 20.000.';
} }
type.addEventListener('change', syncHint); syncHint(); type.addEventListener('change', syncHint); syncHint();
@@ -217,8 +217,8 @@
var applyBtn = document.getElementById('applyBtn'); var applyBtn = document.getElementById('applyBtn');
function updateSelCount(){ function updateSelCount(){
var n = document.querySelectorAll('#custTable .cust-sel:checked').length; var n = document.querySelectorAll('#custTable .cust-sel:checked').length;
if (selCount) selCount.textContent = n + ' dipilih'; if (selCount) selCount.textContent = n + ' <%= t('admin.disc.selected','dipilih') %>';
if (applyBtn) applyBtn.innerHTML = '<i class="bi bi-percent"></i> Terapkan Diskon' + ((mode.value === 'selected' && n) ? ' (' + n + ')' : ''); if (applyBtn) applyBtn.innerHTML = '<i class="bi bi-percent"></i> <%= t('admin.disc.apply','Terapkan Diskon') %>' + ((mode.value === 'selected' && n) ? ' (' + n + ')' : '');
} }
all.addEventListener('change', function(){ visibleBoxes().forEach(function(b){ b.checked = all.checked; }); updateSelCount(); }); all.addEventListener('change', function(){ visibleBoxes().forEach(function(b){ b.checked = all.checked; }); updateSelCount(); });
document.getElementById('custTable').addEventListener('change', function(e){ if (e.target && e.target.classList.contains('cust-sel')) updateSelCount(); }); document.getElementById('custTable').addEventListener('change', function(e){ if (e.target && e.target.classList.contains('cust-sel')) updateSelCount(); });
+4 -4
View File
@@ -85,7 +85,7 @@
</div> </div>
<div id="refresh-indicator" style="position:absolute;top:-8px;right:0;font-size:11px;color:var(--muted);display:none;align-items:center;gap:4px"> <div id="refresh-indicator" style="position:absolute;top:-8px;right:0;font-size:11px;color:var(--muted);display:none;align-items:center;gap:4px">
<i class="bi bi-arrow-clockwise" style="animation:spin 1s linear infinite"></i> <i class="bi bi-arrow-clockwise" style="animation:spin 1s linear infinite"></i>
<span>Memperbarui...</span> <span><%= t('admin.mt.updating','Memperbarui...') %></span>
</div> </div>
</div> </div>
<style> <style>
@@ -250,7 +250,7 @@
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"><label class="fl">Limit Uptime (e.g. 1h)</label><input class="fc" id="h_uptime"></div> <div class="fg"><label class="fl"><%= t('admin.mt.limit_uptime','Limit Uptime (e.g. 1h)') %></label><input class="fc" id="h_uptime"></div>
<div class="fg"><label class="fl">MAC Address</label><input class="fc" id="h_mac"></div> <div class="fg"><label class="fl">MAC Address</label><input class="fc" id="h_mac"></div>
</div> </div>
<div class="fg"><label class="fl">Comment</label><input class="fc" id="h_comment"></div> <div class="fg"><label class="fl">Comment</label><input class="fc" id="h_comment"></div>
@@ -267,7 +267,7 @@
<div class="mo" id="onlineUsersModal"> <div class="mo" id="onlineUsersModal">
<div class="mb" style="max-width:900px"> <div class="mb" style="max-width:900px">
<div class="mh"> <div class="mh">
<span class="mt"><i class="bi bi-ethernet"></i> Daftar User PPPoE Online</span> <span class="mt"><i class="bi bi-ethernet"></i> <%= t('admin.mt.online_list','Daftar User PPPoE Online') %></span>
<button class="mc" onclick="closeModal('onlineUsersModal')">&times;</button> <button class="mc" onclick="closeModal('onlineUsersModal')">&times;</button>
</div> </div>
<div class="mbody" style="max-height:500px;overflow-y:auto"> <div class="mbody" style="max-height:500px;overflow-y:auto">
@@ -307,7 +307,7 @@
<div class="mo" id="offlineUsersModal"> <div class="mo" id="offlineUsersModal">
<div class="mb" style="max-width:800px"> <div class="mb" style="max-width:800px">
<div class="mh"> <div class="mh">
<span class="mt"><i class="bi bi-plug"></i> Daftar User PPPoE Offline</span> <span class="mt"><i class="bi bi-plug"></i> <%= t('admin.mt.offline_list','Daftar User PPPoE Offline') %></span>
<button class="mc" onclick="closeModal('offlineUsersModal')">&times;</button> <button class="mc" onclick="closeModal('offlineUsersModal')">&times;</button>
</div> </div>
<div class="mbody" style="max-height:500px;overflow-y:auto"> <div class="mbody" style="max-height:500px;overflow-y:auto">
+5 -5
View File
@@ -315,7 +315,7 @@
</div> </div>
</div> </div>
<div class="hint" style="margin:0 0 10px;padding:12px;border:1px dashed var(--border);border-radius:10px;background:var(--bg2)"> <div class="hint" style="margin:0 0 10px;padding:12px;border:1px dashed var(--border);border-radius:10px;background:var(--bg2)">
<i class="bi bi-robot"></i> <strong>Otomatisasi dipindah:</strong> Auto-Nonaktif, Auto-Hapus, Auto-Peringatan &amp; Auto-Lunas kini tertata di menu <a href="/admin/automation" style="color:var(--primary);font-weight:800">Automation &rarr;</a> <i class="bi bi-robot"></i> <strong>Otomatisasi dipindah:</strong> Auto-Nonaktif, Auto-Hapus, Auto-Peringatan &amp; Auto-<%= t('admin.set.ew_paid','Lunas') %> kini tertata di menu <a href="/admin/automation" style="color:var(--primary);font-weight:800">Automation &rarr;</a>
</div> </div>
<div class="hint" style="margin-top:6px;color:var(--warning)"> <div class="hint" style="margin-top:6px;color:var(--warning)">
<i class="bi bi-info-circle"></i> <strong><%= t('admin.set.colpay_label','Pembayaran Kolektor:') %></strong> <%= t('admin.set.colpay_desc','Pengaturan approval pembayaran kolektor sekarang diatur per akun kolektor pada menu Manajemen Kolektor, bukan dari pengaturan global.') %> <i class="bi bi-info-circle"></i> <strong><%= t('admin.set.colpay_label','Pembayaran Kolektor:') %></strong> <%= t('admin.set.colpay_desc','Pengaturan approval pembayaran kolektor sekarang diatur per akun kolektor pada menu Manajemen Kolektor, bukan dari pengaturan global.') %>
@@ -1031,7 +1031,7 @@ async function loadEwalletLogs() {
if (!res.ok || !data.ok) throw new Error(data.error || 'Gagal memuat log'); if (!res.ok || !data.ok) throw new Error(data.error || 'Gagal memuat log');
const rows = Array.isArray(data.rows) ? data.rows : []; const rows = Array.isArray(data.rows) ? data.rows : [];
if (rows.length === 0) { if (rows.length === 0) {
body.innerHTML = '<tr><td colspan="7" class="text-center text-muted">Belum ada data.</td></tr>'; body.innerHTML = '<tr><td colspan="7" class="text-center text-muted"><%= t('admin.set.no_data','Belum ada data.') %></td></tr>';
return; return;
} }
const newest = rows[0]; const newest = rows[0];
@@ -1056,9 +1056,9 @@ async function loadEwalletLogs() {
const md = Number(r.matched_donation_order_id || 0) || 0; const md = Number(r.matched_donation_order_id || 0) || 0;
const isMatched = !!(mi || mv || md); const isMatched = !!(mi || mv || md);
let statusBadge; let statusBadge;
if (!parsed) statusBadge = '<span class="ew-badge ew-gray">Bukan transaksi</span>'; if (!parsed) statusBadge = '<span class="ew-badge ew-gray"><%= t('admin.set.ew_not_tx','Bukan transaksi') %></span>';
else if (isMatched) statusBadge = '<span class="ew-badge ew-green">Lunas</span>'; else if (isMatched) statusBadge = '<span class="ew-badge ew-green"><%= t('admin.set.ew_paid','Lunas') %></span>';
else statusBadge = '<span class="ew-badge ew-amber">Belum cocok</span>'; else statusBadge = '<span class="ew-badge ew-amber"><%= t('admin.set.ew_unmatched','Belum cocok') %></span>';
const amountHtml = (amountNum == null) const amountHtml = (amountNum == null)
? '<span class="muted">—</span>' ? '<span class="muted">—</span>'
: ('<span class="mono" style="font-weight:800;' + (parsed ? 'color:var(--primary)' : 'color:var(--muted)') + '">Rp ' + amountNum.toLocaleString('id-ID') + '</span>'); : ('<span class="mono" style="font-weight:800;' + (parsed ? 'color:var(--primary)' : 'color:var(--muted)') + '">Rp ' + amountNum.toLocaleString('id-ID') + '</span>');