feat(qr-custom): filter tanggal + pilih baris & hapus terpilih
All checks were successful
CI / test (push) Successful in 1m37s
All checks were successful
CI / test (push) Successful in 1m37s
- filter from/to pada list - checkbox pilih semua, batch delete dengan password admin - reset histori tetap - versi 0.6.6
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
|
||||
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.6.6] — 2026-11-06 — "qr-custom-bulk"
|
||||
|
||||
### Ditambahkan
|
||||
- **Filter tanggal** (dari/sampai) pada histori QR-Custom Payment.
|
||||
- **Pilih semua / per baris** + tombol **Hapus Terpilih** (batch remove) dengan password admin.
|
||||
- **Reset Histori** tetap tersedia untuk menghapus semua data.
|
||||
|
||||
## [0.6.5] — 2026-11-06 — "qr-custom-delete"
|
||||
|
||||
### Ditambahkan
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
|
||||
<img src="https://img.shields.io/badge/versi-0.6.5_qr--delete-ADFF2F" alt="Versi 0.6.5 (qr-custom-delete)">
|
||||
<img src="https://img.shields.io/badge/versi-0.6.6_qr--bulk-ADFF2F" alt="Versi 0.6.6 (qr-custom-bulk)">
|
||||
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
|
||||
<img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker">
|
||||
<a href="https://demo-openisp.oxpoe.xyz"><img src="https://img.shields.io/badge/demo-live-ADFF2F?logo=googlechrome&logoColor=black" alt="Demo"></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-isp",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
|
||||
"main": "app-customer.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -4019,6 +4019,8 @@ router.get('/custom-qr', requireAdminSession, requireSidebarMenuAccess('custom_q
|
||||
let detail = null;
|
||||
const detailId = Number(req.query.id) || 0;
|
||||
if (detailId > 0) { try { detail = svc.getById(detailId); } catch (e) {} }
|
||||
const filterFrom = String(req.query.from || '').trim();
|
||||
const filterTo = String(req.query.to || '').trim();
|
||||
const settings = getSettings();
|
||||
const baseUrl = String(settings.public_base_url || ((req.headers['x-forwarded-proto'] || req.protocol) + '://' + req.get('host'))).replace(/\/+$/, '');
|
||||
res.render('admin/custom_qr', {
|
||||
@@ -4026,7 +4028,8 @@ router.get('/custom-qr', requireAdminSession, requireSidebarMenuAccess('custom_q
|
||||
company: company(),
|
||||
activePage: 'custom_qr',
|
||||
msg: flashMsg(req),
|
||||
rows: svc.list(150),
|
||||
rows: svc.list(200, { from: filterFrom, to: filterTo }),
|
||||
filterFrom, filterTo,
|
||||
stats: svc.stats(),
|
||||
detail,
|
||||
baseUrl,
|
||||
@@ -4086,6 +4089,19 @@ router.post('/custom-qr/reset', requireAdminSession, restrictToAdmin, requireSid
|
||||
res.redirect('/admin/custom-qr');
|
||||
});
|
||||
|
||||
router.post('/custom-qr/delete-bulk', requireAdminSession, restrictToAdmin, requireSidebarMenuAccess('custom_qr'), express.urlencoded({ extended: true }), (req, res) => {
|
||||
try {
|
||||
if (!_checkAdminPassword(req)) throw new Error('Password admin salah.');
|
||||
const ids = Array.isArray(req.body.ids) ? req.body.ids : [req.body.ids];
|
||||
const n = require('../services/customQrService').removeMany(ids);
|
||||
req.session._msg = { type: 'success', text: `${n} QR custom dihapus.` };
|
||||
} catch (e) { req.session._msg = { type: 'error', text: e.message }; }
|
||||
const q = [];
|
||||
if (req.body.from) q.push('from=' + encodeURIComponent(req.body.from));
|
||||
if (req.body.to) q.push('to=' + encodeURIComponent(req.body.to));
|
||||
res.redirect('/admin/custom-qr' + (q.length ? ('?' + q.join('&')) : ''));
|
||||
});
|
||||
|
||||
router.post('/custom-qr/:id/regenerate', requireAdminSession, requireSidebarMenuAccess('custom_qr'), express.urlencoded({ extended: true }), (req, res) => {
|
||||
try {
|
||||
const row = require('../services/customQrService').regenerate(req.params.id, resolvePaidByName(req, 'Admin'));
|
||||
|
||||
@@ -75,9 +75,17 @@ function getByToken(token) {
|
||||
return db.prepare('SELECT * FROM custom_qr_payments WHERE share_token=?').get(String(token || ''));
|
||||
}
|
||||
|
||||
function list(limit = 100) {
|
||||
function list(limit = 100, opts = {}) {
|
||||
sweepExpired();
|
||||
return db.prepare('SELECT * FROM custom_qr_payments ORDER BY id DESC LIMIT ?').all(Math.max(1, Math.min(500, Number(limit) || 100)));
|
||||
const where = [];
|
||||
const params = [];
|
||||
if (opts.from) { where.push('date(created_at) >= date(?)'); params.push(String(opts.from)); }
|
||||
if (opts.to) { where.push('date(created_at) <= date(?)'); params.push(String(opts.to)); }
|
||||
const sql = 'SELECT * FROM custom_qr_payments' +
|
||||
(where.length ? (' WHERE ' + where.join(' AND ')) : '') +
|
||||
' ORDER BY id DESC LIMIT ?';
|
||||
params.push(Math.max(1, Math.min(500, Number(limit) || 100)));
|
||||
return db.prepare(sql).all(...params);
|
||||
}
|
||||
|
||||
function stats() {
|
||||
@@ -123,6 +131,16 @@ function remove(id) {
|
||||
return r.changes || 0;
|
||||
}
|
||||
|
||||
function removeMany(ids) {
|
||||
const list = (Array.isArray(ids) ? ids : [ids])
|
||||
.map(function (x) { return Number(x); })
|
||||
.filter(function (n) { return Number.isFinite(n) && n > 0; });
|
||||
if (!list.length) return 0;
|
||||
const marks = list.map(function () { return '?'; }).join(',');
|
||||
const r = db.prepare('DELETE FROM custom_qr_payments WHERE id IN (' + marks + ')').run(...list);
|
||||
return r.changes || 0;
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const r = db.prepare('DELETE FROM custom_qr_payments').run();
|
||||
return r.changes || 0;
|
||||
@@ -207,6 +225,7 @@ module.exports = {
|
||||
acceptWarning,
|
||||
regenerate,
|
||||
remove,
|
||||
removeMany,
|
||||
clearAll,
|
||||
reject,
|
||||
markPaidFromWebhook,
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.5
|
||||
0.6.6
|
||||
@@ -127,15 +127,34 @@
|
||||
<button class="btn btn-g btn-sm"><i class="bi bi-trash3"></i> Reset Histori</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-bd" style="border-bottom:1px solid var(--border)">
|
||||
<form method="GET" action="/admin/custom-qr" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
|
||||
<div class="fg"><label class="fl">Dari tanggal</label><input class="fc" type="date" name="from" value="<%= filterFrom || '' %>"></div>
|
||||
<div class="fg"><label class="fl">Sampai tanggal</label><input class="fc" type="date" name="to" value="<%= filterTo || '' %>"></div>
|
||||
<button class="btn btn-p btn-sm" type="submit"><i class="bi bi-funnel"></i> Filter</button>
|
||||
<a class="btn btn-g btn-sm" href="/admin/custom-qr"><i class="bi bi-x-circle"></i> Reset Filter</a>
|
||||
</form>
|
||||
</div>
|
||||
<form id="cqBulkForm" method="POST" action="/admin/custom-qr/delete-bulk"></form>
|
||||
<div class="card-bd" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;border-bottom:1px solid var(--border)">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer">
|
||||
<input type="checkbox" id="cqAll" onclick="cqToggleAll(this)"> Pilih semua
|
||||
</label>
|
||||
<input form="cqBulkForm" class="fc" type="password" name="password" required placeholder="Password admin" style="max-width:180px">
|
||||
<input form="cqBulkForm" type="hidden" name="from" value="<%= filterFrom || '' %>">
|
||||
<input form="cqBulkForm" type="hidden" name="to" value="<%= filterTo || '' %>">
|
||||
<button form="cqBulkForm" class="btn btn-g btn-sm" onclick="return confirm('Hapus QR yang dipilih?')"><i class="bi bi-trash"></i> Hapus Terpilih</button>
|
||||
</div>
|
||||
<div class="card-bd" style="overflow:auto">
|
||||
<table class="table" style="width:100%;font-size:12.5px">
|
||||
<thead><tr><th>Judul</th><th>Kategori</th><th>Nominal</th><th>Status</th><th>Dibuat</th><th>Kedaluwarsa</th><th>Aksi</th></tr></thead>
|
||||
<thead><tr><th style="width:30px"></th><th>Judul</th><th>Kategori</th><th>Nominal</th><th>Status</th><th>Dibuat</th><th>Kedaluwarsa</th><th>Aksi</th></tr></thead>
|
||||
<tbody>
|
||||
<% if (!rows || rows.length === 0) { %>
|
||||
<tr><td colspan="7" class="muted" style="text-align:center;padding:20px">Belum ada QR custom.</td></tr>
|
||||
<tr><td colspan="8" class="muted" style="text-align:center;padding:20px">Belum ada QR custom.</td></tr>
|
||||
<% } %>
|
||||
<% rows.forEach(function(r) { %>
|
||||
<tr>
|
||||
<td><input type="checkbox" class="cq-check" form="cqBulkForm" name="ids" value="<%= r.id %>"></td>
|
||||
<td><b><%= r.title %></b><% if (r.note) { %><div class="muted" style="font-size:11px"><%= r.note %></div><% } %></td>
|
||||
<td><%= r.category || '-' %></td>
|
||||
<td>Rp <%= Number(r.amount_unique||0).toLocaleString('id-ID') %></td>
|
||||
@@ -166,6 +185,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function cqToggleAll(el) {
|
||||
var on = !!(el && el.checked);
|
||||
document.querySelectorAll('.cq-check').forEach(function (c) { c.checked = on; });
|
||||
}
|
||||
function copyCqLink() {
|
||||
var el = document.getElementById('cqLink');
|
||||
if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user