Files
EmployeeManagementSystem/backend-deno/routes/attendance.ts
2025-11-28 19:04:35 +00:00

424 lines
14 KiB
TypeScript

import { Router } from "@oak/oak";
import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts";
const router = new Router();
// Get all attendance records
router.get("/", authenticateToken, async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const employeeId = params.get("employeeId");
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const status = 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<Attendance[]>(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<Attendance[]>(
`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<User[]>(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<Attendance[]>(
"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<Attendance[]>(
`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<Attendance[]>(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<Attendance[]>(
`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<Attendance[]>(
"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<Attendance[]>(
`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<Attendance[]>(
"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<Attendance[]>(
`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<Attendance[]>(
`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) => {
try {
const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams;
const startDate = params.get("startDate");
const endDate = params.get("endDate");
const departmentId = params.get("departmentId");
let query = `
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: unknown[] = [];
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;