(Feat): Initial Commit

This commit is contained in:
2025-11-27 22:50:08 +00:00
commit 00f9ed128b
79 changed files with 17413 additions and 0 deletions

235
src/pages/DashboardPage.tsx Normal file
View File

@@ -0,0 +1,235 @@
import React, { useState, useEffect } from 'react';
import { Users, Briefcase, Clock, Building2 } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer } 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';
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<any[]>([]);
const [roleData, setRoleData] = useState<Array<{ name: string; value: number; fill: string }>>([]);
// Filter departments for supervisors (only show their department)
const isSupervisor = user?.role === 'Supervisor';
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);
}, []);
// Calculate role distribution
useEffect(() => {
if (employees.length > 0) {
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'
};
setRoleData(
Object.entries(roleCounts).map(([role, count]) => ({
name: role,
value: count,
fill: colors[role] || '#6b7280'
}))
);
}
}, [employees]);
const loading = employeesLoading || deptLoading || allocLoading;
// Stats calculations
const stats = {
totalUsers: employees.length,
totalDepartments: filteredDepartments.length,
totalAllocations: allocations.length,
pendingAllocations: allocations.filter(a => a.status === 'Pending').length,
completedAllocations: allocations.filter(a => a.status === 'Completed').length,
todayAttendance: attendance.length,
checkedIn: attendance.filter(a => a.status === 'CheckedIn').length,
checkedOut: attendance.filter(a => 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>
)}
{/* 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">TOTAL USERS</h3>
<Users size={20} className="text-blue-600" />
</div>
<div className="text-3xl font-bold text-gray-800">{stats.totalUsers}</div>
<div className="text-xs text-gray-500 mt-1">Registered in system</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">DEPARTMENTS</h3>
<Building2 size={20} className="text-purple-600" />
</div>
<div className="text-3xl font-bold text-gray-800">{stats.totalDepartments}</div>
<div className="text-xs text-gray-500 mt-1">Active departments</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 ALLOCATIONS</h3>
<Briefcase size={20} className="text-orange-600" />
</div>
<div className="text-3xl font-bold text-gray-800">{stats.totalAllocations}</div>
<div className="text-xs text-gray-500 mt-1">
{stats.pendingAllocations} pending, {stats.completedAllocations} completed
</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">TODAY'S ATTENDANCE</h3>
<Clock size={20} className="text-green-600" />
</div>
<div className="text-3xl font-bold text-gray-800">{stats.todayAttendance}</div>
<div className="text-xs text-gray-500 mt-1">
{stats.checkedIn} in, {stats.checkedOut} out
</div>
</div>
</div>
{/* Charts 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>
{/* Departments Overview */}
<Card>
<CardHeader title={isSupervisor ? 'My Department' : 'Departments'} />
<CardContent>
{filteredDepartments.length > 0 ? (
<div className="space-y-4">
{filteredDepartments.map((dept, idx) => {
const deptUsers = employees.filter(e => e.department_id === dept.id).length;
const colors = ['#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444'];
return (
<div key={dept.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-3"
style={{ backgroundColor: colors[idx % colors.length] }}
></div>
<span className="font-medium text-gray-800">{dept.name}</span>
</div>
<span className="text-sm text-gray-600">{deptUsers} users</span>
</div>
);
})}
</div>
) : (
<div className="text-center text-gray-400 py-8">No departments</div>
)}
</CardContent>
</Card>
</div>
{/* Recent Activity */}
<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>
);
};