feat(akses): batasi teknisi & kolektor sesuai wilayah tugas (port dari upstream)

- techService: getTechAreaWhere + canTechAccessCustomer, filter pool/stats
- techPortal: filter peta, ODP, daftar perangkat; guard perangkat per wilayah; area pelanggan baru dipaksa sesuai wilayah
- customerService: getCollectorCustomerWhere + canCollectorAccessCustomer (multi-area)
- collectorPortal: daftar tagihan pakai helper; tolak bayar/cetak/kirim PDF di luar wilayah
- customerAPI: dashboard & pay-bill kolektor dibatasi wilayah
- tests/unit/area-access.test.js: self-check logika area
This commit is contained in:
oxpoe
2026-11-05 01:16:26 +07:00
parent 2a29739dc3
commit d24cabf818
6 changed files with 290 additions and 30 deletions

View File

@@ -740,6 +740,13 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => {
const curMonth = now.getMonth() + 1;
const curYear = now.getFullYear();
const isFullAdmin = req.collector?.role === 'admin' || req.collector?.role === 'root';
const collectorId = Number(req.collector?.collectorId || req.collector?.id || 0);
const collector = collectorId
? db.prepare('SELECT id, name, area, auto_approve FROM collectors WHERE id = ?').get(collectorId)
: null;
const colFilter = (!isFullAdmin && collector) ? customerSvc.getCollectorCustomerWhere(collector, 'c') : null;
let q = `
SELECT c.id, c.name, c.phone, c.address, c.status, c.area, c.isolate_day, c.pppoe_username,
p.name as package_name, p.price as package_price,
@@ -752,6 +759,11 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => {
`;
const params = [curMonth, curYear];
if (colFilter) {
q += ` AND ${colFilter.where}`;
params.push(...colFilter.params);
}
if (filterStatus === 'unpaid') {
q += ` AND (i.status = 'unpaid' OR i.status IS NULL)`;
} else if (filterStatus === 'today') {
@@ -770,6 +782,7 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => {
q += ` ORDER BY (CASE WHEN c.status = 'suspended' THEN 0 WHEN i.status = 'unpaid' THEN 1 ELSE 2 END), c.name ASC LIMIT 150`;
const list = db.prepare(q).all(...params) || [];
const statsWhere = colFilter ? ` WHERE ${colFilter.where}` : '';
const stats = db.prepare(`
SELECT
SUM(CASE WHEN (i.status = 'unpaid' OR i.status IS NULL) THEN 1 ELSE 0 END) as unpaid_count,
@@ -779,7 +792,8 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => {
FROM customers c
LEFT JOIN packages p ON p.id = c.package_id
LEFT JOIN invoices i ON i.customer_id = c.id AND i.period_month = ? AND i.period_year = ?
`).get(todayDay, curMonth, curYear) || {};
${statsWhere}
`).get(todayDay, curMonth, curYear, ...(colFilter ? colFilter.params : [])) || {};
res.json({
success: true,
@@ -819,7 +833,17 @@ router.post('/app/collector/pay-bill', requireCollectorApiAuth, async (req, res)
if (!inv) return res.status(404).json({ success: false, message: 'Tagihan tidak ditemukan' });
const collectorId = Number(req.collector?.collectorId || req.collector?.id || 0);
const collector = collectorId ? db.prepare('SELECT id, name, auto_approve FROM collectors WHERE id = ?').get(collectorId) : null;
const collector = collectorId ? db.prepare('SELECT id, name, area, auto_approve FROM collectors WHERE id = ?').get(collectorId) : null;
const isFullAdmin = req.collector?.role === 'admin' || req.collector?.role === 'root';
if (!isFullAdmin && collector) {
const cust = customerSvc.getCustomerById(inv.customer_id);
if (!cust) return res.status(404).json({ success: false, message: 'Pelanggan tidak ditemukan' });
if (!customerSvc.canCollectorAccessCustomer(collector, cust)) {
return res.status(403).json({ success: false, message: 'Akses ditolak: pelanggan di luar wilayah/tugas Anda' });
}
}
const collectorName = collector?.name || req.collector?.name || 'Kolektor Lapangan';
const isAutoApprove = collector && collector.auto_approve === 1;