addition expense claims

This commit is contained in:
Aya
2026-04-22 11:36:47 +03:00
parent e262d8c09c
commit 0a9e1bbd4d
16 changed files with 1553 additions and 31 deletions

View File

@@ -0,0 +1,409 @@
'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';
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 [claims, setClaims] = useState<ExpenseClaim[]>([]);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState('PENDING');
const [submittingId, setSubmittingId] = useState<string | null>(null);
const [rejectModalOpen, setRejectModalOpen] = useState(false);
const [selectedClaim, setSelectedClaim] = useState<ExpenseClaim | null>(null);
const [rejectReason, setRejectReason] = useState('');
const searchParams = useSearchParams();
const claimId = searchParams.get('claimId');
async function loadClaims(status = statusFilter) {
try {
setLoading(true);
const data = await portalAPI.getManagedExpenseClaims(
status === 'all' ? undefined : status
);
setClaims(data);
} catch (error: any) {
alert(error?.response?.data?.message || 'تعذر تحميل طلبات كشف المصاريف');
} finally {
setLoading(false);
}
}
useEffect(() => {
loadClaims(statusFilter);
}, [statusFilter]);
async function handleApprove(id: string) {
const confirmed = window.confirm('هل أنت متأكد من الموافقة على طلب كشف المصاريف؟');
if (!confirmed) return;
try {
setSubmittingId(id);
await portalAPI.approveManagedExpenseClaim(id);
await loadClaims(statusFilter);
} 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<HTMLFormElement>) {
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);
} catch (error: any) {
alert(error?.response?.data?.message || 'تعذر تنفيذ الرفض');
} finally {
setSubmittingId(null);
}
}
return (
<div className="space-y-6" dir="rtl">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="text-2xl font-bold text-gray-900">
طلبات كشف المصاريف للقسم
</h1>
<p className="mt-1 text-sm text-gray-500">
يمكنك مراجعة طلبات كشف المصاريف والموافقة عليها أو رفضها
</p>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">الحالة:</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
>
<option value="PENDING">قيد المراجعة</option>
<option value="APPROVED">مقبول</option>
<option value="REJECTED">مرفوض</option>
<option value="all">الكل</option>
</select>
</div>
</div>
<div className="rounded-xl border bg-white shadow-sm">
<div className="border-b px-5 py-4">
<h2 className="text-lg font-semibold text-gray-900">الطلبات</h2>
</div>
{loading ? (
<div className="p-6 text-sm text-gray-500">جاري التحميل...</div>
) : claims.length === 0 ? (
<div className="p-6 text-sm text-gray-500">
لا توجد طلبات ضمن هذا الفلتر
</div>
) : (
<div className="divide-y">
{claims.map((claim) => {
const employeeName = claim.employee
? `${claim.employee.firstName} ${claim.employee.lastName}`
: '-';
const isBusy = submittingId === claim.id;
const isSelected = claim.id === claimId;
return (
<div
key={claim.id}
className={`p-5 ${
isSelected
? 'bg-yellow-50 ring-2 ring-yellow-300 rounded-lg'
: ''
}`}
>
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="space-y-3 w-full">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-base font-bold text-gray-900">
{claim.claimNumber}
</span>
<span
className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${getStatusClasses(
claim.status
)}`}
>
{getStatusLabel(claim.status)}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2 text-sm text-gray-600">
<div>
<span className="font-medium text-gray-800">
الموظف:
</span>{' '}
{employeeName}
</div>
<div>
<span className="font-medium text-gray-800">
الرقم الوظيفي:
</span>{' '}
{claim.employee?.uniqueEmployeeId || '-'}
</div>
<div>
<span className="font-medium text-gray-800">
إجمالي المبلغ:
</span>{' '}
{claim.totalAmount ?? claim.amount ?? '-'}
</div>
<div>
<span className="font-medium text-gray-800">
تاريخ الإنشاء:
</span>{' '}
{formatDate(claim.createdAt)}
</div>
<div>
<span className="font-medium text-gray-800">
عدد البنود:
</span>{' '}
{Array.isArray(claim.items)
? claim.items.length
: claim.description
? 1
: 0}
</div>
<div>
<span className="font-medium text-gray-800">
آخر تحديث:
</span>{' '}
{formatDate(claim.updatedAt)}
</div>
</div>
{claim.description ? (
<div className="text-sm text-gray-700">
<span className="font-medium text-gray-800">
ملاحظات عامة:
</span>{' '}
{claim.description}
</div>
) : null}
{Array.isArray(claim.items) && claim.items.length > 0 ? (
<div className="mt-3 space-y-2">
<div className="text-sm font-medium text-gray-800">
البنود:
</div>
<div className="space-y-2">
{claim.items.map((item: any, idx: number) => (
<div
key={idx}
className="rounded-lg border bg-gray-50 p-3 text-sm text-gray-700"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div>
<span className="font-medium">التاريخ:</span>{' '}
{formatDate(item.expenseDate)}
</div>
<div>
<span className="font-medium">المبلغ:</span>{' '}
{item.amount ?? '-'}
</div>
<div>
<span className="font-medium">اسم الجهة:</span>{' '}
{item.entityName || '-'}
</div>
<div>
<span className="font-medium">
المشروع / المناقصة:
</span>{' '}
{item.projectOrTender || '-'}
</div>
<div className="md:col-span-2">
<span className="font-medium">
الأوراق المثبتة:
</span>{' '}
{item.proofRef || '-'}
</div>
<div className="md:col-span-2">
<span className="font-medium">البيان:</span>{' '}
{item.description || '-'}
</div>
</div>
</div>
))}
</div>
</div>
) : (
<div className="mt-3 rounded-lg border bg-gray-50 p-3 text-sm text-gray-700">
<div>
<span className="font-medium">تاريخ المصروف:</span>{' '}
{formatDate(claim.expenseDate)}
</div>
<div>
<span className="font-medium">المبلغ:</span>{' '}
{claim.amount ?? '-'}
</div>
<div>
<span className="font-medium">
المشروع / المناقصة:
</span>{' '}
{claim.projectOrTender || '-'}
</div>
<div>
<span className="font-medium">البيان:</span>{' '}
{claim.description || '-'}
</div>
</div>
)}
{claim.status === 'REJECTED' && claim.rejectedReason ? (
<div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">
<span className="font-medium">سبب الرفض:</span>{' '}
{claim.rejectedReason}
</div>
) : null}
{claim.status === 'PENDING' ? (
<div className="flex items-center gap-2 pt-2">
<button
onClick={() => handleApprove(claim.id)}
disabled={isBusy}
className="rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:opacity-60"
>
{isBusy ? 'جارٍ التنفيذ...' : 'موافقة'}
</button>
<button
onClick={() => openRejectModal(claim)}
disabled={isBusy}
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
>
رفض
</button>
</div>
) : null}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
<Modal
isOpen={rejectModalOpen}
onClose={() => {
setRejectModalOpen(false);
setSelectedClaim(null);
setRejectReason('');
}}
title="رفض طلب كشف المصاريف"
>
<form onSubmit={handleRejectSubmit} className="space-y-4">
<div className="text-sm text-gray-600">
{selectedClaim ? (
<>
سيتم رفض الطلب:{' '}
<span className="font-semibold">
{selectedClaim.claimNumber}
</span>
</>
) : null}
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
سبب الرفض
</label>
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
className="min-h-[120px] w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-red-500"
placeholder="اكتب سبب الرفض"
required
/>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<button
type="button"
onClick={() => {
setRejectModalOpen(false);
setSelectedClaim(null);
setRejectReason('');
}}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
إلغاء
</button>
<button
type="submit"
disabled={!selectedClaim || submittingId === selectedClaim?.id}
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
>
{selectedClaim && submittingId === selectedClaim.id
? 'جارٍ التنفيذ...'
: 'تأكيد الرفض'}
</button>
</div>
</form>
</Modal>
</div>
);
}