From 3823af0ad9e5a14780136540225b94b1227d4000 Mon Sep 17 00:00:00 2001 From: LathanDevers Date: Wed, 1 Jul 2026 00:24:43 +0200 Subject: [PATCH] ajout du paiement par virement --- src/App.jsx | 4 + .../dashboard/DashboardServiceCard.jsx | 31 +- src/pages/app/Checkout.jsx | 11 +- src/pages/app/Dashboard.jsx | 51 +++- src/pages/app/OrderManagement.jsx | 161 ++++++++++ src/pages/app/Payment.jsx | 274 ++++++++++++++++++ src/services/api.js | 33 ++- 7 files changed, 551 insertions(+), 14 deletions(-) create mode 100644 src/pages/app/OrderManagement.jsx create mode 100644 src/pages/app/Payment.jsx diff --git a/src/App.jsx b/src/App.jsx index c982aca..2ed5c47 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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() { } /> } /> } /> + } /> + } /> diff --git a/src/components/dashboard/DashboardServiceCard.jsx b/src/components/dashboard/DashboardServiceCard.jsx index 6435788..2a06324 100644 --- a/src/components/dashboard/DashboardServiceCard.jsx +++ b/src/components/dashboard/DashboardServiceCard.jsx @@ -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 (
{getStatusBadge(order.status)}
-

- {order.title} +

+ {shortTitle}

+
+ {displayDomain ? (

{displayDomain}

) : (

En attente de déploiement

)} +

Facturation : {formatPeriod(order.period)}

- ID Réseau: #{order.id} - Gérer > +
); diff --git a/src/pages/app/Checkout.jsx b/src/pages/app/Checkout.jsx index 861a432..ae0bed7 100644 --- a/src/pages/app/Checkout.jsx +++ b/src/pages/app/Checkout.jsx @@ -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'} diff --git a/src/pages/app/Dashboard.jsx b/src/pages/app/Dashboard.jsx index b78f41b..170308a 100644 --- a/src/pages/app/Dashboard.jsx +++ b/src/pages/app/Dashboard.jsx @@ -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 (
@@ -80,7 +126,8 @@ export default function Dashboard() { navigate(`/services/${order.id}`)} + onClick={() => handleManageOrder(order)} + isActionLoading={loadingActionId === order.id} /> ))} diff --git a/src/pages/app/OrderManagement.jsx b/src/pages/app/OrderManagement.jsx new file mode 100644 index 0000000..3e556f0 --- /dev/null +++ b/src/pages/app/OrderManagement.jsx @@ -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
ANALYSE DU CONTRAT...
; + } + + if (!order) return null; + + // FOSSBilling renvoie souvent les upgradables sous forme de { "id_produit": "Nom du produit" } + const upgradeOptions = Object.entries(upgradables); + + return ( +
+
+ +
+

Gestion du contrat : {order.title}

+

Contrat réseau #{order.id} — Renouvellement : {order.period}

+
+
+ + {/* SECTIONS DES UPGRADES / DOWNGRADES */} +

Moduler l'infrastructure

+ + {upgradeOptions.length === 0 ? ( +
+ Aucune modification de forfait n'est disponible pour cette instance actuellement. +
+ ) : ( +
+ {upgradeOptions.map(([targetId, targetName]) => ( +
+ {targetName} + +
+ ))} +
+ * 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. +
+
+ )} + + {/* ZONE DANGER : RÉSILIATION */} +
+

+ Zone Critique +

+
+
+

Renoncer à l'abonnement

+

+ 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. +

+
+ +
+
+ + setCustomAlert(null)} /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/app/Payment.jsx b/src/pages/app/Payment.jsx new file mode 100644 index 0000000..ed8cc74 --- /dev/null +++ b/src/pages/app/Payment.jsx @@ -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: , color: 'hover:border-blue-500 hover:bg-blue-500/10 text-gray-400' }; + if (name.includes('bancontact') || name.includes('mollie')) return { icon: , 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: , color: 'hover:border-purple-500 hover:bg-purple-500/10 text-gray-400' }; + return { icon: , 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 ( +
+

RÉCUPÉRATION DU CONTRAT FINANCIER...

+
+ ); + } + + if (error) { + return ( +
+ +

{error}

+ +
+ ); + } + + // ========================================================================= + // ÉCRAN 2 : INSTRUCTIONS DE VIREMENT NATIVES (Pas de redirection) + // ========================================================================= + if (showBankInstructions) { + return ( +
+
+ +
+

PROCÉDURE DE VIREMENT

+

Action manuelle requise pour activer l'instance

+
+
+ +
+

Montant exact à transférer :

+

{invoice.total} {invoice.currency}

+ +
+
+ Bénéficiaire +
+ GISE CLOUD SERVICES +
+
+
+ IBAN (Compte Bancaire) +
+ BE43 XXXX XXXX XXXX {/* 🌟 METS TON VRAI IBAN ICI 🌟 */} +
+
+
+ Communication Structurée +
+ {invoice.serie}{invoice.nr} + +
+
+
+
+ +
+ L'instance sera provisionnée automatiquement sur l'hyperviseur dès réception des fonds par notre service comptable. +
+ + +
+ ); + } + + // ========================================================================= + // ÉCRAN 1 : SÉLECTION DU MOYEN DE PAIEMENT + // ========================================================================= + return ( +
+
+ +
+

PASSERELLE SÉCURISÉE

+

Règlement de la facture de provisionnement

+
+
+ +
+
+ Description + + {invoice.lines && invoice.lines.length > 0 ? invoice.lines[0].title : 'Instance Cloud'} + + Facture N° {invoice.serie}{invoice.nr} +
+
+ Montant TTC + {invoice.total} {invoice.currency} +
+
+ +

+ Sélectionner un canal de paiement +

+ +
+ {gateways.length === 0 ? ( +
+ Aucun terminal de paiement n'est configuré sur l'infrastructure (FOSSBilling). +
+ ) : ( + gateways.map((gw) => { + const style = getGatewayStyle(gw.title); + const isSelected = selectedMethod === gw.id; + + return ( +
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}` + } + `} + > +
+ {style.icon} +
+ + {gw.title} + +
+ ); + }) + )} +
+ +
+ + + + + +
+
+ ); +} \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index d546f75..b11a710 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -220,4 +220,35 @@ 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'); \ No newline at end of file + 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 }); \ No newline at end of file