- Database: Add Loan, LoanInstallment, PurchaseRequest, LeaveEntitlement, EmployeeContract models - Database: Extend Attendance with ZK Tico fields (sourceDeviceId, externalId, rawData) - Database: Add Employee.attendancePin for device mapping - Backend: HR admin - Loans, Purchase Requests, Leave entitlements, Employee contracts CRUD - Backend: Leave reject, bulk attendance sync (ZK Tico ready) - Backend: Employee Portal API - scoped by employeeId (loans, leaves, purchase-requests, attendance, salaries) - Frontend: Employee Portal - dashboard, loans, leave, purchase-requests, attendance, salaries - Frontend: HR Admin - new tabs for Leaves, Loans, Purchase Requests, Contracts (approve/reject) - Dashboard: Add My Portal link - No destructive schema changes; additive migrations only Made-with: Cursor
95 lines
4.0 KiB
TypeScript
95 lines
4.0 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { portalAPI, type Attendance } from '@/lib/api/portal'
|
||
import LoadingSpinner from '@/components/LoadingSpinner'
|
||
import { Clock } from 'lucide-react'
|
||
|
||
export default function PortalAttendancePage() {
|
||
const [attendance, setAttendance] = useState<Attendance[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [month, setMonth] = useState(new Date().getMonth() + 1)
|
||
const [year, setYear] = useState(new Date().getFullYear())
|
||
|
||
useEffect(() => {
|
||
setLoading(true)
|
||
portalAPI.getAttendance(month, year)
|
||
.then(setAttendance)
|
||
.catch(() => setAttendance([]))
|
||
.finally(() => setLoading(false))
|
||
}, [month, year])
|
||
|
||
const months = Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: new Date(2000, i).toLocaleDateString('ar-SA', { month: 'long' }) }))
|
||
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
|
||
|
||
if (loading) return <LoadingSpinner />
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||
<h1 className="text-2xl font-bold text-gray-900">حضوري</h1>
|
||
<div className="flex gap-2">
|
||
<select
|
||
value={month}
|
||
onChange={(e) => setMonth(parseInt(e.target.value))}
|
||
className="px-3 py-2 border border-gray-300 rounded-lg"
|
||
>
|
||
{months.map((m) => (
|
||
<option key={m.value} value={m.value}>{m.label}</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
value={year}
|
||
onChange={(e) => setYear(parseInt(e.target.value))}
|
||
className="px-3 py-2 border border-gray-300 rounded-lg"
|
||
>
|
||
{years.map((y) => (
|
||
<option key={y} value={y}>{y}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{attendance.length === 0 ? (
|
||
<div className="bg-white rounded-xl shadow p-12 text-center text-gray-500">
|
||
<Clock className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||
<p>لا توجد سجلات حضور لهذا الشهر</p>
|
||
</div>
|
||
) : (
|
||
<div className="bg-white rounded-xl shadow overflow-hidden border border-gray-100">
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-gray-50">
|
||
<tr>
|
||
<th className="text-right py-3 px-4">التاريخ</th>
|
||
<th className="text-right py-3 px-4">دخول</th>
|
||
<th className="text-right py-3 px-4">خروج</th>
|
||
<th className="text-right py-3 px-4">ساعات العمل</th>
|
||
<th className="text-right py-3 px-4">الحالة</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{attendance.map((a) => (
|
||
<tr key={a.id} className="border-t">
|
||
<td className="py-3 px-4">{new Date(a.date).toLocaleDateString('ar-SA')}</td>
|
||
<td className="py-3 px-4">{a.checkIn ? new Date(a.checkIn).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' }) : '-'}</td>
|
||
<td className="py-3 px-4">{a.checkOut ? new Date(a.checkOut).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' }) : '-'}</td>
|
||
<td className="py-3 px-4">{a.workHours != null ? Number(a.workHours).toFixed(1) : '-'}</td>
|
||
<td className="py-3 px-4">
|
||
<span className={`px-2 py-1 rounded text-xs ${
|
||
a.status === 'PRESENT' ? 'bg-green-100 text-green-800' :
|
||
a.status === 'ABSENT' ? 'bg-red-100 text-red-800' :
|
||
a.status === 'LATE' ? 'bg-amber-100 text-amber-800' : 'bg-gray-100'
|
||
}`}>
|
||
{a.status === 'PRESENT' ? 'حاضر' : a.status === 'ABSENT' ? 'غائب' : a.status === 'LATE' ? 'متأخر' : a.status}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|