/* GarageWiz — Openers catalog + Parts list */
(function () {
  const e = React.createElement;
  const { useState } = React;
  const Icon = window.Icon, Btn = window.Btn, Badge = window.Badge, Segmented = window.Segmented, SectionTitle = window.SectionTitle,
        Field = window.Field, Input = window.Input;

  const typeIcon = { Belt: 'opener', Chain: 'opener', Jackshaft: 'settings' };

  // shared estimate-ticket PDF (used by Openers + Parts standalone sales)
  function saleTicketPdf(result, cust, app, opts) {
    const ns = window.jspdf;
    if (!ns || !ns.jsPDF) { app.toast('PDF tool still loading — try again in a moment.'); return; }
    const doc = new ns.jsPDF({ unit: 'pt', format: 'letter' });
    const W = doc.internal.pageSize.getWidth(), M = 48;
    doc.setFillColor(0, 95, 122); doc.rect(0, 0, W, 96, 'F');
    doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(26); doc.text('GarageWiz', M, 50);
    doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(245, 224, 65); doc.text(opts.kicker, M, 68);
    doc.setTextColor(255, 255, 255); doc.setFontSize(11); doc.text(opts.title, W - M, 44, { align: 'right' });
    doc.setFontSize(10); doc.text(new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), W - M, 62, { align: 'right' });
    let y = 134; doc.setTextColor(25, 25, 25);
    if (cust.name || cust.phone || cust.email) {
      doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text('PREPARED FOR', M, y); y += 18;
      doc.setTextColor(25, 25, 25); doc.setFontSize(15); if (cust.name) { doc.text(cust.name, M, y); y += 18; }
      const contact = [cust.phone, cust.email].filter(Boolean).join('   ·   ');
      if (contact) { doc.setFont('helvetica', 'normal'); doc.setFontSize(11); doc.setTextColor(110, 110, 110); doc.text(contact, M, y); y += 14; }
      y += 14;
    }
    doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text(opts.section, M, y); y += 8;
    doc.setDrawColor(225, 225, 225); doc.line(M, y, W - M, y); y += 22;
    result.lines.forEach(l => {
      doc.setFont('helvetica', 'bold'); doc.setFontSize(12.5); doc.setTextColor(25, 25, 25);
      doc.text((l.qty > 1 ? l.qty + '× ' : '') + String(l.name), M, y);
      doc.text(GW.fmt(l.linePrice), W - M, y, { align: 'right' });
      if (l.desc) { doc.setFont('helvetica', 'normal'); doc.setFontSize(10.5); doc.setTextColor(130, 130, 130); doc.text(String(l.desc), M, y + 15); y += 34; } else { y += 24; }
      doc.setDrawColor(238, 238, 238); doc.line(M, y - 10, W - M, y - 10);
    });
    y += 16;
    doc.setFillColor(0, 95, 122); doc.roundedRect(M, y, W - M * 2, 78, 8, 8, 'F');
    doc.setTextColor(245, 224, 65); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.text('TOTAL · INSTALLED', M + 22, y + 30);
    doc.setTextColor(255, 255, 255); doc.setFontSize(30); doc.text(GW.fmt(result.price), W - M - 22, y + 44, { align: 'right' });
    doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(220, 235, 240); doc.text('Includes professional installation and workmanship guarantee.', M + 22, y + 56);
    y += 100;
    if (result.discount > 0) { doc.setFont('helvetica', 'bold'); doc.setFontSize(10.5); doc.setTextColor(34, 150, 94); doc.text((result.coupon ? ('Coupon ' + result.coupon.code + ': ') : 'Discount: ') + '−' + GW.fmt(result.discount) + ' applied.', M, y); y += 22; }
    const sig = opts.signature;
    if (sig && sig.dataUrl) {
      doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text('CUSTOMER APPROVAL', M, y); y += 6;
      doc.setDrawColor(225, 225, 225); doc.line(M, y, W - M, y); y += 10;
      try { doc.addImage(sig.dataUrl, 'PNG', M, y, 200, 64); } catch (e) {}
      doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(25, 25, 25);
      doc.text('Approved by ' + (sig.name || 'Customer'), M, y + 80);
      doc.setTextColor(130, 130, 130); doc.text('Signed ' + new Date(sig.signedAt || Date.now()).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), M, y + 95);
      y += 110;
    }
    const safeName = (cust.name || opts.file).replace(/[^a-z0-9]+/gi, '-');
    doc.save(safeName + '.pdf'); app.toast('Estimate downloaded');
  }
  window.saleTicketPdf = saleTicketPdf;

  // shared coupon + customer + total panel for standalone sale tickets
  function SaleTicketPanel({ result, cust, setCust, code, setCode, cpErr, applyCoupon, removeCoupon, onClear, onPdf, onAddToEstimate, emptyMsg, signature, onSign, onUnsign }) {
    return e('div', { className: 'parts-ticket', style: { position: 'sticky', top: 88, display: 'flex', flexDirection: 'column', gap: 14 } },
      e('div', { className: 'card', style: { padding: 20 } },
        e('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 } },
          e('div', { style: { width: 36, height: 36, borderRadius: 10, background: 'var(--field)', color: 'var(--teal)', display: 'grid', placeItems: 'center' } }, e(Icon, { name: 'tag', size: 20 })),
          e('div', { className: 'display', style: { fontSize: 18, fontWeight: 600 } }, 'Estimate'),
        ),
        result.lines.length === 0
          ? e('div', { className: 'muted', style: { fontSize: 13.5, padding: '10px 0 4px' } }, emptyMsg)
          : e('div', { style: { display: 'flex', flexDirection: 'column' } },
            result.lines.map((l, i) => e('div', { key: l.id, style: { display: 'flex', alignItems: 'baseline', gap: 10, padding: '9px 0', borderTop: i ? '1px dashed var(--line)' : 'none' } },
              e('div', { className: 'grow', style: { minWidth: 0 } }, e('div', { style: { fontWeight: 700, fontSize: 14 } }, (l.qty > 1 ? l.qty + '× ' : '') + l.name)),
              e('div', { className: 'num', style: { fontWeight: 700, color: 'var(--ink)' } }, GW.fmt(l.linePrice)),
            )),
          ),
        result.discount > 0 ? e('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, background: 'var(--good-bg)', color: 'var(--good)', borderRadius: 10, padding: '9px 12px', fontWeight: 700, fontSize: 13 } },
          e(Icon, { name: 'tag', size: 15 }), (result.coupon ? result.coupon.code + ' · ' : '') + '−' + GW.fmt(result.discount)) : null,
        e('div', { style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--line)' } },
          e('div', { className: 'eyebrow' }, 'Total · installed'),
          e('div', { style: { textAlign: 'right' } },
            result.discount > 0 ? e('div', { className: 'num', style: { fontSize: 15, color: 'var(--ink-3)', textDecoration: 'line-through' } }, GW.fmt(result.listPrice)) : null,
            e('div', { className: 'display num', style: { fontSize: 30, fontWeight: 700, color: 'var(--teal)', lineHeight: 1 } }, GW.fmt(result.price))),
        ),
      ),
      result.lines.length ? e('div', { className: 'card', style: { padding: 18 } },
        e('div', { style: { fontSize: 12, fontWeight: 800, letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 7 } }, e(Icon, { name: 'tag', size: 15 }), 'Coupon code'),
        result.couponCode
          ? e('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
              e('div', { className: 'grow' },
                e('div', { style: { fontWeight: 800, fontFamily: 'var(--display)', letterSpacing: '.05em', fontSize: 16, color: 'var(--good)' } }, result.couponCode),
                e('div', { className: 'muted', style: { fontSize: 12.5 } }, result.coupon ? (GW.couponLabel(result.coupon) + ' · −' + GW.fmt(result.discount) + ' applied') : 'Applied')),
              e(Btn, { variant: 'ghost', size: 'sm', onClick: removeCoupon }, 'Remove'))
          : e('div', null,
              e('div', { style: { display: 'flex', gap: 8 } },
                e('input', { value: code, onChange: ev => { setCode(ev.target.value.toUpperCase()); }, onKeyDown: ev => ev.key === 'Enter' && applyCoupon(), placeholder: 'Enter approved code', style: { flex: 1, minWidth: 0, padding: '10px 12px', borderRadius: 10, border: '1.5px solid var(--line-2)', fontWeight: 700, letterSpacing: '.05em', textTransform: 'uppercase', outline: 'none', fontFamily: 'var(--display)' } }),
                e(Btn, { variant: 'teal', size: 'sm', onClick: applyCoupon }, 'Apply')),
              cpErr ? e('div', { style: { color: 'var(--danger)', fontSize: 12.5, fontWeight: 600, marginTop: 8 } }, cpErr) : null,
            ),
      ) : null,
      result.lines.length ? e('div', { className: 'card', style: { padding: 20, display: 'flex', flexDirection: 'column', gap: 12 } },
        e('div', { className: 'eyebrow' }, 'Customer (optional)'),
        e(Field, { label: 'Name' }, e(Input, { placeholder: 'e.g. The Hendersons', value: cust.name, onChange: ev => setCust(c => ({ ...c, name: ev.target.value })) })),
        e('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 } },
          e(Field, { label: 'Phone' }, e(Input, { placeholder: '(555) 123-4567', value: cust.phone, onChange: ev => setCust(c => ({ ...c, phone: ev.target.value })) })),
          e(Field, { label: 'Email' }, e(Input, { placeholder: 'name@email.com', value: cust.email, onChange: ev => setCust(c => ({ ...c, email: ev.target.value })) })),
        ),
        onAddToEstimate ? e(Btn, { variant: 'primary', block: true, icon: 'plus', onClick: onAddToEstimate }, 'Add to estimate') : null,
        e(Btn, { variant: onAddToEstimate ? 'ghost' : 'primary', block: true, icon: 'download', onClick: onPdf }, 'Download estimate PDF'),
        e('button', { onClick: onClear, style: { border: 'none', background: 'transparent', color: 'var(--ink-3)', fontWeight: 600, fontSize: 13, cursor: 'pointer', padding: 4 } }, 'Clear estimate'),
      ) : null,
      result.lines.length && onSign ? e(window.ApprovalBlock, { signature, onApprove: onSign, onClear: onUnsign }) : null,
    );
  }
  window.SaleTicketPanel = SaleTicketPanel;

  /* ===================== OPENERS (standalone estimate) ===================== */
  function Openers() {
    const app = window.useApp();
    const [rail, setRail] = useState(7);
    const [brand, setBrand] = useState('All');
    const [openers, setOpeners] = useState({});
    const [cust, setCust] = useState({ name: '', phone: '', email: '' });
    const [couponCode, setCouponCode] = useState(null);
    const [code, setCode] = useState('');
    const [cpErr, setCpErr] = useState('');
    const [signature, setSignature] = useState(null);
    const dis = app.disabledMfrs || {};
    const brands = ['All'].concat(GW.manufacturers.filter(m => !dis[m]));
    const list = GW.openers.filter(o => o.enabled && !dis[o.brand] && (brand === 'All' || o.brand === brand));
    const setOpenerQty = (id, qty) => setOpeners(m => { const n = Object.assign({}, m); if (qty > 0) n[id] = qty; else delete n[id]; return n; });
    const result = GW.computeParts({ openers, rail, couponCode }, app.settings);
    const applyCoupon = () => { const cp = GW.findCoupon(code); if (!cp) { setCpErr('Invalid or inactive code.'); return; } setCpErr(''); setCouponCode(cp.code); };
    const removeCoupon = () => { setCode(''); setCpErr(''); setCouponCode(null); };

    function OStepper({ id }) {
      const qty = openers[id] || 0;
      if (!qty) return e(Btn, { variant: 'teal', size: 'sm', icon: 'plus', onClick: () => setOpenerQty(id, 1) }, 'Add');
      const sq = { border: 'none', background: 'var(--field)', borderRadius: 8, width: 30, height: 30, display: 'grid', placeItems: 'center', cursor: 'pointer', color: 'var(--ink)' };
      return e('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
        e('button', { onClick: () => setOpenerQty(id, qty - 1), style: sq }, e(Icon, { name: 'chevD', size: 16 })),
        e('span', { className: 'num', style: { minWidth: 22, textAlign: 'center', fontWeight: 700 } }, qty),
        e('button', { onClick: () => setOpenerQty(id, qty + 1), style: sq }, e(Icon, { name: 'plus', size: 16 })),
      );
    }

    return e('div', { style: { maxWidth: 1320, margin: '0 auto', padding: '34px 28px 80px' } },
      e(SectionTitle, {
        eyebrow: 'Openers', title: 'Opener sale',
        sub: 'Standalone opener estimate — no door needed. Add openers, apply a coupon, and hand over a printed estimate.',
        right: e(Segmented, { value: rail, onChange: setRail, options: [{ value: 7, label: '7ft rail' }, { value: 8, label: '8ft rail' }] }),
      }),
      e('div', { className: 'parts-sale-grid', style: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 360px', gap: 24, alignItems: 'start' } },
        e('div', null,
          e('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 18 } },
            brands.map(b => e('button', { key: b, onClick: () => setBrand(b), className: 'display', style: { border: 'none', cursor: 'pointer', borderRadius: 999, padding: '8px 16px', fontWeight: 600, fontSize: 14.5, background: brand === b ? 'var(--teal)' : '#fff', color: brand === b ? '#fff' : 'var(--ink-2)', boxShadow: brand === b ? '0 3px 8px rgba(0,95,122,.3)' : 'var(--sh-1)' } }, b)),
          ),
          e('div', { className: 'opener-grid', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 } },
            list.map(o => {
              const single = o.single != null;
              const sell = GW.openerCost(o, rail);
              const avail = single || !(rail === 8 && o.cost8 == null);
              const p = GW.priceFromSell(sell, app.settings, 'opener', o.override);
              const on = (openers[o.id] || 0) > 0;
              return e('div', { key: o.id, className: 'card', style: { padding: 20, display: 'flex', flexDirection: 'column', gap: 12, opacity: avail ? 1 : .45, border: on ? '2px solid var(--good)' : '1px solid var(--line)' } },
                e('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 12 } },
                  e('div', { style: { width: 46, height: 46, borderRadius: 13, background: 'var(--field)', color: 'var(--teal)', display: 'grid', placeItems: 'center', flexShrink: 0 } }, e(Icon, { name: typeIcon[o.type] || 'opener', size: 24 })),
                  e('div', { className: 'grow' },
                    e('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, e('div', { className: 'display', style: { fontSize: 18, fontWeight: 600 } }, o.brand), e('span', { className: 'badge badge-start' }, o.type)),
                    e('div', { className: 'muted', style: { fontSize: 12.5, fontWeight: 700, letterSpacing: '.03em' } }, 'Model ' + o.id),
                  ),
                ),
                e('div', { className: 'muted', style: { fontSize: 13.5, lineHeight: 1.4, flexGrow: 1 } }, o.desc + (single ? ' · single price (no rail)' : '')),
                e('div', { style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', borderTop: '1px solid var(--line)', paddingTop: 12 } },
                  e('div', null, e('div', { className: 'eyebrow' }, 'Installed'), e('div', { className: 'display num', style: { fontSize: 24, fontWeight: 700, color: 'var(--teal)' } }, avail ? GW.fmt(p.price) : '—')),
                  avail ? e(OStepper, { id: o.id }) : null,
                ),
              );
            }),
          ),
        ),
        e(SaleTicketPanel, {
          result, cust, setCust, code, setCode, cpErr, applyCoupon, removeCoupon,
          emptyMsg: 'Add an opener from the catalog to build the estimate.',
          signature, onSign: setSignature, onUnsign: () => setSignature(null),
          onClear: () => { setOpeners({}); removeCoupon(); setSignature(null); },
          onPdf: () => saleTicketPdf(result, cust, app, { kicker: 'OPENER SALE', title: 'Opener estimate', section: 'OPENERS', file: 'GarageWiz-Openers', signature }),
          onAddToEstimate: () => { Object.keys(openers).forEach(id => app.addToEstimate({ type: 'opener', openerId: id, rail, qty: openers[id] })); setOpeners({}); removeCoupon(); setSignature(null); app.toast('Added to estimate'); app.go('#/estimate'); },
        }),
      ),
    );
  }
  window.Openers = Openers;

  /* ===================== PARTS — repair / service ticket builder ===================== */
  function Parts() {
    const app = window.useApp();
    const [q, setQ] = useState('');
    const [items, setItems] = useState({});           // { partId: qty }
    const [cust, setCust] = useState({ name: '', phone: '', email: '' });
    const [couponCode, setCouponCode] = useState(null);
    const [code, setCode] = useState('');
    const [cpErr, setCpErr] = useState('');
    const [signature, setSignature] = useState(null);

    const cats = Array.from(new Set(GW.parts.filter(p => p.enabled).map(p => p.cat)));
    const filtered = GW.parts.filter(p => p.enabled && (p.name.toLowerCase().includes(q.toLowerCase()) || p.cat.toLowerCase().includes(q.toLowerCase())));
    const setQty = (id, qty) => setItems(m => { const n = Object.assign({}, m); if (qty > 0) n[id] = qty; else delete n[id]; return n; });
    const result = GW.computeParts({ items, couponCode }, app.settings);

    const applyCoupon = () => { const cp = GW.findCoupon(code); if (!cp) { setCpErr('Invalid or inactive code.'); return; } setCpErr(''); setCouponCode(cp.code); };
    const removeCoupon = () => { setCode(''); setCpErr(''); setCouponCode(null); };

    // parts-target bundles available to suggest
    const partBundles = (GW.bundles || []).filter(b => b.enabled !== false && (b.target === 'parts' || b.target === 'any'));
    const addBundle = (b) => setItems(m => { const n = Object.assign({}, m); b.items.forEach(id => { if (GW.resolveItem(id) && GW.parts.find(p => p.id === id)) n[id] = Math.max(1, n[id] || 0); }); return n; });

    function Stepper({ qty, onSet }) {
      if (!qty) return e(Btn, { variant: 'teal', size: 'sm', icon: 'plus', onClick: () => onSet(1) }, 'Add');
      const sq = { border: 'none', background: 'var(--field)', borderRadius: 8, width: 30, height: 30, display: 'grid', placeItems: 'center', cursor: 'pointer', color: 'var(--ink)' };
      return e('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
        e('button', { onClick: () => onSet(qty - 1), style: sq }, e(Icon, { name: 'chevD', size: 16 })),
        e('span', { className: 'num', style: { minWidth: 22, textAlign: 'center', fontWeight: 700 } }, qty),
        e('button', { onClick: () => onSet(qty + 1), style: sq }, e(Icon, { name: 'plus', size: 16 })),
      );
    }

    function downloadPdf() {
      const ns = window.jspdf;
      if (!ns || !ns.jsPDF) { app.toast('PDF tool still loading — try again in a moment.'); return; }
      const doc = new ns.jsPDF({ unit: 'pt', format: 'letter' });
      const W = doc.internal.pageSize.getWidth(), M = 48;
      doc.setFillColor(0, 95, 122); doc.rect(0, 0, W, 96, 'F');
      doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(26); doc.text('GarageWiz', M, 50);
      doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(245, 224, 65); doc.text('SERVICE & REPAIR', M, 68);
      doc.setTextColor(255, 255, 255); doc.setFontSize(11); doc.text('Repair estimate', W - M, 44, { align: 'right' });
      doc.setFontSize(10); doc.text(new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), W - M, 62, { align: 'right' });
      let y = 134; doc.setTextColor(25, 25, 25);
      if (cust.name || cust.phone || cust.email) {
        doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text('PREPARED FOR', M, y); y += 18;
        doc.setTextColor(25, 25, 25); doc.setFontSize(15); if (cust.name) { doc.text(cust.name, M, y); y += 18; }
        const contact = [cust.phone, cust.email].filter(Boolean).join('   ·   ');
        if (contact) { doc.setFont('helvetica', 'normal'); doc.setFontSize(11); doc.setTextColor(110, 110, 110); doc.text(contact, M, y); y += 14; }
        y += 14;
      }
      doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text('PARTS & LABOR', M, y); y += 8;
      doc.setDrawColor(225, 225, 225); doc.line(M, y, W - M, y); y += 22;
      result.lines.forEach(l => {
        doc.setFont('helvetica', 'bold'); doc.setFontSize(12.5); doc.setTextColor(25, 25, 25);
        doc.text((l.qty > 1 ? l.qty + '× ' : '') + String(l.name), M, y);
        doc.text(GW.fmt(l.linePrice), W - M, y, { align: 'right' });
        if (l.desc) { doc.setFont('helvetica', 'normal'); doc.setFontSize(10.5); doc.setTextColor(130, 130, 130); doc.text(String(l.desc), M, y + 15); y += 34; } else { y += 24; }
        doc.setDrawColor(238, 238, 238); doc.line(M, y - 10, W - M, y - 10);
      });
      y += 16;
      doc.setFillColor(0, 95, 122); doc.roundedRect(M, y, W - M * 2, 78, 8, 8, 'F');
      doc.setTextColor(245, 224, 65); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.text('TOTAL · INSTALLED', M + 22, y + 30);
      doc.setTextColor(255, 255, 255); doc.setFontSize(30); doc.text(GW.fmt(result.price), W - M - 22, y + 44, { align: 'right' });
      doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(220, 235, 240); doc.text('Includes professional installation and workmanship guarantee.', M + 22, y + 56);
      y += 100;
      if (result.bundleSavings > 0) { doc.setFont('helvetica', 'bold'); doc.setFontSize(10.5); doc.setTextColor(34, 150, 94); doc.text('You saved ' + GW.fmt(result.bundleSavings) + ' with package pricing.', M, y); y += 22; }
      if (result.discount > 0) { doc.setFont('helvetica', 'bold'); doc.setFontSize(10.5); doc.setTextColor(34, 150, 94); doc.text((result.coupon ? ('Coupon ' + result.coupon.code + ': ') : 'Discount: ') + '−' + GW.fmt(result.discount) + ' applied.', M, y); y += 22; }
      if (signature && signature.dataUrl) {
        doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(120, 120, 120); doc.text('CUSTOMER APPROVAL', M, y); y += 6;
        doc.setDrawColor(225, 225, 225); doc.line(M, y, W - M, y); y += 10;
        try { doc.addImage(signature.dataUrl, 'PNG', M, y, 200, 64); } catch (e) {}
        doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(25, 25, 25); doc.text('Approved by ' + (signature.name || 'Customer'), M, y + 80);
        doc.setTextColor(130, 130, 130); doc.text('Signed ' + new Date(signature.signedAt || Date.now()).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), M, y + 95); y += 110;
      }
      const safeName = (cust.name || 'GarageWiz-Repair').replace(/[^a-z0-9]+/gi, '-');
      doc.save(safeName + '.pdf'); app.toast('Repair estimate downloaded');
    }

    return e('div', { style: { maxWidth: 1320, margin: '0 auto', padding: '34px 28px 80px' } },
      e(SectionTitle, {
        eyebrow: 'Parts', title: 'Repair & parts sale',
        sub: 'Build a no-door service ticket — add parts and a package to upsell, then hand the customer a printed estimate.',
        right: e('div', { style: { position: 'relative' } },
          e('span', { style: { position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)', color: 'var(--ink-3)' } }, e(Icon, { name: 'search', size: 18 })),
          e('input', { placeholder: 'Search parts…', value: q, onChange: ev => setQ(ev.target.value), style: { padding: '11px 14px 11px 42px', borderRadius: 999, border: '1.5px solid var(--line-2)', background: '#fff', fontSize: 15, width: 240, outline: 'none' } }),
        ),
      }),

      e('div', { className: 'parts-sale-grid', style: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 360px', gap: 24, alignItems: 'start' } },
        // ---- left: catalog ----
        e('div', null,
          partBundles.length ? e('div', { className: 'card', style: { padding: 18, marginBottom: 18 } },
            e('div', { className: 'eyebrow', style: { color: 'var(--blue)', marginBottom: 12 } }, 'Service packages'),
            e('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 12 } },
              partBundles.map(b => {
                const pv = GW.bundlePreview(b, app.settings);
                const allOn = b.items.every(id => (items[id] || 0) > 0);
                return e('div', { key: b.id, style: { border: '2px solid ' + (allOn ? 'var(--good)' : 'var(--line)'), borderRadius: 'var(--r-lg)', padding: 14, display: 'flex', flexDirection: 'column', gap: 8 } },
                  e('div', { className: 'display', style: { fontSize: 15, fontWeight: 600 } }, b.name),
                  e('div', { className: 'muted', style: { fontSize: 12.5 } }, b.desc),
                  e('div', { style: { display: 'flex', alignItems: 'baseline', gap: 8 } },
                    e('span', { className: 'display num', style: { fontSize: 17, fontWeight: 700, color: 'var(--teal)' } }, GW.fmt(pv.bundlePrice)),
                    pv.save > 0 ? e('span', { className: 'badge badge-good' }, 'Save ' + GW.fmt(pv.save)) : null),
                  e(Btn, { variant: allOn ? 'ghost' : 'primary', size: 'sm', block: true, icon: allOn ? 'check' : 'plus', onClick: () => addBundle(b) }, allOn ? 'Added' : 'Add package'),
                );
              }),
            ),
          ) : null,

          cats.map(cat => {
            const rows = filtered.filter(p => p.cat === cat);
            if (!rows.length) return null;
            return e('div', { key: cat, className: 'card', style: { marginBottom: 18, overflow: 'hidden' } },
              e('div', { style: { background: 'var(--field)', padding: '14px 22px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 10 } },
                e('div', { style: { width: 32, height: 32, borderRadius: 9, background: '#fff', color: 'var(--teal)', display: 'grid', placeItems: 'center', boxShadow: 'var(--sh-1)' } }, e(Icon, { name: 'parts', size: 18 })),
                e('div', { className: 'display', style: { fontSize: 17, fontWeight: 600 } }, cat),
              ),
              rows.map((p, i) => e('div', { key: p.id, style: { display: 'flex', alignItems: 'center', gap: 14, padding: '15px 22px', borderTop: i ? '1px solid var(--line)' : 'none', background: (items[p.id] ? 'rgba(46,182,125,.05)' : 'transparent') } },
                e('div', { className: 'grow' },
                  e('div', { style: { fontWeight: 700, fontSize: 15.5 } }, p.name),
                  e('div', { className: 'muted', style: { fontSize: 13 } }, p.desc),
                ),
                e('div', { className: 'display num', style: { fontSize: 19, fontWeight: 600, color: 'var(--teal)', minWidth: 70, textAlign: 'right' } }, GW.fmt(GW.partPrice(p, app.settings).price)),
                e(Stepper, { qty: items[p.id] || 0, onSet: qty => setQty(p.id, qty) }),
              )),
            );
          }),
        ),

        // ---- right: ticket ----
        e('div', { className: 'parts-ticket', style: { position: 'sticky', top: 88, display: 'flex', flexDirection: 'column', gap: 14 } },
          e('div', { className: 'card', style: { padding: 20 } },
            e('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 } },
              e('div', { style: { width: 36, height: 36, borderRadius: 10, background: 'var(--field)', color: 'var(--teal)', display: 'grid', placeItems: 'center' } }, e(Icon, { name: 'parts', size: 20 })),
              e('div', { className: 'display', style: { fontSize: 18, fontWeight: 600 } }, 'Service ticket'),
            ),
            result.lines.length === 0
              ? e('div', { className: 'muted', style: { fontSize: 13.5, padding: '10px 0 4px' } }, 'Add parts from the list to build the estimate.')
              : e('div', { style: { display: 'flex', flexDirection: 'column' } },
                result.lines.map((l, i) => e('div', { key: l.id, style: { display: 'flex', alignItems: 'baseline', gap: 10, padding: '9px 0', borderTop: i ? '1px dashed var(--line)' : 'none' } },
                  e('div', { className: 'grow', style: { minWidth: 0 } },
                    e('div', { style: { fontWeight: 700, fontSize: 14 } }, (l.qty > 1 ? l.qty + '× ' : '') + l.name),
                  ),
                  e('div', { className: 'num', style: { fontWeight: 700, color: 'var(--ink)' } }, GW.fmt(l.linePrice)),
                )),
              ),

            result.bundleSavings > 0 ? e('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, background: 'var(--good-bg)', color: 'var(--good)', borderRadius: 10, padding: '9px 12px', fontWeight: 700, fontSize: 13 } },
              e(Icon, { name: 'check', size: 15, strokeWidth: 3 }), 'Package savings · ' + GW.fmt(result.bundleSavings)) : null,

            result.discount > 0 ? e('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, background: 'var(--good-bg)', color: 'var(--good)', borderRadius: 10, padding: '9px 12px', fontWeight: 700, fontSize: 13 } },
              e(Icon, { name: 'tag', size: 15 }), (result.coupon ? result.coupon.code + ' · ' : '') + '−' + GW.fmt(result.discount)) : null,

            e('div', { style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--line)' } },
              e('div', { className: 'eyebrow' }, 'Total · installed'),
              e('div', { style: { textAlign: 'right' } },
                result.discount > 0 ? e('div', { className: 'num', style: { fontSize: 15, color: 'var(--ink-3)', textDecoration: 'line-through' } }, GW.fmt(result.listPrice)) : null,
                e('div', { className: 'display num', style: { fontSize: 30, fontWeight: 700, color: 'var(--teal)', lineHeight: 1 } }, GW.fmt(result.price))),
            ),
          ),

          result.lines.length ? e('div', { className: 'card', style: { padding: 18 } },
            e('div', { style: { fontSize: 12, fontWeight: 800, letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 7 } }, e(Icon, { name: 'tag', size: 15 }), 'Coupon code'),
            result.couponCode
              ? e('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
                  e('div', { className: 'grow' },
                    e('div', { style: { fontWeight: 800, fontFamily: 'var(--display)', letterSpacing: '.05em', fontSize: 16, color: 'var(--good)' } }, result.couponCode),
                    e('div', { className: 'muted', style: { fontSize: 12.5 } }, result.coupon ? (GW.couponLabel(result.coupon) + ' · −' + GW.fmt(result.discount) + ' applied') : 'Applied')),
                  e(Btn, { variant: 'ghost', size: 'sm', onClick: removeCoupon }, 'Remove'))
              : e('div', null,
                  e('div', { style: { display: 'flex', gap: 8 } },
                    e('input', { value: code, onChange: ev => { setCode(ev.target.value.toUpperCase()); setCpErr(''); }, onKeyDown: ev => ev.key === 'Enter' && applyCoupon(), placeholder: 'Enter approved code', style: { flex: 1, minWidth: 0, padding: '10px 12px', borderRadius: 10, border: '1.5px solid var(--line-2)', fontWeight: 700, letterSpacing: '.05em', textTransform: 'uppercase', outline: 'none', fontFamily: 'var(--display)' } }),
                    e(Btn, { variant: 'teal', size: 'sm', onClick: applyCoupon }, 'Apply')),
                  cpErr ? e('div', { style: { color: 'var(--danger)', fontSize: 12.5, fontWeight: 600, marginTop: 8 } }, cpErr) : null,
                ),
          ) : null,

          result.lines.length ? e('div', { className: 'card', style: { padding: 20, display: 'flex', flexDirection: 'column', gap: 12 } },
            e('div', { className: 'eyebrow' }, 'Customer (optional)'),
            e(Field, { label: 'Name' }, e(Input, { placeholder: 'e.g. The Hendersons', value: cust.name, onChange: ev => setCust(c => ({ ...c, name: ev.target.value })) })),
            e('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 } },
              e(Field, { label: 'Phone' }, e(Input, { placeholder: '(555) 123-4567', value: cust.phone, onChange: ev => setCust(c => ({ ...c, phone: ev.target.value })) })),
              e(Field, { label: 'Email' }, e(Input, { placeholder: 'name@email.com', value: cust.email, onChange: ev => setCust(c => ({ ...c, email: ev.target.value })) })),
            ),
            e(Btn, { variant: 'primary', block: true, icon: 'plus', onClick: () => { Object.keys(items).forEach(id => app.addToEstimate({ type: 'part', partId: id, qty: items[id] })); setItems({}); removeCoupon(); setSignature(null); app.toast('Added to estimate'); app.go('#/estimate'); } }, 'Add to estimate'),
            e(Btn, { variant: 'ghost', block: true, icon: 'download', onClick: downloadPdf }, 'Download estimate PDF'),
            e('button', { onClick: () => { setItems({}); removeCoupon(); setSignature(null); }, style: { border: 'none', background: 'transparent', color: 'var(--ink-3)', fontWeight: 600, fontSize: 13, cursor: 'pointer', padding: 4 } }, 'Clear ticket'),
          ) : null,

          result.lines.length ? e(window.ApprovalBlock, { signature, onApprove: setSignature, onClear: () => setSignature(null) }) : null,
        ),
      ),
    );
  }
  window.Parts = Parts;
})();
