feat(qr-custom): hapus satu & reset histori dengan password admin
All checks were successful
CI / test (push) Successful in 1m47s

- endpoint delete/reset, verifikasi admin_password
- tombol hapus di detail + reset di header histori
- versi 0.6.5
This commit is contained in:
oxpoe
2026-11-06 05:08:30 +07:00
parent 8fcb50e16a
commit a2888810ad
7 changed files with 56 additions and 4 deletions

View File

@@ -2,6 +2,12 @@
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
## [0.6.5] — 2026-11-06 — "qr-custom-delete"
### Ditambahkan
- **Hapus satu QR custom** dari detail (wajib password admin).
- **Reset seluruh histori QR custom** dari header tabel (wajib password admin, konfirmasi).
## [0.6.4] — 2026-11-06 — "qr-custom-ux"
### Diperbaiki

View File

@@ -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.4_qr--ux-ADFF2F" alt="Versi 0.6.4 (qr-custom-ux)">
<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/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>

View File

@@ -1,6 +1,6 @@
{
"name": "open-isp",
"version": "0.6.4",
"version": "0.6.5",
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js",
"scripts": {

View File

@@ -4062,6 +4062,30 @@ router.post('/custom-qr/:id/manual-paid', requireAdminSession, requireSidebarMen
res.redirect('/admin/custom-qr?id=' + Number(req.params.id));
});
function _checkAdminPassword(req) {
const pass = String((req.body && req.body.password) || '');
const expected = String(getSetting('admin_password', 'demo-isp123') || 'demo-isp123');
return !!expected && pass === expected;
}
router.post('/custom-qr/:id/delete', requireAdminSession, restrictToAdmin, requireSidebarMenuAccess('custom_qr'), express.urlencoded({ extended: true }), (req, res) => {
try {
if (!_checkAdminPassword(req)) throw new Error('Password admin salah.');
const n = require('../services/customQrService').remove(req.params.id);
req.session._msg = n ? { type: 'success', text: 'QR custom dihapus.' } : { type: 'error', text: 'Data tidak ditemukan.' };
} catch (e) { req.session._msg = { type: 'error', text: e.message }; }
res.redirect('/admin/custom-qr');
});
router.post('/custom-qr/reset', requireAdminSession, restrictToAdmin, requireSidebarMenuAccess('custom_qr'), express.urlencoded({ extended: true }), (req, res) => {
try {
if (!_checkAdminPassword(req)) throw new Error('Password admin salah.');
const n = require('../services/customQrService').clearAll();
req.session._msg = { type: 'success', text: `Histori QR custom direset (${n} data dihapus).` };
} catch (e) { req.session._msg = { type: 'error', text: e.message }; }
res.redirect('/admin/custom-qr');
});
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'));

View File

@@ -118,6 +118,16 @@ function acceptWarning(id, actor, note) {
return getById(id);
}
function remove(id) {
const r = db.prepare('DELETE FROM custom_qr_payments WHERE id=?').run(Number(id) || 0);
return r.changes || 0;
}
function clearAll() {
const r = db.prepare('DELETE FROM custom_qr_payments').run();
return r.changes || 0;
}
function regenerate(id, actor) {
const old = getById(id);
if (!old) throw new Error('Data tidak ditemukan.');
@@ -196,6 +206,8 @@ module.exports = {
markManualPaid,
acceptWarning,
regenerate,
remove,
clearAll,
reject,
markPaidFromWebhook,
markWarningFromWebhook,

View File

@@ -1 +1 @@
0.6.4
0.6.5

View File

@@ -110,13 +110,23 @@
</form>
<% } %>
</div>
<form method="POST" action="/admin/custom-qr/<%= detail.id %>/delete" onsubmit="return confirm('Hapus QR ini dari histori?')" style="display:flex;gap:6px;margin-top:12px;flex-wrap:wrap">
<input class="fc" type="password" name="password" required placeholder="Password admin" style="max-width:200px">
<button class="btn btn-g btn-sm"><i class="bi bi-trash"></i> Hapus QR ini</button>
</form>
</div>
</div>
</div>
<% } %>
<div class="card">
<div class="card-hd"><h6><i class="bi bi-clock-history"></i> Histori QR Custom</h6></div>
<div class="card-hd" style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
<h6 style="margin:0"><i class="bi bi-clock-history"></i> Histori QR Custom</h6>
<form method="POST" action="/admin/custom-qr/reset" onsubmit="return confirm('Reset SEMUA histori QR custom? Tindakan ini tidak bisa dibatalkan.')" style="display:flex;gap:6px;align-items:center">
<input class="fc" type="password" name="password" required placeholder="Password admin" style="max-width:180px">
<button class="btn btn-g btn-sm"><i class="bi bi-trash3"></i> Reset Histori</button>
</form>
</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>