Compare commits
2 Commits
52b2e40762
...
6187ec4afc
| Author | SHA1 | Date | |
|---|---|---|---|
| 6187ec4afc | |||
| 1355f2e9cb |
@ -182,4 +182,338 @@ export const processDailyAttendance = async (ctx: Context) => {
|
||||
} finally {
|
||||
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" };
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user