i18n (Phase C batch 10): map, map-gis, vouchers_packages, finance/cash_in, finance/expenses ID/EN

This commit is contained in:
oxpoe committed 2026-10-09 07:56:10 +07:00
1 parent d09ab217f9
commit fd074f8ff7
7 files changed
+401 -139

No files matched your search

+132 -1
View File
@@ -899,7 +899,35 @@
"cable": { "cable": {
"online": "Online", "online": "Online",
"offline": "Offline" "offline": "Offline"
} },
"onu_active": "Active ONU (Map)",
"onu_isolir": "Isolated ONU (Map)",
"no_gps": "No GPS",
"not_loaded": "Map not loaded",
"not_loaded_sub": "Click <b>SEE MAPS</b> to load the map &amp; customer/ODP list (saves resources).",
"see_maps": "SEE MAPS",
"ph_search": "Search customer name, PPPoE, phone, ONU SN, address, ODP...",
"clear_search": "Clear search",
"search_result": "Search Results",
"tip_all": "Show all markers",
"tip_active": "Active ONU only",
"tip_isolir": "Isolated ONU only",
"tip_labels": "Hide/Show name labels on markers",
"label_on": "Label: ON",
"tip_fit": "Center map to all points",
"opt_hybrid": "Satellite (Hybrid)",
"opt_sat": "Satellite (Clean)",
"opt_dark": "Dark",
"list_onu": "Customer ONU List",
"list_odp": "ODP (Box) List",
"ph_filter": "Filter name, PPPoE, phone, SN/Tag...",
"all_gps": "All GPS Statuses",
"only_active": "Active only",
"only_isolir": "Isolated only",
"th_pppoe": "PPPoE / ONU Tag",
"th_pkg_odp": "Package & ODP",
"th_gps": "GPS Coordinates",
"ph_odp": "Search ODP (name / description)..."
}, },
"print_invoice": { "print_invoice": {
"title": "Print Invoice Receipt", "title": "Print Invoice Receipt",
@@ -2052,6 +2080,109 @@
"work_hours": "Work hours", "work_hours": "Work hours",
"avg": "Average", "avg": "Average",
"hours_per_day": "Hours/day" "hours_per_day": "Hours/day"
},
"gis": {
"ph_search": "Search ODP, ODC, HTB, Customer...",
"layers": "Map Layers",
"show_elems": "Show Elements",
"el_server": "Server & OLT",
"el_odc": "ODC Feeder Hub",
"el_odp": "ODP Distribution",
"el_htb": "HTB Media Converter",
"el_fiber": "Fiber Cable Route",
"el_radius": "Signal Radius (150m)",
"tip_ruler": "Measure Fiber Cable Distance on Map",
"ruler": "Measure Distance",
"tip_center": "Center Map to Network",
"tip_workflow": "Open Visual Workflow Mapping (n8n Style)",
"workflow": "Workflow Map",
"tip_dark": "High-contrast Dark Vector Mode",
"tip_clean": "Clean Minimalist Roads & Buildings Mode",
"tip_sat": "High-resolution Satellite Aerial Mode",
"ruler_title": "Cable Length Measurer",
"ruler_desc": "Click points on the map to measure the fiber optic cable route.",
"total_length": "Total Length:",
"legend_server": "Server / OLT",
"coord": "Coordinates:"
},
"vp": {
"list": "Real-Time Hotspot Package List",
"add_config": "Add Package Configuration",
"all_router": "All Routers",
"th_profile": "Profile Name",
"loading": "Loading package data...",
"config": "Package Configuration",
"pick_router": "Select MikroTik Router",
"opt_router": "-- Select Router --",
"router_req": "A router must be selected for voucher packages",
"opt_from_mt": "-- Select from MikroTik --",
"ph_profile": "Type profile name manually...",
"profile_hint": "Select a router & profile to read the default duration, or type manually if MikroTik is offline.",
"price": "Selling Price (Rp) *",
"ph_price": "Example: 5000",
"validity": "Duration / Validity *",
"ph_validity": "Example: 1d, 12h, 3h",
"validity_hint": "Mikhmon format (e.g.: 1d = 1 day, 12h = 12 hours)",
"prefix": "Voucher Code Prefix",
"ph_prefix": "Optional (e.g.: ALI-)",
"code_len": "Code Length (Number of Digits) *",
"char_type": "Code Character Type *",
"title": "Hotspot Voucher Packages",
"th_price": "Price",
"th_validity": "Validity",
"th_prefix": "Prefix",
"th_len": "Code Length",
"th_chars": "Characters"
},
"cashin": {
"heading": "Cash In (Others)",
"add": "Add Cash In",
"month": "Cash In This Month",
"total": "Total Cash In (Others)",
"history": "Cash In History",
"th_recorder": "Recorded By",
"modal": "Record Cash In",
"source": "Source / Category",
"opt_modal": "Business Capital / Fund Injection",
"opt_asset": "Asset/Used Goods Sale",
"opt_service": "Other Service Income",
"opt_other": "Others",
"amount": "Amount / Nominal (Rp)",
"ph_desc": "Capital injection from the owner...",
"method": "Receiving Method",
"pay_cash": "Cash",
"pay_transfer": "Bank Transfer",
"pay_ewallet": "E-Wallet (OVO/Dana/Gopay)",
"ref": "Reference No. (Optional)",
"ph_ref": "Can be left blank",
"save": "Save Income",
"title": "Cash In",
"th_method": "Method"
},
"exp": {
"heading": "Expenses (Cash Out)",
"modal": "Record New Expense",
"add": "Record Expense",
"month": "Expenses This Month",
"total": "Total Recorded Expenses",
"history": "Expense History",
"th_vendor": "Vendor/Store",
"th_recorder": "Recorded By",
"date": "Expense Date",
"opt_category": "-- Select Category --",
"amount": "Amount / Nominal (Rp)",
"ph_desc": "Buy MikroTik Router RB750Gr3...",
"vendor": "Store/Vendor Name (Optional)",
"ph_vendor": "E.g.: Tokopedia / Computer Store X",
"receipt": "Receipt/Invoice No. (Optional)",
"method": "Payment Method",
"pay_cash": "Cash",
"pay_transfer": "Bank Transfer",
"pay_ewallet": "E-Wallet (OVO/Dana/Gopay)",
"pay_credit": "Credit / Paylater",
"save": "Save Expense",
"title": "Expenses",
"th_method": "Method"
} }
}, },
"tech": { "tech": {
+132 -1
View File
@@ -899,7 +899,35 @@
"cable": { "cable": {
"online": "Online", "online": "Online",
"offline": "Offline" "offline": "Offline"
} },
"onu_active": "ONU Aktif (Peta)",
"onu_isolir": "ONU Isolir (Peta)",
"no_gps": "Tanpa GPS",
"not_loaded": "Peta belum dimuat",
"not_loaded_sub": "Klik <b>SEE MAPS</b> untuk memuat peta &amp; daftar pelanggan/ODP (hemat resource).",
"see_maps": "LIHAT PETA",
"ph_search": "Cari nama pelanggan, PPPoE, No HP, SN ONU, alamat, ODP...",
"clear_search": "Hapus pencarian",
"search_result": "Hasil Pencarian",
"tip_all": "Tampilkan semua marker",
"tip_active": "Hanya ONU aktif",
"tip_isolir": "Hanya ONU isolir",
"tip_labels": "Sembunyikan/Tampilkan label nama di marker",
"label_on": "Label: ON",
"tip_fit": "Pusatkan peta ke semua titik",
"opt_hybrid": "Satelit (Hybrid)",
"opt_sat": "Satelit (Bersih)",
"opt_dark": "Gelap (Dark)",
"list_onu": "Daftar ONU Pelanggan",
"list_odp": "Daftar ODP (Box)",
"ph_filter": "Filter nama, PPPoE, HP, SN/Tag...",
"all_gps": "Semua Status GPS",
"only_active": "Hanya Aktif",
"only_isolir": "Hanya Isolir",
"th_pppoe": "PPPoE / Tag ONU",
"th_pkg_odp": "Paket & ODP",
"th_gps": "Koordinat GPS",
"ph_odp": "Cari ODP (nama / deskripsi)..."
}, },
"print_invoice": { "print_invoice": {
"title": "Cetak Struk Tagihan", "title": "Cetak Struk Tagihan",
@@ -2053,6 +2081,109 @@
"work_hours": "Jam kerja", "work_hours": "Jam kerja",
"avg": "Rata-rata", "avg": "Rata-rata",
"hours_per_day": "Jam/hari" "hours_per_day": "Jam/hari"
},
"gis": {
"ph_search": "Cari ODP, ODC, HTB, Pelanggan...",
"layers": "Layer Peta",
"show_elems": "Tampilkan Elemen",
"el_server": "Server & OLT",
"el_odc": "ODC Feeder Hub",
"el_odp": "ODP Distribusi",
"el_htb": "HTB Media Converter",
"el_fiber": "Jalur Kabel Fiber",
"el_radius": "Radius Sinyal (150m)",
"tip_ruler": "Ukur Jarak Kabel Fiber di Peta",
"ruler": "Ukur Jarak",
"tip_center": "Pusatkan Peta ke Jaringan",
"tip_workflow": "Buka Visual Workflow Mapping (n8n Style)",
"workflow": "Workflow Map",
"tip_dark": "Mode Vektor Gelap Berkontras Tinggi",
"tip_clean": "Mode Bersih Jalan & Bangunan Minimalis",
"tip_sat": "Mode Satelit Citra Udara Resolusi Tinggi",
"ruler_title": "Pengukur Panjang Kabel",
"ruler_desc": "Klik titik di peta untuk mengukur rute kabel optik.",
"total_length": "Total Panjang:",
"legend_server": "Server / OLT",
"coord": "Koordinat:"
},
"vp": {
"list": "Daftar Paket Hotspot Real-Time",
"add_config": "Tambah Konfigurasi Paket",
"all_router": "Semua Router",
"th_profile": "Nama Profile",
"loading": "Memuat data paket...",
"config": "Konfigurasi Paket",
"pick_router": "Pilih Router MikroTik",
"opt_router": "-- Pilih Router --",
"router_req": "Router harus dipilih untuk paket voucher",
"opt_from_mt": "-- Pilih dari MikroTik --",
"ph_profile": "Ketik nama profile manual...",
"profile_hint": "Pilih router & profil untuk membaca durasi bawaan atau ketik manual jika MikroTik offline.",
"price": "Harga Jual (Rp) *",
"ph_price": "Contoh: 5000",
"validity": "Durasi / Masa Aktif *",
"ph_validity": "Contoh: 1d, 12h, 3h",
"validity_hint": "Format Mikhmon (misal: 1d = 1 hari, 12h = 12 jam)",
"prefix": "Prefix Kode Voucher",
"ph_prefix": "Opsional (misal: ALI-)",
"code_len": "Panjang Kode (Jumlah Digit) *",
"char_type": "Tipe Karakter Kode *",
"title": "Paket Voucher Hotspot",
"th_price": "Harga",
"th_validity": "Masa Aktif",
"th_prefix": "Prefix",
"th_len": "Panjang Kode",
"th_chars": "Karakter"
},
"cashin": {
"heading": "Kas Masuk (Lain-lain)",
"add": "Tambah Kas Masuk",
"month": "Kas Masuk Bulan Ini",
"total": "Total Kas Masuk (Lainnya)",
"history": "Riwayat Kas Masuk",
"th_recorder": "Pencatat",
"modal": "Catat Kas Masuk",
"source": "Sumber / Kategori",
"opt_modal": "Modal Usaha / Suntikan Dana",
"opt_asset": "Penjualan Aset/Barang Bekas",
"opt_service": "Pendapatan Jasa Lainnya",
"opt_other": "Lain-lain",
"amount": "Jumlah / Nominal (Rp)",
"ph_desc": "Suntikan modal dari owner...",
"method": "Metode Penerimaan",
"pay_cash": "Tunai (Cash)",
"pay_transfer": "Transfer Bank",
"pay_ewallet": "E-Wallet (OVO/Dana/Gopay)",
"ref": "No. Referensi (Opsional)",
"ph_ref": "Bisa dikosongkan",
"save": "Simpan Pemasukan",
"title": "Kas Masuk",
"th_method": "Metode"
},
"exp": {
"heading": "Pengeluaran (Cash Out)",
"modal": "Catat Pengeluaran Baru",
"add": "Catat Pengeluaran",
"month": "Pengeluaran Bulan Ini",
"total": "Total Pengeluaran Dicatat",
"history": "Riwayat Pengeluaran",
"th_vendor": "Vendor/Toko",
"th_recorder": "Pencatat",
"date": "Tanggal Pengeluaran",
"opt_category": "-- Pilih Kategori --",
"amount": "Jumlah / Nominal (Rp)",
"ph_desc": "Beli Router Mikrotik RB750Gr3...",
"vendor": "Nama Toko/Vendor (Opsional)",
"ph_vendor": "Misal: Tokopedia / Toko Komputer X",
"receipt": "No. Nota / Kwitansi (Opsional)",
"method": "Metode Pembayaran",
"pay_cash": "Tunai (Cash)",
"pay_transfer": "Transfer Bank",
"pay_ewallet": "E-Wallet (OVO/Dana/Gopay)",
"pay_credit": "Kredit / Paylater",
"save": "Simpan Pengeluaran",
"title": "Pengeluaran",
"th_method": "Metode"
} }
}, },
"tech": { "tech": {
+23 -23
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>Kas Masuk | <%= company %></title> <title><%= t('admin.cashin.title','Kas Masuk') %> | <%= 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,11 +16,11 @@
<%- include('../partials/sidebar', { activePage: 'cash_in', company }) %> <%- include('../partials/sidebar', { activePage: 'cash_in', 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-cash-stack"></i> Kas Masuk (Lain-lain)</div> <div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-cash-stack"></i> <%= t('admin.cashin.heading','Kas Masuk (Lain-lain)') %></div>
<div class="tb-right"> <div class="tb-right">
<span class="adm-badge" style="font-size:12px;opacity:0.7;"><%= company %></span> <span class="adm-badge" style="font-size:12px;opacity:0.7;"><%= company %></span>
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? 'Kasir' : 'Admin' %></span> <span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? 'Kasir' : 'Admin' %></span>
<button class="btn btn-p btn-sm" onclick="openModal('addModal')"><i class="bi bi-plus-lg"></i> Tambah Kas Masuk</button> <button class="btn btn-p btn-sm" onclick="openModal('addModal')"><i class="bi bi-plus-lg"></i> <%= t('admin.cashin.add','Tambah Kas Masuk') %></button>
</div> </div>
</div> </div>
<div class="page"> <div class="page">
@@ -42,14 +42,14 @@
<div class="val-box"> <div class="val-box">
<div class="val-icon" style="background:rgba(16, 185, 129, 0.1);color:#10b981"><i class="bi bi-graph-up-arrow"></i></div> <div class="val-icon" style="background:rgba(16, 185, 129, 0.1);color:#10b981"><i class="bi bi-graph-up-arrow"></i></div>
<div> <div>
<div class="val-text">Kas Masuk Bulan Ini</div> <div class="val-text"><%= t('admin.cashin.month','Kas Masuk Bulan Ini') %></div>
<div class="val-num">Rp <%= totalThisMonth.toLocaleString('id-ID') %></div> <div class="val-num">Rp <%= totalThisMonth.toLocaleString('id-ID') %></div>
</div> </div>
</div> </div>
<div class="val-box"> <div class="val-box">
<div class="val-icon" style="background:rgba(59, 130, 246, 0.1);color:#3b82f6"><i class="bi bi-piggy-bank-fill"></i></div> <div class="val-icon" style="background:rgba(59, 130, 246, 0.1);color:#3b82f6"><i class="bi bi-piggy-bank-fill"></i></div>
<div> <div>
<div class="val-text">Total Kas Masuk (Lainnya)</div> <div class="val-text"><%= t('admin.cashin.total','Total Kas Masuk (Lainnya)') %></div>
<div class="val-num">Rp <%= totalAll.toLocaleString('id-ID') %></div> <div class="val-num">Rp <%= totalAll.toLocaleString('id-ID') %></div>
</div> </div>
</div> </div>
@@ -57,7 +57,7 @@
<!-- Table --> <!-- Table -->
<div class="card"> <div class="card">
<div class="card-hd"><h6><i class="bi bi-list-ul"></i> Riwayat Kas Masuk</h6></div> <div class="card-hd"><h6><i class="bi bi-list-ul"></i> <%= t('admin.cashin.history','Riwayat Kas Masuk') %></h6></div>
<div class="tbl-wrap"> <div class="tbl-wrap">
<table class="dtbl"> <table class="dtbl">
<thead> <thead>
@@ -65,9 +65,9 @@
<th><%= t('common.date','Tanggal') %></th> <th><%= t('common.date','Tanggal') %></th>
<th><%= t('common.category','Kategori') %></th> <th><%= t('common.category','Kategori') %></th>
<th><%= t('common.desc_note','Deskripsi / Catatan') %></th> <th><%= t('common.desc_note','Deskripsi / Catatan') %></th>
<th>Metode</th> <th><%= t('admin.cashin.th_method','Metode') %></th>
<th class="num"><%= t('common.amount_qty','Jumlah') %></th> <th class="num"><%= t('common.amount_qty','Jumlah') %></th>
<th>Pencatat</th> <th><%= t('admin.cashin.th_recorder','Pencatat') %></th>
<th><%= t('common.actions','Aksi') %></th> <th><%= t('common.actions','Aksi') %></th>
</tr> </tr>
</thead> </thead>
@@ -105,7 +105,7 @@
<!-- Modal Add --> <!-- Modal Add -->
<div class="mo" id="addModal"> <div class="mo" id="addModal">
<div class="mb"> <div class="mb">
<div class="mh"><span class="mt"><i class="bi bi-plus-circle"></i> Catat Kas Masuk</span><button type="button" class="mc" onclick="closeModal('addModal')">&times;</button></div> <div class="mh"><span class="mt"><i class="bi bi-plus-circle"></i> <%= t('admin.cashin.modal','Catat Kas Masuk') %></span><button type="button" class="mc" onclick="closeModal('addModal')">&times;</button></div>
<form method="POST" action="/admin/finance/cash-in"> <form method="POST" action="/admin/finance/cash-in">
<div class="mbody"> <div class="mbody">
<div class="form-row"> <div class="form-row">
@@ -114,41 +114,41 @@
<input type="date" class="fc" name="date" required value="<%= getNowLocal().substring(0,10) %>" max="<%= getNowLocal().substring(0,10) %>"> <input type="date" class="fc" name="date" required value="<%= getNowLocal().substring(0,10) %>" max="<%= getNowLocal().substring(0,10) %>">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Sumber / Kategori <span class="text-danger">*</span></label> <label class="fl"><%= t('admin.cashin.source','Sumber / Kategori') %> <span class="text-danger">*</span></label>
<select class="fc" name="category" required> <select class="fc" name="category" required>
<option value="Modal Usaha">Modal Usaha / Suntikan Dana</option> <option value="Modal Usaha"><%= t('admin.cashin.opt_modal','Modal Usaha / Suntikan Dana') %></option>
<option value="Penjualan Aset">Penjualan Aset/Barang Bekas</option> <option value="Penjualan Aset"><%= t('admin.cashin.opt_asset','Penjualan Aset/Barang Bekas') %></option>
<option value="Jasa Lainnya">Pendapatan Jasa Lainnya</option> <option value="Jasa Lainnya"><%= t('admin.cashin.opt_service','Pendapatan Jasa Lainnya') %></option>
<option value="Lain-lain">Lain-lain</option> <option value="<%= t('admin.cashin.opt_other','Lain-lain') %>"><%= t('admin.cashin.opt_other','Lain-lain') %></option>
</select> </select>
</div> </div>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Jumlah / Nominal (Rp) <span class="text-danger">*</span></label> <label class="fl"><%= t('admin.cashin.amount','Jumlah / Nominal (Rp)') %> <span class="text-danger">*</span></label>
<input type="text" class="fc" name="amount" id="amountInput" required placeholder="Contoh: 500.000" style="font-size:16px;font-weight:bold" autocomplete="off"> <input type="text" class="fc" name="amount" id="amountInput" required placeholder="Contoh: 500.000" style="font-size:16px;font-weight:bold" autocomplete="off">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl"><%= t('common.desc_note2','Deskripsi / Keterangan') %> <span class="text-danger">*</span></label> <label class="fl"><%= t('common.desc_note2','Deskripsi / Keterangan') %> <span class="text-danger">*</span></label>
<textarea class="fc" name="description" required placeholder="Suntikan modal dari owner..."></textarea> <textarea class="fc" name="description" required placeholder="<%= t('admin.cashin.ph_desc','Suntikan modal dari owner...') %>"></textarea>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Metode Penerimaan</label> <label class="fl"><%= t('admin.cashin.method','Metode Penerimaan') %></label>
<select class="fc" name="payment_method"> <select class="fc" name="payment_method">
<option value="cash">Tunai (Cash)</option> <option value="cash"><%= t('admin.cashin.pay_cash','Tunai (Cash)') %></option>
<option value="transfer">Transfer Bank</option> <option value="transfer"><%= t('admin.cashin.pay_transfer','Transfer Bank') %></option>
<option value="ewallet">E-Wallet (OVO/Dana/Gopay)</option> <option value="ewallet"><%= t('admin.cashin.pay_ewallet','E-Wallet (OVO/Dana/Gopay)') %></option>
</select> </select>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">No. Referensi (Opsional)</label> <label class="fl"><%= t('admin.cashin.ref','No. Referensi (Opsional)') %></label>
<input type="text" class="fc" name="receipt_number" placeholder="Bisa dikosongkan"> <input type="text" class="fc" name="receipt_number" placeholder="<%= t('admin.cashin.ph_ref','Bisa dikosongkan') %>">
</div> </div>
</div> </div>
</div> </div>
<div class="mf"> <div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('addModal')"><%= t('common.cancel','Batal') %></button> <button type="button" class="btn btn-g" onclick="closeModal('addModal')"><%= t('common.cancel','Batal') %></button>
<button type="submit" class="btn btn-p"><i class="bi bi-save"></i> Simpan Pemasukan</button> <button type="submit" class="btn btn-p"><i class="bi bi-save"></i> <%= t('admin.cashin.save','Simpan Pemasukan') %></button>
</div> </div>
</form> </form>
</div> </div>
+23 -23
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>Pengeluaran | <%= company %></title> <title><%= t('admin.exp.title','Pengeluaran') %> | <%= 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>
@@ -17,11 +17,11 @@
<%- include('../partials/sidebar', { activePage: 'expenses', company }) %> <%- include('../partials/sidebar', { activePage: 'expenses', 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-wallet2"></i> Pengeluaran (Cash Out)</div> <div class="tb-title"><button class="hb-menu" onclick="toggleSidebar()"><i class="bi bi-list"></i></button><i class="bi bi-wallet2"></i> <%= t('admin.exp.heading','Pengeluaran (Cash Out)') %></div>
<div class="tb-right"> <div class="tb-right">
<span class="adm-badge" style="font-size:12px;opacity:0.7;"><%= company %></span> <span class="adm-badge" style="font-size:12px;opacity:0.7;"><%= company %></span>
<span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? 'Kasir' : 'Admin' %></span> <span class="adm-badge"><i class="bi bi-person-fill"></i> <%= (typeof session !== 'undefined' && session && session.isCashier) ? 'Kasir' : 'Admin' %></span>
<button class="btn btn-p btn-sm" onclick="openModal('addModal')"><i class="bi bi-plus-lg"></i> Catat Pengeluaran</button> <button class="btn btn-p btn-sm" onclick="openModal('addModal')"><i class="bi bi-plus-lg"></i> <%= t('admin.exp.add','Catat Pengeluaran') %></button>
</div> </div>
</div> </div>
<div class="page"> <div class="page">
@@ -39,14 +39,14 @@
<div class="val-box"> <div class="val-box">
<div class="val-icon" style="background:rgba(239, 68, 68, 0.1);color:#ef4444"><i class="bi bi-graph-down-arrow"></i></div> <div class="val-icon" style="background:rgba(239, 68, 68, 0.1);color:#ef4444"><i class="bi bi-graph-down-arrow"></i></div>
<div> <div>
<div class="val-text">Pengeluaran Bulan Ini</div> <div class="val-text"><%= t('admin.exp.month','Pengeluaran Bulan Ini') %></div>
<div class="val-num">Rp <%= totalThisMonth.toLocaleString('id-ID') %></div> <div class="val-num">Rp <%= totalThisMonth.toLocaleString('id-ID') %></div>
</div> </div>
</div> </div>
<div class="val-box"> <div class="val-box">
<div class="val-icon" style="background:rgba(99, 102, 241, 0.1);color:#6366f1"><i class="bi bi-wallet-fill"></i></div> <div class="val-icon" style="background:rgba(99, 102, 241, 0.1);color:#6366f1"><i class="bi bi-wallet-fill"></i></div>
<div> <div>
<div class="val-text">Total Pengeluaran Dicatat</div> <div class="val-text"><%= t('admin.exp.total','Total Pengeluaran Dicatat') %></div>
<div class="val-num">Rp <%= totalAll.toLocaleString('id-ID') %></div> <div class="val-num">Rp <%= totalAll.toLocaleString('id-ID') %></div>
</div> </div>
</div> </div>
@@ -54,7 +54,7 @@
<!-- Table --> <!-- Table -->
<div class="card"> <div class="card">
<div class="card-hd"><h6><i class="bi bi-list-ul"></i> Riwayat Pengeluaran</h6></div> <div class="card-hd"><h6><i class="bi bi-list-ul"></i> <%= t('admin.exp.history','Riwayat Pengeluaran') %></h6></div>
<div class="tbl-wrap"> <div class="tbl-wrap">
<table class="dtbl"> <table class="dtbl">
<thead> <thead>
@@ -62,10 +62,10 @@
<th><%= t('common.date','Tanggal') %></th> <th><%= t('common.date','Tanggal') %></th>
<th><%= t('common.category','Kategori') %></th> <th><%= t('common.category','Kategori') %></th>
<th><%= t('common.desc_note','Deskripsi / Catatan') %></th> <th><%= t('common.desc_note','Deskripsi / Catatan') %></th>
<th>Vendor/Toko</th> <th><%= t('admin.exp.th_vendor','Vendor/Toko') %></th>
<th>Metode</th> <th><%= t('admin.exp.th_method','Metode') %></th>
<th class="num"><%= t('common.amount_qty','Jumlah') %></th> <th class="num"><%= t('common.amount_qty','Jumlah') %></th>
<th>Pencatat</th> <th><%= t('admin.exp.th_recorder','Pencatat') %></th>
<th><%= t('common.actions','Aksi') %></th> <th><%= t('common.actions','Aksi') %></th>
</tr> </tr>
</thead> </thead>
@@ -108,18 +108,18 @@
<!-- Modal Add --> <!-- Modal Add -->
<div class="mo" id="addModal"> <div class="mo" id="addModal">
<div class="mb"> <div class="mb">
<div class="mh"><span class="mt"><i class="bi bi-plus-circle"></i> Catat Pengeluaran Baru</span><button type="button" class="mc" onclick="closeModal('addModal')">&times;</button></div> <div class="mh"><span class="mt"><i class="bi bi-plus-circle"></i> <%= t('admin.exp.modal','Catat Pengeluaran Baru') %></span><button type="button" class="mc" onclick="closeModal('addModal')">&times;</button></div>
<form method="POST" action="/admin/finance/expenses"> <form method="POST" action="/admin/finance/expenses">
<div class="mbody"> <div class="mbody">
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Tanggal Pengeluaran <span class="text-danger">*</span></label> <label class="fl"><%= t('admin.exp.date','Tanggal Pengeluaran') %> <span class="text-danger">*</span></label>
<input type="date" class="fc" name="date" required value="<%= getNowLocal().substring(0,10) %>" max="<%= getNowLocal().substring(0,10) %>"> <input type="date" class="fc" name="date" required value="<%= getNowLocal().substring(0,10) %>" max="<%= getNowLocal().substring(0,10) %>">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl"><%= t('common.category','Kategori') %> <span class="text-danger">*</span></label> <label class="fl"><%= t('common.category','Kategori') %> <span class="text-danger">*</span></label>
<select class="fc" name="category" required> <select class="fc" name="category" required>
<option value="">-- Pilih Kategori --</option> <option value=""><%= t('admin.exp.opt_category','-- Pilih Kategori --') %></option>
<% categories.forEach(c => { %> <% categories.forEach(c => { %>
<option value="<%= c.name %>"><%= c.name %></option> <option value="<%= c.name %>"><%= c.name %></option>
<% }) %> <% }) %>
@@ -127,36 +127,36 @@
</div> </div>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Jumlah / Nominal (Rp) <span class="text-danger">*</span></label> <label class="fl"><%= t('admin.exp.amount','Jumlah / Nominal (Rp)') %> <span class="text-danger">*</span></label>
<input type="text" class="fc" name="amount" id="amountInput" required placeholder="Contoh: 150.000" style="font-size:16px;font-weight:bold" autocomplete="off"> <input type="text" class="fc" name="amount" id="amountInput" required placeholder="Contoh: 150.000" style="font-size:16px;font-weight:bold" autocomplete="off">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl"><%= t('common.desc_note2','Deskripsi / Keterangan') %> <span class="text-danger">*</span></label> <label class="fl"><%= t('common.desc_note2','Deskripsi / Keterangan') %> <span class="text-danger">*</span></label>
<textarea class="fc" name="description" required placeholder="Beli Router Mikrotik RB750Gr3..."></textarea> <textarea class="fc" name="description" required placeholder="<%= t('admin.exp.ph_desc','Beli Router Mikrotik RB750Gr3...') %>"></textarea>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Nama Toko/Vendor (Opsional)</label> <label class="fl"><%= t('admin.exp.vendor','Nama Toko/Vendor (Opsional)') %></label>
<input type="text" class="fc" name="vendor" placeholder="Misal: Tokopedia / Toko Komputer X"> <input type="text" class="fc" name="vendor" placeholder="<%= t('admin.exp.ph_vendor','Misal: Tokopedia / Toko Komputer X') %>">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">No. Nota / Kwitansi (Opsional)</label> <label class="fl"><%= t('admin.exp.receipt','No. Nota / Kwitansi (Opsional)') %></label>
<input type="text" class="fc" name="receipt_number" placeholder="INV-123456"> <input type="text" class="fc" name="receipt_number" placeholder="INV-123456">
</div> </div>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Metode Pembayaran</label> <label class="fl"><%= t('admin.exp.method','Metode Pembayaran') %></label>
<select class="fc" name="payment_method"> <select class="fc" name="payment_method">
<option value="cash">Tunai (Cash)</option> <option value="cash"><%= t('admin.exp.pay_cash','Tunai (Cash)') %></option>
<option value="transfer">Transfer Bank</option> <option value="transfer"><%= t('admin.exp.pay_transfer','Transfer Bank') %></option>
<option value="ewallet">E-Wallet (OVO/Dana/Gopay)</option> <option value="ewallet"><%= t('admin.exp.pay_ewallet','E-Wallet (OVO/Dana/Gopay)') %></option>
<option value="credit">Kredit / Paylater</option> <option value="credit"><%= t('admin.exp.pay_credit','Kredit / Paylater') %></option>
</select> </select>
</div> </div>
</div> </div>
<div class="mf"> <div class="mf">
<button type="button" class="btn btn-g" onclick="closeModal('addModal')"><%= t('common.cancel','Batal') %></button> <button type="button" class="btn btn-g" onclick="closeModal('addModal')"><%= t('common.cancel','Batal') %></button>
<button type="submit" class="btn btn-p"><i class="bi bi-save"></i> Simpan Pengeluaran</button> <button type="submit" class="btn btn-p"><i class="bi bi-save"></i> <%= t('admin.exp.save','Simpan Pengeluaran') %></button>
</div> </div>
</form> </form>
</div> </div>
+27 -27
View File
@@ -268,7 +268,7 @@
<input <input
type="text" type="text"
id="gisSearchInp" id="gisSearchInp"
placeholder="Cari ODP, ODC, HTB, Pelanggan..." placeholder="<%= t('admin.gis.ph_search','Cari ODP, ODC, HTB, Pelanggan...') %>"
oninput="handleGisSearch(this.value)" oninput="handleGisSearch(this.value)"
class="w-full bg-slate-900/90 border border-slate-700/70 rounded-lg pl-8 pr-7 py-1.5 text-xs text-white placeholder-slate-400 focus:outline-none focus:border-sky-500 transition" class="w-full bg-slate-900/90 border border-slate-700/70 rounded-lg pl-8 pr-7 py-1.5 text-xs text-white placeholder-slate-400 focus:outline-none focus:border-sky-500 transition"
> >
@@ -294,18 +294,18 @@
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 hover:text-white text-xs font-semibold transition" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 hover:text-white text-xs font-semibold transition"
> >
<i class="bi bi-layers text-sm"></i> <i class="bi bi-layers text-sm"></i>
<span>Layer Peta</span> <span><%= t('admin.gis.layers','Layer Peta') %></span>
<i class="bi bi-chevron-down text-xs"></i> <i class="bi bi-chevron-down text-xs"></i>
</button> </button>
<!-- Layer Dropdown Box --> <!-- Layer Dropdown Box -->
<div id="gisLayersMenu" class="hidden absolute left-0 top-full mt-2 w-64 bg-slate-900/98 backdrop-blur-2xl border border-slate-700/90 rounded-xl shadow-[0_25px_60px_rgba(0,0,0,0.95)] p-3.5 z-[1050] space-y-2.5 text-xs max-h-[calc(100vh-200px)] overflow-y-auto ring-1 ring-white/10"> <div id="gisLayersMenu" class="hidden absolute left-0 top-full mt-2 w-64 bg-slate-900/98 backdrop-blur-2xl border border-slate-700/90 rounded-xl shadow-[0_25px_60px_rgba(0,0,0,0.95)] p-3.5 z-[1050] space-y-2.5 text-xs max-h-[calc(100vh-200px)] overflow-y-auto ring-1 ring-white/10">
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Tampilkan Elemen</div> <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1"><%= t('admin.gis.show_elems','Tampilkan Elemen') %></div>
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-2.5 h-2.5 rounded bg-blue-500"></span> <span class="w-2.5 h-2.5 rounded bg-blue-500"></span>
<span>Server & OLT</span> <span><%= t('admin.gis.el_server','Server & OLT') %></span>
</span> </span>
<input type="checkbox" id="chkLayerServer" checked onchange="toggleLayerVisibility('server')" class="rounded border-slate-700 text-blue-500 focus:ring-0"> <input type="checkbox" id="chkLayerServer" checked onchange="toggleLayerVisibility('server')" class="rounded border-slate-700 text-blue-500 focus:ring-0">
</label> </label>
@@ -313,7 +313,7 @@
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-2.5 h-2.5 rounded bg-amber-500"></span> <span class="w-2.5 h-2.5 rounded bg-amber-500"></span>
<span>ODC Feeder Hub</span> <span><%= t('admin.gis.el_odc','ODC Feeder Hub') %></span>
</span> </span>
<input type="checkbox" id="chkLayerOdc" checked onchange="toggleLayerVisibility('odc')" class="rounded border-slate-700 text-amber-500 focus:ring-0"> <input type="checkbox" id="chkLayerOdc" checked onchange="toggleLayerVisibility('odc')" class="rounded border-slate-700 text-amber-500 focus:ring-0">
</label> </label>
@@ -321,7 +321,7 @@
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-2.5 h-2.5 rounded bg-emerald-500"></span> <span class="w-2.5 h-2.5 rounded bg-emerald-500"></span>
<span>ODP Distribusi</span> <span><%= t('admin.gis.el_odp','ODP Distribusi') %></span>
</span> </span>
<input type="checkbox" id="chkLayerOdp" checked onchange="toggleLayerVisibility('odp')" class="rounded border-slate-700 text-emerald-500 focus:ring-0"> <input type="checkbox" id="chkLayerOdp" checked onchange="toggleLayerVisibility('odp')" class="rounded border-slate-700 text-emerald-500 focus:ring-0">
</label> </label>
@@ -329,7 +329,7 @@
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-2.5 h-2.5 rounded bg-fuchsia-500"></span> <span class="w-2.5 h-2.5 rounded bg-fuchsia-500"></span>
<span>HTB Media Converter</span> <span><%= t('admin.gis.el_htb','HTB Media Converter') %></span>
</span> </span>
<input type="checkbox" id="chkLayerHtb" checked onchange="toggleLayerVisibility('htb')" class="rounded border-slate-700 text-fuchsia-500 focus:ring-0"> <input type="checkbox" id="chkLayerHtb" checked onchange="toggleLayerVisibility('htb')" class="rounded border-slate-700 text-fuchsia-500 focus:ring-0">
</label> </label>
@@ -346,7 +346,7 @@
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-3 h-0.5 bg-emerald-400"></span> <span class="w-3 h-0.5 bg-emerald-400"></span>
<span>Jalur Kabel Fiber</span> <span><%= t('admin.gis.el_fiber','Jalur Kabel Fiber') %></span>
</span> </span>
<input type="checkbox" id="chkLayerCables" checked onchange="toggleLayerVisibility('cables')" class="rounded border-slate-700 text-emerald-500 focus:ring-0"> <input type="checkbox" id="chkLayerCables" checked onchange="toggleLayerVisibility('cables')" class="rounded border-slate-700 text-emerald-500 focus:ring-0">
</label> </label>
@@ -354,7 +354,7 @@
<label class="flex items-center justify-between cursor-pointer py-1"> <label class="flex items-center justify-between cursor-pointer py-1">
<span class="flex items-center gap-2 text-slate-300"> <span class="flex items-center gap-2 text-slate-300">
<span class="w-2.5 h-2.5 rounded-full border border-sky-400"></span> <span class="w-2.5 h-2.5 rounded-full border border-sky-400"></span>
<span>Radius Sinyal (150m)</span> <span><%= t('admin.gis.el_radius','Radius Sinyal (150m)') %></span>
</span> </span>
<input type="checkbox" id="chkLayerRadius" onchange="toggleLayerVisibility('radius')" class="rounded border-slate-700 text-sky-500 focus:ring-0"> <input type="checkbox" id="chkLayerRadius" onchange="toggleLayerVisibility('radius')" class="rounded border-slate-700 text-sky-500 focus:ring-0">
</label> </label>
@@ -368,15 +368,15 @@
onclick="toggleRulerTool()" onclick="toggleRulerTool()"
id="btnRulerTool" id="btnRulerTool"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 hover:text-white text-xs font-semibold transition" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 hover:text-white text-xs font-semibold transition"
title="Ukur Jarak Kabel Fiber di Peta" title="<%= t('admin.gis.tip_ruler','Ukur Jarak Kabel Fiber di Peta') %>"
> >
<i class="bi bi-rulers text-sm"></i> <i class="bi bi-rulers text-sm"></i>
<span>Ukur Jarak</span> <span><%= t('admin.gis.ruler','Ukur Jarak') %></span>
</button> </button>
</div> </div>
<!-- Right: Mode Switcher & Navigation to Workflow Map --> <!-- Right: Mode Switcher & Navigation to <%= t('admin.gis.workflow','Workflow Map') %> -->
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<!-- Recenter Map Button --> <!-- Recenter Map Button -->
@@ -384,19 +384,19 @@
type="button" type="button"
onclick="resetMapCenter()" onclick="resetMapCenter()"
class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700/80 text-slate-300 flex items-center justify-center transition" class="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700/80 text-slate-300 flex items-center justify-center transition"
title="Pusatkan Peta ke Jaringan" title="<%= t('admin.gis.tip_center','Pusatkan Peta ke Jaringan') %>"
> >
<i class="bi bi-crosshair text-sm"></i> <i class="bi bi-crosshair text-sm"></i>
</button> </button>
<!-- Switch to Visual Workflow Map --> <!-- Switch to Visual <%= t('admin.gis.workflow','Workflow Map') %> -->
<a <a
href="/admin/map" href="/admin/map"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-sky-600/20 border border-sky-500/30 hover:bg-sky-600/30 text-sky-300 text-xs font-semibold shadow-sm transition" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-sky-600/20 border border-sky-500/30 hover:bg-sky-600/30 text-sky-300 text-xs font-semibold shadow-sm transition"
title="Buka Visual Workflow Mapping (n8n Style)" title="<%= t('admin.gis.tip_workflow','Buka Visual Workflow Mapping (n8n Style)') %>"
> >
<i class="bi bi-diagram-3 text-sm"></i> <i class="bi bi-diagram-3 text-sm"></i>
<span>Workflow Map</span> <span><%= t('admin.gis.workflow','Workflow Map') %></span>
</a> </a>
</div> </div>
@@ -412,7 +412,7 @@
id="btnModeVector" id="btnModeVector"
onclick="setGisVisualMode('vector')" onclick="setGisVisualMode('vector')"
class="gis-mode-pill-btn active" class="gis-mode-pill-btn active"
title="Mode Vektor Gelap Berkontras Tinggi" title="<%= t('admin.gis.tip_dark','Mode Vektor Gelap Berkontras Tinggi') %>"
> >
Vector Mode Vector Mode
</button> </button>
@@ -421,7 +421,7 @@
id="btnModeClean" id="btnModeClean"
onclick="setGisVisualMode('clean')" onclick="setGisVisualMode('clean')"
class="gis-mode-pill-btn" class="gis-mode-pill-btn"
title="Mode Bersih Jalan & Bangunan Minimalis" title="<%= t('admin.gis.tip_clean','Mode Bersih Jalan & Bangunan Minimalis') %>"
> >
Clean Mode Clean Mode
</button> </button>
@@ -430,7 +430,7 @@
id="btnModeFull" id="btnModeFull"
onclick="setGisVisualMode('full')" onclick="setGisVisualMode('full')"
class="gis-mode-pill-btn" class="gis-mode-pill-btn"
title="Mode Satelit Citra Udara Resolusi Tinggi" title="<%= t('admin.gis.tip_sat','Mode Satelit Citra Udara Resolusi Tinggi') %>"
> >
Full Satelit Full Satelit
</button> </button>
@@ -441,15 +441,15 @@
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="font-bold text-sky-300 flex items-center gap-1.5"> <div class="font-bold text-sky-300 flex items-center gap-1.5">
<i class="bi bi-rulers text-sm"></i> <i class="bi bi-rulers text-sm"></i>
<span>Pengukur Panjang Kabel</span> <span><%= t('admin.gis.ruler_title','Pengukur Panjang Kabel') %></span>
</div> </div>
<button type="button" onclick="clearMeasurement()" class="text-slate-400 hover:text-white text-xs"> <button type="button" onclick="clearMeasurement()" class="text-slate-400 hover:text-white text-xs">
Reset Reset
</button> </button>
</div> </div>
<div class="text-[11px] text-slate-300">Klik titik di peta untuk mengukur rute kabel optik.</div> <div class="text-[11px] text-slate-300"><%= t('admin.gis.ruler_desc','Klik titik di peta untuk mengukur rute kabel optik.') %></div>
<div class="pt-1 flex items-baseline gap-2"> <div class="pt-1 flex items-baseline gap-2">
<span class="text-slate-400">Total Panjang:</span> <span class="text-slate-400"><%= t('admin.gis.total_length','Total Panjang:') %></span>
<span id="rulerDistanceText" class="font-mono font-bold text-sm text-emerald-400">0 m</span> <span id="rulerDistanceText" class="font-mono font-bold text-sm text-emerald-400">0 m</span>
</div> </div>
</div> </div>
@@ -458,7 +458,7 @@
<div class="absolute bottom-4 right-4 z-[1000] hidden md:flex items-center gap-3 bg-slate-900/90 backdrop-blur-md px-3.5 py-2 rounded-xl border border-slate-700/80 text-xs shadow-2xl text-slate-300"> <div class="absolute bottom-4 right-4 z-[1000] hidden md:flex items-center gap-3 bg-slate-900/90 backdrop-blur-md px-3.5 py-2 rounded-xl border border-slate-700/80 text-xs shadow-2xl text-slate-300">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="w-2.5 h-2.5 rounded bg-blue-500"></span> <span class="w-2.5 h-2.5 rounded bg-blue-500"></span>
<span>Server / OLT</span> <span><%= t('admin.gis.legend_server','Server / OLT') %></span>
</div> </div>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="w-2.5 h-2.5 rounded bg-amber-500"></span> <span class="w-2.5 h-2.5 rounded bg-amber-500"></span>
@@ -697,7 +697,7 @@
const m = L.marker([lat, lng], { icon: icon }).addTo(groupOdc); const m = L.marker([lat, lng], { icon: icon }).addTo(groupOdc);
m.bindPopup(buildNodePopupHtml({ m.bindPopup(buildNodePopupHtml({
type: isCascade ? 'ODC Cascade Feeder' : 'ODC Feeder Hub', type: isCascade ? 'ODC Cascade Feeder' : '<%= t('admin.gis.el_odc','ODC Feeder Hub') %>',
title: odc.name, title: odc.name,
subtitle: `Kapasitas: ${odc.port_capacity || 4} Tray Out • PON ${odc.pon_port || 1}`, subtitle: `Kapasitas: ${odc.port_capacity || 4} Tray Out • PON ${odc.pon_port || 1}`,
status: 'online', status: 'online',
@@ -768,7 +768,7 @@
const m = L.marker([lat, lng], { icon: icon }).addTo(groupHtb); const m = L.marker([lat, lng], { icon: icon }).addTo(groupHtb);
m.bindPopup(buildNodePopupHtml({ m.bindPopup(buildNodePopupHtml({
type: 'HTB Media Converter (P2P 1:1)', type: '<%= t('admin.gis.el_htb','HTB Media Converter') %> (P2P 1:1)',
title: odp.name, title: odp.name,
subtitle: `P2P Target: ${targetCust?.name || 'Belum Ada'}`, subtitle: `P2P Target: ${targetCust?.name || 'Belum Ada'}`,
status: odpStatus, status: odpStatus,
@@ -820,7 +820,7 @@
const m = L.marker([lat, lng], { icon: icon }).addTo(groupOdp); const m = L.marker([lat, lng], { icon: icon }).addTo(groupOdp);
m.bindPopup(buildNodePopupHtml({ m.bindPopup(buildNodePopupHtml({
type: isCascade ? 'ODP Cascade' : 'ODP Distribusi', type: isCascade ? 'ODP Cascade' : '<%= t('admin.gis.el_odp','ODP Distribusi') %>',
title: odp.name, title: odp.name,
subtitle: `Port: ${totalCust}/${capacity} (${activeCust} Aktif)`, subtitle: `Port: ${totalCust}/${capacity} (${activeCust} Aktif)`,
status: odpStatus, status: odpStatus,
@@ -972,7 +972,7 @@
<div class="text-[11px] text-slate-300">${d.subtitle}</div> <div class="text-[11px] text-slate-300">${d.subtitle}</div>
<div class="bg-slate-950/70 p-2 rounded-lg border border-slate-800 text-[10px] font-mono text-slate-400 flex items-center justify-between"> <div class="bg-slate-950/70 p-2 rounded-lg border border-slate-800 text-[10px] font-mono text-slate-400 flex items-center justify-between">
<span>Koordinat:</span> <span><%= t('admin.gis.coord','Koordinat:') %></span>
<span class="text-white">${d.lat ? d.lat.toFixed(6) : '-'}, ${d.lng ? d.lng.toFixed(6) : '-'}</span> <span class="text-white">${d.lat ? d.lat.toFixed(6) : '-'}, ${d.lng ? d.lng.toFixed(6) : '-'}</span>
</div> </div>
+34 -34
View File
@@ -653,7 +653,7 @@
<span><%= title %></span> <span><%= title %></span>
</div> </div>
<div class="tb-right"> <div class="tb-right">
<button class="btn btn-g btn-sm" id="mapsToggleBtn" onclick="toggleBillingMaps()"><i class="bi bi-map"></i> <span id="mapsToggleTxt">SEE MAPS</span></button> <button class="btn btn-g btn-sm" id="mapsToggleBtn" onclick="toggleBillingMaps()"><i class="bi bi-map"></i> <span id="mapsToggleTxt"><%= t('admin.map.see_maps','LIHAT PETA') %></span></button>
<button class="btn btn-p btn-sm" onclick="if(typeof openModal==='function'){openModal('addOdpModal')}else{toggleBillingMaps()}"><i class="bi bi-plus-circle"></i> <%= t('admin.map.add_odp', 'Tambah ODP') %></button> <button class="btn btn-p btn-sm" onclick="if(typeof openModal==='function'){openModal('addOdpModal')}else{toggleBillingMaps()}"><i class="bi bi-plus-circle"></i> <%= t('admin.map.add_odp', 'Tambah ODP') %></button>
</div> </div>
</div> </div>
@@ -676,30 +676,30 @@
<div class="msc-icon msc-s"><i class="bi bi-router-fill"></i></div> <div class="msc-icon msc-s"><i class="bi bi-router-fill"></i></div>
<div class="msc-info"> <div class="msc-info">
<div class="val"><%= customers.filter(c => c.status === 'active' && c.lat && c.lng).length %></div> <div class="val"><%= customers.filter(c => c.status === 'active' && c.lat && c.lng).length %></div>
<div class="lbl">ONU Aktif (Peta)</div> <div class="lbl"><%= t('admin.map.onu_active','ONU Aktif (Peta)') %></div>
</div> </div>
</div> </div>
<div class="msc"> <div class="msc">
<div class="msc-icon msc-d"><i class="bi bi-exclamation-triangle-fill"></i></div> <div class="msc-icon msc-d"><i class="bi bi-exclamation-triangle-fill"></i></div>
<div class="msc-info"> <div class="msc-info">
<div class="val"><%= customers.filter(c => c.status !== 'active' && c.lat && c.lng).length %></div> <div class="val"><%= customers.filter(c => c.status !== 'active' && c.lat && c.lng).length %></div>
<div class="lbl">ONU Isolir (Peta)</div> <div class="lbl"><%= t('admin.map.onu_isolir','ONU Isolir (Peta)') %></div>
</div> </div>
</div> </div>
<div class="msc"> <div class="msc">
<div class="msc-icon msc-p"><i class="bi bi-geo-alt-fill"></i></div> <div class="msc-icon msc-p"><i class="bi bi-geo-alt-fill"></i></div>
<div class="msc-info"> <div class="msc-info">
<div class="val"><%= customers.filter(c => !c.lat || !c.lng).length %></div> <div class="val"><%= customers.filter(c => !c.lat || !c.lng).length %></div>
<div class="lbl">Tanpa GPS</div> <div class="lbl"><%= t('admin.map.no_gps','Tanpa GPS') %></div>
</div> </div>
</div> </div>
</div> </div>
<div id="mapsPlaceholder" class="maps-placeholder"> <div id="mapsPlaceholder" class="maps-placeholder">
<i class="bi bi-map"></i> <i class="bi bi-map"></i>
<div class="maps-ph-title">Peta belum dimuat</div> <div class="maps-ph-title"><%= t('admin.map.not_loaded','Peta belum dimuat') %></div>
<div class="maps-ph-sub">Klik <b>SEE MAPS</b> untuk memuat peta &amp; daftar pelanggan/ODP (hemat resource).</div> <div class="maps-ph-sub"><%- t('admin.map.not_loaded_sub','Klik <b>SEE MAPS</b> untuk memuat peta &amp; daftar pelanggan/ODP (hemat resource).') %></div>
<button type="button" class="btn btn-p" onclick="toggleBillingMaps()"><i class="bi bi-eye"></i> SEE MAPS</button> <button type="button" class="btn btn-p" onclick="toggleBillingMaps()"><i class="bi bi-eye"></i> <%= t('admin.map.see_maps','LIHAT PETA') %></button>
</div> </div>
<div id="mapsContent" style="display:none"> <div id="mapsContent" style="display:none">
@@ -711,15 +711,15 @@
id="mapGlobalSearch" id="mapGlobalSearch"
class="map-search-input" class="map-search-input"
type="text" type="text"
placeholder="Cari nama pelanggan, PPPoE, No HP, SN ONU, alamat, ODP..." placeholder="<%= t('admin.map.ph_search','Cari nama pelanggan, PPPoE, No HP, SN ONU, alamat, ODP...') %>"
autocomplete="off" autocomplete="off"
> >
<button id="mapSearchClearBtn" class="map-search-clear" onclick="clearMapSearch()" title="Hapus pencarian">&times;</button> <button id="mapSearchClearBtn" class="map-search-clear" onclick="clearMapSearch()" title="<%= t('admin.map.clear_search','Hapus pencarian') %>">&times;</button>
<!-- Auto-Suggest Dropdown Results --> <!-- Auto-Suggest Dropdown Results -->
<div id="mapSearchDropdown" class="map-search-dropdown"> <div id="mapSearchDropdown" class="map-search-dropdown">
<div class="ms-header"> <div class="ms-header">
<span id="msHeaderCount">Hasil Pencarian</span> <span id="msHeaderCount"><%= t('admin.map.search_result','Hasil Pencarian') %></span>
<span><kbd style="background:#e2e8f0; padding:1px 4px; border-radius:4px; font-size:10px;">ESC</kbd> tutup</span> <span><kbd style="background:#e2e8f0; padding:1px 4px; border-radius:4px; font-size:10px;">ESC</kbd> tutup</span>
</div> </div>
<div id="msResultsList"></div> <div id="msResultsList"></div>
@@ -728,26 +728,26 @@
<!-- Quick Filter Pills --> <!-- Quick Filter Pills -->
<div class="map-filter-group"> <div class="map-filter-group">
<button type="button" class="map-pill-btn active" id="pillAll" onclick="applyStatusFilter('all')" title="Tampilkan semua marker"> <button type="button" class="map-pill-btn active" id="pillAll" onclick="applyStatusFilter('all')" title="<%= t('admin.map.tip_all','Tampilkan semua marker') %>">
<i class="bi bi-grid-fill"></i> <%= t('collector.filter_all','Semua') %> <i class="bi bi-grid-fill"></i> <%= t('collector.filter_all','Semua') %>
</button> </button>
<button type="button" class="map-pill-btn" id="pillActive" onclick="applyStatusFilter('active')" title="Hanya ONU aktif"> <button type="button" class="map-pill-btn" id="pillActive" onclick="applyStatusFilter('active')" title="<%= t('admin.map.tip_active','Hanya ONU aktif') %>">
<i class="bi bi-check-circle-fill text-success"></i> Aktif (<%= customers.filter(c => c.status === 'active' && c.lat && c.lng).length %>) <i class="bi bi-check-circle-fill text-success"></i> Aktif (<%= customers.filter(c => c.status === 'active' && c.lat && c.lng).length %>)
</button> </button>
<button type="button" class="map-pill-btn" id="pillSuspended" onclick="applyStatusFilter('suspended')" title="Hanya ONU isolir"> <button type="button" class="map-pill-btn" id="pillSuspended" onclick="applyStatusFilter('suspended')" title="<%= t('admin.map.tip_isolir','Hanya ONU isolir') %>">
<i class="bi bi-x-circle-fill text-danger"></i> Isolir (<%= customers.filter(c => c.status !== 'active' && c.lat && c.lng).length %>) <i class="bi bi-x-circle-fill text-danger"></i> Isolir (<%= customers.filter(c => c.status !== 'active' && c.lat && c.lng).length %>)
</button> </button>
<button type="button" class="map-pill-btn active" id="pillLabels" onclick="toggleMarkerLabels()" title="Sembunyikan/Tampilkan label nama di marker"> <button type="button" class="map-pill-btn active" id="pillLabels" onclick="toggleMarkerLabels()" title="<%= t('admin.map.tip_labels','Sembunyikan/Tampilkan label nama di marker') %>">
<i class="bi bi-tag-fill text-primary"></i> <span id="labelToggleText">Label: ON</span> <i class="bi bi-tag-fill text-primary"></i> <span id="labelToggleText"><%= t('admin.map.label_on','Label: ON') %></span>
</button> </button>
<button type="button" class="map-pill-btn" onclick="fitAllMarkers()" title="Pusatkan peta ke semua titik"> <button type="button" class="map-pill-btn" onclick="fitAllMarkers()" title="<%= t('admin.map.tip_fit','Pusatkan peta ke semua titik') %>">
<i class="bi bi-arrows-fullscreen"></i> Fit Peta <i class="bi bi-arrows-fullscreen"></i> Fit Peta
</button> </button>
<select id="baseMapSelect" class="map-pill-btn" style="padding:6px 8px" onchange="switchBaseMap(this.value)" title="Pilih peta dasar"> <select id="baseMapSelect" class="map-pill-btn" style="padding:6px 8px" onchange="switchBaseMap(this.value)" title="Pilih peta dasar">
<option value="hybrid" selected>Satelit (Hybrid)</option> <option value="hybrid" selected><%= t('admin.map.opt_hybrid','Satelit (Hybrid)') %></option>
<option value="sat">Satelit (Bersih)</option> <option value="sat"><%= t('admin.map.opt_sat','Satelit (Bersih)') %></option>
<option value="osm">OpenStreetMap</option> <option value="osm">OpenStreetMap</option>
<option value="dark">Gelap (Dark)</option> <option value="dark"><%= t('admin.map.opt_dark','Gelap (Dark)') %></option>
</select> </select>
</div> </div>
</div> </div>
@@ -760,12 +760,12 @@
<div class="data-tabs-nav"> <div class="data-tabs-nav">
<button type="button" class="tab-btn active" onclick="switchDataTab('onu-tab', this)"> <button type="button" class="tab-btn active" onclick="switchDataTab('onu-tab', this)">
<i class="bi bi-router-fill"></i> <i class="bi bi-router-fill"></i>
<span>Daftar ONU Pelanggan</span> <span><%= t('admin.map.list_onu','Daftar ONU Pelanggan') %></span>
<span class="tab-badge"><%= customers.length %></span> <span class="tab-badge"><%= customers.length %></span>
</button> </button>
<button type="button" class="tab-btn" onclick="switchDataTab('odp-tab', this)"> <button type="button" class="tab-btn" onclick="switchDataTab('odp-tab', this)">
<i class="bi bi-box-seam-fill"></i> <i class="bi bi-box-seam-fill"></i>
<span>Daftar ODP (Box)</span> <span><%= t('admin.map.list_odp','Daftar ODP (Box)') %></span>
<span class="tab-badge"><%= odps.length %></span> <span class="tab-badge"><%= odps.length %></span>
</button> </button>
</div> </div>
@@ -779,16 +779,16 @@
class="table-search-input" class="table-search-input"
style="width: 100%;" style="width: 100%;"
type="text" type="text"
placeholder="Filter nama, PPPoE, HP, SN/Tag..." placeholder="<%= t('admin.map.ph_filter','Filter nama, PPPoE, HP, SN/Tag...') %>"
> >
</div> </div>
<div style="display: flex; gap: 8px; align-items: center;"> <div style="display: flex; gap: 8px; align-items: center;">
<select id="customerGpsFilter" class="table-search-input" style="min-width: 160px;" onchange="filterCustomerTable()"> <select id="customerGpsFilter" class="table-search-input" style="min-width: 160px;" onchange="filterCustomerTable()">
<option value="all">Semua Status GPS</option> <option value="all"><%= t('admin.map.all_gps','Semua Status GPS') %></option>
<option value="has_gps">Hanya Memiliki GPS (<%= customers.filter(c => c.lat && c.lng).length %>)</option> <option value="has_gps">Hanya Memiliki GPS (<%= customers.filter(c => c.lat && c.lng).length %>)</option>
<option value="no_gps">Tanpa Koordinat GPS (<%= customers.filter(c => !c.lat || !c.lng).length %>)</option> <option value="no_gps">Tanpa <%= t('admin.map.th_gps','Koordinat GPS') %> (<%= customers.filter(c => !c.lat || !c.lng).length %>)</option>
<option value="active">Hanya Aktif</option> <option value="active"><%= t('admin.map.only_active','Hanya Aktif') %></option>
<option value="suspended">Hanya Isolir</option> <option value="suspended"><%= t('admin.map.only_isolir','Hanya Isolir') %></option>
</select> </select>
</div> </div>
</div> </div>
@@ -798,10 +798,10 @@
<thead> <thead>
<tr> <tr>
<th><%= t('common.customer','Pelanggan') %></th> <th><%= t('common.customer','Pelanggan') %></th>
<th>PPPoE / Tag ONU</th> <th><%= t('admin.map.th_pppoe','PPPoE / Tag ONU') %></th>
<th><%= t('common.status','Status') %></th> <th><%= t('common.status','Status') %></th>
<th>Paket & ODP</th> <th><%= t('admin.map.th_pkg_odp','Paket & ODP') %></th>
<th>Koordinat GPS</th> <th><%= t('admin.map.th_gps','Koordinat GPS') %></th>
<th style="text-align: right; width: 140px;"><%= t('common.actions','Aksi') %></th> <th style="text-align: right; width: 140px;"><%= t('common.actions','Aksi') %></th>
</tr> </tr>
</thead> </thead>
@@ -813,7 +813,7 @@
<!-- TAB 2: DAFTAR ODP --> <!-- TAB 2: DAFTAR ODP -->
<div id="odp-tab" class="tab-content"> <div id="odp-tab" class="tab-content">
<div class="panel-filter-bar"> <div class="panel-filter-bar">
<input id="odpSearch" class="table-search-input" type="text" placeholder="Cari ODP (nama / deskripsi)..."> <input id="odpSearch" class="table-search-input" type="text" placeholder="<%= t('admin.map.ph_odp','Cari ODP (nama / deskripsi)...') %>">
<button type="button" class="btn btn-p btn-sm" onclick="openModal('addOdpModal')"><i class="bi bi-plus-circle"></i> Tambah ODP</button> <button type="button" class="btn btn-p btn-sm" onclick="openModal('addOdpModal')"><i class="bi bi-plus-circle"></i> Tambah ODP</button>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
@@ -932,7 +932,7 @@
<script id="customers-data" type="application/json"><%- JSON.stringify(customers).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') %></script> <script id="customers-data" type="application/json"><%- JSON.stringify(customers).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') %></script>
<script id="odps-data" type="application/json"><%- JSON.stringify(odps).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') %></script> <script id="odps-data" type="application/json"><%- JSON.stringify(odps).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--') %></script>
<!-- Bootstrap lazy-load peta (SEE MAPS / CLOSE MAPS) --> <!-- Bootstrap lazy-load peta (<%= t('admin.map.see_maps','LIHAT PETA') %> / CLOSE MAPS) -->
<script> <script>
window.__billingMapLoaded = false; window.__billingMapLoaded = false;
function loadMapScript() { function loadMapScript() {
@@ -953,7 +953,7 @@
if (showing) { if (showing) {
content.style.display = 'none'; content.style.display = 'none';
if (ph) ph.style.display = ''; if (ph) ph.style.display = '';
if (txt) txt.textContent = 'SEE MAPS'; if (txt) txt.textContent = '<%= t('admin.map.see_maps','LIHAT PETA') %>';
} else { } else {
if (ph) ph.style.display = 'none'; if (ph) ph.style.display = 'none';
content.style.display = ''; content.style.display = '';
@@ -1372,7 +1372,7 @@
<div class="ms-item-info"> <div class="ms-item-info">
<div class="ms-item-name"> <div class="ms-item-name">
<span>${highlightMatch(c.name, query)}</span> <span>${highlightMatch(c.name, query)}</span>
${!hasGps ? '<span style="color:#ef4444; font-size:10px; font-weight:700;"><i class="bi bi-exclamation-triangle"></i> Tanpa GPS</span>' : ''} ${!hasGps ? '<span style="color:#ef4444; font-size:10px; font-weight:700;"><i class="bi bi-exclamation-triangle"></i> <%= t('admin.map.no_gps','Tanpa GPS') %></span>' : ''}
</div> </div>
<div class="ms-item-meta"> <div class="ms-item-meta">
${c.pppoe_username ? `<span>PPPoE: ${highlightMatch(c.pppoe_username, query)}</span> • ` : ''} ${c.pppoe_username ? `<span>PPPoE: ${highlightMatch(c.pppoe_username, query)}</span> • ` : ''}
@@ -1589,7 +1589,7 @@
if (showLabels) { if (showLabels) {
mapEl.classList.remove('hide-marker-labels'); mapEl.classList.remove('hide-marker-labels');
btn.classList.add('active'); btn.classList.add('active');
txt.textContent = 'Label: ON'; txt.textContent = '<%= t('admin.map.label_on','Label: ON') %>';
} else { } else {
mapEl.classList.add('hide-marker-labels'); mapEl.classList.add('hide-marker-labels');
btn.classList.remove('active'); btn.classList.remove('active');
+30 -30
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>Paket Voucher Hotspot | <%= company %></title> <title><%= t('admin.vp.title','Paket Voucher Hotspot') %> | <%= 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>
@@ -36,22 +36,22 @@
<div class="mtabs"> <div class="mtabs">
<a href="/admin/vouchers" class="mtab"><i class="bi bi-ticket-perforated"></i> Pembuatan Voucher (Batch)</a> <a href="/admin/vouchers" class="mtab"><i class="bi bi-ticket-perforated"></i> Pembuatan Voucher (Batch)</a>
<a href="/admin/vouchers/packages" class="mtab active"><i class="bi bi-ticket-detailed"></i> Konfigurasi Paket (Real-Time)</a> <a href="/admin/vouchers/packages" class="mtab active"><i class="bi bi-ticket-detailed"></i> <%= t('admin.vp.config','Konfigurasi Paket') %> (Real-Time)</a>
</div> </div>
<div class="card"> <div class="card">
<div class="card-hd"> <div class="card-hd">
<h6><i class="bi bi-list-stars"></i> Daftar Paket Hotspot Real-Time</h6> <h6><i class="bi bi-list-stars"></i> <%= t('admin.vp.list','Daftar Paket Hotspot Real-Time') %></h6>
<div style="display:flex;align-items:center;gap:10px"> <div style="display:flex;align-items:center;gap:10px">
<% if (typeof routers !== 'undefined' && routers && routers.length > 0) { %> <% if (typeof routers !== 'undefined' && routers && routers.length > 0) { %>
<select class="fs" style="width:160px" onchange="window.location.href='/admin/set-active-router?router_id=' + this.value"> <select class="fs" style="width:160px" onchange="window.location.href='/admin/set-active-router?router_id=' + this.value">
<option value="all" <%= (!selectedRouterId) ? 'selected' : '' %>> Semua Router</option> <option value="all" <%= (!selectedRouterId) ? 'selected' : '' %>> <%= t('admin.vp.all_router','Semua Router') %></option>
<% routers.forEach(r => { %> <% routers.forEach(r => { %>
<option value="<%= r.id %>" <%= (selectedRouterId === r.id) ? 'selected' : '' %>> <%= r.name %></option> <option value="<%= r.id %>" <%= (selectedRouterId === r.id) ? 'selected' : '' %>> <%= r.name %></option>
<% }) %> <% }) %>
</select> </select>
<% } %> <% } %>
<button class="btn btn-p btn-sm" onclick="openAddModal()"><i class="bi bi-plus-lg"></i> Tambah Konfigurasi Paket</button> <button class="btn btn-p btn-sm" onclick="openAddModal()"><i class="bi bi-plus-lg"></i> <%= t('admin.vp.add_config','Tambah Konfigurasi Paket') %></button>
</div> </div>
</div> </div>
<div class="tbl-wrap"> <div class="tbl-wrap">
@@ -59,18 +59,18 @@
<thead> <thead>
<tr> <tr>
<th>Router</th> <th>Router</th>
<th>Nama Profile</th> <th><%= t('admin.vp.th_profile','Nama Profile') %></th>
<th>Harga</th> <th><%= t('admin.vp.th_price','Harga') %></th>
<th>Masa Aktif</th> <th><%= t('admin.vp.th_validity','Masa Aktif') %></th>
<th>Prefix</th> <th><%= t('admin.vp.th_prefix','Prefix') %></th>
<th>Panjang Kode</th> <th><%= t('admin.vp.th_len','Panjang Kode') %></th>
<th>Karakter</th> <th><%= t('admin.vp.th_chars','Karakter') %></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>
</thead> </thead>
<tbody id="packages-list"> <tbody id="packages-list">
<tr><td colspan="9" class="text-center text-muted">Memuat data paket...</td></tr> <tr><td colspan="9" class="text-center text-muted"><%= t('admin.vp.loading','Memuat data paket...') %></td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -82,7 +82,7 @@
<div class="mo" id="packageModal"> <div class="mo" id="packageModal">
<div class="mb" style="max-width: 600px;"> <div class="mb" style="max-width: 600px;">
<div class="mh"> <div class="mh">
<span class="mt" id="modalTitle">Konfigurasi Paket</span> <span class="mt" id="modalTitle"><%= t('admin.vp.config','Konfigurasi Paket') %></span>
<button class="mc" onclick="closeModal('packageModal')">&times;</button> <button class="mc" onclick="closeModal('packageModal')">&times;</button>
</div> </div>
<form id="packageForm" onsubmit="savePackage(event)"> <form id="packageForm" onsubmit="savePackage(event)">
@@ -90,53 +90,53 @@
<div class="mbody"> <div class="mbody">
<div class="fg"> <div class="fg">
<label class="fl">Pilih Router MikroTik <span class="text-danger">*</span></label> <label class="fl"><%= t('admin.vp.pick_router','Pilih Router MikroTik') %> <span class="text-danger">*</span></label>
<select class="fc" id="form_router_id" required onchange="loadMikrotikProfiles()"> <select class="fc" id="form_router_id" required onchange="loadMikrotikProfiles()">
<option value="">-- Pilih Router --</option> <option value=""><%= t('admin.vp.opt_router','-- Pilih Router --') %></option>
<% routers.forEach(r => { %> <% routers.forEach(r => { %>
<option value="<%= r.id %>"><%= r.name %></option> <option value="<%= r.id %>"><%= r.name %></option>
<% }) %> <% }) %>
</select> </select>
<small class="text-muted d-block mt-1">Router harus dipilih untuk paket voucher</small> <small class="text-muted d-block mt-1"><%= t('admin.vp.router_req','Router harus dipilih untuk paket voucher') %></small>
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Hotspot User Profile *</label> <label class="fl">Hotspot User Profile *</label>
<div style="display:flex;gap:8px"> <div style="display:flex;gap:8px">
<select class="fs" id="form_profile_select" style="max-width:220px" onchange="document.getElementById('form_profile_name').value=this.value; onProfileSelected();"> <select class="fs" id="form_profile_select" style="max-width:220px" onchange="document.getElementById('form_profile_name').value=this.value; onProfileSelected();">
<option value="">-- Pilih dari MikroTik --</option> <option value=""><%= t('admin.vp.opt_from_mt','-- Pilih dari MikroTik --') %></option>
</select> </select>
<input class="fc" id="form_profile_name" required placeholder="Ketik nama profile manual..."> <input class="fc" id="form_profile_name" required placeholder="<%= t('admin.vp.ph_profile','Ketik nama profile manual...') %>">
</div> </div>
<div class="hint" id="profileHint">Pilih router & profil untuk membaca durasi bawaan atau ketik manual jika MikroTik offline.</div> <div class="hint" id="profileHint"><%= t('admin.vp.profile_hint','Pilih router & profil untuk membaca durasi bawaan atau ketik manual jika MikroTik offline.') %></div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Harga Jual (Rp) *</label> <label class="fl"><%= t('admin.vp.price','Harga Jual (Rp) *') %></label>
<input class="fc" id="form_price" type="number" min="500" required placeholder="Contoh: 5000"> <input class="fc" id="form_price" type="number" min="500" required placeholder="<%= t('admin.vp.ph_price','Contoh: 5000') %>">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Durasi / Masa Aktif *</label> <label class="fl"><%= t('admin.vp.validity','Durasi / Masa Aktif *') %></label>
<input class="fc" id="form_validity" required placeholder="Contoh: 1d, 12h, 3h"> <input class="fc" id="form_validity" required placeholder="<%= t('admin.vp.ph_validity','Contoh: 1d, 12h, 3h') %>">
<div class="hint">Format Mikhmon (misal: 1d = 1 hari, 12h = 12 jam)</div> <div class="hint"><%= t('admin.vp.validity_hint','Format Mikhmon (misal: 1d = 1 hari, 12h = 12 jam)') %></div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Prefix Kode Voucher</label> <label class="fl"><%= t('admin.vp.prefix','Prefix Kode Voucher') %></label>
<input class="fc" id="form_prefix" placeholder="Opsional (misal: ALI-)"> <input class="fc" id="form_prefix" placeholder="<%= t('admin.vp.ph_prefix','Opsional (misal: ALI-)') %>">
</div> </div>
<div class="fg"> <div class="fg">
<label class="fl">Panjang Kode (Jumlah Digit) *</label> <label class="fl"><%= t('admin.vp.code_len','Panjang Kode (Jumlah Digit) *') %></label>
<input class="fc" id="form_code_length" type="number" min="4" max="16" value="6" required> <input class="fc" id="form_code_length" type="number" min="4" max="16" value="6" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="fg"> <div class="fg">
<label class="fl">Tipe Karakter Kode *</label> <label class="fl"><%= t('admin.vp.char_type','Tipe Karakter Kode *') %></label>
<select class="fc" id="form_charset" required> <select class="fc" id="form_charset" required>
<option value="numbers">Hanya Angka (0-9)</option> <option value="numbers">Hanya Angka (0-9)</option>
<option value="letters">Hanya Huruf Kecil (a-z)</option> <option value="letters">Hanya Huruf Kecil (a-z)</option>
@@ -231,7 +231,7 @@
} }
cachedMikrotikProfiles = Array.isArray(data) ? data : []; cachedMikrotikProfiles = Array.isArray(data) ? data : [];
profileSelect.innerHTML = '<option value="">-- Pilih dari MikroTik --</option>'; profileSelect.innerHTML = '<option value=""><%= t('admin.vp.opt_from_mt','-- Pilih dari MikroTik --') %></option>';
cachedMikrotikProfiles.forEach(p => { cachedMikrotikProfiles.forEach(p => {
profileSelect.insertAdjacentHTML('beforeend', `<option value="${p.name}">${p.name}</option>`); profileSelect.insertAdjacentHTML('beforeend', `<option value="${p.name}">${p.name}</option>`);
}); });
@@ -290,7 +290,7 @@
const p = currentPackages[idx]; const p = currentPackages[idx];
if (!p) return; if (!p) return;
document.getElementById('modalTitle').innerText = 'Edit Konfigurasi Paket'; document.getElementById('modalTitle').innerText = 'Edit <%= t('admin.vp.config','Konfigurasi Paket') %>';
document.getElementById('packageId').value = p.id; document.getElementById('packageId').value = p.id;
document.getElementById('form_router_id').value = p.router_id || ''; document.getElementById('form_router_id').value = p.router_id || '';