Files
zerp/frontend/src/app/portal/managed-leaves/page.tsx
2026-05-07 15:21:10 +03:00

208 lines
8.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 { useAuth } from '@/contexts/AuthContext'
import { useEffect, useState } from 'react'
import { portalAPI, type ManagedLeave } from '@/lib/api/portal'
import LoadingSpinner from '@/components/LoadingSpinner'
import { toast } from 'react-hot-toast'
import { CheckCircle2, XCircle, Calendar, User, ArrowLeft } from 'lucide-react'
import Link from 'next/link'
const COMPANY_TIME_ZONE = 'Asia/Riyadh'
const formatCompanyTime = (value: string) => {
return new Date(value).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
timeZone: COMPANY_TIME_ZONE,
})
}
const formatCompanyDateTime = (value: string) => {
return new Date(value).toLocaleString('ar-SA', {
timeZone: COMPANY_TIME_ZONE,
})
}
export default function ManagedLeavesPage() {
const { hasPermission } = useAuth()
const [leaves, setLeaves] = useState<ManagedLeave[]>([])
const [loading, setLoading] = useState(true)
const [processingId, setProcessingId] = useState<string | null>(null)
const canViewDepartmentLeaveRequests = hasPermission('department_leave_requests', 'view')
const canApproveDepartmentLeaveRequests = hasPermission('department_leave_requests', 'approve')
const fetchLeaves = async () => {
try {
setLoading(true)
const data = await portalAPI.getManagedLeaves('PENDING')
setLeaves(data)
} catch (error: any) {
toast.error(error?.response?.data?.message || 'فشل تحميل طلبات الإجازات')
} finally {
setLoading(false)
}
}
useEffect(() => {
if (canViewDepartmentLeaveRequests) {
fetchLeaves()
} else {
setLoading(false)
}
}, [canViewDepartmentLeaveRequests])
const handleApprove = async (id: string) => {
try {
setProcessingId(id)
await portalAPI.approveManagedLeave(id)
toast.success('تمت الموافقة على طلب الإجازة')
fetchLeaves()
} catch (error: any) {
toast.error(error?.response?.data?.message || 'فشل اعتماد طلب الإجازة')
} finally {
setProcessingId(null)
}
}
const handleReject = async (id: string) => {
const rejectedReason = window.prompt('اكتب سبب الرفض')
if (!rejectedReason || !rejectedReason.trim()) return
try {
setProcessingId(id)
await portalAPI.rejectManagedLeave(id, rejectedReason.trim())
toast.success('تم رفض طلب الإجازة')
fetchLeaves()
} catch (error: any) {
toast.error(error?.response?.data?.message || 'فشل رفض طلب الإجازة')
} finally {
setProcessingId(null)
}
}
const formatLeaveType = (leaveType: string) => {
if (leaveType === 'ANNUAL') return 'إجازة سنوية'
if (leaveType === 'HOURLY') return 'إجازة ساعية'
return leaveType
}
if (!canViewDepartmentLeaveRequests) {
return <div className="text-center text-gray-500 py-12">الوصول مرفوض</div>
}
if (loading) return <LoadingSpinner />
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">طلبات إجازات القسم</h1>
<p className="text-gray-600 mt-1">اعتماد أو رفض طلبات موظفي القسم المباشرين</p>
</div>
<Link
href="/portal"
className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
>
<ArrowLeft className="h-4 w-4" />
العودة
</Link>
</div>
<div className="bg-white rounded-xl shadow border border-gray-100 overflow-hidden">
{leaves.length === 0 ? (
<div className="p-12 text-center text-gray-500">
لا توجد طلبات إجازات معلقة لموظفي القسم
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-right font-semibold text-gray-700">الموظف</th>
<th className="px-6 py-4 text-right font-semibold text-gray-700">نوع الإجازة</th>
<th className="px-6 py-4 text-right font-semibold text-gray-700">الفترة</th>
<th className="px-6 py-4 text-right font-semibold text-gray-700">المدة</th>
<th className="px-6 py-4 text-right font-semibold text-gray-700">السبب</th>
<th className="px-6 py-4 text-right font-semibold text-gray-700">الإجراءات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{leaves.map((leave) => (
<tr key={leave.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-gray-400" />
<div>
<p className="font-medium text-gray-900">
{leave.employee.firstName} {leave.employee.lastName}
</p>
<p className="text-xs text-gray-500">{leave.employee.uniqueEmployeeId}</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-gray-900">
{formatLeaveType(leave.leaveType)}
</td>
<td className="px-6 py-4 text-gray-600">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-400" />
<div>
<p>{formatCompanyDateTime(leave.startDate)}</p>
<p>{formatCompanyDateTime(leave.endDate)}</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-gray-900">
{leave.leaveType === 'HOURLY'
? `${formatCompanyTime(leave.startDate)} - ${formatCompanyTime(leave.endDate)}`
: `${leave.days} يوم`}
</td>
<td className="px-6 py-4 text-gray-600 max-w-xs">
<p className="truncate" title={leave.reason || ''}>
{leave.reason || '-'}
</p>
</td>
<td className="px-6 py-4">
{canApproveDepartmentLeaveRequests ? (
<div className="flex items-center gap-2">
<button
onClick={() => handleApprove(leave.id)}
disabled={processingId === leave.id}
className="inline-flex items-center gap-2 px-3 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50"
>
<CheckCircle2 className="h-4 w-4" />
قبول
</button>
<button
onClick={() => handleReject(leave.id)}
disabled={processingId === leave.id}
className="inline-flex items-center gap-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
>
<XCircle className="h-4 w-4" />
رفض
</button>
</div>
) : (
<span className="text-sm text-gray-500">عرض فقط</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}