Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 338dccf30b | |||
| 151aca69de | |||
| 9ad44d57b3 | |||
| eb48f506cf | |||
| ef8f4a96b4 |
@@ -0,0 +1,101 @@
|
|||||||
|
### 🛒 Épique 1 : Souscription et Achat (Abonnement Web)
|
||||||
|
|
||||||
|
**US 1.1 : Souscription initiale et information légale**
|
||||||
|
|
||||||
|
> **En tant que** client,
|
||||||
|
> **Je veux** pouvoir choisir un abonnement web et le payer en ligne,
|
||||||
|
> **Afin d'** y accéder immédiatement tout en étant informé de mes droits.
|
||||||
|
|
||||||
|
* **CA 1 :** Le client doit pouvoir sélectionner un plan d'hébergement et un cycle de facturation (ex: 1 mois, 1 an).
|
||||||
|
* **CA 2 :** Dès validation du paiement, le compte HestiaCP est provisionné automatiquement et les accès sont envoyés par email.
|
||||||
|
* **CA 3 :** Le système enregistre l'horodatage exact (date et heure) de l'activation, définissant le "Jour 0".
|
||||||
|
* **CA 4 :** L'email de confirmation de commande **doit explicitement mentionner** le droit de rétractation de 14 jours prévu par le Code de droit économique belge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛑 Épique 2 : Résiliation et Rétractation
|
||||||
|
|
||||||
|
**US 2.1 : Droit de rétractation (Résiliation Jours 0 à 14 inclus)**
|
||||||
|
|
||||||
|
> **En tant que** client dans ma période d'essai légale de 14 jours,
|
||||||
|
> **Je veux** pouvoir annuler mon abonnement d'un simple clic depuis mon espace,
|
||||||
|
> **Afin de** stopper le service immédiatement et d'obtenir un remboursement complet.
|
||||||
|
|
||||||
|
* **CA 1 :** Un bouton "Annuler mon abonnement" est disponible sur le détail du service.
|
||||||
|
* **CA 2 :** **Règle métier :** Si la demande d'annulation intervient avant ou le 14ème jour à 23h59 suivant l'activation, le système affiche un message indiquant que le remboursement sera intégral et immédiat.
|
||||||
|
* **CA 3 :** Dès confirmation du client, le service (FOSSBilling + HestiaCP) est **immédiatement suspendu ou supprimé**.
|
||||||
|
* **CA 4 :** Une facture de remboursement (montant négatif) est générée, et l'ordre de remboursement est envoyé à la passerelle de paiement.
|
||||||
|
|
||||||
|
**US 2.2 : Résiliation standard (Après 14 jours)**
|
||||||
|
|
||||||
|
> **En tant que** client abonné depuis plus de 14 jours,
|
||||||
|
> **Je veux** pouvoir annuler le renouvellement automatique de mon abonnement,
|
||||||
|
> **Afin de** ne pas être facturé à la prochaine échéance.
|
||||||
|
|
||||||
|
* **CA 1 :** **Règle métier :** Si la demande d'annulation intervient après le 14ème jour, aucun remboursement n'est proposé.
|
||||||
|
* **CA 2 :** L'interface affiche clairement : *"Votre abonnement restera actif jusqu'au [Date de fin de période payée] et ne sera pas renouvelé."*
|
||||||
|
* **CA 3 :** Le statut de l'abonnement dans FOSSBilling passe en "Annulé à l'échéance".
|
||||||
|
* **CA 4 :** Le système coupe le service HestiaCP **uniquement à la date d'échéance**, sans action manuelle de l'administrateur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⬆️ Épique 3 : Upgrade (Montée en gamme)
|
||||||
|
|
||||||
|
**US 3.1 : Upgrade d'une offre en cours de cycle**
|
||||||
|
|
||||||
|
> **En tant que** client,
|
||||||
|
> **Je veux** pouvoir passer à une offre supérieure à tout moment,
|
||||||
|
> **Afin de** bénéficier de plus de ressources immédiatement.
|
||||||
|
|
||||||
|
* **CA 1 :** L'interface propose les plans supérieurs disponibles et calcule le prix à payer au *Prorata Temporis* (différence de prix calculée au prorata des jours restants avant le prochain renouvellement).
|
||||||
|
* **CA 2 :** Le changement de ressources techniques sur HestiaCP et la mise à jour des paramètres sur FOSSBilling ont lieu **immédiatement après validation du paiement de l'upgrade**.
|
||||||
|
* **CA 3 :** **Légal :** L'upgrade constituant un nouveau contrat/avenant, si le client se rétracte dans les 14 jours suivant cet upgrade, il est remboursé du prorata payé et redescendu à son offre initiale (ses ressources HestiaCP sont rétrogradées).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⬇️ Épique 4 : Downgrade (Descente en gamme standard)
|
||||||
|
|
||||||
|
**US 4.1 : Demande de Downgrade pour le prochain cycle**
|
||||||
|
|
||||||
|
> **En tant que** client,
|
||||||
|
> **Je veux** pouvoir demander le passage à une offre inférieure,
|
||||||
|
> **Afin de** réduire mes coûts lors de mon prochain renouvellement.
|
||||||
|
|
||||||
|
* **CA 1 :** L'interface permet de sélectionner un plan inférieur.
|
||||||
|
* **CA 2 :** Un message d'avertissement clair doit s'afficher : *"Votre demande est enregistrée. Le changement d'offre (ressources et tarif) prendra effet à votre prochaine date de renouvellement, le [Date]. Aucun remboursement partiel ne sera effectué pour la période en cours."*
|
||||||
|
* **CA 3 :** Le système enregistre l'intention de downgrade (Modification du `product_id` et du tarif pour la prochaine facture, mais maintien du package actuel sur HestiaCP jusqu'à la date d'échéance).
|
||||||
|
* **CA 4 :** À la date anniversaire, un script Cron effectue la bascule effective des ressources sur HestiaCP et génère la facture au nouveau tarif.
|
||||||
|
|
||||||
|
|
||||||
|
### 🟢 Phase 1 : Le "Quick Win" légal (Épique 1)
|
||||||
|
|
||||||
|
C'est le plus facile et on peut le régler en 5 minutes directement dans l'interface de FOSSBilling, sans toucher au code.
|
||||||
|
|
||||||
|
* **L'objectif :** Modifier les templates d'emails automatiques envoyés lors d'une commande pour y inclure la mention légale obligatoire du droit de rétractation de 14 jours.
|
||||||
|
* **L'action :** Éditer le template `mod_email_order_activated` dans FOSSBilling.
|
||||||
|
|
||||||
|
### 🟠 Phase 2 : Le cœur du réacteur - Les 14 jours (Épique 2)
|
||||||
|
|
||||||
|
C'est ici qu'on va faire du vrai développement PHP. Par défaut, quand un client annule sur FOSSBilling, ça coupe à la fin du mois sans rembourser. On doit créer un "intercepteur" (un Hook).
|
||||||
|
|
||||||
|
* **L'objectif :** Quand le client clique sur "Annuler", le système calcule la différence entre la date d'aujourd'hui et la date de la commande.
|
||||||
|
* **La logique :**
|
||||||
|
* Si `<= 14 jours` : On déclenche l'API `refund` de FOSSBilling, on suspend HestiaCP immédiatement, et on passe la commande en annulée.
|
||||||
|
* Si `> 14 jours` : On laisse FOSSBilling faire son comportement par défaut (Annulation à la prochaine date d'échéance).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 🔵 Phase 3 : L'adaptation du Downgrade (Épique 4)
|
||||||
|
|
||||||
|
Tu te souviens de notre script de Downgrade instantané qu'on avait fait il y a quelques jours ? Puisque tu as choisi le modèle "Standard SaaS" (le downgrade ne s'applique qu'à la prochaine facture), on va devoir le modifier !
|
||||||
|
|
||||||
|
* **L'objectif :** Le clic sur "Downgrade" ne doit plus modifier HestiaCP immédiatement. Il doit juste préparer la prochaine facture dans FOSSBilling, et c'est un Cron (une tâche planifiée) qui réduira les ressources HestiaCP le jour du renouvellement.
|
||||||
|
|
||||||
|
### 🟣 Phase 4 : L'Upgrade (Épique 3)
|
||||||
|
|
||||||
|
FOSSBilling gère plutôt bien les upgrades nativement (avec calcul du prorata). On devra juste s'assurer que l'appel API vers HestiaCP se fait correctement dès que la facture de prorata est payée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Comment on attaque ?**
|
||||||
|
Est-ce qu'on se met en chauffe avec la **Phase 1 (Les templates d'emails)**, ou est-ce qu'on plonge directement dans le code PHP avec la **Phase 2 (La logique de rétractation des 14 jours)** ?
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NEXUS by gise | Portail d'Infrastructure Centralisée</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+50
-109
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { CreditCard, Check, Globe, Loader, ToggleLeft, ShieldAlert, Calendar, TrendingUp, TrendingDown, RefreshCcw } from 'lucide-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 || '';
|
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 [loading, setLoading] = useState(false);
|
||||||
const [cancelLoading, setCancelLoading] = useState(false);
|
const [cancelLoading, setCancelLoading] = useState(false);
|
||||||
const [catalog, setCatalog] = useState([]);
|
const [catalog, setCatalog] = useState([]);
|
||||||
@@ -15,15 +15,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
|
|
||||||
const [billingPeriod, setBillingPeriod] = useState(order.period || '1M');
|
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)
|
// 1. Récupération des détails techniques (Domaine/IP)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDetails = async () => {
|
const fetchDetails = async () => {
|
||||||
setIsDetailsLoading(true);
|
setIsDetailsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getServiceDetails(order.id);
|
const data = await getHostingServiceDetails(order.id);
|
||||||
setServiceDetails(data);
|
setServiceDetails(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur détails service:", err);
|
console.error("Erreur détails service:", err);
|
||||||
@@ -42,8 +39,8 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
|
|
||||||
// 2. Extraction robuste du domaine
|
// 2. Extraction robuste du domaine
|
||||||
let extractedDomain = "Aucun domaine lié";
|
let extractedDomain = "Aucun domaine lié";
|
||||||
if (serviceDetails?.config?.hostname || serviceDetails?.config?.sld && serviceDetails?.config?.tld) {
|
if (serviceDetails?.domain || serviceDetails?.config?.domain) {
|
||||||
extractedDomain = serviceDetails.config.hostname || serviceDetails.config.sld+serviceDetails.config.tld;
|
extractedDomain = serviceDetails.domain || serviceDetails.config.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMarkdownFeatures = (text) => {
|
const renderMarkdownFeatures = (text) => {
|
||||||
@@ -177,7 +174,12 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
|
|
||||||
isRefund = finalInvoicePrice < -0.01;
|
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);
|
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 baseCurrentPrice = currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0;
|
||||||
const baseSelectedPrice = selectedPlan?.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;
|
const isSamePeriod = currentBillingPeriod === billingPeriod;
|
||||||
|
|
||||||
if (isSamePlan && !isSamePeriod) {
|
if (isSamePlan && !isSamePeriod) {
|
||||||
|
// Le pack est identique, seule la période change
|
||||||
migrationLabel = "Changer de cycle";
|
migrationLabel = "Changer de cycle";
|
||||||
buttonColor = "bg-blue-500 hover:bg-blue-400 text-black shadow-[0_0_20px_rgba(59,130,246,0.2)]";
|
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" />;
|
ActionIcon = <RefreshCcw className="w-4 h-4" />;
|
||||||
} else if (!isSamePlan) {
|
} else if (!isSamePlan) {
|
||||||
|
// Le pack change, on compare la hiérarchie absolue
|
||||||
if (parseFloat(baseSelectedPrice) > parseFloat(baseCurrentPrice)) {
|
if (parseFloat(baseSelectedPrice) > parseFloat(baseCurrentPrice)) {
|
||||||
migrationLabel = `Valider l'Upgrade`;
|
migrationLabel = `Valider l'Upgrade`;
|
||||||
buttonColor = "bg-emerald-500 hover:bg-emerald-400 text-black shadow-[0_0_20px_rgba(16,185,129,0.2)]";
|
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 () => {
|
const handleMigration = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
// 🎯 On détermine dynamiquement le type d'action pour la facture
|
||||||
let actionType = 'upgrade';
|
let actionType = 'upgrade';
|
||||||
const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId);
|
const currentPlanFromCatalog = catalog.find(p => p.id === currentPlanId);
|
||||||
const baseCurrent = parseFloat(currentPlanFromCatalog?.pricing?.recurrent?.['1M']?.price || 0);
|
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,
|
target_plan_id: selectedPlan.id,
|
||||||
new_period: billingPeriod,
|
new_period: billingPeriod,
|
||||||
new_price: finalInvoicePrice.toFixed(2),
|
new_price: finalInvoicePrice.toFixed(2),
|
||||||
action_type: actionType
|
action_type: actionType // 🎯 On envoie l'information au backend !
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
@@ -242,55 +249,51 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
const handleCancel = async () => {
|
||||||
// 🛡️ NOUVELLE LOGIQUE DE RÉSILIATION (US 2.1 / 2.2)
|
if (!window.confirm("Êtes-vous sûr de vouloir résilier cet abonnement ? Le service sera désactivé au prochain cycle de facturation.")) {
|
||||||
// ==========================================
|
try {
|
||||||
const calculateRefundEligibility = () => {
|
const response = await cancelSubscription(user.id, order.id);
|
||||||
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 isEligibleForRefund = calculateRefundEligibility();
|
// Le message s'adapte tout seul !
|
||||||
|
// Soit: "Annulé avec effet immédiat et remboursé"
|
||||||
|
// Soit: "Actif jusqu'à la fin de la période"
|
||||||
|
alert(response.message);
|
||||||
|
|
||||||
|
// Rafraîchir la liste des abonnements...
|
||||||
|
} catch (error) {
|
||||||
|
alert("Erreur lors de l'annulation : " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleConfirmCancel = async () => {
|
|
||||||
setCancelLoading(true);
|
setCancelLoading(true);
|
||||||
try {
|
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 (data.status === 'success') {
|
||||||
if (onAlert) onAlert("Résilié", data.message, "success");
|
if (onAlert) onAlert("Résilié", data.message || "L'abonnement a été annulé avec succès.", "success");
|
||||||
setIsCancelModalOpen(false);
|
|
||||||
if (onRefresh) onRefresh();
|
if (onRefresh) onRefresh();
|
||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
} else {
|
} else {
|
||||||
if (onAlert) onAlert("Erreur", data.error, "error");
|
if (onAlert) onAlert("Erreur", data.error || "Impossible de résilier l'abonnement.", "error");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur API Annulation:", err);
|
if (onAlert) onAlert("Erreur", "Problème réseau ou serveur.", "error");
|
||||||
|
|
||||||
// 🛡️ 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");
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setCancelLoading(false);
|
setCancelLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="shrink-0 space-y-4 mb-4 pr-2">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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">
|
<div className="bg-[#090f1c] border border-gray-800 p-3.5 rounded-xl flex items-center gap-3">
|
||||||
@@ -324,7 +327,10 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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
|
Voir les factures
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,6 +440,7 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 mt-auto pt-4 border-t border-gray-800/40 space-y-3">
|
<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 && (
|
{selectedPlan && !isNoChange && (
|
||||||
<div className={`border p-3 rounded-xl mb-3 flex items-center justify-between transition-colors
|
<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'}`}
|
${isRefund ? 'bg-amber-950/20 border-amber-900/50' : 'bg-[#0e2238] border-cyan-900/50'}`}
|
||||||
@@ -477,9 +484,8 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 🎯 NOUVEAU BOUTON : Ouvre la modale au lieu de faire un confirm() */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCancelModalOpen(true)}
|
onClick={handleCancel}
|
||||||
disabled={loading || cancelLoading}
|
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"
|
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 +493,6 @@ export default function WebServiceSubscriptionManager({ order, onClose, onRefres
|
|||||||
{cancelLoading ? 'Résiliation en cours...' : 'Résilier l\'abonnement réseau'}
|
{cancelLoading ? 'Résiliation en cours...' : 'Résilier l\'abonnement réseau'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,55 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Server, Database, Cloud, Globe, ExternalLink, Play, Loader } from 'lucide-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 }) {
|
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 titleLower = (service.title || '').toLowerCase();
|
||||||
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
const isCloud = titleLower.includes('cloud') || titleLower.includes('nextcloud');
|
||||||
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
const isDB = titleLower.includes('db') || titleLower.includes('base') || titleLower.includes('sql');
|
||||||
const isWeb = !isVPS && !isCloud && !isDB;
|
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 buttonText = "CONSOLE D'ADMINISTRATION";
|
||||||
let ButtonIcon = ExternalLink;
|
let ButtonIcon = ExternalLink;
|
||||||
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
|
let buttonStyle = "bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border-cyan-400";
|
||||||
|
|
||||||
if (isVPS) {
|
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 { 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 (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"; }
|
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" />;
|
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 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 (
|
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">
|
<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>
|
<div className="p-2 bg-black/40 rounded-lg">{getServiceIcon()}</div>
|
||||||
{service.status === 'active' ? (
|
{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">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<h3 className="font-bold text-white text-lg truncate" title={baseTitle}>{shortTitle}</h3>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto space-y-3">
|
<div className="mt-auto space-y-3">
|
||||||
<div className="flex items-center justify-between text-sm border-t border-gray-800 pt-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>
|
<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>
|
<option value="free">-- Libre --</option>
|
||||||
{vpcs.map(vpc => ( <option key={vpc.id} value={vpc.id}>{vpc.name}</option> ))}
|
{vpcs.map(vpc => ( <option key={vpc.id} value={vpc.id}>{vpc.name}</option> ))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 }) {
|
export default function CreateTicketModal({ isOpen, onClose, onSubmit, helpdesks, defaultHelpdesk, isSubmitting }) {
|
||||||
const [subject, setSubject] = useState('');
|
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">
|
<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
|
<Plus className="w-5 h-5 text-cyan-400" /> OUVRIR UNE REQUÊTE
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
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 }) {
|
export default function TicketThreadModal({ ticket, onClose, onReply, isLoadingConversation }) {
|
||||||
const [replyMessage, setReplyMessage] = useState('');
|
const [replyMessage, setReplyMessage] = useState('');
|
||||||
@@ -32,7 +32,9 @@ export default function TicketThreadModal({ ticket, onClose, onReply, isLoadingC
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-white line-clamp-1">{ticket.subject}</h3>
|
<h3 className="text-xl font-bold text-white line-clamp-1">{ticket.subject}</h3>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-6 scrollbar-thin scrollbar-thumb-gray-800">
|
<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 { useState, useEffect } from 'react';
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||||
import { getClientTickets } from '../services/api';
|
import { getClientTickets } from '../services/api';
|
||||||
|
import { LayoutDashboard, Globe, Settings, FileText } from 'lucide-react';
|
||||||
import ConfirmLogoutModal from '../components/ui/ConfirmLogoutModal'; // Le modal extrait !
|
import ConfirmLogoutModal from '../components/ui/ConfirmLogoutModal'; // Le modal extrait !
|
||||||
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
@@ -31,8 +32,7 @@ export default function AppLayout() {
|
|||||||
|
|
||||||
// 🌟 La fonction magique Tailwind pour les liens du menu
|
// 🌟 La fonction magique Tailwind pour les liens du menu
|
||||||
const navLinkClass = ({ isActive }) =>
|
const navLinkClass = ({ isActive }) =>
|
||||||
`block px-6 py-4 no-underline border-l-4 transition-all uppercase tracking-widest text-sm font-mono ${
|
`block px-6 py-4 no-underline border-l-4 transition-all uppercase tracking-widest text-sm font-mono ${isActive
|
||||||
isActive
|
|
||||||
? 'text-cyan-400 bg-cyan-400/5 border-cyan-400'
|
? 'text-cyan-400 bg-cyan-400/5 border-cyan-400'
|
||||||
: 'text-[#888] border-transparent hover:text-gray-300 hover:bg-white/5'
|
: 'text-[#888] border-transparent hover:text-gray-300 hover:bg-white/5'
|
||||||
}`;
|
}`;
|
||||||
@@ -75,6 +75,7 @@ export default function AppLayout() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink className={navLinkClass} to="/facturation">Facturation</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">
|
<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>Profil & Sécurité</span>
|
||||||
<span className="text-[9px] text-[#333] border border-[#333] px-1.5 py-0.5 rounded font-bold">WIP</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 { useState, useEffect } from 'react';
|
||||||
import { FileText, Download, CreditCard, Clock, CheckCircle2, XCircle, AlertCircle, Loader, X, Receipt, Trash2, ArrowRight, RefreshCcw } from 'lucide-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 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() {
|
export default function BillingHistory() {
|
||||||
const [invoices, setInvoices] = useState([]);
|
const [invoices, setInvoices] = useState([]);
|
||||||
@@ -21,12 +21,12 @@ export default function BillingHistory() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const profileData = await getClient();
|
const profileData = await getClientProfile();
|
||||||
if (!profileData || !profileData.id) {
|
if (!profileData || !profileData.id) {
|
||||||
throw new Error("Impossible de vérifier votre identité ou session expirée.");
|
throw new Error("Impossible de vérifier votre identité ou session expirée.");
|
||||||
}
|
}
|
||||||
const invoicesData = await getInvoiceList(profileData.id);
|
const invoicesData = await getInvoicesHistory(profileData.id);
|
||||||
setInvoices(invoicesData.list || []);
|
setInvoices(invoicesData.data || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || "Problème de connexion avec le serveur.");
|
setError(err.message || "Problème de connexion avec le serveur.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -212,7 +212,9 @@ export default function BillingHistory() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{(() => {
|
{(() => {
|
||||||
// FILTRE : On ne garde que les items dont le prix est strictement supérieur à 0
|
// 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) {
|
if (validItems.length > 0) {
|
||||||
return validItems.map((item, idx) => (
|
return validItems.map((item, idx) => (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
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() {
|
export default function Checkout() {
|
||||||
const { productId } = useParams();
|
const { productId } = useParams();
|
||||||
@@ -18,7 +18,7 @@ export default function Checkout() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. On charge d'abord le produit
|
// 1. On charge d'abord le produit (Requête Publique)
|
||||||
const productData = await getProductList();
|
const productData = await getProductList();
|
||||||
const foundProduct = productData.list?.find(p => p.id === parseInt(productId));
|
const foundProduct = productData.list?.find(p => p.id === parseInt(productId));
|
||||||
|
|
||||||
@@ -29,14 +29,15 @@ export default function Checkout() {
|
|||||||
}
|
}
|
||||||
setProduct(foundProduct);
|
setProduct(foundProduct);
|
||||||
|
|
||||||
// 2. Ensuite, on tente de charger le profil
|
// 2. Ensuite, on tente de charger le profil (Requête Privée)
|
||||||
try {
|
try {
|
||||||
const profileData = await getClient();
|
const profileData = await getClientProfile();
|
||||||
setUserProfile(profileData);
|
setUserProfile(profileData);
|
||||||
} catch (profileErr) {
|
} catch (profileErr) {
|
||||||
// Si on tombe ici, c'est que FOSSBilling refuse l'accès au profil.
|
// Si on tombe ici, c'est que FOSSBilling refuse l'accès au profil.
|
||||||
console.error("Rejet API Profil :", profileErr);
|
console.error("Rejet API Profil :", profileErr);
|
||||||
setError("Accès refusé. Vous devez être connecté à votre compte pour provisionner une instance.");
|
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');
|
// navigate('/login');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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';
|
import { AlertCircle, Loader, FileText, X } from 'lucide-react';
|
||||||
|
|
||||||
// Importation des composants
|
// Importation des composants
|
||||||
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
import DashboardServiceCard from '../../components/dashboard/DashboardServiceCard';
|
||||||
import NewServiceCard from '../../components/dashboard/NewServiceCard';
|
import NewServiceCard from '../../components/dashboard/NewServiceCard';
|
||||||
import NotificationModal from '../../components/ui/NotificationModal';
|
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() {
|
export default function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -20,12 +20,10 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
|
const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
|
||||||
|
|
||||||
const fetchInventory = async (retryCount = 0) => {
|
const fetchInventory = async () => {
|
||||||
// On n'affiche le loader global que lors du tout premier essai
|
setIsLoading(true);
|
||||||
if (retryCount === 0) setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getOrdersList();
|
const data = await getClientOrders();
|
||||||
|
|
||||||
if (data.list) {
|
if (data.list) {
|
||||||
// LE FILTRE CHIRURGICAL PAR PREFIXE
|
// LE FILTRE CHIRURGICAL PAR PREFIXE
|
||||||
@@ -39,29 +37,18 @@ export default function Dashboard() {
|
|||||||
title.startsWith('domaine ') ||
|
title.startsWith('domaine ') ||
|
||||||
title.startsWith('enregistrement ');
|
title.startsWith('enregistrement ');
|
||||||
|
|
||||||
return (order.status === 'active' || order.status === 'pending_setup') && !isGhostProduct;
|
return !isGhostProduct;
|
||||||
});
|
});
|
||||||
|
|
||||||
setOrders(filteredOrders);
|
setOrders(filteredOrders);
|
||||||
} else {
|
} else {
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(null); // On efface l'erreur s'il y en avait une
|
|
||||||
setIsLoading(false); // Le chargement est terminé
|
|
||||||
|
|
||||||
} catch (err) {
|
} 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.");
|
setError(err.message || "Impossible de récupérer la télémétrie des services.");
|
||||||
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,7 +57,6 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
|
||||||
const handleManageSubscription = (order) => {
|
const handleManageSubscription = (order) => {
|
||||||
console.log("Gestion de l'abonnement pour la commande :", order);
|
|
||||||
// 1. Détection du type de service
|
// 1. Détection du type de service
|
||||||
const titleLower = (order.title || '').toLowerCase();
|
const titleLower = (order.title || '').toLowerCase();
|
||||||
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
const isVPS = titleLower.includes('vps') || titleLower.includes('compute');
|
||||||
@@ -142,7 +128,7 @@ export default function Dashboard() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WebServiceSubscriptionManager
|
<SubscriptionManager
|
||||||
order={activeSubscriptionModal}
|
order={activeSubscriptionModal}
|
||||||
onClose={() => setActiveSubscriptionModal(null)}
|
onClose={() => setActiveSubscriptionModal(null)}
|
||||||
onRefresh={fetchInventory}
|
onRefresh={fetchInventory}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
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 { 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 !
|
// Importation de tes nouveaux composants modulaires !
|
||||||
import NotificationModal from '../../components/ui/NotificationModal';
|
import NotificationModal from '../../components/ui/NotificationModal';
|
||||||
@@ -25,12 +25,12 @@ export default function Services() {
|
|||||||
|
|
||||||
const fetchServices = useCallback(async () => {
|
const fetchServices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getOrdersList();
|
const data = await getMyServices();
|
||||||
if (data.list && data.list.length > 0) {
|
if (data.list && data.list.length > 0) {
|
||||||
const detailedServices = await Promise.all(data.list.map(async (order) => {
|
const detailedServices = await Promise.all(data.list.map(async (order) => {
|
||||||
let hDetails = null;
|
let hDetails = null;
|
||||||
if (order.status === 'active' && (order.plugin === 'hosting' || (order.title || '').toLowerCase().includes('vps'))) {
|
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 };
|
return { ...order, hostingDetails: hDetails };
|
||||||
}));
|
}));
|
||||||
@@ -65,7 +65,7 @@ export default function Services() {
|
|||||||
} else if (isDB) {
|
} else if (isDB) {
|
||||||
window.open('https://pma.gise.be/', '_blank');
|
window.open('https://pma.gise.be/', '_blank');
|
||||||
} else if (isVPS || isWeb) {
|
} 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 });
|
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
|
||||||
}
|
}
|
||||||
} catch (err) { triggerAlert("Erreur", err.message, "error"); }
|
} 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">
|
<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</>}
|
{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>
|
</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>
|
</div>
|
||||||
{activeServiceModal.type === 'vps' ? (
|
{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} />
|
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
|
// src/pages/app/Store.jsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Server, Database, Cloud, Globe, Loader, AlertCircle } from 'lucide-react';
|
import { Server, Database, Cloud, Globe, Loader, AlertCircle } from 'lucide-react';
|
||||||
import { getProductList } from '../../services/billing_api';
|
import { getProductList } from '../../services/api';
|
||||||
import CategorySection from '../../components/store/CategorySection';
|
import CategorySection from '../../components/store/CategorySection'; // L'import magique
|
||||||
|
|
||||||
export default function Store() {
|
export default function Store() {
|
||||||
const [groupedProducts, setGroupedProducts] = useState({});
|
const [groupedProducts, setGroupedProducts] = useState({});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { loginClient } from '../../services/billing_api';
|
import { loginClient } from '../../services/api';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { createNewClient } from '../../services/billing_api';
|
import { registerUnifiedClient } from '../../services/api';
|
||||||
import NotificationModal from '../../components/ui/NotificationModal';
|
import NotificationModal from '../../components/ui/NotificationModal';
|
||||||
|
|
||||||
export default function Register() {
|
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." });
|
setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès ne correspondent pas." });
|
||||||
return;
|
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)." });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await createNewClient(email, firstName, lastName, password, confirmPassword, "individual", username);
|
await registerUnifiedClient(email, username, password, firstName, lastName);
|
||||||
setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Votre compte a été initialisé.\n\nVous pouvez maintenant vous connecter." });
|
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) {
|
} catch (err) {
|
||||||
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." });
|
setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -56,7 +56,7 @@ export default function Register() {
|
|||||||
Créer un accès réseau
|
Créer un accès réseau
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#888] font-mono text-sm mb-6">
|
<p className="text-[#888] font-mono text-sm mb-6">
|
||||||
[ INITIALISATION DU PROVISIONNEMENT ]
|
[ INITIALISATION DU PROVISIONNEMENT TRIPLE ]
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleRegister} className="font-mono">
|
<form onSubmit={handleRegister} className="font-mono">
|
||||||
|
|||||||
+83
-185
@@ -1,7 +1,7 @@
|
|||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_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) => {
|
const apiCall = async (endpoint, param2 = 'GET', param3 = null) => {
|
||||||
let method = 'GET';
|
let method = 'GET';
|
||||||
let body = null;
|
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)
|
// Vide le panier (Action PUBLIQUE : on passe par l'API Guest)
|
||||||
export const resetCart = async () => {
|
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)
|
// 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)
|
// Force la réinitialisation du mot de passe sur le serveur distant (HestiaCP)
|
||||||
export const resetHostingPassword = (orderId, newPassword) =>
|
export const resetHostingPassword = (orderId, newPassword) =>
|
||||||
apiCall(`${BASE_URL}/api/client/servicehosting/change_password`, 'POST', {
|
apiCall(`${BASE_URL}/api/client/servicehosting/change_password`, 'POST', {
|
||||||
@@ -145,204 +191,56 @@ export const launchSSOGateway = (username, password) => {
|
|||||||
document.body.removeChild(form);
|
document.body.removeChild(form);
|
||||||
};
|
};
|
||||||
|
|
||||||
// #######################################################################################
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GESTION DES ACCES
|
// ROUTES SUPPORT FOSSBILLING (NATIVES)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// Connexion
|
// Récupère la liste de tous les tickets du client
|
||||||
export const loginClient = (email, password) =>
|
export const getClientTickets = () =>
|
||||||
apiCall(`${BASE_URL}/api/guest/client/login`, { email, password });
|
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`, 'GET');
|
||||||
|
|
||||||
// Deconnexion
|
// Récupère le contenu et les messages d'un ticket spécifique
|
||||||
export const logoutClient = () =>
|
export const getTicketDetails = (ticketId) =>
|
||||||
apiCall(`${BASE_URL}/api/client/profile/logout`);
|
apiCall(`${BASE_URL}/api/client/support/ticket_get`, 'POST', { id: ticketId });
|
||||||
|
|
||||||
|
// Crée un nouveau ticket de support
|
||||||
// ==========================================
|
|
||||||
// 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
|
|
||||||
export const createTicket = (subject, message, helpdesk_id) =>
|
export const createTicket = (subject, message, helpdesk_id) =>
|
||||||
apiCall(`${BASE_URL}/api/client/support/ticket_create`, 'POST', {
|
apiCall(`${BASE_URL}/api/client/support/ticket_create`, 'POST', {
|
||||||
support_helpdesk_id: helpdesk_id,
|
support_helpdesk_id: helpdesk_id,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
content: message
|
content: message
|
||||||
});
|
});
|
||||||
// Suppression
|
|
||||||
|
|
||||||
// Lister les tickets
|
// Répond à un ticket existant
|
||||||
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
|
|
||||||
export const replyTicket = (ticketId, message) =>
|
export const replyTicket = (ticketId, message) =>
|
||||||
apiCall(`${BASE_URL}/api/client/support/ticket_reply`, 'POST', {
|
apiCall(`${BASE_URL}/api/client/support/ticket_reply`, 'POST', {
|
||||||
id: ticketId,
|
id: ticketId,
|
||||||
content: message
|
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 });
|
||||||
|
|
||||||
|
// Annule un abonnement (Gère automatiquement la règle des 14 jours côté serveur)
|
||||||
|
export const cancelSubscription = (clientId, orderId) =>
|
||||||
|
apiCall(`${CUSTOM_API_BASE_URL}/custom_api/nexus_subscription.php`, 'POST', {
|
||||||
|
action: 'cancel_order',
|
||||||
|
client_id: clientId,
|
||||||
|
order_id: orderId
|
||||||
|
});
|
||||||
@@ -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