update HR modules
This commit is contained in:
191
frontend/src/app/portal/managed-leaves/page.tsx
Normal file
191
frontend/src/app/portal/managed-leaves/page.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'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'
|
||||
|
||||
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>{new Date(leave.startDate).toLocaleString()}</p>
|
||||
<p>{new Date(leave.endDate).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-gray-900">
|
||||
{leave.leaveType === 'HOURLY'
|
||||
? `${new Date(leave.startDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${new Date(leave.endDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
: `${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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user