Merge pull request 'Feat/billing history' (#3) from feat/billing_history into master
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 14s
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 14s
Reviewed-on: maximus/portail-gise#3
This commit was merged in pull request #3.
This commit is contained in:
@@ -20,6 +20,7 @@ import Store from './pages/app/Store';
|
|||||||
import Checkout from './pages/app/Checkout';
|
import Checkout from './pages/app/Checkout';
|
||||||
import Services from './pages/app/Services';
|
import Services from './pages/app/Services';
|
||||||
import Support from './pages/app/Support';
|
import Support from './pages/app/Support';
|
||||||
|
import BillingHistory from './pages/app/BillingHistory';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +50,7 @@ export default function App() {
|
|||||||
<Route path="/checkout/:productId" element={<Checkout />} />
|
<Route path="/checkout/:productId" element={<Checkout />} />
|
||||||
<Route path="/services" element={<Services />} />
|
<Route path="/services" element={<Services />} />
|
||||||
<Route path="/support" element={<Support />} />
|
<Route path="/support" element={<Support />} />
|
||||||
|
<Route path="/facturation" element={<BillingHistory />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -246,8 +246,9 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
// 🛡️ NOUVELLE LOGIQUE DE RÉSILIATION (US 2.1 / 2.2)
|
// 🛡️ NOUVELLE LOGIQUE DE RÉSILIATION (US 2.1 / 2.2)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const calculateRefundEligibility = () => {
|
const calculateRefundEligibility = () => {
|
||||||
if (!order || !order.created_at) return false;
|
if (!order || !order.activated_at) return false;
|
||||||
const orderDate = new Date(order.created_at);
|
const orderDate = new Date(order.activated_at);
|
||||||
|
console.log("Order Date:", orderDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffDays = Math.ceil(Math.abs(now - orderDate) / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(Math.abs(now - orderDate) / (1000 * 60 * 60 * 24));
|
||||||
return diffDays <= 14;
|
return diffDays <= 14;
|
||||||
@@ -508,7 +509,7 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
<p className="text-emerald-500/80 mt-2">
|
<p className="text-emerald-500/80 mt-2">
|
||||||
Votre abonnement a été activé il y a moins de 14 jours.
|
Votre abonnement a été activé il y a moins de 14 jours.
|
||||||
<strong className="text-emerald-400"> Vous allez être intégralement remboursé</strong>.
|
<strong className="text-emerald-400"> Vous allez être intégralement remboursé</strong>.
|
||||||
Vos services HestiaCP et bases de données seront supprimés immédiatement.
|
Vos services seront supprimés immédiatement.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ export default function AppLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
<NavLink className={navLinkClass} to="/facturation">Facturation</NavLink>
|
||||||
<span>Facturation</span>
|
|
||||||
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
||||||
<span>Profil & Sécurité</span>
|
<span>Profil & Sécurité</span>
|
||||||
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
||||||
const handleManageSubscription = (order) => {
|
const handleManageSubscription = (order) => {
|
||||||
|
console.log("Gestion de l'abonnement pour la commande :", order);
|
||||||
// 1. Détection du type de service
|
// 1. Détection du type de service
|
||||||
const titleLower = (order.title || '').toLowerCase();
|
const titleLower = (order.title || '').toLowerCase();
|
||||||
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
|
|||||||
@@ -305,12 +305,12 @@ export const updateHostingPlan = (order_id, new_plan_id) =>
|
|||||||
// Suppression
|
// Suppression
|
||||||
// Modification
|
// Modification
|
||||||
// Lister
|
// Lister
|
||||||
export const getInvoiceList = () =>
|
export const getInvoiceList = (client_id) =>
|
||||||
apiCall(`${BASE_URL}/api/client/invoice/get_list`);
|
apiCall(`${BASE_URL}/api/client/invoice/get_list`, { client_id: client_id });
|
||||||
|
|
||||||
// Lire
|
// Lire
|
||||||
export const getInvoiceDetails = (InvoiceHash) =>
|
export const getInvoiceDetails = (invoiceHash) =>
|
||||||
apiCall(`${BASE_URL}/api/client/invoice/get`);
|
apiCall(`${BASE_URL}/api/client/invoice/get`, { hash: invoiceHash });
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user