diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a24f9e..37fd8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index e474680..1afeba6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
-
+
diff --git a/package.json b/package.json
index 4fe0624..e27f087 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/routes/adminPortal.js b/routes/adminPortal.js
index 9106f62..64976d1 100644
--- a/routes/adminPortal.js
+++ b/routes/adminPortal.js
@@ -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'));
diff --git a/services/customQrService.js b/services/customQrService.js
index bd4a9ba..050fae8 100644
--- a/services/customQrService.js
+++ b/services/customQrService.js
@@ -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,
diff --git a/version.txt b/version.txt
index e0ea44c..bf21f52 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.6.5
\ No newline at end of file
+0.6.6
\ No newline at end of file
diff --git a/views/admin/custom_qr.ejs b/views/admin/custom_qr.ejs
index f87a07c..8c37f41 100644
--- a/views/admin/custom_qr.ejs
+++ b/views/admin/custom_qr.ejs
@@ -127,15 +127,34 @@
+
| Judul | Kategori | Nominal | Status | Dibuat | Kedaluwarsa | Aksi | |
|---|---|---|---|---|---|---|---|
| Judul | Kategori | Nominal | Status | Dibuat | Kedaluwarsa | Aksi | |
| Belum ada QR custom. | |||||||
| Belum ada QR custom. | |||||||
| <%= r.title %><% if (r.note) { %> <%= r.note %> <% } %> |
<%= r.category || '-' %> | Rp <%= Number(r.amount_unique||0).toLocaleString('id-ID') %> | @@ -166,6 +185,10 @@|||||