import { Router, type RouterContext, type State } from "@oak/oak"; import { db } from "../config/database.ts"; import { authenticateToken, authorize, getCurrentUser, } from "../middleware/auth.ts"; import type { Attendance, AttendanceStatus, CheckInOutRequest, JWTPayload, UpdateAttendanceStatusRequest, User, } from "../types/index.ts"; const router = new Router(); // Get all attendance records router.get( "/", authenticateToken, async ( ctx: RouterContext<"/", Record, State>, ) => { try { const currentUser: JWTPayload = getCurrentUser(ctx); const params: URLSearchParams = ctx.request.url.searchParams; const employeeId: string | null = params.get("employeeId"); const startDate: string | null = params.get("startDate"); const endDate: string | null = params.get("endDate"); const status: string | null = params.get("status"); let query = ` SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE 1=1 `; const queryParams: unknown[] = []; // Role-based filtering if (currentUser.role === "Supervisor") { query += " AND a.supervisor_id = ?"; queryParams.push(currentUser.id); } else if (currentUser.role === "Employee") { query += " AND a.employee_id = ?"; queryParams.push(currentUser.id); } if (employeeId) { query += " AND a.employee_id = ?"; queryParams.push(employeeId); } if (startDate) { query += " AND a.work_date >= ?"; queryParams.push(startDate); } if (endDate) { query += " AND a.work_date <= ?"; queryParams.push(endDate); } if (status) { query += " AND a.status = ?"; queryParams.push(status); } query += " ORDER BY a.work_date DESC, a.check_in_time DESC"; const records = await db.query(query, queryParams); ctx.response.body = records; } catch (error) { console.error("Get attendance error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); // Get attendance by ID router.get("/:id", authenticateToken, async (ctx) => { try { const attendanceId = ctx.params.id; const records = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [attendanceId], ); if (records.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Attendance record not found" }; return; } ctx.response.body = records[0]; } catch (error) { console.error("Get attendance error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }); // Check in employee (Supervisor or SuperAdmin) router.post( "/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as CheckInOutRequest; const { employeeId, workDate } = body; if (!employeeId || !workDate) { ctx.response.status = 400; ctx.response.body = { error: "Employee ID and work date required" }; return; } // Verify employee exists let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?"; const employeeParams: unknown[] = [employeeId, "Employee"]; if (currentUser.role === "Supervisor") { employeeQuery += " AND department_id = ?"; employeeParams.push(currentUser.departmentId); } const employees = await db.query(employeeQuery, employeeParams); if (employees.length === 0) { ctx.response.status = 403; ctx.response.body = { error: "Employee not found or not in your department", }; return; } // Check if already checked in today const existing = await db.query( "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?", [employeeId, workDate, "CheckedIn"], ); if (existing.length > 0) { ctx.response.status = 400; ctx.response.body = { error: "Employee already checked in today" }; return; } const checkInTime = new Date().toISOString().slice(0, 19).replace( "T", " ", ); const result = await db.execute( "INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)", [employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"], ); const newRecord = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [result.insertId], ); ctx.response.status = 201; ctx.response.body = newRecord[0]; } catch (error) { console.error("Check in error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); // Check out employee (Supervisor or SuperAdmin) router.post( "/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json() as CheckInOutRequest; const { employeeId, workDate } = body; if (!employeeId || !workDate) { ctx.response.status = 400; ctx.response.body = { error: "Employee ID and work date required" }; return; } // Find the check-in record let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?"; const params: unknown[] = [employeeId, workDate, "CheckedIn"]; if (currentUser.role === "Supervisor") { query += " AND supervisor_id = ?"; params.push(currentUser.id); } const records = await db.query(query, params); if (records.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "No check-in record found for today" }; return; } const checkOutTime = new Date().toISOString().slice(0, 19).replace( "T", " ", ); await db.execute( "UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?", [checkOutTime, "CheckedOut", records[0].id], ); const updatedRecord = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [records[0].id], ); ctx.response.body = updatedRecord[0]; } catch (error) { console.error("Check out error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); // Update attendance status (mark as Absent, HalfDay, Late) router.put( "/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { try { const attendanceId = ctx.params.id; const body = await ctx.request.body .json() as UpdateAttendanceStatusRequest; const { status, remark } = body; // Validate status const validStatuses: AttendanceStatus[] = [ "CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late", ]; if (!validStatuses.includes(status)) { ctx.response.status = 400; ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late", }; return; } // Check if record exists const existing = await db.query( "SELECT * FROM attendance WHERE id = ?", [attendanceId], ); if (existing.length === 0) { ctx.response.status = 404; ctx.response.body = { error: "Attendance record not found" }; return; } // Update the status await db.execute( "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", [status, remark || null, attendanceId], ); const updatedRecord = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [attendanceId], ); ctx.response.body = updatedRecord[0]; } catch (error) { console.error("Update attendance status error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); // Mark employee as absent (create absent record) router.post( "/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { try { const currentUser = getCurrentUser(ctx); const body = await ctx.request.body.json(); const { employeeId, workDate, remark } = body; if (!employeeId || !workDate) { ctx.response.status = 400; ctx.response.body = { error: "Employee ID and work date required" }; return; } // Check if record already exists for this date const existing = await db.query( "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?", [employeeId, workDate], ); if (existing.length > 0) { // Update existing record to Absent await db.execute( "UPDATE attendance SET status = ?, remark = ? WHERE id = ?", ["Absent", remark || "Marked absent", existing[0].id], ); const updatedRecord = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [existing[0].id], ); ctx.response.body = updatedRecord[0]; } else { // Create new absent record const result = await db.execute( "INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)", [ employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent", ], ); const newRecord = await db.query( `SELECT a.*, e.name as employee_name, e.username as employee_username, s.name as supervisor_name, d.name as department_name, c.name as contractor_name FROM attendance a JOIN users e ON a.employee_id = e.id JOIN users s ON a.supervisor_id = s.id LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN users c ON e.contractor_id = c.id WHERE a.id = ?`, [result.insertId], ); ctx.response.status = 201; ctx.response.body = newRecord[0]; } } catch (error) { console.error("Mark absent error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); // Get attendance summary router.get( "/summary/stats", authenticateToken, async ( ctx: RouterContext< "/summary/stats", Record, State >, ) => { try { const currentUser: JWTPayload = getCurrentUser(ctx); const params: URLSearchParams = ctx.request.url.searchParams; const startDate: string | null = params.get("startDate"); const endDate: string | null = params.get("endDate"); const departmentId: string | null = params.get("departmentId"); let query: string = ` SELECT COUNT(DISTINCT a.employee_id) as total_employees, COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in, COUNT(DISTINCT CASE WHEN a.status = 'CheckedOut' THEN a.employee_id END) as checked_out, d.name as department_name FROM attendance a JOIN users e ON a.employee_id = e.id LEFT JOIN departments d ON e.department_id = d.id WHERE 1=1 `; const queryParams: (number | string)[] = []; if (currentUser.role === "Supervisor") { query += " AND a.supervisor_id = ?"; queryParams.push(currentUser.id); } if (startDate) { query += " AND a.work_date >= ?"; queryParams.push(startDate); } if (endDate) { query += " AND a.work_date <= ?"; queryParams.push(endDate); } if (departmentId) { query += " AND e.department_id = ?"; queryParams.push(departmentId); } query += " GROUP BY d.id, d.name"; const summary = await db.query(query, queryParams); ctx.response.body = summary; } catch (error) { console.error("Get attendance summary error:", error); ctx.response.status = 500; ctx.response.body = { error: "Internal server error" }; } }, ); export default router;