208 lines
8.0 KiB
TypeScript
208 lines
8.0 KiB
TypeScript
'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>
|
||
)
|
||
} |