◆ Portail Antalaha

Annuaire › Code ouvert

Calculateur de prix de vanille (Alpine.js)

tutorial · Voir le site ↗
Connectez-vous pour copier ce site

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;
    }
  };
}