From 04e25527e8f7afa4afeb91f6effc1ac53c524bd6 Mon Sep 17 00:00:00 2001 From: bereck-work Date: Sun, 21 Dec 2025 08:54:47 +0000 Subject: [PATCH] (Feat-Fix): Fixed the check-in, check-out system, added a contractor payment system. --- backend-deno/routes/attendance.ts | 28 +- backend-deno/routes/employee-swaps.ts | 4 +- src/App.tsx | 6 +- src/components/layout/Sidebar.tsx | 11 + src/pages/AttendancePage.tsx | 157 ++++--- src/pages/ContractorPaymentPage.tsx | 576 ++++++++++++++++++++++++++ src/pages/EmployeeSwapPage.tsx | 6 +- 7 files changed, 705 insertions(+), 83 deletions(-) create mode 100644 src/pages/ContractorPaymentPage.tsx diff --git a/backend-deno/routes/attendance.ts b/backend-deno/routes/attendance.ts index 6f65c50..bd89a34 100644 --- a/backend-deno/routes/attendance.ts +++ b/backend-deno/routes/attendance.ts @@ -138,34 +138,34 @@ router.post( return; } - // Verify employee exists - let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?"; - const employeeParams: unknown[] = [employeeId, "Employee"]; + // Verify user exists and is Employee or Contractor + let userQuery = "SELECT * FROM users WHERE id = ? AND role IN ('Employee', 'Contractor')"; + const userParams: unknown[] = [employeeId]; if (currentUser.role === "Supervisor") { - employeeQuery += " AND department_id = ?"; - employeeParams.push(currentUser.departmentId); + userQuery += " AND department_id = ?"; + userParams.push(currentUser.departmentId); } - const employees = await db.query(employeeQuery, employeeParams); + const users = await db.query(userQuery, userParams); - if (employees.length === 0) { + if (users.length === 0) { ctx.response.status = 403; ctx.response.body = { - error: "Employee not found or not in your department", + error: "User not found, not an Employee/Contractor, 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"], + // Check if there's an active check-in (not yet checked out) for today + const activeCheckIn = await db.query( + "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = 'CheckedIn'", + [employeeId, workDate], ); - if (existing.length > 0) { + if (activeCheckIn.length > 0) { ctx.response.status = 400; - ctx.response.body = { error: "Employee already checked in today" }; + ctx.response.body = { error: "User has an active check-in. Please check out first before checking in again." }; return; } diff --git a/backend-deno/routes/employee-swaps.ts b/backend-deno/routes/employee-swaps.ts index 1d84fa7..98c748f 100644 --- a/backend-deno/routes/employee-swaps.ts +++ b/backend-deno/routes/employee-swaps.ts @@ -221,9 +221,11 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { } // Update the employee's department and contractor + // If no target contractor specified, keep the original contractor + const newContractorId = targetContractorId || employee.contractor_id || null; const [updateResult] = await connection.execute( "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", - [targetDepartmentId, targetContractorId || null, employeeId], + [targetDepartmentId, newContractorId, employeeId], ); const affectedRows = diff --git a/src/App.tsx b/src/App.tsx index 5d345c2..b952b52 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { ReportingPage } from "./pages/ReportingPage.tsx"; import { StandardRatesPage } from "./pages/StandardRatesPage.tsx"; import { AllRatesPage } from "./pages/AllRatesPage.tsx"; import { ActivitiesPage } from "./pages/ActivitiesPage.tsx"; +import { ContractorPaymentPage } from "./pages/ContractorPaymentPage.tsx"; type PageType = | "dashboard" @@ -25,7 +26,8 @@ type PageType = | "reports" | "standard-rates" | "all-rates" - | "activities"; + | "activities" + | "contractor-payment"; const AppContent: React.FC = () => { const [activePage, setActivePage] = useState("dashboard"); @@ -53,6 +55,8 @@ const AppContent: React.FC = () => { return ; case "activities": return ; + case "contractor-payment": + return ; default: return ; } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 9179d36..b77be1a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -4,6 +4,7 @@ import { Briefcase, CalendarCheck, ClipboardList, + CreditCard, DollarSign, Eye, FileSpreadsheet, @@ -168,6 +169,16 @@ export const Sidebar: React.FC = ({ activePage, onNavigate }) => { onClick={() => onNavigate("activities")} /> )} + + {/* Contractor Payment Report - SuperAdmin only */} + {isSuperAdmin && ( + onNavigate("contractor-payment")} + /> + )} {/* Role indicator at bottom */} diff --git a/src/pages/AttendancePage.tsx b/src/pages/AttendancePage.tsx index cb8ef63..93f6a96 100644 --- a/src/pages/AttendancePage.tsx +++ b/src/pages/AttendancePage.tsx @@ -43,7 +43,6 @@ export const AttendancePage: React.FC = () => { new Date().toISOString().split("T")[0], ); const [checkInLoading, setCheckInLoading] = useState(false); - const [employeeStatus, setEmployeeStatus] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [sortField, setSortField] = useState<"date" | "employee" | "status">( "date", @@ -72,30 +71,15 @@ export const AttendancePage: React.FC = () => { fetchAttendance(); }, []); - // Check employee status when selected - useEffect(() => { - if (selectedEmployee && workDate) { - const record = attendance.find( - (a) => - a.employee_id === parseInt(selectedEmployee) && - a.work_date?.split("T")[0] === workDate, - ); - setEmployeeStatus(record || null); - } else { - setEmployeeStatus(null); - } - }, [selectedEmployee, workDate, attendance]); - const handleCheckIn = async () => { if (!selectedEmployee) { - alert("Please select an employee"); + alert("Please select an employee/contractor"); return; } setCheckInLoading(true); try { await api.checkIn(parseInt(selectedEmployee), workDate); await fetchAttendance(); - setEmployeeStatus({ status: "CheckedIn" }); } catch (err: any) { alert(err.message || "Failed to check in"); } finally { @@ -105,14 +89,13 @@ export const AttendancePage: React.FC = () => { const handleCheckOut = async () => { if (!selectedEmployee) { - alert("Please select an employee"); + alert("Please select an employee/contractor"); return; } setCheckInLoading(true); try { await api.checkOut(parseInt(selectedEmployee), workDate); await fetchAttendance(); - setEmployeeStatus({ status: "CheckedOut" }); } catch (err: any) { alert(err.message || "Failed to check out"); } finally { @@ -122,7 +105,7 @@ export const AttendancePage: React.FC = () => { const handleMarkAbsent = async () => { if (!selectedEmployee) { - alert("Please select an employee"); + alert("Please select an employee/contractor"); return; } setCheckInLoading(true); @@ -133,7 +116,6 @@ export const AttendancePage: React.FC = () => { "Marked absent by supervisor", ); await fetchAttendance(); - setEmployeeStatus({ status: "Absent" }); } catch (err: any) { alert(err.message || "Failed to mark absent"); } finally { @@ -166,14 +148,30 @@ export const AttendancePage: React.FC = () => { const canEditAttendance = user?.role === "SuperAdmin" || user?.role === "Supervisor"; - const employeeOptions = [ - { value: "", label: "Select Employee" }, - ...employees.filter((e) => e.role === "Employee").map((e) => ({ - value: String(e.id), - label: `${e.name} (${e.username})`, - })), + // Include both Employees and Contractors for attendance + const attendanceUserOptions = [ + { value: "", label: "Select Employee/Contractor" }, + ...employees + .filter((e) => e.role === "Employee" || e.role === "Contractor") + .map((e) => ({ + value: String(e.id), + label: `${e.name} (${e.role})`, + })), ]; + // Get all attendance records for selected user on selected date + const userDayRecords = useMemo(() => { + if (!selectedEmployee || !workDate) return []; + return attendance.filter( + (a) => + a.employee_id === parseInt(selectedEmployee) && + a.work_date?.split("T")[0] === workDate + ); + }, [selectedEmployee, workDate, attendance]); + + // Check if user has an active (not checked out) session + const hasActiveCheckIn = userDayRecords.some((r) => r.status === "CheckedIn"); + // Filter and sort attendance records const filteredAndSortedAttendance = useMemo(() => { let filtered = attendance; @@ -465,16 +463,16 @@ export const AttendancePage: React.FC = () => { Check In / Check Out Management

- Manage employee attendance + Manage employee and contractor attendance. Multiple check-ins per day are supported.

{
{selectedEmployee && ( -
- {employeeStatus?.status === "CheckedIn" - ? ( +
+ {/* Current Status */} +
0 + ? "bg-green-50 border-green-200" + : "bg-yellow-50 border-yellow-200" + }`} + > + {hasActiveCheckIn ? ( <>

- Employee is currently checked in. Check-in time: - {" "} - {employeeStatus.check_in_time - ? new Date(employeeStatus.check_in_time) - .toLocaleTimeString() - : "N/A"} + Currently checked in. Please check out before checking in again.

- ) - : employeeStatus?.status === "CheckedOut" - ? ( + ) : userDayRecords.length > 0 ? ( <>

- Employee has completed attendance for this date. + {userDayRecords.length} attendance record(s) for this date. Can check in again.

- ) - : ( + ) : ( <>

- Employee has not checked in for this date + No attendance records for this date

)} +
+ + {/* Today's Records */} + {userDayRecords.length > 0 && ( +
+

+ Today's Sessions ({userDayRecords.length}) +

+
+ {userDayRecords.map((record, idx) => ( +
+ Session {idx + 1} +
+ + In: {record.check_in_time + ? new Date(record.check_in_time).toLocaleTimeString() + : "-"} + + + Out: {record.check_out_time + ? new Date(record.check_out_time).toLocaleTimeString() + : "-"} + + + {record.status === "CheckedOut" ? "Completed" : record.status} + +
+
+ ))} +
+
+ )}
)} @@ -541,20 +575,16 @@ export const AttendancePage: React.FC = () => { + +
+
+ + + + {/* Filters */} +
+
+
+ + setCompanyName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + /> +
+
+ + +
+
+ + setStartDate(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setEndDate(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + /> +
+
+
+ +
+
+ + {/* Report Table */} + {reportData.length > 0 && ( +
+
+ {/* Report Header */} +
+

{companyName}

+

{formatDateRange()}

+

ALL CONTRACTOR PAYMENT SHEET

+
+ + {/* Table */} + + + + + + + + + + + + + + + + + + + + + + + + {reportData.map((subDept, subDeptIdx) => ( + + {/* Sub-department header */} + + + + + {/* Contractors */} + {subDept.contractors.map((contractor, idx) => { + const rowNum = reportData + .slice(0, subDeptIdx) + .reduce((acc, sd) => acc + sd.contractors.length, 0) + idx + 1; + + return ( + + + + + + + + + + + + + + + + + ); + })} + + {/* Sub Total */} + + + + + + + + + + + + + + + + + + ))} + + {/* Grand Total */} + + + + + + + + + + + + + + + + + +
S.NONAME CONTRACTORAs per DAFPLTDS @1%
(BASE AMOUNT)
Payable
(Before deduction)
SECURITY
DEDUCTION 0.35%
ADVANCEFinal PayableExcess/Short
ALLDANATUKDIGROUNDNUTCommission
& Salary
Total
+ {subDept.sub_department_name.toUpperCase()} +
{rowNum}{contractor.contractor_name}{contractor.as_per_contractor || ""}{contractor.dana || ""}{contractor.tukdi || ""}{contractor.groundnut || ""}{contractor.commission_salary || ""}{contractor.total || ""}{contractor.tds_base_amount || ""}{contractor.payable_before_deduction || ""}{contractor.security_deduction || ""}{contractor.advance || ""}{contractor.final_payable || ""}{contractor.excess_short || ""}
Sub Total{subDept.subTotal.as_per_contractor}{subDept.subTotal.dana}{subDept.subTotal.tukdi}{subDept.subTotal.groundnut}{subDept.subTotal.commission_salary}{subDept.subTotal.total}{subDept.subTotal.tds_base_amount}{subDept.subTotal.payable_before_deduction}{subDept.subTotal.security_deduction}{subDept.subTotal.advance}{subDept.subTotal.final_payable}{subDept.subTotal.excess_short}
GRAND TOTAL{grandTotal.as_per_contractor}{grandTotal.dana}{grandTotal.tukdi}{grandTotal.groundnut}{grandTotal.commission_salary}{grandTotal.total}{grandTotal.tds_base_amount}{grandTotal.payable_before_deduction}{grandTotal.security_deduction}{grandTotal.advance}{grandTotal.final_payable}{grandTotal.excess_short}
+ + {/* Footer */} +
+
+
CONTRACTOR
+
+
+
CHECKER
+
+
+
AUTHORISED AUTHORITY
+
+
+
+
+ )} + + {reportData.length === 0 && !loading && ( +
+ +

Select a department and date range, then click "Generate Report"

+
+ )} +
+ + + {/* Print Styles */} + + + ); +}; diff --git a/src/pages/EmployeeSwapPage.tsx b/src/pages/EmployeeSwapPage.tsx index befc3ec..2c4cb52 100644 --- a/src/pages/EmployeeSwapPage.tsx +++ b/src/pages/EmployeeSwapPage.tsx @@ -571,7 +571,9 @@ export const EmployeeSwapPage: React.FC = () => { targetContractorId: e.target.value, })} options={[ - { value: "", label: "No contractor" }, + { value: "", label: selectedEmployee?.contractor_name + ? `Keep Original (${selectedEmployee.contractor_name})` + : "Keep Original Contractor" }, ...targetContractors.map((c) => ({ value: String(c.id), label: c.name, @@ -669,7 +671,7 @@ export const EmployeeSwapPage: React.FC = () => {