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
+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>
);
}