// Leave Requests API Routes - Pengajuan Izin/Sakit Siswa import express from 'express'; import pool from '../db.js'; import crypto from 'crypto'; import { addToQueuePriority } from '../services/fonnteQueue.js'; const router = express.Router(); // Send WhatsApp Notification to Guru BK via Queue (prevents mass sending) const sendWhatsAppToGuruBK = async (message) => { try { // Get all active Guru BK with phone numbers const [guruBKList] = await pool.query( "SELECT name, phone FROM staff_users WHERE role = 'GURU_BK' AND is_active = TRUE AND phone IS NOT NULL AND phone != ''" ); if (guruBKList.length === 0) { console.log('📱 No Guru BK with phone number found'); return; } // Get Fonnte token from settings const [settings] = await pool.query( "SELECT setting_value FROM settings WHERE setting_key = 'FONNTE_TOKEN'" ); const fonnteToken = settings.length > 0 ? settings[0].setting_value : process.env.FONNTE_TOKEN; if (!fonnteToken) { console.log('📱 Fonnte token not configured'); return; } // Add all messages to queue with HIGH priority (queue handles the delays) for (const guru of guruBKList) { addToQueuePriority(guru.phone, message, fonnteToken); console.log(`📱 ⚡ HIGH PRIORITY: Queued WhatsApp for Guru BK ${guru.name}: ${guru.phone}`); } } catch (error) { console.error('📱 WhatsApp notification error:', error.message); } }; // GET /api/leave-requests - Get leave requests router.get('/', async (req, res) => { try { const { status, studentId } = req.query; let query = 'SELECT * FROM leave_requests'; const params = []; const conditions = []; if (status) { conditions.push('status = ?'); params.push(status); } if (studentId) { conditions.push('student_id = ?'); params.push(studentId); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY created_at DESC'; const [rows] = await pool.query(query, params); // Map to frontend format const requests = rows.map(row => ({ id: row.id, studentId: row.student_id, studentName: row.student_name, studentNis: row.student_nis, studentClass: row.student_class, requestType: row.request_type, requestDate: row.request_date, reason: row.reason, photoEvidence: row.photo_evidence, status: row.status, reviewedBy: row.reviewed_by, reviewedByName: row.reviewed_by_name, reviewedAt: row.reviewed_at, rejectionReason: row.rejection_reason, createdAt: row.created_at })); res.json(requests); } catch (error) { console.error('GET /api/leave-requests Error:', error); res.status(500).json({ error: error.message }); } }); // GET /api/leave-requests/pending-count - Get pending count for notification badge router.get('/pending-count', async (req, res) => { try { const [rows] = await pool.query( "SELECT COUNT(*) as count FROM leave_requests WHERE status = 'PENDING'" ); res.json({ count: rows[0].count }); } catch (error) { console.error('GET /api/leave-requests/pending-count Error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/leave-requests - Submit new leave request (by student) router.post('/', async (req, res) => { try { const { studentId, studentName, studentNis, studentClass, requestType, requestDate, reason, photoEvidence } = req.body; if (!studentId || !requestType || !requestDate || !reason) { return res.status(400).json({ error: 'Data tidak lengkap. Wajib: tipe, tanggal, alasan.' }); } const id = crypto.randomUUID(); await pool.query( `INSERT INTO leave_requests (id, student_id, student_name, student_nis, student_class, request_type, request_date, reason, photo_evidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, studentId, studentName, studentNis, studentClass, requestType, requestDate, reason, photoEvidence || null] ); console.log(`📝 New leave request from ${studentName}: ${requestType} for ${requestDate}`); // Send WhatsApp notification to all Guru BK const message = `*🔔 Pengajuan ${requestType} Baru*\n\n` + `Siswa: *${studentName}*\n` + `NIS: ${studentNis || '-'}\n` + `Kelas: ${studentClass || '-'}\n` + `Tanggal: ${requestDate}\n` + `Alasan: ${reason}\n\n` + `Silakan buka aplikasi Absensi untuk menyetujui atau menolak pengajuan ini.`; sendWhatsAppToGuruBK(message); res.json({ success: true, id, message: 'Pengajuan berhasil dikirim.' }); } catch (error) { console.error('POST /api/leave-requests Error:', error); res.status(500).json({ error: error.message }); } }); // PUT /api/leave-requests/:id/approve - Approve leave request (by Guru BK) router.put('/:id/approve', async (req, res) => { try { const { id } = req.params; const { reviewerId, reviewerName } = req.body; // Get the request first const [rows] = await pool.query('SELECT * FROM leave_requests WHERE id = ?', [id]); if (rows.length === 0) { return res.status(404).json({ error: 'Pengajuan tidak ditemukan.' }); } const request = rows[0]; // Update leave request status await pool.query( `UPDATE leave_requests SET status = 'APPROVED', reviewed_by = ?, reviewed_by_name = ?, reviewed_at = NOW() WHERE id = ?`, [reviewerId, reviewerName, id] ); // Also update attendance record if exists for this date await pool.query( `UPDATE attendance SET status = ? WHERE user_id = ? AND date_str = ? AND status = 'ALFA'`, [request.request_type, request.student_id, request.request_date] ); // If no attendance record, create one const [existingAttendance] = await pool.query( `SELECT id FROM attendance WHERE user_id = ? AND date_str = ?`, [request.student_id, request.request_date] ); if (existingAttendance.length === 0) { const attendanceId = crypto.randomUUID(); await pool.query( `INSERT INTO attendance (id, user_id, user_name, nis, class_name, timestamp, date_str, time_str, status, ai_verification) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [attendanceId, request.student_id, request.student_name, request.student_nis, request.student_class, Date.now(), request.request_date, '00:00', request.request_type, `Disetujui oleh ${reviewerName}`] ); } console.log(`✅ Leave request ${id} APPROVED by ${reviewerName}`); // Send WhatsApp notification to student/parent try { const [attendanceRows] = await pool.query( "SELECT parent_phone FROM attendance WHERE user_id = ? AND parent_phone IS NOT NULL AND parent_phone != '' ORDER BY timestamp DESC LIMIT 1", [request.student_id] ); const studentPhone = attendanceRows.length > 0 ? attendanceRows[0].parent_phone : null; if (studentPhone) { const [settings] = await pool.query("SELECT setting_value FROM settings WHERE setting_key = 'FONNTE_TOKEN'"); const fonnteToken = settings.length > 0 ? settings[0].setting_value : process.env.FONNTE_TOKEN; if (fonnteToken) { const message = `*✅ Pengajuan ${request.request_type} Disetujui*\n\n` + `Halo *${request.student_name}*,\n` + `Pengajuan ${request.request_type} Anda untuk tanggal *${request.request_date}* telah *DISETUJUI* oleh ${reviewerName}.\n\n` + `Terima kasih.`; addToQueuePriority(studentPhone, message, fonnteToken); console.log(`📱 Notified student ${request.student_name} about approval`); } } } catch (waErr) { console.error('📱 Approval WA notification error:', waErr.message); } res.json({ success: true, message: 'Pengajuan disetujui.' }); } catch (error) { console.error('PUT /api/leave-requests/:id/approve Error:', error); res.status(500).json({ error: error.message }); } }); // PUT /api/leave-requests/:id/reject - Reject leave request (by Guru BK) router.put('/:id/reject', async (req, res) => { try { const { id } = req.params; const { reviewerId, reviewerName, rejectionReason } = req.body; if (!rejectionReason) { return res.status(400).json({ error: 'Alasan penolakan wajib diisi.' }); } await pool.query( `UPDATE leave_requests SET status = 'REJECTED', reviewed_by = ?, reviewed_by_name = ?, reviewed_at = NOW(), rejection_reason = ? WHERE id = ?`, [reviewerId, reviewerName, rejectionReason, id] ); console.log(`❌ Leave request ${id} REJECTED by ${reviewerName}: ${rejectionReason}`); // Send WhatsApp notification to student/parent try { // Get the request again to get student info const [reqRows] = await pool.query('SELECT * FROM leave_requests WHERE id = ?', [id]); if (reqRows.length > 0) { const request = reqRows[0]; const [attendanceRows] = await pool.query( "SELECT parent_phone FROM attendance WHERE user_id = ? AND parent_phone IS NOT NULL AND parent_phone != '' ORDER BY timestamp DESC LIMIT 1", [request.student_id] ); const studentPhone = attendanceRows.length > 0 ? attendanceRows[0].parent_phone : null; if (studentPhone) { const [settings] = await pool.query("SELECT setting_value FROM settings WHERE setting_key = 'FONNTE_TOKEN'"); const fonnteToken = settings.length > 0 ? settings[0].setting_value : process.env.FONNTE_TOKEN; if (fonnteToken) { const message = `*❌ Pengajuan ${request.request_type} Ditolak*\n\n` + `Halo *${request.student_name}*,\n` + `Mohon maaf, pengajuan ${request.request_type} Anda untuk tanggal *${request.request_date}* telah *DITOLAK* oleh ${reviewerName}.\n\n` + `*Alasan:* ${rejectionReason}\n\n` + `Silakan hubungi Guru BK jika ada pertanyaan.`; addToQueuePriority(studentPhone, message, fonnteToken); console.log(`📱 Notified student ${request.student_name} about rejection`); } } } } catch (waErr) { console.error('📱 Rejection WA notification error:', waErr.message); } res.json({ success: true, message: 'Pengajuan ditolak.' }); } catch (error) { console.error('PUT /api/leave-requests/:id/reject Error:', error); res.status(500).json({ error: error.message }); } }); // DELETE /api/leave-requests/:id - Delete leave request router.delete('/:id', async (req, res) => { try { const { id } = req.params; await pool.query('DELETE FROM leave_requests WHERE id = ?', [id]); res.json({ success: true, message: 'Pengajuan dihapus.' }); } catch (error) { console.error('DELETE /api/leave-requests Error:', error); res.status(500).json({ error: error.message }); } }); export default router;