(Feat): Initial Commit
This commit is contained in:
235
src/pages/DashboardPage.tsx
Normal file
235
src/pages/DashboardPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user