Feat/billing history #3
@@ -0,0 +1,336 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileText, Download, CreditCard, Clock, CheckCircle2, XCircle, AlertCircle, Loader, X, Receipt, Trash2, ArrowRight, RefreshCcw } from 'lucide-react';
|
||||
import NotificationModal from '../../components/ui/NotificationModal'; // Vérifie ton chemin
|
||||
import { getClient, getInvoiceList } from '../../services/billing_api'; // Vérifie ton chemin
|
||||
|
||||
export default function BillingHistory() {
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [customAlert, setCustomAlert] = useState(null);
|
||||
|
||||
// NOUVEL ÉTAT : La facture actuellement ouverte dans la modale
|
||||
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
||||
|
||||
const triggerAlert = (title, message, type) => {
|
||||
setCustomAlert({ title, message, type });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBillingData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const profileData = await getClient();
|
||||
if (!profileData || !profileData.id) {
|
||||
throw new Error("Impossible de vérifier votre identité ou session expirée.");
|
||||
}
|
||||
const invoicesData = await getInvoiceList(profileData.id);
|
||||
setInvoices(invoicesData.list || []);
|
||||
} catch (err) {
|
||||
setError(err.message || "Problème de connexion avec le serveur.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBillingData();
|
||||
}, []);
|
||||
|
||||
const getStatusBadge = (status, total = 0) => {
|
||||
const isNegative = parseFloat(total) < 0;
|
||||
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'paid':
|
||||
if (isNegative) {
|
||||
return { label: 'REMBOURSÉE', css: 'bg-blue-500/10 text-blue-400 border-blue-500/20', icon: <RefreshCcw className="w-3.5 h-3.5" /> };
|
||||
}
|
||||
return { label: 'PAYÉE', css: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', icon: <CheckCircle2 className="w-3.5 h-3.5" /> };
|
||||
case 'refunded':
|
||||
// NOUVEAU STATUT : REMBOURSÉE
|
||||
case 'unpaid':
|
||||
// SI IMPAYÉE MAIS NÉGATIVE = EN COURS DE TRAITEMENT
|
||||
if (isNegative) {
|
||||
return { label: 'REMBOURSEMENT EN COURS', css: 'bg-amber-500/10 text-amber-400 border-amber-500/20', icon: <Clock className="w-3.5 h-3.5" /> };
|
||||
}
|
||||
return { label: 'EN ATTENTE', css: 'bg-amber-500/10 text-amber-400 border-amber-500/20', icon: <Clock className="w-3.5 h-3.5" /> };
|
||||
case 'canceled':
|
||||
return { label: 'ANNULÉE', css: 'bg-gray-500/10 text-gray-400 border-gray-500/20', icon: <XCircle className="w-3.5 h-3.5" /> };
|
||||
default:
|
||||
return { label: status?.toUpperCase() || 'INCONNU', css: 'bg-blue-500/10 text-blue-400 border-blue-500/20', icon: <AlertCircle className="w-3.5 h-3.5" /> };
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '--/--/----';
|
||||
return new Date(dateString).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
};
|
||||
|
||||
// Fonctions vides pour la future logique
|
||||
const handlePayInvoice = (invoiceId) => {
|
||||
triggerAlert("Paiement initié", `Logique de paiement Stripe/PayPal à venir pour la facture #${invoiceId}.`, "info");
|
||||
};
|
||||
|
||||
const handleCancelInvoice = (invoiceId) => {
|
||||
triggerAlert("Annulation", `Logique d'annulation à venir pour la facture #${invoiceId}.`, "info");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl p-6 mx-auto relative">
|
||||
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-black text-white tracking-wider">HISTORIQUE DE <span className="text-cyan-400">FACTURATION</span></h1>
|
||||
<p className="text-gray-400 mt-2">Consultez vos reçus, payez vos transactions en attente et gérez vos factures.</p>
|
||||
</header>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center space-x-3 text-cyan-400 mb-6">
|
||||
<Loader className="w-6 h-6 animate-spin" />
|
||||
<span>Synchronisation avec le registre comptable...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center space-x-3 text-red-400 bg-red-400/10 border border-red-400 p-4 rounded-lg mb-6">
|
||||
<AlertCircle className="w-6 h-6" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && (
|
||||
<div className="bg-[#090f1c] border border-gray-800 rounded-lg overflow-hidden shadow-xl">
|
||||
{invoices.length === 0 ? (
|
||||
<div className="text-center py-12 px-4">
|
||||
<FileText className="w-10 h-10 mx-auto text-gray-600 mb-3" />
|
||||
<p className="text-base font-bold text-gray-400 tracking-wider uppercase">Aucune facture</p>
|
||||
<p className="text-sm text-gray-600 mt-1">Vos transactions apparaîtront ici dès qu'elles seront générées.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-900 border-b border-gray-800 text-gray-500 text-[10px] uppercase font-black tracking-widest">
|
||||
<th className="py-4 px-6">Référence</th>
|
||||
<th className="py-4 px-6">Date d'émission</th>
|
||||
<th className="py-4 px-6">Statut</th>
|
||||
<th className="py-4 px-6 text-right">Montant</th>
|
||||
<th className="py-4 px-6 text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-800/40 text-sm">
|
||||
{invoices.map((invoice) => {
|
||||
const badge = getStatusBadge(invoice.status, invoice.total);
|
||||
return (
|
||||
<tr key={invoice.id} className="hover:bg-gray-800/20 transition-colors">
|
||||
<td className="py-5 px-6 font-mono text-xs font-bold text-gray-300">
|
||||
{invoice.invoice_number}
|
||||
</td>
|
||||
<td className="py-5 px-6 text-gray-400 text-xs">
|
||||
{formatDate(invoice.created_at)}
|
||||
</td>
|
||||
<td className="py-5 px-6">
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black tracking-widest border ${badge.css}`}>
|
||||
{badge.icon}
|
||||
{badge.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-5 px-6 text-right font-mono font-bold text-white tracking-wider">
|
||||
{parseFloat(invoice.total).toFixed(2)} {invoice.currency || '€'}
|
||||
</td>
|
||||
<td className="py-5 px-6">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
{invoice.status?.toLowerCase() === 'unpaid' && parseFloat(invoice.total) >= 0 ? (
|
||||
// 🎯 Facture positive et impayée : BOUTON PAYER
|
||||
<button
|
||||
onClick={() => setSelectedInvoice(invoice)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-amber-500 hover:bg-amber-400 text-black text-[10px] font-black uppercase tracking-widest rounded transition-all shadow-[0_0_15px_rgba(245,158,11,0.2)]"
|
||||
>
|
||||
<CreditCard className="w-3.5 h-3.5" /> PAYER
|
||||
</button>
|
||||
) : (
|
||||
// 🎯 Facture payée, annulée, ou NÉGATIVE (Remboursement) : BOUTON DÉTAILS UNIQUEMENT
|
||||
<button
|
||||
onClick={() => setSelectedInvoice(invoice)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-gray-900 hover:bg-gray-800 text-gray-300 hover:text-cyan-400 border border-gray-700 hover:border-cyan-900 text-[10px] font-black uppercase tracking-widest rounded transition-all"
|
||||
>
|
||||
<Receipt className="w-3.5 h-3.5" /> DÉTAILS
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ========================================== */}
|
||||
{/* LA MODALE DE DÉTAILS DE FACTURE */}
|
||||
{selectedInvoice && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
|
||||
<div className="bg-[#090f1c] border border-gray-800 shadow-2xl shadow-cyan-500/10 rounded-2xl w-full max-w-2xl flex flex-col max-h-[90vh] overflow-hidden">
|
||||
|
||||
{/* HEADER MODALE */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-800 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-cyan-950/30 text-cyan-400 rounded-xl border border-cyan-900/50">
|
||||
<Receipt className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-white tracking-widest uppercase">
|
||||
FACTURE <span className="text-cyan-400">{selectedInvoice.invoice_number}</span>
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 font-medium mt-1">
|
||||
Émise le {formatDate(selectedInvoice.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setSelectedInvoice(null)} className="p-2 text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CORPS DE LA MODALE */}
|
||||
<div className="p-6 overflow-y-auto grow custom-scrollbar space-y-6">
|
||||
|
||||
{/* Statut Badge */}
|
||||
<div className="flex items-center justify-between p-4 bg-[#0d1527] rounded-xl border border-gray-800">
|
||||
<span className="text-xs font-bold text-gray-400 uppercase tracking-wider">Statut du document</span>
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest border ${getStatusBadge(selectedInvoice.status).css}`}>
|
||||
{getStatusBadge(selectedInvoice.status).icon}
|
||||
{getStatusBadge(selectedInvoice.status).label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Lignes de facture (Filtrées : uniquement les prix > 0) */}
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-3 border-b border-gray-800 pb-2">Détail des prestations</h4>
|
||||
<div className="space-y-4">
|
||||
{(() => {
|
||||
// FILTRE : On ne garde que les items dont le prix est strictement supérieur à 0
|
||||
const validItems = selectedInvoice.lines
|
||||
|
||||
if (validItems.length > 0) {
|
||||
return validItems.map((item, idx) => (
|
||||
<div key={idx} className="flex justify-between items-start text-sm">
|
||||
<div className="pr-4">
|
||||
<p className="text-white font-medium">{item.title}</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Quantité : {item.quantity}</p>
|
||||
</div>
|
||||
<div className="text-right font-mono font-bold text-gray-300 shrink-0">
|
||||
{(parseFloat(item.price) * parseInt(item.quantity)).toFixed(2)} {selectedInvoice.currency || '€'}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
// Fallback si la facture n'a que des items à 0€ ou aucun détail
|
||||
return (
|
||||
<div className="flex justify-between items-start text-sm">
|
||||
<div>
|
||||
<p className="text-white font-medium">Services d'infrastructure technique</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Détail inclus ou facturation sans frais complémentaires.</p>
|
||||
</div>
|
||||
<div className="text-right font-mono font-bold text-gray-300 shrink-0">
|
||||
{parseFloat(selectedInvoice.total).toFixed(2)} {selectedInvoice.currency || '€'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CALCULS (EXTRACTION TVA DEPUIS LE TTC) */}
|
||||
{(() => {
|
||||
// Le total récupéré de la BDD est considéré comme le PRIX FINAL (TTC)
|
||||
const totalTTC = parseFloat(selectedInvoice.total) || 0;
|
||||
const vatRate = 0.21; // TVA Belge à 21%
|
||||
|
||||
// Extraction du Hors Taxe : TTC / 1.21
|
||||
const subtotalHT = totalTTC / (1 + vatRate);
|
||||
// Le montant exact de la taxe
|
||||
const vatAmount = totalTTC - subtotalHT;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end pt-5 border-t border-gray-800">
|
||||
<div className="flex justify-between w-full sm:w-[60%] text-sm mb-2">
|
||||
<span className="text-gray-500">Sous-total (HT)</span>
|
||||
<span className="text-gray-400 font-mono">{subtotalHT.toFixed(2)} {selectedInvoice.currency || '€'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between w-full sm:w-[60%] text-sm mb-4">
|
||||
<span className="text-gray-500">TVA (21%)</span>
|
||||
<span className="text-gray-400 font-mono">{vatAmount.toFixed(2)} {selectedInvoice.currency || '€'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between w-full sm:w-[60%] text-lg border-t border-gray-800 pt-3">
|
||||
<span className="font-bold text-white uppercase tracking-wider">Total TTC</span>
|
||||
<span className="font-black text-cyan-400 font-mono text-xl">{totalTTC.toFixed(2)} {selectedInvoice.currency || '€'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
</div>
|
||||
|
||||
{/* PIED DE LA MODALE (Actions) */}
|
||||
<div className="p-6 border-t border-gray-800 bg-gray-900/50 shrink-0 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
|
||||
{selectedInvoice.status?.toLowerCase() === 'unpaid' && parseFloat(selectedInvoice.total) >= 0 ? (
|
||||
<>
|
||||
{/* Cas standard : Facture positive en attente de paiement */}
|
||||
<button
|
||||
onClick={() => handleCancelInvoice(selectedInvoice.id)}
|
||||
className="w-full sm:w-auto px-5 py-3 text-red-400 hover:text-white bg-red-500/10 hover:bg-red-500 border border-red-500/20 hover:border-red-500 text-xs font-bold uppercase tracking-wider rounded-xl transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Annuler la facture
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handlePayInvoice(selectedInvoice.id)}
|
||||
className="w-full sm:w-auto px-8 py-3 bg-amber-500 hover:bg-amber-400 text-black text-xs font-black uppercase tracking-widest rounded-xl transition-all shadow-[0_0_20px_rgba(245,158,11,0.2)] flex items-center justify-center gap-2"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" /> Procéder au paiement <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Cas alternatif : Facture payée, annulée, ou NÉGATIVE (Remboursement) */}
|
||||
<button onClick={() => setSelectedInvoice(null)} className="w-full sm:w-auto px-5 py-3 text-gray-400 hover:text-white bg-gray-900 hover:bg-gray-800 border border-gray-800 text-xs font-bold uppercase tracking-wider rounded-xl transition-all">
|
||||
Fermer
|
||||
</button>
|
||||
|
||||
{parseFloat(selectedInvoice.total) < 0 && selectedInvoice.status?.toLowerCase() === 'unpaid' && (
|
||||
<div className="text-xs font-bold text-amber-400 bg-amber-500/5 border border-amber-500/10 px-4 py-2.5 rounded-xl uppercase tracking-wider animate-pulse">
|
||||
⚡ Flux financier en cours de traitement
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedInvoice.status?.toLowerCase() === 'refunded' && (
|
||||
<div className="text-xs font-bold text-blue-400 bg-blue-500/5 border border-blue-500/10 px-4 py-2.5 rounded-xl uppercase tracking-wider">
|
||||
✓ Fonds reversés au client
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedInvoice.status?.toLowerCase() === 'paid' && (
|
||||
<a
|
||||
href={`/invoice/pdf/${selectedInvoice.id}`}
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
className="w-full sm:w-auto px-8 py-3 bg-cyan-500 hover:bg-cyan-400 text-black text-xs font-black uppercase tracking-widest rounded-xl transition-all shadow-[0_0_20px_rgba(6,182,212,0.2)] flex items-center justify-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" /> Télécharger le reçu
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user