function SectionsPanel({ offer, onChange, openCatalog }) {
  // Menyn "Lägg till avsnitt" är ett <details>. Den stängdes varken när man
  // valt ett avsnitt eller när man klickade utanför, så den låg kvar och täckte
  // innehållet efter varje tillägg. Nu stängs den i båda fallen och med Esc.
  const menyRef = React.useRef(null);
  const stangMeny = () => { if (menyRef.current) menyRef.current.open = false; };
  React.useEffect(() => {
    const utanfor = (e) => {
      if (menyRef.current && menyRef.current.open && !menyRef.current.contains(e.target)) stangMeny();
    };
    const esc = (e) => { if (e.key === 'Escape') stangMeny(); };
    document.addEventListener('mousedown', utanfor);
    document.addEventListener('keydown', esc);
    return () => { document.removeEventListener('mousedown', utanfor); document.removeEventListener('keydown', esc); };
  }, []);

  const [aiEnabled, setAiEnabled] = React.useState(false);
  React.useEffect(() => {
    api('/api/ai/status').then(s => setAiEnabled(!!s.configured)).catch(() => setAiEnabled(false));
  }, []);
  const existingKinds = new Set((offer.sections||[]).map(s => s.kind));
  const addSection = preset => {
    stangMeny();
    // Egna avsnitt (kind='custom') får alltid läggas till — annars hindra dubblett
    if (preset.id !== 'custom' && existingKinds.has(preset.id)) {
      alert(`Avsnittet "${preset.title}" finns redan i offerten. Bläddra ner för att redigera det.`);
      return;
    }
    if (preset.id === 'custom') {
      const title = prompt('Rubrik för det egna avsnittet:', 'Månadslicenser');
      if (!title) return;
      onChange({ sections: [...(offer.sections||[]), {
        id: crypto.randomUUID(), kind:'custom', title, body:'', isProductList:false
      }] });
      return;
    }
    // Smart placering: services direkt efter products, cost direkt efter services/products
    const newSec = makeSection(preset);
    const arr = [...(offer.sections||[])];
    const insertAfter = (kinds) => {
      for (let i = arr.length - 1; i >= 0; i--) {
        if (kinds.includes(arr[i].kind)) return i + 1;
      }
      return -1;
    };
    let pos = -1;
    if (preset.id === 'services') pos = insertAfter(['products']);
    else if (preset.id === 'cost') pos = insertAfter(['services','products']);
    else if (preset.id === 'finance') pos = insertAfter(['cost','services','products']);
    if (pos >= 0) arr.splice(pos, 0, newSec);
    else arr.push(newSec);
    onChange({ sections: arr });
  };
  const updateSection = (id, patch) => onChange({
    sections: offer.sections.map(s => s.id===id ? {...s, ...patch} : s)
  });
  const removeSection = id => onChange({ sections: offer.sections.filter(s => s.id!==id) });
  const move = (id, dir) => {
    const arr = [...offer.sections];
    const i = arr.findIndex(s => s.id===id);
    const j = i + dir;
    if (j<0 || j>=arr.length) return;
    [arr[i], arr[j]] = [arr[j], arr[i]];
    onChange({ sections: arr });
  };

  return (
    <section style={card}>
      <SectionHead eyebrow="Steg 2" title="Innehåll i offerten"
        right={
          <details ref={menyRef} style={{position:'relative'}}>
            <summary style={{...btn('secondary'), listStyle:'none'}}>
              <Icon name="plus" size={14}/> Lägg till avsnitt
            </summary>
            <div style={{
              position:'absolute', right:0, top:'calc(100% + 6px)', zIndex:50,
              background:'#fff', border:'1px solid var(--border)', borderRadius:6,
              boxShadow:'0 8px 24px rgba(0,54,95,.12)', padding:6, width:280,
              maxHeight:480, overflowY:'auto',
            }}>
              <button onClick={() => addSection({id:'custom', title:'Eget avsnitt', icon:'edit'})} style={{
                width:'100%', display:'flex', alignItems:'center', gap:10,
                padding:'10px 10px', background:'var(--ulab-cream)', border:'none',
                borderRadius:4, cursor:'pointer', textAlign:'left', marginBottom:6,
                fontFamily:'var(--font-sans)', fontSize:13, color:'var(--ulab-navy)', fontWeight:700,
              }}
              ><Icon name="plus" size={14} stroke="var(--ulab-coral)"/>Eget avsnitt med valfri rubrik…</button>
              <div style={{fontSize:10, fontWeight:600, letterSpacing:'.14em', textTransform:'uppercase', color:'var(--fg-muted)', padding:'4px 8px 4px'}}>Standardavsnitt</div>
              {SECTION_PRESETS.map(p => {
                const exists = existingKinds.has(p.id);
                return (
                <button key={p.id} onClick={() => addSection(p)} disabled={exists} style={{
                  width:'100%', display:'flex', alignItems:'center', gap:10,
                  padding:'8px 10px', background:'transparent', border:'none',
                  borderRadius:4, cursor: exists ? 'not-allowed' : 'pointer', textAlign:'left',
                  fontFamily:'var(--font-sans)', fontSize:13,
                  color: exists ? 'var(--fg-muted)' : 'var(--ulab-navy)',
                  opacity: exists ? 0.5 : 1,
                }}
                onMouseEnter={e => { if (!exists) e.currentTarget.style.background='var(--ulab-cream)'; }}
                onMouseLeave={e => e.currentTarget.style.background='transparent'}
                ><Icon name={p.icon} size={14} stroke="var(--ulab-blue)"/>{p.title}{exists && <span style={{marginLeft:'auto', fontSize:10, color:'var(--fg-muted)'}}>finns</span>}</button>
                );
              })}
            </div>
          </details>
        }/>

      {(!offer.sections || offer.sections.length===0) && (
        <div style={{padding:'18px 4px', color:'var(--fg-muted)', fontSize:14, textAlign:'center'}}>
          Lägg till t.ex. <i>Sammanfattning</i>, <i>Behov</i> och <i>Lösning</i> för att skriva en utförlig offert — eller hoppa direkt till produktraderna nedan.
        </div>
      )}

      <div style={{display:'flex', flexDirection:'column', gap:14}}>
        {(offer.sections||[]).map((s, i) => (
          <div key={s.id} style={{
            border:'1px solid var(--border)', borderRadius:6, padding:16, background:'#fff',
          }}>
            <div style={{display:'flex', alignItems:'center', gap:10, marginBottom:10}}>
              <LocalInput value={s.title} onChange={v => updateSection(s.id, {title:v})} style={{
                ...inp, fontFamily:'var(--font-serif)', fontSize:18, fontWeight:500,
                border:'none', padding:'4px 0', background:'transparent',
              }}/>
              <div style={{display:'flex', gap:4, color:'var(--fg-muted)'}}>
                <button onClick={() => move(s.id, -1)} style={iconBtn} title="Flytta upp"><span style={{display:'inline-block', transform:'rotate(-90deg)'}}><Icon name="chevron" size={14} sw={2}/></span></button>
                <button onClick={() => move(s.id, 1)}  style={iconBtn} title="Flytta ned"><span style={{display:'inline-block', transform:'rotate(90deg)'}}><Icon name="chevron" size={14} sw={2}/></span></button>
                <button onClick={() => removeSection(s.id)} style={iconBtn} title="Ta bort"><Icon name="trash" size={14}/></button>
              </div>
            </div>
            {s.isProductList ? (
              <ProductLines offer={offer} onChange={onChange} openCatalog={openCatalog}/>
            ) : s.isServiceList ? (
              <ServiceLines offer={offer} onChange={onChange}/>
            ) : (
              <>
                {aiEnabled && <AIDraftBox offerId={offer.id} section={s} onResult={text => {
                  updateSection(s.id, { body: text });
                }}/>}
                {/* Textruta med rubrik/punktlista/fet — och inklistring från Word
                    som behåller rubriker, listor och fet stil (KlistraIn.jsx). */}
                <FormatTextarea value={s.body} onChange={v => updateSection(s.id, {body:v})}
                  rows={Math.max(4, (s.body||'').split('\n').length+1)}
                  placeholder={SECTION_PRESETS.find(p=>p.id===s.kind)?.placeholder || 'Skriv innehåll...'}
                  style={{...inp, resize:'vertical', fontFamily:'var(--font-sans)', lineHeight:1.55}}/>
              </>
            )}
          </div>
        ))}
      </div>
    </section>
  );
}
const iconBtn = {
  background:'transparent', border:'1px solid var(--border)', borderRadius:4,
  padding:'4px 6px', cursor:'pointer', color:'var(--ulab-navy)',
};
window.SectionsPanel = SectionsPanel;

// Liten "skriv med AI"-ruta ovanför textfältet i ett avsnitt: man skriver en
// kort instruktion ("kunden har 20 anställda, vill ha ny brandvägg och wifi"),
// och får ett textförslag tillbaka som antingen fyller i tomt avsnitt direkt
// eller (om det redan finns text) frågar innan det skriver över.
function AIDraftBox({ offerId, section, onResult }) {
  const [open, setOpen] = React.useState(false);
  const [prompt, setPrompt] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  // När avsnittet redan har text lagras det nya förslaget här istället för att
  // skrivas in direkt — säljaren får se förslaget och välja Ersätt/Avbryt i
  // samma ruta, hellre än webbläsarens fula inbyggda confirm()-dialog.
  const [pendingText, setPendingText] = React.useState(null);

  const generate = async () => {
    if (!prompt.trim()) return;
    setBusy(true); setError('');
    try {
      const { text } = await api(`/api/offers/${offerId}/sections/${section.id}/ai-draft`, {
        method: 'POST', body: JSON.stringify({ prompt }),
      });
      if (section.body && section.body.trim()) {
        setPendingText(text);
      } else {
        onResult(text);
        setOpen(false); setPrompt('');
      }
    } catch (e) {
      setError(e.message || 'Kunde inte generera text.');
    } finally {
      setBusy(false);
    }
  };

  if (pendingText !== null) {
    return (
      <div style={{marginBottom:12, padding:12, background:'var(--ulab-cream)', borderRadius:6, border:'1px solid #ede4d4'}}>
        <div style={{fontSize:12, fontWeight:600, color:'var(--ulab-navy)', marginBottom:6}}>
          AI-förslag — ersätt den befintliga texten i avsnittet med detta?
        </div>
        <div style={{
          fontSize:13, lineHeight:1.5, whiteSpace:'pre-wrap', background:'#fff',
          border:'1px solid var(--border)', borderRadius:4, padding:10, marginBottom:8, maxHeight:200, overflow:'auto',
        }}>{pendingText}</div>
        <div style={{display:'flex', gap:8}}>
          <button onClick={() => { onResult(pendingText); setPendingText(null); setOpen(false); setPrompt(''); }}
            style={{...btn('primary'), fontSize:12, padding:'7px 12px'}}>Ersätt texten</button>
          <button onClick={() => setPendingText(null)} style={{...btn('ghost'), fontSize:12, padding:'7px 12px'}}>Avbryt</button>
        </div>
      </div>
    );
  }

  if (!open) {
    return (
      <button onClick={() => setOpen(true)} style={{
        ...btn('ghost'), marginBottom:10, fontSize:12, padding:'6px 10px',
        color:'var(--ulab-coral)', borderColor:'var(--ulab-coral)',
      }}>✨ Skriv med AI</button>
    );
  }

  return (
    <div style={{marginBottom:12, padding:12, background:'var(--ulab-cream)', borderRadius:6, border:'1px solid #ede4d4'}}>
      <div style={{fontSize:12, fontWeight:600, color:'var(--ulab-navy)', marginBottom:6}}>
        Vad ska "{section.title}" handla om? Skriv några ord, AI:n skriver texten.
      </div>
      <LocalTextarea value={prompt} onChange={setPrompt} rows={2}
        placeholder="ex. Kunden har 20 anställda på kontor i Borås, vill ha ny brandvägg och wifi i hela huset."
        style={{...inp, resize:'vertical', fontFamily:'var(--font-sans)', fontSize:13}}
        onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) generate(); }}/>
      {error && <div style={{color:'var(--ulab-coral)', fontSize:12, marginTop:6}}>{error}</div>}
      <div style={{display:'flex', gap:8, marginTop:8}}>
        <button onClick={generate} disabled={busy || !prompt.trim()} style={{...btn('primary'), fontSize:12, padding:'7px 12px'}}>
          {busy ? 'Skriver…' : '✨ Generera'}
        </button>
        <button onClick={() => { setOpen(false); setError(''); }} style={{...btn('ghost'), fontSize:12, padding:'7px 12px'}}>Avbryt</button>
      </div>
    </div>
  );
}
window.AIDraftBox = AIDraftBox;

// (Förenklad) produktradslista som återanvänds inne i ett "Produkter"-avsnitt
function ProductLines({ offer, onChange, openCatalog }) {
  const update = (id, patch) => onChange({ lines: offer.lines.map(l => l.id===id ? {...l, ...patch} : l) });
  const remove = (id) => onChange({ lines: offer.lines.filter(l => l.id!==id) });
  const addBlank = () => {
    // Använd senaste kategorin om det finns rader, annars första katalog‑gruppen
    const lastCat = offer.lines.length > 0 ? offer.lines[offer.lines.length-1].cat : (CATALOG[0]?.cat || 'Tjänster');
    onChange({ lines: [...offer.lines, { id: crypto.randomUUID(), name:'', unit:'st', qty:1, price:0, cat:lastCat, desc:'' }]});
  };
  // Stabil ordning av kategorier (förstadykningsordning) — så grupperna inte hoppar runt vid rerender
  const catOrder = [];
  const groups = {};
  for (const l of offer.lines) {
    const c = l.cat || 'Övrigt';
    if (!(c in groups)) { groups[c] = []; catOrder.push(c); }
    groups[c].push(l);
  }

  // Hantera kategori‑byte per rad — uppdaterar bara den raden, inte alla i gruppen
  const renameLineCat = (id, newCat) => {
    onChange({ lines: offer.lines.map(l => l.id===id ? {...l, cat:newCat} : l) });
  };

  // Flytta och kopiera rader. Avsnitten har kunnat flyttas länge, men raderna
  // låg i den ordning man råkade klicka i katalogen — och ordningen är precis
  // den kunden ser i dokumentet.
  const move = (id, steg) => {
    const rader = [...offer.lines];
    const i = rader.findIndex(l => l.id === id);
    const j = i + steg;
    if (i < 0 || j < 0 || j >= rader.length) return;
    [rader[i], rader[j]] = [rader[j], rader[i]];
    onChange({ lines: rader });
  };
  const duplicate = (id) => {
    const rader = [...offer.lines];
    const i = rader.findIndex(l => l.id === id);
    if (i < 0) return;
    rader.splice(i + 1, 0, { ...rader[i], id: crypto.randomUUID() });
    onChange({ lines: rader });
  };

  // Kategorierna som redan används, till förslagslistan nedan. Utan den blev
  // "Hårdvara" och "Hårdvara " (med mellanslag på slutet) två skilda grupper i
  // kundens dokument, utan att det syntes i redigeraren.
  const kategoriforslag = Array.from(new Set([
    ...offer.lines.map(l => (l.cat || '').trim()).filter(Boolean),
    ...(window.CATALOG || []).map(g => g.cat),
  ]));

  // Kolla vilka ikryssade bilagor som faktiskt HAR ett produktblad sparat.
  // Saknas underlaget kommer ingen bilaga med i offerten, och det ska säljaren
  // få veta här — inte upptäcka genom att bläddra till sista sidan i
  // förhandsgranskningen (eller låta bli att upptäcka det alls).
  const [sheetStatus, setSheetStatus] = React.useState({});
  const sheetSkus = offer.lines.filter(l => l.sheet && l.sku).map(l => l.sku).join(',');
  React.useEffect(() => {
    if (!sheetSkus) { setSheetStatus({}); return; }
    let cancelled = false;
    api(`/api/product-assets/lookup?skus=${encodeURIComponent(sheetSkus)}`)
      .then(rows => {
        if (cancelled) return;
        const map = {};
        for (const r of rows) map[r.sku] = (r.description || r.imageUrl) ? (r.status || 'granskad') : false;
        setSheetStatus(map);
      })
      .catch(() => { if (!cancelled) setSheetStatus({}); });
    return () => { cancelled = true; };
  }, [sheetSkus]);
  const missingSheets = offer.lines.filter(l => l.sheet && l.sku && sheetStatus[l.sku] === false);
  // Ett AI-utkast som ingen läst igenom ska inte gå ut till kund utan att
  // säljaren i alla fall fått veta det.
  const draftSheets = offer.lines.filter(l => l.sheet && l.sku && sheetStatus[l.sku] === 'utkast');

  // Prismodellen styr vilka priskolumner som visas. Kontant: köppris. Finansierat:
  // bara kr/mån (finansiärens pris, per styck). Båda: båda. Alla värden sparas
  // oavsett läge, så man kan byta prismodell utan att siffror försvinner.
  const lage = offer.pricingMode || 'kontant';
  const visaKop = lage !== 'hyra';
  const visaHyra = lage !== 'kontant';
  const kolumner = '1fr 110px 70px 90px' + (visaKop ? ' 100px 100px' : '') + (visaHyra ? ' 118px 100px' : '') + ' 44px 96px';
  const { hyraSaknas } = calcOffer(offer);
  // Rader vars månadspris kopierats från en annan offert (dubblett / "Utgå
  // från senaste") — finansiärens priser ändras med ränta och avtalstid.
  const kopierade = visaHyra ? offer.lines.filter(l => l.monthlyFrom && l.monthly != null) : [];

  return (
    <div>
      <div style={{display:'flex', gap:8, marginBottom:10, alignItems:'center', flexWrap:'wrap'}}>
        <button onClick={openCatalog} style={btn('secondary')}><Icon name="plus" size={14}/> Från katalog</button>
        <button onClick={addBlank} style={btn('ghost')}><Icon name="edit" size={14}/> Egen rad</button>
        {visaHyra && <span style={{fontSize:12, color:'var(--fg-muted)'}}>Finansierat: skriv in finansiärens månadspris <b>per styck</b> på varje rad.{lage === 'hyra' ? ' Köppriset visas inte i offerten.' : ''}</span>}
      </div>
      {visaHyra && hyraSaknas.length > 0 && (
        <div style={{marginBottom:10, padding:'8px 10px', background:'#fff7ed', border:'1px solid #f0b866', borderRadius:4, fontSize:12, color:'#8a5a12'}}>
          ⚠ {hyraSaknas.length} rad(er) saknar månadspris: {hyraSaknas.map(l => l.name || 'namnlös rad').slice(0, 4).join(', ')}{hyraSaknas.length > 4 ? '…' : ''}.
          Offerten kan inte skickas förrän alla rader har kr/mån.
        </div>
      )}
      {kopierade.length > 0 && (
        <div style={{marginBottom:10, padding:'8px 10px', background:'#fff7ed', border:'1px solid #f0b866', borderRadius:4, fontSize:12, color:'#8a5a12'}}>
          ⚠ Månadspriserna på {kopierade.length} rad(er) är kopierade från offert <b>#{kopierade[0].monthlyFrom}</b> — kontrollera dem mot finansiärens aktuella offert. Varningen försvinner när du ändrat priset.
        </div>
      )}
      {missingSheets.length > 0 && (
        <div style={{marginBottom:10, padding:'8px 10px', background:'#fff7ed', border:'1px solid #f0b866', borderRadius:4, fontSize:12, color:'#8a5a12'}}>
          ⚠ Produktblad saknas för {missingSheets.map(l => l.name || l.sku).join(', ')} — ingen bilaga kommer med i offerten.
          En admin kan lägga in bild och text under <b>Produktblad</b> i menyn.
        </div>
      )}
      {draftSheets.length > 0 && (
        <div style={{marginBottom:10, padding:'8px 10px', background:'#fff7ed', border:'1px solid #f0b866', borderRadius:4, fontSize:12, color:'#8a5a12'}}>
          ⚠ Produktbladet för {draftSheets.map(l => l.name || l.sku).join(', ')} är ett <b>AI-utkast som ingen granskat</b>.
          Läs igenom det under <b>Produktblad</b> i menyn innan offerten går till kund.
        </div>
      )}
      {offer.lines.length===0 && (
        <div style={{padding:'14px 4px', fontSize:13, color:'var(--fg-muted)', textAlign:'center'}}>Inga rader — lägg till från katalog eller skapa egna.</div>
      )}
      {offer.lines.length>0 && (
        <div style={{display:'grid', gridTemplateColumns:kolumner, gap:8, padding:'10px 0 4px', borderTop:'1px solid var(--border)', fontSize:11, fontWeight:600, letterSpacing:'.14em', textTransform:'uppercase', color:'var(--ulab-blue)'}}>
          <span>Namn / beskrivning</span><span>Kategori</span><span style={{textAlign:'right'}}>Antal</span><span>Enhet</span>
          {visaKop && <span style={{textAlign:'right'}}>À‑pris</span>}{visaKop && <span style={{textAlign:'right'}}>Summa</span>}
          {visaHyra && <span style={{textAlign:'right'}} title="Finansiärens månadspris per styck">kr/mån per st</span>}{visaHyra && <span style={{textAlign:'right'}}>Per månad</span>}
          <span style={{textAlign:'center'}} title="Bifoga produktblad för denna artikel som bilaga">Bilaga</span><span style={{textAlign:'center'}}>Rad</span>
        </div>
      )}
      <datalist id="kategori-forslag">
        {kategoriforslag.map(c => <option key={c} value={c}/>)}
      </datalist>
      {offer.lines.map((l, i) => (
        <div key={l.id} style={{display:'grid', gridTemplateColumns:kolumner, gap:8, padding:'6px 0', alignItems:'flex-start', borderTop:'1px solid #f0ece2'}}>
          <div>
            <LocalInput aria-label="Namn på raden" value={l.name} onChange={v => update(l.id, {name:v})} style={cellInp} placeholder="Namn"/>
            <LocalInput aria-label="Beskrivning" value={l.desc||''} onChange={v => update(l.id, {desc:v})} style={{...cellInp, marginTop:4, fontSize:12, color:'var(--fg-muted)'}} placeholder="Beskrivning"/>
          </div>
          <LocalInput aria-label="Kategori" list="kategori-forslag" value={l.cat||''} onChange={v => renameLineCat(l.id, v)} style={{...cellInp, fontSize:12}} placeholder="Kategori"/>
          <LocalNumberInput aria-label="Antal" min={0} value={l.qty ?? ''} onChange={v => update(l.id, {qty:v})} style={{...cellInp, textAlign:'right'}}/>
          <LocalInput aria-label="Enhet" value={l.unit} onChange={v => update(l.id, {unit:v})} style={cellInp}/>
          {visaKop && <LocalNumberInput aria-label="Pris per enhet" min={0} value={l.price ?? ''} placeholder="0" onChange={v => update(l.id, {price:v})} style={{...cellInp, textAlign:'right'}}/>}
          {visaKop && <div style={{textAlign:'right', fontVariantNumeric:'tabular-nums', fontWeight:600, color:'var(--ulab-navy)', alignSelf:'center'}}>{fmtKrOrIngar(l.price == null || l.qty == null ? null : l.qty*l.price)}</div>}
          {visaHyra && <LocalNumberInput aria-label="Månadspris per styck" min={0} value={l.monthly ?? ''} placeholder="kr/mån"
            onChange={v => update(l.id, {monthly:v, monthlyFrom: null})}
            style={{...cellInp, textAlign:'right', borderColor: (l.monthly == null || l.monthly === '') ? 'var(--ulab-coral)' : undefined}}/>}
          {visaHyra && <div style={{textAlign:'right', fontVariantNumeric:'tabular-nums', fontWeight:600, color:'var(--ulab-navy)', alignSelf:'center'}}>{l.monthly == null || l.qty == null ? '—' : fmtKrOrIngar(l.qty*l.monthly) + (l.qty*l.monthly ? '/mån' : '')}</div>}
          <div style={{textAlign:'center', alignSelf:'center'}} title={l.sku ? 'Bifoga ett produktblad för denna artikel i offerten' : 'Bara artiklar från katalogen kan få ett produktblad'}>
            <input type="checkbox" aria-label="Bifoga produktblad" checked={!!l.sheet} disabled={!l.sku} onChange={e => update(l.id, {sheet: e.target.checked})}/>
          </div>
          <div style={{display:'flex', gap:2, alignSelf:'center', justifyContent:'flex-end'}}>
            <button onClick={() => move(l.id, -1)} disabled={i===0} title="Flytta upp" aria-label="Flytta raden uppåt" style={radKnapp(i===0)}>↑</button>
            <button onClick={() => move(l.id, 1)} disabled={i===offer.lines.length-1} title="Flytta ner" aria-label="Flytta raden nedåt" style={radKnapp(i===offer.lines.length-1)}>↓</button>
            <button onClick={() => duplicate(l.id)} title="Kopiera raden" aria-label="Kopiera raden" style={radKnapp(false)}>⧉</button>
            <button onClick={() => remove(l.id)} title="Ta bort raden" aria-label="Ta bort raden" style={{background:'transparent', border:'none', cursor:'pointer', color:'var(--fg-muted)', padding:2}}><Icon name="trash" size={16}/></button>
          </div>
        </div>
      ))}
    </div>
  );
}
function radKnapp(inaktiv) {
  return {
    background:'transparent', border:'1px solid var(--border)', borderRadius:3,
    width:20, height:20, lineHeight:'16px', padding:0, fontSize:12,
    color: inaktiv ? 'var(--border-strong)' : 'var(--fg-muted)',
    cursor: inaktiv ? 'default' : 'pointer', fontFamily:'var(--font-sans)',
  };
}
const cellInp = {
  fontFamily:'var(--font-sans)', fontSize:14, padding:'8px 10px',
  border:'1px solid var(--border)', borderRadius:4, background:'#fff',
  color:'var(--ulab-navy)', width:'100%', fontVariantNumeric:'tabular-nums',
};
window.ProductLines = ProductLines;

// Tjänster / månadskostnader — separat från produktrader
function ServiceLines({ offer, onChange }) {
  const services = offer.services || [];
  const update = (id, patch) => onChange({ services: services.map(l => l.id===id ? {...l, ...patch} : l) });
  const remove = (id) => onChange({ services: services.filter(l => l.id!==id) });
  const addBlank = () => onChange({ services: [...services, { id: crypto.randomUUID(), name:'', unit:'månad', qty:1, price:0, desc:'' }]});

  return (
    <div>
      <div style={{display:'flex', gap:8, marginBottom:10}}>
        <button onClick={addBlank} style={btn('secondary')}><Icon name="plus" size={14}/> Lägg till tjänst</button>
        <span style={{alignSelf:'center', fontSize:12, color:'var(--fg-muted)'}}>Återkommande månadskostnader visas separat i offerten.</span>
      </div>
      {services.length===0 && (
        <div style={{padding:'14px 4px', fontSize:13, color:'var(--fg-muted)', textAlign:'center'}}>Inga tjänster — lägg till t.ex. drift, supportavtal, M365‑licenser.</div>
      )}
      {services.length>0 && (
        <div style={{display:'grid', gridTemplateColumns:'1fr 70px 128px 110px 110px 32px', gap:8, padding:'10px 0 4px', borderTop:'1px solid var(--border)', fontSize:11, fontWeight:600, letterSpacing:'.14em', textTransform:'uppercase', color:'var(--ulab-blue)'}}>
          <span>Namn / beskrivning</span><span style={{textAlign:'right'}}>Antal</span><span>Period</span><span style={{textAlign:'right'}}>À‑pris/period</span><span style={{textAlign:'right'}}>Summa/period</span><span></span>
        </div>
      )}
      {services.map(l => (
        <div key={l.id} style={{display:'grid', gridTemplateColumns:'1fr 70px 128px 110px 110px 32px', gap:8, padding:'6px 0', alignItems:'flex-start', borderTop:'1px solid #f0ece2'}}>
          <div>
            <LocalInput value={l.name} onChange={v => update(l.id, {name:v})} style={cellInp} placeholder="Namn på tjänst"/>
            <LocalInput value={l.desc||''} onChange={v => update(l.id, {desc:v})} style={{...cellInp, marginTop:4, fontSize:12, color:'var(--fg-muted)'}} placeholder="Beskrivning"/>
          </div>
          <LocalNumberInput aria-label="Antal" min={0} value={l.qty ?? ''} onChange={v => update(l.id, {qty:v})} style={{...cellInp, textAlign:'right'}}/>
          <LocalInput value={l.unit} onChange={v => update(l.id, {unit:v})} style={cellInp} placeholder="månad / år"/>
          <LocalNumberInput aria-label="Pris per enhet" min={0} value={l.price ?? ''} placeholder="0" onChange={v => update(l.id, {price:v})} style={{...cellInp, textAlign:'right'}}/>
          <div style={{textAlign:'right', fontVariantNumeric:'tabular-nums', fontWeight:600, color:'var(--ulab-navy)', alignSelf:'center'}}>{fmtKrOrIngar(l.price == null || l.qty == null ? null : l.qty*l.price)}</div>
          <button onClick={() => remove(l.id)} style={{background:'transparent', border:'none', cursor:'pointer', color:'var(--fg-muted)', alignSelf:'center'}}><Icon name="trash" size={16}/></button>
        </div>
      ))}
    </div>
  );
}
window.ServiceLines = ServiceLines;

function TotalsPanel({ offer, onChange }) {
  // Samma uträkning som kunddokumentet använder (calcOffer i Icon.jsx), så
  // redigeraren och offerten aldrig kan visa olika siffror.
  const { sub, disc, net, vat, total, monthlyServiceTotal, term, termMissing, hyraPerManad, hyraSaknas, totaltPerManad } = calcOffer(offer);
  const services = offer.services || [];
  const lage = offer.pricingMode || 'kontant';
  const finansierat = lage !== 'kontant';
  const finansiar = offer.financier || 'De Lage Landen';

  // Byter man till Finansierat läggs ett avsnitt "Finansiering" till med en
  // standardtext (om det inte redan finns ett). Texten är bara ett utkast —
  // den redigeras som vilket avsnitt som helst.
  const bytLage = (nytt) => {
    const patch = { pricingMode: nytt };
    const sections = offer.sections || [];
    if (nytt !== 'kontant' && !sections.some(x => x.kind === 'finance')) {
      const preset = (window.SECTION_PRESETS || []).find(x => x.id === 'finance');
      if (preset && window.makeSection) {
        const sek = window.makeSection(preset);
        sek.body = `Utrustningen hyrs genom ${finansiar} under ${termMissing ? 48 : term} månader. Månadskostnaden faktureras av ${finansiar}, och avtalet förutsätter sedvanlig kreditprövning. Installation och tjänster från ULAB IT Partner ingår i månadskostnaden enligt ovan.`;
        const i = sections.findIndex(x => x.kind === 'cost');
        const nya = [...sections];
        nya.splice(i >= 0 ? i + 1 : nya.length, 0, sek);
        patch.sections = nya;
      }
    }
    onChange(patch);
  };

  const etikett = { fontSize:12, fontWeight:600, color:'var(--ulab-navy)', display:'block', marginBottom:6 };
  return (
    <section style={card}>
      <SectionHead eyebrow="Steg 3" title="Pris & villkor"/>
      <div style={{display:'grid', gridTemplateColumns:'1fr 320px', gap:32}}>
        <div>
          <label style={etikett}>Pris‑modell</label>
          <select value={lage} onChange={e => bytLage(e.target.value)} style={{...inp, maxWidth:240}}>
            <option value="kontant">Kontant (engångsköp)</option>
            <option value="hyra">Finansierat (hyra/månad)</option>
            <option value="bada">Visa båda alternativen</option>
          </select>
          {finansierat && (
            <div style={{display:'flex', gap:10, marginTop:10}}>
              <div><label style={etikett}>Avtalstid (mån)</label><LocalNumberInput min={0} value={offer.term ?? ''} placeholder="48" onChange={v=>onChange({term:v})} style={{...inp, maxWidth:120, borderColor: termMissing ? 'var(--ulab-coral)' : undefined}}/></div>
              <div><label style={etikett}>Finansiär</label><input value={offer.financier||'De Lage Landen'} onChange={e=>onChange({financier:e.target.value})} style={{...inp, maxWidth:200}}/></div>
            </div>
          )}
          {finansierat && termMissing && (
            <div style={{marginTop:8, padding:'8px 10px', background:'#fff7ed', border:'1px solid #f0b866', borderRadius:4, fontSize:12, color:'#8a5a12'}}>
              ⚠ Avtalstiden saknas. Den står i offerten ("… kr/mån i 48 mån") och måste fyllas i innan offerten kan skickas.
            </div>
          )}
          {finansierat && (
            <div style={{marginTop:10, fontSize:12, color:'var(--fg-muted)', lineHeight:1.6}}>
              Månadspriserna skrivs in per rad i Steg 2 (kolumnen <b>kr/mån per st</b>), från finansiärens offert.
              {lage === 'hyra' ? ' Köppriser och rabatt visas inte i dokumentet.' : ' Dokumentet visar både köppris och månadspris.'}
            </div>
          )}
          {lage !== 'hyra' && (
            <React.Fragment>
              <label style={{...etikett, margin:'14px 0 6px'}}>Rabatt (%)</label>
              <LocalNumberInput min={0} max={100} value={offer.discount ?? ''} placeholder="0" onChange={v => onChange({discount: v ?? 0})} style={{...inp, maxWidth:120}}/>
              {lage === 'bada' && <div style={{fontSize:12, color:'var(--fg-muted)', marginTop:4}}>Rabatten gäller köppriset — inte månadspriserna.</div>}
            </React.Fragment>
          )}
        </div>
        <div style={{background:'var(--ulab-cream)', borderRadius:6, padding:20, fontVariantNumeric:'tabular-nums'}}>
          {lage !== 'hyra' && (
            <React.Fragment>
              <Row label="Summa" v={fmtKr(sub)}/>
              {disc>0 && <Row label={`Rabatt ${offer.discount}%`} v={'– '+fmtKr(disc)}/>}
              <Row label="Netto" v={fmtKr(net)}/>
              <Row label="Moms 25%" v={fmtKr(vat)}/>
              <div style={{height:1, background:'var(--border-strong)', margin:'10px 0'}}/>
              <Row big label="Att betala" v={fmtKr(total)}/>
            </React.Fragment>
          )}
          {finansierat && (
            <div style={{marginTop: lage === 'bada' ? 14 : 0, paddingTop: lage === 'bada' ? 12 : 0, borderTop: lage === 'bada' ? '1px dashed var(--border-strong)' : 'none'}}>
              <div style={{fontSize:10, fontWeight:600, letterSpacing:'.18em', textTransform:'uppercase', color:'var(--ulab-coral)', marginBottom:6}}>Finansierat</div>
              <Row label={`Hyra via ${finansiar}`} v={hyraSaknas.length ? '—' : fmtKr(hyraPerManad) + '/mån'}/>
              {services.length>0 && <Row label="Tjänster från ULAB" v={fmtKr(monthlyServiceTotal) + '/mån'}/>}
              <div style={{height:1, background:'var(--border-strong)', margin:'6px 0'}}/>
              <Row big label="Totalt per månad" v={hyraSaknas.length ? '—' : fmtKr(totaltPerManad)}/>
              <div style={{fontSize:12, color:'var(--fg-muted)', marginTop:4}}>exkl. moms · i {term} mån{hyraSaknas.length ? ` · ${hyraSaknas.length} rad(er) saknar månadspris` : ''}</div>
            </div>
          )}
          {!finansierat && services.length>0 && (
            <div style={{marginTop:14, paddingTop:12, borderTop:'1px dashed var(--border-strong)'}}>
              <div style={{fontSize:10, fontWeight:600, letterSpacing:'.18em', textTransform:'uppercase', color:'var(--ulab-coral)', marginBottom:6}}>Månadskostnader</div>
              {services.map(l => (
                <div key={l.id} style={{display:'flex', justifyContent:'space-between', fontSize:13, color:'var(--ulab-navy)', padding:'2px 0'}}>
                  <span>{l.name || '—'}{l.qty>1 && <span style={{color:'var(--fg-muted)'}}> × {l.qty}</span>}</span>
                  <span>{fmtKrOrIngar((l.qty||0)*(l.price||0))}/{l.unit||'mån'}</span>
                </div>
              ))}
              <div style={{height:1, background:'var(--border-strong)', margin:'6px 0'}}/>
              <Row big label="Per månad" v={fmtKr(monthlyServiceTotal)}/>
            </div>
          )}
        </div>
      </div>
    </section>
  );
}
function Row({label, v, big}) {
  return (
    <div style={{display:'flex', justifyContent:'space-between', padding:'4px 0', fontSize: big?16:14, fontWeight: big?700:500, color:'var(--ulab-navy)'}}>
      <span>{label}</span><span>{v}</span>
    </div>
  );
}
window.TotalsPanel = TotalsPanel;
