Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
376 lines
20 KiB
JavaScript
376 lines
20 KiB
JavaScript
const crypto = require('crypto');
|
|
const { getSetting, getSettings, saveSettings } = require('../config/settingsManager');
|
|
const { getAppSetting, saveAppSetting } = require('../config/database');
|
|
|
|
const FEATURE_PASSWORD_HASH = 'af03458614be223016c5bc1c0788eaefc6a6bfd0c11cdcd4bc520f49d0cb474a'; // sha256 kode aktivasi — plaintext hanya di settings.json (activation_code), tidak di repo
|
|
const FEATURE_CONTACT_PHONE = '081947215703';
|
|
|
|
function getFeaturePasswordHash() {
|
|
return getSetting('feature_password_hash', FEATURE_PASSWORD_HASH);
|
|
}
|
|
|
|
function getFeatureContactPhone() {
|
|
return getSetting('feature_contact_phone', FEATURE_CONTACT_PHONE);
|
|
}
|
|
|
|
const SETTINGS_KEY = 'sidebar_menu_states';
|
|
const STATE_VISIBLE = 'visible';
|
|
const STATE_HIDDEN = 'hidden';
|
|
const STATE_LOCKED = 'locked';
|
|
const VALID_STATES = new Set([STATE_VISIBLE, STATE_HIDDEN, STATE_LOCKED]);
|
|
|
|
const MENU_DEFINITIONS = [
|
|
{ key: 'dashboard', section: 'main', href: '/admin', icon: 'bi bi-speedometer2', labelKey: 'admin.nav.dashboard', labelDefault: 'Dashboard', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 1, activePages: ['dashboard'] },
|
|
{ key: 'network', section: 'main', href: '/admin/mikrotik', icon: 'bi bi-diagram-3', labelKey: 'admin.nav.network', labelDefault: 'Jaringan', roles: ['admin', 'cashier'], activePages: ['mikrotik', 'radius_settings', 'acs_pro', 'onu_provision', 'olts', 'monitoring'], children: [
|
|
{ key: 'mikrotik', section: 'main', href: '/admin/mikrotik', icon: 'bi bi-router', labelKey: 'admin.nav.mikrotik', labelDefault: 'MikroTik', roles: ['admin', 'cashier'], activePages: ['mikrotik'] },
|
|
{ key: 'radius_server', section: 'main', href: '/admin/radius-settings', icon: 'bi bi-broadcast', labelKey: 'admin.nav.radius_server', labelDefault: 'RADIUS Server', roles: ['admin'], activePages: ['radius_settings'] },
|
|
{ key: 'acs_pro', section: 'main', href: '/admin/acs', icon: 'bi bi-hdd-network', labelKey: 'admin.nav.acs_pro', labelDefault: 'GenieACS Pro', roles: ['admin'], activePages: ['acs_pro'] },
|
|
{ key: 'onu_provision', section: 'main', href: '/admin/onu-provision', icon: 'bi bi-hdd-network-fill', labelKey: 'admin.nav.onu_provision', labelDefault: 'ONU Provision', roles: ['admin'], activePages: ['onu_provision'] },
|
|
{ key: 'olts', section: 'main', href: '/admin/olts', icon: 'bi bi-hdd-fill', labelKey: 'admin.nav.olt_management', labelDefault: 'Manajemen OLT', roles: ['admin'], activePages: ['olts'] },
|
|
{ key: 'monitoring', section: 'main', href: '/admin/monitoring', icon: 'bi bi-activity', labelKey: 'admin.nav.monitoring', labelDefault: 'Monitoring Sistem', roles: ['admin'], activePages: ['monitoring'] }
|
|
] },
|
|
{ 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: 'automation', section: 'main', href: '/admin/automation', icon: 'bi bi-robot', labelKey: 'admin.nav.automation', labelDefault: 'Automation', roles: ['admin'], activePages: ['automation'] },
|
|
|
|
{ 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: 'onu_stickers', section: 'billing', href: '/admin/onu-stickers', icon: 'bi bi-qr-code', labelKey: 'admin.nav.onu_stickers', labelDefault: 'Stiker Modem ONU', roles: ['admin', 'cashier'], activePages: ['onu_stickers'] }
|
|
] },
|
|
{ key: 'grp_packages', section: 'billing', href: '/admin/packages', icon: 'bi bi-box-seam', labelKey: 'admin.nav.grp_packages', labelDefault: 'Paket & Voucher', roles: ['admin', 'cashier'], activePages: ['packages', 'voucher_packages', 'vouchers_discounts', 'discounts', 'promo_slides'], children: [
|
|
{ key: 'packages', section: 'billing', href: '/admin/packages', icon: 'bi bi-box-seam', labelKey: 'admin.nav.internet_packages', labelDefault: 'Paket Internet', roles: ['admin', 'cashier'], activePages: ['packages'] },
|
|
{ key: 'voucher_packages', section: 'billing', href: '/admin/vouchers/packages', icon: 'bi bi-ticket-detailed', labelKey: 'admin.nav.voucher_packages', labelDefault: 'Paket Voucher', roles: ['admin', 'cashier'], activePages: ['voucher_packages'] },
|
|
{ key: 'vouchers_discounts', section: 'billing', href: '/admin/vouchers-discounts', icon: 'bi bi-ticket-perforated', labelKey: 'admin.nav.vouchers_discounts', labelDefault: 'Biaya Pasang', roles: ['admin'], activePages: ['vouchers_discounts'] },
|
|
{ key: 'discounts', section: 'billing', href: '/admin/discounts', icon: 'bi bi-percent', labelKey: 'admin.nav.discounts', labelDefault: 'Diskon', roles: ['admin', 'cashier'], activePages: ['discounts'] },
|
|
{ key: 'promo_slides', section: 'billing', href: '/admin/promo-slides', icon: 'bi bi-image', labelKey: 'admin.nav.promo_slides', labelDefault: 'Promo Slides', roles: ['admin'], activePages: ['promo_slides'] }
|
|
] },
|
|
{ key: 'grp_billing', section: 'billing', href: '/admin/billing', icon: 'bi bi-receipt', labelKey: 'admin.nav.invoices', labelDefault: 'Tagihan', roles: ['admin', 'cashier'], bottomNav: true, bottomNavOrder: 3, activePages: ['billing', 'collector_payments'], children: [
|
|
{ key: 'billing', section: 'billing', href: '/admin/billing', icon: 'bi bi-receipt', labelKey: 'admin.nav.invoices_list', labelDefault: 'Data Tagihan', roles: ['admin', 'cashier'], activePages: ['billing'] },
|
|
{ key: 'collector_payments', section: 'billing', href: '/admin/collector-payments', icon: 'bi bi-check2-square', labelKey: 'admin.nav.collector_payments', labelDefault: 'Approval Kolektor', roles: ['admin', 'cashier'], activePages: ['collector_payments'] }
|
|
] },
|
|
|
|
{ key: 'grp_finance', section: 'finance', href: '/admin/reports', icon: 'bi bi-cash-stack', labelKey: 'admin.nav.grp_finance', labelDefault: 'Keuangan', roles: ['admin', 'cashier'], activePages: ['reports', 'cashiers_reports', 'payroll', 'cash_in', 'expenses', 'expense_categories'], children: [
|
|
{ key: 'reports', section: 'finance', href: '/admin/reports', icon: 'bi bi-bar-chart-line', labelKey: 'admin.nav.finance_report', labelDefault: 'Laporan Keuangan', roles: ['admin', 'cashier'], activePages: ['reports'] },
|
|
{ key: 'cashiers_reports', section: 'finance', href: '/admin/cashiers/reports', icon: 'bi bi-journal-text', labelKey: 'admin.nav.cashiers_reports', labelDefault: 'Laporan Kasir', roles: ['admin', 'cashier'], activePages: ['cashiers_reports'] },
|
|
{ key: 'cash_in', section: 'finance', href: '/admin/finance/cash-in', icon: 'bi bi-cash-stack', labelKey: 'admin.nav.cash_in', labelDefault: 'Kas Masuk', roles: ['admin', 'cashier'], activePages: ['cash_in'] },
|
|
{ key: 'expenses', section: 'finance', href: '/admin/finance/expenses', icon: 'bi bi-wallet2', labelKey: 'admin.nav.expenses', labelDefault: 'Pengeluaran', roles: ['admin', 'cashier'], activePages: ['expenses'] },
|
|
{ key: 'expense_categories', section: 'finance', href: '/admin/finance/expense-categories', icon: 'bi bi-tags', labelKey: 'admin.nav.expense_categories', labelDefault: 'Kategori Pengeluaran', roles: ['admin'], activePages: ['expense_categories'] },
|
|
{ key: 'payroll', section: 'finance', href: '/admin/payroll', icon: 'bi bi-wallet2', labelKey: 'admin.nav.payroll', labelDefault: 'Gaji & Payroll', roles: ['admin'], activePages: ['payroll'] }
|
|
] },
|
|
|
|
{ key: 'grp_service', section: 'service', href: '/admin/tickets', icon: 'bi bi-headset', labelKey: 'admin.nav.grp_service', labelDefault: 'Layanan', roles: ['admin', 'cashier'], activePages: ['tickets', 'inventory', 'attendance'], children: [
|
|
{ key: 'tickets', section: 'service', href: '/admin/tickets', icon: 'bi bi-headset', labelKey: 'admin.nav.customer_tickets', labelDefault: 'Keluhan Pelanggan', roles: ['admin', 'cashier'], activePages: ['tickets'] },
|
|
{ key: 'inventory', section: 'service', href: '/admin/inventory', icon: 'bi bi-boxes', labelKey: 'admin.nav.inventory', labelDefault: 'Inventaris (Stok)', roles: ['admin', 'cashier'], activePages: ['inventory'] },
|
|
{ key: 'attendance', section: 'service', href: '/admin/attendance', icon: 'bi bi-calendar-check', labelKey: 'admin.nav.attendance', labelDefault: 'Absensi Karyawan', roles: ['admin', 'cashier'], activePages: ['attendance'] }
|
|
] },
|
|
|
|
{ key: 'cashier_attendance', section: 'cashier', href: '/admin/cashiers/attendance', icon: 'bi bi-calendar-check', labelKey: 'admin.nav.cashier_attendance', labelDefault: 'Absensi Saya', roles: ['cashier'], activePages: ['cashier_attendance'] },
|
|
|
|
{ key: 'grp_users', section: 'user_management', href: '/admin/technicians', icon: 'bi bi-person-gear', labelKey: 'admin.nav.grp_users', labelDefault: 'Pengguna', roles: ['admin', 'cashier'], activePages: ['technicians', 'cashiers', 'collectors', 'areas'], children: [
|
|
{ key: 'technicians', section: 'user_management', href: '/admin/technicians', icon: 'bi bi-person-gear', labelKey: 'admin.nav.technicians', labelDefault: 'Teknisi', roles: ['admin'], activePages: ['technicians'] },
|
|
{ key: 'cashiers', section: 'user_management', href: '/admin/cashiers', icon: 'bi bi-person-vcard', labelKey: 'admin.nav.cashiers', labelDefault: 'Kasir', roles: ['admin'], activePages: ['cashiers'] },
|
|
{ key: 'collectors', section: 'user_management', href: '/admin/collectors', icon: 'bi bi-person-badge', labelKey: 'admin.nav.collectors', labelDefault: 'Kolektor', roles: ['admin'], activePages: ['collectors'] },
|
|
{ key: 'areas', section: 'user_management', href: '/admin/areas', icon: 'bi bi-geo-alt', labelKey: 'admin.nav.areas', labelDefault: 'Manajemen Area', roles: ['admin', 'cashier'], activePages: ['areas'] }
|
|
] },
|
|
|
|
{ key: 'grp_system', section: 'system', href: '/admin/settings', icon: 'bi bi-gear', labelKey: 'admin.nav.grp_system', labelDefault: 'Sistem', roles: ['admin'], activePages: ['update', 'service_notice', 'settings', 'ewallet_logs', 'backup', 'audit_logs'], children: [
|
|
{ key: 'update', section: 'system', href: '/admin/update', icon: 'bi bi-cloud-arrow-down', labelKey: 'admin.nav.update', labelDefault: 'Update GitHub', roles: ['admin'], activePages: ['update'] },
|
|
{ key: 'service_notice', section: 'system', href: '/admin/service-notice', icon: 'bi bi-megaphone-fill', labelKey: 'admin.nav.service_notice', labelDefault: 'Status Layanan', roles: ['admin'], activePages: ['service_notice'] },
|
|
{ key: 'settings', section: 'system', href: '/admin/settings', icon: 'bi bi-gear', labelKey: 'admin.nav.settings', labelDefault: 'Pengaturan', roles: ['admin'], activePages: ['settings'] },
|
|
{ key: 'ewallet_logs', section: 'system', href: '/admin/ewallet-logs', icon: 'bi bi-journal-text', labelKey: 'admin.nav.log_rekap', labelDefault: 'Log & Rekap', roles: ['admin'], activePages: ['ewallet_logs'] },
|
|
{ key: 'backup', section: 'system', href: '/admin/backup', icon: 'bi bi-hdd-stack', labelKey: 'admin.nav.backup', labelDefault: 'Backup & Recovery', roles: ['admin'], activePages: ['backup'] },
|
|
{ key: 'audit_logs', section: 'system', href: '/admin/audit-logs', icon: 'bi bi-shield-lock', labelKey: 'admin.nav.audit_logs', labelDefault: 'Log Aktivitas', roles: ['admin'], activePages: ['audit_logs'] }
|
|
] }
|
|
|
|
];
|
|
|
|
const DEFAULT_MENU_STATES = {
|
|
dashboard: STATE_VISIBLE,
|
|
mikrotik: STATE_VISIBLE,
|
|
network: STATE_VISIBLE,
|
|
grp_customers: STATE_VISIBLE,
|
|
grp_packages: STATE_VISIBLE,
|
|
grp_billing: STATE_VISIBLE,
|
|
grp_finance: STATE_VISIBLE,
|
|
grp_service: STATE_VISIBLE,
|
|
grp_users: STATE_VISIBLE,
|
|
grp_system: STATE_VISIBLE,
|
|
map: STATE_VISIBLE,
|
|
radius_server: STATE_VISIBLE,
|
|
map: STATE_VISIBLE,
|
|
acs_pro: STATE_VISIBLE,
|
|
onu_provision: STATE_VISIBLE,
|
|
olts: STATE_HIDDEN,
|
|
whatsapp: STATE_VISIBLE,
|
|
automation: STATE_VISIBLE,
|
|
broadcast: STATE_VISIBLE,
|
|
whatsapp_live_chat: STATE_VISIBLE,
|
|
promo_slides: STATE_VISIBLE,
|
|
customers: STATE_VISIBLE,
|
|
onu_stickers: STATE_VISIBLE,
|
|
packages: STATE_VISIBLE,
|
|
voucher_packages: STATE_VISIBLE,
|
|
billing: STATE_VISIBLE,
|
|
reports: STATE_VISIBLE,
|
|
cashiers_reports: STATE_VISIBLE,
|
|
collector_payments: STATE_VISIBLE,
|
|
tickets: STATE_VISIBLE,
|
|
inventory: STATE_LOCKED,
|
|
attendance: STATE_LOCKED,
|
|
payroll: STATE_LOCKED,
|
|
cash_in: STATE_VISIBLE,
|
|
expenses: STATE_VISIBLE,
|
|
expense_categories: STATE_VISIBLE,
|
|
cashier_attendance: STATE_VISIBLE,
|
|
technicians: STATE_LOCKED,
|
|
cashiers: STATE_LOCKED,
|
|
collectors: STATE_LOCKED,
|
|
areas: STATE_VISIBLE,
|
|
update: STATE_VISIBLE,
|
|
vouchers_discounts: STATE_VISIBLE,
|
|
discounts: STATE_VISIBLE,
|
|
service_notice: STATE_VISIBLE,
|
|
settings: STATE_VISIBLE,
|
|
ewallet_logs: STATE_VISIBLE,
|
|
backup: STATE_VISIBLE,
|
|
monitoring: STATE_VISIBLE,
|
|
audit_logs: STATE_VISIBLE
|
|
};
|
|
|
|
const SECTION_DEFINITIONS = [
|
|
{ key: 'main', labelKey: 'admin.section.main', labelDefault: 'UTAMA' },
|
|
{ key: 'billing', labelKey: 'admin.section.billing', labelDefault: 'BILLING' },
|
|
{ key: 'finance', labelKey: 'admin.section.finance', labelDefault: 'KEUANGAN' },
|
|
{ key: 'service', labelKey: 'admin.section.service', labelDefault: 'LAYANAN' },
|
|
{ key: 'cashier', labelKey: 'admin.section.cashier', labelDefault: 'KASIR' },
|
|
{ key: 'user_management', labelKey: 'admin.section.user_management', labelDefault: 'MANAJEMEN USER' },
|
|
{ key: 'system', labelKey: 'admin.section.system', labelDefault: 'SISTEM' }
|
|
];
|
|
|
|
function sha256(input) {
|
|
return crypto.createHash('sha256').update(String(input || '')).digest('hex');
|
|
}
|
|
|
|
function normalizeState(value) {
|
|
const normalized = String(value || '').trim().toLowerCase();
|
|
return VALID_STATES.has(normalized) ? normalized : STATE_VISIBLE;
|
|
}
|
|
|
|
function allMenuDefs() {
|
|
const out = [];
|
|
for (const m of MENU_DEFINITIONS) {
|
|
out.push(m);
|
|
if (Array.isArray(m.children)) { for (const c of m.children) out.push(c); }
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function getStoredMenuStates() {
|
|
// Coba ambil dari Database dulu (Lebih Aman)
|
|
let raw = getAppSetting(SETTINGS_KEY, null);
|
|
let activationKeys = getAppSetting('sidebar_activation_keys', {});
|
|
|
|
// Fallback ke settings.json jika di DB masih kosong (Migration)
|
|
if (raw === null) {
|
|
raw = getSetting(SETTINGS_KEY, {});
|
|
activationKeys = getSetting('sidebar_activation_keys', {});
|
|
// Langsung migrasi ke DB agar kedepannya pakai DB
|
|
if (Object.keys(raw).length > 0) {
|
|
saveAppSetting(SETTINGS_KEY, raw);
|
|
saveAppSetting('sidebar_activation_keys', activationKeys);
|
|
}
|
|
}
|
|
|
|
const stateMap = {};
|
|
|
|
for (const menu of allMenuDefs()) {
|
|
const defaultState = DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
|
|
let storedState = raw && raw[menu.key] ? raw[menu.key] : defaultState;
|
|
|
|
const normalized = normalizeState(storedState);
|
|
|
|
// Jika menu aslinya LOCKED tapi diubah jadi VISIBLE/HIDDEN, cek kunci aktivasinya
|
|
if (defaultState === STATE_LOCKED && normalized !== STATE_LOCKED) {
|
|
const expectedKey = sha256(menu.key + getFeaturePasswordHash());
|
|
const providedKey = activationKeys[menu.key];
|
|
|
|
if (providedKey !== expectedKey) {
|
|
// Kunci tidak cocok! Kembalikan ke LOCKED
|
|
stateMap[menu.key] = STATE_LOCKED;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
stateMap[menu.key] = normalized;
|
|
}
|
|
return stateMap;
|
|
}
|
|
|
|
function saveMenuStates(stateMap) {
|
|
const activationKeys = getAppSetting('sidebar_activation_keys', {});
|
|
const passwordHash = getFeaturePasswordHash();
|
|
|
|
for (const key in stateMap) {
|
|
const newState = stateMap[key];
|
|
const defaultState = DEFAULT_MENU_STATES[key] || STATE_VISIBLE;
|
|
|
|
// Jika menu yang aslinya LOCKED diaktifkan (jadi visible/hidden), generate kunci
|
|
if (defaultState === STATE_LOCKED && newState !== STATE_LOCKED) {
|
|
activationKeys[key] = sha256(key + passwordHash);
|
|
} else if (newState === STATE_LOCKED) {
|
|
// Jika dikunci kembali, hapus kuncinya
|
|
delete activationKeys[key];
|
|
}
|
|
}
|
|
|
|
// Simpan ke Database (Utama)
|
|
saveAppSetting(SETTINGS_KEY, sanitizeMenuStates(stateMap));
|
|
saveAppSetting('sidebar_activation_keys', activationKeys);
|
|
|
|
// Hanya gunakan database, tidak perlu backup ke settings.json
|
|
return true;
|
|
}
|
|
|
|
function sanitizeMenuStates(input) {
|
|
const clean = {};
|
|
for (const menu of allMenuDefs()) {
|
|
const defaultState = DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
|
|
let state = input && input[menu.key] ? input[menu.key] : defaultState;
|
|
|
|
clean[menu.key] = normalizeState(state);
|
|
}
|
|
return clean;
|
|
}
|
|
|
|
function isMenuAllowedForSession(menu, session) {
|
|
const roles = Array.isArray(menu.roles) ? menu.roles : ['admin'];
|
|
if (roles.includes('admin') && session && session.isAdmin) return true;
|
|
if (roles.includes('cashier') && session && session.isCashier) return true;
|
|
return false;
|
|
}
|
|
|
|
function enrichMenu(menu, states) {
|
|
const state = states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
|
|
const locked = state === STATE_LOCKED;
|
|
const hidden = state === STATE_HIDDEN;
|
|
const out = {
|
|
...menu,
|
|
state,
|
|
locked,
|
|
hidden,
|
|
hrefResolved: menu.href,
|
|
lockedMessage: locked ? `Menu "${menu.labelDefault}" terkunci. Hubungi ${getFeatureContactPhone()} untuk mendapatkan password aktivasi.` : ''
|
|
};
|
|
if (Array.isArray(menu.children)) {
|
|
out.children = menu.children.map((ch) => enrichMenu(ch, states));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function getSidebarSections(session) {
|
|
const states = getStoredMenuStates();
|
|
return SECTION_DEFINITIONS.map((section) => {
|
|
const items = MENU_DEFINITIONS
|
|
.filter((menu) => menu.section === section.key)
|
|
.filter((menu) => isMenuAllowedForSession(menu, session))
|
|
.map((menu) => {
|
|
const enriched = enrichMenu(menu, states);
|
|
if (Array.isArray(enriched.children)) {
|
|
enriched.children = enriched.children
|
|
.filter((ch) => isMenuAllowedForSession(ch, session))
|
|
.filter((ch) => !ch.hidden);
|
|
}
|
|
return enriched;
|
|
})
|
|
.filter((menu) => !menu.hidden);
|
|
|
|
return {
|
|
...section,
|
|
items
|
|
};
|
|
}).filter((section) => section.items.length > 0);
|
|
}
|
|
|
|
function getBottomNavItems(session) {
|
|
const states = getStoredMenuStates();
|
|
return MENU_DEFINITIONS
|
|
.filter((menu) => menu.bottomNav)
|
|
.filter((menu) => isMenuAllowedForSession(menu, session))
|
|
.map((menu) => enrichMenu(menu, states))
|
|
.filter((menu) => !menu.hidden)
|
|
.sort((a, b) => (a.bottomNavOrder || 99) - (b.bottomNavOrder || 99));
|
|
}
|
|
|
|
function getConfigMenus() {
|
|
const states = getStoredMenuStates();
|
|
return MENU_DEFINITIONS.map((menu) => {
|
|
const section = SECTION_DEFINITIONS.find((s) => s.key === menu.section);
|
|
return {
|
|
...menu,
|
|
state: states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE,
|
|
roleLabel: menu.roles.includes('admin') && menu.roles.includes('cashier')
|
|
? 'Admin & Kasir'
|
|
: menu.roles.includes('cashier')
|
|
? 'Kasir'
|
|
: 'Admin',
|
|
sectionLabel: section?.labelDefault || menu.section,
|
|
sectionLabelKey: section?.labelKey || ''
|
|
};
|
|
});
|
|
}
|
|
|
|
function getMenuDefinition(key) {
|
|
const top = MENU_DEFINITIONS.find((menu) => menu.key === key);
|
|
if (top) return top;
|
|
for (const menu of MENU_DEFINITIONS) {
|
|
if (Array.isArray(menu.children)) {
|
|
const ch = menu.children.find((c) => c.key === key);
|
|
if (ch) return ch;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isFeaturePasswordValid(password) {
|
|
return sha256(password) === getFeaturePasswordHash();
|
|
}
|
|
|
|
function evaluateMenuAccess(menuKey, session) {
|
|
const menu = getMenuDefinition(menuKey);
|
|
if (!menu) {
|
|
return { allowed: true, state: STATE_VISIBLE, menu: null };
|
|
}
|
|
|
|
if (!isMenuAllowedForSession(menu, session)) {
|
|
return { allowed: false, state: 'forbidden', menu, reason: 'forbidden' };
|
|
}
|
|
|
|
const states = getStoredMenuStates();
|
|
const state = states[menu.key] || DEFAULT_MENU_STATES[menu.key] || STATE_VISIBLE;
|
|
if (state === STATE_HIDDEN) {
|
|
return { allowed: false, state, menu, reason: 'hidden' };
|
|
}
|
|
if (state === STATE_LOCKED) {
|
|
return { allowed: false, state, menu, reason: 'locked' };
|
|
}
|
|
return { allowed: true, state, menu, reason: null };
|
|
}
|
|
|
|
module.exports = {
|
|
getFeatureContactPhone,
|
|
getFeaturePasswordHash,
|
|
STATE_VISIBLE,
|
|
STATE_HIDDEN,
|
|
STATE_LOCKED,
|
|
MENU_DEFINITIONS,
|
|
getSidebarSections,
|
|
getBottomNavItems,
|
|
getConfigMenus,
|
|
getMenuDefinition,
|
|
getStoredMenuStates,
|
|
sanitizeMenuStates,
|
|
isFeaturePasswordValid,
|
|
saveMenuStates,
|
|
evaluateMenuAccess,
|
|
};
|