'use strict'
/**
 * store.js — Boutique matériel (PAGE MODÈLE, staging, servie par le hub).
 * Objectif : valider l'EXPÉRIENCE (panier optimistic + visualisation de variantes)
 * avant tout branchement. Données démo en dur pour l'instant.
 *
 * Prochaine étape (P0.2) : remplacer DEMO_CATALOG par GET /store/catalog
 * (Items + Product Bundle d'ERPNext) et prix via apply_pricing_rule.
 *
 * Routes publiques :
 *   GET /store         → page modèle (Vue 3 via CDN, self-contained)
 *   GET /store/catalog → (à venir) catalogue réel ERPNext
 *
 * IMPORTANT : ce fichier est un template literal. NE PAS utiliser la séquence
 * dollar-accolade dans le HTML/JS interne (Vue utilise les doubles accolades).
 */
const { json } = require('./helpers')
const erp = require('./erp')

const PAGE = `<!doctype html><html lang=fr><head>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>Boutique Gigafibre</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
:root{--g:#019547;--g2:#01733a;--ink:#1a1a1a;--mut:#6b7280;--line:#e6e9ee;--bg:#f5f7fa}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--ink);line-height:1.5}
a{color:inherit}
.top{position:sticky;top:0;z-index:30;background:rgba(255,255,255,.86);backdrop-filter:saturate(180%) blur(10px);border-bottom:1px solid var(--line)}
.top .in{max-width:1080px;margin:0 auto;padding:12px 18px;display:flex;align-items:center;gap:14px}
.brand{font-weight:800;font-size:18px}.brand b{color:var(--g)}
.brand .s{color:var(--mut);font-weight:500;font-size:13px;margin-left:6px}
.cartbtn{margin-left:auto;position:relative;border:1px solid var(--line);background:#fff;border-radius:12px;padding:9px 14px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:14px;transition:.15s}
.cartbtn:hover{border-color:var(--g);box-shadow:0 2px 10px rgba(1,149,71,.12)}
.badge{position:absolute;top:-7px;right:-7px;min-width:20px;height:20px;border-radius:10px;background:#e11d48;color:#fff;font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;padding:0 5px;transition:transform .2s}
.badge.bump{transform:scale(1.45)}
.wrap{max-width:1080px;margin:0 auto;padding:18px}
.cats{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px}
.cat{border:1px solid var(--line);background:#fff;border-radius:999px;padding:7px 15px;font-size:13px;font-weight:600;color:var(--mut);cursor:pointer;transition:.15s}
.cat:hover{border-color:#cbd5e1}
.cat.on{background:var(--g);border-color:var(--g);color:#fff}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:16px}
.card{background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden;cursor:pointer;transition:transform .18s,box-shadow .18s;display:flex;flex-direction:column}
.card:hover{transform:translateY(-4px);box-shadow:0 12px 28px rgba(15,23,42,.10)}
.tile{height:140px;display:flex;align-items:center;justify-content:center;font-size:52px;position:relative;transition:background .25s}
.tile .bdg{position:absolute;top:10px;left:10px;background:rgba(255,255,255,.92);color:var(--g2);font-size:11px;font-weight:800;border-radius:8px;padding:3px 8px}
.tile .dots{position:absolute;bottom:10px;left:10px;display:flex;gap:4px}
.tile .dots i{width:14px;height:14px;border-radius:50%;border:1.5px solid rgba(255,255,255,.9);display:inline-block}
.cbody{padding:12px 14px;display:flex;flex-direction:column;gap:3px;flex:1}
.ccat{font-size:11px;color:var(--mut);text-transform:uppercase;letter-spacing:.4px;font-weight:700}
.cname{font-weight:700;font-size:14.5px;line-height:1.3}
.cprice{margin-top:auto;font-weight:800;color:var(--g);font-size:16px;padding-top:6px}
.cprice .from{color:var(--mut);font-weight:500;font-size:11px}
.cprice s{color:#9ca3af;font-weight:500;font-size:13px;margin-right:5px}
/* overlay + modal */
.ov{position:fixed;inset:0;background:rgba(15,23,42,.5);z-index:40;display:flex;align-items:center;justify-content:center;padding:16px}
.modal{background:#fff;border-radius:20px;max-width:760px;width:100%;max-height:92vh;overflow:auto;display:grid;grid-template-columns:1fr 1fr}
@media(max-width:640px){.modal{grid-template-columns:1fr}}
.mtile{min-height:300px;display:flex;align-items:center;justify-content:center;font-size:120px;transition:background .25s}
.mbody{padding:22px 24px;display:flex;flex-direction:column}
.mcat{font-size:11px;color:var(--mut);text-transform:uppercase;letter-spacing:.5px;font-weight:700}
.mname{font-size:22px;font-weight:800;margin:2px 0 4px}
.mdesc{font-size:13px;color:var(--mut);margin-bottom:14px}
.mprice{font-size:26px;font-weight:800;color:var(--g);margin-bottom:2px}
.mprice s{color:#9ca3af;font-weight:500;font-size:18px;margin-right:8px}
.stock{font-size:12.5px;font-weight:600;margin-bottom:16px}
.stock.ok{color:var(--g)}.stock.low{color:#d97706}.stock.no{color:#e11d48}
.opt{margin-bottom:16px}
.opt .lab{font-size:12.5px;font-weight:700;color:#374151;margin-bottom:7px}
.opt .lab span{color:var(--mut);font-weight:500}
.sw{display:flex;gap:10px;flex-wrap:wrap}
.swatch{width:34px;height:34px;border-radius:50%;cursor:pointer;border:2.5px solid transparent;box-shadow:0 0 0 1px var(--line) inset;position:relative;transition:.15s}
.swatch:hover{transform:scale(1.08)}
.swatch.on{border-color:var(--g);box-shadow:0 0 0 1px var(--g)}
.swatch.on::after{content:"✓";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800;color:#fff;text-shadow:0 0 3px rgba(0,0,0,.45)}
.pills{display:flex;gap:8px;flex-wrap:wrap}
.pill{border:1.5px solid var(--line);background:#fff;border-radius:10px;padding:8px 13px;font-size:13px;font-weight:600;cursor:pointer;transition:.15s;min-width:46px;text-align:center}
.pill:hover{border-color:#cbd5e1}
.pill.on{background:var(--g);border-color:var(--g);color:#fff}
.pill.dis{color:#cbd5e1;text-decoration:line-through;cursor:not-allowed;background:#fafbfc}
.pill .add{display:block;font-size:10px;font-weight:600;color:var(--mut);margin-top:1px}
.pill.on .add{color:rgba(255,255,255,.85)}
.qtyrow{display:flex;align-items:center;gap:14px;margin:6px 0 18px}
.qty{display:flex;align-items:center;border:1.5px solid var(--line);border-radius:12px;overflow:hidden}
.qty button{width:38px;height:40px;border:none;background:#fff;font-size:18px;cursor:pointer;color:var(--g);font-weight:700}
.qty button:hover{background:#f3fbf6}
.qty span{width:42px;text-align:center;font-weight:700}
.add-btn{margin-top:auto;border:none;background:var(--g);color:#fff;font-weight:800;font-size:15.5px;border-radius:14px;padding:15px;cursor:pointer;transition:.15s;display:flex;align-items:center;justify-content:center;gap:8px}
.add-btn:hover{background:var(--g2)}
.add-btn:disabled{background:#cbd5e1;cursor:not-allowed}
.add-btn.done{background:var(--g2)}
.closex{position:absolute;top:14px;right:16px;font-size:26px;color:#fff;cursor:pointer;text-shadow:0 1px 4px rgba(0,0,0,.4);z-index:2;line-height:1}
/* bundle */
.bundle{background:linear-gradient(135deg,#f3fbf6,#eefbf3)}
.comp{font-size:12.5px;color:#374151;display:flex;gap:6px;align-items:center;margin:3px 0}
.comp i{color:var(--g)}
/* drawer */
.drawer{position:fixed;top:0;right:0;height:100%;width:400px;max-width:92vw;background:#fff;z-index:50;box-shadow:-8px 0 30px rgba(15,23,42,.18);display:flex;flex-direction:column}
.dhead{padding:18px 20px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px}
.dhead h3{font-size:17px;font-weight:800}
.dhead .x{margin-left:auto;font-size:24px;color:var(--mut);cursor:pointer;line-height:1}
.ditems{flex:1;overflow:auto;padding:8px 16px}
.line{display:flex;gap:12px;padding:14px 4px;border-bottom:1px solid #f1f5f9}
.lthumb{width:54px;height:54px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:26px;flex-shrink:0}
.lmid{flex:1;min-width:0}
.lname{font-weight:700;font-size:13.5px}
.lopt{font-size:11.5px;color:var(--mut)}
.lrem{font-size:11.5px;color:#e11d48;cursor:pointer;font-weight:600;margin-top:3px;display:inline-block}
.lright{text-align:right;display:flex;flex-direction:column;align-items:flex-end;gap:6px}
.lprice{font-weight:800;font-size:14px}
.miniqty{display:flex;align-items:center;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.miniqty button{width:26px;height:26px;border:none;background:#fff;cursor:pointer;color:var(--g);font-weight:800}
.miniqty span{width:26px;text-align:center;font-size:12.5px;font-weight:700}
.empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--mut);gap:10px;padding:40px}
.empty .big{font-size:52px;opacity:.5}
.dfoot{padding:18px 20px;border-top:1px solid var(--line)}
.sub{display:flex;justify-content:space-between;font-size:14px;margin-bottom:4px}
.sub.tot{font-size:18px;font-weight:800}
.sub .mut{color:var(--mut)}
.cta{width:100%;border:none;background:var(--g);color:#fff;font-weight:800;font-size:15.5px;border-radius:14px;padding:15px;cursor:pointer;margin-top:12px;transition:.15s}
.cta:hover{background:var(--g2)}
.note{font-size:11px;color:var(--mut);text-align:center;margin-top:8px}
/* toast */
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;padding:13px 20px;border-radius:14px;font-weight:700;font-size:14px;z-index:60;box-shadow:0 10px 30px rgba(0,0,0,.25);display:flex;align-items:center;gap:9px}
.toast .c{width:22px;height:22px;border-radius:50%;background:var(--g);display:flex;align-items:center;justify-content:center;font-size:13px}
.demo{background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:12px;border-radius:10px;padding:8px 12px;margin-bottom:16px}
/* transitions */
.fade-enter-active,.fade-leave-active{transition:opacity .2s}
.fade-enter-from,.fade-leave-to{opacity:0}
.pop-enter-active{transition:transform .22s cubic-bezier(.2,1.3,.4,1),opacity .2s}
.pop-leave-active{transition:transform .15s,opacity .15s}
.pop-enter-from,.pop-leave-to{transform:scale(.92);opacity:0}
.slide-enter-active,.slide-leave-active{transition:transform .26s cubic-bezier(.3,.8,.3,1)}
.slide-enter-from,.slide-leave-to{transform:translateX(100%)}
.tup-enter-active{transition:transform .26s cubic-bezier(.2,1.2,.4,1),opacity .2s}
.tup-leave-active{transition:opacity .2s}
.tup-enter-from{transform:translateX(-50%) translateY(20px);opacity:0}
.tup-leave-to{opacity:0}
</style></head><body>
<div id=app>
  <div class=top><div class=in>
    <div class=brand>Giga<b>fibre</b><span class=s>Boutique</span></div>
    <button class=cartbtn @click="cartOpen=true">
      🛒 Panier
      <span v-if="count" class=badge :class="{bump}">{{ count }}</span>
    </button>
  </div></div>

  <div class=wrap>
    <div class=demo v-if="!live">⚙️ <b>Page modèle</b> — données démo (catalogue ERPNext vide ou indisponible). Cochez « Afficher dans la boutique » sur des Items pour les voir ici.</div>
    <div class=demo v-else style="background:#f0fdf4;border-color:#bbf7d0;color:#15803d">✓ <b>Catalogue ERPNext en direct</b> — produits cochés « Afficher dans la boutique » · prix Item Price · stock live.</div>
    <div class=cats>
      <div class=cat v-for="c in cats" :class="{on:cat===c}" @click="cat=c">{{ c }}</div>
    </div>
    <div class=grid>
      <div class=card v-for="p in shown" :key="p.id" @click="open(p)">
        <div class=tile :style="{background:tileBg(p)}">
          <span>{{ p.icon }}</span>
          <span v-if="p.bundle" class=bdg>BUNDLE</span>
          <div v-if="colorOf(p)" class=dots>
            <i v-for="v in colorOf(p).values" :key="v.v" :style="{background:v.hex}"></i>
          </div>
        </div>
        <div class=cbody>
          <div class=ccat>{{ p.cat }}</div>
          <div class=cname>{{ p.name }}</div>
          <div class=cprice>
            <template v-if="p.bundle"><s>{{ money(p.sum) }}</s>{{ money(p.price) }}</template>
            <template v-else-if="p.options"><s v-if="p.reg">{{ money(p.reg) }}</s> <span class=from>dès </span>{{ money(fromPrice(p)) }}</template>
            <template v-else><s v-if="p.reg">{{ money(p.reg) }}</s> {{ money(p.base) }}</template>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- DETAIL MODAL -->
  <transition name=fade>
  <div class=ov v-if="sel" @click.self="sel=null">
    <transition name=pop appear>
    <div class=modal>
      <div class=mtile :style="{background:tileBg(sel,true)}">
        <span class=closex @click="sel=null">×</span>
        <span>{{ sel.icon }}</span>
      </div>
      <div class=mbody>
        <div class=mcat>{{ sel.cat }}</div>
        <div class=mname>{{ sel.name }}</div>
        <div class=mdesc>{{ sel.desc }}</div>

        <div class=mprice>
          <s v-if="sel.bundle">{{ money(sel.sum) }}</s><s v-else-if="sel.reg && sel.reg > curPrice">{{ money(sel.reg) }}</s> {{ money(curPrice) }}
        </div>
        <div class=stock :class="stockClass">{{ stockLabel }}</div>

        <!-- bundle components -->
        <div v-if="sel.bundle" class=opt>
          <div class=lab>Inclus dans le kit</div>
          <div class=comp v-for="c in sel.items" :key="c.n"><i>✓</i> {{ c.q }}× {{ c.n }}</div>
        </div>

        <!-- variant options -->
        <div class=opt v-for="o in (sel.options||[])" :key="o.name">
          <div class=lab>{{ o.name }} <span v-if="o.type==='color'">· {{ picked[o.name] }}</span></div>
          <div v-if="o.type==='color'" class=sw>
            <div class=swatch v-for="v in o.values" :key="v.v"
              :class="{on:picked[o.name]===v.v}" :style="{background:v.hex}"
              :title="v.v" @click="pick(o,v)"></div>
          </div>
          <div v-else class=pills>
            <div class=pill v-for="v in o.values" :key="v.v"
              :class="{on:picked[o.name]===v.v, dis:v.stock===0}"
              @click="v.stock!==0 && pick(o,v)">
              {{ v.v }}
              <span v-if="v.add" class=add>+{{ money(v.add) }}</span>
            </div>
          </div>
        </div>

        <div class=qtyrow>
          <div class=qty>
            <button @click="qty=Math.max(1,qty-1)">−</button>
            <span>{{ qty }}</span>
            <button @click="qty++">+</button>
          </div>
          <div style="color:var(--mut);font-size:13px">Total : <b style="color:var(--ink)">{{ money(curPrice*qty) }}</b></div>
        </div>

        <button class=add-btn :class="{done:added}" :disabled="curStock===0" @click="addSel">
          <span v-if="added">✓ Ajouté</span>
          <span v-else-if="curStock===0">Rupture de stock</span>
          <span v-else>Ajouter au panier · {{ money(curPrice*qty) }}</span>
        </button>
      </div>
    </div>
    </transition>
  </div>
  </transition>

  <!-- CART DRAWER -->
  <transition name=fade>
    <div class=ov v-if="cartOpen" @click.self="cartOpen=false" style="justify-content:flex-end;padding:0"></div>
  </transition>
  <transition name=slide>
  <div class=drawer v-if="cartOpen">
    <div class=dhead>
      <h3>Votre panier</h3>
      <span v-if="count" style="color:var(--mut);font-weight:600;font-size:13px">{{ count }} article{{ count>1?'s':'' }}</span>
      <span class=x @click="cartOpen=false">×</span>
    </div>
    <div v-if="!cart.length" class=empty>
      <div class=big>🛒</div>
      <div>Votre panier est vide</div>
      <button class=cat @click="cartOpen=false">Continuer mes achats</button>
    </div>
    <div v-else class=ditems>
      <div class=line v-for="(l,i) in cart" :key="l.key">
        <div class=lthumb :style="{background:l.bg}">{{ l.icon }}</div>
        <div class=lmid>
          <div class=lname>{{ l.name }}</div>
          <div class=lopt v-if="l.opt">{{ l.opt }}</div>
          <span class=lrem @click="remove(i)">Retirer</span>
        </div>
        <div class=lright>
          <div class=lprice>{{ money(l.price*l.qty) }}</div>
          <div class=miniqty>
            <button @click="l.qty>1?l.qty--:remove(i)">−</button>
            <span>{{ l.qty }}</span>
            <button @click="l.qty++">+</button>
          </div>
        </div>
      </div>
    </div>
    <div v-if="cart.length" class=dfoot>
      <div class="sub"><span class=mut>Sous-total</span><span>{{ money(total) }}</span></div>
      <div class="sub"><span class=mut>Taxes (TPS+TVQ ~14,975 %)</span><span>{{ money(total*0.14975) }}</span></div>
      <div class="sub tot"><span>Total</span><span>{{ money(total*1.14975) }}</span></div>
      <button class=cta @click="checkout">Passer à la caisse →</button>
      <div class=note>Paiement sécurisé Stripe · livraison ou ramassage</div>
    </div>
  </div>
  </transition>

  <transition name=tup>
    <div class=toast v-if="toast"><span class=c>✓</span>{{ toast }}</div>
  </transition>
</div>

<script>
const CAT_ACCENT={'Caméras':'#eef0ff','Boosters WiFi':'#e6f6ff','Câbles réseau':'#e7f8f0','TP-Link':'#fff4e0','Bundles':'#eafaf1','Accessoires':'#f1f5f9'};
const DEMO=[
 {id:'cam4k',cat:'Caméras',icon:'📷',name:'Caméra extérieure 4K',desc:'Vision nocturne, étanche IP67, détection IA.',base:129.99,
  options:[
   {name:'Couleur',type:'color',values:[{v:'Blanc',hex:'#f3f4f6'},{v:'Noir',hex:'#1f2937'}]},
   {name:'Résolution',type:'pill',values:[{v:'2K',add:0,stock:14},{v:'4K',add:40,stock:9}]}
  ]},
 {id:'camint',cat:'Caméras',icon:'🎥',name:'Caméra intérieure 2K',desc:'Pan/tilt 360°, audio bidirectionnel.',base:79.99,
  options:[{name:'Couleur',type:'color',values:[{v:'Blanc',hex:'#f3f4f6'},{v:'Noir',hex:'#1f2937'}]}]},
 {id:'mesh',cat:'Boosters WiFi',icon:'📶',name:'Booster WiFi Mesh',desc:'Couverture sans coupure, Wi-Fi 6.',base:89.99,
  options:[{name:'Nœuds',type:'pill',values:[{v:'1',add:0,stock:20},{v:'2',add:75,stock:11},{v:'3',add:140,stock:6}]}]},
 {id:'cat6',cat:'Câbles réseau',icon:'🔌',name:'Câble Ethernet Cat6',desc:'Blindé, 10 Gbps, gaine LSZH.',base:9.99,
  options:[
   {name:'Couleur',type:'color',values:[{v:'Gris',hex:'#9ca3af'},{v:'Bleu',hex:'#2563eb'},{v:'Noir',hex:'#1f2937'}]},
   {name:'Longueur',type:'pill',values:[{v:'1 m',add:0,stock:50},{v:'3 m',add:5,stock:40},{v:'5 m',add:9,stock:22},{v:'10 m',add:18,stock:0}]}
  ]},
 {id:'router',cat:'TP-Link',icon:'📡',name:'Routeur TP-Link Wi-Fi 6',desc:'MU-MIMO, OFDMA, 4 ports Gigabit.',base:99.99,
  options:[{name:'Modèle',type:'pill',values:[{v:'AX1500',add:0,stock:15},{v:'AX3000',add:60,stock:8},{v:'AX6000',add:160,stock:3}]}]},
 {id:'poe',cat:'Accessoires',icon:'⚡',name:'Adaptateur PoE Gigabit',desc:'Alimente une caméra via le câble réseau.',base:24.99},
 {id:'kitcam',cat:'Bundles',icon:'📦',bundle:true,name:'Kit Sécurité 4 caméras + NVR',desc:'Tout pour sécuriser la maison.',price:549.99,sum:719.95,
  items:[{q:4,n:'Caméra extérieure 4K'},{q:1,n:'Enregistreur NVR 2 To'},{q:4,n:'Câble Cat6 5 m'}]},
 {id:'kitmesh',cat:'Bundles',icon:'🧰',bundle:true,name:'Mesh WiFi 3-pack + routeur',desc:'Couverture totale, prêt à brancher.',price:299.99,sum:389.96,
  items:[{q:3,n:'Booster Mesh Wi-Fi 6'},{q:1,n:'Routeur TP-Link AX3000'}]}
];

Vue.createApp({
  data(){return{cat:'Tous',sel:null,picked:{},qty:1,cart:[],cartOpen:false,toast:null,added:false,bump:false,items:DEMO,live:false,_t:0,_b:0}},
  mounted(){this.load()},
  computed:{
    cats(){return ['Tous'].concat([...new Set(this.items.map(p=>p.cat))])},
    shown(){return this.cat==='Tous'?this.items:this.items.filter(p=>p.cat===this.cat)},
    count(){return this.cart.reduce((s,l)=>s+l.qty,0)},
    total(){return this.cart.reduce((s,l)=>s+l.price*l.qty,0)},
    curPrice(){
      if(!this.sel)return 0;
      if(this.sel.bundle)return this.sel.price;
      let p=this.sel.base;(this.sel.options||[]).forEach(o=>{const v=this.valOf(o);if(v&&v.add)p+=v.add});return p;
    },
    curStock(){
      if(!this.sel)return 1;if(this.sel.bundle)return 5;
      if(!this.sel.options)return this.sel.stock!==undefined?this.sel.stock:99;
      let st=99;(this.sel.options||[]).forEach(o=>{const v=this.valOf(o);if(v&&v.stock!==undefined)st=Math.min(st,v.stock)});return st;
    },
    stockClass(){const s=this.curStock;return s===0?'no':(s<=6?'low':'ok')},
    stockLabel(){const s=this.curStock;return s===0?'Rupture de stock':(s<=6?('Plus que '+s+' en stock'):'En stock')}
  },
  methods:{
    load(){fetch('/store/catalog').then(r=>r.json()).then(d=>{if(d&&d.products&&d.products.length){this.items=d.products;this.live=true}}).catch(()=>{})},
    money(n){return (Math.round(n*100)/100).toFixed(2).replace('.',',')+' $'},
    colorOf(p){return (p.options||[]).find(o=>o.type==='color')},
    valOf(o){return o.values.find(v=>v.v===this.picked[o.name])},
    tileBg(p,big){
      const c=this.colorOf(p);
      if(c){const sel=big&&this.sel&&this.sel.id===p.id?this.picked[c.name]:c.values[0].v;const v=c.values.find(x=>x.v===sel)||c.values[0];
        return 'radial-gradient(circle at 50% 38%, '+v.hex+'33, '+(CAT_ACCENT[p.cat]||'#f1f5f9')+')';}
      return CAT_ACCENT[p.cat]||'#f1f5f9';
    },
    fromPrice(p){let m=p.base;(p.options||[]).forEach(o=>{const a=Math.min(...o.values.map(v=>v.add||0));m+=a});return m},
    open(p){
      this.sel=p;this.qty=1;this.added=false;const pk={};
      (p.options||[]).forEach(o=>{const first=o.values.find(v=>v.stock!==0)||o.values[0];pk[o.name]=first.v});
      this.picked=pk;
    },
    pick(o,v){this.picked[o.name]=v.v},
    bgFor(p){const c=this.colorOf(p);if(c){const v=c.values.find(x=>x.v===this.picked[c.name])||c.values[0];return 'radial-gradient(circle at 50% 40%, '+v.hex+'33, '+(CAT_ACCENT[p.cat]||'#f1f5f9')+')'}return CAT_ACCENT[p.cat]||'#f1f5f9'},
    addSel(){
      if(this.curStock===0)return;
      const opt=(this.sel.options||[]).map(o=>this.picked[o.name]).join(' · ');
      const key=this.sel.id+'|'+opt;
      const ex=this.cart.find(l=>l.key===key);
      if(ex)ex.qty+=this.qty;
      else this.cart.push({key,name:this.sel.name,opt,price:this.curPrice,qty:this.qty,icon:this.sel.icon,bg:this.bgFor(this.sel)});
      this.fire('Ajouté au panier');this.added=true;
      clearTimeout(this._a);this._a=setTimeout(()=>{this.added=false;this.sel=null;this.cartOpen=true},650);
    },
    remove(i){this.cart.splice(i,1)},
    fire(msg){
      this.toast=msg;this.bump=true;
      clearTimeout(this._t);this._t=setTimeout(()=>this.toast=null,1800);
      clearTimeout(this._b);this._b=setTimeout(()=>this.bump=false,220);
    },
    checkout(){this.fire('Checkout Stripe — à brancher (P0.2)')}
  }
}).mount('#app');
</script>
</body></html>`

// ── Catalogue live depuis ERPNext ──────────────────────────────────────────
// Curation = champ Item.show_in_store (case à cocher). Catégorie = item_group.
// Prix = Item Price (Standard Selling). Stock = somme Bin.actual_qty. Bundles =
// Product Bundle. Variantes mono-attribut → options additives (add + stock/valeur).
const ERP_HOST = 'https://erp.gigafibre.ca'
const PRICE_LIST = 'Standard Selling'
const COLOR_MAP = { blanc: '#f3f4f6', noir: '#1f2937', gris: '#9ca3af', bleu: '#2563eb', rouge: '#dc2626', vert: '#16a34a', jaune: '#eab308', orange: '#f97316', argent: '#cbd5e1' }
const GROUP_ICON = { 'Caméras': '📷', 'Boosters WiFi': '📶', 'Câbles réseau': '🔌', 'TP-Link': '📡', 'Accessoires': '⚡', 'Bundles': '📦' }
const round2 = n => Math.round((Number(n) || 0) * 100) / 100
const stripHtml = s => (s || '').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
const imgUrl = img => (img && img.startsWith('/files')) ? ERP_HOST + img : ''

async function priceOf (code) {
  const rows = await erp.list('Item Price', { filters: [['item_code', '=', code], ['price_list', '=', PRICE_LIST], ['selling', '=', 1]], fields: ['price_list_rate'], limit: 1 })
  return rows && rows[0] ? round2(rows[0].price_list_rate) : 0
}
async function stockOf (code) {
  const bins = await erp.list('Bin', { filters: [['item_code', '=', code]], fields: ['actual_qty'], limit: 50 })
  return Math.round((bins || []).reduce((s, b) => s + (Number(b.actual_qty) || 0), 0))
}
async function nameOf (code) {
  const it = await erp.get('Item', code, { fields: ['item_name'] })
  return (it && it.item_name) || code
}

async function buildCatalog () {
  const items = await erp.list('Item', {
    filters: [['show_in_store', '=', 1], ['disabled', '=', 0]],
    fields: ['name', 'item_name', 'item_group', 'image', 'description', 'standard_rate', 'has_variants', 'variant_of', 'store_regular_price'],
    limit: 200,
  })
  const bundles = await erp.list('Product Bundle', { fields: ['name'], limit: 200 })
  const bundleSet = new Set((bundles || []).map(b => b.name))
  const out = []
  for (const it of (items || [])) {
    if (it.variant_of) continue // les variantes enfant sont exposées via leur template
    const p = {
      id: it.name, cat: it.item_group || 'Boutique', name: it.item_name || it.name,
      desc: stripHtml(it.description), icon: GROUP_ICON[it.item_group] || '📦', image: imgUrl(it.image),
    }
    if (bundleSet.has(it.name)) {
      const pb = await erp.get('Product Bundle', it.name)
      const comps = (pb && pb.items) || []
      let sum = 0; const list = []
      for (const c of (comps || [])) {
        const q = Number(c.qty) || 1; sum += (await priceOf(c.item_code)) * q
        list.push({ q, n: await nameOf(c.item_code) })
      }
      p.bundle = true; p.sum = round2(sum); p.price = (await priceOf(it.name)) || p.sum; p.items = list
    } else if (it.has_variants) {
      const vs = await erp.list('Item', { filters: [['variant_of', '=', it.name], ['disabled', '=', 0]], fields: ['name'], limit: 100 })
      const info = []
      for (const v of (vs || [])) {
        const vd = await erp.get('Item', v.name)
        const attrs = (vd && vd.attributes) || []
        info.push({ attrs, price: await priceOf(v.name), stock: await stockOf(v.name) })
      }
      const prices = info.map(v => v.price).filter(n => n > 0)
      const baseP = prices.length ? Math.min(...prices) : (round2(it.standard_rate) || 0)
      p.base = baseP
      const rg = round2(it.store_regular_price); if (rg > baseP) p.reg = rg
      const attrNames = [...new Set(info.flatMap(v => v.attrs.map(a => a.attribute)))]
      p.options = attrNames.map(an => {
        const isColor = /couleur|color/i.test(an)
        const vals = [...new Set(info.flatMap(v => v.attrs.filter(a => a.attribute === an).map(a => a.attribute_value)))]
        return {
          name: an, type: isColor ? 'color' : 'pill',
          values: vals.map(val => {
            const match = info.filter(v => v.attrs.some(a => a.attribute === an && a.attribute_value === val))
            const mp = match.map(v => v.price).filter(n => n > 0)
            const o = { v: val, add: round2((mp.length ? Math.min(...mp) : baseP) - baseP), stock: match.reduce((s, v) => s + v.stock, 0) }
            if (isColor) o.hex = COLOR_MAP[String(val).toLowerCase()] || '#9ca3af'
            return o
          }),
        }
      })
    } else {
      p.base = (await priceOf(it.name)) || round2(it.standard_rate) || 0
      p.stock = await stockOf(it.name)
      const rg = round2(it.store_regular_price); if (rg > p.base) p.reg = rg
    }
    out.push(p)
  }
  return out
}

async function handle (req, res, method, path) {
  if (path === '/store' && method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
    return res.end(PAGE)
  }
  if (path === '/store/catalog' && method === 'GET') {
    try { return json(res, 200, { products: await buildCatalog() }) }
    catch (e) { return json(res, 200, { products: [], error: e.message }) }
  }
  return json(res, 404, { error: 'not found' })
}

module.exports = { handle }
