Files
zerp/frontend/src/app/portal/leave/page.tsx
2026-05-03 10:30:03 +03:00

309 lines
10 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 } from '@/lib/api/portal'
import Modal from '@/components/Modal'
import LoadingSpinner from '@/components/LoadingSpinner'
import { toast } from 'react-hot-toast'
import { Plus } from 'lucide-react'
const TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
const hour = Math.floor(i / 2).toString().padStart(2, '0')
const minute = i % 2 === 0 ? '00' : '30'
return `${hour}:${minute}`
})
const LEAVE_TYPES = [
{ value: 'ANNUAL', label: 'إجازة سنوية' },
{ value: 'HOURLY', label: 'إجازة ساعية' },
]
const STATUS_MAP: Record<string, { label: string; color: string }> = {
PENDING: { label: 'قيد المراجعة', color: 'bg-amber-100 text-amber-800' },
APPROVED: { label: 'معتمدة', color: 'bg-green-100 text-green-800' },
REJECTED: { label: 'مرفوضة', color: 'bg-red-100 text-red-800' },
}
export default function PortalLeavePage() {
const [leaveBalance, setLeaveBalance] = useState<any[]>([])
const [leaves, setLeaves] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [form, setForm] = useState({
leaveType: 'ANNUAL',
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
const load = () => {
setLoading(true)
Promise.all([portalAPI.getLeaveBalance(), portalAPI.getLeaves()])
.then(([balance, list]) => {
setLeaveBalance(balance)
setLeaves(list)
})
.catch(() => toast.error('فشل تحميل البيانات'))
.finally(() => setLoading(false))
}
useEffect(() => load(), [])
const toCompanyDateTime = (date: string, time: string) => {
return `${date}T${time}:00+03:00`
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
let payload: any = {
leaveType: form.leaveType,
reason: form.reason || undefined,
}
if (form.leaveType === 'ANNUAL') {
if (!form.startDate || !form.endDate) {
toast.error('أدخل تاريخ البداية والنهاية')
return
}
if (new Date(form.endDate) < new Date(form.startDate)) {
toast.error('تاريخ النهاية يجب أن يكون بعد البداية')
return
}
payload.startDate = form.startDate
payload.endDate = form.endDate
} else {
if (!form.leaveDate || !form.startTime || !form.endTime) {
toast.error('أدخل التاريخ والوقت للإجازة الساعية')
return
}
if (form.startTime >= form.endTime) {
toast.error('وقت النهاية يجب أن يكون بعد البداية')
return
}
payload.startDate = `${form.leaveDate}T${form.startTime}:00+03:00`
payload.endDate = `${form.leaveDate}T${form.endTime}:00+03:00`
}
setSubmitting(true)
portalAPI.submitLeaveRequest(payload)
.then(() => {
setShowModal(false)
setForm({
leaveType: 'ANNUAL',
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
toast.success('تم إرسال طلب الإجازة')
load()
})
.catch(() => toast.error('فشل إرسال الطلب'))
.finally(() => setSubmitting(false))
}
if (loading) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* HEADER */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">إجازاتي</h1>
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"
>
<Plus className="h-4 w-4" />
طلب إجازة
</button>
</div>
{/* الرصيد */}
{leaveBalance.length > 0 && (
<div className="bg-white rounded-xl shadow p-6 border border-gray-100">
<h2 className="text-lg font-semibold text-gray-900 mb-4">رصيد الإجازات</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{leaveBalance.map((b) => (
<div key={b.leaveType} className="border rounded-lg p-4">
<p className="text-sm text-gray-600">
{b.leaveType === 'ANNUAL' ? 'سنوية' : b.leaveType}
</p>
<p className="text-2xl font-bold text-teal-600 mt-1">{b.available} يوم</p>
</div>
))}
</div>
</div>
)}
{/* الطلبات */}
<div className="bg-white rounded-xl shadow p-6 border border-gray-100">
<h2 className="text-lg font-semibold text-gray-900 mb-4">طلباتي</h2>
{leaves.length === 0 ? (
<p className="text-gray-500 text-center py-8">لا توجد طلبات</p>
) : (
<div className="space-y-3">
{leaves.map((l) => {
const statusInfo = STATUS_MAP[l.status] || { label: l.status, color: 'bg-gray-100' }
return (
<div key={l.id} className="flex justify-between items-center py-3 border-b">
<div>
<p className="font-medium">
{l.leaveType === 'ANNUAL' ? 'سنوية' : 'ساعية'} -{' '}
{l.leaveType === 'HOURLY'
? `${new Date(l.startDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${new Date(l.endDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
: `${l.days} يوم`}
</p>
<p className="text-sm text-gray-600">
{new Date(l.startDate).toLocaleDateString('ar-SA')} - {new Date(l.endDate).toLocaleDateString('ar-SA')}
</p>
</div>
<span className={`px-3 py-1 rounded-full text-xs ${statusInfo.color}`}>
{statusInfo.label}
</span>
</div>
)
})}
</div>
)}
</div>
{/* الفورم */}
<Modal isOpen={showModal} onClose={() => setShowModal(false)} title="طلب إجازة جديد">
<form onSubmit={handleSubmit} className="space-y-4">
{/* نوع الإجازة */}
<select
value={form.leaveType}
onChange={(e) =>
setForm({
leaveType: e.target.value,
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
}
className="w-full px-3 py-2 border rounded-lg"
>
{LEAVE_TYPES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
{/* سنوية */}
{form.leaveType === 'ANNUAL' ? (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm">من تاريخ</label>
<input
type="date"
value={form.startDate}
onChange={(e) => setForm(p => ({ ...p, startDate: e.target.value }))}
className="border p-2 rounded w-full"
/>
</div>
<div>
<label className="text-sm">إلى تاريخ</label>
<input
type="date"
value={form.endDate}
onChange={(e) => setForm(p => ({ ...p, endDate: e.target.value }))}
className="border p-2 rounded w-full"
/>
</div>
</div>
) : (
/* ساعية */
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-sm">التاريخ</label>
<input
type="date"
value={form.leaveDate}
onChange={(e) => setForm(p => ({ ...p, leaveDate: e.target.value }))}
className="border p-2 rounded w-full"
/>
</div>
<div>
<label className="text-sm">من الساعة</label>
<select
value={form.startTime}
onChange={(e) => setForm(p => ({ ...p, startTime: e.target.value }))}
className="border p-2 rounded w-full"
>
<option value="">اختر الوقت</option>
{TIME_OPTIONS.map((time) => (
<option key={time} value={time}>{time}</option>
))}
</select>
</div>
<div>
<label className="text-sm">إلى الساعة</label>
<select
value={form.endTime}
onChange={(e) => setForm(p => ({ ...p, endTime: e.target.value }))}
className="border p-2 rounded w-full"
>
<option value="">اختر الوقت</option>
{TIME_OPTIONS.map((time) => (
<option key={time} value={time}>{time}</option>
))}
</select>
</div>
</div>
)}
{/* السبب */}
<textarea
placeholder="اكتب سبب الإجازة..."
value={form.reason}
onChange={(e) => setForm(p => ({ ...p, reason: e.target.value }))}
className="w-full border p-2 rounded"
/>
{/* أزرار */}
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setShowModal(false)}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
>
إلغاء
</button>
<button
type="submit"
disabled={submitting}
className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{submitting ? 'جاري الإرسال...' : 'إرسال الطلب'}
</button>
</div>
</form>
</Modal>
</div>
)
}