diff --git a/backend-deno/routes/attendance.ts b/backend-deno/routes/attendance.ts index 8f7dcf8..ca43da3 100644 --- a/backend-deno/routes/attendance.ts +++ b/backend-deno/routes/attendance.ts @@ -36,12 +36,20 @@ router.get( 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 + COALESCE(oc.name, c.name) as contractor_name, + es.id as active_swap_id, + es.original_contractor_id, + es.target_contractor_id, + oc.name as original_contractor_name, + tc.name as target_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 + LEFT JOIN employee_swaps es ON es.employee_id = e.id AND es.status = 'Active' AND es.swap_date <= a.work_date + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id WHERE 1=1 `; const queryParams: unknown[] = []; @@ -97,12 +105,18 @@ router.get("/:id", authenticateToken, async (ctx) => { 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 + COALESCE(oc.name, c.name) as contractor_name, + es.id as active_swap_id, + oc.name as original_contractor_name, + tc.name as target_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 + LEFT JOIN employee_swaps es ON es.employee_id = e.id AND es.status = 'Active' AND es.swap_date <= a.work_date + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id WHERE a.id = ?`, [attendanceId], ); diff --git a/backend-deno/routes/reports.ts b/backend-deno/routes/reports.ts index 87488b7..2f02d08 100644 --- a/backend-deno/routes/reports.ts +++ b/backend-deno/routes/reports.ts @@ -29,11 +29,14 @@ router.get( e.name as employee_name, e.username as employee_username, e.phone_number as employee_phone, s.name as supervisor_name, - c.name as contractor_name, + COALESCE(oc.name, c.name) as contractor_name, sd.name as sub_department_name, d.name as department_name, d.id as department_id, - sr.rate as standard_rate + sr.rate as standard_rate, + es.id as active_swap_id, + oc.name as original_contractor_name, + tc.name as target_contractor_name FROM work_allocations wa JOIN users e ON wa.employee_id = e.id JOIN users s ON wa.supervisor_id = s.id @@ -42,6 +45,9 @@ router.get( LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN standard_rates sr ON wa.sub_department_id = sr.sub_department_id AND wa.activity = sr.activity + LEFT JOIN employee_swaps es ON es.employee_id = e.id AND es.status = 'Active' AND es.swap_date <= wa.assigned_date + LEFT JOIN users oc ON es.original_contractor_id = oc.id + LEFT JOIN users tc ON es.target_contractor_id = tc.id WHERE wa.status = 'Completed' `; const queryParams: unknown[] = []; diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index a077238..91c23de 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -76,6 +76,13 @@ export const DashboardPage: React.FC = () => { const [contractorRates, setContractorRates] = useState< Record >({}); + const [activeSwaps, setActiveSwaps] = useState<{ + employee_id: number; + original_department_id: number; + target_department_id: number; + original_contractor_id?: number; + target_contractor_id?: number; + }[]>([]); const refreshAllData = () => { refreshEmployees(); @@ -84,6 +91,12 @@ export const DashboardPage: React.FC = () => { api.getAttendance({ startDate: today, endDate: today }) .then(setAttendance) .catch(console.error); + // Refresh active swaps for SuperAdmin + if (isSuperAdmin) { + api.getEmployeeSwaps({ status: "Active" }) + .then(setActiveSwaps) + .catch(console.error); + } }; const isSuperAdmin = user?.role === "SuperAdmin"; @@ -102,6 +115,15 @@ export const DashboardPage: React.FC = () => { .catch(console.error); }, []); + // Fetch active swaps for SuperAdmin + useEffect(() => { + if (isSuperAdmin) { + api.getEmployeeSwaps({ status: "Active" }) + .then(setActiveSwaps) + .catch(console.error); + } + }, [isSuperAdmin]); + // Fetch contractor rates for supervisor view useEffect(() => { if (isSupervisor) { @@ -213,6 +235,26 @@ export const DashboardPage: React.FC = () => { const supervisors = employees.filter((e) => e.role === "Supervisor"); + // Helper function to get the effective contractor ID for an employee + // If employee has an active swap, return the original contractor ID + const getEffectiveContractorId = (employeeId: number, currentContractorId?: number) => { + const swap = activeSwaps.find((s) => s.employee_id === employeeId); + if (swap && swap.original_contractor_id) { + return swap.original_contractor_id; + } + return currentContractorId; + }; + + // Helper function to get the effective department ID for an employee + // If employee has an active swap, return the original department ID + const getEffectiveDepartmentId = (employeeId: number, currentDepartmentId?: number) => { + const swap = activeSwaps.find((s) => s.employee_id === employeeId); + if (swap) { + return swap.original_department_id; + } + return currentDepartmentId; + }; + return supervisors.map((supervisor) => { const deptContractors = employees.filter( (e) => @@ -221,55 +263,114 @@ export const DashboardPage: React.FC = () => { ); // Get employees without a contractor but in this department (e.g., swapped employees) + // But exclude employees who have an active swap with an original contractor const unassignedEmployees = employees.filter( (e) => e.role === "Employee" && - e.department_id === supervisor.department_id && - !e.contractor_id, + getEffectiveDepartmentId(e.id, e.department_id) === supervisor.department_id && + !e.contractor_id && + !activeSwaps.find((s) => s.employee_id === e.id && s.original_contractor_id), ); const contractorNodes = deptContractors.map((contractor) => { + // Include employees whose original contractor (from swap) is this one + // OR whose current contractor is this one AND they're not swapped away const contractorEmployees = employees.filter( - (e) => e.role === "Employee" && e.contractor_id === contractor.id, + (e) => e.role === "Employee" && getEffectiveContractorId(e.id, e.contractor_id) === contractor.id, ); + // Find employees swapped INTO this contractor (target_contractor_id matches) + const swappedInEmployees = activeSwaps + .filter((s) => s.target_contractor_id === contractor.id) + .map((swap) => employees.find((e) => e.id === swap.employee_id)) + .filter((e): e is NonNullable => e !== undefined); + + // Build children: regular employees + swapped-in employees as nested + const employeeNodes = contractorEmployees.map((emp) => { + const empAttendance = attendance.find((a) => + a.employee_id === emp.id + ); + const empAllocation = allocations.find((a) => + a.employee_id === emp.id && a.status !== "Completed" + ); + const empSwap = activeSwaps.find((s) => s.employee_id === emp.id); + + // If employee is swapped OUT, show swap info in activity + let activityText = empAllocation?.description || empAllocation?.activity || "-"; + if (empSwap) { + // Find target contractor/department name + const targetContractor = employees.find((e) => e.id === empSwap.target_contractor_id); + const targetDept = departments.find((d) => d.id === empSwap.target_department_id); + activityText = `Swapped to ${targetContractor?.name || targetDept?.name || "another dept"}`; + } + + // Swapped-in employees are added at contractor level, not as children of individual employees + const swappedInChildren: HierarchyNode[] = []; + + return { + id: emp.id, + name: emp.name, + role: "Employee", + department: emp.department_name || "", + subDepartment: empAllocation?.sub_department_name || "-", + activity: activityText, + status: empAttendance + ? (empAttendance.status === "CheckedIn" || + empAttendance.status === "CheckedOut" + ? "Present" + : "Absent") + : undefined, + inTime: empAttendance?.check_in_time + ? new Date(empAttendance.check_in_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : undefined, + outTime: empAttendance?.check_out_time + ? new Date(empAttendance.check_out_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : undefined, + remark: empAttendance?.remark, + children: swappedInChildren, + }; + }); + + // Add swapped-in employees as a separate group under this contractor + const swappedInNodes = swappedInEmployees.map((emp) => { + const empAttendance = attendance.find((a) => a.employee_id === emp.id); + const empAllocation = allocations.find((a) => + a.employee_id === emp.id && a.status !== "Completed" + ); + const empSwap = activeSwaps.find((s) => s.employee_id === emp.id); + const originalContractor = employees.find((e) => e.id === empSwap?.original_contractor_id); + const originalDept = departments.find((d) => d.id === empSwap?.original_department_id); + + return { + id: emp.id, + name: `${emp.name} (from ${originalContractor?.name || originalDept?.name || "other"})`, + role: "Employee", + department: emp.department_name || "", + subDepartment: empAllocation?.sub_department_name || "-", + activity: empAllocation?.description || empAllocation?.activity || "-", + status: empAttendance + ? (empAttendance.status === "CheckedIn" || + empAttendance.status === "CheckedOut" + ? "Present" + : "Absent") + : undefined, + inTime: empAttendance?.check_in_time + ? new Date(empAttendance.check_in_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : undefined, + outTime: empAttendance?.check_out_time + ? new Date(empAttendance.check_out_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : undefined, + remark: empAttendance?.remark, + children: [], + }; + }); + return { id: contractor.id, name: contractor.name, role: "Contractor", department: contractor.department_name || "", - children: contractorEmployees.map((emp) => { - const empAttendance = attendance.find((a) => - a.employee_id === emp.id - ); - const empAllocation = allocations.find((a) => - a.employee_id === emp.id && a.status !== "Completed" - ); - - return { - id: emp.id, - name: emp.name, - role: "Employee", - department: emp.department_name || "", - subDepartment: empAllocation?.sub_department_name || "-", - activity: empAllocation?.description || empAllocation?.activity || - "-", - status: empAttendance - ? (empAttendance.status === "CheckedIn" || - empAttendance.status === "CheckedOut" - ? "Present" - : "Absent") - : undefined, - inTime: empAttendance?.check_in_time - ? new Date(empAttendance.check_in_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : undefined, - outTime: empAttendance?.check_out_time - ? new Date(empAttendance.check_out_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : undefined, - remark: empAttendance?.remark, - children: [], - }; - }), + children: [...employeeNodes, ...swappedInNodes], }; }); @@ -325,7 +426,7 @@ export const DashboardPage: React.FC = () => { return supervisorNode; }); - }, [isSuperAdmin, employees, attendance, allocations]); + }, [isSuperAdmin, employees, attendance, allocations, activeSwaps, departments]); // Build hierarchy data for Supervisor view (department-specific) const supervisorHierarchyData = useMemo(() => {