Compare commits

...

5 Commits

16 changed files with 2556 additions and 99 deletions

View File

@@ -6,7 +6,7 @@ DB_NAME=work_allocation
DB_PORT=3307
# JWT Configuration - CHANGE IN PRODUCTION!
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024
JWT_SECRET=VwoPOwDth7aqCSlrbf/qYYY0upLu0kRuTcuM71ionJI=
JWT_EXPIRES_IN=7d
# Server Configuration

View File

@@ -9,11 +9,27 @@
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"mysql2": "npm:mysql2@^3.11.0",
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts"
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts",
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18"
},
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
"strictNullChecks": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": false
}
}

View File

@@ -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],
);
@@ -157,27 +171,43 @@ router.post(
return;
}
// Check if there's an active check-in (not yet checked out) for today
const activeCheckIn = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = 'CheckedIn'",
// Check if there's any existing attendance record for today
const existingRecord = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
[employeeId, workDate],
);
if (activeCheckIn.length > 0) {
ctx.response.status = 400;
ctx.response.body = { error: "User has an active check-in. Please check out first before checking in again." };
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"],
);
let recordId: number;
if (existingRecord.length > 0) {
const record = existingRecord[0];
// If already checked in, don't allow another check-in
if (record.status === "CheckedIn") {
ctx.response.status = 400;
ctx.response.body = { error: "User has an active check-in. Please check out first before checking in again." };
return;
}
// If checked out or other status, update the existing record to check in again
await db.execute(
"UPDATE attendance SET check_in_time = ?, check_out_time = NULL, status = ?, supervisor_id = ? WHERE id = ?",
[checkInTime, "CheckedIn", currentUser.id, record.id],
);
recordId = record.id;
} else {
// No existing record, create new one
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"],
);
recordId = result.insertId as number;
}
const newRecord = await db.query<Attendance[]>(
`SELECT a.*,
@@ -191,10 +221,10 @@ router.post(
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],
[recordId],
);
ctx.response.status = 201;
ctx.response.status = existingRecord.length > 0 ? 200 : 201;
ctx.response.body = newRecord[0];
} catch (error) {
console.error("Check in error:", error);

View File

@@ -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[] = [];
@@ -63,9 +69,9 @@ router.get(
queryParams.push(endDate);
}
// Department filter (for SuperAdmin)
if (departmentId && currentUser.role === "SuperAdmin") {
query += " AND e.department_id = ?";
// Department filter - filter by sub-department's department
if (departmentId) {
query += " AND sd.department_id = ?";
queryParams.push(departmentId);
}

View File

@@ -304,6 +304,91 @@ router.put(
},
);
// Update work allocation units (Supervisor, Contractor, or SuperAdmin)
router.put(
"/:id/units",
authenticateToken,
authorize("Supervisor", "Contractor", "SuperAdmin"),
async (ctx) => {
try {
const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as {
units?: number;
completedUnits?: number;
markComplete?: boolean;
};
const { units, completedUnits, markComplete } = body;
if (units === undefined && completedUnits === undefined) {
ctx.response.status = 400;
ctx.response.body = { error: "Units or completedUnits required" };
return;
}
// Verify allocation exists and user has access
let query = "SELECT * FROM work_allocations WHERE id = ?";
const params: unknown[] = [allocationId];
if (currentUser.role === "Supervisor") {
query += " AND supervisor_id = ?";
params.push(currentUser.id);
} else if (currentUser.role === "Contractor") {
query += " AND contractor_id = ?";
params.push(currentUser.id);
}
const allocations = await db.query<WorkAllocation[]>(query, params);
if (allocations.length === 0) {
ctx.response.status = 403;
ctx.response.body = {
error: "Work allocation not found or access denied",
};
return;
}
const allocation = allocations[0];
const newUnits = units !== undefined ? units : allocation.units;
const newCompletedUnits = completedUnits !== undefined ? completedUnits : (allocation as any).completed_units || 0;
const rate = allocation.rate || 0;
const newTotalAmount = newCompletedUnits * rate;
// Determine status: mark as Completed if markComplete is true
const newStatus = markComplete ? "Completed" : allocation.status;
const completionDate = markComplete ? new Date().toISOString().split("T")[0] : allocation.completion_date;
await db.execute(
"UPDATE work_allocations SET units = ?, completed_units = ?, total_amount = ?, status = ?, completion_date = ? WHERE id = ?",
[newUnits, newCompletedUnits, newTotalAmount, newStatus, completionDate, allocationId],
);
const updatedAllocation = await db.query<WorkAllocation[]>(
`SELECT wa.*,
e.name as employee_name, e.username as employee_username,
s.name as supervisor_name,
c.name as contractor_name,
sd.name as sub_department_name,
d.name as department_name
FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`,
[allocationId],
);
ctx.response.body = updatedAllocation[0];
} catch (error) {
console.error("Update work allocation units error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Delete work allocation (Supervisor or SuperAdmin)
router.delete(
"/:id",

View File

@@ -0,0 +1,15 @@
-- Add completed_units column to work_allocations table
-- Run this migration to support tracking completed units separately from total units
-- Note: MySQL 8.0 does not support IF NOT EXISTS for ADD COLUMN
-- Check if column exists before running:
-- SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'work_allocations' AND COLUMN_NAME = 'completed_units';
-- Add the column (will error if already exists)
ALTER TABLE work_allocations
ADD COLUMN completed_units DECIMAL(10,2) DEFAULT 0 AFTER units;
-- Update existing records to set completed_units equal to units for completed allocations
UPDATE work_allocations
SET completed_units = units
WHERE status = 'Completed' AND units IS NOT NULL;

View File

@@ -70,6 +70,7 @@ export interface WorkAllocation {
status: AllocationStatus;
rate: number | null;
units: number | null;
completed_units: number | null;
total_amount: number | null;
created_at: Date;
employee_name?: string;

1972
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,27 @@ export const useWorkAllocations = (
}
};
const updateUnits = async (
id: number,
units?: number,
completedUnits?: number,
markComplete?: boolean,
) => {
setLoading(true);
setError(null);
try {
const updated = await api.updateWorkAllocationUnits(id, units, completedUnits, markComplete);
await fetchAllocations(); // Refresh list
return updated;
} catch (err: never) {
setError(err.message || "Failed to update work allocation units");
console.error("Failed to update work allocation units:", err);
throw err;
} finally {
setLoading(false);
}
};
return {
allocations,
loading,
@@ -90,5 +111,6 @@ export const useWorkAllocations = (
createAllocation,
updateAllocation,
deleteAllocation,
updateUnits,
};
};

View File

@@ -45,7 +45,7 @@ export const AllRatesPage: React.FC = () => {
setLoading(true);
setError("");
try {
const params: any = {};
const params: { departmentId?: number; startDate?: string; endDate?: string } = {};
if (filters.departmentId) {
params.departmentId = parseInt(filters.departmentId);
}
@@ -55,8 +55,8 @@ export const AllRatesPage: React.FC = () => {
const data = await api.getAllRates(params);
setAllRates(data.allRates);
setSummary(data.summary);
} catch (err: any) {
setError(err.message || "Failed to fetch rates");
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch rates");
} finally {
setLoading(false);
}

View File

@@ -47,20 +47,19 @@ export const ContractorPaymentPage: React.FC = () => {
// Fetch report data
const fetchReport = async () => {
if (!selectedDepartment) {
alert("Please select a department");
return;
}
setLoading(true);
try {
// Fetch completed allocations for the date range
const params: Record<string, any> = {
startDate,
endDate,
departmentId: parseInt(selectedDepartment),
};
// Only add departmentId if a specific department is selected
if (selectedDepartment) {
params.departmentId = parseInt(selectedDepartment);
}
const data = await api.getCompletedAllocationsReport(params);
// Process data to group by sub-department and contractor
@@ -113,14 +112,17 @@ export const ContractorPaymentPage: React.FC = () => {
const contractor = contractorMap.get(contractorId)!;
const amount = parseFloat(alloc.total_amount) || parseFloat(alloc.rate) || 0;
const departmentName = (alloc.department_name || "").toLowerCase();
const subDeptNameLower = (alloc.sub_department_name || "").toLowerCase();
const activity = (alloc.activity || "").toLowerCase();
// Categorize by activity type
if (activity.includes("dana")) {
// Categorize by department/sub-department name
if (departmentName.includes("dana") || subDeptNameLower.includes("dana")) {
contractor.dana += amount;
} else if (activity.includes("tukdi")) {
} else if (departmentName.includes("tudki") || departmentName.includes("tukdi") ||
subDeptNameLower.includes("tudki") || subDeptNameLower.includes("tukdi")) {
contractor.tukdi += amount;
} else if (activity.includes("groundnut")) {
} else if (departmentName.includes("groundnut") || subDeptNameLower.includes("groundnut")) {
contractor.groundnut += amount;
} else if (activity.includes("commission") || activity.includes("salary")) {
contractor.commission_salary += amount;
@@ -383,7 +385,7 @@ export const ContractorPaymentPage: React.FC = () => {
onChange={(e) => setSelectedDepartment(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Department</option>
<option value="">All Departments</option>
{departments.map((dept) => (
<option key={dept.id} value={dept.id}>{dept.name}</option>
))}

View File

@@ -76,6 +76,13 @@ export const DashboardPage: React.FC = () => {
const [contractorRates, setContractorRates] = useState<
Record<number, number>
>({});
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,16 @@ 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;
};
return supervisors.map((supervisor) => {
const deptContractors = employees.filter(
(e) =>
@@ -220,67 +252,125 @@ export const DashboardPage: React.FC = () => {
e.department_id === supervisor.department_id,
);
// Get employees without a contractor but in this department (e.g., swapped employees)
const unassignedEmployees = employees.filter(
(e) =>
e.role === "Employee" &&
e.department_id === supervisor.department_id &&
!e.contractor_id,
);
// Find employees swapped INTO this department (without a specific target contractor)
const swappedIntoThisDept = activeSwaps
.filter((s) => s.target_department_id === supervisor.department_id && !s.target_contractor_id)
.map((swap) => {
const emp = employees.find((e) => e.id === swap.employee_id);
const originalContractor = employees.find((e) => e.id === swap.original_contractor_id);
return emp ? { emp, originalContractor, swap } : null;
})
.filter((x): x is NonNullable<typeof x> => x !== null);
const contractorNodes = deptContractors.map((contractor) => {
// Include employees whose original contractor is this one (they belong here)
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 specific contractor
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<typeof e> => 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],
};
});
// Add unassigned employees node if there are any
if (unassignedEmployees.length > 0) {
// Add "Swapped In" node for employees swapped into this department without a specific contractor
if (swappedIntoThisDept.length > 0) {
contractorNodes.push({
id: -supervisor.department_id!, // Negative ID to avoid conflicts
name: "Unassigned (Swapped)",
name: "Swapped In",
role: "Contractor",
department: supervisor.department_name || "",
children: unassignedEmployees.map((emp) => {
children: swappedIntoThisDept.map(({ emp, originalContractor }) => {
const empAttendance = attendance.find((a) =>
a.employee_id === emp.id
);
@@ -290,12 +380,11 @@ export const DashboardPage: React.FC = () => {
return {
id: emp.id,
name: emp.name,
name: `${emp.name} (from ${originalContractor?.name || "other"})`,
role: "Employee",
department: emp.department_name || "",
subDepartment: empAllocation?.sub_department_name || "-",
activity: empAllocation?.description || empAllocation?.activity ||
"Swapped",
activity: empAllocation?.description || empAllocation?.activity || "-",
status: empAttendance
? (empAttendance.status === "CheckedIn" ||
empAttendance.status === "CheckedOut"
@@ -325,7 +414,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(() => {

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { CheckCircle, Plus, RefreshCw, Search, Trash2 } from "lucide-react";
import { CheckCircle, Edit2, Plus, RefreshCw, Search, Trash2, X } from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx";
import {
Table,
@@ -31,6 +31,7 @@ export const WorkAllocationPage: React.FC = () => {
createAllocation,
updateAllocation,
deleteAllocation,
updateUnits,
} = useWorkAllocations();
const { departments } = useDepartments();
const { employees } = useEmployees();
@@ -39,6 +40,15 @@ export const WorkAllocationPage: React.FC = () => {
// Check if user is supervisor (limited to their department)
const isSupervisor = user?.role === "Supervisor";
// Check if user can edit units (supervisor, contractor, or super admin)
const canEditUnits = user?.role === "Supervisor" || user?.role === "Contractor" || user?.role === "SuperAdmin";
// State for edit units modal
const [editingAllocation, setEditingAllocation] = useState<any | null>(null);
const [editUnitsValue, setEditUnitsValue] = useState<string>("");
const [editCompletedUnitsValue, setEditCompletedUnitsValue] = useState<string>("");
const [markCompleteChecked, setMarkCompleteChecked] = useState<boolean>(false);
// Get supervisor's department name
const supervisorDeptName =
@@ -213,6 +223,42 @@ export const WorkAllocationPage: React.FC = () => {
}
};
const handleOpenEditUnits = (allocation: any, fromCheckmark = false) => {
setEditingAllocation(allocation);
setEditUnitsValue(String(allocation.units || 0));
setEditCompletedUnitsValue(String(allocation.completed_units || 0));
// If opened from checkmark, default to marking complete
setMarkCompleteChecked(fromCheckmark && allocation.status !== "Completed");
};
const handleCloseEditUnits = () => {
setEditingAllocation(null);
setEditUnitsValue("");
setEditCompletedUnitsValue("");
setMarkCompleteChecked(false);
};
const handleSaveUnits = async () => {
if (!editingAllocation) return;
try {
const units = parseFloat(editUnitsValue) || 0;
const completedUnits = parseFloat(editCompletedUnitsValue) || 0;
await updateUnits(editingAllocation.id, units, completedUnits, markCompleteChecked);
handleCloseEditUnits();
} catch (err: any) {
alert(err.message || "Failed to update units");
}
};
const handleMarkCompleteWithUnits = async (allocation: any) => {
const isPerUnit = allocation.units && allocation.units > 0;
if (isPerUnit && canEditUnits) {
handleOpenEditUnits(allocation, true);
} else {
await handleMarkComplete(allocation.id);
}
};
// Calculate summary stats
const stats = {
total: allocations.length,
@@ -517,8 +563,10 @@ export const WorkAllocationPage: React.FC = () => {
<TableHead>ID</TableHead>
<TableHead>Employee</TableHead>
<TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Units</TableHead>
<TableHead>Date</TableHead>
<TableHead>Rate Details</TableHead>
<TableHead>Status</TableHead>
@@ -526,12 +574,12 @@ export const WorkAllocationPage: React.FC = () => {
</TableHeader>
<TableBody>
{filteredAllocations.map((allocation) => {
const isPerUnit = allocation.activity === "Loading" ||
allocation.activity === "Unloading";
const isPerUnit = allocation.units && allocation.units > 0;
const units = parseFloat(allocation.units) || 0;
const completedUnits = parseFloat(allocation.completed_units) || 0;
const rate = parseFloat(allocation.rate) || 0;
const total = parseFloat(allocation.total_amount) ||
(isPerUnit ? units * rate : rate);
(isPerUnit ? completedUnits * rate : rate);
return (
<TableRow key={allocation.id}>
@@ -542,6 +590,9 @@ export const WorkAllocationPage: React.FC = () => {
<TableCell>
{allocation.contractor_name || "-"}
</TableCell>
<TableCell>
{allocation.department_name || "-"}
</TableCell>
<TableCell>
{allocation.sub_department_name || "-"}
</TableCell>
@@ -550,8 +601,7 @@ export const WorkAllocationPage: React.FC = () => {
? (
<span
className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === "Loading" ||
allocation.activity === "Unloading"
isPerUnit
? "bg-purple-100 text-purple-700"
: "bg-gray-100 text-gray-700"
}`}
@@ -561,6 +611,41 @@ export const WorkAllocationPage: React.FC = () => {
)
: "-"}
</TableCell>
<TableCell>
{isPerUnit ? (
<div className="flex items-center gap-2">
<div className="text-sm">
<span className={`font-semibold ${completedUnits < units ? "text-orange-600" : "text-green-600"}`}>
{completedUnits}
</span>
<span className="text-gray-500"> / {units}</span>
{completedUnits < units && (
<div className="text-xs text-orange-500">
({units - completedUnits} remaining)
</div>
)}
{completedUnits >= units && units > 0 && (
<div className="text-xs text-green-500">
Complete
</div>
)}
</div>
{canEditUnits && (
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenEditUnits(allocation)}
className="text-blue-600 p-1"
title="Edit Units"
>
<Edit2 size={12} />
</Button>
)}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</TableCell>
<TableCell>
{new Date(allocation.assigned_date)
.toLocaleDateString()}
@@ -569,11 +654,11 @@ export const WorkAllocationPage: React.FC = () => {
{rate > 0
? (
<div className="text-sm">
{isPerUnit && units > 0
{isPerUnit && completedUnits > 0
? (
<div>
<div className="text-gray-500">
{units} × {rate.toFixed(2)}
{completedUnits} × {rate.toFixed(2)}
</div>
<div className="font-semibold text-green-600">
= {total.toFixed(2)}
@@ -611,9 +696,9 @@ export const WorkAllocationPage: React.FC = () => {
variant="ghost"
size="sm"
onClick={() =>
handleMarkComplete(allocation.id)}
handleMarkCompleteWithUnits(allocation)}
className="text-green-600"
title="Mark Complete"
title={isPerUnit ? "Complete & Set Units" : "Mark Complete"}
>
<CheckCircle size={14} />
</Button>
@@ -691,6 +776,123 @@ export const WorkAllocationPage: React.FC = () => {
)}
</CardContent>
</Card>
{/* Edit Units Modal */}
{editingAllocation && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md shadow-xl">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-800">
Edit Units - {editingAllocation.activity}
</h3>
<button
type="button"
onClick={handleCloseEditUnits}
className="text-gray-500 hover:text-gray-700"
>
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Total Units Assigned
</label>
<input
type="number"
value={editUnitsValue}
onChange={(e) => setEditUnitsValue(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min="0"
step="1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Completed Units
</label>
<input
type="number"
value={editCompletedUnitsValue}
onChange={(e) => setEditCompletedUnitsValue(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
min="0"
step="1"
/>
</div>
{/* Units Difference Display */}
{(() => {
const totalUnits = parseFloat(editUnitsValue) || 0;
const completed = parseFloat(editCompletedUnitsValue) || 0;
const remaining = totalUnits - completed;
return (
<div className={`p-3 rounded-md ${remaining > 0 ? "bg-orange-50 border border-orange-200" : "bg-green-50 border border-green-200"}`}>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Total Units:</span>
<span className="font-medium">{totalUnits}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Completed:</span>
<span className="font-medium text-green-600">{completed}</span>
</div>
{remaining > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">Remaining:</span>
<span className="font-medium text-orange-600">{remaining}</span>
</div>
)}
{remaining === 0 && totalUnits > 0 && (
<div className="text-sm text-green-600 font-medium mt-1">
All units completed
</div>
)}
</div>
);
})()}
{/* Calculation Preview */}
{parseFloat(editingAllocation.rate) > 0 && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<div className="text-sm text-gray-600">
Rate: {parseFloat(editingAllocation.rate).toFixed(2)} per unit
</div>
<div className="text-lg font-semibold text-green-600 mt-1">
Total Payment: {((parseFloat(editCompletedUnitsValue) || 0) * parseFloat(editingAllocation.rate)).toFixed(2)}
</div>
</div>
)}
{/* Mark as Complete Checkbox */}
{editingAllocation.status !== "Completed" && (
<div className="flex items-center gap-2">
<input
type="checkbox"
id="markComplete"
checked={markCompleteChecked}
onChange={(e) => setMarkCompleteChecked(e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="markComplete" className="text-sm font-medium text-gray-700">
Mark as Completed
</label>
</div>
)}
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" onClick={handleCloseEditUnits}>
Cancel
</Button>
<Button variant="primary" onClick={handleSaveUnits}>
{markCompleteChecked ? "Save & Complete" : "Save Units"}
</Button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -174,6 +174,18 @@ class ApiService {
});
}
updateWorkAllocationUnits(
id: number,
units?: number,
completedUnits?: number,
markComplete?: boolean,
) {
return this.request<any>(`/work-allocations/${id}/units`, {
method: "PUT",
body: JSON.stringify({ units, completedUnits, markComplete }),
});
}
// Attendance
getAttendance(
params?: {

View File

@@ -56,11 +56,15 @@ export interface WorkAllocation {
supervisor_id: number;
contractor_id: number;
sub_department_id?: number;
activity?: string;
description?: string;
assigned_date: string;
status: "Pending" | "InProgress" | "Completed" | "Cancelled";
completion_date?: string;
rate?: number;
units?: number;
completed_units?: number;
total_amount?: number;
created_at: string;
updated_at: string;
employee_name?: string;

View File

@@ -452,7 +452,7 @@ export const exportReportToXLSX = (
}));
const ws3 = XLSX.utils.json_to_sheet(contractorData);
ws3["!cols"] = [
ws3["!cols"] = [.
{ wch: 25 }, // Contractor
{ wch: 12 }, // Employees
{ wch: 16 }, // Total Completed
@@ -466,7 +466,8 @@ export const exportReportToXLSX = (
? `${dateRange.startDate}_to_${dateRange.endDate}`
: new Date().toISOString().split("T")[0];
const deptStr = departmentName.toLowerCase().replace(/\s+/g, "_");
const filename = `work_report_${deptStr}_${dateStr}.xlsx`;
const randomStr = Math.random().toString(36).substring(2, 11);
const filename = `work_report${randomStr}_${deptStr}_${dateStr}.xlsx`;
// Write and download
XLSX.writeFile(wb, filename);