rebuild api fossbilling

This commit is contained in:
maximus
2026-07-15 14:42:44 +02:00
parent a4f9663f60
commit 074e3ab195
4 changed files with 253 additions and 113 deletions
@@ -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, getHostingServiceDetails } from '../../services/api'; import { getProductList, getServiceDetails } 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 SubscriptionManager({ order, onClose, onRefresh, onAlert }) { export default function WebServiceSubscriptionManager({ 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([]);
@@ -20,7 +20,7 @@ export default function SubscriptionManager({ order, onClose, onRefresh, onAlert
const fetchDetails = async () => { const fetchDetails = async () => {
setIsDetailsLoading(true); setIsDetailsLoading(true);
try { try {
const data = await getHostingServiceDetails(order.id); const data = await getServiceDetails(order.id);
setServiceDetails(data); setServiceDetails(data);
} catch (err) { } catch (err) {
console.error("Erreur détails service:", err); console.error("Erreur détails service:", err);
+95 -45
View File
@@ -1,11 +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 { getClientOrders } from '../../services/api'; import { getClientOrders } from '../../services/api';
import { AlertCircle, Loader } from 'lucide-react'; import { AlertCircle, Loader, FileText, X } from 'lucide-react';
// Importation des composants isolés // 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 WebServiceSubscriptionManager from '../../components/dashboard/WebServiceSubscriptionManager';
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -13,54 +15,82 @@ export default function Dashboard() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Chargement des données à l'ouverture du Sas const [activeSubscriptionModal, setActiveSubscriptionModal] = useState(null);
useEffect(() => { const [customAlert, setCustomAlert] = useState(null);
const fetchInventory = async () => {
try {
const data = await getClientOrders();
if (data.list) {
// LE FILTRE CHIRURGICAL PAR PREFIXE
const filteredOrders = data.list.filter(order => {
const title = (order.title || '').toLowerCase();
const type = (order.type || '').toLowerCase();
const isGhostProduct = const triggerAlert = (title, message, type = "info") => setCustomAlert({ title, message, type });
type === 'domain' ||
title.startsWith('domain ') ||
title.startsWith('domaine ') ||
title.startsWith('enregistrement ');
return !isGhostProduct;
});
setOrders(filteredOrders); const fetchInventory = async () => {
} else { setIsLoading(true);
setOrders([]); try {
} const data = await getClientOrders();
} catch (err) {
setError(err.message || "Impossible de récupérer la télémétrie des services."); if (data.list) {
} finally { // LE FILTRE CHIRURGICAL PAR PREFIXE
setIsLoading(false); const filteredOrders = data.list.filter(order => {
const title = (order.title || '').toLowerCase();
const type = (order.type || '').toLowerCase();
const isGhostProduct =
type === 'domain' ||
title.startsWith('domain ') ||
title.startsWith('domaine ') ||
title.startsWith('enregistrement ');
return !isGhostProduct;
});
setOrders(filteredOrders);
} else {
setOrders([]);
} }
}; } catch (err) {
setError(err.message || "Impossible de récupérer la télémétrie des services.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchInventory(); fetchInventory();
}, []); }, []);
// 🌟 Plus besoin de charger les IPs, on ouvre juste la modale comptable !
const handleManageSubscription = (order) => {
// 1. Détection du type de service
const titleLower = (order.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');
// C'est un hébergement web s'il ne correspond à aucun des cas ci-dessus
const isWeb = !isVPS && !isCloud && !isDB;
// 2. Logique de blocage
if (isWeb) {
// On ouvre la modale comptable uniquement pour le WEB
setActiveSubscriptionModal(order);
} else {
// On affiche une notification pour les autres types d'instances
triggerAlert(
"Action non disponible",
"La modification autonome d'abonnement est actuellement exclusive aux Hébergements Web. Pour restructurer cette instance, veuillez contacter les ingénieurs via le Centre de Support.",
"info"
);
}
};
return ( return (
<div className="w-full max-w-6xl p-6 mx-auto"> <div className="w-full max-w-6xl p-6 mx-auto relative">
<header className="mb-8"> <header className="mb-8">
<h1 className="text-3xl font-black text-white tracking-wider">TABLEAU DE <span className="text-cyan-400">BORD</span></h1> <h1 className="text-3xl font-black text-white tracking-wider">TABLEAU DE <span className="text-cyan-400">BORD</span></h1>
<p className="text-gray-400 mt-2">Aperçu de vos accréditations réseau et infrastructures.</p> <p className="text-gray-400 mt-2">Gestion financière et accréditations de vos infrastructures.</p>
</header> </header>
{/* GESTION DES ERREURS & CHARGEMENT */}
{isLoading && ( {isLoading && (
<div className="flex items-center space-x-3 text-cyan-400"> <div className="flex items-center space-x-3 text-cyan-400">
<Loader className="w-6 h-6 animate-spin" /> <Loader className="w-6 h-6 animate-spin" />
<span>Synchronisation avec l'orchestrateur en cours...</span> <span>Synchronisation avec le registre comptable...</span>
</div> </div>
)} )}
@@ -71,24 +101,44 @@ export default function Dashboard() {
</div> </div>
)} )}
{/* GRILLE DES SERVICES */}
{!isLoading && !error && ( {!isLoading && !error && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{/* Boucle sur les composants isolés */}
{orders.map((order) => ( {orders.map((order) => (
<DashboardServiceCard <DashboardServiceCard
key={order.id} key={order.id}
order={order} order={order}
onClick={() => navigate(`/services/${order.id}`)} onClick={() => handleManageSubscription(order)}
/> />
))} ))}
{/* Le composant carte d'ajout */}
<NewServiceCard onClick={() => navigate('/store')} /> <NewServiceCard onClick={() => navigate('/store')} />
</div> </div>
)} )}
{/* 🌟 LA MODALE DE GESTION D'ABONNEMENT */}
{activeSubscriptionModal && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-gray-900 border border-cyan-500/50 shadow-2xl shadow-cyan-500/10 rounded-lg max-w-xxl w-full p-6 text-gray-200">
<div className="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
<h3 className="text-xl font-bold text-white tracking-wider flex items-center gap-3">
<FileText className="w-6 h-6 text-cyan-400" />
GESTION DE L'ABONNEMENT
</h3>
<button onClick={() => setActiveSubscriptionModal(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>
<WebServiceSubscriptionManager
order={activeSubscriptionModal}
onClose={() => setActiveSubscriptionModal(null)}
onRefresh={fetchInventory}
onAlert={triggerAlert}
/>
</div>
</div>
)}
<NotificationModal notification={customAlert} onClose={() => setCustomAlert(null)} />
</div> </div>
); );
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { getMyServices, getHostingServiceDetails } from '../../services/api'; import { getClientOrders, getOrderDetails } 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 } from 'lucide-react';
@@ -25,12 +25,12 @@ export default function Services() {
const fetchServices = useCallback(async () => { const fetchServices = useCallback(async () => {
try { try {
const data = await getMyServices(); const data = await getClientOrders();
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 getHostingServiceDetails(order.id); } catch (e) {} try { hDetails = await getOrderDetails(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 getHostingServiceDetails(service.id); const freshDetails = await getOrderDetails(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"); }
+151 -61
View File
@@ -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 unifié et intelligent // Le moteur de requête
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,24 +64,6 @@ 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 () => {
@@ -100,22 +82,6 @@ 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)
// ========================================== // ==========================================
@@ -144,18 +110,6 @@ 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', {
@@ -191,33 +145,169 @@ export const launchSSOGateway = (username, password) => {
document.body.removeChild(form); document.body.removeChild(form);
}; };
// #######################################################################################
// ========================================== // ==========================================
// ROUTES SUPPORT FOSSBILLING (NATIVES) // GESTION DES ACCES
// ========================================== // ==========================================
// Récupère la liste de tous les tickets du client // Connexion
export const getClientTickets = () => export const loginClient = (email, password) =>
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`, 'GET'); apiCall(`${BASE_URL}/api/guest/client/login`, { email, password });
// Récupère le contenu et les messages d'un ticket spécifique // Deconnexion
export const getTicketDetails = (ticketId) => export const logoutClient = () =>
apiCall(`${BASE_URL}/api/client/support/ticket_get`, 'POST', { id: ticketId }); apiCall(`${BASE_URL}/api/client/profile/logout`);
// Crée un nouveau ticket de support
// ==========================================
// GESTION DU CLIENT
// ==========================================
// 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
// Répond à un ticket existant // Lister les tickets
export const getClientTickets = () =>
apiCall(`${BASE_URL}/api/client/support/ticket_get_list`, 'GET');
// Lister les helpdesks
export const getHelpdesks = () =>
apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`, 'GET');
// Lire
export const getTicketDetails = (ticketId) =>
apiCall(`${BASE_URL}/api/client/support/ticket_get`, 'POST', { id: ticketId });
// Repondre
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');