294 lines
12 KiB
JavaScript
Executable File
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;
|