ajout du paiement par virement

This commit is contained in:
LathanDevers
2026-07-01 00:24:43 +02:00
parent c29047e6fa
commit 3823af0ad9
7 changed files with 551 additions and 14 deletions
+4
View File
@@ -20,6 +20,8 @@ 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 Payment from './pages/app/Payment';
import OrderManagement from './pages/app/OrderManagement';
export default function App() {
return (
@@ -49,6 +51,8 @@ export default function App() {
<Route path="/checkout/:productId" element={<Checkout />} />
<Route path="/services" element={<Services />} />
<Route path="/support" element={<Support />} />
<Route path="/payment/:invoiceId" element={<Payment />} />
<Route path="/manage/:orderId" element={<OrderManagement />} />
</Route>
</Route>
@@ -1,6 +1,6 @@
import { Server, Database, Cloud, Globe } from 'lucide-react';
import { Server, Database, Cloud, Globe, Loader } from 'lucide-react';
export default function DashboardServiceCard({ order, onClick }) {
export default function DashboardServiceCard({ order, onClick, isActionLoading }) {
// Fonction Radar : Détecte le type de service selon son nom pour afficher la bonne icône
const getServiceIcon = (title) => {
const t = title.toLowerCase();
@@ -35,6 +35,13 @@ export default function DashboardServiceCard({ order, onClick }) {
return periods[periodCode] || periodCode;
};
const baseTitle = (order.title || '').split(/(?: for | pour )/i)[0].trim();
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' ');
const titleMatch = (order.title || '').match(/(?: for | pour )(.+)$/i);
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
let displayDomain = order.hostingDetails?.domain && order.hostingDetails.domain !== '127.0.0.1' ? order.hostingDetails.domain : domainFromTitle || order.domain;
if (!displayDomain || displayDomain === '127.0.0.1') displayDomain = null;
return (
<div
className="bg-gray-900 border border-gray-800 p-6 rounded-xl hover:border-cyan-400/50 transition-colors group cursor-pointer flex flex-col justify-between shadow-lg"
@@ -47,17 +54,29 @@ export default function DashboardServiceCard({ order, onClick }) {
</div>
{getStatusBadge(order.status)}
</div>
<h3 className="text-lg font-semibold text-white truncate" title={order.title}>
{order.title}
<h3 className="text-lg font-semibold text-white truncate" title={baseTitle}>
{shortTitle}
</h3>
<div className="flex flex-col gap-1 mt-2 mb-4 h-8 justify-center">
{displayDomain ? (<p className={`${shortTitle.toLowerCase().includes('vps') ? 'text-cyan-400' : 'text-emerald-400'} text-xs font-mono truncate`} title={displayDomain}>{displayDomain}</p>) : (<p className="text-gray-600 text-xs font-mono italic">En attente de déploiement</p>)}
</div>
<p className="text-sm text-gray-500 mt-1">
Facturation : <span className="text-cyan-400 font-medium">{formatPeriod(order.period)}</span>
</p>
</div>
<div className="mt-6 pt-4 border-t border-gray-800 flex justify-between items-center text-sm">
<span className="text-gray-400">ID Réseau: #{order.id}</span>
<span className="text-cyan-400 group-hover:underline">Gérer &gt;</span>
<button
onClick={(e) => { e.stopPropagation(); onClick(); }}
disabled={isActionLoading}
className="w-full bg-cyan-900/20 hover:bg-cyan-900/40 border border-cyan-800/50 hover:border-cyan-500 text-cyan-400 py-3 rounded-lg font-bold tracking-widest transition-all text-xs font-mono uppercase flex justify-center items-center gap-2"
>
{isActionLoading ? (
<><Loader className="w-4 h-4 animate-spin" /> SYNCHRONISATION...</>
) : (
"GÉRER L'INSTANCE"
)}
</button>
</div>
</div>
);
+6 -5
View File
@@ -111,11 +111,12 @@ export default function Checkout() {
};
await addToCart(productId, period, productConfig);
await checkoutCart();
// FOSSBilling retourne le hash sécurisé après le checkout
const checkoutResult = await checkoutCart();
const invoiceHash = checkoutResult?.hash || checkoutResult?.invoice_hash || checkoutResult?.id;
navigate('/dashboard', {
state: { successMessage: `Instance initialisée sur ${serverName}.gise.be` }
});
// On redirige vers /payment/LE_HASH
navigate(`/payment/${invoiceHash}`);
} catch (err) {
setError(err.message || "La transaction a échoué. L'API a refusé le contrat.");
@@ -185,7 +186,7 @@ export default function Checkout() {
disabled={isProcessing}
className="w-2/3 bg-cyan-400 hover:bg-cyan-500 text-gray-900 py-3 rounded-lg font-black tracking-widest transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(34,211,238,0.15)] font-mono"
>
{isProcessing ? 'PROVISIONNEMENT...' : 'INITIALISER L\'INSTANCE'}
{isProcessing ? 'GÉNÉRATION...' : 'GÉNÉRER LA FACTURE'}
</button>
</div>
</div>
+49 -2
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getClientOrders } from '../../services/api';
import { AlertCircle, Loader } from 'lucide-react';
import { getClientOrders, getClientInvoices } from '../../services/api';
// Importation des composants isolés
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
@@ -11,6 +11,7 @@ export default function Dashboard() {
const navigate = useNavigate();
const [orders, setOrders] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [loadingActionId, setLoadingActionId] = useState(null);
const [error, setError] = useState(null);
// Chargement des données à l'ouverture du Sas
@@ -48,6 +49,51 @@ export default function Dashboard() {
fetchInventory();
}, []);
const handleManageOrder = async (order) => {
// Sécurité pour éviter les doubles clics rapides
if (loadingActionId) return;
setLoadingActionId(order.id);
try {
let needsPayment = false;
let targetHash = null;
// 1. On vérifie la présence d'une facture liée
if (order.unpaid_invoice_id) {
const invoicesData = await getClientInvoices();
const invoicesList = Array.isArray(invoicesData) ? invoicesData : (invoicesData?.list || invoicesData?.result || []);
const targetInvoice = invoicesList.find(inv => inv.id == order.unpaid_invoice_id);
// 🌟 LE CORRECTIF EST ICI 🌟
// On vérifie que la facture est RÉELLEMENT impayée ('unpaid')
if (targetInvoice && targetInvoice.status === 'unpaid') {
needsPayment = true;
targetHash = targetInvoice.hash;
}
}
// 2. ROUTAGE DÉFINITIF
if (needsPayment && targetHash) {
// A. La facture est vraiment impayée -> Page de Paiement
navigate(`/payment/${targetHash}`);
}
else if (order.status === 'active') {
// B. Payé et Actif -> Page de Gestion du Serveur
navigate(`/manage/${order.id}`);
}
else {
// C. Payé mais en cours d'installation (Pending)
alert(`[ LOGISTIQUE NÉXUS ]\n\nInstance en cours de configuration sur le réseau.\nStatut actuel : ${order.status.toUpperCase()}`);
}
} catch (err) {
console.error(err);
alert("Erreur de communication avec la base de données de facturation.");
} finally {
setLoadingActionId(null);
}
};
return (
<div className="w-full max-w-6xl p-6 mx-auto">
@@ -80,7 +126,8 @@ export default function Dashboard() {
<DashboardServiceCard
key={order.id}
order={order}
onClick={() => navigate(`/services/${order.id}`)}
onClick={() => handleManageOrder(order)}
isActionLoading={loadingActionId === order.id}
/>
))}
+161
View File
@@ -0,0 +1,161 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getOrderDetails, getOrderUpgradables, createTicket, getHelpdesks } from '../../services/api';
import { Server, ArrowUpRight, ArrowDownRight, Trash2, ShieldCheck, Loader, AlertCircle } from 'lucide-react';
import NotificationModal from '../../components/ui/NotificationModal';
export default function OrderManagement() {
const { orderId } = useParams();
const navigate = useNavigate();
const [order, setOrder] = useState(null);
const [upgradables, setUpgradables] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
const [customAlert, setCustomAlert] = useState(null);
useEffect(() => {
const fetchOrderData = async () => {
try {
const orderData = await getOrderDetails(orderId);
setOrder(orderData);
// Si la commande est active, on cherche les upgrades possibles
if (orderData.status === 'active') {
const upgradeData = await getOrderUpgradables(orderId);
if (upgradeData) setUpgradables(upgradeData);
}
} catch (err) {
setCustomAlert({ type: 'error', title: 'Erreur', message: "Impossible de récupérer les données du contrat." });
} finally {
setIsLoading(false);
}
};
fetchOrderData();
}, [orderId]);
const handleUpgrade = async (targetProductId, targetProductName) => {
setIsProcessing(true);
try {
// 1. On récupère le département de support réseau (ou le premier disponible)
const hdesks = await getHelpdesks();
const targetHelpdeskId = Object.keys(hdesks)[1] || 1;
// 2. On formate une requête automatique claire pour l'administrateur
const subject = `[AUTO] Migration d'instance - ${order.title}`;
const message = `SYSTÈME NEXUS : Demande de migration automatisée.\n\nLe client demande le passage de l'instance #${order.id} (${order.title}) vers le forfait ID : ${targetProductId} (${targetProductName}).\n\nMerci de générer la facture de prorata depuis l'interface d'administration.`;
// 3. On génère le ticket silencieusement
await createTicket(subject, message, targetHelpdeskId);
setCustomAlert({
type: 'success',
title: 'Requête Transmise',
message: `La demande de migration vers ${targetProductName} a été envoyée aux ingénieurs GISE.\n\nUne facture de prorata sera générée dans votre espace d'ici quelques minutes. Vous serez notifié par e-mail.`
});
setTimeout(() => navigate('/support'), 4500);
} catch (err) {
setCustomAlert({ type: 'error', title: 'Échec de la transmission', message: err.message });
setIsProcessing(false);
}
};
const handleCancel = async () => {
if (!window.confirm("Alerte : Êtes-vous sûr de vouloir renoncer à cet abonnement ? Le service restera actif jusqu'à la fin de la période facturée.")) return;
setIsProcessing(true);
try {
const hdesks = await getHelpdesks();
const targetHelpdeskId = Object.keys(hdesks)[1] || 1;
const subject = `[AUTO] Demande de résiliation - ${order.title}`;
const message = `SYSTÈME NEXUS : Demande de résiliation automatisée.\n\nLe client demande la non-reconduction du contrat #${order.id} (${order.title}).\n\nL'instance doit être détruite à la fin du cycle de facturation actuel.`;
await createTicket(subject, message, targetHelpdeskId);
setCustomAlert({
type: 'success',
title: 'Résiliation Programmée',
message: "L'ordre d'arrêt a été transmis. L'abonnement ne sera pas renouvelé à la fin du cycle et l'instance sera détruite."
});
setTimeout(() => navigate('/support'), 4500);
} catch (err) {
setCustomAlert({ type: 'error', title: 'Échec', message: err.message });
setIsProcessing(false);
}
};
if (isLoading) {
return <div className="flex justify-center items-center mt-20 text-cyan-400 font-mono"><Loader className="w-8 h-8 animate-spin mr-3"/> ANALYSE DU CONTRAT...</div>;
}
if (!order) return null;
// FOSSBilling renvoie souvent les upgradables sous forme de { "id_produit": "Nom du produit" }
const upgradeOptions = Object.entries(upgradables);
return (
<div className="w-full max-w-4xl mx-auto mt-12 p-8 bg-gray-900 border border-gray-800 rounded-2xl shadow-2xl">
<div className="flex items-center gap-3 mb-8 border-b border-gray-800 pb-6">
<ShieldCheck className="w-8 h-8 text-cyan-400" />
<div>
<h2 className="text-2xl font-black text-white tracking-wider uppercase">Gestion du contrat : {order.title}</h2>
<p className="text-gray-400 font-mono text-sm">Contrat réseau #{order.id} Renouvellement : {order.period}</p>
</div>
</div>
{/* SECTIONS DES UPGRADES / DOWNGRADES */}
<h3 className="text-sm font-bold text-gray-400 tracking-widest uppercase mb-4 font-mono">Moduler l'infrastructure</h3>
{upgradeOptions.length === 0 ? (
<div className="bg-black/50 p-6 rounded-xl border border-gray-800 mb-8 text-center font-mono text-sm text-gray-500">
Aucune modification de forfait n'est disponible pour cette instance actuellement.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-10">
{upgradeOptions.map(([targetId, targetName]) => (
<div key={targetId} className="bg-black/40 border border-gray-800 p-5 rounded-xl flex justify-between items-center hover:border-cyan-500/50 transition-colors">
<span className="font-bold text-white font-mono">{targetName}</span>
<button
onClick={() => handleUpgrade(targetId, targetName)}
disabled={isProcessing}
className="bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500 hover:text-black border border-cyan-500/30 px-4 py-2 rounded text-xs font-bold transition-all disabled:opacity-50 uppercase tracking-widest flex items-center gap-1"
>
Migrer <ArrowUpRight className="w-4 h-4" />
</button>
</div>
))}
<div className="col-span-full mt-2 text-[10px] text-gray-500 font-mono text-center">
* Le système calculera automatiquement le prorata. Une facture de différence ou une note de crédit sera générée selon votre choix.
</div>
</div>
)}
{/* ZONE DANGER : RÉSILIATION */}
<div className="border-t border-red-900/30 pt-8 mt-4">
<h3 className="text-sm font-bold text-red-500 tracking-widest uppercase mb-4 font-mono flex items-center gap-2">
<AlertCircle className="w-4 h-4" /> Zone Critique
</h3>
<div className="bg-red-950/20 border border-red-900/30 p-6 rounded-xl flex flex-col md:flex-row justify-between items-center">
<div>
<h4 className="text-white font-bold mb-1">Renoncer à l'abonnement</h4>
<p className="text-xs text-gray-400 font-mono max-w-md">
L'instance restera opérationnelle jusqu'à la fin de la période facturée. Elle sera ensuite détruite et les données effacées.
</p>
</div>
<button
onClick={handleCancel}
disabled={isProcessing}
className="mt-4 md:mt-0 bg-red-500/10 hover:bg-red-500 text-red-400 hover:text-white border border-red-500/30 px-6 py-3 rounded text-xs font-bold tracking-widest uppercase transition flex items-center gap-2 disabled:opacity-50"
>
<Trash2 className="w-4 h-4" /> Résilier l'instance
</button>
</div>
</div>
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
</div>
);
}
+274
View File
@@ -0,0 +1,274 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getInvoiceDetails, getPaymentGateways, deleteOrder } from '../../services/api';
import { CreditCard, Smartphone, Wallet, ShieldCheck, Loader, ArrowRight, AlertCircle, Building, Check, Copy, Trash2 } from 'lucide-react';
export default function Payment() {
const { invoiceId } = useParams();
const navigate = useNavigate();
const [invoice, setInvoice] = useState(null);
const [gateways, setGateways] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
// 🌟 Nouvel état pour l'affichage natif des virements
const [showBankInstructions, setShowBankInstructions] = useState(false);
const [copied, setCopied] = useState(false);
useEffect(() => {
const fetchPaymentData = async () => {
try {
const invData = await getInvoiceDetails(invoiceId);
setInvoice(invData);
if (invData.status === 'paid') {
navigate('/dashboard');
return;
}
const gwData = await getPaymentGateways();
if (Array.isArray(gwData)) setGateways(gwData);
else if (gwData && Array.isArray(gwData.result)) setGateways(gwData.result);
else if (gwData && gwData.list) setGateways(gwData.list);
} catch (err) {
setError("Impossible de récupérer la facture sécurisée.");
} finally {
setIsLoading(false);
}
};
if (invoiceId) fetchPaymentData();
}, [invoiceId, navigate]);
const getGatewayStyle = (gatewayName) => {
const name = (gatewayName || '').toLowerCase();
if (name.includes('paypal')) return { icon: <Wallet className="w-6 h-6" />, color: 'hover:border-blue-500 hover:bg-blue-500/10 text-gray-400' };
if (name.includes('bancontact') || name.includes('mollie')) return { icon: <Smartphone className="w-6 h-6" />, color: 'hover:border-orange-500 hover:bg-orange-500/10 text-gray-400' };
if (name.includes('virement') || name.includes('bank') || name.includes('transfer') || name.includes('custom')) return { icon: <Building className="w-6 h-6" />, color: 'hover:border-purple-500 hover:bg-purple-500/10 text-gray-400' };
return { icon: <CreditCard className="w-6 h-6" />, color: 'hover:border-cyan-500 hover:bg-cyan-500/10 text-gray-400' };
};
const handlePayment = () => {
if (!selectedMethod || !invoice) return;
setIsProcessing(true);
// On cherche le moyen de paiement sélectionné dans la liste
const selectedGw = gateways.find(g => g.id === selectedMethod);
// 🌟 L'INTERCEPTEUR : Si c'est le module Custom (Virement), on affiche notre interface native
if (selectedGw && selectedGw.code === 'Custom') {
setTimeout(() => {
setIsProcessing(false);
setShowBankInstructions(true); // Bascule l'affichage
}, 600); // Petit délai de simulation pour l'UX
return;
}
// Sinon (Stripe, PayPal, Mollie...), on redirige vers l'URL bancaire générée par FOSSBilling
const billingBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://web.gise.be';
window.location.href = `${billingBaseUrl}/invoice/banklink/${invoice.hash}/${selectedMethod}`;
};
const handleCopyCommunication = () => {
navigator.clipboard.writeText(`${invoice.serie}${invoice.nr}`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleCancelOrder = async () => {
// La facture contient les lignes de commande. On récupère l'ID de la commande liée.
const orderId = invoice.lines && invoice.lines.length > 0 ? invoice.lines[0].order_id : null;
if (!orderId) {
alert("Impossible de lier cette facture à une commande.");
return;
}
if (!window.confirm("Ceci annulera définitivement cette commande et cette facture. Confirmer ?")) return;
setIsProcessing(true);
try {
await deleteOrder(orderId);
navigate('/dashboard');
} catch (err) {
alert("Erreur lors de la suppression : " + err.message);
setIsProcessing(false);
}
};
if (isLoading) {
return (
<div className="w-full max-w-md mx-auto mt-20 p-6 bg-gray-900 border border-gray-800 rounded-2xl text-center">
<p className="text-cyan-400 font-mono tracking-widest animate-pulse">RÉCUPÉRATION DU CONTRAT FINANCIER...</p>
</div>
);
}
if (error) {
return (
<div className="w-full max-w-md mx-auto mt-20 p-6 bg-gray-900 border border-red-500/50 rounded-2xl text-center">
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<p className="text-white font-mono tracking-widest mb-4">{error}</p>
<button onClick={() => navigate('/dashboard')} className="text-xs bg-red-500 text-black px-4 py-2 rounded font-bold">RETOUR AU TERMINAL</button>
</div>
);
}
// =========================================================================
// ÉCRAN 2 : INSTRUCTIONS DE VIREMENT NATIVES (Pas de redirection)
// =========================================================================
if (showBankInstructions) {
return (
<div className="w-full max-w-2xl mx-auto mt-12 p-8 bg-gray-900 border border-purple-500/50 rounded-2xl shadow-[0_0_30px_rgba(168,85,247,0.1)]">
<div className="flex items-center gap-3 mb-8 border-b border-gray-800 pb-6">
<Building className="w-8 h-8 text-purple-400" />
<div>
<h2 className="text-2xl font-black text-white tracking-wider">PROCÉDURE DE VIREMENT</h2>
<p className="text-gray-400 font-mono text-sm">Action manuelle requise pour activer l'instance</p>
</div>
</div>
<div className="bg-black/50 p-6 rounded-xl border border-gray-800 mb-8 text-center">
<p className="text-gray-400 text-sm mb-2">Montant exact à transférer :</p>
<p className="text-4xl font-black text-cyan-400 mb-6">{invoice.total} {invoice.currency}</p>
<div className="space-y-4 text-left max-w-sm mx-auto">
<div>
<span className="text-xs text-gray-500 font-mono uppercase tracking-widest">Bénéficiaire</span>
<div className="font-mono text-white bg-gray-950 p-3 rounded border border-gray-800">
GISE CLOUD SERVICES
</div>
</div>
<div>
<span className="text-xs text-gray-500 font-mono uppercase tracking-widest">IBAN (Compte Bancaire)</span>
<div className="font-mono text-purple-400 font-bold bg-gray-950 p-3 rounded border border-gray-800">
BE43 XXXX XXXX XXXX {/* 🌟 METS TON VRAI IBAN ICI 🌟 */}
</div>
</div>
<div>
<span className="text-xs text-gray-500 font-mono uppercase tracking-widest">Communication Structurée</span>
<div className="flex justify-between items-center bg-gray-950 p-3 rounded border border-gray-800">
<span className="font-mono text-white font-bold">{invoice.serie}{invoice.nr}</span>
<button onClick={handleCopyCommunication} className="text-gray-400 hover:text-cyan-400 transition-colors">
{copied ? <Check className="w-5 h-5 text-emerald-400" /> : <Copy className="w-5 h-5" />}
</button>
</div>
</div>
</div>
</div>
<div className="text-xs text-gray-500 font-mono mb-8 text-center bg-purple-500/10 p-4 rounded border border-purple-500/20">
L'instance sera provisionnée automatiquement sur l'hyperviseur dès réception des fonds par notre service comptable.
</div>
<button
onClick={() => navigate('/dashboard')}
className="w-full bg-cyan-600 hover:bg-cyan-500 text-white py-4 rounded-lg font-black tracking-widest transition-all shadow-[0_0_20px_rgba(34,211,238,0.2)] font-mono uppercase"
>
J'AI COMPRIS, RETOUR AU TERMINAL
</button>
</div>
);
}
// =========================================================================
// ÉCRAN 1 : SÉLECTION DU MOYEN DE PAIEMENT
// =========================================================================
return (
<div className="w-full max-w-2xl mx-auto mt-12 p-8 bg-gray-900 border border-gray-800 rounded-2xl shadow-2xl">
<div className="flex items-center gap-3 mb-8 border-b border-gray-800 pb-6">
<ShieldCheck className="w-8 h-8 text-emerald-400" />
<div>
<h2 className="text-2xl font-black text-white tracking-wider">PASSERELLE SÉCURISÉE</h2>
<p className="text-gray-400 font-mono text-sm">Règlement de la facture de provisionnement</p>
</div>
</div>
<div className="bg-black/50 p-6 rounded-xl border border-gray-800 mb-8 flex justify-between items-center">
<div>
<span className="text-xs text-gray-500 font-mono block uppercase">Description</span>
<span className="text-lg font-bold text-white">
{invoice.lines && invoice.lines.length > 0 ? invoice.lines[0].title : 'Instance Cloud'}
</span>
<span className="text-xs text-gray-500 font-mono block mt-1">Facture N° {invoice.serie}{invoice.nr}</span>
</div>
<div className="text-right">
<span className="text-xs text-gray-500 font-mono block uppercase">Montant TTC</span>
<span className="text-3xl font-black text-emerald-400">{invoice.total} {invoice.currency}</span>
</div>
</div>
<h3 className="text-sm font-bold text-gray-400 tracking-widest uppercase mb-4 font-mono">
Sélectionner un canal de paiement
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
{gateways.length === 0 ? (
<div className="col-span-full text-red-400 text-sm font-mono border border-red-500/20 bg-red-500/10 p-4 rounded-xl text-center">
Aucun terminal de paiement n'est configuré sur l'infrastructure (FOSSBilling).
</div>
) : (
gateways.map((gw) => {
const style = getGatewayStyle(gw.title);
const isSelected = selectedMethod === gw.id;
return (
<div
key={gw.id}
onClick={() => setSelectedMethod(gw.id)}
className={`p-4 rounded-xl border cursor-pointer transition-all flex flex-col items-center justify-center gap-3 h-28
${isSelected
? 'border-emerald-500 bg-emerald-500/10 shadow-[0_0_15px_rgba(16,185,129,0.2)]'
: `border-gray-800 bg-black/40 ${style.color}`
}
`}
>
<div className={isSelected ? 'text-emerald-400' : ''}>
{style.icon}
</div>
<span className={`font-bold font-mono text-sm text-center ${isSelected ? 'text-emerald-400' : 'text-gray-300'}`}>
{gw.title}
</span>
</div>
);
})
)}
</div>
<div className="flex space-x-4">
<button
onClick={() => navigate('/dashboard')}
disabled={isProcessing}
className="w-1/3 bg-transparent hover:bg-gray-800 text-gray-400 border border-gray-700 py-3 rounded-lg font-bold tracking-widest transition-colors font-mono"
>
ANNULER
</button>
<button
onClick={handleCancelOrder}
disabled={isProcessing}
className="w-1/4 flex justify-center items-center bg-red-900/20 hover:bg-red-900/40 text-red-500 border border-red-900/50 hover:border-red-500 py-3 rounded-lg font-bold transition-colors"
title="Supprimer la commande"
>
<Trash2 className="w-5 h-5" />
</button>
<button
onClick={handlePayment}
disabled={!selectedMethod || isProcessing || gateways.length === 0}
className="w-2/3 flex items-center justify-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-black py-3 rounded-lg font-black tracking-widest transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(16,185,129,0.15)] font-mono uppercase"
>
{isProcessing ? (
<><Loader className="w-5 h-5 animate-spin" /> CONNEXION... </>
) : (
<>VALIDER ({invoice.total} {invoice.currency}) <ArrowRight className="w-5 h-5" /></>
)}
</button>
</div>
</div>
);
}
+31
View File
@@ -221,3 +221,34 @@ export const replyTicket = (ticketId, message) =>
// Récupère la liste dynamique des départements (Helpdesks) configurés sur FOSSBilling
export const getHelpdesks = () =>
apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`, 'GET');
// ==========================================
// ROUTES FACTURATION & PAIEMENTS
// ==========================================
// Récupère les détails d'une facture générée
export const getInvoiceDetails = (invoiceHash) =>
apiCall(`${BASE_URL}/api/client/invoice/get`, 'POST', { hash: invoiceHash });
// Récupère la liste des terminaux de paiement (Gateways) activés sur FOSSBilling
export const getPaymentGateways = () =>
apiCall(`${BASE_URL}/api/guest/invoice/gateways`, 'GET');
// ==========================================
// GESTION DU CYCLE DE VIE (UPGRADE / CANCEL)
// ==========================================
// Récupère la liste de toutes les factures (utile pour trouver le hash d'une facture impayée)
export const getClientInvoices = () =>
apiCall(`${BASE_URL}/api/client/invoice/get_list`, 'GET');
// Récupère les détails spécifiques d'une commande
export const getOrderDetails = (orderId) =>
apiCall(`${BASE_URL}/api/client/order/get`, 'POST', { id: orderId });
// Liste les forfaits vers lesquels cette commande peut migrer
export const getOrderUpgradables = (orderId) =>
apiCall(`${BASE_URL}/api/client/order/upgradables`, 'POST', { id: orderId });
export const deleteOrder = (orderId) =>
apiCall(`${BASE_URL}/api/client/order/delete`, 'POST', { id: orderId });