Files
hr-portal/app/frontend/app/(dashboard)/attendance/page.tsx
T
2026-05-04 20:37:44 +00:00

191 lines
7.6 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
import { useEffect, useState } from 'react';
import { useAuth } from '@/lib/auth-context';
import { attendanceApi } from '@/lib/api';
import { getDaysInMonth, monthNames } from '@/lib/utils';
import Topbar from '@/components/layout/Topbar';
const statusColors: Record<string, string> = {
present: 'bg-green-500',
absent: 'bg-red-400',
wfh: 'bg-blue-500',
half_day: 'bg-yellow-500',
holiday: 'bg-purple-500',
};
const statusLabels: Record<string, string> = {
present: 'Present',
absent: 'Absent',
wfh: 'WFH',
half_day: 'Half Day',
holiday: 'Holiday',
};
export default function AttendancePage() {
const { user } = useAuth();
const today = new Date();
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth() + 1);
const [records, setRecords] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [markedToday, setMarkedToday] = useState(false);
const [marking, setMarking] = useState(false);
useEffect(() => {
if (!user) return;
setLoading(true);
attendanceApi.getAll({ month, year }).then((res) => {
setRecords(res.data);
const todayStr = today.toISOString().split('T')[0];
setMarkedToday(res.data.some((r: any) => {
const rDate = new Date(r.date).toISOString().split('T')[0];
return rDate === todayStr;
}));
setLoading(false);
}).catch(() => setLoading(false));
}, [user, month, year]);
const handleMark = async () => {
setMarking(true);
try {
await attendanceApi.mark({ status: 'present' });
setMarkedToday(true);
const res = await attendanceApi.getAll({ month, year });
setRecords(res.data);
} catch (err: any) {
alert(err.response?.data?.message || 'Failed to mark attendance');
}
setMarking(false);
};
const getRecordForDay = (day: number) => {
return records.find((r) => {
const d = new Date(r.date);
return d.getDate() === day && d.getMonth() + 1 === month && d.getFullYear() === year;
});
};
const daysInMonth = getDaysInMonth(year, month);
const firstDay = new Date(year, month - 1, 1).getDay();
const summary = records.reduce((acc, r) => {
acc[r.status] = (acc[r.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return (
<div className="min-h-screen bg-[#F8FAFC]">
<Topbar title="Attendance" />
<div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 p-5">
{/* Month Selector */}
<div className="flex items-center justify-between mb-5">
<h3 className="font-semibold text-gray-800">{monthNames[month]} {year}</h3>
<div className="flex gap-2">
<select
value={month}
onChange={(e) => setMonth(Number(e.target.value))}
className="text-sm border border-gray-300 rounded-lg px-2 py-1"
>
{monthNames.slice(1).map((m, i) => (
<option key={i + 1} value={i + 1}>{m}</option>
))}
</select>
<select
value={year}
onChange={(e) => setYear(Number(e.target.value))}
className="text-sm border border-gray-300 rounded-lg px-2 py-1"
>
{[2023, 2024, 2025, 2026].map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
</div>
{/* Calendar */}
<div className="grid grid-cols-7 gap-1 mb-2">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
<div key={d} className="text-center text-xs font-medium text-gray-400 py-2">{d}</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{Array.from({ length: firstDay }).map((_, i) => (
<div key={`empty-${i}`} />
))}
{Array.from({ length: daysInMonth }, (_, i) => i + 1).map((day) => {
const record = getRecordForDay(day);
const isToday = day === today.getDate() && month === today.getMonth() + 1 && year === today.getFullYear();
const isWeekend = new Date(year, month - 1, day).getDay() === 0 || new Date(year, month - 1, day).getDay() === 6;
return (
<div
key={day}
className={`aspect-square rounded-lg flex items-center justify-center text-xs font-medium relative
${isToday ? 'ring-2 ring-indigo-500' : ''}
${record ? statusColors[record.status] + ' text-white' : isWeekend ? 'bg-gray-50 text-gray-400' : 'bg-gray-100 text-gray-500'}
`}
>
{day}
</div>
);
})}
</div>
{/* Legend */}
<div className="flex flex-wrap gap-3 mt-4">
{Object.entries(statusColors).map(([status, color]) => (
<div key={status} className="flex items-center gap-1.5">
<div className={`w-3 h-3 rounded ${color}`}></div>
<span className="text-xs text-gray-500">{statusLabels[status]}</span>
</div>
))}
</div>
</div>
{/* Summary Sidebar */}
<div className="space-y-4">
{/* Mark Today */}
{!markedToday && month === today.getMonth() + 1 && year === today.getFullYear() && (
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-5">
<p className="text-sm font-semibold text-indigo-800 mb-2">Mark Today&apos;s Attendance</p>
<p className="text-xs text-indigo-600 mb-3">You haven&apos;t marked attendance for today.</p>
<button
onClick={handleMark}
disabled={marking}
className="w-full bg-indigo-600 text-white text-sm py-2 rounded-lg hover:bg-indigo-700 transition disabled:opacity-50"
>
{marking ? 'Marking...' : 'Mark Present'}
</button>
</div>
)}
{markedToday && (
<div className="bg-green-50 border border-green-200 rounded-xl p-5">
<p className="text-sm font-semibold text-green-800">Attendance Marked!</p>
<p className="text-xs text-green-600 mt-1">You are marked as present today.</p>
</div>
)}
{/* Monthly Summary */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-5">
<h3 className="font-semibold text-gray-800 mb-4">Monthly Summary</h3>
<div className="space-y-3">
{Object.entries(statusLabels).map(([status, label]) => (
<div key={status} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${statusColors[status]}`}></div>
<span className="text-sm text-gray-600">{label}</span>
</div>
<span className="text-sm font-bold text-gray-800">{summary[status] || 0}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}