1194 lines
53 KiB
TypeScript
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>
|
|
);
|
|
};
|