const storage_price = [.0656,.0878,.1093,.1231]; const prices = { nc: { storage: storage_price, users: [0.66,1.16,1.76,2.16], options: { lxc: { basePrice: 31, factor: 1.217 }, dedicated: { basePrice: 64, factor: 1.753 }, onprem: { basePrice: 111, factor: 1.498 }, vol: [75,90,105,120] } }, wh: { storage: storage_price, sites: [3.6,4.7,6.9,9.1], options: { preinstall: { basePrice: 3.773, factor: 1.168 } } }, mail: { storage: storage_price, users: [2.1,4.2,7.3,11.4], options: { catchall: { basePrice: 3.787, factor: 1.196 } } }, vps: { storage: storage_price.map(x => x * 1.1297), cpu: [6.146, 7.687, 9.218, 10.749], ram: [1.646, 2.057, 2.468, 2.879], options: { ip4: { basePrice: 4.393, factor: 1.097 } } } }; // ============================================================================ // PHASE 1: Configuration Manager - Définition déclarative des contrôles // ============================================================================ const ConfigurationManager = { /** * Configuration des sliders pour chaque service * Structure : { id, displayId, min, max, step, restrictedLabels } */ sliders: { nc: { storage: { id: 'nextcloud_stockage_source', displayId: 'nextcloud_stockage_display', min: 0, max: 500, step: 5, restrictedLabels: [0, 50, 100, 200, 300, 400, 500] }, users: { id: 'nextcloud_users_source', displayId: 'nextcloud_users_display', min: 0, max: 50, step: 1, restrictedLabels: [0, 5, 10, 15, 20, 30, 40, 50] } }, wh: { storage: { id: 'wh_stockage_source', displayId: 'wh_stockage_display', min: 0, max: 250, step: 1, restrictedLabels: [0, 5, 10, 25, 50, 75, 100, 250] }, sites: { id: 'wh_sites_source', displayId: 'wh_sites_display', min: 0, max: 20, step: 1, restrictedLabels: [0, 5, 10, 15, 20] } }, mail: { storage: { id: 'mail_stockage_source', displayId: 'mail_stockage_display', min: 0, max: 500, step: 1, restrictedLabels: [0, 5, 10, 25, 50, 75, 150, 250, 500] }, users: { id: 'mail_users_source', displayId: 'mail_users_display', min: 0, max: 50, step: 1, restrictedLabels: [0, 5, 10, 15, 20, 30, 40, 50] } }, vps: { storage: { id: 'vps_stockage_source', displayId: 'vps_stockage_display', min: 0, max: 500, step: 25, restrictedLabels: [0, 25, 50, 75, 100, 200, 300, 400, 500] }, cpu: { id: 'vps_cpu_source', displayId: 'vps_cpu_display', min: 0, max: 32, step: 1, restrictedLabels: [0, 1, 2, 4, 6, 8, 12, 16, 24, 32] }, ram: { id: 'vps_ram_source', displayId: 'vps_ram_display', min: 0, max: 64, step: 1, restrictedLabels: [0, 1, 2, 4, 8, 12, 16, 24, 32, 48, 64] } } }, /** * Configuration des checkboxes pour chaque service * Structure : { id, class, enables, defaultDisabled } */ checkboxes: { nc: { lxc: { id: 'nc_opt_lxc', class: 'nc_opt_lxc', enables: ['dedicated'], defaultDisabled: false }, dedicated: { id: 'nc_opt_decicated', class: 'nc_opt_decicated', enables: ['onprem'], defaultDisabled: true }, onprem: { id: 'nc_opt_onprem', class: 'nc_opt_onprem', enables: [], defaultDisabled: true }, vol: { id: 'nc_opt_vol', class: 'nc_opt_vol', inputId: 'nc_opt_vol_val', enables: [], defaultDisabled: false } }, wh: { preinstall: { id: 'wh_opt_preinstall', class: 'wh_opt_preinstall', enables: [], defaultDisabled: false } }, mail: { catchall: { id: 'mail_opt_catchall', class: 'mail_opt_catchall', enables: [], defaultDisabled: false } }, vps: { ip4: { id: 'vps_opt_ip4', class: 'vps_opt_ip4', enables: [], defaultDisabled: false } } }, /** * Récupère la configuration complète d'un slider */ getSliderConfig(service, field) { return this.sliders[service] && this.sliders[service][field]; }, /** * Récupère la configuration complète d'une checkbox */ getCheckboxConfig(service, option) { return this.checkboxes[service] && this.checkboxes[service][option]; }, /** * Retourne tous les services configurés */ getServices() { return Object.keys(this.sliders); } }; // ============================================================================ // PHASE 1: State Manager - Gestion centralisée de l'état // ============================================================================ const StateManager = { /** * État actuel de toutes les sélections */ state: { nc: { storage: 0, users: 0, opt_lxc: false, opt_dedicated: false, opt_onprem: false, opt_vol: false, opt_vol_val: 1 }, wh: { storage: 0, sites: 0, opt_preinstall: false }, mail: { storage: 0, users: 0, opt_catchall: false }, vps: { storage: 0, cpu: 0, ram: 0, opt_ip4: false } }, /** * Récupère une valeur de l'état */ get(service, field) { return this.state[service] && this.state[service][field]; }, /** * Définit une valeur dans l'état */ set(service, field, value) { if (this.state[service]) { this.state[service][field] = value; } }, /** * Récupère tout l'état d'un service */ getServiceState(service) { return this.state[service]; }, /** * Récupère l'état complet */ getState() { return JSON.parse(JSON.stringify(this.state)); // Deep copy }, /** * Définit l'état complet */ setState(newState) { this.state = JSON.parse(JSON.stringify(newState)); // Deep copy }, /** * Réinitialise l'état aux valeurs par défaut */ reset() { Object.keys(this.state).forEach(service => { Object.keys(this.state[service]).forEach(field => { if (typeof this.state[service][field] === 'boolean') { this.state[service][field] = false; } else if (field === 'opt_vol_val') { this.state[service][field] = 1; } else { this.state[service][field] = 0; } }); }); }, /** * Sérialise l'état en paramètres URL * Format compact : nc_s=100&nc_u=5&nc_lxc=1... */ toURLParams() { const params = new URLSearchParams(); Object.keys(this.state).forEach(service => { Object.keys(this.state[service]).forEach(field => { const value = this.state[service][field]; // Ne sauvegarder que les valeurs non-nulles/non-false if (value !== 0 && value !== false && value !== '') { // Créer une clé courte let key; if (field === 'storage') { key = service + '_s'; } else if (field === 'users') { key = service + '_u'; } else if (field === 'sites') { key = service + '_si'; } else if (field === 'cpu') { key = service + '_c'; } else if (field === 'ram') { key = service + '_r'; } else if (field.startsWith('opt_')) { // Pour les options, enlever le préfixe opt_ const optName = field.replace('opt_', ''); key = service + '_' + optName; } else { key = service + '_' + field; } // Valeur : 1 pour boolean true, sinon la valeur params.append(key, typeof value === 'boolean' ? '1' : value); } }); }); return params.toString(); }, /** * Désérialise des paramètres URL en état */ fromURLParams(urlParams) { const params = new URLSearchParams(urlParams); // Mapping inverse pour retrouver les noms complets const fieldMapping = { s: 'storage', u: 'users', c: 'cpu', r: 'ram', si: 'sites' }; params.forEach((value, key) => { const parts = key.split('_'); if (parts.length >= 2) { const service = parts[0]; let field = parts.slice(1).join('_'); // Gérer les cas comme vol_val // Si c'est un raccourci d'une lettre, utiliser le mapping if (field.length <= 2 && fieldMapping[field]) { field = fieldMapping[field]; } // Gérer les options (qui ne sont pas dans le mapping) else if (['lxc', 'dedicated', 'onprem', 'vol', 'preinstall', 'catchall', 'ip4'].includes(field)) { field = 'opt_' + field; } // Gérer vol_val spécifiquement else if (field === 'vol_val') { field = 'opt_vol_val'; } if (this.state[service] && this.state[service].hasOwnProperty(field)) { // Parser la valeur selon le type attendu if (typeof this.state[service][field] === 'boolean') { this.state[service][field] = value === '1' || value === 'true'; } else { this.state[service][field] = parseFloat(value) || 0; } } } }); } }; // ============================================================================ // PHASE 2: Price Engine - Logique de calcul pure (fonctions testables) // ============================================================================ const PriceEngine = { /** * Calcule le prix Nextcloud pour une tranche donnée */ calculateNextcloudPrice(state, tierIndex) { const ncState = state.nc; let price = ncState.storage * prices.nc.storage[tierIndex]; // Ajouter les utilisateurs si pas en mode LXC if (!ncState.opt_lxc) { price += ncState.users * prices.nc.users[tierIndex]; } // Déterminer quelle option appliquer (hiérarchique: lxc < dedicated < onprem) let optionMultiplier = { basePrice: 0, factor: 1 }; if (ncState.opt_lxc) { optionMultiplier = prices.nc.options.lxc; } if (ncState.opt_lxc && ncState.opt_dedicated) { optionMultiplier = prices.nc.options.dedicated; } if (ncState.opt_lxc && ncState.opt_dedicated && ncState.opt_onprem) { optionMultiplier = prices.nc.options.onprem; } // Appliquer le multiplicateur d'option price = (price + optionMultiplier.basePrice) * optionMultiplier.factor; // Ajouter l'option gros volume si activée if (ncState.opt_vol && ncState.opt_vol_val > 0) { const volPrice = Math.max(100, Math.pow(ncState.opt_vol_val, 0.7) * prices.nc.options.vol[tierIndex]); price += volPrice; } return price; }, /** * Calcule le prix par To pour l'option volume Nextcloud */ calculateNextcloudVolumePerTB(state, tierIndex) { const ncState = state.nc; if (!ncState.opt_vol || ncState.opt_vol_val <= 0) { return null; } const totalPrice = this.calculateNextcloudPrice(state, tierIndex); return totalPrice / ncState.opt_vol_val; }, /** * Calcule le prix Hébergement Web pour une tranche donnée */ calculateWebHostingPrice(state, tierIndex) { const whState = state.wh; let price = whState.storage * prices.wh.storage[tierIndex] + whState.sites * prices.wh.sites[tierIndex]; // Appliquer l'option préinstallation si activée if (whState.opt_preinstall) { price = (price + prices.wh.options.preinstall.basePrice) * prices.wh.options.preinstall.factor; } return price; }, /** * Calcule le prix Hébergement Mail pour une tranche donnée */ calculateMailPrice(state, tierIndex) { const mailState = state.mail; let price = mailState.storage * prices.mail.storage[tierIndex] + mailState.users * prices.mail.users[tierIndex]; // Appliquer l'option catch-all si activée if (mailState.opt_catchall) { price = (price + prices.mail.options.catchall.basePrice) * prices.mail.options.catchall.factor; } return price; }, /** * Calcule le prix VPS pour une tranche donnée */ calculateVPSPrice(state, tierIndex) { const vpsState = state.vps; let price = vpsState.storage * prices.vps.storage[tierIndex] + vpsState.cpu * prices.vps.cpu[tierIndex] + vpsState.ram * prices.vps.ram[tierIndex]; // Appliquer l'option IPv4 si activée if (vpsState.opt_ip4) { price = (price + prices.vps.options.ip4.basePrice) * prices.vps.options.ip4.factor; } return price; }, /** * Calcule le prix d'un service spécifique */ calculateServicePrice(service, state, tierIndex) { switch(service) { case 'nc': return this.calculateNextcloudPrice(state, tierIndex); case 'wh': return this.calculateWebHostingPrice(state, tierIndex); case 'mail': return this.calculateMailPrice(state, tierIndex); case 'vps': return this.calculateVPSPrice(state, tierIndex); default: return 0; } }, /** * Calcule tous les prix pour les 4 tranches * Retourne un tableau de 4 objets avec les détails par service */ calculateAllPrices(state) { const results = []; for (let tierIndex = 0; tierIndex < 4; tierIndex++) { const nc = this.calculateNextcloudPrice(state, tierIndex); const wh = this.calculateWebHostingPrice(state, tierIndex); const mail = this.calculateMailPrice(state, tierIndex); const vps = this.calculateVPSPrice(state, tierIndex); const total = Math.ceil(nc + wh + mail + vps); results.push({ tierIndex, tierName: ['minimum', 'correct', 'généreux', 'maximum'][tierIndex], nc, wh, mail, vps, total, // Calcul du prix par To pour Nextcloud (si volume activé) ncVolumePerTB: this.calculateNextcloudVolumePerTB(state, tierIndex) }); } return results; }, /** * Vérifie si un service est actif (au moins une option non-nulle) */ isServiceActive(service, state) { const serviceState = state[service]; if (!serviceState) return false; return Object.values(serviceState).some(value => { if (typeof value === 'boolean') return value; if (typeof value === 'number') return value > 0; return false; }); } }; // ============================================================================ // PHASE 3: UI Controller - Gestion déclarative des contrôles // ============================================================================ const UIController = { /** * Initialise tous les contrôles de l'interface */ initialize() { this.initializeAccordion(); this.initializeSticky(); this.initializeShape(); this.initializeAllSliders(); this.initializeAllCheckboxes(); this.bindSpecialEvents(); }, /** * Initialise l'accordéon Semantic UI */ initializeAccordion() { $('.ui.accordion').accordion({ exclusive: true, collapsible: false }); }, /** * Initialise le sticky header */ initializeSticky() { $('.ui.sticky').sticky(); }, /** * Initialise les animations shape */ initializeShape() { $('.shape').shape(); }, /** * Factory: Crée et initialise un slider à partir de sa configuration */ createSlider(config) { const self = this; $('#' + config.id).slider({ showThumbTooltip: true, restrictedLabels: config.restrictedLabels, min: config.min, max: config.max, step: config.step, autoAdjustLabels: false, onMove: function(range, firstVal, secondVal) { $('#' + config.displayId).html(firstVal); self.syncStateFromDOM(); self.updateDisplay(); // Phase 4: Mise à jour de l'URL après chaque changement URLSync.updateURL(); // Phase 5: Mise à jour du récapitulatif SummaryGenerator.updateSummary(); } }); }, /** * Initialise tous les sliders depuis la configuration */ initializeAllSliders() { const services = ConfigurationManager.getServices(); services.forEach(service => { const sliderConfigs = ConfigurationManager.sliders[service]; Object.keys(sliderConfigs).forEach(field => { const config = sliderConfigs[field]; this.createSlider(config); }); }); }, /** * Initialise toutes les checkboxes depuis la configuration */ initializeAllCheckboxes() { const self = this; // Première passe : initialiser toutes les checkboxes Semantic UI Object.keys(ConfigurationManager.checkboxes).forEach(service => { const checkboxConfigs = ConfigurationManager.checkboxes[service]; Object.keys(checkboxConfigs).forEach(option => { const config = checkboxConfigs[option]; const $checkbox = $('.ui.toggle.checkbox.' + config.class); // Initialiser la checkbox Semantic UI $checkbox.checkbox(); // Appliquer l'état disabled par défaut if (config.defaultDisabled) { $checkbox.checkbox('set disabled'); } }); }); // Deuxième passe : attacher les événements de dépendances et de mise à jour Object.keys(ConfigurationManager.checkboxes).forEach(service => { const checkboxConfigs = ConfigurationManager.checkboxes[service]; Object.keys(checkboxConfigs).forEach(option => { const config = checkboxConfigs[option]; const $checkbox = $('.ui.toggle.checkbox.' + config.class); const $input = $checkbox.find('input[type="checkbox"]'); $input.on('change', function() { const isChecked = $(this).is(':checked'); // Gérer les dépendances enables if (config.enables && config.enables.length > 0) { if (isChecked) { // Activer les options dépendantes config.enables.forEach(targetOption => { const targetConfig = ConfigurationManager.getCheckboxConfig(service, targetOption); if (targetConfig) { $('.ui.toggle.checkbox.' + targetConfig.class).checkbox('set enabled'); } }); } else { // Désactiver et décocher les options dépendantes (avec cascade) config.enables.forEach(targetOption => { self.disableCheckboxCascade(service, targetOption); }); } } // Synchroniser l'état et mettre à jour l'affichage self.syncStateFromDOM(); self.updateDisplay(); // Phase 4: Mise à jour de l'URL après chaque changement URLSync.updateURL(); // Phase 5: Mise à jour du récapitulatif SummaryGenerator.updateSummary(); }); }); }); }, /** * Désactive récursivement une checkbox et toutes ses dépendances */ disableCheckboxCascade(service, option) { const config = ConfigurationManager.getCheckboxConfig(service, option); if (!config) { return; } // Désactiver et décocher cette checkbox const $checkbox = $('.ui.toggle.checkbox.' + config.class); $checkbox.checkbox('uncheck').checkbox('set disabled'); // Si elle a des dépendances, les désactiver aussi récursivement if (config.enables && config.enables.length > 0) { config.enables.forEach(targetOption => { this.disableCheckboxCascade(service, targetOption); }); } }, /** * Bind les événements spéciaux (cas particuliers) */ bindSpecialEvents() { const self = this; // Événement sur l'input du volume Nextcloud $('#nc_opt_vol_val').on('change', function() { StateManager.set('nc', 'opt_vol_val', parseInt($(this).val()) || 1); self.updateDisplay(); // Phase 4: Mise à jour de l'URL après chaque changement URLSync.updateURL(); // Phase 5: Mise à jour du récapitulatif SummaryGenerator.updateSummary(); }); // Événement spécial pour la checkbox LXC (toggle shape) $('.ui.toggle.checkbox.nc_opt_lxc').find('input[type="checkbox"]').on('change', function() { $('#ncusers').shape('flip up'); $('#nextcloud_users_source').toggleClass('disabled'); }); }, /** * Synchronise l'état depuis le DOM (lit toutes les valeurs) */ syncStateFromDOM() { // Nextcloud StateManager.set('nc', 'storage', $('#nextcloud_stockage_source').slider('get value')); StateManager.set('nc', 'users', $('#nextcloud_users_source').slider('get value')); StateManager.set('nc', 'opt_lxc', $('.ui.toggle.checkbox.nc_opt_lxc').checkbox('is checked')); StateManager.set('nc', 'opt_dedicated', $('.ui.toggle.checkbox.nc_opt_decicated').checkbox('is checked')); StateManager.set('nc', 'opt_onprem', $('.ui.toggle.checkbox.nc_opt_onprem').checkbox('is checked')); StateManager.set('nc', 'opt_vol', $('.ui.checkbox.nc_opt_vol').checkbox('is checked')); StateManager.set('nc', 'opt_vol_val', parseInt($('#nc_opt_vol_val').val()) || 1); // Web Hosting StateManager.set('wh', 'storage', $('#wh_stockage_source').slider('get value')); StateManager.set('wh', 'sites', $('#wh_sites_source').slider('get value')); StateManager.set('wh', 'opt_preinstall', $('.ui.toggle.checkbox.wh_opt_preinstall').checkbox('is checked')); // Mail StateManager.set('mail', 'storage', $('#mail_stockage_source').slider('get value')); StateManager.set('mail', 'users', $('#mail_users_source').slider('get value')); StateManager.set('mail', 'opt_catchall', $('.ui.toggle.checkbox.mail_opt_catchall').checkbox('is checked')); // VPS StateManager.set('vps', 'storage', $('#vps_stockage_source').slider('get value')); StateManager.set('vps', 'cpu', $('#vps_cpu_source').slider('get value')); StateManager.set('vps', 'ram', $('#vps_ram_source').slider('get value')); StateManager.set('vps', 'opt_ip4', $('.ui.toggle.checkbox.vps_opt_ip4').checkbox('is checked')); }, /** * Synchronise le DOM depuis l'état (écrit toutes les valeurs) * Utilisé pour charger un état sauvegardé (URL params, localStorage, etc.) */ syncDOMFromState() { const state = StateManager.getState(); // Nextcloud $('#nextcloud_stockage_source').slider('set value', state.nc.storage); $('#nextcloud_stockage_display').html(state.nc.storage); $('#nextcloud_users_source').slider('set value', state.nc.users); $('#nextcloud_users_display').html(state.nc.users); $('#nc_opt_vol_val').val(state.nc.opt_vol_val); this.setCheckboxState('nc_opt_lxc', state.nc.opt_lxc); this.setCheckboxState('nc_opt_decicated', state.nc.opt_dedicated); this.setCheckboxState('nc_opt_onprem', state.nc.opt_onprem); this.setCheckboxState('nc_opt_vol', state.nc.opt_vol); // Gérer l'état enabled/disabled des checkboxes hiérarchiques Nextcloud if (state.nc.opt_lxc) { // Si lxc est coché, activer dedicated $('.ui.toggle.checkbox.nc_opt_decicated').checkbox('set enabled'); if (state.nc.opt_dedicated) { // Si dedicated est coché, activer onprem $('.ui.toggle.checkbox.nc_opt_onprem').checkbox('set enabled'); } else { // Sinon, désactiver onprem $('.ui.toggle.checkbox.nc_opt_onprem').checkbox('set disabled'); } } else { // Si lxc n'est pas coché, désactiver dedicated et onprem $('.ui.toggle.checkbox.nc_opt_decicated').checkbox('set disabled'); $('.ui.toggle.checkbox.nc_opt_onprem').checkbox('set disabled'); } // Gérer l'affichage du shape pour Nextcloud users const $ncShape = $('#ncusers'); const currentSideActive = $ncShape.find('.side.active').index(); if (state.nc.opt_lxc) { // LXC activé : afficher "utilisateurs illimités" (premier côté du shape, index 0) if (currentSideActive !== 0) { $ncShape.shape('flip up'); } $('#nextcloud_users_source').addClass('disabled'); } else { // LXC désactivé : afficher le nombre d'utilisateurs (deuxième côté, index 1) if (currentSideActive !== 1) { $ncShape.shape('flip up'); } $('#nextcloud_users_source').removeClass('disabled'); } // Web Hosting $('#wh_stockage_source').slider('set value', state.wh.storage); $('#wh_stockage_display').html(state.wh.storage); $('#wh_sites_source').slider('set value', state.wh.sites); $('#wh_sites_display').html(state.wh.sites); this.setCheckboxState('wh_opt_preinstall', state.wh.opt_preinstall); // Mail $('#mail_stockage_source').slider('set value', state.mail.storage); $('#mail_stockage_display').html(state.mail.storage); $('#mail_users_source').slider('set value', state.mail.users); $('#mail_users_display').html(state.mail.users); this.setCheckboxState('mail_opt_catchall', state.mail.opt_catchall); // VPS $('#vps_stockage_source').slider('set value', state.vps.storage); $('#vps_stockage_display').html(state.vps.storage); $('#vps_cpu_source').slider('set value', state.vps.cpu); $('#vps_cpu_display').html(state.vps.cpu); $('#vps_ram_source').slider('set value', state.vps.ram); $('#vps_ram_display').html(state.vps.ram); this.setCheckboxState('vps_opt_ip4', state.vps.opt_ip4); // Mettre à jour l'affichage des prix this.updateDisplay(); }, /** * Helper pour définir l'état d'une checkbox */ setCheckboxState(checkboxClass, checked) { const $checkbox = $('.ui.toggle.checkbox.' + checkboxClass); if (checked) { $checkbox.checkbox('set checked'); } else { $checkbox.checkbox('set unchecked'); } }, /** * Met à jour l'affichage des prix */ updateDisplay() { // Calculer tous les prix via le PriceEngine const priceResults = PriceEngine.calculateAllPrices(StateManager.getState()); // Mettre à jour l'affichage des prix priceResults.forEach((result, index) => { $('#price_' + index).html(result.total + '€'); }); // Mettre à jour l'affichage du prix par To (Nextcloud volume) if (priceResults[0].ncVolumePerTB !== null) { $('#teraeurolow').html(priceResults[0].ncVolumePerTB.toPrecision(3)); $('#teraeurohigh').html(priceResults[3].ncVolumePerTB.toPrecision(3)); } else { $('#teraeurolow').html('-'); $('#teraeurohigh').html('-'); } } }; // ============================================================================ // PHASE 4: URL Sync - Sauvegarde et rechargement de l'état via URL // ============================================================================ const URLSync = { /** * Met à jour l'URL avec l'état actuel (sans recharger la page) */ updateURL() { const urlParams = StateManager.toURLParams(); if (urlParams) { const newURL = window.location.pathname + '?' + urlParams; // Utiliser replaceState pour ne pas polluer l'historique window.history.replaceState({}, '', newURL); } else { // Si aucun paramètre, nettoyer l'URL window.history.replaceState({}, '', window.location.pathname); } }, /** * Charge l'état depuis l'URL au chargement de la page */ loadFromURL() { const urlParams = window.location.search; if (urlParams) { // Charger l'état depuis l'URL StateManager.fromURLParams(urlParams); // Synchroniser le DOM avec cet état UIController.syncDOMFromState(); return true; } return false; }, /** * Génère une URL complète partageable */ getShareableURL() { const urlParams = StateManager.toURLParams(); const baseURL = window.location.origin + window.location.pathname; if (urlParams) { return baseURL + '?' + urlParams; } return baseURL; }, /** * Copie l'URL partageable dans le presse-papier */ copyShareableURL() { const url = this.getShareableURL(); if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(url).then(() => { return true; }).catch(err => { console.error('Erreur lors de la copie:', err); return false; }); } else { // Fallback pour les navigateurs plus anciens const textArea = document.createElement('textarea'); textArea.value = url; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); document.body.removeChild(textArea); return Promise.resolve(true); } catch (err) { console.error('Erreur lors de la copie:', err); document.body.removeChild(textArea); return Promise.resolve(false); } } } }; // ============================================================================ // PHASE 5: Summary Generator - Génération du récapitulatif // ============================================================================ const SummaryGenerator = { /** * Labels lisibles pour chaque service */ serviceLabels: { nc: 'Nextcloud', wh: 'Hébergement Web', mail: 'Hébergement Mail', vps: 'VPS' }, /** * Génère le récapitulatif HTML d'un service Nextcloud */ generateNextcloudSummary(state) { const nc = state.nc; const items = []; if (nc.storage > 0) { items.push(`${nc.storage} Go de stockage`); } if (!nc.opt_lxc && nc.users > 0) { items.push(`${nc.users} utilisateurs`); } if (nc.opt_lxc) { items.push(`Instance privée`); items.push(`Utilisateurs illimités`); } if (nc.opt_dedicated) { items.push(`Serveur dédié`); } if (nc.opt_onprem) { items.push(`Sur site`); } if (nc.opt_vol && nc.opt_vol_val > 0) { items.push(`${nc.opt_vol_val} To de volume supplémentaire`); } return items.length > 0 ? items.join(' ') : null; }, /** * Génère le récapitulatif texte d'un service Nextcloud (pour email) */ generateNextcloudTextSummary(state) { const nc = state.nc; const items = []; if (nc.storage > 0) { items.push(`${nc.storage} Go de stockage`); } if (!nc.opt_lxc && nc.users > 0) { items.push(`${nc.users} utilisateurs`); } if (nc.opt_lxc) { items.push(`Instance privée (utilisateurs illimités)`); } if (nc.opt_dedicated) { items.push(`Serveur dédié`); } if (nc.opt_onprem) { items.push(`Sur site (on-premise)`); } if (nc.opt_vol && nc.opt_vol_val > 0) { items.push(`Gros volume: ${nc.opt_vol_val} To supplémentaires`); } return items.length > 0 ? items.join(', ') : null; }, /** * Génère le récapitulatif HTML d'un service Web Hosting */ generateWebHostingSummary(state) { const wh = state.wh; const items = []; if (wh.storage > 0) { items.push(`${wh.storage} Go de stockage`); } if (wh.sites > 0) { items.push(`${wh.sites} ${wh.sites > 1 ? 'sites' : 'site'}`); } if (wh.opt_preinstall) { items.push(`Clé-en-main`); } return items.length > 0 ? items.join(' ') : null; }, /** * Génère le récapitulatif texte d'un service Web Hosting (pour email) */ generateWebHostingTextSummary(state) { const wh = state.wh; const items = []; if (wh.storage > 0) { items.push(`${wh.storage} Go de stockage`); } if (wh.sites > 0) { items.push(`${wh.sites} ${wh.sites > 1 ? 'sites' : 'site'}`); } if (wh.opt_preinstall) { items.push(`Pré-installation clé-en-main`); } return items.length > 0 ? items.join(', ') : null; }, /** * Génère le récapitulatif HTML d'un service Mail */ generateMailSummary(state) { const mail = state.mail; const items = []; if (mail.storage > 0) { items.push(`${mail.storage} Go de stockage`); } if (mail.users > 0) { items.push(`${mail.users} ${mail.users > 1 ? 'boîtes' : 'boîte'} mail`); } if (mail.opt_catchall) { items.push(`Catch-all`); } return items.length > 0 ? items.join(' ') : null; }, /** * Génère le récapitulatif texte d'un service Mail (pour email) */ generateMailTextSummary(state) { const mail = state.mail; const items = []; if (mail.storage > 0) { items.push(`${mail.storage} Go de stockage`); } if (mail.users > 0) { items.push(`${mail.users} ${mail.users > 1 ? 'boîtes' : 'boîte'} mail`); } if (mail.opt_catchall) { items.push(`Catch-all activé`); } return items.length > 0 ? items.join(', ') : null; }, /** * Génère le récapitulatif HTML d'un service VPS */ generateVPSSummary(state) { const vps = state.vps; const items = []; if (vps.storage > 0) { items.push(`${vps.storage} Go HDD`); } if (vps.cpu > 0) { items.push(`${vps.cpu} vCPU`); } if (vps.ram > 0) { items.push(`${vps.ram} Go RAM`); } if (vps.opt_ip4) { items.push(`IPv4 dédiée`); } return items.length > 0 ? items.join(' ') : null; }, /** * Génère le récapitulatif texte d'un service VPS (pour email) */ generateVPSTextSummary(state) { const vps = state.vps; const items = []; if (vps.storage > 0) { items.push(`${vps.storage} Go HDD`); } if (vps.cpu > 0) { items.push(`${vps.cpu} vCPU`); } if (vps.ram > 0) { items.push(`${vps.ram} Go RAM`); } if (vps.opt_ip4) { items.push(`Adresse IPv4 publique dédiée`); } return items.length > 0 ? items.join(', ') : null; }, /** * Met à jour le récapitulatif complet */ updateSummary() { const state = StateManager.getState(); const $container = $('#summary-container'); const $content = $('#summary-content'); // Vérifier s'il y a au moins un service actif const hasActiveServices = PriceEngine.isServiceActive('nc', state) || PriceEngine.isServiceActive('wh', state) || PriceEngine.isServiceActive('mail', state) || PriceEngine.isServiceActive('vps', state); if (!hasActiveServices) { $container.hide(); return; } // Générer le HTML du récapitulatif let html = ''; // Nextcloud const ncSummary = this.generateNextcloudSummary(state); if (ncSummary) { html += `