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 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 Payment from './pages/app/Payment';
|
||||||
|
import OrderManagement from './pages/app/OrderManagement';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +51,8 @@ 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="/payment/:invoiceId" element={<Payment />} />
|
||||||
|
<Route path="/manage/:orderId" element={<OrderManagement />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
</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
|
// Fonction Radar : Détecte le type de service selon son nom pour afficher la bonne icône
|
||||||
const getServiceIcon = (title) => {
|
const getServiceIcon = (title) => {
|
||||||
const t = title.toLowerCase();
|
const t = title.toLowerCase();
|
||||||
@@ -35,6 +35,13 @@ export default function DashboardServiceCard({ order, onClick }) {
|
|||||||
return periods[periodCode] || periodCode;
|
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 (
|
return (
|
||||||
<div
|
<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"
|
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>
|
</div>
|
||||||
{getStatusBadge(order.status)}
|
{getStatusBadge(order.status)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-white truncate" title={order.title}>
|
<h3 className="text-lg font-semibold text-white truncate" title={baseTitle}>
|
||||||
{order.title}
|
{shortTitle}
|
||||||
</h3>
|
</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">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
Facturation : <span className="text-cyan-400 font-medium">{formatPeriod(order.period)}</span>
|
Facturation : <span className="text-cyan-400 font-medium">{formatPeriod(order.period)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-gray-800 flex justify-between items-center text-sm">
|
<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>
|
<button
|
||||||
<span className="text-cyan-400 group-hover:underline">Gérer ></span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -111,11 +111,12 @@ export default function Checkout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await addToCart(productId, period, productConfig);
|
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', {
|
// On redirige vers /payment/LE_HASH
|
||||||
state: { successMessage: `Instance initialisée sur ${serverName}.gise.be` }
|
navigate(`/payment/${invoiceHash}`);
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || "La transaction a échoué. L'API a refusé le contrat.");
|
setError(err.message || "La transaction a échoué. L'API a refusé le contrat.");
|
||||||
@@ -185,7 +186,7 @@ export default function Checkout() {
|
|||||||
disabled={isProcessing}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getClientOrders } from '../../services/api';
|
|
||||||
import { AlertCircle, Loader } from 'lucide-react';
|
import { AlertCircle, Loader } from 'lucide-react';
|
||||||
|
import { getClientOrders, getClientInvoices } from '../../services/api';
|
||||||
|
|
||||||
// Importation des composants isolés
|
// Importation des composants isolés
|
||||||
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
||||||
@@ -11,6 +11,7 @@ export default function Dashboard() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [orders, setOrders] = useState([]);
|
const [orders, setOrders] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [loadingActionId, setLoadingActionId] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Chargement des données à l'ouverture du Sas
|
// Chargement des données à l'ouverture du Sas
|
||||||
@@ -48,6 +49,51 @@ export default function Dashboard() {
|
|||||||
fetchInventory();
|
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 (
|
return (
|
||||||
<div className="w-full max-w-6xl p-6 mx-auto">
|
<div className="w-full max-w-6xl p-6 mx-auto">
|
||||||
|
|
||||||
@@ -80,7 +126,8 @@ export default function Dashboard() {
|
|||||||
<DashboardServiceCard
|
<DashboardServiceCard
|
||||||
key={order.id}
|
key={order.id}
|
||||||
order={order}
|
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
|
// Récupère la liste dynamique des départements (Helpdesks) configurés sur FOSSBilling
|
||||||
export const getHelpdesks = () =>
|
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