Files
zerp/frontend/src/app/portal/leave/page.tsx
2026-06-03 13:01:51 +03:00

420 lines
14 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' },
}
const COMPANY_TIME_ZONE = 'Asia/Riyadh'
const COMPANY_UTC_OFFSET = '+03:00'
const toCompanyDateTime = (date: string, time: string) => {
return `${date}T${time}:00${COMPANY_UTC_OFFSET}`
}
const formatCompanyTime = (value: string) => {
return new Date(value).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: COMPANY_TIME_ZONE,
})
}
const formatCompanyDate = (value: string) => {
return new Date(value).toLocaleDateString('ar-SA', {
timeZone: COMPANY_TIME_ZONE,
})
}
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 [editingId, setEditingId] = useState<string | null>(null)
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 resetForm = () => {
setForm({
leaveType: 'ANNUAL',
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
setEditingId(null)
}
const openEdit = (l: any) => {
setEditingId(l.id)
if (l.leaveType === 'HOURLY') {
const start = new Date(l.startDate)
const end = new Date(l.endDate)
const dateStr = start.toLocaleDateString('en-CA', { timeZone: COMPANY_TIME_ZONE })
const fmt = (d: Date) =>
d.toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: COMPANY_TIME_ZONE,
})
setForm({
leaveType: 'HOURLY',
startDate: '',
endDate: '',
leaveDate: dateStr,
startTime: fmt(start),
endTime: fmt(end),
reason: l.reason || '',
})
} else {
setForm({
leaveType: 'ANNUAL',
startDate: String(l.startDate).split('T')[0],
endDate: String(l.endDate).split('T')[0],
leaveDate: '',
startTime: '',
endTime: '',
reason: l.reason || '',
})
}
setShowModal(true)
}
const handleDelete = async (id: string) => {
if (!confirm('حذف طلب الإجازة؟')) return
try {
await portalAPI.deleteLeaveRequest(id)
toast.success('تم حذف الطلب')
load()
} catch (err: any) {
toast.error(err?.response?.data?.message || 'فشل الحذف')
}
}
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.leaveDate = form.leaveDate
payload.startTime = form.startTime
payload.endTime = form.endTime
payload.startDate = toCompanyDateTime(form.leaveDate, form.startTime)
payload.endDate = toCompanyDateTime(form.leaveDate, form.endTime)
}
setSubmitting(true)
const action = editingId
? portalAPI.updateLeaveRequest(editingId, payload)
: portalAPI.submitLeaveRequest(payload)
action
.then(() => {
setShowModal(false)
resetForm()
toast.success(editingId ? 'تم تعديل الطلب' : 'تم إرسال طلب الإجازة')
load()
})
.catch((err: any) => {
const message =
err.response?.data?.message ||
err.response?.data?.error ||
'فشل إرسال الطلب'
console.error('Leave request error:', err.response?.data || err)
toast.error(message)
})
.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={() => { resetForm(); 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'
? `${formatCompanyTime(l.startDate)} - ${formatCompanyTime(l.endDate)}`
: `${l.days} يوم`}
</p>
<p className="text-sm text-gray-600">
{formatCompanyDate(l.startDate)} - {formatCompanyDate(l.endDate)}
</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-3 py-1 rounded-full text-xs ${statusInfo.color}`}>
{statusInfo.label}
</span>
{l.status === 'PENDING' && (
<>
<button
type="button"
onClick={() => openEdit(l)}
className="text-xs text-teal-600 hover:underline"
>
تعديل
</button>
<button
type="button"
onClick={() => handleDelete(l.id)}
className="text-xs text-red-600 hover:underline"
>
حذف
</button>
</>
)}
</div>
</div>
)
})}
</div>
)}
</div>
{/* الفورم */}
<Modal
isOpen={showModal}
onClose={() => { setShowModal(false); resetForm() }}
title={editingId ? 'تعديل طلب الإجازة' : 'طلب إجازة جديد'}
>
<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); resetForm() }}
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 ? 'جاري الإرسال...' : (editingId ? 'حفظ التعديل' : 'إرسال الطلب')}
</button>
</div>
</form>
</Modal>
</div>
)
}