Files
EmployeeManagementSystem/src/pages/DashboardPage.tsx

1194 lines
53 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { useEmployees } from '../hooks/useEmployees';
import { useDepartments } from '../hooks/useDepartments';
import { useWorkAllocations } from '../hooks/useWorkAllocations';
import { useAuth } from '../contexts/AuthContext';
import { api } from '../services/api';
// 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 } = useEmployees();
const { departments, loading: deptLoading } = useDepartments();
const { allocations, loading: allocLoading } = 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 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
);
const supervisorNode: HierarchyNode = {
id: supervisor.id,
name: supervisor.name,
role: 'Supervisor',
department: supervisor.department_name || '',
children: 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);
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || 'Loading',
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 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
);
return deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
const contractorNode: HierarchyNode = {
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);
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || '-',
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 contractorNode;
});
}, [isSupervisor, user, employees, attendance, allocations]);
// Department presence data for bar chart
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>
<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>
{/* 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>
<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>
{/* 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>
);
};