diff --git a/src/App.jsx b/src/App.jsx index c982aca..7a6413c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,6 +20,7 @@ import Store from './pages/app/Store'; import Checkout from './pages/app/Checkout'; import Services from './pages/app/Services'; import Support from './pages/app/Support'; +import BillingHistory from './pages/app/BillingHistory'; export default function App() { return ( @@ -49,6 +50,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/src/components/dashboard/WebServiceSubscriptionManager.jsx b/src/components/dashboard/WebServiceSubscriptionManager.jsx index 19f1092..fb3ca8e 100644 --- a/src/components/dashboard/WebServiceSubscriptionManager.jsx +++ b/src/components/dashboard/WebServiceSubscriptionManager.jsx @@ -246,8 +246,9 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres // 🛡️ NOUVELLE LOGIQUE DE RÉSILIATION (US 2.1 / 2.2) // ========================================== const calculateRefundEligibility = () => { - if (!order || !order.created_at) return false; - const orderDate = new Date(order.created_at); + if (!order || !order.activated_at) return false; + const orderDate = new Date(order.activated_at); + console.log("Order Date:", orderDate); const now = new Date(); const diffDays = Math.ceil(Math.abs(now - orderDate) / (1000 * 60 * 60 * 24)); return diffDays <= 14; @@ -508,7 +509,7 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres

Votre abonnement a été activé il y a moins de 14 jours. Vous allez être intégralement remboursé. - Vos services HestiaCP et bases de données seront supprimés immédiatement. + Vos services seront supprimés immédiatement.

) : ( diff --git a/src/layouts/AppLayout.jsx b/src/layouts/AppLayout.jsx index 158dc86..d44ecea 100644 --- a/src/layouts/AppLayout.jsx +++ b/src/layouts/AppLayout.jsx @@ -74,10 +74,7 @@ export default function AppLayout() { -
- Facturation - WIP -
+ Facturation
Profil & Sécurité WIP diff --git a/src/pages/app/BillingHistory.jsx b/src/pages/app/BillingHistory.jsx new file mode 100644 index 0000000..3038973 --- /dev/null +++ b/src/pages/app/BillingHistory.jsx @@ -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: }; + } + return { label: 'PAYÉE', css: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', icon: }; + 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: }; + } + return { label: 'EN ATTENTE', css: 'bg-amber-500/10 text-amber-400 border-amber-500/20', icon: }; + case 'canceled': + return { label: 'ANNULÉE', css: 'bg-gray-500/10 text-gray-400 border-gray-500/20', icon: }; + default: + return { label: status?.toUpperCase() || 'INCONNU', css: 'bg-blue-500/10 text-blue-400 border-blue-500/20', icon: }; + } + }; + + 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 ( +
+ +
+

HISTORIQUE DE FACTURATION

+

Consultez vos reçus, payez vos transactions en attente et gérez vos factures.

+
+ + {isLoading && ( +
+ + Synchronisation avec le registre comptable... +
+ )} + + {error && ( +
+ + {error} +
+ )} + + {!isLoading && !error && ( +
+ {invoices.length === 0 ? ( +
+ +

Aucune facture

+

Vos transactions apparaîtront ici dès qu'elles seront générées.

+
+ ) : ( +
+ + + + + + + + + + + + {invoices.map((invoice) => { + const badge = getStatusBadge(invoice.status, invoice.total); + return ( + + + + + + + + ); + })} + +
RéférenceDate d'émissionStatutMontantActions
+ {invoice.invoice_number} + + {formatDate(invoice.created_at)} + + + {badge.icon} + {badge.label} + + + {parseFloat(invoice.total).toFixed(2)} {invoice.currency || '€'} + +
+ {invoice.status?.toLowerCase() === 'unpaid' && parseFloat(invoice.total) >= 0 ? ( + // 🎯 Facture positive et impayée : BOUTON PAYER + + ) : ( + // 🎯 Facture payée, annulée, ou NÉGATIVE (Remboursement) : BOUTON DÉTAILS UNIQUEMENT + + )} +
+
+
+ )} +
+ )} + + {/* ========================================== */} + {/* LA MODALE DE DÉTAILS DE FACTURE */} + {selectedInvoice && ( +
+
+ + {/* HEADER MODALE */} +
+
+
+ +
+
+

+ FACTURE {selectedInvoice.invoice_number} +

+

+ Émise le {formatDate(selectedInvoice.created_at)} +

+
+
+ +
+ + {/* CORPS DE LA MODALE */} +
+ + {/* Statut Badge */} +
+ Statut du document + + {getStatusBadge(selectedInvoice.status).icon} + {getStatusBadge(selectedInvoice.status).label} + +
+ + {/* Lignes de facture (Filtrées : uniquement les prix > 0) */} +
+

Détail des prestations

+
+ {(() => { + // 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) => ( +
+
+

{item.title}

+

Quantité : {item.quantity}

+
+
+ {(parseFloat(item.price) * parseInt(item.quantity)).toFixed(2)} {selectedInvoice.currency || '€'} +
+
+ )); + } else { + // Fallback si la facture n'a que des items à 0€ ou aucun détail + return ( +
+
+

Services d'infrastructure technique

+

Détail inclus ou facturation sans frais complémentaires.

+
+
+ {parseFloat(selectedInvoice.total).toFixed(2)} {selectedInvoice.currency || '€'} +
+
+ ); + } + })()} +
+
+ + {/* 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 ( +
+
+ Sous-total (HT) + {subtotalHT.toFixed(2)} {selectedInvoice.currency || '€'} +
+
+ TVA (21%) + {vatAmount.toFixed(2)} {selectedInvoice.currency || '€'} +
+
+ Total TTC + {totalTTC.toFixed(2)} {selectedInvoice.currency || '€'} +
+
+ ); + })()} + +
+ + {/* PIED DE LA MODALE (Actions) */} +
+ + {selectedInvoice.status?.toLowerCase() === 'unpaid' && parseFloat(selectedInvoice.total) >= 0 ? ( + <> + {/* Cas standard : Facture positive en attente de paiement */} + + + + + ) : ( + <> + {/* Cas alternatif : Facture payée, annulée, ou NÉGATIVE (Remboursement) */} + + + {parseFloat(selectedInvoice.total) < 0 && selectedInvoice.status?.toLowerCase() === 'unpaid' && ( +
+ ⚡ Flux financier en cours de traitement +
+ )} + + {selectedInvoice.status?.toLowerCase() === 'refunded' && ( +
+ ✓ Fonds reversés au client +
+ )} + + {selectedInvoice.status?.toLowerCase() === 'paid' && ( + + Télécharger le reçu + + )} + + )} +
+
+
+ )} + + setCustomAlert(null)} /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/app/Dashboard.jsx b/src/pages/app/Dashboard.jsx index 85f9dd3..4ca61aa 100644 --- a/src/pages/app/Dashboard.jsx +++ b/src/pages/app/Dashboard.jsx @@ -70,6 +70,7 @@ export default function Dashboard() { // 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable ! const handleManageSubscription = (order) => { + console.log("Gestion de l'abonnement pour la commande :", order); // 1. Détection du type de service const titleLower = (order.title || '').toLowerCase(); const isVPS = titleLower.includes('vps') || titleLower.includes('compute'); diff --git a/src/services/billing_api.js b/src/services/billing_api.js index 19a67fe..ffc5b14 100644 --- a/src/services/billing_api.js +++ b/src/services/billing_api.js @@ -305,12 +305,12 @@ export const updateHostingPlan = (order_id, new_plan_id) => // Suppression // Modification // Lister -export const getInvoiceList = () => - apiCall(`${BASE_URL}/api/client/invoice/get_list`); +export const getInvoiceList = (client_id) => + apiCall(`${BASE_URL}/api/client/invoice/get_list`, { client_id: client_id }); // Lire -export const getInvoiceDetails = (InvoiceHash) => - apiCall(`${BASE_URL}/api/client/invoice/get`); +export const getInvoiceDetails = (invoiceHash) => + apiCall(`${BASE_URL}/api/client/invoice/get`, { hash: invoiceHash }); // ==========================================