diff --git a/src/components/dashboard/SubscriptionManager.jsx b/src/components/dashboard/SubscriptionManager.jsx new file mode 100644 index 0000000..a7a9cd0 --- /dev/null +++ b/src/components/dashboard/SubscriptionManager.jsx @@ -0,0 +1,487 @@ +import { useState, useEffect } from 'react'; +import { CreditCard, Check, Globe, Loader, ToggleLeft, ShieldAlert, Calendar, TrendingUp, TrendingDown, RefreshCcw } from 'lucide-react'; +import { getProductList, getHostingServiceDetails } from '../../services/api'; +const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_API_BASE_URL || ''; + +export default function SubscriptionManager({ order, onClose, onRefresh, onAlert }) { + const [loading, setLoading] = useState(false); + const [cancelLoading, setCancelLoading] = useState(false); + const [catalog, setCatalog] = useState([]); + const [selectedPlan, setSelectedPlan] = useState(null); + const [isCatalogLoading, setIsCatalogLoading] = useState(true); + + const [serviceDetails, setServiceDetails] = useState(null); + const [isDetailsLoading, setIsDetailsLoading] = useState(true); + + const [billingPeriod, setBillingPeriod] = useState(order.period || '1M'); + + // 1. Récupération des détails techniques (Domaine/IP) + useEffect(() => { + const fetchDetails = async () => { + setIsDetailsLoading(true); + try { + const data = await getHostingServiceDetails(order.id); + setServiceDetails(data); + } catch (err) { + console.error("Erreur détails service:", err); + } finally { + setIsDetailsLoading(false); + } + }; + fetchDetails(); + }, [order.id]); + + const rawOrderTitle = order.title || ""; + const titleParts = rawOrderTitle.split(' pour '); + const currentPlanName = titleParts[0].trim(); + const currentPlanId = order.product_id; + const currentBillingPeriod = order.period; + + // 2. Extraction robuste du domaine + let extractedDomain = "Aucun domaine lié"; + if (serviceDetails?.domain || serviceDetails?.config?.domain) { + extractedDomain = serviceDetails.domain || serviceDetails.config.domain; + } + + 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); + + 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; oldDisplayPrice = displayPrice; + if (!priceW) isAvailable = false; + } else if (period === '1M') { + billingPrice = priceM; oldBillingPrice = priceW > 0 ? priceW * 4.333 : priceM; + displayPrice = priceM; 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; oldDisplayPrice = oldBillingPrice / 12; + if (!priceY) isAvailable = false; + } + + 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 data = await getProductList(); + const rawProducts = data.list || data.catalog || (Array.isArray(data) ? data : []); + const webProducts = rawProducts.filter(p => p.product_category_id === 1); + + if (webProducts.length > 0) { + setCatalog(webProducts); + const current = webProducts.find(p => currentPlanName.toLowerCase() === p.title.toLowerCase()); + if (current) setSelectedPlan(current); + } else { + console.warn("Aucun produit de type 'Web Service' n'a été trouvé."); + setCatalog([]); + } + } catch (err) { + console.error("Erreur API Catalogue:", err); + } finally { + setIsCatalogLoading(false); + } + }; + fetchCatalog(); + }, [currentPlanName]); + + const isNoChange = selectedPlan && (currentPlanId === selectedPlan.id) && (currentBillingPeriod === billingPeriod); + + // ========================================== + // 🧠 LOGIQUE UPGRADE / DOWNGRADE & PRORATA + // ========================================== + let migrationLabel = "Valider la modification"; + let buttonColor = "bg-cyan-500 hover:bg-cyan-400 shadow-[0_0_20px_rgba(6,182,212,0.2)]"; + let ActionIcon = null; + let finalInvoicePrice = 0; + let isProrataApplied = false; + let isRefund = false; + + if (selectedPlan && !isNoChange) { + const currentPrice = parseFloat(order.price) || 0; + let currentMonthly = currentPrice; + if (currentBillingPeriod === '1W') currentMonthly = currentPrice * 4.333; + if (currentBillingPeriod === '1M') currentMonthly = currentPrice; + if (currentBillingPeriod === '1Y') currentMonthly = currentPrice / 12; + + const selectedMetrics = getProductCardMetrics(selectedPlan.pricing, billingPeriod); + const selectedMonthly = parseFloat(selectedMetrics.displayPrice); + + const p1 = parseFloat(order.price) || 0; + const p2 = parseFloat(selectedMetrics.billingPrice) || 0; + + let m = 30; + if (currentBillingPeriod === '1Y') m = 365; + if (currentBillingPeriod === '1M') m = 30; + if (currentBillingPeriod === '1W') m = 7; + + const expiresAt = new Date(order.expires_at); + const now = new Date(); + let remainingDays = 0; + + if (!isNaN(expiresAt)) { + remainingDays = Math.max(0, Math.ceil((expiresAt - now) / (1000 * 60 * 60 * 24))); + } + + if (remainingDays > 0 && remainingDays <= m) { + const n = m - remainingDays; // Jours écoulés + finalInvoicePrice = p2 - p1 - (n * (p1 - p2) / m); + isProrataApplied = true; + } else { + finalInvoicePrice = p2; + } + + isRefund = finalInvoicePrice < -0.01; + + // --- 3. Style du bouton (Triggers : Upgrade / Downgrade / Cycle) --- + + // On récupère le forfait actuel depuis le catalogue pour comparer la "puissance" brute des forfaits + const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId); + + // Pour définir la hiérarchie absolue, on compare les prix de base sur 1 Mois + const baseCurrentPrice = currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0; + const baseSelectedPrice = selectedPlan?.pricing?.recurrent?.['1M']?.price || 0; + + const isSamePlan = currentPlanId === selectedPlan.id; + const isSamePeriod = currentBillingPeriod === billingPeriod; + + if (isSamePlan && !isSamePeriod) { + // Le pack est identique, seule la période change + migrationLabel = "Changer de cycle"; + buttonColor = "bg-blue-500 hover:bg-blue-400 text-black shadow-[0_0_20px_rgba(59,130,246,0.2)]"; + ActionIcon = ; + } else if (!isSamePlan) { + // Le pack change, on compare la hiérarchie absolue + if (parseFloat(baseSelectedPrice) > parseFloat(baseCurrentPrice)) { + migrationLabel = `Valider l'Upgrade`; + buttonColor = "bg-emerald-500 hover:bg-emerald-400 text-black shadow-[0_0_20px_rgba(16,185,129,0.2)]"; + ActionIcon = ; + } else { + migrationLabel = "Valider le Downgrade"; + buttonColor = "bg-amber-500 hover:bg-amber-400 text-black shadow-[0_0_20px_rgba(245,158,11,0.2)]"; + ActionIcon = ; + } + } + } + // ========================================== + + const handleMigration = async () => { + setLoading(true); + + // 🎯 On détermine dynamiquement le type d'action pour la facture + let actionType = 'upgrade'; + const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId); + const baseCurrent = parseFloat(currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0); + const baseSelected = parseFloat(selectedPlan?.pricing?.recurrent?.['1M']?.price || 0); + + if (selectedPlan.id === currentPlanId) { + actionType = 'cycle'; + } else if (baseSelected < baseCurrent) { + actionType = 'downgrade'; + } + + try { + const resp = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + action: 'migrate', + order_id: order.id, + target_plan_id: selectedPlan.id, + new_period: billingPeriod, + new_price: finalInvoicePrice.toFixed(2), + action_type: actionType // 🎯 On envoie l'information au backend ! + }) + }); + const data = await resp.json(); + if (data.status === 'success') { + onAlert?.("Facture générée", "Redirection...", "success"); + onClose?.(); + setTimeout(() => window.location.href = '/facturation', 2000); + } else { + onAlert?.("Erreur", data.error, "error"); + } + } catch (err) { + onAlert?.("Erreur", "Problème réseau", "error"); + } finally { + setLoading(false); + } + }; + + const handleCancel = async () => { + if (!window.confirm("Êtes-vous sûr de vouloir résilier cet abonnement ? Le service sera désactivé au prochain cycle de facturation.")) { + return; + } + + setCancelLoading(true); + try { + const resp = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + action: 'cancel', + order_id: order.id + }) + }); + const data = await resp.json(); + + if (data.status === 'success') { + if (onAlert) onAlert("Résilié", data.message || "L'abonnement a été annulé avec succès.", "success"); + if (onRefresh) onRefresh(); + if (onClose) onClose(); + } else { + if (onAlert) onAlert("Erreur", data.error || "Impossible de résilier l'abonnement.", "error"); + } + } catch (err) { + if (onAlert) onAlert("Erreur", "Problème réseau ou serveur.", "error"); + } finally { + setCancelLoading(false); + } + }; + + return ( +
+ +
+
+
+
+
+

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 les factures + +
+ +
+ + 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 ( +
!loading && !cancelLoading && 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 || loading || cancelLoading ? '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 ? ( + <> +
+ {metrics.displayPrice} € + / mois +
+ + {metrics.saving > 0 && ( +

+ Au lieu de {metrics.oldDisplayPrice} € / mois +

+ )} + +
+ Facturé {metrics.billingPrice} € par {billingPeriod === '1W' ? 'semaine' : billingPeriod === '1M' ? 'mois' : 'an'} +
+ + ) : ( +
+ Non disponible +

Pour ce cycle

+
+ )} +
+ +
+ {renderMarkdownFeatures(plan.description)} +
+
+ ); + })} +
+ )} +
+ +
+ {/* 🎯 NOUVEAU : AFFICHAGE DYNAMIQUE (PRORATA ET REMBOURSEMENT) */} + {selectedPlan && !isNoChange && ( +
+
+

+ {isRefund + ? "Remboursement estimé" + : finalInvoicePrice > 0.01 + ? "Montant à régler aujourd'hui" + : "Aucun frais immédiat"} +

+ {isProrataApplied &&

Calculé au prorata des jours restants

} +
+
+ + {isRefund ? "-" : ""}{Math.abs(finalInvoicePrice).toFixed(2)} € + +
+
+ )} + + {selectedPlan && ( + + )} + + +
+
+ ); +} \ No newline at end of file