1761 lines
66 KiB
TypeScript
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>
|
|
);
|
|
};
|