(Feat-Fix): Added unit display and completion in work allocation system, and ability to edit completed units. Fixed contractor reporting system not displaying data under corresponding departments.
This commit is contained in:
@@ -6,7 +6,7 @@ DB_NAME=work_allocation
|
|||||||
DB_PORT=3307
|
DB_PORT=3307
|
||||||
|
|
||||||
# JWT Configuration - CHANGE IN PRODUCTION!
|
# JWT Configuration - CHANGE IN PRODUCTION!
|
||||||
JWT_SECRET=work_alloc_jwt_secret_key_change_in_production_2024
|
JWT_SECRET=VwoPOwDth7aqCSlrbf/qYYY0upLu0kRuTcuM71ionJI=
|
||||||
JWT_EXPIRES_IN=7d
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
|
|||||||
@@ -9,11 +9,27 @@
|
|||||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
|
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
|
||||||
"mysql2": "npm:mysql2@^3.11.0",
|
"mysql2": "npm:mysql2@^3.11.0",
|
||||||
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
|
"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": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ router.get(
|
|||||||
queryParams.push(endDate);
|
queryParams.push(endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Department filter (for SuperAdmin)
|
// Department filter - filter by sub-department's department
|
||||||
if (departmentId && currentUser.role === "SuperAdmin") {
|
if (departmentId) {
|
||||||
query += " AND e.department_id = ?";
|
query += " AND sd.department_id = ?";
|
||||||
queryParams.push(departmentId);
|
queryParams.push(departmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
// Delete work allocation (Supervisor or SuperAdmin)
|
||||||
router.delete(
|
router.delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
|
|||||||
15
backend-deno/scripts/add_completed_units.sql
Normal file
15
backend-deno/scripts/add_completed_units.sql
Normal 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;
|
||||||
@@ -70,6 +70,7 @@ export interface WorkAllocation {
|
|||||||
status: AllocationStatus;
|
status: AllocationStatus;
|
||||||
rate: number | null;
|
rate: number | null;
|
||||||
units: number | null;
|
units: number | null;
|
||||||
|
completed_units: number | null;
|
||||||
total_amount: number | null;
|
total_amount: number | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
employee_name?: string;
|
employee_name?: string;
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
allocations,
|
allocations,
|
||||||
loading,
|
loading,
|
||||||
@@ -90,5 +111,6 @@ export const useWorkAllocations = (
|
|||||||
createAllocation,
|
createAllocation,
|
||||||
updateAllocation,
|
updateAllocation,
|
||||||
deleteAllocation,
|
deleteAllocation,
|
||||||
|
updateUnits,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,20 +47,19 @@ export const ContractorPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
// Fetch report data
|
// Fetch report data
|
||||||
const fetchReport = async () => {
|
const fetchReport = async () => {
|
||||||
if (!selectedDepartment) {
|
|
||||||
alert("Please select a department");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Fetch completed allocations for the date range
|
// Fetch completed allocations for the date range
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
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);
|
const data = await api.getCompletedAllocationsReport(params);
|
||||||
|
|
||||||
// Process data to group by sub-department and contractor
|
// Process data to group by sub-department and contractor
|
||||||
@@ -113,14 +112,17 @@ export const ContractorPaymentPage: React.FC = () => {
|
|||||||
|
|
||||||
const contractor = contractorMap.get(contractorId)!;
|
const contractor = contractorMap.get(contractorId)!;
|
||||||
const amount = parseFloat(alloc.total_amount) || parseFloat(alloc.rate) || 0;
|
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();
|
const activity = (alloc.activity || "").toLowerCase();
|
||||||
|
|
||||||
// Categorize by activity type
|
// Categorize by department/sub-department name
|
||||||
if (activity.includes("dana")) {
|
if (departmentName.includes("dana") || subDeptNameLower.includes("dana")) {
|
||||||
contractor.dana += amount;
|
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;
|
contractor.tukdi += amount;
|
||||||
} else if (activity.includes("groundnut")) {
|
} else if (departmentName.includes("groundnut") || subDeptNameLower.includes("groundnut")) {
|
||||||
contractor.groundnut += amount;
|
contractor.groundnut += amount;
|
||||||
} else if (activity.includes("commission") || activity.includes("salary")) {
|
} else if (activity.includes("commission") || activity.includes("salary")) {
|
||||||
contractor.commission_salary += amount;
|
contractor.commission_salary += amount;
|
||||||
@@ -383,7 +385,7 @@ export const ContractorPaymentPage: React.FC = () => {
|
|||||||
onChange={(e) => setSelectedDepartment(e.target.value)}
|
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"
|
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) => (
|
{departments.map((dept) => (
|
||||||
<option key={dept.id} value={dept.id}>{dept.name}</option>
|
<option key={dept.id} value={dept.id}>{dept.name}</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
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 { Card, CardContent } from "../components/ui/Card.tsx";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -31,6 +31,7 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
createAllocation,
|
createAllocation,
|
||||||
updateAllocation,
|
updateAllocation,
|
||||||
deleteAllocation,
|
deleteAllocation,
|
||||||
|
updateUnits,
|
||||||
} = useWorkAllocations();
|
} = useWorkAllocations();
|
||||||
const { departments } = useDepartments();
|
const { departments } = useDepartments();
|
||||||
const { employees } = useEmployees();
|
const { employees } = useEmployees();
|
||||||
@@ -39,6 +40,15 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
|
|
||||||
// Check if user is supervisor (limited to their department)
|
// Check if user is supervisor (limited to their department)
|
||||||
const isSupervisor = user?.role === "Supervisor";
|
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
|
// Get supervisor's department name
|
||||||
const supervisorDeptName =
|
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
|
// Calculate summary stats
|
||||||
const stats = {
|
const stats = {
|
||||||
total: allocations.length,
|
total: allocations.length,
|
||||||
@@ -517,8 +563,10 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
<TableHead>ID</TableHead>
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>Employee</TableHead>
|
<TableHead>Employee</TableHead>
|
||||||
<TableHead>Contractor</TableHead>
|
<TableHead>Contractor</TableHead>
|
||||||
|
<TableHead>Department</TableHead>
|
||||||
<TableHead>Sub-Department</TableHead>
|
<TableHead>Sub-Department</TableHead>
|
||||||
<TableHead>Activity</TableHead>
|
<TableHead>Activity</TableHead>
|
||||||
|
<TableHead>Units</TableHead>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead>Date</TableHead>
|
||||||
<TableHead>Rate Details</TableHead>
|
<TableHead>Rate Details</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
@@ -526,12 +574,12 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredAllocations.map((allocation) => {
|
{filteredAllocations.map((allocation) => {
|
||||||
const isPerUnit = allocation.activity === "Loading" ||
|
const isPerUnit = allocation.units && allocation.units > 0;
|
||||||
allocation.activity === "Unloading";
|
|
||||||
const units = parseFloat(allocation.units) || 0;
|
const units = parseFloat(allocation.units) || 0;
|
||||||
|
const completedUnits = parseFloat(allocation.completed_units) || 0;
|
||||||
const rate = parseFloat(allocation.rate) || 0;
|
const rate = parseFloat(allocation.rate) || 0;
|
||||||
const total = parseFloat(allocation.total_amount) ||
|
const total = parseFloat(allocation.total_amount) ||
|
||||||
(isPerUnit ? units * rate : rate);
|
(isPerUnit ? completedUnits * rate : rate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={allocation.id}>
|
<TableRow key={allocation.id}>
|
||||||
@@ -542,6 +590,9 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{allocation.contractor_name || "-"}
|
{allocation.contractor_name || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{allocation.department_name || "-"}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{allocation.sub_department_name || "-"}
|
{allocation.sub_department_name || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -550,8 +601,7 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
? (
|
? (
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
allocation.activity === "Loading" ||
|
isPerUnit
|
||||||
allocation.activity === "Unloading"
|
|
||||||
? "bg-purple-100 text-purple-700"
|
? "bg-purple-100 text-purple-700"
|
||||||
: "bg-gray-100 text-gray-700"
|
: "bg-gray-100 text-gray-700"
|
||||||
}`}
|
}`}
|
||||||
@@ -561,6 +611,41 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>
|
</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>
|
<TableCell>
|
||||||
{new Date(allocation.assigned_date)
|
{new Date(allocation.assigned_date)
|
||||||
.toLocaleDateString()}
|
.toLocaleDateString()}
|
||||||
@@ -569,11 +654,11 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
{rate > 0
|
{rate > 0
|
||||||
? (
|
? (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{isPerUnit && units > 0
|
{isPerUnit && completedUnits > 0
|
||||||
? (
|
? (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
{units} × ₹{rate.toFixed(2)}
|
{completedUnits} × ₹{rate.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-semibold text-green-600">
|
<div className="font-semibold text-green-600">
|
||||||
= ₹{total.toFixed(2)}
|
= ₹{total.toFixed(2)}
|
||||||
@@ -611,9 +696,9 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleMarkComplete(allocation.id)}
|
handleMarkCompleteWithUnits(allocation)}
|
||||||
className="text-green-600"
|
className="text-green-600"
|
||||||
title="Mark Complete"
|
title={isPerUnit ? "Complete & Set Units" : "Mark Complete"}
|
||||||
>
|
>
|
||||||
<CheckCircle size={14} />
|
<CheckCircle size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -691,6 +776,123 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
// Attendance
|
||||||
getAttendance(
|
getAttendance(
|
||||||
params?: {
|
params?: {
|
||||||
|
|||||||
@@ -56,11 +56,15 @@ export interface WorkAllocation {
|
|||||||
supervisor_id: number;
|
supervisor_id: number;
|
||||||
contractor_id: number;
|
contractor_id: number;
|
||||||
sub_department_id?: number;
|
sub_department_id?: number;
|
||||||
|
activity?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
assigned_date: string;
|
assigned_date: string;
|
||||||
status: "Pending" | "InProgress" | "Completed" | "Cancelled";
|
status: "Pending" | "InProgress" | "Completed" | "Cancelled";
|
||||||
completion_date?: string;
|
completion_date?: string;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
|
units?: number;
|
||||||
|
completed_units?: number;
|
||||||
|
total_amount?: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
employee_name?: string;
|
employee_name?: string;
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ export const exportReportToXLSX = (
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const ws3 = XLSX.utils.json_to_sheet(contractorData);
|
const ws3 = XLSX.utils.json_to_sheet(contractorData);
|
||||||
ws3["!cols"] = [
|
ws3["!cols"] = [.
|
||||||
{ wch: 25 }, // Contractor
|
{ wch: 25 }, // Contractor
|
||||||
{ wch: 12 }, // Employees
|
{ wch: 12 }, // Employees
|
||||||
{ wch: 16 }, // Total Completed
|
{ wch: 16 }, // Total Completed
|
||||||
@@ -466,7 +466,8 @@ export const exportReportToXLSX = (
|
|||||||
? `${dateRange.startDate}_to_${dateRange.endDate}`
|
? `${dateRange.startDate}_to_${dateRange.endDate}`
|
||||||
: new Date().toISOString().split("T")[0];
|
: new Date().toISOString().split("T")[0];
|
||||||
const deptStr = departmentName.toLowerCase().replace(/\s+/g, "_");
|
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
|
// Write and download
|
||||||
XLSX.writeFile(wb, filename);
|
XLSX.writeFile(wb, filename);
|
||||||
|
|||||||
Reference in New Issue
Block a user