diff --git a/src/components/dashboard/SubscriptionManager.jsx b/src/components/dashboard/SubscriptionManager.jsx
new file mode 100644
index 0000000..11fcc93
--- /dev/null
+++ b/src/components/dashboard/SubscriptionManager.jsx
@@ -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 Aucune spécification disponible.;
+ return text.split('\n').map((line, index) => {
+ const cleanLine = line.trim();
+ if (cleanLine.startsWith('* ') || cleanLine.startsWith('- ')) {
+ return (
+
+
+
+
+
{parseBold(cleanLine.substring(2))}
+
+ );
+ }
+ return {parseBold(cleanLine)}
;
+ });
+ };
+
+ const parseBold = (text) => text.split('**').map((part, i) => i % 2 === 1 ? {part} : 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
+
+
+
+
+
+
+
+
Abonnement Actuel
+
{currentPlanName}
+
+
+
+
+
+
Espace Domaine
+
{extractedDomain}
+
+
+
+
+
+
+
+
+
+
+
Prochain Renouvellement
+
+ {order.expires_at ? order.expires_at : "Date non définie"}
+ |
+ {order.price || '0.00'} {order.currency || '€'}
+ / {order.period === '1W' ? 'semaine' : order.period === '1Y' ? 'an' : 'mois'}
+
+
+
+
+ Voir la facture ↗
+
+
+
+
+
+ Options de renouvellement
+
+
+ {[
+ { id: '1W', label: 'Semaine' },
+ { id: '1M', label: 'Mois' },
+ { id: '1Y', label: 'Année' }
+ ].map((p) => (
+
+ ))}
+
+
+
+
+
+ {isCatalogLoading ? (
+
+
+
Synchronisation des tarifs...
+
+ ) : (
+
+ {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 (
+
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' : ''}`}
+ >
+
+
+ {topBadge.label}
+
+
+
+
+
+ {plan.category?.title || "WEB"}
+
+ {metrics.saving > 0 && metrics.isAvailable && (
+
+ Économie {metrics.saving}%
+
+ )}
+
+
+
+
+ {plan.title}
+ {isCurrentActive && Actif}
+
+
+ {metrics.isAvailable ? (
+ <>
+ {/* 🎯 L'affichage dynamique toujours converti au mois */}
+
+ {metrics.displayPrice} €
+ / mois
+
+
+ {metrics.saving > 0 && (
+
+ Au lieu de {metrics.oldDisplayPrice} € / mois
+
+ )}
+
+ {/* 🎯 L'engagement de facturation reste avec le vrai montant et le vrai cycle */}
+
+ Facturé {metrics.billingPrice} € par {billingPeriod === '1W' ? 'semaine' : billingPeriod === '1M' ? 'mois' : 'an'}
+
+ >
+ ) : (
+
+
Non disponible
+
Pour ce cycle
+
+ )}
+
+
+
+ {renderMarkdownFeatures(plan.description)}
+
+
+ );
+ })}
+
+ )}
+
+
+
+ {selectedPlan && (
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/app/Dashboard.jsx b/src/pages/app/Dashboard.jsx
index b78f41b..6786596 100644
--- a/src/pages/app/Dashboard.jsx
+++ b/src/pages/app/Dashboard.jsx
@@ -1,11 +1,13 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
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 NewServiceCard from '../../components/dashboard/NewServiceCard';
+import NotificationModal from '../../components/ui/NotificationModal';
+import SubscriptionManager from '../../components/dashboard/SubscriptionManager'; // 🌟 Le nouveau composant !
export default function Dashboard() {
const navigate = useNavigate();
@@ -13,54 +15,82 @@ export default function Dashboard() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
- // Chargement des données à l'ouverture du Sas
- useEffect(() => {
- 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 [activeSubscriptionModal, setActiveSubscriptionModal] = useState(null);
+ const [customAlert, setCustomAlert] = useState(null);
- const isGhostProduct =
- type === 'domain' ||
- title.startsWith('domain ') ||
- title.startsWith('domaine ') ||
- title.startsWith('enregistrement ');
-
- return !isGhostProduct;
- });
+ const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
- setOrders(filteredOrders);
- } else {
- setOrders([]);
- }
- } catch (err) {
- setError(err.message || "Impossible de récupérer la télémétrie des services.");
- } finally {
- setIsLoading(false);
+ const fetchInventory = async () => {
+ setIsLoading(true);
+ 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 =
+ 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();
}, []);
+ // 🌟 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 (
-
-
+
TABLEAU DE BORD
- Aperçu de vos accréditations réseau et infrastructures.
+ Gestion financière et accréditations de vos infrastructures.
- {/* GESTION DES ERREURS & CHARGEMENT */}
{isLoading && (
- Synchronisation avec l'orchestrateur en cours...
+ Synchronisation avec le registre comptable...
)}
@@ -71,24 +101,42 @@ export default function Dashboard() {
)}
- {/* GRILLE DES SERVICES */}
{!isLoading && !error && (
-
- {/* Boucle sur les composants isolés */}
{orders.map((order) => (
- navigate(`/services/${order.id}`)}
+ handleManageSubscription(order)}
/>
))}
-
- {/* Le composant carte d'ajout */}
navigate('/store')} />
-
)}
+
+ {/* 🌟 LA MODALE DE GESTION D'ABONNEMENT */}
+ {activeSubscriptionModal && (
+
+
+
+
+
+ GESTION DE L'ABONNEMENT
+
+
+
+
+
setActiveSubscriptionModal(null)}
+ onRefresh={fetchInventory}
+ onAlert={triggerAlert}
+ />
+
+
+ )}
+
+
setCustomAlert(null)} />
);
}
\ No newline at end of file