Files
EmployeeManagementSystem/src/pages/DashboardPage.tsx

1761 lines
66 KiB
TypeScript

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<AttendanceRecord[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [contractorRates, setContractorRates] = useState<
Record<number, number>
>({});
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<number, number> = {};
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<string, number> = {};
employees.forEach((e) => {
roleCounts[e.role] = (roleCounts[e.role] || 0) + 1;
});
const colors: Record<string, string> = {
"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 (
<React.Fragment key={nodeKey}>
<tr
className={`border-b border-gray-100 hover:bg-gray-50 transition-colors ${
level === 0 ? "bg-blue-50/50" : ""
}`}
>
<td className="py-3 px-4">
<div
className="flex items-center"
style={{ paddingLeft: `${indent}px` }}
>
{hasChildren
? (
<button
onClick={() => toggleNode(nodeKey)}
className="mr-2 p-0.5 hover:bg-gray-200 rounded transition-colors"
>
{isExpanded
? <ChevronDown size={16} className="text-gray-500" />
: <ChevronRight size={16} className="text-gray-500" />}
</button>
)
: <span className="w-5 mr-2" />}
<div className={`w-2 h-2 rounded-full ${colors.bg} mr-2`} />
<span className="font-medium text-gray-800">{node.name}</span>
<span
className={`ml-2 px-2 py-0.5 rounded text-xs font-medium ${colors.badge}`}
>
{node.role}
</span>
</div>
</td>
<td className="py-3 px-4 text-gray-600">{node.department}</td>
<td className="py-3 px-4 text-gray-600">
{node.subDepartment || "-"}
</td>
<td className="py-3 px-4 text-gray-600">{node.activity || "-"}</td>
<td className="py-3 px-4">
{node.status && (
<span
className={`px-2 py-1 rounded text-xs font-medium ${
node.status === "Present"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{node.status}
</span>
)}
</td>
<td className="py-3 px-4 text-gray-600">{node.inTime || "-"}</td>
<td className="py-3 px-4 text-gray-600">{node.outTime || "-"}</td>
<td className="py-3 px-4 text-gray-500 text-sm">
{node.remark || "-"}
</td>
</tr>
{isExpanded &&
node.children.map((child) =>
renderHierarchyRow(child, level + 1, nodeKey)
)}
</React.Fragment>
);
};
// SuperAdmin Dashboard View
if (isSuperAdmin) {
return (
<div className="p-6 space-y-6">
{loading && (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600">
</div>
<span className="ml-2 text-gray-600">Loading...</span>
</div>
)}
{/* Daily Attendance Report Header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">
Daily Attendance Report
</h1>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative max-w-md">
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
{/* Hierarchical Attendance Table */}
<Card>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Level / Name
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Dept
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Sub-Dept
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Activity
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Status
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
In Time
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Out Time
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Remark
</th>
</tr>
</thead>
<tbody>
{filteredHierarchy.length > 0
? (
filteredHierarchy.map((node) =>
renderHierarchyRow(node, 0, "root")
)
)
: (
<tr>
<td
colSpan={8}
className="py-8 text-center text-gray-500"
>
{searchQuery
? "No matching records found"
: "No attendance data available"}
</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
{/* Comparative Analysis Section */}
<h2 className="text-xl font-bold text-gray-800 mt-8">
Comparative Analysis
</h2>
<div className="grid grid-cols-3 gap-6">
{/* Total Workforce Card */}
<Card>
<CardContent>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide">
Total Workforce
</h3>
<ExternalLink size={16} className="text-blue-500" />
</div>
<div className="flex items-baseline gap-2 mb-4">
<span className="text-5xl font-bold text-gray-800">
{stats.contractorCount + stats.employeeCount}
</span>
<span className="text-gray-500">Total Personnel</span>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center py-2 border-t border-gray-100">
<span className="text-gray-600">Contractors</span>
<span className="font-semibold text-gray-800">
{stats.contractorCount}
</span>
</div>
<div className="flex justify-between items-center py-2 border-t border-gray-100">
<span className="text-gray-600">Employees</span>
<span className="font-semibold text-gray-800">
{stats.employeeCount}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Attendance Ratio Donut Chart */}
<Card>
<CardContent>
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide mb-4">
Attendance Ratio
</h3>
<div className="flex items-center justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={[
{
name: "Present",
value: stats.presentCount,
fill: "#10b981",
},
{
name: "Absent",
value: stats.absentCount,
fill: "#ef4444",
},
]}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={70}
dataKey="value"
startAngle={90}
endAngle={-270}
>
<Cell fill="#10b981" />
<Cell fill="#ef4444" />
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-red-500" />
<span className="text-sm text-gray-600">Absent</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-green-500" />
<span className="text-sm text-gray-600">Present</span>
</div>
</div>
</CardContent>
</Card>
{/* Department Presence Bar Chart */}
<Card>
<CardContent>
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide mb-4">
Department Presence
</h3>
<ResponsiveContainer width="100%" height={180}>
<BarChart data={departmentPresenceData} layout="vertical">
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="name"
width={70}
tick={{ fontSize: 12 }}
/>
<Tooltip
formatter={(
value,
name,
props,
) => [`${value} / ${props.payload.total}`, "Present"]}
labelFormatter={(label) =>
departmentPresenceData.find((d) => d.name === label)
?.fullName || label}
/>
<Bar dataKey="present" radius={[0, 4, 4, 0]}>
{departmentPresenceData.map((entry, index) => {
const colors = [
"#10b981",
"#f59e0b",
"#3b82f6",
"#8b5cf6",
"#ef4444",
];
return (
<Cell
key={`cell-${index}`}
fill={colors[index % colors.length]}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* User Distribution & Recent Allocations Row */}
<div className="grid grid-cols-2 gap-6">
{/* User Distribution */}
<Card>
<CardHeader title="User Distribution by Role" />
<CardContent>
{roleData.length > 0
? (
<div className="flex items-center">
<div className="w-1/2">
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={roleData}
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={80}
dataKey="value"
>
{roleData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="w-1/2 space-y-2">
{roleData.map((item) => (
<div
key={item.name}
className="flex items-center justify-between"
>
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: item.fill }}
>
</div>
<span className="text-sm text-gray-600">
{item.name}
</span>
</div>
<span className="text-sm font-medium text-gray-800">
{item.value}
</span>
</div>
))}
</div>
</div>
)
: (
<div className="text-center text-gray-400 py-8">
No user data
</div>
)}
</CardContent>
</Card>
{/* Recent Work Allocations */}
<Card>
<CardHeader title="Recent Work Allocations" />
<CardContent>
{allocations.length > 0
? (
<div className="space-y-3">
{allocations.slice(0, 5).map((alloc) => (
<div
key={alloc.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div>
<div className="font-medium text-gray-800">
{alloc.employee_name || "Unknown Employee"}
</div>
<div className="text-sm text-gray-500">
{alloc.description || "No description"} {" "}
{new Date(alloc.assigned_date).toLocaleDateString()}
</div>
</div>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
alloc.status === "Completed"
? "bg-green-100 text-green-700"
: alloc.status === "InProgress"
? "bg-blue-100 text-blue-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{alloc.status}
</span>
</div>
))}
</div>
)
: (
<div className="text-center text-gray-400 py-8">
No recent allocations
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
// Supervisor Dashboard View
if (isSupervisor) {
const departmentName = filteredDepartments[0]?.name || "My Department";
return (
<div className="p-6 space-y-6">
{loading && (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600">
</div>
<span className="ml-2 text-gray-600">Loading...</span>
</div>
)}
{/* Department Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-800">
{departmentName} Dashboard
</h1>
<p className="text-gray-500 mt-1">
Daily Attendance & Work Overview
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative max-w-md">
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Search contractors or employees..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
{/* Hierarchical Attendance Table */}
<Card>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Contractor / Employee
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Dept
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Sub-Dept
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Activity
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Rate ()
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Status
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
In Time
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Out Time
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Remark
</th>
</tr>
</thead>
<tbody>
{filteredSupervisorHierarchy.length > 0
? (
filteredSupervisorHierarchy.map((node) => {
const rate = contractorRates[node.id];
return (
<React.Fragment key={`supervisor-${node.id}`}>
{/* Contractor Row */}
<tr className="border-b border-gray-100 bg-orange-50 hover:bg-orange-100">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
{node.children.length > 0 && (
<button
onClick={() =>
toggleNode(`supervisor-${node.id}`)}
className="p-0.5 hover:bg-orange-200 rounded"
>
{expandedNodes.has(`supervisor-${node.id}`)
? (
<ChevronDown
size={16}
className="text-gray-500"
/>
)
: (
<ChevronRight
size={16}
className="text-gray-500"
/>
)}
</button>
)}
<span className="font-medium text-gray-800">
{node.name}
</span>
<span className="px-2 py-0.5 text-xs font-medium bg-orange-200 text-orange-800 rounded">
Contractor
</span>
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{node.department}
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4">
{rate
? (
<span className="font-medium text-green-700">
{Number(rate).toFixed(2)}
</span>
)
: (
<span className="text-gray-400">Not set</span>
)}
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
</tr>
{/* Employee Rows */}
{expandedNodes.has(`supervisor-${node.id}`) &&
node.children.map((emp) => (
<tr
key={`emp-${emp.id}`}
className="border-b border-gray-100 hover:bg-gray-50"
>
<td className="py-3 px-4 pl-12">
<div className="flex items-center gap-2">
<span className="text-gray-700">
{emp.name}
</span>
<span className="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded">
Employee
</span>
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{emp.department}
</td>
<td className="py-3 px-4 text-sm text-gray-500">
{emp.subDepartment || "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{emp.activity || "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-500">
-
</td>
<td className="py-3 px-4">
{emp.status && (
<span
className={`px-2 py-1 text-xs font-medium rounded ${
emp.status === "Present"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{emp.status}
</span>
)}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{emp.inTime || "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{emp.outTime || "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-500">
{emp.remark || "-"}
</td>
</tr>
))}
</React.Fragment>
);
})
)
: (
<tr>
<td
colSpan={9}
className="py-8 text-center text-gray-500"
>
{searchQuery
? "No matching records found"
: "No contractors or employees in your department"}
</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
{/* Comparative Analysis Section */}
<h2 className="text-xl font-bold text-gray-800 mt-8">
Department Overview
</h2>
<div className="grid grid-cols-3 gap-6">
{/* Total Workforce Card */}
<Card>
<CardContent>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide">
Department Workforce
</h3>
<ExternalLink size={16} className="text-blue-500" />
</div>
<div className="flex items-baseline gap-2 mb-4">
<span className="text-5xl font-bold text-gray-800">
{stats.contractorCount + stats.employeeCount}
</span>
<span className="text-gray-500">Total Personnel</span>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center py-2 border-t border-gray-100">
<span className="text-gray-600">Contractors</span>
<span className="font-semibold text-gray-800">
{stats.contractorCount}
</span>
</div>
<div className="flex justify-between items-center py-2 border-t border-gray-100">
<span className="text-gray-600">Employees</span>
<span className="font-semibold text-gray-800">
{stats.employeeCount}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Attendance Ratio Donut Chart */}
<Card>
<CardContent>
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide mb-4">
Attendance Ratio
</h3>
<div className="flex items-center justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={[
{
name: "Present",
value: stats.presentCount,
fill: "#10b981",
},
{
name: "Absent",
value: stats.absentCount,
fill: "#ef4444",
},
]}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={70}
dataKey="value"
startAngle={90}
endAngle={-270}
>
<Cell fill="#10b981" />
<Cell fill="#ef4444" />
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-6 mt-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-green-500" />
<span className="text-sm text-gray-600">
Present ({stats.presentCount})
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-red-500" />
<span className="text-sm text-gray-600">
Absent ({stats.absentCount})
</span>
</div>
</div>
</CardContent>
</Card>
{/* Work Allocation Status */}
<Card>
<CardContent>
<h3 className="text-sm font-semibold text-gray-600 uppercase tracking-wide mb-4">
Work Allocations
</h3>
<div className="flex items-baseline gap-2 mb-4">
<span className="text-4xl font-bold text-gray-800">
{stats.totalAllocations}
</span>
<span className="text-gray-500">Total</span>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-yellow-500" />
<span className="text-sm text-gray-600">Pending</span>
</div>
<span className="font-semibold text-gray-800">
{stats.pendingAllocations}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-green-500" />
<span className="text-sm text-gray-600">Completed</span>
</div>
<span className="font-semibold text-gray-800">
{stats.completedAllocations}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-blue-500" />
<span className="text-sm text-gray-600">In Progress</span>
</div>
<span className="font-semibold text-gray-800">
{stats.totalAllocations - stats.pendingAllocations -
stats.completedAllocations}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Recent Work Allocations */}
<Card>
<CardHeader title="Recent Work Allocations" />
<CardContent>
{allocations.length > 0
? (
<div className="space-y-3">
{allocations.slice(0, 5).map((alloc) => (
<div
key={alloc.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div>
<div className="font-medium text-gray-800">
{alloc.employee_name || "Unknown Employee"}
</div>
<div className="text-sm text-gray-500">
{alloc.description || "No description"} {" "}
{new Date(alloc.assigned_date).toLocaleDateString()}
</div>
</div>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
alloc.status === "Completed"
? "bg-green-100 text-green-700"
: alloc.status === "InProgress"
? "bg-blue-100 text-blue-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{alloc.status}
</span>
</div>
))}
</div>
)
: (
<div className="text-center text-gray-400 py-8">
No recent allocations
</div>
)}
</CardContent>
</Card>
</div>
);
}
// 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 (
<div className="p-6 space-y-6">
{loading && (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600">
</div>
<span className="ml-2 text-gray-600">Loading...</span>
</div>
)}
{/* Welcome Header */}
<div className="bg-gradient-to-r from-green-500 to-green-600 rounded-xl p-6 text-white">
<h1 className="text-2xl font-bold">
Welcome, {user?.name || "Employee"}
</h1>
<p className="text-green-100 mt-1">Your personal dashboard</p>
</div>
{/* Employee Info Cards */}
<div className="grid grid-cols-3 gap-6">
{/* Department Info */}
<Card>
<CardContent>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-purple-100 rounded-lg">
<Building2 size={24} className="text-purple-600" />
</div>
<div>
<div className="text-sm text-gray-500">Department</div>
<div className="font-semibold text-gray-800">
{myDepartment?.name || "Not Assigned"}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Contractor Info */}
<Card>
<CardContent>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-orange-100 rounded-lg">
<Users size={24} className="text-orange-600" />
</div>
<div>
<div className="text-sm text-gray-500">Assigned To</div>
<div className="font-semibold text-gray-800">
{myContractor?.name || "Not Assigned"}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Today's Status */}
<Card>
<CardContent>
<div className="flex items-center gap-3 mb-4">
<div
className={`p-3 rounded-lg ${
myAttendance?.status === "CheckedIn" ||
myAttendance?.status === "CheckedOut"
? "bg-green-100"
: "bg-red-100"
}`}
>
<Clock
size={24}
className={myAttendance?.status === "CheckedIn" ||
myAttendance?.status === "CheckedOut"
? "text-green-600"
: "text-red-600"}
/>
</div>
<div>
<div className="text-sm text-gray-500">Today's Status</div>
<div
className={`font-semibold ${
myAttendance?.status === "CheckedIn" ||
myAttendance?.status === "CheckedOut"
? "text-green-600"
: "text-red-600"
}`}
>
{myAttendance?.status === "CheckedIn"
? "Checked In"
: myAttendance?.status === "CheckedOut"
? "Checked Out"
: myAttendance?.status || "Not Checked In"}
</div>
</div>
</div>
{myAttendance && (
<div className="text-sm text-gray-500 space-y-1">
{myAttendance.check_in_time && (
<div>
In: {new Date(myAttendance.check_in_time)
.toLocaleTimeString()}
</div>
)}
{myAttendance.check_out_time && (
<div>
Out: {new Date(myAttendance.check_out_time)
.toLocaleTimeString()}
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
{/* My Work Allocations */}
<Card>
<CardHeader title="My Work Allocations" />
<CardContent>
{myAllocations.length > 0
? (
<div className="space-y-3">
{myAllocations.map((alloc) => (
<div
key={alloc.id}
className="p-4 bg-gray-50 rounded-lg border border-gray-100"
>
<div className="flex items-center justify-between mb-2">
<div className="font-medium text-gray-800">
{alloc.description || "Work Assignment"}
</div>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
alloc.status === "Completed"
? "bg-green-100 text-green-700"
: alloc.status === "InProgress"
? "bg-blue-100 text-blue-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{alloc.status}
</span>
</div>
<div className="text-sm text-gray-500 space-y-1">
<div>
Assigned:{" "}
{new Date(alloc.assigned_date).toLocaleDateString()}
</div>
{alloc.sub_department_name && (
<div>Sub-Dept: {alloc.sub_department_name}</div>
)}
{alloc.supervisor_name && (
<div>Supervisor: {alloc.supervisor_name}</div>
)}
</div>
</div>
))}
</div>
)
: (
<div className="text-center text-gray-400 py-8">
<Briefcase size={48} className="mx-auto mb-4 text-gray-300" />
<p>No work allocations assigned yet</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}
// 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 (
<div className="p-6 space-y-6">
{loading && (
<div className="text-center py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600">
</div>
<span className="ml-2 text-gray-600">Loading...</span>
</div>
)}
{/* Welcome Header */}
<div className="bg-gradient-to-r from-orange-500 to-orange-600 rounded-xl p-6 text-white">
<h1 className="text-2xl font-bold">
Welcome, {user?.name || "Contractor"}
</h1>
<p className="text-orange-100 mt-1">Manage your assigned employees</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-6">
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">
MY EMPLOYEES
</h3>
<Users size={20} className="text-blue-600" />
</div>
<div className="text-3xl font-bold text-gray-800">
{myEmployees.length}
</div>
<div className="text-xs text-gray-500 mt-1">Assigned to you</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">DEPARTMENT</h3>
<Building2 size={20} className="text-purple-600" />
</div>
<div className="text-lg font-bold text-gray-800">
{myDepartment?.name || "N/A"}
</div>
<div className="text-xs text-gray-500 mt-1">Your department</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">
PRESENT TODAY
</h3>
<Clock size={20} className="text-green-600" />
</div>
<div className="text-3xl font-bold text-green-600">
{presentCount}
</div>
<div className="text-xs text-gray-500 mt-1">
of {myEmployees.length} employees
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">WORK TASKS</h3>
<Briefcase size={20} className="text-orange-600" />
</div>
<div className="text-3xl font-bold text-gray-800">
{myEmployeesAllocations.length}
</div>
<div className="text-xs text-gray-500 mt-1">Active allocations</div>
</div>
</div>
{/* My Employees Table */}
<Card>
<CardHeader title="My Employees" />
<CardContent>
{myEmployees.length > 0
? (
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Employee
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Today's Status
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Check In
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Check Out
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-600 text-sm">
Current Work
</th>
</tr>
</thead>
<tbody>
{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 (
<tr
key={emp.id}
className="border-b border-gray-100 hover:bg-gray-50"
>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-green-700 font-medium text-sm">
{emp.name.charAt(0).toUpperCase()}
</span>
</div>
<div>
<div className="font-medium text-gray-800">
{emp.name}
</div>
<div className="text-xs text-gray-500">
{emp.username}
</div>
</div>
</div>
</td>
<td className="py-3 px-4">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
isPresent
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{empAttendance?.status === "CheckedIn"
? "Checked In"
: empAttendance?.status === "CheckedOut"
? "Checked Out"
: empAttendance?.status || "Absent"}
</span>
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{empAttendance?.check_in_time
? new Date(empAttendance.check_in_time)
.toLocaleTimeString()
: "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{empAttendance?.check_out_time
? new Date(empAttendance.check_out_time)
.toLocaleTimeString()
: "-"}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{empAllocation?.description || "No work assigned"}
</td>
</tr>
);
})}
</tbody>
</table>
)
: (
<div className="text-center text-gray-400 py-8">
<Users size={48} className="mx-auto mb-4 text-gray-300" />
<p>No employees assigned to you yet</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}
// Fallback - should not reach here
return (
<div className="p-6">
<div className="text-center py-8 text-gray-500">
<p>Dashboard not available for your role</p>
</div>
</div>
);
};