(Feat-Fix): Fixed Reports Page, overhauled the filtering system for reports.

This commit is contained in:
2025-12-21 08:40:40 +00:00
parent 6cc9ebdcf7
commit 8fcbfe2a47
5 changed files with 607 additions and 355 deletions

View File

@@ -32,13 +32,16 @@ router.get(
c.name as contractor_name, c.name as contractor_name,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as department_name, d.name as department_name,
d.id as department_id d.id as department_id,
sr.rate as standard_rate
FROM work_allocations wa FROM work_allocations wa
JOIN users e ON wa.employee_id = e.id JOIN users e ON wa.employee_id = e.id
JOIN users s ON wa.supervisor_id = s.id JOIN users s ON wa.supervisor_id = s.id
JOIN users c ON wa.contractor_id = c.id JOIN users c ON wa.contractor_id = c.id
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id 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
WHERE wa.status = 'Completed' WHERE wa.status = 'Completed'
`; `;
const queryParams: unknown[] = []; const queryParams: unknown[] = [];

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>my-dashboard</title> <title>WorkAllocation</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -485,7 +485,7 @@ export const RatesPage: React.FC = () => {
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button <Button
variant="outline" variant="primary"
onClick={() => { onClick={() => {
setActiveTab("list"); setActiveTab("list");
resetForm(); resetForm();

View File

@@ -1,10 +1,17 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { import {
ArrowLeft,
ArrowRight,
Building2,
Calendar,
Check,
CheckCircle2,
Download, Download,
FileSpreadsheet, FileSpreadsheet,
Filter,
RefreshCw, RefreshCw,
Search, Search,
User,
Users,
} from "lucide-react"; } from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx"; import { Card, CardContent } from "../components/ui/Card.tsx";
import { import {
@@ -16,87 +23,99 @@ import {
TableRow, TableRow,
} from "../components/ui/Table.tsx"; } from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx"; import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts"; import { api } from "../services/api.ts";
import { useDepartments } from "../hooks/useDepartments.ts"; import { useDepartments } from "../hooks/useDepartments.ts";
import { useAuth } from "../contexts/authContext.ts"; import { useAuth } from "../contexts/authContext.ts";
import { import { exportReportToXLSX } from "../utils/excelExport.ts";
exportAllocationsToXLSX,
exportWorkReportToXLSX, type WizardStep = 1 | 2 | 3 | 4;
} from "../utils/excelExport.ts";
interface SelectionCard {
id: string | number;
name: string;
subtitle?: string;
}
export const ReportingPage: React.FC = () => { export const ReportingPage: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
// Wizard state
const [wizardStep, setWizardStep] = useState<WizardStep>(1);
const [showResults, setShowResults] = useState(false);
// Selections
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const [selectedDepartment, setSelectedDepartment] = useState<string | null>(null);
const [selectedContractor, setSelectedContractor] = useState<string | null>(null);
const [selectedEmployee, setSelectedEmployee] = useState<string | null>(null);
// Data
const [allocations, setAllocations] = useState<any[]>([]); const [allocations, setAllocations] = useState<any[]>([]);
const [summary, setSummary] = useState< const [summary, setSummary] = useState<{
{ totalAllocations: number; totalAmount: string; totalUnits: string } | null totalAllocations: number;
>(null); totalAmount: string;
totalUnits: string;
} | null>(null);
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
const [employees, setEmployees] = useState<any[]>([]); const [employees, setEmployees] = useState<any[]>([]);
const [subDepartments, setSubDepartments] = useState<any[]>([]);
const [activities, setActivities] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
// Filters
const [filters, setFilters] = useState({
startDate: "",
endDate: "",
departmentId: "",
contractorId: "",
employeeId: "",
subDepartmentId: "",
activity: "",
});
const isSupervisor = user?.role === "Supervisor"; const isSupervisor = user?.role === "Supervisor";
const isContractor = user?.role === "Contractor"; const isContractor = user?.role === "Contractor";
// Fetch filter options // Fetch contractors and employees
useEffect(() => { useEffect(() => {
api.getUsers({ role: "Contractor" }).then(setContractors).catch( api.getUsers({ role: "Contractor" }).then(setContractors).catch(console.error);
console.error,
);
api.getUsers({ role: "Employee" }).then(setEmployees).catch(console.error); api.getUsers({ role: "Employee" }).then(setEmployees).catch(console.error);
api.getAllSubDepartments().then(setSubDepartments).catch(console.error);
}, []); }, []);
// Fetch report data // Filter contractors by department
const fetchReport = async () => { const filteredContractors = useMemo(() => {
if (!selectedDepartment || selectedDepartment === "all") return contractors;
return contractors.filter((c) => c.department_id === parseInt(selectedDepartment));
}, [contractors, selectedDepartment]);
// Filter employees by contractor
const filteredEmployees = useMemo(() => {
if (!selectedContractor || selectedContractor === "all") return employees;
return employees.filter((e) => e.contractor_id === parseInt(selectedContractor));
}, [employees, selectedContractor]);
// Generate report
const generateReport = async () => {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const params: any = {}; const params: Record<string, any> = {};
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
// Department filter - use user's department if Supervisor, otherwise use filter if (dateRange.startDate) params.startDate = dateRange.startDate;
const deptId = isSupervisor if (dateRange.endDate) params.endDate = dateRange.endDate;
? user?.department_id
: (filters.departmentId ? parseInt(filters.departmentId) : null);
if (deptId) params.departmentId = deptId;
// Contractor filter - use user's id if Contractor, otherwise use filter // Use user's department if Supervisor
const contractorIdValue = isContractor if (isSupervisor && user?.department_id) {
? user?.id params.departmentId = user.department_id;
: (filters.contractorId ? parseInt(filters.contractorId) : null); } else if (selectedDepartment && selectedDepartment !== "all") {
if (contractorIdValue) params.contractorId = contractorIdValue; params.departmentId = parseInt(selectedDepartment);
}
if (filters.employeeId) params.employeeId = parseInt(filters.employeeId); // Use user's id if Contractor
if (isContractor && user?.id) {
params.contractorId = user.id;
} else if (selectedContractor && selectedContractor !== "all") {
params.contractorId = parseInt(selectedContractor);
}
if (selectedEmployee && selectedEmployee !== "all") {
params.employeeId = parseInt(selectedEmployee);
}
const data = await api.getCompletedAllocationsReport(params); const data = await api.getCompletedAllocationsReport(params);
setAllocations(data.allocations); setAllocations(data.allocations);
setSummary(data.summary); setSummary(data.summary);
setShowResults(true);
// Extract unique activities from allocations for the filter dropdown
const uniqueActivities = [
...new Set(
data.allocations.map((a: any) => a.activity).filter(Boolean),
),
] as string[];
setActivities(uniqueActivities);
} catch (err: any) { } catch (err: any) {
setError(err.message || "Failed to fetch report"); setError(err.message || "Failed to fetch report");
} finally { } finally {
@@ -104,303 +123,346 @@ export const ReportingPage: React.FC = () => {
} }
}; };
useEffect(() => { // Filter allocations by search
fetchReport();
}, []);
// Filter allocations based on search and dropdown filters
const filteredAllocations = useMemo(() => { const filteredAllocations = useMemo(() => {
let result = allocations; if (!searchQuery) return allocations;
const query = searchQuery.toLowerCase();
// Apply search filter return allocations.filter((a) =>
if (searchQuery) { a.employee_name?.toLowerCase().includes(query) ||
const query = searchQuery.toLowerCase(); a.contractor_name?.toLowerCase().includes(query) ||
result = result.filter((a) => a.sub_department_name?.toLowerCase().includes(query) ||
a.employee_name?.toLowerCase().includes(query) || a.activity?.toLowerCase().includes(query) ||
a.contractor_name?.toLowerCase().includes(query) || a.department_name?.toLowerCase().includes(query)
a.sub_department_name?.toLowerCase().includes(query) ||
a.activity?.toLowerCase().includes(query) ||
a.department_name?.toLowerCase().includes(query)
);
}
// Apply sub-department filter (client-side)
if (filters.subDepartmentId) {
result = result.filter((a) =>
a.sub_department_id === parseInt(filters.subDepartmentId)
);
}
// Apply activity filter (client-side)
if (filters.activity) {
result = result.filter((a) => a.activity === filters.activity);
}
return result;
}, [allocations, searchQuery, filters.subDepartmentId, filters.activity]);
// Get selected department name
const selectedDeptName = filters.departmentId
? departments.find((d) => d.id === parseInt(filters.departmentId))?.name ||
"All Departments"
: user?.role === "Supervisor"
? departments.find((d) => d.id === user?.department_id)?.name ||
"Department"
: "All Departments";
// Export to Excel (XLSX format) - Formatted Report
const exportFormattedReport = () => {
if (filteredAllocations.length === 0) {
alert("No data to export");
return;
}
exportWorkReportToXLSX(
filteredAllocations,
selectedDeptName,
{ startDate: filters.startDate, endDate: filters.endDate },
); );
}, [allocations, searchQuery]);
// Get selection names for summary
const getSelectionSummary = () => {
const deptName = selectedDepartment === "all"
? "All Departments"
: departments.find((d) => d.id === parseInt(selectedDepartment || ""))?.name || "All";
const contractorName = selectedContractor === "all"
? "All Contractors"
: contractors.find((c) => c.id === parseInt(selectedContractor || ""))?.name || "All";
const employeeName = selectedEmployee === "all"
? "All Employees"
: employees.find((e) => e.id === parseInt(selectedEmployee || ""))?.name || "All";
return { deptName, contractorName, employeeName };
}; };
// Export to Excel (XLSX format) - Simple List // Export to Excel
const exportSimpleList = () => { const handleExport = () => {
if (filteredAllocations.length === 0) { if (filteredAllocations.length === 0) {
alert("No data to export"); alert("No data to export");
return; return;
} }
const { deptName } = getSelectionSummary();
exportAllocationsToXLSX(filteredAllocations); exportReportToXLSX(filteredAllocations, deptName, dateRange);
}; };
const handleFilterChange = ( // Reset wizard
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>, const resetWizard = () => {
) => { setWizardStep(1);
const { name, value } = e.target; setShowResults(false);
setFilters((prev) => ({ ...prev, [name]: value })); setDateRange({ startDate: "", endDate: "" });
setSelectedDepartment(null);
setSelectedContractor(null);
setSelectedEmployee(null);
setAllocations([]);
setSummary(null);
setSearchQuery("");
}; };
const applyFilters = () => { // Selection card component
fetchReport(); const SelectionCard = ({
}; item,
isSelected,
const clearFilters = () => { onClick,
setFilters({ icon,
startDate: "", }: {
endDate: "", item: SelectionCard;
departmentId: "", isSelected: boolean;
contractorId: "", onClick: () => void;
employeeId: "", icon: React.ReactNode;
subDepartmentId: "", }) => (
activity: "", <button
}); type="button"
setTimeout(fetchReport, 0); onClick={onClick}
}; className={`p-4 rounded-lg border-2 transition-all text-left w-full ${
isSelected
return ( ? "border-blue-500 bg-blue-50 ring-2 ring-blue-200"
<div className="p-6"> : "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
<Card> }`}
<div className="border-b border-gray-200 px-6 py-4"> >
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <div className={`p-2 rounded-lg ${isSelected ? "bg-blue-100" : "bg-gray-100"}`}>
<FileSpreadsheet className="text-green-600" size={24} /> {icon}
<h2 className="text-xl font-semibold text-gray-800">
Work Allocation Reports
</h2>
</div>
<div className="flex gap-2">
<Button
onClick={exportFormattedReport}
disabled={filteredAllocations.length === 0}
>
<Download size={16} className="mr-2" />
Export Report (XLSX)
</Button>
<Button
variant="outline"
onClick={exportSimpleList}
disabled={filteredAllocations.length === 0}
>
<Download size={16} className="mr-2" />
Export List
</Button>
</div>
</div>
</div> </div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${isSelected ? "text-blue-700" : "text-gray-800"}`}>
{item.name}
</div>
{item.subtitle && (
<div className="text-sm text-gray-500 truncate">{item.subtitle}</div>
)}
</div>
{isSelected && <CheckCircle2 className="text-blue-500 flex-shrink-0" size={20} />}
</div>
</button>
);
<CardContent> // Step indicator
{/* Filters */} const StepIndicator = () => (
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200"> <div className="flex items-center justify-center gap-2 mb-8">
<div className="flex items-center gap-2 mb-4"> {[1, 2, 3, 4].map((step) => (
<Filter size={18} className="text-gray-500" /> <React.Fragment key={step}>
<h3 className="font-medium text-gray-700">Filters</h3> <div
className={`w-10 h-10 rounded-full flex items-center justify-center font-medium transition-all ${
step < wizardStep
? "bg-green-500 text-white"
: step === wizardStep
? "bg-blue-600 text-white ring-4 ring-blue-200"
: "bg-gray-200 text-gray-500"
}`}
>
{step < wizardStep ? <Check size={18} /> : step}
</div>
{step < 4 && (
<div className={`w-12 h-1 rounded ${step < wizardStep ? "bg-green-500" : "bg-gray-200"}`} />
)}
</React.Fragment>
))}
</div>
);
// Render wizard steps
const renderWizardContent = () => {
switch (wizardStep) {
case 1:
return (
<div className="space-y-6">
<div className="text-center">
<Calendar className="mx-auto text-blue-600 mb-3" size={48} />
<h3 className="text-xl font-semibold text-gray-800">Select Date Range</h3>
<p className="text-gray-500 mt-1">Choose the period for your report</p>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="max-w-md mx-auto space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-2">Start Date</label>
Start Date <input
</label>
<Input
type="date" type="date"
name="startDate" value={dateRange.startDate}
value={filters.startDate} onChange={(e) => setDateRange((prev) => ({ ...prev, startDate: e.target.value }))}
onChange={handleFilterChange} className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-2">End Date</label>
End Date <input
</label>
<Input
type="date" type="date"
name="endDate" value={dateRange.endDate}
value={filters.endDate} onChange={(e) => setDateRange((prev) => ({ ...prev, endDate: e.target.value }))}
onChange={handleFilterChange} className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<Select <button
label="Department" type="button"
name="departmentId" onClick={() => {
value={isSupervisor const today = new Date();
? String(user?.department_id || "") const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
: filters.departmentId} setDateRange({
onChange={handleFilterChange} startDate: firstDay.toISOString().split("T")[0],
disabled={isSupervisor} endDate: today.toISOString().split("T")[0],
options={[ });
{ value: "", label: "All Departments" }, }}
...departments.map((d) => ({ className="text-sm text-blue-600 hover:text-blue-700 font-medium"
value: String(d.id), >
label: d.name, Use Current Month
})), </button>
]}
/>
<Select
label="Contractor"
name="contractorId"
value={isContractor
? String(user?.id || "")
: filters.contractorId}
onChange={handleFilterChange}
disabled={isContractor}
options={[
{ value: "", label: "All Contractors" },
...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]}
/>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mt-4"> </div>
<Select );
label="Employee"
name="employeeId" case 2:
value={filters.employeeId} return (
onChange={handleFilterChange} <div className="space-y-6">
options={[ <div className="text-center">
{ value: "", label: "All Employees" }, <Building2 className="mx-auto text-blue-600 mb-3" size={48} />
...employees.map((e) => ({ <h3 className="text-xl font-semibold text-gray-800">Select Department</h3>
value: String(e.id), <p className="text-gray-500 mt-1">Choose a department or select all</p>
label: e.name,
})),
]}
/>
<Select
label="Sub-Department"
name="subDepartmentId"
value={filters.subDepartmentId}
onChange={handleFilterChange}
options={[
{ value: "", label: "All Sub-Departments" },
...subDepartments.map((sd) => ({
value: String(sd.id),
label: sd.name,
})),
]}
/>
<Select
label="Activity"
name="activity"
value={filters.activity}
onChange={handleFilterChange}
options={[
{ value: "", label: "All Activities" },
...activities.map((a) => ({ value: a, label: a })),
]}
/>
</div> </div>
<div className="flex gap-2 mt-4"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3 max-w-3xl mx-auto">
<Button onClick={applyFilters} size="sm"> <SelectionCard
Apply Filters item={{ id: "all", name: "All Departments", subtitle: `${departments.length} departments` }}
</Button> isSelected={selectedDepartment === "all"}
<Button variant="outline" onClick={clearFilters} size="sm"> onClick={() => setSelectedDepartment("all")}
Clear icon={<Building2 size={20} className={selectedDepartment === "all" ? "text-blue-600" : "text-gray-500"} />}
</Button> />
{departments.map((dept) => (
<SelectionCard
key={dept.id}
item={{ id: dept.id, name: dept.name }}
isSelected={selectedDepartment === String(dept.id)}
onClick={() => setSelectedDepartment(String(dept.id))}
icon={<Building2 size={20} className={selectedDepartment === String(dept.id) ? "text-blue-600" : "text-gray-500"} />}
/>
))}
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div className="text-center">
<Users className="mx-auto text-blue-600 mb-3" size={48} />
<h3 className="text-xl font-semibold text-gray-800">Select Contractor</h3>
<p className="text-gray-500 mt-1">Choose a contractor or select all</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 max-w-3xl mx-auto max-h-96 overflow-y-auto">
<SelectionCard
item={{ id: "all", name: "All Contractors", subtitle: `${filteredContractors.length} contractors` }}
isSelected={selectedContractor === "all"}
onClick={() => setSelectedContractor("all")}
icon={<Users size={20} className={selectedContractor === "all" ? "text-blue-600" : "text-gray-500"} />}
/>
{filteredContractors.map((contractor) => (
<SelectionCard
key={contractor.id}
item={{ id: contractor.id, name: contractor.name, subtitle: contractor.department_name }}
isSelected={selectedContractor === String(contractor.id)}
onClick={() => setSelectedContractor(String(contractor.id))}
icon={<Users size={20} className={selectedContractor === String(contractor.id) ? "text-blue-600" : "text-gray-500"} />}
/>
))}
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div className="text-center">
<User className="mx-auto text-blue-600 mb-3" size={48} />
<h3 className="text-xl font-semibold text-gray-800">Select Employee</h3>
<p className="text-gray-500 mt-1">Choose an employee or select all</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 max-w-3xl mx-auto max-h-96 overflow-y-auto">
<SelectionCard
item={{ id: "all", name: "All Employees", subtitle: `${filteredEmployees.length} employees` }}
isSelected={selectedEmployee === "all"}
onClick={() => setSelectedEmployee("all")}
icon={<User size={20} className={selectedEmployee === "all" ? "text-blue-600" : "text-gray-500"} />}
/>
{filteredEmployees.map((employee) => (
<SelectionCard
key={employee.id}
item={{ id: employee.id, name: employee.name, subtitle: employee.contractor_name }}
isSelected={selectedEmployee === String(employee.id)}
onClick={() => setSelectedEmployee(String(employee.id))}
icon={<User size={20} className={selectedEmployee === String(employee.id) ? "text-blue-600" : "text-gray-500"} />}
/>
))}
</div>
</div>
);
}
};
// Check if can proceed to next step
const canProceed = () => {
switch (wizardStep) {
case 1: return true; // Date range is optional
case 2: return selectedDepartment !== null;
case 3: return selectedContractor !== null;
case 4: return selectedEmployee !== null;
default: return false;
}
};
// Results view
if (showResults) {
const { deptName, contractorName, employeeName } = getSelectionSummary();
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileSpreadsheet className="text-green-600" size={24} />
<div>
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Report</h2>
<p className="text-sm text-gray-500">
{dateRange.startDate && dateRange.endDate
? `${new Date(dateRange.startDate).toLocaleDateString()} - ${new Date(dateRange.endDate).toLocaleDateString()}`
: "All Time"} {deptName} {contractorName} {employeeName}
</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={resetWizard}>
<ArrowLeft size={16} className="mr-2" />
New Report
</Button>
<Button onClick={handleExport} disabled={filteredAllocations.length === 0}>
<Download size={16} className="mr-2" />
Export to Excel
</Button>
</div>
</div> </div>
</div> </div>
{/* Summary Cards */} <CardContent>
{summary && ( {/* Summary Cards */}
<div className="grid grid-cols-3 gap-4 mb-6"> {summary && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-sm text-blue-600 font-medium"> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
Total Completed <div className="text-sm text-blue-600 font-medium">Total Completed</div>
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div>
</div> </div>
<div className="text-2xl font-bold text-blue-800"> <div className="bg-green-50 border border-green-200 rounded-lg p-4">
{summary.totalAllocations} <div className="text-sm text-green-600 font-medium">Total Amount</div>
<div className="text-2xl font-bold text-green-800">
{parseFloat(summary.totalAmount).toLocaleString()}
</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-sm text-purple-600 font-medium">Total Units</div>
<div className="text-2xl font-bold text-purple-800">
{parseFloat(summary.totalUnits).toLocaleString()}
</div>
</div> </div>
</div> </div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4"> )}
<div className="text-sm text-green-600 font-medium">
Total Amount {/* Search */}
</div> <div className="flex gap-4 mb-4">
<div className="text-2xl font-bold text-green-800"> <div className="relative flex-1">
{parseFloat(summary.totalAmount).toLocaleString()} <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
</div> <input
</div> type="text"
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4"> placeholder="Search results..."
<div className="text-sm text-purple-600 font-medium"> value={searchQuery}
Total Units onChange={(e) => setSearchQuery(e.target.value)}
</div> className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
<div className="text-2xl font-bold text-purple-800"> />
{parseFloat(summary.totalUnits).toLocaleString()}
</div>
</div> </div>
<Button variant="ghost" onClick={generateReport}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div> </div>
)}
{/* Search and Refresh */} {error && (
<div className="flex gap-4 mb-4"> <div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">Error: {error}</div>
<div className="relative flex-1"> )}
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Search by employee, contractor, department..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchReport}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Error */} {/* Table */}
{error && ( {loading ? (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4"> <div className="text-center py-8">Loading report data...</div>
Error: {error} ) : filteredAllocations.length > 0 ? (
</div>
)}
{/* Table */}
{loading
? <div className="text-center py-8">Loading report data...</div>
: filteredAllocations.length > 0
? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -412,71 +474,111 @@ export const ReportingPage: React.FC = () => {
<TableHead>Activity</TableHead> <TableHead>Activity</TableHead>
<TableHead>Assigned</TableHead> <TableHead>Assigned</TableHead>
<TableHead>Completed</TableHead> <TableHead>Completed</TableHead>
<TableHead>Rate ()</TableHead> <TableHead>Actual Rate ()</TableHead>
<TableHead>Standard Rate ()</TableHead>
<TableHead>Difference ()</TableHead>
<TableHead>Units</TableHead> <TableHead>Units</TableHead>
<TableHead>Total ()</TableHead> <TableHead>Total ()</TableHead>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredAllocations.map((allocation) => { {filteredAllocations.map((allocation) => {
const rate = parseFloat(allocation.rate) || 0; const rate = parseFloat(allocation.rate) || 0;
const standardRate = parseFloat(allocation.standard_rate) || 0;
const difference = rate - standardRate;
const units = parseFloat(allocation.units) || 0; const units = parseFloat(allocation.units) || 0;
const total = parseFloat(allocation.total_amount) || rate; const total = parseFloat(allocation.total_amount) || rate;
return ( return (
<TableRow key={allocation.id}> <TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell> <TableCell>{allocation.id}</TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">{allocation.employee_name || "-"}</TableCell>
{allocation.employee_name || "-"} <TableCell>{allocation.contractor_name || "-"}</TableCell>
</TableCell> <TableCell>{allocation.department_name || "-"}</TableCell>
<TableCell>{allocation.sub_department_name || "-"}</TableCell>
<TableCell> <TableCell>
{allocation.contractor_name || "-"} <span className={`px-2 py-1 rounded text-xs font-medium ${
</TableCell> allocation.activity === "Loading" || allocation.activity === "Unloading"
<TableCell> ? "bg-purple-100 text-purple-700"
{allocation.department_name || "-"} : "bg-gray-100 text-gray-700"
</TableCell> }`}>
<TableCell>
{allocation.sub_department_name || "-"}
</TableCell>
<TableCell>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === "Loading" ||
allocation.activity === "Unloading"
? "bg-purple-100 text-purple-700"
: "bg-gray-100 text-gray-700"
}`}
>
{allocation.activity || "Standard"} {allocation.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
<TableCell> <TableCell>
{new Date(allocation.assigned_date) {allocation.completion_date ? new Date(allocation.completion_date).toLocaleDateString() : "-"}
.toLocaleDateString()}
</TableCell>
<TableCell>
{allocation.completion_date
? new Date(allocation.completion_date)
.toLocaleDateString()
: "-"}
</TableCell> </TableCell>
<TableCell>{rate.toFixed(2)}</TableCell> <TableCell>{rate.toFixed(2)}</TableCell>
<TableCell>{units > 0 ? units : "-"}</TableCell> <TableCell>{standardRate > 0 ? `${standardRate.toFixed(2)}` : "-"}</TableCell>
<TableCell className="font-semibold text-green-600"> <TableCell>
{total.toFixed(2)} {standardRate > 0 ? (
<span className={`font-medium ${
difference > 0 ? "text-red-600" : difference < 0 ? "text-green-600" : "text-gray-600"
}`}>
{difference > 0 ? "+" : ""}{difference.toFixed(2)}
</span>
) : "-"}
</TableCell> </TableCell>
<TableCell>{units > 0 ? units : "-"}</TableCell>
<TableCell className="font-semibold text-green-600">{total.toFixed(2)}</TableCell>
</TableRow> </TableRow>
); );
})} })}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
) ) : (
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No completed work allocations found. Adjust your filters or No completed work allocations found for the selected criteria.
check back later.
</div> </div>
)} )}
</CardContent>
</Card>
</div>
);
}
// Wizard view
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center gap-3">
<FileSpreadsheet className="text-green-600" size={24} />
<h2 className="text-xl font-semibold text-gray-800">Generate Work Allocation Report</h2>
</div>
</div>
<CardContent>
<div className="py-8">
<StepIndicator />
{renderWizardContent()}
{/* Navigation */}
<div className="flex justify-between mt-8 max-w-3xl mx-auto">
<Button
variant="secondary"
onClick={() => setWizardStep((prev) => Math.max(1, prev - 1) as WizardStep)}
disabled={wizardStep === 1}
>
<ArrowLeft size={16} className="mr-2" />
Back
</Button>
{wizardStep < 4 ? (
<Button onClick={() => setWizardStep((prev) => (prev + 1) as WizardStep)} disabled={!canProceed()}>
Next
<ArrowRight size={16} className="ml-2" />
</Button>
) : (
<Button onClick={generateReport} disabled={!canProceed() || loading}>
{loading ? "Generating..." : "Generate Report"}
<FileSpreadsheet size={16} className="ml-2" />
</Button>
)}
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -324,3 +324,150 @@ export const exportAllocationsToXLSX = (
// Write and download // Write and download
XLSX.writeFile(wb, outputFilename); XLSX.writeFile(wb, outputFilename);
}; };
/**
* Combined export function with improved formatting
* Includes standard rate and difference columns
*/
export const exportReportToXLSX = (
allocations: AllocationData[],
departmentName: string,
dateRange: { startDate: string; endDate: string },
) => {
if (allocations.length === 0) {
alert("No data to export");
return;
}
// Create workbook
const wb = XLSX.utils.book_new();
// ===== Sheet 1: Detailed Report =====
const detailedData = allocations.map((a, index) => {
const rate = parseFloat(String(a.rate)) || 0;
const standardRate = parseFloat(String((a as any).standard_rate)) || 0;
const difference = standardRate > 0 ? rate - standardRate : 0;
const units = parseFloat(String(a.units)) || 0;
const total = parseFloat(String(a.total_amount)) || rate;
return {
"S.No": index + 1,
"Employee": a.employee_name || "",
"Contractor": a.contractor_name || "",
"Department": a.department_name || "",
"Sub-Department": a.sub_department_name || "",
"Activity": a.activity || "Standard",
"Assigned Date": a.assigned_date
? new Date(a.assigned_date).toLocaleDateString()
: "",
"Completed Date": a.completion_date
? new Date(a.completion_date).toLocaleDateString()
: "",
"Actual Rate (₹)": rate,
"Standard Rate (₹)": standardRate > 0 ? standardRate : "-",
"Difference (₹)": standardRate > 0 ? difference : "-",
"Units": units > 0 ? units : "-",
"Total Amount (₹)": total,
};
});
const ws1 = XLSX.utils.json_to_sheet(detailedData);
// Set column widths for detailed sheet
ws1["!cols"] = [
{ wch: 6 }, // S.No
{ wch: 22 }, // Employee
{ wch: 20 }, // Contractor
{ wch: 15 }, // Department
{ wch: 18 }, // Sub-Department
{ wch: 15 }, // Activity
{ wch: 12 }, // Assigned Date
{ wch: 14 }, // Completed Date
{ wch: 14 }, // Actual Rate
{ wch: 16 }, // Standard Rate
{ wch: 14 }, // Difference
{ wch: 8 }, // Units
{ wch: 16 }, // Total Amount
];
XLSX.utils.book_append_sheet(wb, ws1, "Detailed Report");
// ===== Sheet 2: Summary by Activity =====
const activitySummary = new Map<string, { count: number; totalAmount: number; totalUnits: number }>();
allocations.forEach((a) => {
const activity = a.activity || "Standard";
const existing = activitySummary.get(activity) || { count: 0, totalAmount: 0, totalUnits: 0 };
existing.count += 1;
existing.totalAmount += parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0;
existing.totalUnits += parseFloat(String(a.units)) || 0;
activitySummary.set(activity, existing);
});
const summaryData = Array.from(activitySummary.entries()).map(([activity, data]) => ({
"Activity": activity,
"Total Completed": data.count,
"Total Units": data.totalUnits,
"Total Amount (₹)": data.totalAmount.toFixed(2),
}));
// Add grand total row
const grandTotal = {
"Activity": "GRAND TOTAL",
"Total Completed": allocations.length,
"Total Units": allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0),
"Total Amount (₹)": allocations.reduce((sum, a) =>
sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0
).toFixed(2),
};
summaryData.push(grandTotal);
const ws2 = XLSX.utils.json_to_sheet(summaryData);
ws2["!cols"] = [
{ wch: 25 }, // Activity
{ wch: 16 }, // Total Completed
{ wch: 12 }, // Total Units
{ wch: 18 }, // Total Amount
];
XLSX.utils.book_append_sheet(wb, ws2, "Summary by Activity");
// ===== Sheet 3: Summary by Contractor =====
const contractorSummary = new Map<string, { count: number; totalAmount: number; employees: Set<string> }>();
allocations.forEach((a) => {
const contractor = a.contractor_name || "Unknown";
const existing = contractorSummary.get(contractor) || { count: 0, totalAmount: 0, employees: new Set() };
existing.count += 1;
existing.totalAmount += parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0;
if (a.employee_name) existing.employees.add(a.employee_name);
contractorSummary.set(contractor, existing);
});
const contractorData = Array.from(contractorSummary.entries()).map(([contractor, data]) => ({
"Contractor": contractor,
"Employees": data.employees.size,
"Total Completed": data.count,
"Total Amount (₹)": data.totalAmount.toFixed(2),
}));
const ws3 = XLSX.utils.json_to_sheet(contractorData);
ws3["!cols"] = [
{ wch: 25 }, // Contractor
{ wch: 12 }, // Employees
{ wch: 16 }, // Total Completed
{ wch: 18 }, // Total Amount
];
XLSX.utils.book_append_sheet(wb, ws3, "Summary by Contractor");
// Generate filename
const dateStr = dateRange.startDate && dateRange.endDate
? `${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`;
// Write and download
XLSX.writeFile(wb, filename);
};