ajout du paiement par virement
This commit is contained in:
@@ -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 ></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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+32
-1
@@ -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');
|
||||
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 });
|
||||
Reference in New Issue
Block a user