import React, { useEffect, useMemo, useState } from "react"; import { Briefcase, Building2, Calendar, ChevronDown, ChevronRight, Clock, ExternalLink, RefreshCw, Search, Users, } from "lucide-react"; import { Bar, BarChart, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { Card, CardContent, CardHeader } from "../components/ui/Card.tsx"; import { useEmployees } from "../hooks/useEmployees.ts"; import { useDepartments } from "../hooks/useDepartments.ts"; import { useWorkAllocations } from "../hooks/useWorkAllocations.ts"; import { useAuth } from "../contexts/AuthContext.tsx"; import { api } from "../services/api.ts"; // Types for attendance hierarchy interface AttendanceRecord { id: number; employee_id: number; employee_name: string; work_date: string; check_in_time: string | null; check_out_time: string | null; status: string; department_id: number; department_name: string; sub_department_id?: number; sub_department_name?: string; role: string; contractor_id?: number; contractor_name?: string; remark?: string; } interface HierarchyNode { id: number; name: string; role: string; department: string; subDepartment?: string; activity?: string; status?: string; inTime?: string; outTime?: string; remark?: string; children: HierarchyNode[]; isExpanded?: boolean; } export const DashboardPage: React.FC = () => { const { employees, loading: employeesLoading, refresh: refreshEmployees } = useEmployees(); const { departments, loading: deptLoading } = useDepartments(); const { allocations, loading: allocLoading, refresh: refreshAllocations } = useWorkAllocations(); const { user } = useAuth(); const [attendance, setAttendance] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [contractorRates, setContractorRates] = useState< Record >({}); const refreshAllData = () => { refreshEmployees(); refreshAllocations(); const today = new Date().toISOString().split("T")[0]; api.getAttendance({ startDate: today, endDate: today }) .then(setAttendance) .catch(console.error); }; const isSuperAdmin = user?.role === "SuperAdmin"; const isSupervisor = user?.role === "Supervisor"; const isContractor = user?.role === "Contractor"; const isEmployee = user?.role === "Employee"; const filteredDepartments = isSupervisor ? departments.filter((d) => d.id === user?.department_id) : departments; // Fetch today's attendance useEffect(() => { const today = new Date().toISOString().split("T")[0]; api.getAttendance({ startDate: today, endDate: today }) .then(setAttendance) .catch(console.error); }, []); // Fetch contractor rates for supervisor view useEffect(() => { if (isSupervisor) { api.getContractorRates() .then((rates: { contractor_id: number; rate: number }[]) => { const rateMap: Record = {}; rates.forEach((r) => { // Keep the latest rate for each contractor if ( !rateMap[r.contractor_id] || r.rate > rateMap[r.contractor_id] ) { rateMap[r.contractor_id] = r.rate; } }); setContractorRates(rateMap); }) .catch(console.error); } }, [isSupervisor]); // Calculate role distribution using useMemo instead of useEffect const roleData = useMemo(() => { if (employees.length === 0) return []; const roleCounts: Record = {}; employees.forEach((e) => { roleCounts[e.role] = (roleCounts[e.role] || 0) + 1; }); const colors: Record = { "SuperAdmin": "#8b5cf6", "Supervisor": "#3b82f6", "Contractor": "#f59e0b", "Employee": "#10b981", }; return Object.entries(roleCounts).map(([role, count]) => ({ name: role, value: count, fill: colors[role] || "#6b7280", })); }, [employees]); const loading = employeesLoading || deptLoading || allocLoading; // Stats calculations (filtered for supervisor's department if applicable) const stats = useMemo(() => { // Filter employees based on role const relevantEmployees = isSupervisor && user?.department_id ? employees.filter((e) => e.department_id === user.department_id) : employees; const contractorCount = relevantEmployees.filter((e) => e.role === "Contractor" ).length; const employeeCount = relevantEmployees.filter((e) => e.role === "Employee").length; // Filter attendance for relevant employees const relevantAttendance = isSupervisor && user?.department_id ? attendance.filter((a) => relevantEmployees.some((e) => e.id === a.employee_id) ) : attendance; const presentCount = relevantAttendance.filter((a) => a.status === "CheckedIn" || a.status === "CheckedOut" ).length; const absentCount = employeeCount - presentCount; // Filter allocations for relevant employees const relevantAllocations = isSupervisor && user?.department_id ? allocations.filter((a) => relevantEmployees.some((e) => e.id === a.employee_id) ) : allocations; return { totalUsers: relevantEmployees.length, totalDepartments: filteredDepartments.length, totalAllocations: relevantAllocations.length, pendingAllocations: relevantAllocations.filter((a) => a.status === "Pending" ).length, completedAllocations: relevantAllocations.filter((a) => a.status === "Completed").length, todayAttendance: relevantAttendance.length, checkedIn: relevantAttendance.filter((a) => a.status === "CheckedIn").length, checkedOut: relevantAttendance.filter((a) => a.status === "CheckedOut").length, contractorCount, employeeCount, presentCount, absentCount: Math.max(0, absentCount), }; }, [ employees, attendance, allocations, filteredDepartments, isSupervisor, user, ]); // Build hierarchy data for SuperAdmin view const hierarchyData = useMemo(() => { if (!isSuperAdmin) return []; const supervisors = employees.filter((e) => e.role === "Supervisor"); return supervisors.map((supervisor) => { const deptContractors = employees.filter( (e) => e.role === "Contractor" && e.department_id === supervisor.department_id, ); // Get employees without a contractor but in this department (e.g., swapped employees) const unassignedEmployees = employees.filter( (e) => e.role === "Employee" && e.department_id === supervisor.department_id && !e.contractor_id, ); const contractorNodes = deptContractors.map((contractor) => { const contractorEmployees = employees.filter( (e) => e.role === "Employee" && e.contractor_id === contractor.id, ); return { id: contractor.id, name: contractor.name, role: "Contractor", department: contractor.department_name || "", children: contractorEmployees.map((emp) => { const empAttendance = attendance.find((a) => a.employee_id === emp.id ); const empAllocation = allocations.find((a) => a.employee_id === emp.id && a.status !== "Completed" ); return { id: emp.id, name: emp.name, role: "Employee", department: emp.department_name || "", subDepartment: empAllocation?.sub_department_name || "-", activity: empAllocation?.description || empAllocation?.activity || "-", status: empAttendance ? (empAttendance.status === "CheckedIn" || empAttendance.status === "CheckedOut" ? "Present" : "Absent") : undefined, inTime: empAttendance?.check_in_time?.substring(0, 5), outTime: empAttendance?.check_out_time?.substring(0, 5), remark: empAttendance?.remark, children: [], }; }), }; }); // Add unassigned employees node if there are any if (unassignedEmployees.length > 0) { contractorNodes.push({ id: -supervisor.department_id!, // Negative ID to avoid conflicts name: "Unassigned (Swapped)", role: "Contractor", department: supervisor.department_name || "", children: unassignedEmployees.map((emp) => { const empAttendance = attendance.find((a) => a.employee_id === emp.id ); const empAllocation = allocations.find((a) => a.employee_id === emp.id && a.status !== "Completed" ); return { id: emp.id, name: emp.name, role: "Employee", department: emp.department_name || "", subDepartment: empAllocation?.sub_department_name || "-", activity: empAllocation?.description || empAllocation?.activity || "Swapped", status: empAttendance ? (empAttendance.status === "CheckedIn" || empAttendance.status === "CheckedOut" ? "Present" : "Absent") : undefined, inTime: empAttendance?.check_in_time?.substring(0, 5), outTime: empAttendance?.check_out_time?.substring(0, 5), remark: empAttendance?.remark, children: [], }; }), }); } const supervisorNode: HierarchyNode = { id: supervisor.id, name: supervisor.name, role: "Supervisor", department: supervisor.department_name || "", children: contractorNodes, }; return supervisorNode; }); }, [isSuperAdmin, employees, attendance, allocations]); // Build hierarchy data for Supervisor view (department-specific) const supervisorHierarchyData = useMemo(() => { if (!isSupervisor || !user?.department_id) return []; // Get contractors in supervisor's department const deptContractors = employees.filter( (e) => e.role === "Contractor" && e.department_id === user.department_id, ); // Get employees without a contractor but in this department (e.g., swapped employees) const unassignedEmployees = employees.filter( (e) => e.role === "Employee" && e.department_id === user.department_id && !e.contractor_id, ); const contractorNodes: HierarchyNode[] = deptContractors.map( (contractor) => { const contractorEmployees = employees.filter( (e) => e.role === "Employee" && e.contractor_id === contractor.id, ); return { id: contractor.id, name: contractor.name, role: "Contractor", department: contractor.department_name || "", children: contractorEmployees.map((emp) => { const empAttendance = attendance.find((a) => a.employee_id === emp.id ); const empAllocation = allocations.find((a) => a.employee_id === emp.id && a.status !== "Completed" ); return { id: emp.id, name: emp.name, role: "Employee", department: emp.department_name || "", subDepartment: empAllocation?.sub_department_name || "-", activity: empAllocation?.description || empAllocation?.activity || "-", status: empAttendance ? (empAttendance.status === "CheckedIn" || empAttendance.status === "CheckedOut" ? "Present" : "Absent") : undefined, inTime: empAttendance?.check_in_time?.substring(0, 5), outTime: empAttendance?.check_out_time?.substring(0, 5), remark: empAttendance?.remark, children: [], }; }), }; }, ); // Add unassigned employees node if there are any if (unassignedEmployees.length > 0) { contractorNodes.push({ id: -user.department_id, // Negative ID to avoid conflicts name: "Unassigned (Swapped)", role: "Contractor", department: filteredDepartments[0]?.name || "", children: unassignedEmployees.map((emp) => { const empAttendance = attendance.find((a) => a.employee_id === emp.id ); const empAllocation = allocations.find((a) => a.employee_id === emp.id && a.status !== "Completed" ); return { id: emp.id, name: emp.name, role: "Employee", department: emp.department_name || "", subDepartment: empAllocation?.sub_department_name || "-", activity: empAllocation?.description || empAllocation?.activity || "Swapped", status: empAttendance ? (empAttendance.status === "CheckedIn" || empAttendance.status === "CheckedOut" ? "Present" : "Absent") : undefined, inTime: empAttendance?.check_in_time?.substring(0, 5), outTime: empAttendance?.check_out_time?.substring(0, 5), remark: empAttendance?.remark, children: [], }; }), }); } return contractorNodes; }, [ isSupervisor, user, employees, attendance, allocations, filteredDepartments, ]); const departmentPresenceData = useMemo(() => { return filteredDepartments.map((dept) => { const deptEmployees = employees.filter((e) => e.department_id === dept.id && e.role === "Employee" ); const presentInDept = attendance.filter((a) => deptEmployees.some((e) => e.id === a.employee_id ) && (a.status === "CheckedIn" || a.status === "CheckedOut") ).length; return { name: dept.name.substring(0, 10), fullName: dept.name, present: presentInDept, total: deptEmployees.length, }; }); }, [filteredDepartments, employees, attendance]); const toggleNode = (nodeKey: string) => { setExpandedNodes((prev) => { const next = new Set(prev); if (next.has(nodeKey)) { next.delete(nodeKey); } else { next.add(nodeKey); } return next; }); }; // Filter hierarchy based on search (for SuperAdmin) const filteredHierarchy = useMemo(() => { if (!searchQuery) return hierarchyData; const query = searchQuery.toLowerCase(); const filterNode = (node: HierarchyNode): HierarchyNode | null => { const matchesSearch = node.name.toLowerCase().includes(query) || node.department.toLowerCase().includes(query) || node.subDepartment?.toLowerCase().includes(query); const filteredChildren = node.children .map((child) => filterNode(child)) .filter((child): child is HierarchyNode => child !== null); if (matchesSearch || filteredChildren.length > 0) { return { ...node, children: filteredChildren }; } return null; }; return hierarchyData .map((node) => filterNode(node)) .filter((node): node is HierarchyNode => node !== null); }, [hierarchyData, searchQuery]); // Filter hierarchy based on search (for Supervisor) const filteredSupervisorHierarchy = useMemo(() => { if (!searchQuery) return supervisorHierarchyData; const query = searchQuery.toLowerCase(); const filterNode = (node: HierarchyNode): HierarchyNode | null => { const matchesSearch = node.name.toLowerCase().includes(query) || node.department.toLowerCase().includes(query) || node.subDepartment?.toLowerCase().includes(query); const filteredChildren = node.children .map((child) => filterNode(child)) .filter((child): child is HierarchyNode => child !== null); if (matchesSearch || filteredChildren.length > 0) { return { ...node, children: filteredChildren }; } return null; }; return supervisorHierarchyData .map((node) => filterNode(node)) .filter((node): node is HierarchyNode => node !== null); }, [supervisorHierarchyData, searchQuery]); // Render hierarchy row const renderHierarchyRow = ( node: HierarchyNode, level: number = 0, parentKey: string = "", ) => { const nodeKey = `${parentKey}-${node.id}`; const isExpanded = expandedNodes.has(nodeKey); const hasChildren = node.children.length > 0; const indent = level * 24; const roleColors: Record< string, { bg: string; text: string; badge: string } > = { "Supervisor": { bg: "bg-blue-500", text: "text-blue-700", badge: "bg-blue-100 text-blue-700", }, "Contractor": { bg: "bg-orange-500", text: "text-orange-700", badge: "bg-orange-100 text-orange-700", }, "Employee": { bg: "bg-gray-400", text: "text-gray-600", badge: "bg-gray-100 text-gray-600", }, }; const colors = roleColors[node.role] || roleColors["Employee"]; return (
{hasChildren ? ( ) : }
{node.name} {node.role}
{node.department} {node.subDepartment || "-"} {node.activity || "-"} {node.status && ( {node.status} )} {node.inTime || "-"} {node.outTime || "-"} {node.remark || "-"} {isExpanded && node.children.map((child) => renderHierarchyRow(child, level + 1, nodeKey) )} ); }; // SuperAdmin Dashboard View if (isSuperAdmin) { return (
{loading && (
Loading...
)} {/* Daily Attendance Report Header */}

Daily Attendance Report

{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm" />
{/* Hierarchical Attendance Table */}
{filteredHierarchy.length > 0 ? ( filteredHierarchy.map((node) => renderHierarchyRow(node, 0, "root") ) ) : ( )}
Level / Name Dept Sub-Dept Activity Status In Time Out Time Remark
{searchQuery ? "No matching records found" : "No attendance data available"}
{/* Comparative Analysis Section */}

Comparative Analysis

{/* Total Workforce Card */}

Total Workforce

{stats.contractorCount + stats.employeeCount} Total Personnel
Contractors {stats.contractorCount}
Employees {stats.employeeCount}
{/* Attendance Ratio Donut Chart */}

Attendance Ratio

Absent
Present
{/* Department Presence Bar Chart */}

Department Presence

[`${value} / ${props.payload.total}`, "Present"]} labelFormatter={(label) => departmentPresenceData.find((d) => d.name === label) ?.fullName || label} /> {departmentPresenceData.map((entry, index) => { const colors = [ "#10b981", "#f59e0b", "#3b82f6", "#8b5cf6", "#ef4444", ]; return ( ); })}
{/* User Distribution & Recent Allocations Row */}
{/* User Distribution */} {roleData.length > 0 ? (
{roleData.map((entry, index) => ( ))}
{roleData.map((item) => (
{item.name}
{item.value}
))}
) : (
No user data
)}
{/* Recent Work Allocations */} {allocations.length > 0 ? (
{allocations.slice(0, 5).map((alloc) => (
{alloc.employee_name || "Unknown Employee"}
{alloc.description || "No description"} •{" "} {new Date(alloc.assigned_date).toLocaleDateString()}
{alloc.status}
))}
) : (
No recent allocations
)}
); } // Supervisor Dashboard View if (isSupervisor) { const departmentName = filteredDepartments[0]?.name || "My Department"; return (
{loading && (
Loading...
)} {/* Department Header */}

{departmentName} Dashboard

Daily Attendance & Work Overview

{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm" />
{/* Hierarchical Attendance Table */}
{filteredSupervisorHierarchy.length > 0 ? ( filteredSupervisorHierarchy.map((node) => { const rate = contractorRates[node.id]; return ( {/* Contractor Row */} {/* Employee Rows */} {expandedNodes.has(`supervisor-${node.id}`) && node.children.map((emp) => ( ))} ); }) ) : ( )}
Contractor / Employee Dept Sub-Dept Activity Rate (₹) Status In Time Out Time Remark
{node.children.length > 0 && ( )} {node.name} Contractor
{node.department} - - {rate ? ( ₹{Number(rate).toFixed(2)} ) : ( Not set )} - - - -
{emp.name} Employee
{emp.department} {emp.subDepartment || "-"} {emp.activity || "-"} - {emp.status && ( {emp.status} )} {emp.inTime || "-"} {emp.outTime || "-"} {emp.remark || "-"}
{searchQuery ? "No matching records found" : "No contractors or employees in your department"}
{/* Comparative Analysis Section */}

Department Overview

{/* Total Workforce Card */}

Department Workforce

{stats.contractorCount + stats.employeeCount} Total Personnel
Contractors {stats.contractorCount}
Employees {stats.employeeCount}
{/* Attendance Ratio Donut Chart */}

Attendance Ratio

Present ({stats.presentCount})
Absent ({stats.absentCount})
{/* Work Allocation Status */}

Work Allocations

{stats.totalAllocations} Total
Pending
{stats.pendingAllocations}
Completed
{stats.completedAllocations}
In Progress
{stats.totalAllocations - stats.pendingAllocations - stats.completedAllocations}
{/* Recent Work Allocations */} {allocations.length > 0 ? (
{allocations.slice(0, 5).map((alloc) => (
{alloc.employee_name || "Unknown Employee"}
{alloc.description || "No description"} •{" "} {new Date(alloc.assigned_date).toLocaleDateString()}
{alloc.status}
))}
) : (
No recent allocations
)}
); } // Employee Dashboard View if (isEmployee) { // Get employee's own data const myAttendance = attendance.find((a) => a.employee_id === user?.id); const myAllocations = allocations.filter((a) => a.employee_id === user?.id); const myContractor = employees.find((e) => e.id === user?.contractor_id); const myDepartment = departments.find((d) => d.id === user?.department_id); return (
{loading && (
Loading...
)} {/* Welcome Header */}

Welcome, {user?.name || "Employee"}

Your personal dashboard

{/* Employee Info Cards */}
{/* Department Info */}
Department
{myDepartment?.name || "Not Assigned"}
{/* Contractor Info */}
Assigned To
{myContractor?.name || "Not Assigned"}
{/* Today's Status */}
Today's Status
{myAttendance?.status === "CheckedIn" ? "Checked In" : myAttendance?.status === "CheckedOut" ? "Checked Out" : myAttendance?.status || "Not Checked In"}
{myAttendance && (
{myAttendance.check_in_time && (
In: {new Date(myAttendance.check_in_time) .toLocaleTimeString()}
)} {myAttendance.check_out_time && (
Out: {new Date(myAttendance.check_out_time) .toLocaleTimeString()}
)}
)}
{/* My Work Allocations */} {myAllocations.length > 0 ? (
{myAllocations.map((alloc) => (
{alloc.description || "Work Assignment"}
{alloc.status}
Assigned:{" "} {new Date(alloc.assigned_date).toLocaleDateString()}
{alloc.sub_department_name && (
Sub-Dept: {alloc.sub_department_name}
)} {alloc.supervisor_name && (
Supervisor: {alloc.supervisor_name}
)}
))}
) : (

No work allocations assigned yet

)}
); } // Contractor Dashboard View if (isContractor) { // Get contractor's employees const myEmployees = employees.filter((e) => e.contractor_id === user?.id); const myDepartment = departments.find((d) => d.id === user?.department_id); const myEmployeeIds = myEmployees.map((e) => e.id); const myEmployeesAttendance = attendance.filter((a) => myEmployeeIds.includes(a.employee_id) ); const myEmployeesAllocations = allocations.filter((a) => myEmployeeIds.includes(a.employee_id) ); const presentCount = myEmployeesAttendance.filter( (a) => a.status === "CheckedIn" || a.status === "CheckedOut", ).length; return (
{loading && (
Loading...
)} {/* Welcome Header */}

Welcome, {user?.name || "Contractor"}

Manage your assigned employees

{/* Stats Cards */}

MY EMPLOYEES

{myEmployees.length}
Assigned to you

DEPARTMENT

{myDepartment?.name || "N/A"}
Your department

PRESENT TODAY

{presentCount}
of {myEmployees.length} employees

WORK TASKS

{myEmployeesAllocations.length}
Active allocations
{/* My Employees Table */} {myEmployees.length > 0 ? ( {myEmployees.map((emp) => { const empAttendance = myEmployeesAttendance.find((a) => a.employee_id === emp.id ); const empAllocation = myEmployeesAllocations.find((a) => a.employee_id === emp.id ); const isPresent = empAttendance?.status === "CheckedIn" || empAttendance?.status === "CheckedOut"; return ( ); })}
Employee Today's Status Check In Check Out Current Work
{emp.name.charAt(0).toUpperCase()}
{emp.name}
{emp.username}
{empAttendance?.status === "CheckedIn" ? "Checked In" : empAttendance?.status === "CheckedOut" ? "Checked Out" : empAttendance?.status || "Absent"} {empAttendance?.check_in_time ? new Date(empAttendance.check_in_time) .toLocaleTimeString() : "-"} {empAttendance?.check_out_time ? new Date(empAttendance.check_out_time) .toLocaleTimeString() : "-"} {empAllocation?.description || "No work assigned"}
) : (

No employees assigned to you yet

)}
); } // Fallback - should not reach here return (

Dashboard not available for your role

); };