fix(qr-custom): ikon menu + disable sampai QRIS statis siap
All checks were successful
CI / test (push) Successful in 1m43s
All checks were successful
CI / test (push) Successful in 1m43s
- icon bi-qr-code - menu disembunyikan bila QRIS belum aktif/payload kosong - form & POST ditolak, tidak membuat data tanpa QR - versi 0.6.1
This commit is contained in:
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
|
Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [0.6.1] — 2026-11-06 — "qr-custom-gate"
|
||||||
|
|
||||||
|
### Diperbaiki
|
||||||
|
- Ikon menu **QR-Custom Payment** memakai ikon yang tersedia (`bi-qr-code`).
|
||||||
|
- Fitur QR-Custom **dinonaktifkan otomatis** bila QRIS Statis (Semi-Otomatis) belum aktif atau `qris_static_payload` kosong:
|
||||||
|
- menu disembunyikan,
|
||||||
|
- form pembuatan dinonaktifkan,
|
||||||
|
- request pembuatan ditolak (tidak ada data/list dibuat tanpa QR).
|
||||||
|
|
||||||
## [0.6.0] — 2026-11-06 — "qr-custom"
|
## [0.6.0] — 2026-11-06 — "qr-custom"
|
||||||
|
|
||||||
### Ditambahkan
|
### Ditambahkan
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
|
<img src="https://img.shields.io/badge/lisensi-ISC-ADFF2F" alt="Lisensi: ISC">
|
||||||
<img src="https://img.shields.io/badge/versi-0.6.0_qr--custom-ADFF2F" alt="Versi 0.6.0 (qr-custom)">
|
<img src="https://img.shields.io/badge/versi-0.6.1_qr--gate-ADFF2F" alt="Versi 0.6.1 (qr-custom-gate)">
|
||||||
<img src="https://img.shields.io/badge/Node.js-%E2%89%A520-339933" alt="Node.js >= 20">
|
<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">
|
<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>
|
<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",
|
"name": "open-isp",
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
|
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
|
||||||
"main": "app-customer.js",
|
"main": "app-customer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4031,7 +4031,8 @@ router.get('/custom-qr', requireAdminSession, requireSidebarMenuAccess('custom_q
|
|||||||
detail,
|
detail,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
durationMinutes: svc.DURATION_MINUTES,
|
durationMinutes: svc.DURATION_MINUTES,
|
||||||
hasQrisPayload: !!String(settings.qris_static_payload || '').trim()
|
hasQrisPayload: !!String(settings.qris_static_payload || '').trim(),
|
||||||
|
ready: svc.isReady()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,19 @@ const crypto = require('crypto');
|
|||||||
const db = require('../config/database');
|
const db = require('../config/database');
|
||||||
const { isQrisAmountTaken } = require('./invoiceQrisService');
|
const { isQrisAmountTaken } = require('./invoiceQrisService');
|
||||||
const { logger } = require('../config/logger');
|
const { logger } = require('../config/logger');
|
||||||
|
const { getSetting } = require('../config/settingsManager');
|
||||||
|
|
||||||
const DURATION_MINUTES = 20;
|
const DURATION_MINUTES = 20;
|
||||||
|
|
||||||
|
function isReady() {
|
||||||
|
try {
|
||||||
|
const enabled = getSetting('qris_static_enabled', true);
|
||||||
|
const off = enabled === false || enabled === 'false' || enabled === 0 || enabled === '0';
|
||||||
|
const payload = String(getSetting('qris_static_payload', '') || '').trim();
|
||||||
|
return !off && payload.length > 0;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
function nowIso() { return new Date().toISOString(); }
|
function nowIso() { return new Date().toISOString(); }
|
||||||
|
|
||||||
function sweepExpired() {
|
function sweepExpired() {
|
||||||
@@ -27,6 +37,7 @@ function isTaken(amount, excludeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function create({ title, category, amount, note, createdBy }) {
|
function create({ title, category, amount, note, createdBy }) {
|
||||||
|
if (!isReady()) throw new Error('QRIS Statis (Semi-Otomatis) belum aktif/berisi payload. Atur dulu di Pengaturan → QRIS.');
|
||||||
const base = Math.max(1, Math.floor(Number(amount) || 0));
|
const base = Math.max(1, Math.floor(Number(amount) || 0));
|
||||||
if (!title || !String(title).trim()) throw new Error('Judul wajib diisi.');
|
if (!title || !String(title).trim()) throw new Error('Judul wajib diisi.');
|
||||||
if (!base) throw new Error('Nominal wajib diisi.');
|
if (!base) throw new Error('Nominal wajib diisi.');
|
||||||
@@ -161,6 +172,7 @@ function isExpired(row) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DURATION_MINUTES,
|
DURATION_MINUTES,
|
||||||
|
isReady,
|
||||||
create,
|
create,
|
||||||
list,
|
list,
|
||||||
stats,
|
stats,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const MENU_DEFINITIONS = [
|
|||||||
{ key: 'map', section: 'main', href: '/admin/map', icon: 'bi bi-map', labelKey: 'admin.nav.network_map', labelDefault: 'Peta Jaringan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 4, activePages: ['map'] },
|
{ key: 'map', section: 'main', href: '/admin/map', icon: 'bi bi-map', labelKey: 'admin.nav.network_map', labelDefault: 'Peta Jaringan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 4, activePages: ['map'] },
|
||||||
{ key: 'whatsapp', section: 'main', href: '/admin/whatsapp', icon: 'bi bi-whatsapp', labelKey: 'admin.nav.whatsapp', labelDefault: 'WhatsApp', roles: ['admin', 'cashier'], activePages: ['whatsapp','broadcast','whatsapp_live_chat','whatsapp_templates'] },
|
{ key: 'whatsapp', section: 'main', href: '/admin/whatsapp', icon: 'bi bi-whatsapp', labelKey: 'admin.nav.whatsapp', labelDefault: 'WhatsApp', roles: ['admin', 'cashier'], activePages: ['whatsapp','broadcast','whatsapp_live_chat','whatsapp_templates'] },
|
||||||
{ key: 'automation', section: 'main', href: '/admin/automation', icon: 'bi bi-robot', labelKey: 'admin.nav.automation', labelDefault: 'Automation', roles: ['admin'], activePages: ['automation'] },
|
{ key: 'automation', section: 'main', href: '/admin/automation', icon: 'bi bi-robot', labelKey: 'admin.nav.automation', labelDefault: 'Automation', roles: ['admin'], activePages: ['automation'] },
|
||||||
{ key: 'custom_qr', section: 'main', href: '/admin/custom-qr', icon: 'bi bi-qr-code-simple', labelKey: 'admin.nav.custom_qr', labelDefault: 'QR-Custom Payment', roles: ['admin'], activePages: ['custom_qr'] },
|
{ key: 'custom_qr', section: 'main', href: '/admin/custom-qr', icon: 'bi bi-qr-code', labelKey: 'admin.nav.custom_qr', labelDefault: 'QR-Custom Payment', roles: ['admin'], activePages: ['custom_qr'] },
|
||||||
|
|
||||||
{ key: 'grp_customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers', labelDefault: 'Pelanggan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 2, activePages: ['customers', 'onu_stickers'], children: [
|
{ key: 'grp_customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers', labelDefault: 'Pelanggan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 2, activePages: ['customers', 'onu_stickers'], children: [
|
||||||
{ key: 'customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers_data', labelDefault: 'Data Pelanggan', roles: ['admin', 'cashier'], activePages: ['customers'] },
|
{ key: 'customers', section: 'billing', href: '/admin/customers', icon: 'bi bi-people', labelKey: 'admin.nav.customers_data', labelDefault: 'Data Pelanggan', roles: ['admin', 'cashier'], activePages: ['customers'] },
|
||||||
@@ -269,12 +269,21 @@ function enrichMenu(menu, states) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCustomQrReady() {
|
||||||
|
try {
|
||||||
|
const enabled = getSetting('qris_static_enabled', true);
|
||||||
|
const off = enabled === false || enabled === 'false' || enabled === 0 || enabled === '0';
|
||||||
|
return !off && String(getSetting('qris_static_payload', '') || '').trim().length > 0;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
function getSidebarSections(session) {
|
function getSidebarSections(session) {
|
||||||
const states = getStoredMenuStates();
|
const states = getStoredMenuStates();
|
||||||
return SECTION_DEFINITIONS.map((section) => {
|
return SECTION_DEFINITIONS.map((section) => {
|
||||||
const items = MENU_DEFINITIONS
|
const items = MENU_DEFINITIONS
|
||||||
.filter((menu) => menu.section === section.key)
|
.filter((menu) => menu.section === section.key)
|
||||||
.filter((menu) => isMenuAllowedForSession(menu, session))
|
.filter((menu) => isMenuAllowedForSession(menu, session))
|
||||||
|
.filter((menu) => !(menu.key === 'custom_qr' && !isCustomQrReady()))
|
||||||
.map((menu) => {
|
.map((menu) => {
|
||||||
const enriched = enrichMenu(menu, states);
|
const enriched = enrichMenu(menu, states);
|
||||||
if (Array.isArray(enriched.children)) {
|
if (Array.isArray(enriched.children)) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.6.0
|
0.6.1
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (typeof hasQrisPayload !== 'undefined' && !hasQrisPayload) { %>
|
<% if (typeof ready !== 'undefined' && !ready) { %>
|
||||||
<div class="alert alert-d"><i class="bi bi-exclamation-triangle"></i> QRIS statis belum diatur. Isi <b>QRIS static payload</b> di Pengaturan agar QR bisa dibuat.</div>
|
<div class="alert alert-d"><i class="bi bi-exclamation-triangle"></i> <b>QR-Custom Payment dinonaktifkan.</b> Aktifkan <b>QRIS Statis (Semi-Otomatis)</b> dan isi <b>QRIS static payload</b> di <a href="/admin/settings" style="color:inherit;text-decoration:underline">Pengaturan</a> terlebih dahulu.</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (typeof ready === 'undefined' || ready) { %>
|
||||||
<div class="card mb16">
|
<div class="card mb16">
|
||||||
<div class="card-hd"><h6><i class="bi bi-plus-circle"></i> Buat QR Nominal Custom</h6></div>
|
<div class="card-hd"><h6><i class="bi bi-plus-circle"></i> Buat QR Nominal Custom</h6></div>
|
||||||
<div class="card-bd">
|
<div class="card-bd">
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<div class="form-row mb16">
|
<div class="form-row mb16">
|
||||||
<div class="fg"><div class="card"><div class="card-bd"><div class="muted" style="font-size:12px">Pembayaran masuk</div><div style="font-size:22px;font-weight:800"><%= stats.paidCount %></div><div class="muted" style="font-size:11px">Rp <%= Number(stats.paidTotal||0).toLocaleString('id-ID') %></div></div></div></div>
|
<div class="fg"><div class="card"><div class="card-bd"><div class="muted" style="font-size:12px">Pembayaran masuk</div><div style="font-size:22px;font-weight:800"><%= stats.paidCount %></div><div class="muted" style="font-size:11px">Rp <%= Number(stats.paidTotal||0).toLocaleString('id-ID') %></div></div></div></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user