Files
open-isp/services/techService.js
oxpoe d24cabf818 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
2026-11-05 01:16:26 +07:00

170 lines
6.1 KiB
JavaScript

const db = require('../config/database');
function authenticate(username, password) {
const tech = db.prepare('SELECT * FROM technicians WHERE username = ? AND password = ? AND is_active = 1').get(username, password);
return tech;
}
function getTechById(id) {
return db.prepare('SELECT id, username, name, phone, area FROM technicians WHERE id = ?').get(id);
}
/**
* Batas wilayah tugas teknisi.
* area kosong / "semua" / "all" = tanpa batas (semua wilayah).
*/
function getTechAreaWhere(technician, tableAlias = 'c') {
let tech = technician;
if (typeof technician === 'number' || typeof technician === 'string') {
tech = db.prepare('SELECT id, area FROM technicians WHERE id = ?').get(Number(technician));
}
const areas = String(tech?.area || '')
.split(',')
.map(a => a.trim().toLowerCase())
.filter(a => a.length > 0 && a !== 'semua' && a !== 'all');
if (areas.length > 0) {
const placeholders = areas.map(() => '?').join(', ');
return {
where: `LOWER(TRIM(${tableAlias}.area)) IN (${placeholders})`,
params: areas,
isRestricted: true,
areas,
};
}
return { where: '1=1', params: [], isRestricted: false, areas: [] };
}
function canTechAccessCustomer(technician, customer) {
if (!technician) return false;
const filter = getTechAreaWhere(technician);
if (!filter.isRestricted) return true;
const custArea = String(customer?.area || '').trim().toLowerCase();
return !!custArea && filter.areas.includes(custArea);
}
function getTechStats(techId) {
const total = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ?").get(techId).count;
const inProgress = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ? AND status = 'in_progress'").get(techId).count;
const resolved = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ? AND status = 'resolved'").get(techId).count;
const filter = techId ? getTechAreaWhere(techId, 'c') : null;
const open = filter && filter.isRestricted
? db.prepare(`SELECT COUNT(*) as count FROM tickets t JOIN customers c ON t.customer_id = c.id WHERE t.status = 'open' AND ${filter.where}`).get(...filter.params).count
: db.prepare("SELECT COUNT(*) as count FROM tickets WHERE status = 'open'").get().count;
return { total, open, inProgress, resolved };
}
function getAssignedTickets(techId) {
return db.prepare(`
SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area
FROM tickets t
JOIN customers c ON t.customer_id = c.id
WHERE t.technician_id = ? AND t.status != 'resolved'
ORDER BY t.created_at DESC
`).all(techId);
}
function getOpenTickets(techId = null) {
const filter = techId ? getTechAreaWhere(techId, 'c') : null;
const areaWhere = filter && filter.isRestricted ? `AND ${filter.where}` : '';
return db.prepare(`
SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area
FROM tickets t
JOIN customers c ON t.customer_id = c.id
WHERE t.status = 'open' ${areaWhere}
ORDER BY t.created_at DESC
`).all(...(filter && filter.isRestricted ? filter.params : []));
}
function getResolvedTickets(techId) {
return db.prepare(`
SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area
FROM tickets t
JOIN customers c ON t.customer_id = c.id
WHERE t.technician_id = ? AND t.status = 'resolved'
ORDER BY t.updated_at DESC LIMIT 50
`).all(techId);
}
function takeTicket(ticketId, techId) {
let validTechId = Number(techId);
let tech = db.prepare('SELECT id, area FROM technicians WHERE id = ?').get(validTechId);
if (!tech) {
const fallback = db.prepare('SELECT id, area FROM technicians WHERE is_active = 1 ORDER BY id ASC LIMIT 1').get();
if (!fallback) throw new Error('Teknisi tidak ditemukan.');
validTechId = fallback.id;
tech = fallback;
}
const ticket = db.prepare(`
SELECT t.id, t.customer_id, t.status, c.area as customer_area
FROM tickets t
LEFT JOIN customers c ON t.customer_id = c.id
WHERE t.id = ?
`).get(ticketId);
if (!ticket) throw new Error('Tiket tidak ditemukan.');
if (ticket.status !== 'open') throw new Error('Tiket sudah diambil oleh teknisi lain atau sudah selesai.');
if (ticket.customer_id && !canTechAccessCustomer(tech, { area: ticket.customer_area })) {
throw new Error(`Tiket ini berada di wilayah "${ticket.customer_area || 'Tanpa Area'}", di luar wilayah tugas Anda (${tech.area}).`);
}
db.prepare("UPDATE tickets SET technician_id = ?, status = 'in_progress', updated_at = (NOW_LOCAL()) WHERE id = ?").run(validTechId, ticketId);
return validTechId;
}
function updateTicketStatus(ticketId, techId, status, extraData = {}) {
const { notes, photos, photoMetadata } = extraData;
const updates = ['status = ?', 'updated_at = (NOW_LOCAL())'];
const params = [status];
if (techId) {
updates.push('technician_id = COALESCE(technician_id, ?)');
params.push(techId);
}
if (notes !== undefined) {
updates.push('technician_notes = ?');
params.push(notes);
}
if (photos !== undefined) {
updates.push('photos = ?');
params.push(photos);
}
if (photoMetadata !== undefined) {
updates.push('photo_metadata = ?');
params.push(photoMetadata);
}
params.push(ticketId);
const sql = `UPDATE tickets SET ${updates.join(', ')} WHERE id = ?`;
db.prepare(sql).run(...params);
}
// Admin helper
function getAllTechnicians() {
return db.prepare('SELECT id, username, name, phone, area, is_active FROM technicians').all();
}
function createTechnician(data) {
const stmt = db.prepare('INSERT INTO technicians (username, password, name, phone, area) VALUES (?, ?, ?, ?, ?)');
return stmt.run(data.username, data.password, data.name, data.phone || '', data.area || '');
}
module.exports = {
authenticate,
getTechById,
getTechAreaWhere,
canTechAccessCustomer,
getTechStats,
getAssignedTickets,
getOpenTickets,
getResolvedTickets,
takeTicket,
updateTicketStatus,
getAllTechnicians,
createTechnician
};