Files
smanab/absensi-siswa/backend/routes/leaveRequests.js

294 lines
12 KiB
JavaScript
Executable File

// 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;