'use client'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { portalAPI, type ExpenseClaim } from '@/lib/api/portal'; import Modal from '@/components/Modal'; import { useAuth } from '@/contexts/AuthContext'; function formatDate(value?: string | null) { if (!value) return '-'; const d = new Date(value); if (Number.isNaN(d.getTime())) return value; return d.toLocaleDateString('en-CA'); } function getStatusLabel(status: string) { switch (status) { case 'PENDING': return 'قيد المراجعة'; case 'APPROVED': return 'مقبول'; case 'REJECTED': return 'مرفوض'; default: return status; } } function getStatusClasses(status: string) { switch (status) { case 'PENDING': return 'bg-yellow-100 text-yellow-800'; case 'APPROVED': return 'bg-green-100 text-green-800'; case 'REJECTED': return 'bg-red-100 text-red-800'; default: return 'bg-gray-100 text-gray-700'; } } export default function ManagedExpenseClaimsPage() { const { hasPermission } = useAuth(); const canMarkAsPaid = hasPermission('department_expense_claims', 'mark-as-paid'); const [claims, setClaims] = useState([]); const [loading, setLoading] = useState(true); const [statusFilter, setStatusFilter] = useState('PENDING'); const [paidFilter, setPaidFilter] = useState<'all' | 'paid' | 'unpaid'>('all'); const [searchQuery, setSearchQuery] = useState(''); const [submittingId, setSubmittingId] = useState(null); const [payingId, setPayingId] = useState(null); const [rejectModalOpen, setRejectModalOpen] = useState(false); const [selectedClaim, setSelectedClaim] = useState(null); const [rejectReason, setRejectReason] = useState(''); const searchParams = useSearchParams(); const claimId = searchParams.get('claimId'); async function loadClaims( status = statusFilter, search = searchQuery, paid: 'all' | 'paid' | 'unpaid' = paidFilter, ) { try { setLoading(true); const data = await portalAPI.getManagedExpenseClaims( status === 'all' ? undefined : status, search.trim() || undefined, paid, ); setClaims(data); } catch (error: any) { alert(error?.response?.data?.message || 'تعذر تحميل طلبات كشف المصاريف'); } finally { setLoading(false); } } useEffect(() => { // Debounce the search so we don't fire a request on every keystroke. const handle = setTimeout(() => { loadClaims(statusFilter, searchQuery, paidFilter); }, 400); return () => clearTimeout(handle); // eslint-disable-next-line react-hooks/exhaustive-deps }, [statusFilter, searchQuery, paidFilter]); async function openAttachment(attachment: any) { try { const blob = await portalAPI.viewExpenseClaimAttachment(attachment.id); const blobUrl = window.URL.createObjectURL( new Blob([blob], { type: attachment.mimeType }) ); window.open(blobUrl, '_blank'); setTimeout(() => { window.URL.revokeObjectURL(blobUrl); }, 10000); } catch (error) { alert('تعذر فتح المرفق'); } } async function handleApprove(id: string) { const note = window.prompt( 'ملاحظة مع الموافقة (اتركها فارغة إذا لا توجد):', '', ); if (note === null) return; try { setSubmittingId(id); await portalAPI.approveManagedExpenseClaim(id, note.trim() || undefined); await loadClaims(statusFilter, searchQuery, paidFilter); } catch (error: any) { alert(error?.response?.data?.message || 'تعذر تنفيذ الموافقة'); } finally { setSubmittingId(null); } } function openRejectModal(claim: ExpenseClaim) { setSelectedClaim(claim); setRejectReason(''); setRejectModalOpen(true); } async function handleRejectSubmit(e: React.FormEvent) { e.preventDefault(); if (!selectedClaim) return; if (!rejectReason.trim()) { alert('سبب الرفض مطلوب'); return; } try { setSubmittingId(selectedClaim.id); await portalAPI.rejectManagedExpenseClaim( selectedClaim.id, rejectReason.trim() ); setRejectModalOpen(false); setSelectedClaim(null); setRejectReason(''); await loadClaims(statusFilter, searchQuery, paidFilter); } catch (error: any) { alert(error?.response?.data?.message || 'تعذر تنفيذ الرفض'); } finally { setSubmittingId(null); } } async function handleTogglePaid(claim: ExpenseClaim, nextValue: boolean) { if (!canMarkAsPaid) return; if (payingId) return; const previousValue = Boolean(claim.isPaid); // Optimistic update for snappy UX. setClaims((prev) => prev.map((c) => (c.id === claim.id ? { ...c, isPaid: nextValue } : c)) ); setPayingId(claim.id); try { const updated = await portalAPI.markExpenseClaimPaid(claim.id, nextValue); // Sync local state with server response (in case server normalized anything). setClaims((prev) => prev.map((c) => c.id === claim.id ? { ...c, isPaid: Boolean(updated?.isPaid) } : c ) ); } catch (error: any) { // Rollback on error. setClaims((prev) => prev.map((c) => (c.id === claim.id ? { ...c, isPaid: previousValue } : c)) ); alert(error?.response?.data?.message || 'تعذر تحديث حالة القبض'); } finally { setPayingId(null); } } return (

طلبات كشف المصاريف للقسم

يمكنك مراجعة طلبات كشف المصاريف والموافقة عليها أو رفضها

setSearchQuery(e.target.value)} placeholder="الاسم أو الرقم الوظيفي" className="w-64 rounded-lg border border-gray-300 px-3 py-2 pe-8 text-sm" /> {searchQuery && ( )}

الطلبات

{loading ? (
جاري التحميل...
) : claims.length === 0 ? (
لا توجد طلبات ضمن هذا الفلتر
) : (
{claims.map((claim) => { const employeeName = claim.employee ? `${claim.employee.firstName} ${claim.employee.lastName}` : '-'; const isBusy = submittingId === claim.id; const isSelected = claim.id === claimId; return (
{claim.claimNumber} {getStatusLabel(claim.status)}
الموظف: {' '} {employeeName}
الرقم الوظيفي: {' '} {claim.employee?.uniqueEmployeeId || '-'}
إجمالي المبلغ: {' '} {claim.totalAmount ?? claim.amount ?? '-'}
تاريخ الإنشاء: {' '} {formatDate(claim.createdAt)}
عدد البنود: {' '} {Array.isArray(claim.items) ? claim.items.length : claim.description ? 1 : 0}
آخر تحديث: {' '} {formatDate(claim.updatedAt)}
{claim.description ? (
ملاحظات عامة: {' '} {claim.description}
) : null} {Array.isArray(claim.items) && claim.items.length > 0 ? (
البنود:
{claim.items.map((item: any, idx: number) => (
التاريخ:{' '} {formatDate(item.expenseDate)}
المبلغ:{' '} {item.amount ?? '-'}
اسم الجهة:{' '} {item.entityName || '-'}
المشروع / المناقصة: {' '} {item.projectOrTender || '-'}
الأوراق المثبتة: {' '} {item.proofRef || '-'}
البيان:{' '} {item.description || '-'}
))}
) : (
تاريخ المصروف:{' '} {formatDate(claim.expenseDate)}
المبلغ:{' '} {claim.amount ?? '-'}
المشروع / المناقصة: {' '} {claim.projectOrTender || '-'}
البيان:{' '} {claim.description || '-'}
)} {claim.attachments && claim.attachments.length > 0 ? (
المرفقات:
{claim.attachments.map((attachment) => ( {attachment.originalName} ))}
) : null} {claim.status === 'REJECTED' && claim.rejectedReason ? (
سبب الرفض:{' '} {claim.rejectedReason}
) : null} {claim.status === 'APPROVED' && claim.approvalNote ? (
ملاحظة المعتمِد:{' '} {claim.approvalNote}
) : null} {claim.status === 'APPROVED' ? (
handleTogglePaid(claim, e.target.checked)} className="h-4 w-4 cursor-pointer accent-emerald-600 disabled:cursor-not-allowed" /> {payingId === claim.id && ( جارٍ الحفظ... )} {!canMarkAsPaid && ( (للعرض فقط) )}
) : null} {claim.status === 'PENDING' ? (
) : null}
); })}
)}
{ setRejectModalOpen(false); setSelectedClaim(null); setRejectReason(''); }} title="رفض طلب كشف المصاريف" >
{selectedClaim ? ( <> سيتم رفض الطلب:{' '} {selectedClaim.claimNumber} ) : null}