Files
zerp/frontend/src/app/portal/attendance/page.tsx
Talal Sharabi 72ed9a2ff5 feat(hr): Complete HR module with Employee Portal, Loans, Leave, Purchase Requests, Contracts
- 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
2026-03-04 19:44:09 +04:00

95 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}