Annuaire › Code ouvert
Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.
js/calculateur.js · 6.2 Ko
/* ==========================================================================
js/calculateur.js — La « logique » du calculateur de prix de vanille
--------------------------------------------------------------------------
Ce fichier définit UNE fonction globale : calculateur().
Dans index.html, on écrit x-data="calculateur()" : Alpine appelle la
fonction, récupère l'objet retourné et le rend « réactif ».
Réactif = dès qu'une propriété change (ex. : le poids), tout ce qui en
dépend dans la page (x-text, x-show…) est recalculé automatiquement.
Pourquoi un fichier .js séparé ? Pour garder le HTML lisible. On aurait
pu écrire l'objet directement dans l'attribut x-data (voir README).
PIÈGE : ce script est chargé AVANT Alpine (voir index.html). Comme les
deux sont en « defer », ils s'exécutent dans l'ordre où ils sont écrits :
la fonction calculateur() existe donc quand Alpine démarre.
========================================================================== */
// Tableau des qualités de vanille : les prix sont FICTIFS (exemple pédagogique).
// « prixGramme » = prix en Ariary (Ar) pour 1 gramme.
const QUALITES = {
gourmet: { nom: 'Gourmet (gousses noires, 16 cm et plus)', prixGramme: 900 },
standard: { nom: 'Standard (gousses brun foncé, 14 à 16 cm)', prixGramme: 600 },
fendue: { nom: 'Fendue (idéale pour les extraits)', prixGramme: 350 }
};
// Zones de livraison : frais de base + supplément par tranche de 100 g,
// et délai estimé (en jours : minimum et maximum).
const ZONES = {
antalaha: { nom: 'Antalaha (en main propre)', base: 0, par100g: 0, jMin: 0, jMax: 1 },
sambava: { nom: 'Sambava / Vohémar', base: 4000, par100g: 300, jMin: 1, jMax: 3 },
tana: { nom: 'Antananarivo', base: 9000, par100g: 800, jMin: 3, jMax: 7 },
export: { nom: 'Étranger (colis postal)', base: 45000, par100g: 4500, jMin: 10, jMax: 21 }
};
/**
* Crée l'objet de données du calculateur.
* Tout ce qui est retourné ici est accessible dans le HTML.
*/
function calculateur() {
return {
/* ---------- 1. ÉTAT : les valeurs que l'utilisateur modifie ---------- */
// Les noms de propriétés sont librement choisis, mais doivent être
// utilisés à l'identique dans le HTML (x-model="qualite", etc.).
qualite: 'standard', // clé dans QUALITES
poids: 250, // en grammes (nombre grâce à x-model.number)
zone: 'antalaha', // clé dans ZONES
clientFidele: false, // case à cocher : remise de 5 %
cadeau: false, // case à cocher : emballage cadeau
detailOuvert: false, // sert à x-show pour afficher/cacher le détail
// On expose les objets pour que le HTML puisse boucler dessus
// (ici avec x-for dans les <select>).
qualites: QUALITES,
zones: ZONES,
/* ---------- 2. GETTERS : des valeurs CALCULÉES ----------
Un « getter » (mot-clé get) s'utilise comme une propriété simple
(this.sousTotal, sans parenthèses) mais son résultat est recalculé
à partir des autres propriétés. Alpine le surveille tout seul. */
/** Poids sûr : un champ vide donne '' ou NaN ; on le transforme en 0. */
get poidsValide() {
const p = Number(this.poids);
// Number.isFinite écarte NaN ; on refuse aussi les négatifs.
return Number.isFinite(p) && p > 0 ? Math.round(p) : 0;
},
/** Prix au gramme de la qualité choisie. */
get prixGramme() {
return QUALITES[this.qualite].prixGramme;
},
/** Prix brut : prix au gramme × nombre de grammes. */
get sousTotal() {
return this.prixGramme * this.poidsValide;
},
/** Remise « volume » : plus on achète, plus le pourcentage monte. */
get remiseVolume() {
if (this.poidsValide >= 1000) return 10;
if (this.poidsValide >= 250) return 5;
return 0;
},
/** Remise totale en % : volume + fidélité (plafonnée à 15 %). */
get remisePourcent() {
return Math.min(15, this.remiseVolume + (this.clientFidele ? 5 : 0));
},
/** Montant de la remise en Ariary. */
get remiseMontant() {
return Math.round(this.sousTotal * this.remisePourcent / 100);
},
/** Frais de livraison : base + supplément par tranche de 100 g entamée. */
get livraison() {
if (this.poidsValide === 0) return 0;
const z = ZONES[this.zone];
const tranches = Math.ceil(this.poidsValide / 100);
return z.base + z.par100g * tranches;
},
/** Emballage cadeau : 3 000 Ar si la case est cochée. */
get emballage() {
return this.cadeau ? 3000 : 0;
},
/** TOTAL À PAYER. */
get total() {
return this.sousTotal - this.remiseMontant + this.livraison + this.emballage;
},
/** Phrase d'estimation du délai de livraison. */
get delai() {
const z = ZONES[this.zone];
if (z.jMax <= 1) return 'Remise sous 24 h';
return 'Entre ' + z.jMin + ' et ' + z.jMax + ' jours';
},
/** Message d'erreur pour le champ poids ('' si tout va bien). */
get erreurPoids() {
if (this.poids === '' || this.poids === null) return 'Indiquez un poids en grammes.';
if (this.poidsValide === 0) return 'Le poids doit être supérieur à 0 g.';
if (this.poidsValide > 20000) return 'Au-delà de 20 kg, contactez-nous pour un devis.';
return '';
},
/* ---------- 3. MÉTHODES : des actions déclenchées par @click ---------- */
/** Met en forme un nombre en Ariary : 1234567 -> « 1 234 567 Ar ». */
ariary(n) {
// Intl.NumberFormat est fourni par le navigateur (rien à installer).
return new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(n) + ' Ar';
},
/** Ajoute (ou retire) des grammes avec les boutons +/-. */
ajuster(delta) {
// Si le champ est vide, poidsValide vaut 0 : on repart de zéro.
this.poids = Math.max(0, this.poidsValide + delta);
},
/** Remet toutes les valeurs de départ. */
reinitialiser() {
this.qualite = 'standard';
this.poids = 250;
this.zone = 'antalaha';
this.clientFidele = false;
this.cadeau = false;
this.detailOuvert = false;
}
};
}