@@ -94,50 +75,17 @@ export default function Dashboard() {
{!isLoading && !error && (
- {/* Boucle sur les services FOSSBilling */}
+ {/* Boucle sur les composants isolés */}
{orders.map((order) => (
-
navigate(`/services/${order.id}`)} // Redirection future vers les détails
- >
-
-
-
- {getServiceIcon(order.title)}
-
- {getStatusBadge(order.status)}
-
-
- {order.title}
-
-
- Facturation : {order.period}
-
-
-
-
- ID Réseau: #{order.id}
- Gérer >
-
-
+ order={order}
+ onClick={() => navigate(`/services/${order.id}`)}
+ />
))}
- {/* LA CARTE : OBTENIR UN NOUVEAU PRODUIT */}
-
navigate('/store')} // Remplace /store par l'URL de ton catalogue
- className="bg-transparent border-2 border-dashed border-gray-700 hover:border-cyan-400 p-6 rounded-xl transition-colors cursor-pointer flex flex-col items-center justify-center text-center group min-h-[200px]"
- >
-
-
- Demander une accréditation
-
-
- Déployer un nouveau serveur Web, VPS ou Cloud.
-
-
+ {/* Le composant carte d'ajout */}
+
navigate('/store')} />
)}
diff --git a/src/pages/app/Services.jsx b/src/pages/app/Services.jsx
index 0ee409f..75ab49b 100644
--- a/src/pages/app/Services.jsx
+++ b/src/pages/app/Services.jsx
@@ -1,496 +1,39 @@
import { useState, useEffect, useCallback } from 'react';
-import { getMyServices, getServiceDetails, getHostingServiceDetails, resetHostingPassword } from '../../services/api';
+import { getMyServices, getHostingServiceDetails } from '../../services/api';
import { useVPC } from '../../services/useVPC';
-import { Server, Database, Cloud, Globe, Folder, Trash2, ExternalLink, Loader, Plus, Play, Lock, Maximize2, AlertCircle } from 'lucide-react'; // Ajout de AlertCircle
+import { Server, Globe, Folder, Trash2, Loader, Plus, AlertCircle } from 'lucide-react';
-// ============================================================================
-// COMPOSANT 0 : MODAL DE NOTIFICATION (Remplace les alert() natifs)
-// ============================================================================
-const NotificationModal = ({ notification, onClose }) => {
- if (!notification) return null;
- const isError = notification.type === 'error';
- return (
-
-
-
- {notification.title.toUpperCase()}
-
-
{notification.message}
-
-
-
- );
-};
+// Importation de tes nouveaux composants modulaires !
+import NotificationModal from '../../components/ui/NotificationModal';
+import InstanceCard from '../../components/services/InstanceCard';
+import VpsDeployer from '../../components/services/VpsDeployer';
+import VpsManager from '../../components/services/VpsManager';
+import WebManager from '../../components/services/WebManager';
-// ============================================================================
-// COMPOSANT 1 : INSTANCE CARD (Mode PaaS Pur - IP Masquée)
-// ============================================================================
-const InstanceCard = ({ service, vpcs, onAssignVpc, onRemoveVpc, onOpenConsole, isConnecting }) => {
- 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 !== '';
-
- 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;
- } 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";
- } else if (isDB) {
- buttonText = "PHPMYADMIN / CLUSTER";
- ButtonIcon = Database;
- buttonStyle = "bg-purple-400/10 hover:bg-purple-400 text-purple-400 hover:text-gray-900 border-purple-400";
- }
-
- const getServiceIcon = () => {
- if (isVPS) return
;
- if (isCloud) return
;
- if (isDB) return
;
- return
;
- };
-
- const baseTitle = (service.title || '').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 (
-
-
-
-
- {getServiceIcon()}
-
- {service.status === 'active' ? (
-
- {isVPS && !hasIP ? 'AWAITING INIT' : 'ONLINE'}
-
- ) : (
-
DEPLOYING
- )}
-
-
-
- {shortTitle}
-
-
-
- {displayDomain ? (
-
- {displayDomain}
-
- ) : (
-
En attente de déploiement
- )}
-
-
-
-
-
- Projet VPC:
-
-
-
-
-
-
- );
-};
-
-// ============================================================================
-// COMPOSANT 2 : VPS DEPLOYER (Instanciation)
-// ============================================================================
-const VpsDeployer = ({ serviceId, onDeploySuccess, onAlert }) => {
- const [domain, setDomain] = useState('');
- const [password, setPassword] = useState('');
- const [isDeploying, setIsDeploying] = useState(false);
-
- const handleDeploy = async () => {
- if (password.length < 8) { onAlert("Sécurité", "Le mot de passe doit faire au moins 8 caractères.", "error"); return; }
-
- setIsDeploying(true);
- try {
- const response = await fetch('https://web.gise.be/custom_api/proxmox_create_vps.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ service_id: serviceId, domain: domain, password: password })
- });
- const data = await response.json();
-
- if (data.status === 'success') {
- onDeploySuccess({ ip: data.ip, domain: data.domain, password: password });
- } else {
- onAlert("Échec", "Erreur de provisionnement : " + (data.message || 'Inconnue'), "error");
- }
- } catch (err) {
- onAlert("Erreur Réseau", "Erreur de communication avec l'API Proxmox Gateway.", "error");
- } finally {
- setIsDeploying(false);
- }
- };
-
- return (
-
-
- PHASE D'INITIALISATION REQUISE
- La facturation est activée. Veuillez définir un mot de passe Root pour lancer la création de l'instance sur l'hyperviseur.
-
-
-
-
-
- );
-};
-
-// ============================================================================
-// COMPOSANT 3 : VPS MANAGER (Day-2 Operations)
-// ============================================================================
-const VpsManager = ({ details, orderId, onRefresh, onAlert }) => {
- const [showTerminal, setShowTerminal] = useState(false);
- const [isEditingDomain, setIsEditingDomain] = useState(false);
- const [newDomain, setNewDomain] = useState(details.domain);
- const [isUpdating, setIsUpdating] = useState(false);
- const [sslStatus, setSslStatus] = useState(null);
-
- const isInternal = details.domain && details.domain.endsWith('.gise.be');
- const isCustomDomain = details.domain && !isInternal;
-
- const fileManagerUrl = isInternal ? `https://file-${details.domain}` : `http://${details.domain}:8080`;
- const terminalUrl = isInternal ? `https://terminal-${details.domain}` : `http://${details.domain}:8081`;
-
- const handleUpdateDomain = async () => {
- if (!newDomain || newDomain === details.domain) return;
- setIsUpdating(true);
- try {
- const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ order_id: orderId, new_domain: newDomain })
- });
- const data = await response.json();
- if (data.status === 'success') {
- setIsEditingDomain(false);
- if (onRefresh) onRefresh();
- onAlert("Succès", "Domaine VPS mis à jour avec succès sur l'infrastructure !", "success");
- } else onAlert("Erreur", data.error, "error");
- } catch (err) {
- onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
- } finally { setIsUpdating(false); }
- };
-
- const handleGenerateSSL = async () => {
- setSslStatus('loading');
- try {
- const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ domain: details.domain })
- });
- const data = await response.json();
- if (data.status === 'success') {
- setSslStatus('success');
- onAlert("SSL Activé", "Certificat SSL Let's Encrypt généré et activé avec succès !", "success");
- } else {
- setSslStatus('error');
- onAlert("Challenge DNS Échoué", "Vérifiez que votre domaine pointe bien vers notre IP publique.", "error");
- }
- } catch (e) { setSslStatus('error'); }
- };
-
- return (
-
- {!showTerminal ? (
- <>
-
-
- {!isEditingDomain ? (
-
-
-
- {details.domain}
-
-
-
- ) : (
-
- setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-cyan-500 outline-none" disabled={isUpdating} />
-
-
-
- )}
- {isCustomDomain && (
-
-
Domaine externe détecté. Sécurisez après pointage DNS.
-
-
- )}
-
-
-
Identifiants d'usine :
-
Console SSH : root / Clé choisie à l'initialisation
-
Explorateur : root / NexusGise2026! (à changer)
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
-
-
NEXUS TERMINAL ({details.domain})
-
-
-
-
-
-
-
-
-
- )}
-
- );
-};
-
-// ============================================================================
-// COMPOSANT 4 : WEB MANAGER (HestiaCP & Day-2 Operations)
-// ============================================================================
-const WebManager = ({ details, orderId, onRefresh, onOpenSso, onAlert }) => {
- const [isEditingDomain, setIsEditingDomain] = useState(false);
- const [newDomain, setNewDomain] = useState(details.domain);
- const [isUpdating, setIsUpdating] = useState(false);
- const [sslStatus, setSslStatus] = useState(null);
- const [isGeneratingSso, setIsGeneratingSso] = useState(false);
-
- const isCustomDomain = details.domain && !details.domain.endsWith('.gise.be');
- const siteUrl = (isCustomDomain && sslStatus !== 'success') ? `http://${details.domain}` : `https://${details.domain}`;
-
- const handleUpdateDomain = async () => {
- if (!newDomain || newDomain === details.domain) return;
- setIsUpdating(true);
- try {
- const response = await fetch('https://web.gise.be/custom_api/update_service_domain.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ order_id: orderId, new_domain: newDomain })
- });
- const data = await response.json();
- if (data.status === 'success') {
- setIsEditingDomain(false);
- if (onRefresh) onRefresh();
- onAlert("Succès", "Domaine web mis à jour avec succès sur le serveur HestiaCP !", "success");
- } else onAlert("Erreur", data.error, "error");
- } catch (err) {
- onAlert("Erreur", "Erreur de connexion avec l'API.", "error");
- } finally { setIsUpdating(false); }
- };
-
- const handleGenerateSSL = async () => {
- setSslStatus('loading');
- try {
- const response = await fetch('https://web.gise.be/custom_api/generate_custom_ssl.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ domain: details.domain })
- });
- const data = await response.json();
- if (data.status === 'success') {
- setSslStatus('success');
- onAlert("SSL Validé", "Certificat SSL Let's Encrypt généré !", "success");
- } else {
- setSslStatus('error');
- onAlert("Erreur DNS", "Échec Let's Encrypt. Vérifiez vos DNS.", "error");
- }
- } catch (e) { setSslStatus('error'); }
- };
-
- const handleOpenPanel = async () => {
- setIsGeneratingSso(true);
- try {
- const secureHash = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase();
- const rollingPassword = `Nx${secureHash}`;
- await resetHostingPassword(orderId, rollingPassword);
- onOpenSso({ username: details.username, password: rollingPassword, url: 'https://panel.gise.be/login/' });
- } catch (err) {
- onAlert("Erreur Métal", "Erreur lors de la génération du SSO HestiaCP.", "error");
- } finally {
- setIsGeneratingSso(false);
- }
- };
-
- return (
-
-
-
- {!isEditingDomain ? (
-
-
-
- {details.domain}
-
-
-
- ) : (
-
- setNewDomain(e.target.value.toLowerCase())} className="flex-1 bg-black border border-gray-700 rounded px-3 py-1.5 text-white font-mono text-sm focus:border-emerald-500 outline-none" disabled={isUpdating} />
-
-
-
- )}
-
- {isCustomDomain && (
-
-
Domaine personnalisé détecté. Sécurisez après pointage DNS.
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-// ============================================================================
-// COMPOSANT PRINCIPAL : SERVICES
-// ============================================================================
export default function Services() {
const [services, setServices] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [newVpcName, setNewVpcName] = useState("");
const [isConnecting, setIsConnecting] = useState(null);
- const [ssoVault, setSsoVault] = useState(null);
const [activeServiceModal, setActiveServiceModal] = useState(null);
const [customAlert, setCustomAlert] = useState(null);
const { vpcs, createVPC, deleteVPC, assignToVPC, removeFromVPC } = useVPC();
- const triggerAlert = (title, message, type = "info") => {
- setCustomAlert({ title, message, type });
- };
+ const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
const fetchServices = useCallback(async () => {
try {
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 getHostingServiceDetails(order.id); }
- catch (e) { console.warn(`Détails inaccessibles pour ${order.id}:`, e); }
- }
- return { ...order, hostingDetails: hDetails };
- })
- );
+ 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 getHostingServiceDetails(order.id); } catch (e) {}
+ }
+ return { ...order, hostingDetails: hDetails };
+ }));
const filteredServices = detailedServices.filter(s => {
const type = (s.type || '').toLowerCase();
@@ -498,19 +41,13 @@ export default function Services() {
const isGhostProduct = type === 'domain' || title.includes('domain') || title.includes('domaine') || title.includes('enregistrement');
return (s.status === 'active' || s.status === 'pending_setup') && !isGhostProduct;
});
-
setServices(filteredServices);
}
- } catch (err) {
- setError(err.message || "Impossible de charger la télémétrie des services.");
- } finally {
- setIsLoading(false);
- }
+ } catch (err) { setError(err.message || "Impossible de charger la télémétrie."); }
+ finally { setIsLoading(false); }
}, []);
- useEffect(() => {
- fetchServices();
- }, [fetchServices]);
+ useEffect(() => { fetchServices(); }, [fetchServices]);
const handleOpenConsole = async (service) => {
const titleLower = (service.title || '').toLowerCase();
@@ -520,7 +57,6 @@ export default function Services() {
const isWeb = !isVPS && !isCloud && !isDB;
setIsConnecting(service.id);
-
try {
if (isCloud) {
const titleMatch = (service.title || '').match(/(?: for | pour )(.+)$/i);
@@ -532,53 +68,33 @@ export default function Services() {
const freshDetails = await getHostingServiceDetails(service.id);
setActiveServiceModal({ type: isVPS ? 'vps' : 'web', data: service, details: freshDetails });
}
- } catch (err) {
- triggerAlert("Erreur d'Orchestration", "Échec du protocole : " + err.message, "error");
- } finally {
- setIsConnecting(null);
- }
+ } catch (err) { triggerAlert("Erreur", err.message, "error"); }
+ finally { setIsConnecting(null); }
};
const assignedServiceIds = vpcs.flatMap(vpc => vpc.services);
const freeServices = services.filter(s => !assignedServiceIds.includes(s.id));
return (
-
{/* Alignement avec le Dashboard */}
+
- {/* GESTION DU CHARGEMENT IDENTIQUE AU DASHBOARD */}
- {isLoading && (
-
-
- Analyse du réseau et des instances en cours...
-
- )}
+ {isLoading &&
Analyse en cours...
}
+ {error &&
}
- {/* GESTION DES ERREURS IDENTIQUE AU DASHBOARD */}
- {error && (
-
- )}
-
- {/* CONTENU (Caché pendant le chargement pour rester propre) */}
{!isLoading && !error && (
<>
@@ -588,118 +104,45 @@ export default function Services() {
-
-
{vpc.name.toUpperCase()}
- {vpcServices.length} INSTANCES
+ {vpc.name.toUpperCase()}
{vpcServices.length} INSTANCES
-
+
- {vpcServices.length === 0 ? (
-
- Réseau virtuel vide. Assigner des instances depuis le pool libre.
-
- ) : (
- vpcServices.map(service => (
-
- ))
- )}
+ {vpcServices.length === 0 ?
Vide.
: vpcServices.map(service =>
)}
);
})}
-
-
- POOL D'INSTANCES LIBRES
-
+
POOL LIBRE
- {freeServices.length === 0 ? (
-
- Aucune instance libre. Toutes vos accréditations sont assignées à des VPC.
-
- ) : (
- freeServices.map(service => (
-
- ))
- )}
+ {freeServices.length === 0 ?
Aucune instance libre.
: freeServices.map(service =>
)}
>
)}
- {/* MODAL 1 : HESTIACP SSO VAULT */}
- {ssoVault && (
-
-
-
-
ACCÈS AUTORISÉ
-
-
-
Le pare-feu HestiaCP bloque les injections directes. Un mot de passe jetable a été généré. Copiez-le et connectez-vous.
-
-
-
-
- {ssoVault.username}
-
-
-
-
-
-
- {ssoVault.password}
-
-
-
-
-
- Ouvrir le Panel Web ↗
-
-
-
- )}
- {/* MODAL 2 : GESTIONNAIRE DE SERVICE GÉNÉRIQUE (VPS & WEB) */}
{activeServiceModal && (
- {activeServiceModal.type === 'vps' ? (
- <> {activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? "GESTION DE L'INSTANCE" : "PHASE D'INITIALISATION"}>
- ) : (
- <> GESTION DE L'HÉBERGEMENT WEB>
- )}
+ {activeServiceModal.type === 'vps' ? <> GESTION VPS> : <> GESTION WEB>}
-
{activeServiceModal.type === 'vps' ? (
- activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? (
-
- ) : (
-
{ setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
- )
+ activeServiceModal.details?.ip && activeServiceModal.details.ip !== '127.0.0.1' ? : { setActiveServiceModal(null); fetchServices(); }} onAlert={triggerAlert} />
) : (
-
+
)}
)}
- {/* VRAI MODAL DE NOTIFICATION REACT SURCHARGE */}
setCustomAlert(null)} />
);
diff --git a/src/pages/app/Store.jsx b/src/pages/app/Store.jsx
index b58b2b9..117fac6 100644
--- a/src/pages/app/Store.jsx
+++ b/src/pages/app/Store.jsx
@@ -1,288 +1,16 @@
+// src/pages/app/Store.jsx
import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import ReactMarkdown from 'react-markdown';
+import { Server, Database, Cloud, Globe, Loader, AlertCircle } from 'lucide-react';
import { getProductList } from '../../services/api';
-import { Server, Database, Cloud, Globe, ShoppingCart, Loader, AlertCircle, ChevronDown, CheckCircle2 } from 'lucide-react';
+import CategorySection from '../../components/store/CategorySection'; // L'import magique
-// ==========================================
-// MOTEUR TEMPOREL : Poids et Traductions
-// ==========================================
-const parsePeriod = (code) => {
- if (!code) return { label: '', weight: 0, factorToYear: 1, billingPhrase: '' };
- const value = parseInt(code);
- if (code.includes('W')) return {
- label: `${value} Semaine${value > 1 ? 's' : ''}`,
- weight: value * 7,
- factorToYear: 52 / value,
- billingPhrase: value === 1 ? 'par semaine' : `toutes les ${value} semaines`
- };
- if (code.includes('M')) return {
- label: `${value} Mois`,
- weight: value * 30,
- factorToYear: 12 / value,
- billingPhrase: value === 1 ? 'par mois' : `tous les ${value} mois`
- };
- if (code.includes('Y')) return {
- label: `${value} An${value > 1 ? 's' : ''}`,
- weight: value * 365,
- factorToYear: 1 / value,
- billingPhrase: value === 1 ? 'par an' : `tous les ${value} ans`
- };
- return { label: code, weight: 999, factorToYear: 1, billingPhrase: `pour ${code}` };
-};
-
-// ==========================================
-// GÉNÉRATEUR DE BADGES COURTS (Mis à jour pour l'anglais)
-// ==========================================
-const getCategoryBadge = (categoryName) => {
- const t = (categoryName || '').toLowerCase();
- if (t.includes('web') || t.includes('hosting')) return 'WEB';
- if (t.includes('vps')) return 'VPS';
- if (t.includes('data') || t.includes('db')) return 'DB';
- if (t.includes('cloud')) return 'CLOUD';
- return 'SRV';
-};
-
-// ==========================================
-// SOUS-COMPOSANT : LA CARTE PRODUIT
-// ==========================================
-const ProductCard = ({ product, selectedPeriod, categoryName }) => {
- const navigate = useNavigate();
-
- const getPricingData = () => {
- // 1. Gestion des produits payables une seule fois
- if (product.pricing?.type !== 'recurrent' || !product.pricing?.recurrent) {
- const oncePrice = product.pricing?.once?.price ? parseFloat(product.pricing.once.price).toFixed(2) : '0.00';
- return { isAvailable: true, displayPrice: oncePrice, suffix: '(Une fois)', originalPrice: null, savingsPercent: 0, isOnce: true };
- }
-
- const recurrentPrices = product.pricing.recurrent;
- const availablePeriods = Object.keys(recurrentPrices).filter(
- period => recurrentPrices[period].enabled == 1 || recurrentPrices[period].enabled === true
- );
-
- if (!recurrentPrices[selectedPeriod] || !availablePeriods.includes(selectedPeriod)) {
- return { isAvailable: false };
- }
-
- const currentPrice = parseFloat(recurrentPrices[selectedPeriod].price);
- const currentPeriodInfo = parsePeriod(selectedPeriod);
-
- // LA MAGIE : On convertit le prix de la période choisie en coût mensuel lissé
- const currentYearlyCost = currentPrice * currentPeriodInfo.factorToYear;
- const currentMonthlyEquivalent = currentYearlyCost / 12;
-
- let savingsPercent = 0;
- let originalPrice = null;
-
- // On cherche le forfait le plus court (ex: 1W) pour s'en servir de base de comparaison
- const sortedAvailablePeriods = [...availablePeriods].sort((a, b) => parsePeriod(a).weight - parsePeriod(b).weight);
- const basePeriodCode = sortedAvailablePeriods[0];
-
- if (basePeriodCode !== selectedPeriod) {
- const basePrice = parseFloat(recurrentPrices[basePeriodCode].price);
- const basePeriodInfo = parsePeriod(basePeriodCode);
- // On calcule aussi le prix mensuel lissé de ce forfait de base
- const baseYearlyCost = basePrice * basePeriodInfo.factorToYear;
- const baseMonthlyEquivalent = baseYearlyCost / 12;
-
- if (baseYearlyCost > currentYearlyCost) {
- savingsPercent = Math.round((1 - (currentYearlyCost / baseYearlyCost)) * 100);
- originalPrice = baseMonthlyEquivalent.toFixed(2);
- }
- }
-
- return {
- isAvailable: true,
- displayPrice: currentMonthlyEquivalent.toFixed(2), // Le GROS texte principal
- suffix: '/ mois',
- originalPrice, // Le texte BARRÉ (null si on est sur la période de base)
- savingsPercent,
- billingPrice: currentPrice.toFixed(2), // Ce que la banque va vraiment prélever
- billingPhrase: currentPeriodInfo.billingPhrase, // "par an", "par semaine", etc.
- isOnce: false
- };
- };
-
- const priceData = getPricingData();
-
- return (
-
-
- {/* ENCART ACRO-BADGE */}
-
-
- {getCategoryBadge(categoryName)}
-
-
-
- {/* Badge d'économie */}
- {priceData.savingsPercent > 0 && priceData.isAvailable && (
-
- ÉCONOMIE {priceData.savingsPercent}%
-
- )}
-
-
-
{product.title}
-
- {priceData.isAvailable ? (
-
- {/* PRIX PRINCIPAL (Toujours ramené au mois) */}
-
- {priceData.displayPrice} €
- {priceData.suffix}
-
-
- {/* BLOC DES PETITES LIGNES BANCAIRES */}
-
- {!priceData.isOnce && (
- <>
- {priceData.originalPrice ? (
-
- Au lieu de {priceData.originalPrice} € / mois
-
- ) : (
-
- Tarif de base équivalent
-
- )}
-
- Facturé {priceData.billingPrice} € {priceData.billingPhrase}
-
- >
- )}
- {priceData.isOnce && (
-
Paiement unique
- )}
-
-
- ) : (
-
Non disponible pour cette durée.
- )}
-
-
-
-
-
,
- li: ({node, ...props}) => {props.children},
- p: ({node, ...props}) => ,
- strong: ({node, ...props}) =>
- }}
- >
- {product.description || "Aucune description technique."}
-
-
-
-
-
-
- );
-};
-
-// ==========================================
-// SOUS-COMPOSANT : LA SECTION
-// ==========================================
-const CategorySection = ({ categoryName, products, getCategoryIcon }) => {
- const availablePeriods = new Set();
- products.forEach(p => {
- if (p.pricing?.type === 'recurrent' && p.pricing.recurrent) {
- Object.keys(p.pricing.recurrent).forEach(period => {
- // FILTRE DE SÉCURITÉ : On ne garde que les périodes actives (enabled = 1 ou true)
- const periodData = p.pricing.recurrent[period];
- if (periodData.enabled == 1 || periodData.enabled === true) {
- availablePeriods.add(period);
- }
- });
- }
- });
-
- const sortedPeriods = Array.from(availablePeriods).sort((a, b) => parsePeriod(a).weight - parsePeriod(b).weight);
- const defaultPeriod = sortedPeriods.includes('1M') ? '1M' : sortedPeriods[0];
- const [sectionPeriod, setSectionPeriod] = useState(defaultPeriod);
-
- return (
-
-
-
-
- {getCategoryIcon(categoryName)}
-
-
-
{categoryName}
-
- {products.length} instance{products.length > 1 ? 's' : ''} disponible{products.length > 1 ? 's' : ''}
-
-
-
-
- {sortedPeriods.length > 0 && (
-
-
Facturation :
-
-
-
-
-
- )}
-
-
-
- {products.map((product) => (
-
- ))}
-
-
- );
-};
-
-// ==========================================
-// COMPOSANT PRINCIPAL : LE MAGASIN (STORE)
-// ==========================================
export default function Store() {
const [groupedProducts, setGroupedProducts] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
- // ALIGNEMENT PARFAIT SUR TES CATÉGORIES FOSSBILLING
- const CATEGORY_ORDER = [
- "Web Hosting",
- "VPS",
- "Database",
- "Cloud"
- ];
-
- const CATEGORY_MAP = {
- 1: "Web Hosting",
- 4: "VPS",
- 3: "Database",
- 2: "Cloud"
- };
+ const CATEGORY_ORDER = ["Web Hosting", "VPS", "Database", "Cloud"];
+ const CATEGORY_MAP = { 1: "Web Hosting", 4: "VPS", 3: "Database", 2: "Cloud" };
useEffect(() => {
const fetchCatalog = async () => {
@@ -291,10 +19,7 @@ export default function Store() {
const products = data.list || [];
const grid = products.reduce((acc, product) => {
- // EXTRACTION DE L'ID : On lit le product_category_id envoyé par FOSSBilling
const catId = product.product_category_id;
-
- // TRADUCTION : On cherche le nom dans notre dictionnaire. Si inconnu -> Autres.
const catName = CATEGORY_MAP[catId] || 'Autres Services';
if (!acc[catName]) acc[catName] = [];
@@ -320,14 +45,11 @@ export default function Store() {
return
;
};
- // MOTEUR DE TRI SANS RISK DE CRASH INDICE
const sortedCategoryNames = Object.keys(groupedProducts).sort((a, b) => {
let indexA = CATEGORY_ORDER.indexOf(a);
let indexB = CATEGORY_ORDER.indexOf(b);
-
if (indexA === -1) indexA = 999;
if (indexB === -1) indexB = 999;
-
return indexA - indexB;
});
diff --git a/src/pages/app/Support.jsx b/src/pages/app/Support.jsx
index d720997..fd30646 100644
--- a/src/pages/app/Support.jsx
+++ b/src/pages/app/Support.jsx
@@ -1,77 +1,38 @@
-import { useState, useEffect, useCallback, useRef } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { useOutletContext } from 'react-router-dom';
-import { getClientTickets, createTicket, getHelpdesks, getTicketDetails, replyTicket } from '../../services/api'; // Ajout des routes de détails et réponses
-import { LifeBuoy, Plus, MessageSquare, Clock, CheckCircle2, Send, AlertCircle, Loader, Lock } from 'lucide-react';
+import { getClientTickets, createTicket, getHelpdesks, getTicketDetails, replyTicket } from '../../services/api';
+import { Plus, AlertCircle, Loader } from 'lucide-react';
-// ============================================================================
-// COMPOSANT 0 : MODAL DE NOTIFICATION (Design System)
-// ============================================================================
-const NotificationModal = ({ notification, onClose }) => {
- if (!notification) return null;
- const isError = notification.type === 'error';
- return (
-
-
-
- {notification.title.toUpperCase()}
-
-
{notification.message}
-
-
-
- );
-};
+// Importation des composants isolés
+import NotificationModal from '../../components/ui/NotificationModal';
+import TicketCard from '../../components/support/TicketCard';
+import CreateTicketModal from '../../components/support/CreateTicketModal';
+import TicketThreadModal from '../../components/support/TicketThreadModal';
-// ============================================================================
-// COMPOSANT PRINCIPAL : SUPPORT TICKETS
-// ============================================================================
export default function Support() {
const { refreshNavbarTickets } = useOutletContext();
const [tickets, setTickets] = useState([]);
const [helpdesks, setHelpdesks] = useState([]);
+ const [defaultHelpdesk, setDefaultHelpdesk] = useState('');
const [isLoading, setIsLoading] = useState(true);
- const [isLoadingConversation, setIsLoadingConversation] = useState(false); // Loader spécifique pour le fil de discussion
const [error, setError] = useState(null);
- // États pour les formulaires
- const [subject, setSubject] = useState('');
- const [message, setMessage] = useState('');
- const [replyMessage, setReplyMessage] = useState(''); // Stocke le texte de la réponse
- const [selectedHelpdesk, setSelectedHelpdesk] = useState('');
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- // États pour les modaux
const [customAlert, setCustomAlert] = useState(null);
const [isCreatingTicket, setIsCreatingTicket] = useState(false);
const [activeTicket, setActiveTicket] = useState(null);
+ const [isLoadingConversation, setIsLoadingConversation] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const messagesEndRef = useRef(null);
- const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
- };
- useEffect(() => {
- if (activeTicket && activeTicket.messages && !isLoadingConversation) {
- scrollToBottom();
- }
- }, [activeTicket?.messages, isLoadingConversation]);
+ const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
- const triggerAlert = (title, message, type = "info") => {
- setCustomAlert({ title, message, type });
- };
-
- // Chargement synchrone des tickets et des helpdesks
const loadSupportData = useCallback(async () => {
try {
setIsLoading(true);
const ticketsData = await getClientTickets();
if (ticketsData && ticketsData.list) {
setTickets(ticketsData.list.map(tkt => ({
- id: `TKT-${tkt.id}`,
- db_id: tkt.id,
- subject: tkt.subject,
+ id: `TKT-${tkt.id}`, db_id: tkt.id, subject: tkt.subject,
department: tkt.helpdesk?.name || 'Support Technique',
status: ['open', 'closed', 'on_hold'].includes(tkt.status) ? tkt.status : 'pending',
lastUpdate: tkt.updated_at || tkt.created_at || 'Récemment',
@@ -83,112 +44,68 @@ export default function Support() {
if (hdeskPairs) {
const formattedDesks = Object.entries(hdeskPairs).map(([id, name]) => ({ id, name }));
setHelpdesks(formattedDesks);
+
const nexusDesk = formattedDesks.find(hd => hd.name.toLowerCase().includes('nexus'));
- if (nexusDesk) {
- setSelectedHelpdesk(nexusDesk.id);
- } else if (formattedDesks.length > 0) {
- setSelectedHelpdesk(formattedDesks[0].id);
- }
+ if (nexusDesk) setDefaultHelpdesk(nexusDesk.id);
+ else if (formattedDesks.length > 0) setDefaultHelpdesk(formattedDesks[0].id);
}
- } catch (err) {
- setError(err.message || "Impossible de synchroniser le centre de support.");
- } finally {
- setIsLoading(false);
- }
+ } catch (err) { setError(err.message || "Impossible de synchroniser le centre de support."); }
+ finally { setIsLoading(false); }
}, []);
- useEffect(() => {
- loadSupportData();
- }, [loadSupportData]);
+ useEffect(() => { loadSupportData(); }, [loadSupportData]);
- // ACTION CLIC : Charger les messages du ticket depuis FOSSBilling
+ // Ouvre le fil de discussion
const handleOpenTicket = async (ticket) => {
try {
setIsLoadingConversation(true);
- // On ouvre immédiatement le modal avec une liste de messages vide pour la fluidité
setActiveTicket({ ...ticket, messages: [] });
-
const details = await getTicketDetails(ticket.db_id);
-
if (details && details.messages) {
- // FOSSBilling renvoie l'auteur dans msg.author.role ('client', 'staff', 'admin')
const formattedMessages = details.messages.map(msg => ({
sender: msg.author?.role === 'client' ? 'client' : 'staff',
- text: msg.content,
- date: msg.created_at || 'Récemment'
+ text: msg.content, date: msg.created_at || 'Récemment'
}));
-
- // Injection dynamique des messages dans le modal actif
setActiveTicket(prev => prev ? { ...prev, messages: formattedMessages } : null);
}
- } catch (err) {
- triggerAlert("Erreur Réseau", "Impossible de récupérer l'historique : " + err.message, "error");
- } finally {
- setIsLoadingConversation(false);
- }
+ } catch (err) { triggerAlert("Erreur", "Impossible de récupérer l'historique : " + err.message, "error"); }
+ finally { setIsLoadingConversation(false); }
};
- // ACTION RÉPONSE : Envoyer un message dans le thread actuel
- const handleReplySubmit = async (e) => {
- e.preventDefault();
- if (!replyMessage.trim() || !activeTicket) return;
-
+ // Soumet une réponse au fil de discussion
+ const handleReplySubmit = async (replyText) => {
+ if (!activeTicket) return;
try {
- await replyTicket(activeTicket.db_id, replyMessage);
+ await replyTicket(activeTicket.db_id, replyText);
loadSupportData();
- setReplyMessage(''); // Nettoyer l'input
-
- // Rechargement instantané du fil de discussion pour afficher le message soumis
+
const details = await getTicketDetails(activeTicket.db_id);
if (details && details.messages) {
const formattedMessages = details.messages.map(msg => ({
sender: msg.author?.role === 'client' ? 'client' : 'staff',
- text: msg.content,
- date: msg.created_at || 'Récemment'
+ text: msg.content, date: msg.created_at || 'Récemment'
}));
setActiveTicket(prev => prev ? { ...prev, messages: formattedMessages } : null);
}
if (refreshNavbarTickets) refreshNavbarTickets();
- } catch (err) {
- triggerAlert("Échec d'envoi", "Votre réponse n'a pas pu être transmise : " + err.message, "error");
- }
+ } catch (err) { triggerAlert("Échec d'envoi", "Votre réponse n'a pas pu être transmise : " + err.message, "error"); }
};
- // Soumission du nouveau ticket
- const handleCreateTicket = async (e) => {
- e.preventDefault();
- if (!subject.trim() || !message.trim() || !selectedHelpdesk) return;
-
+ // Crée un nouveau ticket
+ const handleCreateTicket = async (subject, message, targetHelpdesk) => {
setIsSubmitting(true);
try {
- await createTicket(subject, message, selectedHelpdesk);
+ await createTicket(subject, message, targetHelpdesk);
setIsCreatingTicket(false);
- setSubject('');
- setMessage('');
loadSupportData();
if (refreshNavbarTickets) refreshNavbarTickets();
triggerAlert("Ticket Ouvert", "Votre demande a bien été enregistrée sur le Service Desk.", "success");
- } catch (err) {
- triggerAlert("Échec", "Erreur lors de la création du ticket : " + err.message, "error");
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const getStatusConfig = (status) => {
- switch (status) {
- case 'open': return { color: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', label: 'SUPPORT', icon:
};
- case 'on_hold': return { color: 'text-yellow-400', bg: 'bg-yellow-500/10', border: 'border-yellow-500/20', label: 'RÉPONSE REÇUE', icon:
};
- case 'pending': return { color: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/20', label: 'EN ATTENTE', icon:
};
- case 'closed': return { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/20', label: 'RÉSOLU', icon:
};
- default: return { color: 'text-gray-400', bg: 'bg-gray-800', border: 'border-gray-700', label: status.toUpperCase() };
- }
+ } catch (err) { triggerAlert("Échec", "Erreur lors de la création du ticket : " + err.message, "error"); }
+ finally { setIsSubmitting(false); }
};
return (
-
- {/* HEADER */}
-
- {/* CHARGEMENT & ERREURS GLOBALES */}
- {isLoading && (
-
-
- Déchiffrement de la matrice de support...
-
- )}
+ {isLoading &&
Déchiffrement de la matrice de support...
}
+ {error &&
}
- {error && (
-
- )}
-
- {/* GRILLE DES TICKETS */}
{!isLoading && !error && (
{tickets.length === 0 ? (
@@ -230,171 +133,29 @@ export default function Support() {
Aucun ticket de support actif. L'infrastructure est nominale.
) : (
- tickets.map(ticket => {
- const conf = getStatusConfig(ticket.status);
- return (
-
handleOpenTicket(ticket)} // Modification ici : Appel de la fonction de chargement au lieu du setter direct
- className="bg-gray-900 border border-gray-800 rounded-xl p-5 hover:border-cyan-400/50 hover:bg-gray-800/50 transition-all cursor-pointer group flex flex-col justify-between min-h-[160px]"
- >
-
-
-
- {ticket.id}
-
- {/* LE RADAR : Pastille clignotante en cas de réponse */}
- {ticket.status === 'on_hold' && (
-
-
-
-
- )}
-
-
- {conf.icon} {conf.label}
-
-
-
- {ticket.subject}
-
-
-
- Département: {ticket.department}
- MàJ: {ticket.lastUpdate}
-
-
- )
- })
+ tickets.map(ticket => (
+
handleOpenTicket(ticket)} />
+ ))
)}
)}
- {/* MODAL 1 : CRÉATION DE TICKET */}
- {isCreatingTicket && (
-
-
-
-
- OUVRIR UNE REQUÊTE
-
-
-
+
setIsCreatingTicket(false)}
+ onSubmit={handleCreateTicket}
+ helpdesks={helpdesks}
+ defaultHelpdesk={defaultHelpdesk}
+ isSubmitting={isSubmitting}
+ />
-
-
-
- )}
+
setActiveTicket(null)}
+ onReply={handleReplySubmit}
+ isLoadingConversation={isLoadingConversation}
+ />
- {/* MODAL 2 : FIL DE DISCUSSION INTERACTIF (THREAD NATIVE) */}
- {activeTicket && (
-
-
-
- {/* Thread Header */}
-
-
-
- {activeTicket.id}
-
- {activeTicket.department}
-
-
-
{activeTicket.subject}
-
-
-
-
- {/* Thread Body (Messages avec état de chargement) */}
-
- {isLoadingConversation ? (
-
-
- Téléchargement des paquets de discussion sécurisés...
-
- ) : activeTicket.messages.length === 0 ? (
-
- Aucun message trouvé dans ce fil.
-
- ) : (
- activeTicket.messages.map((msg, idx) => (
-
-
-
- {msg.sender === 'client' ? 'VOUS' : 'INGÉNIEUR GISE'}
-
- {msg.date}
-
-
- {msg.text}
-
-
-
- ))
- )}
-
-
- {/* Thread Footer (Formulaire de réponse branché) */}
- {activeTicket.status !== 'closed' ? (
-
- ) : (
-
- [ CE TICKET EST VERROUILLÉ ET ARCHIVÉ ]
-
- )}
-
-
- )}
-
- {/* VRAI MODAL DE NOTIFICATION */}
setCustomAlert(null)} />
);
diff --git a/src/pages/app/VpsDeployer.jsx b/src/pages/app/VpsDeployer.jsx
deleted file mode 100644
index ff905ae..0000000
--- a/src/pages/app/VpsDeployer.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useState } from 'react';
-
-const VpsDeployer = ({ serviceId }) => {
- // Nouveaux états pour le Domaine et le Mot de passe
- const [domain, setDomain] = useState('');
- const [password, setPassword] = useState('');
- const [isDeploying, setIsDeploying] = useState(false);
- const [result, setResult] = useState(null);
-
- const handleDeploy = async () => {
- if (password.length < 8) {
- alert("Le mot de passe doit faire au moins 8 caractères.");
- return;
- }
- if (!domain.includes('.')) {
- alert("Veuillez entrer un nom de domaine valide (ex: mon-projet.fr).");
- return;
- }
-
- setIsDeploying(true);
-
- try {
- // L'appel vers ton Pont PHP avec les nouvelles données
- const response = await fetch('https://web.gise.be/custom_api/proxmox_create_vps.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({
- service_id: serviceId, // L'ID du produit dans FOSSBilling
- domain: domain, // Le domaine choisi par le client
- password: password
- })
- });
-
- const data = await response.json();
-
- if (data.status === 'success') {
- setResult(data);
- } else {
- alert("Erreur de déploiement : " + data.message);
- }
- } catch (err) {
- alert("Erreur de connexion au serveur de provisionnement.");
- } finally {
- setIsDeploying(false);
- }
- };
-
- return (
-
-
Enregistrement & Instanciation
-
- {!result ? (
-
-
- Configuration système : Ubuntu 26.04 LTS (Docker Ready)
-
-
-
-
- setDomain(e.target.value.toLowerCase())}
- className="w-full bg-gray-950 border border-gray-700 text-white p-3 rounded font-mono focus:border-cyan-500 outline-none"
- placeholder="ex: srv1.mon-domaine.com"
- />
-
-
-
-
- setPassword(e.target.value)}
- className="w-full bg-gray-950 border border-gray-700 text-white p-3 rounded font-mono focus:border-cyan-500 outline-none"
- placeholder="Créez un mot de passe fort"
- />
-
-
-
-
- ) : (
-
-
- ✓ INSTANCE ENREGISTRÉE ET OPÉRATIONNELLE
-
-
- Domaine lié:
- {result.domain}
-
-
- Adresse IP Allouée:
- {result.ip}
-
-
- Ce serveur est maintenant rattaché à votre compte. Vous pouvez vous y connecter en SSH via l'IP ou le domaine (une fois les DNS propagés).
-
-
- )}
-
- );
-};
-
-export default VpsDeployer;
\ No newline at end of file
diff --git a/src/pages/app/VpsManager.jsx b/src/pages/app/VpsManager.jsx
deleted file mode 100644
index 213ef97..0000000
--- a/src/pages/app/VpsManager.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { useState } from 'react';
-
-// Ce composant reçoit les détails du service depuis ton API FOSSBilling habituelle
-const VpsManager = ({ serviceDetails }) => {
- const [isActionPending, setIsActionPending] = useState(false);
-
- // Fonction pour envoyer des ordres d'alimentation à Proxmox (via ton API)
- const handlePowerAction = async (action) => {
- if (!window.confirm(`Êtes-vous sûr de vouloir ${action} ce VPS ?`)) return;
- setIsActionPending(true);
-
- try {
- // Tu devras créer un petit fichier 'proxmox_power.php' plus tard pour gérer ça
- alert(`Signal d'alimentation "${action}" envoyé à l'hyperviseur.`);
- // const response = await fetch('https://web.gise.be/custom_api/proxmox_power.php', { ... })
- } catch (error) {
- alert("Erreur de communication avec l'infrastructure.");
- } finally {
- setIsActionPending(false);
- }
- };
-
- // Si Proxmox n'a pas encore fini de configurer, on affiche un loader
- if (!serviceDetails.ip) {
- return (
-
-
-
-
PROVISIONNEMENT EN COURS...
-
Votre serveur est en cours d'assemblage sur le VLAN 60. Cela prend environ 10 secondes.
-
-
- );
- }
-
- return (
-
-
-
-
- NEXUS COMPUTE INSTANCE
-
-
- ID: {serviceDetails.id}
-
-
-
-
- {/* BLOC RÉSEAU */}
-
-
-
-
- {serviceDetails.ip}
-
-
-
-
-
- {serviceDetails.domain}
-
-
-
-
- {/* BLOC ACCÈS */}
-
-
-
-
- root
-
-
-
-
-
-
- {serviceDetails.password || serviceDetails.pass}
-
-
-
-
-
-
-
- {/* CONTRÔLES D'ALIMENTATION */}
-
-
-
-
-
-
-
-
- );
-};
-
-export default VpsManager;
\ No newline at end of file
diff --git a/src/pages/app/VpsServiceDetail.jsx b/src/pages/app/VpsServiceDetail.jsx
deleted file mode 100644
index 8c63118..0000000
--- a/src/pages/app/VpsServiceDetail.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// composant VpsServiceDetail.jsx
-import React, { useState } from 'react';
-// Tu as déjà ces composants de la discussion précédente :
-// import VpsDeployer from './VpsDeployer';
-// import VpsManager from './VpsManager';
-
-const VpsServiceDetail = ({ serviceDetails }) => {
-
- // Le test logique : Si le service a une IP, il est déjà créé sur Proxmox.
- const isProvisioned = serviceDetails.ip && serviceDetails.ip !== '';
-
- return (
-
-
Gestion VPS
-
- {isProvisioned ? (
- // Étape 4 (après création) : Le tableau de bord
-
- ) : (
- // Étape 3 (avant création) : Le formulaire de déploiement
-
-
-
Votre VPS est prêt à être instancié !
-
La facturation est activée. Veuillez configurer les accès initiaux pour lancer la création sur l'infrastructure (VLAN 60).
-
- {/* On passe le service_id au composant Deployer */}
-
-
- )}
-
- );
-};
-
-export default VpsServiceDetail;
\ No newline at end of file
diff --git a/src/pages/public/Home.jsx b/src/pages/public/Home.jsx
index 7ac9275..ac2ff47 100644
--- a/src/pages/public/Home.jsx
+++ b/src/pages/public/Home.jsx
@@ -2,68 +2,28 @@ import { Link } from 'react-router-dom';
export default function Home() {
return (
-
-
-
+
+
NEXUS
-
+
by GISE
-
+
Bienvenue dans l'infrastructure
-
+
Hébergement web, instances VPS et Stockage Cloud en Belgique.
Propulsé par une architecture bare-metal locale.
- {/* LE BOUTON D'ACTION PRINCIPAL REDIRIGE VERS /REGISTER */}
-
-
+
+
Démarrer le déploiement
diff --git a/src/pages/public/Login.jsx b/src/pages/public/Login.jsx
index a91f94c..c5a3864 100644
--- a/src/pages/public/Login.jsx
+++ b/src/pages/public/Login.jsx
@@ -15,23 +15,9 @@ export default function Login() {
setLoading(true);
try {
- // 1. APPEL À TON API BACKEND (FOSSBilling / PHP)
- // Ici, tu mettras ton vrai 'fetch' vers ton serveur pour vérifier le mot de passe.
- // Pour l'instant, on simule un délai réseau d'une seconde.
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- // --- SIMULATION D'AUTHENTIFICATION ---
- // (À remplacer par la vraie validation de ton serveur)
await loginClient(email, password);
-
- // 2. LA CLÉ DU PROBLÈME EST ICI : L'ATTRIBUTION DU BADGE
- // On sauvegarde le token (généralement renvoyé par ton API) dans le navigateur
localStorage.setItem('gise_token', 'secure_token_alphanumerique_factice');
-
- // 3. AUTORISATION ET REDIRECTION
- // Maintenant que le token est en poche, ProtectedRoute nous laissera passer !
navigate('/dashboard');
-
} catch (err) {
setError(err.message);
} finally {
@@ -39,72 +25,56 @@ export default function Login() {
}
};
- // --- DESIGN SYSTEM "BUNKER" ---
- const inputStyle = {
- width: '100%', padding: '10px', marginBottom: '20px',
- backgroundColor: '#1A1A1A', color: '#00E5FF',
- border: '1px solid #333', fontFamily: 'monospace', outline: 'none',
- boxSizing: 'border-box'
- };
-
- const buttonStyle = {
- width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
- color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
- fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px',
- marginTop: '10px'
- };
-
return (
-
-
-
+
+
+
Connexion au Nexus
-
+
[ IDENTIFICATION REQUISE ]
{error && (
-
+
[ ALERTE ] : {error}
)}
-
-
-
- Aucun accès réseau ?
S'enregistrer >
+
+ Aucun accès réseau ?{' '}
+
+ S'enregistrer >
diff --git a/src/pages/public/Register.jsx b/src/pages/public/Register.jsx
index 560f67f..674188c 100644
--- a/src/pages/public/Register.jsx
+++ b/src/pages/public/Register.jsx
@@ -1,50 +1,8 @@
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { registerUnifiedClient } from '../../services/api';
+import NotificationModal from '../../components/ui/NotificationModal';
-// ============================================================================
-// COMPOSANT : MODAL DE NOTIFICATION (Succès / Erreur)
-// ============================================================================
-const NotificationModal = ({ notification, onClose }) => {
- if (!notification) return null;
- const isError = notification.type === 'error';
-
- return (
-
-
-
- {notification.title.toUpperCase()}
-
-
- {notification.message}
-
-
-
-
- );
-};
-
-// ============================================================================
-// COMPOSANT PRINCIPAL : REGISTER
-// ============================================================================
export default function Register() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
@@ -54,7 +12,7 @@ export default function Register() {
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
- const [customAlert, setCustomAlert] = useState(null); // Remplace l'état "error" et "alert()"
+ const [customAlert, setCustomAlert] = useState(null);
const navigate = useNavigate();
const handleRegister = async (e) => {
@@ -62,119 +20,81 @@ export default function Register() {
setCustomAlert(null);
const passwordPolicy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
-
- // VÉRIFICATIONS FRONTEND (Affiche le Modal d'Erreur)
if (!passwordPolicy.test(password)) {
setCustomAlert({ type: 'error', title: 'Sécurité compromise', message: "Le mot de passe doit contenir 8 caractères min, une majuscule, une minuscule, un chiffre et un caractère spécial." });
return;
}
-
if (password !== confirmPassword) {
- setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès (mots de passe) ne correspondent pas." });
+ setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès ne correspondent pas." });
return;
}
-
- const validUsername = /^[a-zA-Z0-9]{3,12}$/.test(username);
- if (!validUsername) {
- setCustomAlert({ type: 'error', title: 'Identifiant invalide', message: "Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères, sans espace)." });
+ 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 {
- // API BACKEND
await registerUnifiedClient(email, username, password, firstName, lastName);
-
- // SUCCÈS (Affiche le Modal de Succès)
- setCustomAlert({
- type: 'success',
- title: 'PROVISIONNEMENT RÉUSSI',
- message: "Vos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\n\nVous pouvez maintenant vous connecter à l'infrastructure."
- });
-
+ 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) {
- // ERREUR API (Affiche le Modal d'Erreur API)
- setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation de l'infrastructure." });
+ setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." });
} finally {
setLoading(false);
}
};
- // Fermeture du Modal : Redirige vers le Login SI c'était un succès
const handleCloseModal = () => {
- if (customAlert?.type === 'success') {
- navigate('/login');
- } else {
- setCustomAlert(null);
- }
- };
-
- // Styles
- const inputStyle = {
- width: '100%', padding: '10px', marginBottom: '15px',
- backgroundColor: '#1A1A1A', color: '#00E5FF',
- border: '1px solid #333', fontFamily: 'monospace', outline: 'none',
- boxSizing: 'border-box'
- };
-
- const buttonStyle = {
- width: '100%', padding: '12px', backgroundColor: loading ? '#333' : '#00E5FF',
- color: loading ? '#888' : '#000', border: 'none', cursor: loading ? 'not-allowed' : 'pointer',
- fontFamily: 'monospace', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '1px',
- marginTop: '10px'
+ if (customAlert?.type === 'success') navigate('/login');
+ else setCustomAlert(null);
};
return (
-
-
-
+
+
+
Créer un accès réseau
-
- [ INITIALISATION DU PROVISIONNEMENT TRIPLE EN CASCADE ]
+
+ [ INITIALISATION DU PROVISIONNEMENT TRIPLE ]
-