Compare commits
3 Commits
master
..
9ad44d57b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ad44d57b3 | |||
| eb48f506cf | |||
| ef8f4a96b4 |
+1
-1
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NEXUS by gise | Portail d'Infrastructure Centralisée</title>
|
||||
<meta name="description" content="Accédez au portail NEXUS by gise. Pilotez votre hébergement web, vos instances serveurs et votre stockage Cloud depuis une console unifiée.">
|
||||
<meta name="description" content="Accédez au portail GISE Nexus. Pilotez votre hébergement web, vos instances serveurs et votre stockage Cloud depuis une console unifiée.">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+40
-110
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CreditCard, Check, Globe, Loader, ToggleLeft, ShieldAlert, Calendar, TrendingUp, TrendingDown, RefreshCcw } from 'lucide-react';
|
||||
import { getProductList, getServiceDetails, cancelOrder } from '../../services/billing_api';
|
||||
import { getProductList, getHostingServiceDetails } from '../../services/api';
|
||||
const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_API_BASE_URL || '';
|
||||
|
||||
export default function WebServiceSubscriptionManager({ order, onClose, onRefresh, onAlert }) {
|
||||
export default function SubscriptionManager({ order, onClose, onRefresh, onAlert }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cancelLoading, setCancelLoading] = useState(false);
|
||||
const [catalog, setCatalog] = useState([]);
|
||||
@@ -15,15 +15,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
|
||||
const [billingPeriod, setBillingPeriod] = useState(order.period || '1M');
|
||||
|
||||
// 🎯 NOUVEAU : État pour la modale de résiliation
|
||||
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
|
||||
|
||||
// 1. Récupération des détails techniques (Domaine/IP)
|
||||
useEffect(() => {
|
||||
const fetchDetails = async () => {
|
||||
setIsDetailsLoading(true);
|
||||
try {
|
||||
const data = await getServiceDetails(order.id);
|
||||
const data = await getHostingServiceDetails(order.id);
|
||||
setServiceDetails(data);
|
||||
} catch (err) {
|
||||
console.error("Erreur détails service:", err);
|
||||
@@ -42,8 +39,8 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
|
||||
// 2. Extraction robuste du domaine
|
||||
let extractedDomain = "Aucun domaine lié";
|
||||
if (serviceDetails?.config?.hostname || serviceDetails?.config?.sld && serviceDetails?.config?.tld) {
|
||||
extractedDomain = serviceDetails.config.hostname || serviceDetails.config.sld+serviceDetails.config.tld;
|
||||
if (serviceDetails?.domain || serviceDetails?.config?.domain) {
|
||||
extractedDomain = serviceDetails.domain || serviceDetails.config.domain;
|
||||
}
|
||||
|
||||
const renderMarkdownFeatures = (text) => {
|
||||
@@ -177,7 +174,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
|
||||
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;
|
||||
|
||||
@@ -185,10 +187,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
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)]";
|
||||
@@ -200,9 +204,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==========================================
|
||||
|
||||
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);
|
||||
@@ -224,7 +231,7 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
target_plan_id: selectedPlan.id,
|
||||
new_period: billingPeriod,
|
||||
new_price: finalInvoicePrice.toFixed(2),
|
||||
action_type: actionType
|
||||
action_type: actionType // 🎯 On envoie l'information au backend !
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
@@ -242,55 +249,40 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// 🛡️ NOUVELLE LOGIQUE DE RÉSILIATION (US 2.1 / 2.2)
|
||||
// ==========================================
|
||||
const calculateRefundEligibility = () => {
|
||||
if (!order || !order.activated_at) return false;
|
||||
const orderDate = new Date(order.activated_at);
|
||||
console.log("Order Date:", orderDate);
|
||||
const now = new Date();
|
||||
const diffDays = Math.ceil(Math.abs(now - orderDate) / (1000 * 60 * 60 * 24));
|
||||
return diffDays <= 14;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
const isEligibleForRefund = calculateRefundEligibility();
|
||||
|
||||
const handleConfirmCancel = async () => {
|
||||
setCancelLoading(true);
|
||||
try {
|
||||
const data = await cancelOrder(order.id);
|
||||
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();
|
||||
|
||||
// Succès normal
|
||||
if (data.status === 'success') {
|
||||
if (onAlert) onAlert("Résilié", data.message, "success");
|
||||
setIsCancelModalOpen(false);
|
||||
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, "error");
|
||||
if (onAlert) onAlert("Erreur", data.error || "Impossible de résilier l'abonnement.", "error");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erreur API Annulation:", err);
|
||||
|
||||
// 🛡️ LE FILET DE SÉCURITÉ : Si on détecte l'erreur HTML 502 (Unexpected token)
|
||||
// Cela signifie que le serveur a bien redémarré HestiaCP et coupé la connexion.
|
||||
if (err.message && err.message.includes('Unexpected token')) {
|
||||
if (onAlert) onAlert("Succès", "L'abonnement a été annulé et les services mis à jour.", "success");
|
||||
setIsCancelModalOpen(false);
|
||||
// On met un petit délai avant de rafraîchir, le temps qu'HestiaCP finisse de recharger
|
||||
if (onRefresh) setTimeout(() => onRefresh(), 2000);
|
||||
if (onClose) setTimeout(() => onClose(), 2000);
|
||||
} else {
|
||||
if (onAlert) onAlert("Erreur", "Problème réseau lors de la résiliation.", "error");
|
||||
}
|
||||
if (onAlert) onAlert("Erreur", "Problème réseau ou serveur.", "error");
|
||||
} finally {
|
||||
setCancelLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full max-h-[95vh] md:h-auto md:max-h-[90vh] max-w-6xl mx-auto text-white relative">
|
||||
<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">
|
||||
@@ -324,7 +316,10 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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 les factures
|
||||
</a>
|
||||
</div>
|
||||
@@ -434,6 +429,7 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
</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'}`}
|
||||
@@ -477,9 +473,8 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 🎯 NOUVEAU BOUTON : Ouvre la modale au lieu de faire un confirm() */}
|
||||
<button
|
||||
onClick={() => setIsCancelModalOpen(true)}
|
||||
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"
|
||||
>
|
||||
@@ -487,71 +482,6 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
||||
{cancelLoading ? 'Résiliation en cours...' : 'Résilier l\'abonnement réseau'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ========================================================================= */}
|
||||
{/* 🛡️ MODALE DE CONFIRMATION (Thème Sombre Intégré) */}
|
||||
{/* ========================================================================= */}
|
||||
{isCancelModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
||||
<div className="bg-[#0d1527] border border-gray-800 rounded-2xl shadow-2xl max-w-md w-full p-6 transform transition-all">
|
||||
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<ShieldAlert className="w-5 h-5 text-red-500" />
|
||||
Confirmer la résiliation
|
||||
</h3>
|
||||
|
||||
{/* Condition d'affichage : Rétractation J+14 vs Résiliation standard */}
|
||||
{isEligibleForRefund ? (
|
||||
<div className="bg-emerald-950/30 border border-emerald-900/50 p-4 mb-5 text-emerald-400 text-sm rounded-xl">
|
||||
<p className="font-bold mb-1 flex items-center gap-2">
|
||||
<Check className="w-4 h-4" /> Droit de rétractation (≤ 14 jours)
|
||||
</p>
|
||||
<p className="text-emerald-500/80 mt-2">
|
||||
Votre abonnement a été activé il y a moins de 14 jours.
|
||||
<strong className="text-emerald-400"> Vous allez être intégralement remboursé</strong>.
|
||||
Vos services seront supprimés immédiatement.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-amber-950/30 border border-amber-900/50 p-4 mb-5 text-amber-400 text-sm rounded-xl">
|
||||
<p className="font-bold mb-1">📅 Résiliation standard (> 14 jours)</p>
|
||||
<p className="text-amber-500/80 mt-2">
|
||||
La période de rétractation est dépassée. Votre abonnement restera actif jusqu'à sa date d'échéance officielle.
|
||||
<strong className="text-amber-400"> Aucun renouvellement ni prélèvement n'aura lieu.</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-gray-400 mb-6 text-sm">
|
||||
Êtes-vous absolument sûr de vouloir procéder ? Cette action est irréversible une fois validée par nos serveurs.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setIsCancelModalOpen(false)}
|
||||
disabled={cancelLoading}
|
||||
className="px-5 py-2.5 bg-[#090f1c] text-gray-300 border border-gray-800 rounded-xl hover:bg-gray-800 hover:text-white disabled:opacity-50 transition-colors text-sm font-bold tracking-wide"
|
||||
>
|
||||
Retour
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmCancel}
|
||||
disabled={cancelLoading}
|
||||
className="px-5 py-2.5 bg-red-600/90 hover:bg-red-500 text-white border border-red-500/50 rounded-xl flex items-center disabled:opacity-50 transition-colors text-sm font-bold tracking-wide shadow-[0_0_15px_rgba(220,38,38,0.3)]"
|
||||
>
|
||||
{cancelLoading ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 animate-spin mr-2" />
|
||||
Traitement...
|
||||
</>
|
||||
) : (
|
||||
"Confirmer"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,55 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Server, Database, Cloud, Globe, ExternalLink, Play, Loader } from 'lucide-react';
|
||||
import { getHostingServiceDetails } from '../../services/api';
|
||||
|
||||
export default function InstanceCard({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) {
|
||||
// 1. États pour les détails techniques chargés via API
|
||||
const [serviceDetails, setServiceDetails] = useState(null);
|
||||
const [isLoadingDetails, setIsLoadingDetails] = useState(true);
|
||||
|
||||
// 2. Chargement des détails au montage du composant
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const fetchDetails = async () => {
|
||||
try {
|
||||
const data = await getHostingServiceDetails(service.id);
|
||||
if (isMounted) {
|
||||
setServiceDetails(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des détails du service:", error);
|
||||
} finally {
|
||||
if (isMounted) setIsLoadingDetails(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDetails();
|
||||
return () => { isMounted = false; };
|
||||
}, [service.id]);
|
||||
|
||||
// 3. Logique d'affichage (Titre, Type de service)
|
||||
const titleLower = (service.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');
|
||||
const isWeb = !isVPS && !isCloud && !isDB;
|
||||
const hasIP = service.hostingDetails?.ip && service.hostingDetails.ip !== '127.0.0.1' && service.hostingDetails.ip !== '';
|
||||
|
||||
// 4. Extraction intelligente du domaine et de l'IP
|
||||
// Priorité : API > Titre de la commande
|
||||
const rawTitle = service.title || '';
|
||||
const titleMatch = rawTitle.match(/(?: for | pour )(.+)$/i);
|
||||
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
|
||||
|
||||
const displayDomain = serviceDetails?.domain || serviceDetails?.config?.domain || domainFromTitle || null;
|
||||
const displayIP = serviceDetails?.ip || serviceDetails?.config?.ip || null;
|
||||
|
||||
// Logique des boutons
|
||||
let buttonText = "CONSOLE D'ADMINISTRATION";
|
||||
let ButtonIcon = ExternalLink;
|
||||
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
|
||||
|
||||
if (isVPS) {
|
||||
if (hasIP) { buttonText = "GÉRER L'INSTANCE"; ButtonIcon = ExternalLink; }
|
||||
if (displayIP) { buttonText = "GÉRER L'INSTANCE"; ButtonIcon = ExternalLink; }
|
||||
else { buttonText = "INITIALISATION"; ButtonIcon = Play; buttonStyle = "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500 hover:text-gray-900 border-yellow-500/50"; }
|
||||
} else if (isWeb) { buttonText = "GÉRER L'HÉBERGEMENT"; ButtonIcon = Globe; buttonStyle = "bg-emerald-400/10 hover:bg-emerald-400 text-emerald-400 hover:text-gray-900 border-emerald-400"; }
|
||||
else if (isCloud) { buttonText = "ACCÉDER AU CLOUD"; buttonStyle = "bg-blue-400/10 hover:bg-blue-400 text-blue-400 hover:text-gray-900 border-blue-400"; }
|
||||
@@ -26,12 +62,8 @@ export default function InstanceCard({ service, vpcs, onAssignVpc, onRemoveVpc,
|
||||
return <Globe className="w-8 h-8 text-emerald-400" />;
|
||||
};
|
||||
|
||||
const baseTitle = (service.title || '').split(/(?: for | pour )/i)[0].trim();
|
||||
const baseTitle = rawTitle.split(/(?: for | pour )/i)[0].trim();
|
||||
const shortTitle = baseTitle.split(' ').slice(0, 2).join(' ');
|
||||
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
|
||||
const domainFromTitle = titleMatch ? titleMatch[1].trim() : null;
|
||||
let displayDomain = service.hostingDetails?.domain && service.hostingDetails.domain !== '127.0.0.1' ? service.hostingDetails.domain : domainFromTitle || service.domain;
|
||||
if (!displayDomain || displayDomain === '127.0.0.1') displayDomain = null;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between hover:border-gray-700 transition-all shadow-lg">
|
||||
@@ -40,27 +72,63 @@ export default function InstanceCard({ service, vpcs, onAssignVpc, onRemoveVpc,
|
||||
<div className="p-2 bg-black/40 rounded-lg">{getServiceIcon()}</div>
|
||||
{service.status === 'active' ? (
|
||||
<span className="bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded text-xs border border-emerald-500/20 font-bold tracking-widest">
|
||||
{isVPS && !hasIP ? 'AWAITING INIT' : 'ONLINE'}
|
||||
{isVPS && !displayIP ? 'AWAITING INIT' : 'ONLINE'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-orange-500/10 text-orange-400 px-2 py-1 rounded text-xs border border-orange-500/20 animate-pulse">DEPLOYING</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-white text-lg truncate" title={baseTitle}>{shortTitle}</h3>
|
||||
<div className="flex flex-col gap-1 mt-2 mb-4 h-8 justify-center">
|
||||
{displayDomain ? ( <p className={`${isVPS ? 'text-cyan-400' : 'text-emerald-400'} text-xs font-mono truncate`} title={displayDomain}>{displayDomain}</p> ) : ( <p className="text-gray-600 text-xs font-mono italic">En attente de déploiement</p> )}
|
||||
|
||||
{/* Zone Domaine & IP */}
|
||||
<div className="flex flex-col gap-1 mt-2 mb-4 h-12 justify-center">
|
||||
{isLoadingDetails ? (
|
||||
<div className="flex items-center text-gray-500 text-xs gap-2">
|
||||
<Loader className="w-3 h-3 animate-spin" /> Chargement...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{displayDomain ? (
|
||||
<p className={`${isVPS ? 'text-cyan-400' : 'text-emerald-400'} text-xs font-mono truncate`} title={displayDomain}>
|
||||
{displayDomain}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-gray-600 text-xs font-mono italic">Aucun domaine</p>
|
||||
)}
|
||||
{displayIP && (
|
||||
<p className="text-gray-500 text-[10px] font-mono mt-0.5" title={`IP: ${displayIP}`}>
|
||||
IP: {displayIP}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto space-y-3">
|
||||
<div className="flex items-center justify-between text-sm border-t border-gray-800 pt-3">
|
||||
<span className="text-gray-500 text-xs">Projet VPC:</span>
|
||||
<select onChange={(e) => { const val = e.target.value; if (val === "free") onRemoveVpc(service.id); else if (val) onAssignVpc(service.id, val); }} className="bg-black border border-gray-700 text-gray-300 rounded px-2 py-1 outline-none focus:border-cyan-400 text-xs w-32.5" defaultValue={vpcs.find(v => v.services.includes(service.id))?.id || "free"}>
|
||||
<select
|
||||
onChange={(e) => { const val = e.target.value; if (val === "free") onRemoveVpc(service.id); else if (val) onAssignVpc(service.id, val); }}
|
||||
className="bg-black border border-gray-700 text-gray-300 rounded px-2 py-1 outline-none focus:border-cyan-400 text-xs w-32.5"
|
||||
defaultValue={vpcs.find(v => v.services.includes(service.id))?.id || "free"}
|
||||
>
|
||||
<option value="free">-- Libre --</option>
|
||||
{vpcs.map(vpc => ( <option key={vpc.id} value={vpc.id}>{vpc.name}</option> ))}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => onOpenConsole(service)} disabled={isConnecting === service.id || service.status !== 'active'} className={`w-full flex items-center justify-center space-x-2 py-2.5 rounded-lg font-bold text-sm tracking-wide transition-all disabled:opacity-50 border ${buttonStyle}`}>
|
||||
{isConnecting === service.id ? ( <><Loader className="w-4 h-4 animate-spin" /><span>CONNEXION...</span></> ) : ( <><ButtonIcon className="w-4 h-4" /><span>{buttonText}</span></> )}
|
||||
|
||||
<button
|
||||
onClick={() => onOpenConsole(service)}
|
||||
disabled={isConnecting === service.id || service.status !== 'active'}
|
||||
className={`w-full flex items-center justify-center space-x-2 py-2.5 rounded-lg font-bold text-sm tracking-wide transition-all disabled:opacity-50 border ${buttonStyle}`}
|
||||
>
|
||||
{isConnecting === service.id ? (
|
||||
<><Loader className="w-4 h-4 animate-spin" /><span>CONNEXION...</span></>
|
||||
) : (
|
||||
<><ButtonIcon className="w-4 h-4" /><span>{buttonText}</span></>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Lock } from 'lucide-react';
|
||||
import { Plus, Lock, X } from 'lucide-react';
|
||||
|
||||
export default function CreateTicketModal({ isOpen, onClose, onSubmit, helpdesks, defaultHelpdesk, isSubmitting }) {
|
||||
const [subject, setSubject] = useState('');
|
||||
@@ -30,7 +30,9 @@ export default function CreateTicketModal({ isOpen, onClose, onSubmit, helpdesks
|
||||
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-2">
|
||||
<Plus className="w-5 h-5 text-cyan-400" /> OUVRIR UNE REQUÊTE
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
||||
<button onClick={onClose} className="p-2 text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Send, Loader } from 'lucide-react';
|
||||
import { Send, Loader, X } from 'lucide-react';
|
||||
|
||||
export default function TicketThreadModal({ ticket, onClose, onReply, isLoadingConversation }) {
|
||||
const [replyMessage, setReplyMessage] = useState('');
|
||||
@@ -32,7 +32,9 @@ export default function TicketThreadModal({ ticket, onClose, onReply, isLoadingC
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white line-clamp-1">{ticket.subject}</h3>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition text-xl bg-black/50 p-2 rounded-lg">✖</button>
|
||||
<button onClick={onClose} className="p-2 text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-6 scrollbar-thin scrollbar-thumb-gray-800">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { getClientTickets } from '../services/api';
|
||||
import { LayoutDashboard, Globe, Settings, FileText } from 'lucide-react';
|
||||
import ConfirmLogoutModal from '../components/ui/ConfirmLogoutModal'; // Le modal extrait !
|
||||
|
||||
export default function AppLayout() {
|
||||
@@ -15,7 +16,7 @@ export default function AppLayout() {
|
||||
const unread = data.list.filter(t => t.status === 'on_hold' || t.unread).length;
|
||||
setUnreadCount(unread);
|
||||
}
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,10 +32,9 @@ export default function AppLayout() {
|
||||
|
||||
// 🌟 La fonction magique Tailwind pour les liens du menu
|
||||
const navLinkClass = ({ isActive }) =>
|
||||
`block px-6 py-4 no-underline border-l-4 transition-all uppercase tracking-widest text-sm font-mono ${
|
||||
isActive
|
||||
? 'text-cyan-400 bg-cyan-400/5 border-cyan-400'
|
||||
: 'text-[#888] border-transparent hover:text-gray-300 hover:bg-white/5'
|
||||
`block px-6 py-4 no-underline border-l-4 transition-all uppercase tracking-widest text-sm font-mono ${isActive
|
||||
? 'text-cyan-400 bg-cyan-400/5 border-cyan-400'
|
||||
: 'text-[#888] border-transparent hover:text-gray-300 hover:bg-white/5'
|
||||
}`;
|
||||
|
||||
return (
|
||||
@@ -75,6 +75,7 @@ export default function AppLayout() {
|
||||
</NavLink>
|
||||
|
||||
<NavLink className={navLinkClass} to="/facturation">Facturation</NavLink>
|
||||
|
||||
<div className="flex justify-between items-center px-6 py-4 text-[#444] border-l-4 border-transparent uppercase tracking-widest text-sm font-mono cursor-not-allowed select-none">
|
||||
<span>Profil & Sécurité</span>
|
||||
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileText, Download, CreditCard, Clock, CheckCircle2, XCircle, AlertCircle, Loader, X, Receipt, Trash2, ArrowRight, RefreshCcw } from 'lucide-react';
|
||||
import NotificationModal from '../../components/ui/NotificationModal'; // Vérifie ton chemin
|
||||
import { getClient, getInvoiceList } from '../../services/billing_api'; // Vérifie ton chemin
|
||||
import { getClientProfile, getInvoicesHistory } from '../../services/api'; // Vérifie ton chemin
|
||||
|
||||
export default function BillingHistory() {
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
@@ -21,12 +21,12 @@ export default function BillingHistory() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const profileData = await getClient();
|
||||
const profileData = await getClientProfile();
|
||||
if (!profileData || !profileData.id) {
|
||||
throw new Error("Impossible de vérifier votre identité ou session expirée.");
|
||||
}
|
||||
const invoicesData = await getInvoiceList(profileData.id);
|
||||
setInvoices(invoicesData.list || []);
|
||||
const invoicesData = await getInvoicesHistory(profileData.id);
|
||||
setInvoices(invoicesData.data || []);
|
||||
} catch (err) {
|
||||
setError(err.message || "Problème de connexion avec le serveur.");
|
||||
} finally {
|
||||
@@ -212,7 +212,9 @@ export default function BillingHistory() {
|
||||
<div className="space-y-4">
|
||||
{(() => {
|
||||
// FILTRE : On ne garde que les items dont le prix est strictement supérieur à 0
|
||||
const validItems = selectedInvoice.lines
|
||||
const validItems = selectedInvoice.items
|
||||
? selectedInvoice.items.filter(item => parseFloat(item.price) > 0)
|
||||
: [];
|
||||
|
||||
if (validItems.length > 0) {
|
||||
return validItems.map((item, idx) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { resetCart, addToCart, checkoutCart, getProductList, getClient } from '../../services/billing_api';
|
||||
import { resetCart, addToCart, checkoutCart, getProductList, getClientProfile } from '../../services/api';
|
||||
|
||||
export default function Checkout() {
|
||||
const { productId } = useParams();
|
||||
@@ -18,7 +18,7 @@ export default function Checkout() {
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// 1. On charge d'abord le produit
|
||||
// 1. On charge d'abord le produit (Requête Publique)
|
||||
const productData = await getProductList();
|
||||
const foundProduct = productData.list?.find(p => p.id === parseInt(productId));
|
||||
|
||||
@@ -29,14 +29,15 @@ export default function Checkout() {
|
||||
}
|
||||
setProduct(foundProduct);
|
||||
|
||||
// 2. Ensuite, on tente de charger le profil
|
||||
// 2. Ensuite, on tente de charger le profil (Requête Privée)
|
||||
try {
|
||||
const profileData = await getClient();
|
||||
const profileData = await getClientProfile();
|
||||
setUserProfile(profileData);
|
||||
} catch (profileErr) {
|
||||
// Si on tombe ici, c'est que FOSSBilling refuse l'accès au profil.
|
||||
console.error("Rejet API Profil :", profileErr);
|
||||
setError("Accès refusé. Vous devez être connecté à votre compte pour provisionner une instance.");
|
||||
// Tu pourras décommenter la ligne suivante plus tard pour forcer la redirection :
|
||||
// navigate('/login');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
|
||||
+10
-24
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getOrdersList } from '../../services/billing_api';
|
||||
import { getClientOrders } from '../../services/api';
|
||||
import { AlertCircle, Loader, FileText, X } from 'lucide-react';
|
||||
|
||||
// Importation des composants
|
||||
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
||||
import NewServiceCard from '../../components/dashboard/NewServiceCard';
|
||||
import NotificationModal from '../../components/ui/NotificationModal';
|
||||
import WebServiceSubscriptionManager from '../../components/dashboard/WebServiceSubscriptionManager';
|
||||
import SubscriptionManager from '../../components/dashboard/SubscriptionManager'; // 🌟 Le nouveau composant !
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
@@ -20,12 +20,10 @@ export default function Dashboard() {
|
||||
|
||||
const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
|
||||
|
||||
const fetchInventory = async (retryCount = 0) => {
|
||||
// On n'affiche le loader global que lors du tout premier essai
|
||||
if (retryCount === 0) setIsLoading(true);
|
||||
|
||||
const fetchInventory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getOrdersList();
|
||||
const data = await getClientOrders();
|
||||
|
||||
if (data.list) {
|
||||
// LE FILTRE CHIRURGICAL PAR PREFIXE
|
||||
@@ -39,28 +37,17 @@ export default function Dashboard() {
|
||||
title.startsWith('domaine ') ||
|
||||
title.startsWith('enregistrement ');
|
||||
|
||||
return (order.status === 'active' || order.status === 'pending_setup') && !isGhostProduct;
|
||||
return !isGhostProduct;
|
||||
});
|
||||
|
||||
setOrders(filteredOrders);
|
||||
} else {
|
||||
setOrders([]);
|
||||
}
|
||||
|
||||
setError(null); // On efface l'erreur s'il y en avait une
|
||||
setIsLoading(false); // Le chargement est terminé
|
||||
|
||||
} catch (err) {
|
||||
// 🛡️ SYSTÈME D'AUTO-GUÉRISON : Si on détecte le redémarrage d'HestiaCP (Erreur HTML/JSON)
|
||||
if (err.message && err.message.includes('Unexpected token') && retryCount < 3) {
|
||||
console.warn(`Redémarrage d'infrastructure détecté. Nouvelle tentative... (Essai ${retryCount + 1}/3)`);
|
||||
// On attend 2,5 secondes, puis la fonction s'appelle elle-même (récursivité)
|
||||
setTimeout(() => fetchInventory(retryCount + 1), 2500);
|
||||
} else {
|
||||
// S'il y a une vraie erreur persistante, on l'affiche au client
|
||||
setError(err.message || "Impossible de récupérer la télémétrie des services.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
setError(err.message || "Impossible de récupérer la télémétrie des services.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +57,6 @@ export default function Dashboard() {
|
||||
|
||||
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
||||
const handleManageSubscription = (order) => {
|
||||
console.log("Gestion de l'abonnement pour la commande :", order);
|
||||
// 1. Détection du type de service
|
||||
const titleLower = (order.title || '').toLowerCase();
|
||||
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||
@@ -142,7 +128,7 @@ export default function Dashboard() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<WebServiceSubscriptionManager
|
||||
<SubscriptionManager
|
||||
order={activeSubscriptionModal}
|
||||
onClose={() => setActiveSubscriptionModal(null)}
|
||||
onRefresh={fetchInventory}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getOrdersList, getOrderDetails } from '../../services/billing_api';
|
||||
import { getMyServices, getHostingServiceDetails } from '../../services/api';
|
||||
import { useVPC } from '../../services/useVPC';
|
||||
import { Server, Globe, Folder, Trash2, Loader, Plus, AlertCircle } from 'lucide-react';
|
||||
import { Server, Globe, Folder, Trash2, Loader, Plus, AlertCircle, X } from 'lucide-react';
|
||||
|
||||
// Importation de tes nouveaux composants modulaires !
|
||||
import NotificationModal from '../../components/ui/NotificationModal';
|
||||
@@ -25,12 +25,12 @@ export default function Services() {
|
||||
|
||||
const fetchServices = useCallback(async () => {
|
||||
try {
|
||||
const data = await getOrdersList();
|
||||
const data = await getMyServices();
|
||||
if (data.list && data.list.length > 0) {
|
||||
const detailedServices = await Promise.all(data.list.map(async (order) => {
|
||||
let hDetails = null;
|
||||
if (order.status === 'active' && (order.plugin === 'hosting' || (order.title || '').toLowerCase().includes('vps'))) {
|
||||
try { hDetails = await getOrderDetails(order.id); } catch (e) {}
|
||||
try { hDetails = await getHostingServiceDetails(order.id); } catch (e) { }
|
||||
}
|
||||
return { ...order, hostingDetails: hDetails };
|
||||
}));
|
||||
@@ -65,7 +65,7 @@ export default function Services() {
|
||||
} else if (isDB) {
|
||||
window.open('https://pma.gise.be/', '_blank');
|
||||
} else if (isVPS || isWeb) {
|
||||
const freshDetails = await getOrderDetails(service.id);
|
||||
const freshDetails = await getHostingServiceDetails(service.id);
|
||||
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
|
||||
}
|
||||
} catch (err) { triggerAlert("Erreur", err.message, "error"); }
|
||||
@@ -132,7 +132,9 @@ export default function Services() {
|
||||
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
|
||||
{activeServiceModal.type === 'vps' ? <><Server className="w-6 h-6 text-cyan-400" /> GESTION VPS</> : <><Globe className="w-6 h-6 text-emerald-400" /> GESTION WEB</>}
|
||||
</h3>
|
||||
<button onClick={() => setActiveServiceModal(null)} className="text-gray-400 hover:text-white transition text-xl">✖</button>
|
||||
<button onClick={() => setActiveServiceModal(null)} className="p-2 text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{activeServiceModal.type === 'vps' ? (
|
||||
activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? <VpsManager details={activeServiceModal.details} orderId={activeServiceModal.data.id} onRefresh={fetchServices} onAlert={triggerAlert} /> : <VpsDeployer serviceId={activeServiceModal.data.id} onDeploySuccess={() => { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// src/pages/app/Store.jsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Database, Cloud, Globe, Loader, AlertCircle } from 'lucide-react';
|
||||
import { getProductList } from '../../services/billing_api';
|
||||
import CategorySection from '../../components/store/CategorySection';
|
||||
import { getProductList } from '../../services/api';
|
||||
import CategorySection from '../../components/store/CategorySection'; // L'import magique
|
||||
|
||||
export default function Store() {
|
||||
const [groupedProducts, setGroupedProducts] = useState({});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { loginClient } from '../../services/billing_api';
|
||||
import { loginClient } from '../../services/api';
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { createNewClient } from '../../services/billing_api';
|
||||
import { registerUnifiedClient } from '../../services/api';
|
||||
import NotificationModal from '../../components/ui/NotificationModal';
|
||||
|
||||
export default function Register() {
|
||||
@@ -28,15 +28,15 @@ export default function Register() {
|
||||
setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès ne correspondent pas." });
|
||||
return;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9]{3,20}$/.test(username)) {
|
||||
if (!/^[a-zA-Z0-9]{3,12}$/.test(username)) {
|
||||
setCustomAlert({ type: 'error', title: 'Identifiant invalide', message: "Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères)." });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await createNewClient(email, firstName, lastName, password, confirmPassword, "individual", username);
|
||||
setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Votre compte a été initialisé.\n\nVous pouvez maintenant vous connecter." });
|
||||
await registerUnifiedClient(email, username, password, firstName, lastName);
|
||||
setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Vos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\n\nVous pouvez maintenant vous connecter." });
|
||||
} catch (err) {
|
||||
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." });
|
||||
} finally {
|
||||
@@ -56,7 +56,7 @@ export default function Register() {
|
||||
Créer un accès réseau
|
||||
</h2>
|
||||
<p className="text-[#888] font-mono text-sm mb-6">
|
||||
[ INITIALISATION DU PROVISIONNEMENT ]
|
||||
[ INITIALISATION DU PROVISIONNEMENT TRIPLE ]
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleRegister} className="font-mono">
|
||||
|
||||
+75
-185
@@ -1,7 +1,7 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_API_BASE_URL || '';
|
||||
|
||||
// Le moteur de requête
|
||||
// Le moteur de requête unifié et intelligent
|
||||
const apiCall = async (endpoint, param2 = 'GET', param3 = null) => {
|
||||
let method = 'GET';
|
||||
let body = null;
|
||||
@@ -64,6 +64,24 @@ const apiCall = async (endpoint, param2 = 'GET', param3 = null) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// ROUTES FOSSBILLING NATIVES (BACKTICKS INTÉGRÉS)
|
||||
// ==========================================
|
||||
|
||||
export const loginClient = (email, password) =>
|
||||
apiCall(`${BASE_URL}/api/guest/client/login`, { email, password });
|
||||
|
||||
// Récupère la liste des services/commandes du client
|
||||
export const getClientOrders = () =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get_list`);
|
||||
|
||||
// Récupère les détails techniques du service rattaché à une commande
|
||||
export const getOrderService = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/service`, { id: order_id });
|
||||
|
||||
// Récupère le catalogue public des produits FOSSBilling
|
||||
export const getProductList = () =>
|
||||
apiCall(`${BASE_URL}/api/guest/product/get_list`);
|
||||
|
||||
// Vide le panier (Action PUBLIQUE : on passe par l'API Guest)
|
||||
export const resetCart = async () => {
|
||||
@@ -82,6 +100,22 @@ export const resetCart = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Ajoute un produit au panier avec ses options étalées à la racine
|
||||
export const addToCart = (productId, period, additionalData = {}) =>
|
||||
apiCall(`${BASE_URL}/api/guest/cart/add_item`, 'POST', {
|
||||
id: productId,
|
||||
period: period,
|
||||
...additionalData // Les 3 petits points "étalent" le contenu de l'objet
|
||||
});
|
||||
|
||||
// Valide le panier (Action PRIVÉE : on reste sur l'API Client pour générer la facture)
|
||||
export const checkoutCart = () =>
|
||||
apiCall(`${BASE_URL}/api/client/cart/checkout`, 'POST');
|
||||
|
||||
// Récupère les informations du client connecté
|
||||
export const getClientProfile = () =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/get`, 'GET');
|
||||
|
||||
// ==========================================
|
||||
// ROUTES PERSONNALISÉES (CUSTOM API)
|
||||
// ==========================================
|
||||
@@ -110,6 +144,18 @@ export const registerUnifiedClient = async (email, username, password, firstName
|
||||
}
|
||||
}
|
||||
|
||||
// Récupère la liste de toutes les commandes actives du client
|
||||
export const getMyServices = () =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get_list`, 'GET');
|
||||
|
||||
// Récupère les détails secrets d'un service (dont le mot de passe HestiaCP/VPS)
|
||||
export const getServiceDetails = (orderId) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get`, 'POST', { id: orderId });
|
||||
|
||||
// Récupère les secrets spécifiques du service physique attaché à une commande
|
||||
export const getHostingServiceDetails = (orderId) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/service`, 'POST', { id: orderId });
|
||||
|
||||
// Force la réinitialisation du mot de passe sur le serveur distant (HestiaCP)
|
||||
export const resetHostingPassword = (orderId, newPassword) =>
|
||||
apiCall(`${BASE_URL}/api/client/servicehosting/change_password`, 'POST', {
|
||||
@@ -145,204 +191,48 @@ export const launchSSOGateway = (username, password) => {
|
||||
document.body.removeChild(form);
|
||||
};
|
||||
|
||||
// #######################################################################################
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES ACCES
|
||||
// ROUTES SUPPORT FOSSBILLING (NATIVES)
|
||||
// ==========================================
|
||||
|
||||
// Connexion
|
||||
export const loginClient = (email, password) =>
|
||||
apiCall(`${BASE_URL}/api/guest/client/login`, { email, password });
|
||||
// Récupère la liste de tous les tickets du client
|
||||
export const getClientTickets = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`, 'GET');
|
||||
|
||||
// Deconnexion
|
||||
export const logoutClient = () =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/logout`);
|
||||
// Récupère le contenu et les messages d'un ticket spécifique
|
||||
export const getTicketDetails = (ticketId) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get`, 'POST', { id: ticketId });
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DU CLIENT
|
||||
// ==========================================
|
||||
|
||||
// Creer un nouveau profil client
|
||||
export const createClientProfile = (email, last_name, aid, gender, country, city, birthday, company, company_vat, company_number, type, address_1, address_2, postcode, state, phone, phone_cc, document_type, document_nr, notes, lang, custom_1, custom_2, custom_3, custom_4, custom_5, custom_6, custom_7, custom_8, custom_9, custom_10) =>
|
||||
apiCall(`${BASE_URL}/api/guest/profile/create`, {
|
||||
email: email,
|
||||
last_name: last_name,
|
||||
aid: aid,
|
||||
gender: gender,
|
||||
country: country,
|
||||
city: city,
|
||||
birthday: birthday,
|
||||
company: company,
|
||||
company_vat: company_vat,
|
||||
company_number: company_number,
|
||||
type: type,
|
||||
address_1: address_1,
|
||||
address_2: address_2,
|
||||
postcode: postcode,
|
||||
state: state,
|
||||
phone: phone,
|
||||
phone_cc: phone_cc,
|
||||
document_type: document_type,
|
||||
document_nr: document_nr,
|
||||
notes: notes,
|
||||
lang: lang,
|
||||
custom_1: custom_1,
|
||||
custom_2: custom_2,
|
||||
custom_3: custom_3,
|
||||
custom_4: custom_4,
|
||||
custom_5: custom_5,
|
||||
custom_6: custom_6,
|
||||
custom_7: custom_7,
|
||||
custom_8: custom_8,
|
||||
custom_9: custom_9,
|
||||
custom_10: custom_10
|
||||
});
|
||||
|
||||
// Recuperer les informations du client
|
||||
export const getClientProfile = () =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/get`, 'GET');
|
||||
|
||||
// Update les informations du client
|
||||
export const updateClientProfile = (email, last_name, aid, gender, country, city, birthday, company, company_vat, company_number, type, address_1, address_2, postcode, state, phone, phone_cc, document_type, document_nr, notes, lang, custom_1, custom_2, custom_3, custom_4, custom_5, custom_6, custom_7, custom_8, custom_9, custom_10) =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/update`, {
|
||||
email: email,
|
||||
last_name: last_name,
|
||||
aid: aid,
|
||||
gender: gender,
|
||||
country: country,
|
||||
city: city,
|
||||
birthday: birthday,
|
||||
company: company,
|
||||
company_vat: company_vat,
|
||||
company_number: company_number,
|
||||
type: type,
|
||||
address_1: address_1,
|
||||
address_2: address_2,
|
||||
postcode: postcode,
|
||||
state: state,
|
||||
phone: phone,
|
||||
phone_cc: phone_cc,
|
||||
document_type: document_type,
|
||||
document_nr: document_nr,
|
||||
notes: notes,
|
||||
lang: lang,
|
||||
custom_1: custom_1,
|
||||
custom_2: custom_2,
|
||||
custom_3: custom_3,
|
||||
custom_4: custom_4,
|
||||
custom_5: custom_5,
|
||||
custom_6: custom_6,
|
||||
custom_7: custom_7,
|
||||
custom_8: custom_8,
|
||||
custom_9: custom_9,
|
||||
custom_10: custom_10
|
||||
});
|
||||
|
||||
// Changer le mot de passe
|
||||
export const changeClientPassword = (current_password, new_password, confirm_password) =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/change_password`, {current_password:current_password, new_password:new_password, confirm_password:confirm_password});
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES ABONNEMENTS
|
||||
// ==========================================
|
||||
|
||||
// Souscription
|
||||
|
||||
// Resiliation
|
||||
|
||||
// Suppression
|
||||
export const deleteWrongOrder = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/delete`, { id: order_id });
|
||||
|
||||
// Promotion (Surclassement)
|
||||
// Lister
|
||||
export const getUpgrades = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/upgradables`, { id: order_id });
|
||||
|
||||
// Demotion (Declassement)
|
||||
|
||||
// Lister les abonnements du client
|
||||
export const getClientOrders = () =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get_list`);
|
||||
|
||||
// Lister les produits disponibles
|
||||
export const getProductList = () =>
|
||||
apiCall(`${BASE_URL}/api/guest/product/get_list`);
|
||||
|
||||
// Recuperer les details d'un abonnement
|
||||
export const getOrderDetails = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/service`, { id: order_id });
|
||||
|
||||
// Recuperer les details secrets d'un abonnement
|
||||
export const getServiceDetails = (orderId) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get`, 'POST', { id: orderId });
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DU PANIER
|
||||
// ==========================================
|
||||
|
||||
// Ajouter au panier
|
||||
export const addToCart = (productId, period, additionalData = {}) =>
|
||||
apiCall(`${BASE_URL}/api/guest/cart/add_item`, 'POST', {
|
||||
id: productId,
|
||||
period: period,
|
||||
...additionalData // Les 3 petits points "étalent" le contenu de l'objet
|
||||
});
|
||||
|
||||
// Checkout
|
||||
export const checkoutCart = () =>
|
||||
apiCall(`${BASE_URL}/api/client/cart/checkout`, 'POST');
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES FACTURES
|
||||
// ==========================================
|
||||
|
||||
// Creation
|
||||
// Suppression
|
||||
// Modification
|
||||
// Lister
|
||||
export const getInvoiceList = () =>
|
||||
apiCall(`${BASE_URL}/api/client/invoice/get_list`, 'GET');
|
||||
|
||||
// Lire
|
||||
export const getInvoiceDetails = (InvoiceHash) =>
|
||||
apiCall(`${BASE_URL}/api/client/invoice/get`, 'GET');
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES TICKETS
|
||||
// ==========================================
|
||||
|
||||
// Creation
|
||||
// Crée un nouveau ticket de support
|
||||
export const createTicket = (subject, message, helpdesk_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_create`, 'POST', {
|
||||
support_helpdesk_id: helpdesk_id,
|
||||
subject: subject,
|
||||
content: message
|
||||
});
|
||||
// Suppression
|
||||
|
||||
// Lister les tickets
|
||||
export const getClientTickets = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`, 'GET');
|
||||
|
||||
// Lister les helpdesks
|
||||
export const getHelpdesks = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`, 'GET');
|
||||
|
||||
// Lire
|
||||
export const getTicketDetails = (ticketId) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get`, 'POST', { id: ticketId });
|
||||
|
||||
// Repondre
|
||||
// Répond à un ticket existant
|
||||
export const replyTicket = (ticketId, message) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_reply`, 'POST', {
|
||||
id: ticketId,
|
||||
content: message
|
||||
});
|
||||
|
||||
// Récupère la liste dynamique des départements (Helpdesks) configurés sur FOSSBilling
|
||||
export const getHelpdesks = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`, 'GET');
|
||||
|
||||
// ==========================================
|
||||
// ROUTES FACTURATION & REÇUS
|
||||
// ==========================================
|
||||
|
||||
// Récupère l'historique des factures via notre script Custom PHP
|
||||
export const getInvoicesHistory = (clientId) =>
|
||||
apiCall(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, 'POST', {
|
||||
action: 'get_invoices',
|
||||
client_id: clientId
|
||||
});
|
||||
|
||||
// (Optionnel) FOSSBilling Native : Récupère les détails complets d'une facture
|
||||
export const getInvoiceDetails = (invoiceHash) =>
|
||||
apiCall(`${BASE_URL}/api/client/invoice/get`, 'POST', { hash: invoiceHash });
|
||||
@@ -1,346 +0,0 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_API_BASE_URL || '';
|
||||
|
||||
// Le moteur de requête
|
||||
const apiCall = async (endpoint, param2 = 'GET', param3 = null) => {
|
||||
let method = 'GET';
|
||||
let body = null;
|
||||
|
||||
// DÉTECTION DE SIGNATURE (Le bouclier anti-crash)
|
||||
if (typeof param2 === 'string') {
|
||||
// Cas 1 : On a bien envoyé (URL, "POST", {données})
|
||||
method = param2.toUpperCase();
|
||||
body = param3;
|
||||
} else if (typeof param2 === 'object' && param2 !== null) {
|
||||
// Cas 2 : L'ancienne méthode a envoyé (URL, {données})
|
||||
// On redirige l'objet vers le body, et on force en POST
|
||||
body = param2;
|
||||
method = (typeof param3 === 'string') ? param3.toUpperCase() : 'POST';
|
||||
}
|
||||
|
||||
// 1. Récupération du sésame
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// 2. Préparation de l'enveloppe
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 3. Configuration finale garantie sans objets égarés
|
||||
const options = {
|
||||
method: method,
|
||||
headers: headers,
|
||||
credentials: 'include'
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, options);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error("Accès refusé. Session expirée ou non valide.");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Gestion des erreurs internes de l'API
|
||||
if (data.error) {
|
||||
throw new Error(data.error.message || "Erreur renvoyée par le serveur de facturation.");
|
||||
}
|
||||
|
||||
return data.result || data;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[API FAIL] ${method} ${endpoint} :`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Vide le panier (Action PUBLIQUE : on passe par l'API Guest)
|
||||
export const resetCart = async () => {
|
||||
try {
|
||||
const cart = await apiCall(`${BASE_URL}/api/guest/cart/get`);
|
||||
|
||||
if (cart && cart.items && cart.items.length > 0) {
|
||||
for (const item of cart.items) {
|
||||
await apiCall(`${BASE_URL}/api/guest/cart/remove_item`, { id: item.id });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Nettoyage du panier ignoré :", err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// ROUTES PERSONNALISÉES (CUSTOM API)
|
||||
// ==========================================
|
||||
|
||||
export const registerUnifiedClient = async (email, username, password, firstName, lastName) => {
|
||||
try {
|
||||
// Utilisation des backticks ici aussi !
|
||||
const response = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/signup.php`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
username: username,
|
||||
password: password,
|
||||
first_name: firstName,
|
||||
last_name: lastName
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) throw new Error(data.error.message);
|
||||
return data.result;
|
||||
} catch (err) {
|
||||
console.error("Erreur lors de l'inscription unifiée :", err);
|
||||
throw new Error("Échec de l'inscription. Veuillez réessayer plus tard.");
|
||||
}
|
||||
}
|
||||
|
||||
// #######################################################################################
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES ACCES
|
||||
// ==========================================
|
||||
|
||||
// Connexion
|
||||
export const loginClient = (email, password) =>
|
||||
apiCall(`${BASE_URL}/api/guest/client/login`, { email, password });
|
||||
|
||||
// Deconnexion
|
||||
export const logoutClient = () =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/logout`);
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DU CLIENT
|
||||
// ==========================================
|
||||
|
||||
// Creer un nouveau profil client
|
||||
export const createNewClient = (
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
password,
|
||||
password_confirm,
|
||||
type,
|
||||
custom_1) =>
|
||||
apiCall(`${BASE_URL}/api/guest/client/create`, {
|
||||
email: email,
|
||||
first_name: first_name,
|
||||
password: password,
|
||||
password_confirm: password_confirm,
|
||||
last_name: last_name,
|
||||
type: type,
|
||||
custom_1: custom_1
|
||||
});
|
||||
|
||||
// Recuperer les informations du client
|
||||
export const getClient = () =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/get`);
|
||||
|
||||
// Update les informations du client
|
||||
export const updateClient = (email, last_name, aid, gender, country, city, birthday, company, company_vat, company_number, type, address_1, address_2, postcode, state, phone, phone_cc, document_type, document_nr, notes, lang, custom_1, custom_2, custom_3, custom_4, custom_5, custom_6, custom_7, custom_8, custom_9, custom_10) =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/update`, {
|
||||
email: email,
|
||||
last_name: last_name,
|
||||
aid: aid,
|
||||
gender: gender,
|
||||
country: country,
|
||||
city: city,
|
||||
birthday: birthday,
|
||||
company: company,
|
||||
company_vat: company_vat,
|
||||
company_number: company_number,
|
||||
type: type,
|
||||
address_1: address_1,
|
||||
address_2: address_2,
|
||||
postcode: postcode,
|
||||
state: state,
|
||||
phone: phone,
|
||||
phone_cc: phone_cc,
|
||||
document_type: document_type,
|
||||
document_nr: document_nr,
|
||||
notes: notes,
|
||||
lang: lang,
|
||||
custom_1: custom_1,
|
||||
custom_2: custom_2,
|
||||
custom_3: custom_3,
|
||||
custom_4: custom_4,
|
||||
custom_5: custom_5,
|
||||
custom_6: custom_6,
|
||||
custom_7: custom_7,
|
||||
custom_8: custom_8,
|
||||
custom_9: custom_9,
|
||||
custom_10: custom_10
|
||||
|
||||
});
|
||||
|
||||
// Changer le mot de passe
|
||||
export const changeClientPassword = (current_password, new_password) =>
|
||||
apiCall(`${BASE_URL}/api/client/profile/change_password`, {
|
||||
current_password: current_password,
|
||||
new_password: new_password,
|
||||
confirm_password: new_password
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES ABONNEMENTS
|
||||
// ==========================================
|
||||
|
||||
// Souscription
|
||||
|
||||
// Resiliation
|
||||
// Action : Demande d'annulation (S'occupe de vérifier les 14 jours côté serveur)
|
||||
export const cancelOrder = (orderId) => {
|
||||
return apiCall(`${CUSTOM_API_BASE_URL}/custom_api/cancel_subscription.php`, 'POST', {
|
||||
order_id: orderId
|
||||
});
|
||||
};
|
||||
|
||||
// Suppression
|
||||
export const deleteOrder = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/delete`, { id: order_id });
|
||||
|
||||
// Promotion (Surclassement)
|
||||
// Lister
|
||||
export const getUpgrades = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/upgradables`, { id: order_id });
|
||||
|
||||
// Demotion (Declassement)
|
||||
|
||||
// Lister les abonnements du client
|
||||
export const getOrdersList = () =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get_list`);
|
||||
|
||||
// Lister les produits disponibles
|
||||
export const getProductList = () =>
|
||||
apiCall(`${BASE_URL}/api/guest/product/get_list`);
|
||||
|
||||
// Recuperer les details d'un abonnement
|
||||
export const getOrderDetails = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/service`, { id: order_id });
|
||||
|
||||
// Recuperer les details secrets d'un abonnement
|
||||
export const getServiceDetails = (orderId) =>
|
||||
apiCall(`${BASE_URL}/api/client/order/get`, { id: orderId });
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DU PANIER
|
||||
// ==========================================
|
||||
|
||||
// Ajouter au panier
|
||||
export const addToCart = (productId, period, additionalData = {}) =>
|
||||
apiCall(`${BASE_URL}/api/guest/cart/add_item`, {
|
||||
id: productId,
|
||||
period: period,
|
||||
...additionalData // Les 3 petits points "étalent" le contenu de l'objet
|
||||
});
|
||||
|
||||
// Checkout
|
||||
export const checkoutCart = () =>
|
||||
apiCall(`${BASE_URL}/api/client/cart/checkout`, 'POST');
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES SERVICES D'HEBERGEMENT
|
||||
// ==========================================
|
||||
|
||||
// Modification du username
|
||||
export const updateHostingUsername = (order_id, username) =>
|
||||
apiCall(`${BASE_URL}/api/client/servicehosting/change_username`, {
|
||||
order_id: order_id,
|
||||
username: username
|
||||
});
|
||||
|
||||
// Modification du mot de passe
|
||||
export const updateHostingPassword = (order_id, new_password) =>
|
||||
apiCall(`${BASE_URL}/api/client/servicehosting/change_password`, {
|
||||
order_id: order_id,
|
||||
password: new_password,
|
||||
password_confirm: new_password
|
||||
});
|
||||
|
||||
// Modification du domaine principal
|
||||
export const updateHostingDomain = (order_id, new_domain) =>
|
||||
apiCall(`${BASE_URL}/api/client/servicehosting/change_domain`, {
|
||||
order_id: order_id,
|
||||
domain: new_domain
|
||||
});
|
||||
|
||||
// Login URL (SSO)
|
||||
export const getHostingLoginUrl = (order_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/servicehosting/get_login_url`, {
|
||||
order_id: order_id
|
||||
});
|
||||
|
||||
// Modification du plan d'hébergement
|
||||
export const updateHostingPlan = (order_id, new_plan_id) =>
|
||||
apiCall(`${BASE_URL}/api/admin/servicehosting/change_plan`, {
|
||||
order_id: order_id,
|
||||
plan_id: new_plan_id
|
||||
});
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES FACTURES
|
||||
// ==========================================
|
||||
|
||||
// Creation
|
||||
// Suppression
|
||||
// Modification
|
||||
// Lister
|
||||
export const getInvoiceList = (client_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/invoice/get_list`, { client_id: client_id });
|
||||
|
||||
// Lire
|
||||
export const getInvoiceDetails = (invoiceHash) =>
|
||||
apiCall(`${BASE_URL}/api/client/invoice/get`, { hash: invoiceHash });
|
||||
|
||||
|
||||
// ==========================================
|
||||
// GESTION DES TICKETS
|
||||
// ==========================================
|
||||
|
||||
// Creation
|
||||
export const createTicket = (subject, message, helpdesk_id) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_create`, {
|
||||
support_helpdesk_id: helpdesk_id,
|
||||
subject: subject,
|
||||
content: message
|
||||
});
|
||||
// Suppression
|
||||
|
||||
// Lister les tickets
|
||||
export const getClientTickets = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`);
|
||||
|
||||
// Lister les helpdesks
|
||||
export const getHelpdesks = () =>
|
||||
apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`);
|
||||
|
||||
// Lire
|
||||
export const getTicketDetails = (ticketId) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_get`, { id: ticketId });
|
||||
|
||||
// Repondre
|
||||
export const replyTicket = (ticketId, message) =>
|
||||
apiCall(`${BASE_URL}/api/client/support/ticket_reply`, {
|
||||
id: ticketId,
|
||||
content: message
|
||||
});
|
||||
Reference in New Issue
Block a user