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
153 lines
6.1 KiB
JavaScript
153 lines
6.1 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../config/database');
|
|
const { getSetting, getSettings, getNowLocal, formatDateLocal } = require('../config/settingsManager');
|
|
const sidebarMenuSvc = require('../services/sidebarMenuService');
|
|
|
|
// Helper untuk menampilkan notifikasi sukses/error
|
|
function flashMsg(req) {
|
|
const m = req.session._msg;
|
|
delete req.session._msg;
|
|
return m || null;
|
|
}
|
|
|
|
function company() { return getSetting('company_header', 'ISP Admin'); }
|
|
|
|
// Pastikan hanya admin & kasir yang bisa akses
|
|
function requireAdminSession(req, res, next) {
|
|
if (req.session?.isAdmin || req.session?.isCashier) return next();
|
|
return res.redirect('/admin/login');
|
|
}
|
|
|
|
router.use(requireAdminSession);
|
|
|
|
router.use((req, res, next) => {
|
|
res.locals.session = req.session;
|
|
res.locals.sidebarSections = sidebarMenuSvc.getSidebarSections(req.session);
|
|
res.locals.sidebarBottomNavItems = sidebarMenuSvc.getBottomNavItems(req.session);
|
|
res.locals.settings = getSettings();
|
|
res.locals.company = company();
|
|
res.locals.lang = req.session?.lang || 'id';
|
|
res.locals.getNowLocal = getNowLocal;
|
|
res.locals.formatDateLocal = formatDateLocal;
|
|
next();
|
|
});
|
|
|
|
// ─── KATEGORI PENGELUARAN ──────────────────────────────────────────────
|
|
router.get('/expense-categories', (req, res) => {
|
|
const categories = db.prepare('SELECT * FROM expense_categories ORDER BY name ASC').all();
|
|
res.render('admin/finance/expense_categories', {
|
|
activePage: 'expense_categories',
|
|
categories,
|
|
msg: flashMsg(req)
|
|
});
|
|
});
|
|
|
|
router.post('/expense-categories', express.urlencoded({ extended: true }), (req, res) => {
|
|
try {
|
|
const { name, description, icon, color } = req.body;
|
|
db.prepare(`INSERT INTO expense_categories (name, description, icon, color) VALUES (?, ?, ?, ?)`).run(name, description, icon || 'bi bi-tag', color || '#6366f1');
|
|
req.session._msg = { type: 'success', text: 'Kategori berhasil ditambahkan' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal: ' + e.message };
|
|
}
|
|
res.redirect('/admin/finance/expense-categories');
|
|
});
|
|
|
|
router.post('/expense-categories/:id/delete', (req, res) => {
|
|
try {
|
|
db.prepare('DELETE FROM expense_categories WHERE id = ?').run(req.params.id);
|
|
req.session._msg = { type: 'success', text: 'Kategori berhasil dihapus' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal dihapus (mungkin sedang digunakan)' };
|
|
}
|
|
res.redirect('/admin/finance/expense-categories');
|
|
});
|
|
|
|
// ─── PENGELUARAN (CASH OUT) ────────────────────────────────────────────
|
|
router.get('/expenses', (req, res) => {
|
|
const expenses = db.prepare(`
|
|
SELECT e.*, c.color as category_color, c.icon as category_icon
|
|
FROM expenses e
|
|
LEFT JOIN expense_categories c ON c.name = e.category
|
|
ORDER BY e.date DESC, e.id DESC LIMIT 500
|
|
`).all();
|
|
|
|
const categories = db.prepare('SELECT * FROM expense_categories ORDER BY name ASC').all();
|
|
|
|
res.render('admin/finance/expenses', {
|
|
activePage: 'expenses',
|
|
expenses,
|
|
categories,
|
|
msg: flashMsg(req)
|
|
});
|
|
});
|
|
|
|
router.post('/expenses', express.urlencoded({ extended: true }), (req, res) => {
|
|
try {
|
|
const { date, category, amount, description, payment_method, receipt_number, vendor } = req.body;
|
|
const cleanAmount = String(amount).replace(/[^0-9]/g, ''); // bersihkan format rupiah
|
|
const recorded_by_name = req.session.cashierName ? `Kasir ${req.session.cashierName}` : 'Admin';
|
|
|
|
db.prepare(`
|
|
INSERT INTO expenses (date, category, amount, description, payment_method, receipt_number, vendor, recorded_by_name)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(date, category, cleanAmount, description, payment_method, receipt_number, vendor, recorded_by_name);
|
|
|
|
req.session._msg = { type: 'success', text: 'Pengeluaran berhasil dicatat' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal mencatat pengeluaran: ' + e.message };
|
|
}
|
|
res.redirect('/admin/finance/expenses');
|
|
});
|
|
|
|
router.post('/expenses/:id/delete', (req, res) => {
|
|
try {
|
|
db.prepare('DELETE FROM expenses WHERE id = ?').run(req.params.id);
|
|
req.session._msg = { type: 'success', text: 'Data berhasil dihapus' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal dihapus' };
|
|
}
|
|
res.redirect('/admin/finance/expenses');
|
|
});
|
|
|
|
// ─── KAS MASUK (CASH IN) ───────────────────────────────────────────────
|
|
router.get('/cash-in', (req, res) => {
|
|
const cashIn = db.prepare(`SELECT * FROM cash_in ORDER BY date DESC, id DESC LIMIT 500`).all();
|
|
res.render('admin/finance/cash_in', {
|
|
activePage: 'cash_in',
|
|
cashIn,
|
|
msg: flashMsg(req)
|
|
});
|
|
});
|
|
|
|
router.post('/cash-in', express.urlencoded({ extended: true }), (req, res) => {
|
|
try {
|
|
const { date, category, amount, description, payment_method, receipt_number } = req.body;
|
|
const cleanAmount = String(amount).replace(/[^0-9]/g, ''); // bersihkan format rupiah
|
|
const recorded_by_name = req.session.cashierName ? `Kasir ${req.session.cashierName}` : 'Admin';
|
|
|
|
db.prepare(`
|
|
INSERT INTO cash_in (date, category, amount, description, payment_method, receipt_number, recorded_by_name)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(date, category, cleanAmount, description, payment_method, receipt_number, recorded_by_name);
|
|
|
|
req.session._msg = { type: 'success', text: 'Kas Masuk berhasil dicatat' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal mencatat Kas Masuk: ' + e.message };
|
|
}
|
|
res.redirect('/admin/finance/cash-in');
|
|
});
|
|
|
|
router.post('/cash-in/:id/delete', (req, res) => {
|
|
try {
|
|
db.prepare('DELETE FROM cash_in WHERE id = ?').run(req.params.id);
|
|
req.session._msg = { type: 'success', text: 'Data berhasil dihapus' };
|
|
} catch(e) {
|
|
req.session._msg = { type: 'error', text: 'Gagal dihapus' };
|
|
}
|
|
res.redirect('/admin/finance/cash-in');
|
|
});
|
|
|
|
module.exports = router;
|