add migration of servicew
This commit is contained in:
@@ -0,0 +1,291 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { CreditCard, Check, Globe, Loader, ToggleLeft, ShieldAlert, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function SubscriptionManager({ order, onClose, onRefresh, onAlert }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [catalog, setCatalog] = useState([]);
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||||
|
const [isCatalogLoading, setIsCatalogLoading] = useState(true);
|
||||||
|
|
||||||
|
const [billingPeriod, setBillingPeriod] = useState(order.period || '1M');
|
||||||
|
|
||||||
|
const rawOrderTitle = order.title || "";
|
||||||
|
const titleParts = rawOrderTitle.split(' pour ');
|
||||||
|
const currentPlanName = titleParts[0].trim();
|
||||||
|
const currentPlanId = order.product_id;
|
||||||
|
const currentBillingPeriod = order.period;
|
||||||
|
const extractedDomain = titleParts[1] ? titleParts[1].trim() : (order.sld ? `${order.sld}.${order.tld}` : 'Non configuré');
|
||||||
|
|
||||||
|
const renderMarkdownFeatures = (text) => {
|
||||||
|
if (!text) return <span className="text-gray-500 italic">Aucune spécification disponible.</span>;
|
||||||
|
return text.split('\n').map((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (cleanLine.startsWith('* ') || cleanLine.startsWith('- ')) {
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center gap-3 text-sm font-semibold text-white my-2">
|
||||||
|
<div className="shrink-0 w-5 h-5 rounded-full border border-cyan-500/40 flex items-center justify-center bg-cyan-950/10">
|
||||||
|
<Check className="w-3 h-3 text-cyan-400 stroke-[3]" />
|
||||||
|
</div>
|
||||||
|
<span>{parseBold(cleanLine.substring(2))}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p key={index} className="text-gray-400 text-xs my-1">{parseBold(cleanLine)}</p>;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseBold = (text) => text.split('**').map((part, i) => i % 2 === 1 ? <strong key={i} className="text-white font-bold">{part}</strong> : part);
|
||||||
|
|
||||||
|
// 🎯 Nouveau Moteur Financier (Mensualisation de l'affichage)
|
||||||
|
const getProductCardMetrics = (pricing, period) => {
|
||||||
|
if (!pricing || !pricing.recurrent) {
|
||||||
|
return { displayPrice: "0.00", billingPrice: "0.00", oldDisplayPrice: "0.00", saving: 0, isAvailable: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const recurrent = pricing.recurrent;
|
||||||
|
const priceW = recurrent['1W']?.price ? parseFloat(recurrent['1W'].price) : 0;
|
||||||
|
const priceM = recurrent['1M']?.price ? parseFloat(recurrent['1M'].price) : 0;
|
||||||
|
const priceY = recurrent['1Y']?.price ? parseFloat(recurrent['1Y'].price) : 0;
|
||||||
|
|
||||||
|
let billingPrice = 0, oldBillingPrice = 0, savingPercent = 0, isAvailable = true;
|
||||||
|
let displayPrice = 0, oldDisplayPrice = 0;
|
||||||
|
|
||||||
|
if (period === '1W') {
|
||||||
|
billingPrice = priceW;
|
||||||
|
oldBillingPrice = priceW;
|
||||||
|
displayPrice = priceW * 4.333; // Mensualisation estimée
|
||||||
|
oldDisplayPrice = displayPrice;
|
||||||
|
if (!priceW) isAvailable = false;
|
||||||
|
} else if (period === '1M') {
|
||||||
|
billingPrice = priceM;
|
||||||
|
oldBillingPrice = priceW > 0 ? priceW * 4.333 : priceM;
|
||||||
|
displayPrice = priceM; // Déjà au mois
|
||||||
|
oldDisplayPrice = oldBillingPrice;
|
||||||
|
if (!priceM) isAvailable = false;
|
||||||
|
} else if (period === '1Y') {
|
||||||
|
billingPrice = priceY;
|
||||||
|
oldBillingPrice = priceW > 0 ? priceW * 52 : (priceM > 0 ? priceM * 12 : priceY);
|
||||||
|
displayPrice = priceY / 12; // Mensualisation exacte
|
||||||
|
oldDisplayPrice = oldBillingPrice / 12;
|
||||||
|
if (!priceY) isAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le pourcentage d'économie se calcule toujours sur le montant global facturé
|
||||||
|
savingPercent = (oldBillingPrice > billingPrice && billingPrice > 0) ? Math.round(((oldBillingPrice - billingPrice) / oldBillingPrice) * 100) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayPrice: displayPrice.toFixed(2),
|
||||||
|
billingPrice: billingPrice.toFixed(2),
|
||||||
|
oldDisplayPrice: oldDisplayPrice.toFixed(2),
|
||||||
|
saving: savingPercent,
|
||||||
|
isAvailable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanBadge = (index) => {
|
||||||
|
if (index === 0) return { label: "ESSENTIEL", color: "text-gray-400 bg-gray-900 border-gray-700" };
|
||||||
|
if (index === 1) return { label: "PLUS POPULAIRE", color: "text-cyan-300 bg-cyan-950 border-cyan-800 shadow-[0_0_10px_rgba(6,182,212,0.3)]" };
|
||||||
|
return { label: "PRO", color: "text-amber-400 bg-amber-950 border-amber-800 shadow-[0_0_10px_rgba(245,158,11,0.2)]" };
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCatalog = async () => {
|
||||||
|
setIsCatalogLoading(true);
|
||||||
|
try {
|
||||||
|
const resp = await fetch('https://web.gise.be/custom_api/nexus_subscription.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({ action: 'get_catalog', order_id: order.id })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
setCatalog(data.catalog);
|
||||||
|
const current = data.catalog.find(p => currentPlanName.toLowerCase() === p.title.toLowerCase());
|
||||||
|
if (current) setSelectedPlan(current);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (onAlert) onAlert("Erreur", "Impossible de mapper les offres.", "error");
|
||||||
|
} finally {
|
||||||
|
setIsCatalogLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchCatalog();
|
||||||
|
}, [order.id, currentPlanName]);
|
||||||
|
|
||||||
|
const isNoChange = selectedPlan && (currentPlanId === selectedPlan.id) && (currentBillingPeriod === billingPeriod);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// 🎯 Ligne modifiée : Retrait du "bg-[#060b14]" pour s'adapter à ta fenêtre Modal
|
||||||
|
<div className="flex flex-col w-full h-[90vh] md:h-[85vh] max-w-6xl mx-auto text-white">
|
||||||
|
|
||||||
|
<div className="shrink-0 space-y-4 mb-4 pr-2">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-[#090f1c] border border-gray-800 p-3.5 rounded-xl flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-gray-950 rounded-lg text-gray-400"><CreditCard className="w-4 h-4" /></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">Abonnement Actuel</p>
|
||||||
|
<p className="text-sm font-bold text-white">{currentPlanName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#090f1c] border border-gray-800 p-3.5 rounded-xl flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-gray-950 rounded-lg text-cyan-400"><Globe className="w-4 h-4" /></div>
|
||||||
|
<div className="truncate">
|
||||||
|
<p className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">Espace Domaine</p>
|
||||||
|
<p className="text-sm font-mono text-cyan-400 truncate">{extractedDomain}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#090f1c] border border-gray-800 p-4 rounded-xl flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-gray-950 rounded-lg text-emerald-400 border border-gray-800/50">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">Prochain Renouvellement</p>
|
||||||
|
<p className="text-sm font-medium text-gray-300">
|
||||||
|
{order.expires_at ? order.expires_at : "Date non définie"}
|
||||||
|
<span className="text-gray-600 mx-2">|</span>
|
||||||
|
<span className="font-mono text-white font-bold">{order.price || '0.00'} {order.currency || '€'}</span>
|
||||||
|
<span className="text-[10px] text-gray-500 ml-1 uppercase">/ {order.period === '1W' ? 'semaine' : order.period === '1Y' ? 'an' : 'mois'}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`/invoice`}
|
||||||
|
className="text-xs font-bold text-cyan-400 hover:text-cyan-300 transition-colors flex items-center gap-1.5 bg-cyan-950/20 px-4 py-2 rounded-lg border border-cyan-900/30"
|
||||||
|
>
|
||||||
|
Voir la facture ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#090f1c] p-2 rounded-xl border border-gray-800 flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold text-gray-400 ml-3 flex items-center gap-2">
|
||||||
|
<ToggleLeft className="w-4 h-4 text-cyan-400" /> Options de renouvellement
|
||||||
|
</span>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{[
|
||||||
|
{ id: '1W', label: 'Semaine' },
|
||||||
|
{ id: '1M', label: 'Mois' },
|
||||||
|
{ id: '1Y', label: 'Année' }
|
||||||
|
].map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => setBillingPeriod(p.id)}
|
||||||
|
className={`px-5 py-2 rounded-lg text-xs font-black tracking-wide transition-all ${billingPeriod === p.id ? 'bg-cyan-500 text-black shadow-lg shadow-cyan-500/20' : 'text-gray-400 hover:text-white hover:bg-gray-900'}`}
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grow pr-2 custom-scrollbar">
|
||||||
|
{isCatalogLoading ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center text-cyan-400">
|
||||||
|
<Loader className="w-8 h-8 animate-spin mx-auto mb-2" />
|
||||||
|
<p className="text-xs font-mono text-gray-500">Synchronisation des tarifs...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 pt-3 pb-4">
|
||||||
|
{catalog.map((plan, index) => {
|
||||||
|
const metrics = getProductCardMetrics(plan.pricing, billingPeriod);
|
||||||
|
const isSelected = selectedPlan?.id === plan.id;
|
||||||
|
const isCurrentActive = currentPlanId === plan.id && currentBillingPeriod === billingPeriod;
|
||||||
|
const topBadge = getPlanBadge(index);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={plan.id}
|
||||||
|
onClick={() => metrics.isAvailable && setSelectedPlan(plan)}
|
||||||
|
className={`relative h-full p-6 rounded-2xl border transition-all duration-300 flex flex-col justify-between bg-[#0d1527] mt-3
|
||||||
|
${!metrics.isAvailable ? 'opacity-50 grayscale cursor-not-allowed border-gray-900' : 'cursor-pointer'}
|
||||||
|
${isSelected && metrics.isAvailable ? 'border-cyan-500 shadow-[0_0_20px_rgba(6,182,212,0.15)] scale-[1.02] z-10' : 'border-gray-800/80 hover:border-gray-700'}
|
||||||
|
${isCurrentActive ? 'ring-1 ring-gray-700' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||||
|
<span className={`text-[9px] font-black tracking-widest px-3 py-1 rounded-full border ${topBadge.color}`}>
|
||||||
|
{topBadge.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center w-full mb-5 mt-2">
|
||||||
|
<span className="text-[9px] font-black tracking-widest text-white bg-[#090f1c] px-2.5 py-1 rounded border border-gray-800 uppercase">
|
||||||
|
{plan.category?.title || "WEB"}
|
||||||
|
</span>
|
||||||
|
{metrics.saving > 0 && metrics.isAvailable && (
|
||||||
|
<span className="text-[10px] font-bold text-[#00df89] bg-[#00df89]/10 border border-[#00df89]/20 px-3 py-0.5 rounded-full uppercase tracking-wide">
|
||||||
|
Économie {metrics.saving}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5 shrink-0">
|
||||||
|
<h4 className="font-bold text-xl text-white tracking-tight mb-3 flex items-center justify-between">
|
||||||
|
{plan.title}
|
||||||
|
{isCurrentActive && <span className="text-[9px] font-bold text-orange-500 uppercase tracking-widest bg-orange-900 px-2 py-0.5 rounded border border-orange-800">Actif</span>}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{metrics.isAvailable ? (
|
||||||
|
<>
|
||||||
|
{/* 🎯 L'affichage dynamique toujours converti au mois */}
|
||||||
|
<div className="flex items-baseline gap-1.5">
|
||||||
|
<span className="text-3xl font-black text-cyan-400 tracking-tighter">{metrics.displayPrice} €</span>
|
||||||
|
<span className="text-xs text-gray-400 font-medium">/ mois</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{metrics.saving > 0 && (
|
||||||
|
<p className="text-xs text-gray-500 line-through mt-1">
|
||||||
|
Au lieu de {metrics.oldDisplayPrice} € / mois
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🎯 L'engagement de facturation reste avec le vrai montant et le vrai cycle */}
|
||||||
|
<div className="mt-3.5 inline-block bg-[#0e2238] border border-cyan-950/40 text-cyan-400 text-[9px] font-black tracking-widest uppercase px-3 py-1.5 rounded-md">
|
||||||
|
Facturé {metrics.billingPrice} € par {billingPeriod === '1W' ? 'semaine' : billingPeriod === '1M' ? 'mois' : 'an'}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="py-2">
|
||||||
|
<span className="text-lg font-bold text-gray-500">Non disponible</span>
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1 uppercase tracking-wider">Pour ce cycle</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-800/60 pt-4 flex-grow space-y-1.5">
|
||||||
|
{renderMarkdownFeatures(plan.description)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0 mt-auto pt-4 border-t border-gray-800/40 space-y-3">
|
||||||
|
{selectedPlan && (
|
||||||
|
<button
|
||||||
|
disabled={loading || isNoChange}
|
||||||
|
className={`w-full py-4 rounded-xl font-black uppercase text-xs tracking-widest transition-all duration-300
|
||||||
|
${isNoChange
|
||||||
|
? 'bg-gray-900 text-gray-500 border border-gray-800 cursor-not-allowed shadow-none'
|
||||||
|
: 'bg-cyan-500 hover:bg-cyan-400 text-black shadow-[0_0_20px_rgba(6,182,212,0.2)] hover:shadow-[0_0_25px_rgba(6,182,212,0.4)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? 'Traitement en cours...'
|
||||||
|
: isNoChange
|
||||||
|
? 'Abonnement actuel (Aucune modification)'
|
||||||
|
: `Valider la modification`
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="w-full flex items-center justify-center gap-2 text-red-400/80 hover:text-red-400 border border-red-950/30 bg-red-950/5 hover:bg-red-950/15 p-3 rounded-xl text-xs font-bold uppercase tracking-wider transition-all">
|
||||||
|
<ShieldAlert className="w-4 h-4" /> Résilier l'abonnement réseau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+93
-45
@@ -1,11 +1,13 @@
|
|||||||
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 { getClientOrders } from '../../services/api';
|
||||||
import { AlertCircle, Loader } from 'lucide-react';
|
import { AlertCircle, Loader, FileText } from 'lucide-react';
|
||||||
|
|
||||||
// Importation des composants isolés
|
// Importation des composants
|
||||||
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
||||||
import NewServiceCard from '../../components/dashboard/NewServiceCard';
|
import NewServiceCard from '../../components/dashboard/NewServiceCard';
|
||||||
|
import NotificationModal from '../../components/ui/NotificationModal';
|
||||||
|
import SubscriptionManager from '../../components/dashboard/SubscriptionManager'; // 🌟 Le nouveau composant !
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -13,54 +15,82 @@ export default function Dashboard() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Chargement des données à l'ouverture du Sas
|
const [activeSubscriptionModal, setActiveSubscriptionModal] = useState(null);
|
||||||
useEffect(() => {
|
const [customAlert, setCustomAlert] = useState(null);
|
||||||
const fetchInventory = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getClientOrders();
|
|
||||||
|
|
||||||
if (data.list) {
|
|
||||||
// LE FILTRE CHIRURGICAL PAR PREFIXE
|
|
||||||
const filteredOrders = data.list.filter(order => {
|
|
||||||
const title = (order.title || '').toLowerCase();
|
|
||||||
const type = (order.type || '').toLowerCase();
|
|
||||||
|
|
||||||
const isGhostProduct =
|
const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
|
||||||
type === 'domain' ||
|
|
||||||
title.startsWith('domain ') ||
|
|
||||||
title.startsWith('domaine ') ||
|
|
||||||
title.startsWith('enregistrement ');
|
|
||||||
|
|
||||||
return !isGhostProduct;
|
|
||||||
});
|
|
||||||
|
|
||||||
setOrders(filteredOrders);
|
const fetchInventory = async () => {
|
||||||
} else {
|
setIsLoading(true);
|
||||||
setOrders([]);
|
try {
|
||||||
}
|
const data = await getClientOrders();
|
||||||
} catch (err) {
|
|
||||||
setError(err.message || "Impossible de récupérer la télémétrie des services.");
|
if (data.list) {
|
||||||
} finally {
|
// LE FILTRE CHIRURGICAL PAR PREFIXE
|
||||||
setIsLoading(false);
|
const filteredOrders = data.list.filter(order => {
|
||||||
|
const title = (order.title || '').toLowerCase();
|
||||||
|
const type = (order.type || '').toLowerCase();
|
||||||
|
|
||||||
|
const isGhostProduct =
|
||||||
|
type === 'domain' ||
|
||||||
|
title.startsWith('domain ') ||
|
||||||
|
title.startsWith('domaine ') ||
|
||||||
|
title.startsWith('enregistrement ');
|
||||||
|
|
||||||
|
return !isGhostProduct;
|
||||||
|
});
|
||||||
|
|
||||||
|
setOrders(filteredOrders);
|
||||||
|
} else {
|
||||||
|
setOrders([]);
|
||||||
}
|
}
|
||||||
};
|
} catch (err) {
|
||||||
|
setError(err.message || "Impossible de récupérer la télémétrie des services.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
fetchInventory();
|
fetchInventory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
||||||
|
const handleManageSubscription = (order) => {
|
||||||
|
// 1. Détection du type de service
|
||||||
|
const titleLower = (order.title || '').toLowerCase();
|
||||||
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
|
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
||||||
|
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
||||||
|
|
||||||
|
// C'est un hébergement web s'il ne correspond à aucun des cas ci-dessus
|
||||||
|
const isWeb = !isVPS && !isCloud && !isDB;
|
||||||
|
|
||||||
|
// 2. Logique de blocage
|
||||||
|
if (isWeb) {
|
||||||
|
// On ouvre la modale comptable uniquement pour le WEB
|
||||||
|
setActiveSubscriptionModal(order);
|
||||||
|
} else {
|
||||||
|
// On affiche une notification pour les autres types d'instances
|
||||||
|
triggerAlert(
|
||||||
|
"Action non disponible",
|
||||||
|
"La modification autonome d'abonnement est actuellement exclusive aux Hébergements Web. Pour restructurer cette instance, veuillez contacter les ingénieurs via le Centre de Support.",
|
||||||
|
"info"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl p-6 mx-auto">
|
<div className="w-full max-w-6xl p-6 mx-auto relative">
|
||||||
|
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<h1 className="text-3xl font-black text-white tracking-wider">TABLEAU DE <span className="text-cyan-400">BORD</span></h1>
|
<h1 className="text-3xl font-black text-white tracking-wider">TABLEAU DE <span className="text-cyan-400">BORD</span></h1>
|
||||||
<p className="text-gray-400 mt-2">Aperçu de vos accréditations réseau et infrastructures.</p>
|
<p className="text-gray-400 mt-2">Gestion financière et accréditations de vos infrastructures.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* GESTION DES ERREURS & CHARGEMENT */}
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center space-x-3 text-cyan-400">
|
<div className="flex items-center space-x-3 text-cyan-400">
|
||||||
<Loader className="w-6 h-6 animate-spin" />
|
<Loader className="w-6 h-6 animate-spin" />
|
||||||
<span>Synchronisation avec l'orchestrateur en cours...</span>
|
<span>Synchronisation avec le registre comptable...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -71,24 +101,42 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GRILLE DES SERVICES */}
|
|
||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
|
||||||
{/* Boucle sur les composants isolés */}
|
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<DashboardServiceCard
|
<DashboardServiceCard
|
||||||
key={order.id}
|
key={order.id}
|
||||||
order={order}
|
order={order}
|
||||||
onClick={() => navigate(`/services/${order.id}`)}
|
onClick={() => handleManageSubscription(order)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Le composant carte d'ajout */}
|
|
||||||
<NewServiceCard onClick={() => navigate('/store')} />
|
<NewServiceCard onClick={() => navigate('/store')} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🌟 LA MODALE DE GESTION D'ABONNEMENT */}
|
||||||
|
{activeSubscriptionModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-gray-900 border border-cyan-500/50 shadow-2xl shadow-cyan-500/10 rounded-lg max-w-xxl w-full p-6 text-gray-200">
|
||||||
|
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
|
||||||
|
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
|
||||||
|
<FileText className="w-6 h-6 text-cyan-400" />
|
||||||
|
GESTION DE L'ABONNEMENT
|
||||||
|
</h3>
|
||||||
|
<button onClick={() => setActiveSubscriptionModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubscriptionManager
|
||||||
|
order={activeSubscriptionModal}
|
||||||
|
onClose={() => setActiveSubscriptionModal(null)}
|
||||||
|
onRefresh={fetchInventory}
|
||||||
|
onAlert={triggerAlert}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user