Files
website/assets/calc.js

1489 lines
49 KiB
JavaScript

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: 17,
factor: 1.131
},
// dedicated: {
// basePrice: 64,
// factor: 1.753
// },
// onprem: {
// basePrice: 111,
// factor: 1.498
// },
vol: [45,70,95,110]
}
},
wh: {
storage: storage_price,
sites: [2.6,3.7,4.9,6.1],
options: {
preinstall: {
basePrice: 2.773,
factor: 1.168
}
}
},
mail: {
storage: storage_price,
users: [2.1,4.2,7.3,9.4],
options: {
catchall: {
basePrice: 2.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, 125, 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: 250,
step: 1,
restrictedLabels: [0, 5, 10, 25, 50, 75, 125, 250]
},
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(40, 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(`<span class="ui label teal">${nc.storage} Go de stockage</span>`);
}
if (!nc.opt_lxc && nc.users > 0) {
items.push(`<span class="ui label teal">${nc.users} utilisateurs</span>`);
}
if (nc.opt_lxc) {
items.push(`<span class="ui label blue">Instance privée</span>`);
items.push(`<span class="ui label">Utilisateurs illimités</span>`);
}
// if (nc.opt_dedicated) {
// items.push(`<span class="ui label blue">Serveur dédié</span>`);
// }
// if (nc.opt_onprem) {
// items.push(`<span class="ui label blue">Sur site</span>`);
// }
if (nc.opt_vol && nc.opt_vol_val > 0) {
items.push(`<span class="ui label violet">${nc.opt_vol_val} To de volume supplémentaire</span>`);
}
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(`<span class="ui label teal">${wh.storage} Go de stockage</span>`);
}
if (wh.sites > 0) {
items.push(`<span class="ui label teal">${wh.sites} ${wh.sites > 1 ? 'sites' : 'site'}</span>`);
}
if (wh.opt_preinstall) {
items.push(`<span class="ui label blue">Clé-en-main</span>`);
}
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(`<span class="ui label teal">${mail.storage} Go de stockage</span>`);
}
if (mail.users > 0) {
items.push(`<span class="ui label teal">${mail.users} ${mail.users > 1 ? 'boîtes' : 'boîte'} mail</span>`);
}
if (mail.opt_catchall) {
items.push(`<span class="ui label blue">Catch-all</span>`);
}
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(`<span class="ui label teal">${vps.storage} Go HDD</span>`);
}
if (vps.cpu > 0) {
items.push(`<span class="ui label teal">${vps.cpu} vCPU</span>`);
}
if (vps.ram > 0) {
items.push(`<span class="ui label teal">${vps.ram} Go RAM</span>`);
}
if (vps.opt_ip4) {
items.push(`<span class="ui label blue">IPv4 dédiée</span>`);
}
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 += `
<div class="item">
<!-- <i class="large cloud middle aligned icon"></i>-->
<div class="content">
<div class="header">Nextcloud</div>
<div class="description">${ncSummary}</div>
</div>
</div>
`;
}
// Web Hosting
const whSummary = this.generateWebHostingSummary(state);
if (whSummary) {
html += `
<div class="item">
<!-- <i class="large globe middle aligned icon"></i>-->
<div class="content">
<div class="header">Hébergement Web</div>
<div class="description">${whSummary}</div>
</div>
</div>
`;
}
// Mail
const mailSummary = this.generateMailSummary(state);
if (mailSummary) {
html += `
<div class="item">
<!-- <i class="large envelope middle aligned icon"></i>-->
<div class="content">
<div class="header">Hébergement Mail</div>
<div class="description">${mailSummary}</div>
</div>
</div>
`;
}
// VPS
const vpsSummary = this.generateVPSSummary(state);
if (vpsSummary) {
html += `
<div class="item">
<!-- <i class="large server middle aligned icon"></i>-->
<div class="content">
<div class="header">VPS</div>
<div class="description">${vpsSummary}</div>
</div>
</div>
`;
}
$content.html(html);
$container.show();
},
/**
* Génère le corps de l'email de commande
*/
generateOrderEmailBody() {
const state = StateManager.getState();
const priceResults = PriceEngine.calculateAllPrices(state);
const shareURL = URLSync.getShareableURL();
let body = 'Bonjour,\n\n';
body += 'Je souhaite commander l\'offre suivante sélectionnée sur le site clawd.fr :\n\n';
// Nextcloud
const ncText = this.generateNextcloudTextSummary(state);
if (ncText) {
body += 'Nextcloud: ' + ncText + '\n';
}
// Web
const whText = this.generateWebHostingTextSummary(state);
if (whText) {
body += 'Web: ' + whText + '\n';
}
// Mail
const mailText = this.generateMailTextSummary(state);
if (mailText) {
body += 'Mail: ' + mailText + '\n';
}
// VPS
const vpsText = this.generateVPSTextSummary(state);
if (vpsText) {
body += 'VPS: ' + vpsText + '\n';
}
body += '\n';
body += 'Pour ces services, le calculateur a indiqué la fourchette suivante :\n';
body += ` - Minimum : ${priceResults[0].total}€/mois\n`;
body += ` - Correct : ${priceResults[1].total}€/mois\n`;
body += ` - Généreux : ${priceResults[2].total}€/mois\n`;
body += ` - Maximum : ${priceResults[3].total}€/mois\n\n`;
body += 'Fourchette dans laquelle je choisis le tarif suivant : [à compléter]\n\n';
body += 'Informations complémentaires :\n';
body += 'Je représente : [une entreprise/association/famille/seulement moi-même]\n';
body += 'Nom / Raison sociale : [à compléter]\n';
body += 'Je dispose déjà d\'un nom de domaine : [à compléter]\n';
body += 'Date de mise en service souhaitée : [à compléter]\n';
body += 'Commentaires ou demandes spécifiques : [à compléter]\n\n';
body += 'Lien de configuration : ' + shareURL + '\n\n'
body += 'Cordialement,\n';
body += '[Votre nom]';
return body;
},
/**
* Ouvre le client mail avec l'email pré-rempli
*/
openOrderEmail() {
const subject = encodeURIComponent('Commande depuis le calculateur clawd.fr');
const body = encodeURIComponent(this.generateOrderEmailBody());
const mailto = `mailto:contact@clawd.fr?subject=${subject}&body=${body}`;
window.location.href = mailto;
}
};
// ============================================================================
// PHASE 6: Example Formulas - Formules d'exemples pré-définies
// ============================================================================
const ExampleFormulas = {
formulas: {
solo: {
nc: { storage: 50, users: 1, 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: 20, users: 1, opt_catchall: true },
vps: { storage: 0, cpu: 0, ram: 0, opt_ip4: false }
},
team: {
nc: { storage: 150, users: 5, opt_lxc: false, /* opt_dedicated: false, opt_onprem: false, */ opt_vol: false, opt_vol_val: 1 },
wh: { storage: 10, sites: 1, opt_preinstall: false },
mail: { storage: 25, users: 5, opt_catchall: false },
vps: { storage: 0, cpu: 0, ram: 0, opt_ip4: false }
},
company: {
nc: { storage: 0, users: 0, opt_lxc: true, /* opt_dedicated: false, opt_onprem: false, */ opt_vol: true, opt_vol_val: 2 },
wh: { storage: 50, sites: 3, opt_preinstall: false },
mail: { storage: 100, users: 20, opt_catchall: false },
vps: { storage: 0, cpu: 0, ram: 0, opt_ip4: false }
},
storage: {
nc: { storage: 0, users: 0, opt_lxc: true, /* opt_dedicated: true, opt_onprem: false, */ opt_vol: true, opt_vol_val: 10 },
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 }
},
developer: {
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: 50, cpu: 2, ram: 4, opt_ip4: false }
},
devteam: {
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: 250, cpu: 8, ram: 16, opt_ip4: false }
}
},
/**
* Charge une formule d'exemple
*/
loadFormula(formulaName) {
const formula = this.formulas[formulaName];
if (!formula) return;
// Mettre à jour l'état
StateManager.setState(formula);
// Synchroniser le DOM
UIController.syncDOMFromState();
// Mettre à jour l'URL
URLSync.updateURL();
// Mettre à jour le récapitulatif
SummaryGenerator.updateSummary();
// Scroller vers le calculateur
$('html, body').animate({
scrollTop: $('#calc').offset().top - 100
}, 500);
}
};
// ============================================================================
// Initialisation de l'application
// ============================================================================
$(document).ready(function() {
// Initialiser tous les contrôles UI
UIController.initialize();
// Phase 4: Charger l'état depuis l'URL si présent
const loadedFromURL = URLSync.loadFromURL();
// Affichage initial des prix (seulement si pas chargé depuis URL)
if (!loadedFromURL) {
UIController.updateDisplay();
}
// Phase 5: Initialiser le récapitulatif
SummaryGenerator.updateSummary();
// Phase 5: Bouton pour copier l'URL
$('#copy-url-btn').on('click', function() {
URLSync.copyShareableURL().then(success => {
if (success) {
$('#copy-notification').fadeIn().delay(2000).fadeOut();
}
});
});
// Phase 5: Bouton Commander
$('#order-btn').on('click', function() {
SummaryGenerator.openOrderEmail();
});
// Phase 6: Formules d'exemples
$('.example-formula').on('click', function(e) {
e.preventDefault();
const formula = $(this).data('formula');
ExampleFormulas.loadFormula(formula);
});
});