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