add billing system
This commit is contained in:
@@ -1,20 +1,47 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CreditCard, Check, Globe, Loader, ToggleLeft, ShieldAlert, Calendar } from 'lucide-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;
|
||||
const extractedDomain = titleParts[1] ? titleParts[1].trim() : (order.sld ? `${order.sld}.${order.tld}` : 'Non configuré');
|
||||
|
||||
// 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 <span className="text-gray-500 italic">Aucune spécification disponible.</span>;
|
||||
@@ -24,7 +51,7 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
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]" />
|
||||
<Check className="w-3 h-3 text-cyan-400 stroke-3" />
|
||||
</div>
|
||||
<span>{parseBold(cleanLine.substring(2))}</span>
|
||||
</div>
|
||||
@@ -36,7 +63,6 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
|
||||
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 };
|
||||
@@ -51,35 +77,22 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
let displayPrice = 0, oldDisplayPrice = 0;
|
||||
|
||||
if (period === '1W') {
|
||||
billingPrice = priceW;
|
||||
oldBillingPrice = priceW;
|
||||
displayPrice = priceW * 4.333; // Mensualisation estimée
|
||||
oldDisplayPrice = displayPrice;
|
||||
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; // Déjà au mois
|
||||
oldDisplayPrice = oldBillingPrice;
|
||||
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; // Mensualisation exacte
|
||||
oldDisplayPrice = oldBillingPrice / 12;
|
||||
billingPrice = priceY; oldBillingPrice = priceW > 0 ? priceW * 52 : (priceM > 0 ? priceM * 12 : priceY);
|
||||
displayPrice = priceY / 12; 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
|
||||
};
|
||||
return { displayPrice: displayPrice.toFixed(2), billingPrice: billingPrice.toFixed(2), oldDisplayPrice: oldDisplayPrice.toFixed(2), saving: savingPercent, isAvailable };
|
||||
};
|
||||
|
||||
const getPlanBadge = (index) => {
|
||||
@@ -92,31 +105,184 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
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());
|
||||
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) {
|
||||
if (onAlert) onAlert("Erreur", "Impossible de mapper les offres.", "error");
|
||||
console.error("Erreur API Catalogue:", err);
|
||||
} finally {
|
||||
setIsCatalogLoading(false);
|
||||
}
|
||||
};
|
||||
fetchCatalog();
|
||||
}, [order.id, currentPlanName]);
|
||||
}, [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 = <RefreshCcw className="w-4 h-4" />;
|
||||
} 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 = <TrendingUp className="w-4 h-4" />;
|
||||
} 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 = <TrendingDown className="w-4 h-4" />;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==========================================
|
||||
|
||||
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 (
|
||||
// 🎯 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="flex flex-col w-full max-h-[95vh] md:h-auto md:max-h-[90vh] 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">
|
||||
@@ -143,18 +309,18 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
<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>
|
||||
{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`}
|
||||
<a
|
||||
href={`/facturation`}
|
||||
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 ↗
|
||||
Voir les factures
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -168,10 +334,11 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
{ 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'}`}
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => setBillingPeriod(p.id)}
|
||||
disabled={loading || cancelLoading}
|
||||
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'} disabled:opacity-50`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
@@ -195,11 +362,11 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
const topBadge = getPlanBadge(index);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
onClick={() => metrics.isAvailable && setSelectedPlan(plan)}
|
||||
<div
|
||||
key={plan.id}
|
||||
onClick={() => !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 ? 'opacity-50 grayscale cursor-not-allowed border-gray-900' : 'cursor-pointer'}
|
||||
${!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' : ''}`}
|
||||
>
|
||||
@@ -225,10 +392,9 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
{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>
|
||||
@@ -240,7 +406,6 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
</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>
|
||||
@@ -253,7 +418,7 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800/60 pt-4 flex-grow space-y-1.5">
|
||||
<div className="border-t border-gray-800/60 pt-4 grow space-y-1.5">
|
||||
{renderMarkdownFeatures(plan.description)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,26 +429,57 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 mt-auto pt-4 border-t border-gray-800/40 space-y-3">
|
||||
{/* 🎯 NOUVEAU : AFFICHAGE DYNAMIQUE (PRORATA ET REMBOURSEMENT) */}
|
||||
{selectedPlan && !isNoChange && (
|
||||
<div className={`border p-3 rounded-xl mb-3 flex items-center justify-between transition-colors
|
||||
${isRefund ? 'bg-amber-950/20 border-amber-900/50' : 'bg-[#0e2238] border-cyan-900/50'}`}
|
||||
>
|
||||
<div>
|
||||
<p className={`text-xs font-bold uppercase tracking-widest ${isRefund ? 'text-amber-400' : 'text-cyan-400'}`}>
|
||||
{isRefund
|
||||
? "Remboursement estimé"
|
||||
: finalInvoicePrice > 0.01
|
||||
? "Montant à régler aujourd'hui"
|
||||
: "Aucun frais immédiat"}
|
||||
</p>
|
||||
{isProrataApplied && <p className="text-[10px] text-gray-500 mt-0.5">Calculé au prorata des jours restants</p>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`text-xl font-black font-mono ${isRefund ? 'text-amber-400' : 'text-white'}`}>
|
||||
{isRefund ? "-" : ""}{Math.abs(finalInvoicePrice).toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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)]'
|
||||
<button
|
||||
onClick={handleMigration}
|
||||
disabled={loading || cancelLoading || isNoChange}
|
||||
className={`w-full py-4 rounded-xl font-black uppercase text-xs tracking-widest transition-all duration-300 flex items-center justify-center gap-3
|
||||
${isNoChange
|
||||
? 'bg-gray-900 text-gray-500 border border-gray-800 cursor-not-allowed shadow-none'
|
||||
: buttonColor
|
||||
}`}
|
||||
>
|
||||
{loading
|
||||
? 'Traitement en cours...'
|
||||
: isNoChange
|
||||
? 'Abonnement actuel (Aucune modification)'
|
||||
: `Valider la modification`
|
||||
{loading ? <Loader className="w-4 h-4 animate-spin" /> : (!isNoChange && ActionIcon)}
|
||||
|
||||
{loading
|
||||
? 'Traitement en cours...'
|
||||
: isNoChange
|
||||
? 'Abonnement actuel (Aucune modification)'
|
||||
: migrationLabel
|
||||
}
|
||||
</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
|
||||
onClick={handleCancel}
|
||||
disabled={loading || cancelLoading}
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{cancelLoading ? <Loader className="w-4 h-4 animate-spin" /> : <ShieldAlert className="w-4 h-4" />}
|
||||
{cancelLoading ? 'Résiliation en cours...' : 'Résilier l\'abonnement réseau'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user