✨ Feat: Invoice viewing, JoFotara UBL refinements, delete functionality, and tax rate validation
This commit is contained in:
@@ -37,6 +37,12 @@ export const InvoicesPage = () => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// View Modal State
|
||||
const [viewingInvoice, setViewingInvoice] = useState<any | null>(null);
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState<string | null>(null);
|
||||
const [submitLoading, setSubmitLoading] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -90,6 +96,36 @@ export const InvoicesPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('هل أنت متأكد من حذف هذه الفاتورة نهائياً؟')) return;
|
||||
setDeleteLoading(id);
|
||||
try {
|
||||
await apiClient.post(`/invoices/${id}/delete`);
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
alert('فشل حذف الفاتورة');
|
||||
} finally {
|
||||
setDeleteLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitToJoFotara = async (inv: any) => {
|
||||
if (inv.status !== 'validated' && inv.status !== 'extracted') {
|
||||
alert('يجب أن تكون الفاتورة مدققة أو مستخرجة أولاً');
|
||||
return;
|
||||
}
|
||||
setSubmitLoading(inv.id);
|
||||
try {
|
||||
await apiClient.post(`/invoices/${inv.id}/submit`);
|
||||
alert('تم الإرسال لـ جو فوترة بنجاح! 🎉');
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
alert('فشل الإرسال لـ جو فوترة. تأكد من إعدادات الشركة وصحة البيانات.');
|
||||
} finally {
|
||||
setSubmitLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
@@ -211,9 +247,13 @@ export const InvoicesPage = () => {
|
||||
<td className="px-6 py-4 text-center relative">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); console.log('View', inv.id); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setViewingInvoice(inv);
|
||||
setIsViewModalOpen(true);
|
||||
}}
|
||||
className="p-2 text-slate-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition-all"
|
||||
title="عرض التفاصيل"
|
||||
title="عرض الفاتورة"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -228,27 +268,22 @@ export const InvoicesPage = () => {
|
||||
{/* Dropdown Menu */}
|
||||
<div className="absolute left-0 mt-2 w-48 bg-white border border-slate-100 rounded-2xl shadow-xl shadow-slate-200/50 opacity-0 invisible group-hover/menu:opacity-100 group-hover/menu:visible transition-all z-10 py-2">
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (inv.status !== 'validated') {
|
||||
alert('يجب أن تكون الفاتورة مدققة أولاً');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post(`/invoices/${inv.id}/submit`);
|
||||
alert('تم الإرسال لجو فوترة بنجاح! 🎉');
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
alert('فشل الإرسال لجو فوترة');
|
||||
}
|
||||
}}
|
||||
className="w-full text-right px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 flex items-center gap-2"
|
||||
disabled={submitLoading === inv.id}
|
||||
onClick={(e) => { e.stopPropagation(); handleSubmitToJoFotara(inv); }}
|
||||
className="w-full text-right px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
<Send className="w-4 h-4 text-emerald-500" />
|
||||
{submitLoading === inv.id ? (
|
||||
<div className="w-4 h-4 border-2 border-primary-600/30 border-t-primary-600 rounded-full animate-spin" />
|
||||
) : (
|
||||
<Send className="w-4 h-4 text-emerald-500" />
|
||||
)}
|
||||
إرسال لـ جو فوترة
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); window.open(inv.original_file_path, '_blank'); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`${apiClient.defaults.baseURL}/invoices/${inv.id}/file`, '_blank');
|
||||
}}
|
||||
className="w-full text-right px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 flex items-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4 text-slate-400" />
|
||||
@@ -256,9 +291,15 @@ export const InvoicesPage = () => {
|
||||
</button>
|
||||
<div className="h-px bg-slate-100 my-1 mx-2" />
|
||||
<button
|
||||
className="w-full text-right px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
|
||||
disabled={deleteLoading === inv.id}
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(inv.id); }}
|
||||
className="w-full text-right px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{deleteLoading === inv.id ? (
|
||||
<div className="w-4 h-4 border-2 border-red-600/30 border-t-red-600 rounded-full animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
حذف الفاتورة
|
||||
</button>
|
||||
</div>
|
||||
@@ -372,6 +413,89 @@ export const InvoicesPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* ── View Invoice Modal ─────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{isViewModalOpen && viewingInvoice && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="bg-white rounded-[40px] w-full max-w-5xl h-[90vh] shadow-2xl flex flex-col overflow-hidden"
|
||||
>
|
||||
<header className="px-8 py-6 border-b border-slate-100 flex items-center justify-between bg-white">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-slate-900">معاينة الفاتورة</h3>
|
||||
<p className="text-slate-500 font-medium">رقم: {viewingInvoice.invoice_number || '---'} • {viewingInvoice.company?.name}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsViewModalOpen(false)}
|
||||
className="w-12 h-12 flex items-center justify-center rounded-2xl bg-slate-50 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all"
|
||||
>
|
||||
<ChevronLeft className="w-6 h-6 rotate-180" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-auto bg-slate-100 p-8 flex justify-center items-start">
|
||||
<div className="bg-white shadow-2xl rounded-sm overflow-hidden max-w-full">
|
||||
<img
|
||||
src={`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file`}
|
||||
alt="Invoice"
|
||||
className="max-w-full h-auto"
|
||||
onError={(e) => {
|
||||
// Fallback for PDF or Error
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.parentElement!.innerHTML = `
|
||||
<div class="p-20 text-center">
|
||||
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-slate-200">
|
||||
<svg class="w-10 h-10 text-slate-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-slate-900 mb-2">تعذر عرض الصورة مباشرة</h4>
|
||||
<p class="text-slate-500 mb-6">قد يكون الملف بتنسيق PDF أو حدث خطأ أثناء التحميل.</p>
|
||||
<a href="${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file" target="_blank" class="btn-primary px-8 py-3 rounded-xl inline-block">فتح الملف في نافذة جديدة</a>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="px-8 py-6 border-t border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<div className="flex items-center gap-6">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-400 font-bold mb-1">المجموع الكلي</p>
|
||||
<p className="text-xl font-black text-primary-700">{Number(viewingInvoice.grand_total).toLocaleString('en-US', { minimumFractionDigits: 3 })} JOD</p>
|
||||
</div>
|
||||
<div className="w-px h-10 bg-slate-200" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-400 font-bold mb-1">الحالة</p>
|
||||
<StatusBadge status={viewingInvoice.status} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => window.open(`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file`, '_blank')}
|
||||
className="px-6 py-3 rounded-xl bg-white border border-slate-200 text-slate-700 font-bold flex items-center gap-2 hover:bg-slate-50 transition-all shadow-sm"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
تحميل الأصلي
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsViewModalOpen(false);
|
||||
handleSubmitToJoFotara(viewingInvoice);
|
||||
}}
|
||||
className="btn-primary px-8 py-3 rounded-xl flex items-center gap-2 shadow-lg shadow-primary-500/20"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
إرسال لجو فوترة
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user