Compare commits

...

2 Commits

2 changed files with 347 additions and 1 deletions

View File

@ -183,3 +183,337 @@ export const processDailyAttendance = async (ctx: Context) => {
amsConnection.release();
}
};
export const getEmployeeSummary = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const employeeId = params.get("employee_id");
const startDate = params.get("start_date");
const endDate = params.get("end_date");
// Basic validation
if (!employeeId || !startDate || !endDate) {
ctx.response.status = 400;
ctx.response.body = {
success: false,
message: "Missing required query parameters. Expected: employee_id, start_date, and end_date."
};
return;
}
try {
// Fetch the individual breakdown rows for the period
const [rows]: any = await amsDb.execute(
`SELECT
attendance_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours,
check_in_status,
final_status
FROM processed_daily_attendance
WHERE employee_id = ? AND work_date BETWEEN ? AND ?
ORDER BY work_date DESC`,
[employeeId, startDate, endDate]
);
// Dynamic metrics aggregation block
const summaryMetrics = rows.reduce(
(acc: any, row: any) => {
acc.total_days_tracked++;
acc.total_hours_worked += Number(row.worked_hours || 0);
// Count up categorical operational states
if (row.final_status === "FULL_DAY") acc.full_days++;
else if (row.final_status === "HALF_DAY") acc.half_days++;
else if (row.final_status === "ABSENT") acc.absences++;
else if (row.final_status === "MISPUNCH") acc.mispunches++;
if (row.check_in_status === "LATE") acc.late_arrivals++;
return acc;
},
{
total_days_tracked: 0,
total_hours_worked: 0,
full_days: 0,
half_days: 0,
absences: 0,
mispunches: 0,
late_arrivals: 0,
}
);
// Format final working duration to a neat 2-decimal scale
summaryMetrics.total_hours_worked = Math.round(summaryMetrics.total_hours_worked * 100) / 100;
ctx.response.status = 200;
ctx.response.body = {
success: true,
metrics: summaryMetrics,
history: rows
};
} catch (error) {
console.error("Failed to fetch employee summary:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getDailyReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const targetDate = params.get("date");
if (!targetDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing required 'date' query parameter (YYYY-MM-DD)." };
return;
}
try {
// 1. Grab all calculated attendance states for this date from AMS
const [attendanceRows]: any = await amsDb.execute(
`SELECT employee_id, attendance_id,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE work_date = ?`,
[targetDate]
);
// 2. Fetch active corporate names and metadata mappings from EMS
const [employeeRows]: any = await emsDb.execute(
`SELECT e.employee_id, e.employee_code, p.first_name, p.last_name
FROM employees e
INNER JOIN partners p ON e.partner_id = p.partner_id
WHERE e.is_active = TRUE`
);
// Map the attendance records by employee_id for O(1) matching speed
const attendanceMap = attendanceRows.reduce((acc: any, row: any) => {
acc[row.employee_id] = row;
return acc;
}, {});
// Summary metrics counter block for the Admin dashboard cards
const summary = {
total_active_workforce: employeeRows.length,
present: 0,
late: 0,
mispunches: 0,
absent: 0
};
// 3. Seamlessly blend employee profiles with attendance states
const roster = employeeRows.map((emp: any) => {
const attendance = attendanceMap[emp.employee_id];
let status = "ABSENT";
let check_in = null;
let check_out = null;
let worked_hours = "0.00";
let punctuality = "N/A";
let attendance_id = null;
if (attendance) {
status = attendance.final_status;
check_in = attendance.check_in;
check_out = attendance.check_out;
worked_hours = attendance.worked_hours;
punctuality = attendance.check_in_status;
attendance_id = attendance.attendance_id;
// Process summary counter evaluations
if (status === "FULL_DAY" || status === "HALF_DAY") summary.present++;
if (status === "MISPUNCH") summary.mispunches++;
if (punctuality === "LATE") summary.late++;
} else {
summary.absent++;
}
return {
employee_id: emp.employee_id,
employee_code: emp.employee_code,
full_name: `${emp.first_name} ${emp.last_name}`,
attendance_id,
status,
check_in,
check_out,
worked_hours,
punctuality
};
});
ctx.response.status = 200;
ctx.response.body = {
success: true,
date: targetDate,
summary,
roster
};
} catch (error) {
console.error("Failed to generate daily admin report:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getAdminRangeReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const fromDate = params.get("from_date");
const toDate = params.get("to_date");
const branchId = params.get("branch_id"); // Optional filter
const departmentId = params.get("department_id"); // Optional filter
if (!fromDate || !toDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing required date boundaries: from_date and to_date (YYYY-MM-DD)." };
return;
}
try {
// 1. Dynamically build the EMS query based on branch or department selection
let emsQuery = `
SELECT e.employee_id, e.employee_code, p.first_name, p.last_name, b.branch_name, d.name as department_name
FROM employees e
INNER JOIN partners p ON e.partner_id = p.partner_id
INNER JOIN branches b ON e.branch_id = b.branch_id
INNER JOIN contracts c ON e.employee_id = c.employee_id
INNER JOIN departments d ON c.department_id = d.department_id
WHERE e.is_active = TRUE AND c.status = 'ACTIVE'
`;
const emsParams: any[] = [];
if (branchId) {
emsQuery += ` AND e.branch_id = ?`;
emsParams.push(branchId);
}
if (departmentId) {
emsQuery += ` AND c.department_id = ?`;
emsParams.push(departmentId);
}
const [employees]: any = await emsDb.execute(emsQuery, emsParams);
if (employees.length === 0) {
ctx.response.status = 200;
ctx.response.body = { success: true, summary: {}, report: [] };
return;
}
const employeeIds = employees.map((e: any) => e.employee_id);
// 2. Query AMS for all processed ledger entries matching these exact employees in the date range
// Using a parameterized string hack for the IN clause since Deno MySQL handles arrays explicitly
const placeholders = employeeIds.map(() => "?").join(",");
const [attendanceRows]: any = await amsDb.execute(
`SELECT
employee_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE work_date BETWEEN ? AND ? AND employee_id IN (${placeholders})
ORDER BY work_date ASC`,
[fromDate, toDate, ...employeeIds]
);
// Group logs by employee_id for structural assembly
const attendanceGrouped = attendanceRows.reduce((acc: any, row: any) => {
if (!acc[row.employee_id]) acc[row.employee_id] = [];
acc[row.employee_id].push(row);
return acc;
}, {});
// Overall global counters for the Admin's upper summary ribbon card metrics
const globalSummary = { total_records_evaluated: attendanceRows.length, full_days: 0, half_days: 0, late_instances: 0, mispunches: 0 };
const report = employees.map((emp: any) => {
const logs = attendanceGrouped[emp.employee_id] || [];
// Calculate individual metrics summary for this specific employee across the range
const individualMetrics = logs.reduce((acc: any, log: any) => {
if (log.final_status === "FULL_DAY") { acc.full_days++; globalSummary.full_days++; }
else if (log.final_status === "HALF_DAY") { acc.half_days++; globalSummary.half_days++; }
else if (log.final_status === "MISPUNCH") { acc.mispunches++; globalSummary.mispunches++; }
if (log.check_in_status === "LATE") { acc.late_arrivals++; globalSummary.late_instances++; }
return acc;
}, { full_days: 0, half_days: 0, mispunches: 0, late_arrivals: 0 });
return {
...emp,
range_metrics: individualMetrics,
attendance_history: logs
};
});
ctx.response.status = 200;
ctx.response.body = { success: true, date_range: { from: fromDate, to: toDate }, global_summary: globalSummary, report };
} catch (error) {
console.error("Admin range report compilation failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};
export const getSingleEmployeeRangeReport = async (ctx: Context) => {
const params = ctx.request.url.searchParams;
const employeeId = params.get("employee_id");
const fromDate = params.get("from_date");
const toDate = params.get("to_date");
if (!employeeId || !fromDate || !toDate) {
ctx.response.status = 400;
ctx.response.body = { success: false, message: "Missing query parameters. Required: employee_id, from_date, to_date." };
return;
}
try {
// Grab profile data
const [empProfile]: any = await emsDb.execute(
`SELECT e.employee_id, e.employee_code, p.first_name, p.last_name
FROM employees e INNER JOIN partners p ON e.partner_id = p.partner_id WHERE e.employee_id = ?`,
[employeeId]
);
if (empProfile.length === 0) {
ctx.response.status = 404;
ctx.response.body = { success: false, message: "Employee profile not found." };
return;
}
// Grab attendance logs for date-to-date query window
const [logs]: any = await amsDb.execute(
`SELECT
attendance_id,
DATE_FORMAT(work_date, '%Y-%m-%d') as work_date,
DATE_FORMAT(check_in, '%H:%i:%s') as check_in,
DATE_FORMAT(check_out, '%H:%i:%s') as check_out,
worked_hours, check_in_status, final_status
FROM processed_daily_attendance
WHERE employee_id = ? AND work_date BETWEEN ? AND ?
ORDER BY work_date ASC`,
[employeeId, fromDate, toDate]
);
ctx.response.status = 200;
ctx.response.body = {
success: true,
employee: empProfile[0],
range: { from: fromDate, to: toDate },
total_days: logs.length,
history: logs
};
} catch (error) {
console.error("Single employee range query failed:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
};

View File

@ -1,5 +1,11 @@
import { Router } from "@oak/oak";
import { getRawLogs, processDailyAttendance } from "./controllers/attendance.controller.ts";
import {
getRawLogs,
processDailyAttendance,
getEmployeeSummary,
getDailyReport,
getAdminRangeReport,
getSingleEmployeeRangeReport} from "./controllers/attendance.controller.ts";
import {createRegularizationRequest, reviewRegularizationRequest} from "./controllers/regularization.controller.ts"
const router = new Router();
@ -11,6 +17,12 @@ apiV1.post("/attendance/process-daily", processDailyAttendance);
apiV1.post("/attendance/regularize", createRegularizationRequest);
apiV1.post("/attendance/regularize/review", reviewRegularizationRequest);
apiV1.get("/attendance/my-summary", getEmployeeSummary);
apiV1.get("/attendance/daily-report", getDailyReport);
apiV1.get("/attendance/admin-report", getAdminRangeReport);
apiV1.get("/attendance/employee-range-report", getSingleEmployeeRangeReport);
router.use("/api/v1", apiV1.routes(), apiV1.allowedMethods());
export default router;