update HR modules

This commit is contained in:
yotakii
2026-04-01 10:17:38 +03:00
parent 94d651c29e
commit 278d8f6982
16 changed files with 1920 additions and 232 deletions

View File

@@ -5,13 +5,11 @@ import { portalAPI } from '@/lib/api/portal'
import Modal from '@/components/Modal'
import LoadingSpinner from '@/components/LoadingSpinner'
import { toast } from 'react-hot-toast'
import { Calendar, Plus } from 'lucide-react'
import { Plus } from 'lucide-react'
const LEAVE_TYPES = [
{ value: 'ANNUAL', label: 'إجازة سنوية' },
{ value: 'SICK', label: 'إجازة مرضية' },
{ value: 'EMERGENCY', label: 'طوارئ' },
{ value: 'UNPAID', label: 'بدون راتب' },
{ value: 'HOURLY', label: 'إجازة ساعية' },
]
const STATUS_MAP: Record<string, { label: string; color: string }> = {
@@ -26,7 +24,16 @@ export default function PortalLeavePage() {
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [form, setForm] = useState({ leaveType: 'ANNUAL', startDate: '', endDate: '', reason: '' })
const [form, setForm] = useState({
leaveType: 'ANNUAL',
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
const load = () => {
setLoading(true)
@@ -43,24 +50,54 @@ export default function PortalLeavePage() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!form.startDate || !form.endDate) {
toast.error('أدخل تاريخ البداية والنهاية')
return
}
if (new Date(form.endDate) < new Date(form.startDate)) {
toast.error('تاريخ النهاية يجب أن يكون بعد تاريخ البداية')
return
}
setSubmitting(true)
portalAPI.submitLeaveRequest({
let payload: any = {
leaveType: form.leaveType,
startDate: form.startDate,
endDate: form.endDate,
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`
payload.endDate = `${form.leaveDate}T${form.endTime}:00`
}
setSubmitting(true)
portalAPI.submitLeaveRequest(payload)
.then(() => {
setShowModal(false)
setForm({ leaveType: 'ANNUAL', startDate: '', endDate: '', reason: '' })
setForm({
leaveType: 'ANNUAL',
startDate: '',
endDate: '',
leaveDate: '',
startTime: '',
endTime: '',
reason: '',
})
toast.success('تم إرسال طلب الإجازة')
load()
})
@@ -72,6 +109,8 @@ export default function PortalLeavePage() {
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
@@ -83,6 +122,7 @@ export default function PortalLeavePage() {
</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>
@@ -90,36 +130,42 @@ export default function PortalLeavePage() {
{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 === 'SICK' ? 'مرضية' : b.leaveType}
{b.leaveType === 'ANNUAL' ? 'سنوية' : b.leaveType}
</p>
<p className="text-2xl font-bold text-teal-600 mt-1">{b.available} يوم</p>
<p className="text-xs text-gray-500">من {b.totalDays + b.carriedOver} (مستخدم: {b.usedDays})</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>
<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 last:border-0">
<div key={l.id} className="flex justify-between items-center py-3 border-b">
<div>
<p className="font-medium">
{l.leaveType === 'ANNUAL' ? 'سنوية' : l.leaveType === 'SICK' ? 'مرضية' : l.leaveType} - {l.days} يوم
{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>
{l.rejectedReason && <p className="text-sm text-red-600">سبب الرفض: {l.rejectedReason}</p>}
</div>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${statusInfo.color}`}>
<span className={`px-3 py-1 rounded-full text-xs ${statusInfo.color}`}>
{statusInfo.label}
</span>
</div>
@@ -129,61 +175,118 @@ export default function PortalLeavePage() {
)}
</div>
{/* الفورم */}
<Modal isOpen={showModal} onClose={() => setShowModal(false)} title="طلب إجازة جديد">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">نوع الإجازة</label>
<select
value={form.leaveType}
onChange={(e) => setForm((p) => ({ ...p, leaveType: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
{LEAVE_TYPES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">من تاريخ</label>
<input
type="date"
value={form.startDate}
onChange={(e) => setForm((p) => ({ ...p, startDate: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
required
/>
{/* نوع الإجازة */}
<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>
<label className="block text-sm font-medium text-gray-700 mb-1">إلى تاريخ</label>
<input
type="date"
value={form.endDate}
onChange={(e) => setForm((p) => ({ ...p, endDate: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
required
/>
) : (
/* ساعية */
<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>
<input
type="time"
value={form.startTime}
onChange={(e) => setForm(p => ({ ...p, startTime: e.target.value }))}
className="border p-2 rounded w-full"
/>
</div>
<div>
<label className="text-sm">إلى الساعة</label>
<input
type="time"
value={form.endTime}
onChange={(e) => setForm(p => ({ ...p, endTime: e.target.value }))}
className="border p-2 rounded w-full"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">السبب</label>
<textarea
value={form.reason}
onChange={(e) => setForm((p) => ({ ...p, reason: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
rows={3}
/>
</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
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">
<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>
)
}
}