(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,
sd.name as sub_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
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
LEFT JOIN standard_rates sr ON wa.sub_department_id = sr.sub_department_id
AND wa.activity = sr.activity
WHERE wa.status = 'Completed'
`;
const queryParams: unknown[] = [];

View File

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

View File

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

View File

@@ -1,10 +1,17 @@
import React, { useEffect, useMemo, useState } from "react";
import {
ArrowLeft,
ArrowRight,
Building2,
Calendar,
Check,
CheckCircle2,
Download,
FileSpreadsheet,
Filter,
RefreshCw,
Search,
User,
Users,
} from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx";
import {
@@ -16,87 +23,99 @@ import {
TableRow,
} from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts";
import { useDepartments } from "../hooks/useDepartments.ts";
import { useAuth } from "../contexts/authContext.ts";
import {
exportAllocationsToXLSX,
exportWorkReportToXLSX,
} from "../utils/excelExport.ts";
import { exportReportToXLSX } from "../utils/excelExport.ts";
type WizardStep = 1 | 2 | 3 | 4;
interface SelectionCard {
id: string | number;
name: string;
subtitle?: string;
}
export const ReportingPage: React.FC = () => {
const { user } = useAuth();
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 [summary, setSummary] = useState<
{ totalAllocations: number; totalAmount: string; totalUnits: string } | null
>(null);
const [summary, setSummary] = useState<{
totalAllocations: number;
totalAmount: string;
totalUnits: string;
} | null>(null);
const [contractors, setContractors] = useState<any[]>([]);
const [employees, setEmployees] = useState<any[]>([]);
const [subDepartments, setSubDepartments] = useState<any[]>([]);
const [activities, setActivities] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState("");
// Filters
const [filters, setFilters] = useState({
startDate: "",
endDate: "",
departmentId: "",
contractorId: "",
employeeId: "",
subDepartmentId: "",
activity: "",
});
const isSupervisor = user?.role === "Supervisor";
const isContractor = user?.role === "Contractor";
// Fetch filter options
// Fetch contractors and employees
useEffect(() => {
api.getUsers({ role: "Contractor" }).then(setContractors).catch(
console.error,
);
api.getUsers({ role: "Contractor" }).then(setContractors).catch(console.error);
api.getUsers({ role: "Employee" }).then(setEmployees).catch(console.error);
api.getAllSubDepartments().then(setSubDepartments).catch(console.error);
}, []);
// Fetch report data
const fetchReport = async () => {
// Filter contractors by department
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);
setError("");
try {
const params: 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
const deptId = isSupervisor
? 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
const contractorIdValue = isContractor
? user?.id
: (filters.contractorId ? parseInt(filters.contractorId) : null);
if (contractorIdValue) params.contractorId = contractorIdValue;
if (filters.employeeId) params.employeeId = parseInt(filters.employeeId);
const params: Record<string, any> = {};
if (dateRange.startDate) params.startDate = dateRange.startDate;
if (dateRange.endDate) params.endDate = dateRange.endDate;
// Use user's department if Supervisor
if (isSupervisor && user?.department_id) {
params.departmentId = user.department_id;
} else if (selectedDepartment && selectedDepartment !== "all") {
params.departmentId = parseInt(selectedDepartment);
}
// 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);
setAllocations(data.allocations);
setSummary(data.summary);
// 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);
setShowResults(true);
} catch (err: any) {
setError(err.message || "Failed to fetch report");
} finally {
@@ -104,303 +123,346 @@ export const ReportingPage: React.FC = () => {
}
};
useEffect(() => {
fetchReport();
}, []);
// Filter allocations based on search and dropdown filters
// Filter allocations by search
const filteredAllocations = useMemo(() => {
let result = allocations;
// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((a) =>
a.employee_name?.toLowerCase().includes(query) ||
a.contractor_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 },
if (!searchQuery) return allocations;
const query = searchQuery.toLowerCase();
return allocations.filter((a) =>
a.employee_name?.toLowerCase().includes(query) ||
a.contractor_name?.toLowerCase().includes(query) ||
a.sub_department_name?.toLowerCase().includes(query) ||
a.activity?.toLowerCase().includes(query) ||
a.department_name?.toLowerCase().includes(query)
);
}, [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
const exportSimpleList = () => {
// Export to Excel
const handleExport = () => {
if (filteredAllocations.length === 0) {
alert("No data to export");
return;
}
exportAllocationsToXLSX(filteredAllocations);
const { deptName } = getSelectionSummary();
exportReportToXLSX(filteredAllocations, deptName, dateRange);
};
const handleFilterChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target;
setFilters((prev) => ({ ...prev, [name]: value }));
// Reset wizard
const resetWizard = () => {
setWizardStep(1);
setShowResults(false);
setDateRange({ startDate: "", endDate: "" });
setSelectedDepartment(null);
setSelectedContractor(null);
setSelectedEmployee(null);
setAllocations([]);
setSummary(null);
setSearchQuery("");
};
const applyFilters = () => {
fetchReport();
};
const clearFilters = () => {
setFilters({
startDate: "",
endDate: "",
departmentId: "",
contractorId: "",
employeeId: "",
subDepartmentId: "",
activity: "",
});
setTimeout(fetchReport, 0);
};
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} />
<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>
// Selection card component
const SelectionCard = ({
item,
isSelected,
onClick,
icon,
}: {
item: SelectionCard;
isSelected: boolean;
onClick: () => void;
icon: React.ReactNode;
}) => (
<button
type="button"
onClick={onClick}
className={`p-4 rounded-lg border-2 transition-all text-left w-full ${
isSelected
? "border-blue-500 bg-blue-50 ring-2 ring-blue-200"
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isSelected ? "bg-blue-100" : "bg-gray-100"}`}>
{icon}
</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>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter size={18} className="text-gray-500" />
<h3 className="font-medium text-gray-700">Filters</h3>
// Step indicator
const StepIndicator = () => (
<div className="flex items-center justify-center gap-2 mb-8">
{[1, 2, 3, 4].map((step) => (
<React.Fragment key={step}>
<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 className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="max-w-md mx-auto space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<Input
<label className="block text-sm font-medium text-gray-700 mb-2">Start Date</label>
<input
type="date"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
value={dateRange.startDate}
onChange={(e) => setDateRange((prev) => ({ ...prev, startDate: e.target.value }))}
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>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<Input
<label className="block text-sm font-medium text-gray-700 mb-2">End Date</label>
<input
type="date"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
value={dateRange.endDate}
onChange={(e) => setDateRange((prev) => ({ ...prev, endDate: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Select
label="Department"
name="departmentId"
value={isSupervisor
? String(user?.department_id || "")
: filters.departmentId}
onChange={handleFilterChange}
disabled={isSupervisor}
options={[
{ value: "", label: "All Departments" },
...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]}
/>
<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,
})),
]}
/>
<button
type="button"
onClick={() => {
const today = new Date();
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
setDateRange({
startDate: firstDay.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
});
}}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Use Current Month
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mt-4">
<Select
label="Employee"
name="employeeId"
value={filters.employeeId}
onChange={handleFilterChange}
options={[
{ value: "", label: "All Employees" },
...employees.map((e) => ({
value: String(e.id),
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>
);
case 2:
return (
<div className="space-y-6">
<div className="text-center">
<Building2 className="mx-auto text-blue-600 mb-3" size={48} />
<h3 className="text-xl font-semibold text-gray-800">Select Department</h3>
<p className="text-gray-500 mt-1">Choose a department or select all</p>
</div>
<div className="flex gap-2 mt-4">
<Button onClick={applyFilters} size="sm">
Apply Filters
</Button>
<Button variant="outline" onClick={clearFilters} size="sm">
Clear
</Button>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 max-w-3xl mx-auto">
<SelectionCard
item={{ id: "all", name: "All Departments", subtitle: `${departments.length} departments` }}
isSelected={selectedDepartment === "all"}
onClick={() => setSelectedDepartment("all")}
icon={<Building2 size={20} className={selectedDepartment === "all" ? "text-blue-600" : "text-gray-500"} />}
/>
{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>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">
Total Completed
<CardContent>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<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 className="text-2xl font-bold text-blue-800">
{summary.totalAllocations}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<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 className="bg-green-50 border border-green-200 rounded-lg p-4">
<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>
)}
{/* Search */}
<div className="flex gap-4 mb-4">
<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 results..."
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={generateReport}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
)}
{/* Search and Refresh */}
<div className="flex gap-4 mb-4">
<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 && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">Error: {error}</div>
)}
{/* Error */}
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{/* Table */}
{loading
? <div className="text-center py-8">Loading report data...</div>
: filteredAllocations.length > 0
? (
{/* Table */}
{loading ? (
<div className="text-center py-8">Loading report data...</div>
) : filteredAllocations.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
@@ -412,71 +474,111 @@ export const ReportingPage: React.FC = () => {
<TableHead>Activity</TableHead>
<TableHead>Assigned</TableHead>
<TableHead>Completed</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Actual Rate ()</TableHead>
<TableHead>Standard Rate ()</TableHead>
<TableHead>Difference ()</TableHead>
<TableHead>Units</TableHead>
<TableHead>Total ()</TableHead>
</TableHeader>
<TableBody>
{filteredAllocations.map((allocation) => {
const rate = parseFloat(allocation.rate) || 0;
const standardRate = parseFloat(allocation.standard_rate) || 0;
const difference = rate - standardRate;
const units = parseFloat(allocation.units) || 0;
const total = parseFloat(allocation.total_amount) || rate;
return (
<TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell>
<TableCell className="font-medium">
{allocation.employee_name || "-"}
</TableCell>
<TableCell className="font-medium">{allocation.employee_name || "-"}</TableCell>
<TableCell>{allocation.contractor_name || "-"}</TableCell>
<TableCell>{allocation.department_name || "-"}</TableCell>
<TableCell>{allocation.sub_department_name || "-"}</TableCell>
<TableCell>
{allocation.contractor_name || "-"}
</TableCell>
<TableCell>
{allocation.department_name || "-"}
</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"
}`}
>
<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"}
</span>
</TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
<TableCell>
{new Date(allocation.assigned_date)
.toLocaleDateString()}
</TableCell>
<TableCell>
{allocation.completion_date
? new Date(allocation.completion_date)
.toLocaleDateString()
: "-"}
{allocation.completion_date ? new Date(allocation.completion_date).toLocaleDateString() : "-"}
</TableCell>
<TableCell>{rate.toFixed(2)}</TableCell>
<TableCell>{units > 0 ? units : "-"}</TableCell>
<TableCell className="font-semibold text-green-600">
{total.toFixed(2)}
<TableCell>{standardRate > 0 ? `${standardRate.toFixed(2)}` : "-"}</TableCell>
<TableCell>
{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>{units > 0 ? units : "-"}</TableCell>
<TableCell className="font-semibold text-green-600">{total.toFixed(2)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)
: (
) : (
<div className="text-center py-8 text-gray-500">
No completed work allocations found. Adjust your filters or
check back later.
No completed work allocations found for the selected criteria.
</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>
</Card>
</div>

View File

@@ -324,3 +324,150 @@ export const exportAllocationsToXLSX = (
// Write and download
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);
};