function Sidebar({ offers, activeId, onSelect, onNew, onDelete, user, onLogout, onOpenUsers, onOpenChangePassword, onOpenProductAssets, showAll, onToggleShowAll, vy, onVy, onOpenTrash, onOpenSearch }) {
  const [hoverId, setHoverId] = React.useState(null);
  const [query, setQuery] = React.useState('');
  const [statusFilter, setStatusFilter] = React.useState('alla');
  const [sortering, setSortering] = React.useState('andrad');
  // Radens ×-knapp öppnar en liten bekräftelse i själva raden istället för
  // webbläsarens fula confirm()-dialog (samma stil som AI-förslagets Ersätt/Avbryt).
  const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
  const listRef = React.useRef(null);

  const q = query.trim().toLowerCase();
  let visibleOffers = q ? offers.filter(o => (
    (o.customer || '').toLowerCase().includes(q) ||
    (o.subject || '').toLowerCase().includes(q) ||
    (o.number || '').toLowerCase().includes(q)
  )) : offers;
  if (statusFilter !== 'alla') visibleOffers = visibleOffers.filter(o => o.status === statusFilter);
  visibleOffers = [...visibleOffers].sort((a, b) => {
    // Finansierade offerter har ingen köpsumma att sortera på — där används
    // månadsbeloppet gånger 48, så de hamnar i rimlig storleksordning bland
    // kontantofferterna i stället för längst ned.
    if (sortering === 'belopp') return sbSortBelopp(b) - sbSortBelopp(a);
    if (sortering === 'kund') return String(a.customer || '').localeCompare(String(b.customer || ''), 'sv');
    return String(b.updatedAt || '').localeCompare(String(a.updatedAt || ''));
  });

  // Bekräftelsen låg tidigare kvar på ett id även när listan filtrerades om, och
  // kunde då dyka upp igen på fel rad.
  React.useEffect(() => { setConfirmDeleteId(null); }, [query, statusFilter, showAll]);

  const askDelete = (e, o) => { e.stopPropagation(); setConfirmDeleteId(o.id); };
  const confirmDelete = (e, o) => { e.stopPropagation(); setConfirmDeleteId(null); onDelete(o.id); };
  const cancelDelete = (e) => { e.stopPropagation(); setConfirmDeleteId(null); };

  // Tangentbord i listan: piltangenter byter offert, Delete frågar om radering.
  // Raderna var tidigare vanliga <div onClick> utan roll eller tabIndex, så
  // listan gick inte att använda med tangentbordet alls.
  const onListKey = (e) => {
    if (!visibleOffers.length) return;
    const i = visibleOffers.findIndex(o => o.id === activeId);
    if (e.key === 'ArrowDown') { e.preventDefault(); onSelect(visibleOffers[Math.min(i + 1, visibleOffers.length - 1)].id); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); onSelect(visibleOffers[Math.max(i - 1, 0)].id); }
    else if (e.key === 'Home') { e.preventDefault(); onSelect(visibleOffers[0].id); }
    else if (e.key === 'End') { e.preventDefault(); onSelect(visibleOffers[visibleOffers.length - 1].id); }
  };

  const kr = (n) => Math.round(Number(n) || 0).toLocaleString('sv-SE').replace(/,/g, ' ') + ' kr';

  return (
    <aside style={{
      width:280, background:'#fff', borderRight:'1px solid var(--border)',
      display:'flex', flexDirection:'column', flexShrink:0,
    }}>
      <div style={{padding:'18px 20px', borderBottom:'1px solid var(--border)', display:'flex', alignItems:'center', gap:10}}>
        <img src="assets/ulab-logo.png?v=2" alt="ULAB" style={{height:28}}/>
        <div style={{fontSize:11, fontWeight:600, letterSpacing:'.16em', textTransform:'uppercase', color:'var(--fg-muted)'}}>Offert</div>
      </div>

      {/* Offerter och Kunder som två lika stora delar av programmet. */}
      <div style={{display:'flex', borderBottom:'1px solid var(--border)'}} role="tablist">
        {[['offert', 'Offerter'], ['kunder', 'Kunder']].map(([id, txt]) => (
          <button key={id} role="tab" aria-selected={vy === id} onClick={() => onVy(id)} style={{
            flex:1, padding:'10px 8px', border:'none', borderBottom:'2px solid ' + (vy === id ? 'var(--ulab-coral)' : 'transparent'),
            background:'transparent', cursor:'pointer', fontFamily:'var(--font-sans)', fontSize:12.5,
            fontWeight:600, color: vy === id ? 'var(--ulab-navy)' : 'var(--fg-muted)',
          }}>{txt}</button>
        ))}
      </div>

      <div style={{padding:'14px 16px 8px'}}>
        <button onClick={onNew} style={{
          width:'100%', background:'var(--ulab-navy)', color:'#fff', border:'none',
          padding:'10px 14px', borderRadius:4, fontSize:13, fontWeight:600,
          cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', gap:6,
          fontFamily:'var(--font-sans)',
        }}><Icon name="plus" size={14}/> Ny offert</button>
        <button onClick={onOpenSearch} title="Snabbsök i allt (Ctrl+K)" style={{
          width:'100%', marginTop:6, background:'transparent', border:'1px solid var(--border-strong)',
          borderRadius:4, padding:'7px 10px', fontSize:11.5, color:'var(--fg-muted)', cursor:'pointer',
          fontFamily:'var(--font-sans)', display:'flex', justifyContent:'space-between', alignItems:'center',
        }}><span>Sök i allt</span><span style={{fontSize:10, border:'1px solid var(--border-strong)', borderRadius:3, padding:'1px 5px'}}>Ctrl K</span></button>
      </div>

      {vy === 'offert' && (
        <React.Fragment>
          <div style={{padding:'0 16px 8px'}}>
            <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Filtrera listan…"
              aria-label="Filtrera offertlistan"
              style={{
                width:'100%', boxSizing:'border-box', fontSize:12, padding:'8px 10px',
                border:'1px solid var(--border-strong)', borderRadius:4, fontFamily:'var(--font-sans)',
              }}/>
          </div>
          {/* Egna offerter som standard. "Alla" visar även kollegornas — de öppnas
              skrivskyddade, så man kan läsa och duplicera men inte ändra. */}
          <div style={{padding:'0 16px 8px', display:'flex', gap:6}}>
            {[['Mina offerter', false], ['Alla', true]].map(([txt, val]) => (
              <button key={txt} onClick={() => onToggleShowAll(val)} style={{
                flex: val ? '0 0 auto' : 1, padding:'6px 10px', borderRadius:4, fontSize:11, fontWeight:600,
                border:'1px solid ' + (showAll === val ? 'var(--ulab-navy)' : 'var(--border-strong)'),
                background: showAll === val ? 'var(--ulab-navy)' : '#fff',
                color: showAll === val ? '#fff' : 'var(--ulab-navy)',
                cursor:'pointer', fontFamily:'var(--font-sans)',
              }}>{txt}</button>
            ))}
          </div>
          <div style={{padding:'0 16px 10px', display:'flex', gap:6}}>
            <select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} aria-label="Filtrera på status" style={valStil}>
              <option value="alla">Alla statusar</option>
              <option>Utkast</option><option>Skickad</option><option>Godkänd</option><option>Avslagen</option>
            </select>
            <select value={sortering} onChange={e => setSortering(e.target.value)} aria-label="Sortera listan" style={valStil}>
              <option value="andrad">Senast ändrad</option>
              <option value="belopp">Störst belopp</option>
              <option value="kund">Kundnamn</option>
            </select>
          </div>
          <div style={{padding:'4px 12px 6px', fontSize:11, fontWeight:600, letterSpacing:'.14em', textTransform:'uppercase', color:'var(--fg-muted)'}}>
            {q || statusFilter !== 'alla' ? `Träffar (${visibleOffers.length})` : (showAll ? `Alla offerter (${visibleOffers.length})` : `Mina offerter (${visibleOffers.length})`)}
          </div>
          <div ref={listRef} role="listbox" aria-label="Offerter" tabIndex={0} onKeyDown={onListKey}
            style={{flex:1, overflow:'auto', padding:'0 8px'}}>
            {visibleOffers.length === 0 && (
              <div style={{padding:'10px 12px', fontSize:12, color:'var(--fg-muted)'}}>
                {offers.length ? 'Inga träffar med de filtren.' : 'Inga offerter ännu.'}
              </div>
            )}
            {visibleOffers.map(o => {
              const showDelete = hoverId === o.id || activeId === o.id;
              const label = o.customer || o.subject || `#${o.number}`;
              const confirming = confirmDeleteId === o.id;
              const sen = o.status === 'Skickad' && o.followUpAt && o.followUpAt < new Date().toISOString().slice(0, 10);
              return (
                <div key={o.id} role="option" aria-selected={o.id === activeId}
                  onClick={() => !confirming && onSelect(o.id)}
                  onMouseEnter={() => setHoverId(o.id)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    padding:'10px 12px', borderRadius:4, cursor:'pointer', marginBottom:2,
                    background: confirming ? '#fff3ee' : (o.id===activeId ? 'var(--ulab-cream)' : (hoverId===o.id ? '#f8f5ee' : 'transparent')),
                    position:'relative',
                  }}>
                  {confirming ? (
                    <div>
                      <div style={{fontSize:12, color:'var(--ulab-navy)', marginBottom:8, paddingRight:4}}>
                        Ta bort offerten <b>"{label}"</b>? Den hamnar i papperskorgen och går att ta tillbaka.
                      </div>
                      <div style={{display:'flex', gap:6}}>
                        <button onClick={e => confirmDelete(e, o)} style={{
                          background:'var(--ulab-coral)', color:'#fff', border:'none', borderRadius:4,
                          padding:'5px 10px', fontSize:11, cursor:'pointer', fontFamily:'var(--font-sans)',
                        }}>Ta bort</button>
                        <button onClick={cancelDelete} style={{
                          background:'transparent', border:'1px solid var(--border-strong)', borderRadius:4,
                          padding:'5px 10px', fontSize:11, cursor:'pointer', color:'var(--ulab-navy)', fontFamily:'var(--font-sans)',
                        }}>Avbryt</button>
                      </div>
                    </div>
                  ) : (
                    <>
                      <div style={{fontSize:13, fontWeight:600, color:'var(--ulab-navy)', paddingRight:24}}>{o.customer || 'Ny kund'}</div>
                      <div style={{fontSize:12, color:'var(--fg-muted)', marginTop:2, display:'flex', justifyContent:'space-between', gap:6}}>
                        <span>#{o.number}</span>
                        <span style={{fontVariantNumeric:'tabular-nums'}}>{sbBeloppText(o, kr)}</span>
                      </div>
                      <div style={{fontSize:11, color:'var(--fg-muted)', marginTop:2, display:'flex', justifyContent:'space-between', gap:6}}>
                        <span>{o.status}{sen && <span style={{color:'var(--ulab-coral)', fontWeight:600}}> · bevakning passerad</span>}</span>
                        <span>{(o.date || '').slice(0, 10)}</span>
                      </div>
                      {o.canEdit === false && (
                        <div style={{fontSize:11, color:'var(--fg-muted)', marginTop:3, display:'flex', alignItems:'center', gap:4}}>
                          <Icon name="user" size={11}/> {o.ownerName || 'Annan säljare'} · skrivskyddad
                        </div>
                      )}
                      {showDelete && o.canEdit !== false && (
                        <button onClick={e => askDelete(e, o)} title={`Ta bort offert ${label}`}
                          aria-label={`Ta bort offert ${label}`}
                          style={{
                            position:'absolute', top:8, right:8,
                            width:22, height:22, borderRadius:4, border:'none',
                            background:'transparent', color:'var(--fg-muted)', cursor:'pointer',
                            display:'flex', alignItems:'center', justifyContent:'center',
                            fontSize:14, lineHeight:1, padding:0,
                          }}
                          onMouseEnter={e => { e.currentTarget.style.background = 'var(--ulab-coral)'; e.currentTarget.style.color = '#fff'; }}
                          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-muted)'; }}
                        >×</button>
                      )}
                    </>
                  )}
                </div>
              );
            })}
          </div>
        </React.Fragment>
      )}
      {vy !== 'offert' && <div style={{flex:1}}/>}

      <KontoMeny user={user} onOpenTrash={onOpenTrash} onOpenUsers={onOpenUsers}
        onOpenProductAssets={onOpenProductAssets} onOpenChangePassword={onOpenChangePassword} onLogout={onLogout}/>
    </aside>
  );
}

// Kontoraden längst ned. Tidigare låg fem likadana ramade knappar staplade
// under varandra och tog en femtedel av sidopanelen — allt såg lika viktigt ut
// och listan fick mindre plats. Nu är det en rad som visar vem man är inloggad
// som, och menyn öppnas ovanför när man klickar.
function KontoMeny({ user, onOpenTrash, onOpenUsers, onOpenProductAssets, onOpenChangePassword, onLogout }) {
  const [oppen, setOppen] = React.useState(false);
  // Öppnades menyn med tangentbordet ska första raden få fokus. Öppnades den
  // med musen ska den inte det — då ser raden markerad ut utan att man valt
  // något.
  const [medTangent, setMedTangent] = React.useState(false);
  const vardRef = React.useRef(null);
  const menyRef = React.useRef(null);
  const knappRef = React.useRef(null);
  const admin = user?.role === 'admin';

  React.useEffect(() => {
    if (!oppen) return;
    const ut = (e) => { if (vardRef.current && !vardRef.current.contains(e.target)) setOppen(false); };
    const tangent = (e) => {
      if (e.key === 'Escape') { e.stopPropagation(); setOppen(false); if (knappRef.current) knappRef.current.focus(); return; }
      if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
      e.preventDefault();
      const rader = menyRef.current ? [...menyRef.current.querySelectorAll('[role="menuitem"]')] : [];
      if (!rader.length) return;
      const i = rader.indexOf(document.activeElement);
      // Ligger fokus utanför menyn (öppnad med musen) hoppar pil ned till
      // första raden och pil upp till den sista.
      const n = i < 0
        ? (e.key === 'ArrowDown' ? 0 : rader.length - 1)
        : (e.key === 'ArrowDown' ? (i + 1) % rader.length : (i - 1 + rader.length) % rader.length);
      rader[n].focus();
    };
    document.addEventListener('mousedown', ut);
    window.addEventListener('keydown', tangent, true);
    const t = setTimeout(() => {
      if (!medTangent) return;
      const forsta = menyRef.current && menyRef.current.querySelector('[role="menuitem"]');
      if (forsta) forsta.focus();
    }, 0);
    return () => { document.removeEventListener('mousedown', ut); window.removeEventListener('keydown', tangent, true); clearTimeout(t); };
  }, [oppen, medTangent]);

  const initialer = String(user?.name || '?').split(/\s+/).filter(Boolean)
    .slice(0, 2).map(o => o[0].toUpperCase()).join('') || '?';

  const val = (fn) => () => { setOppen(false); if (fn) fn(); };

  const Rad = ({ ikon, text, forklaring, onClick, fara }) => (
    <button role="menuitem" type="button" onClick={onClick} style={{ ...sbMenyRad, color: fara ? '#8a3b30' : 'var(--ulab-navy)' }}
      onMouseEnter={e => { e.currentTarget.style.background = fara ? '#fdf0ee' : 'var(--ulab-cream)'; }}
      onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
      onFocus={e => { e.currentTarget.style.background = fara ? '#fdf0ee' : 'var(--ulab-cream)'; }}
      onBlur={e => { e.currentTarget.style.background = 'transparent'; }}>
      <span style={{ flex: 'none', display: 'flex', color: fara ? '#8a3b30' : 'var(--fg-muted)' }}><Icon name={ikon} size={16}/></span>
      <span style={{ flex: 1, minWidth: 0 }}>
        <span style={{ display: 'block', fontSize: 13, fontWeight: 600 }}>{text}</span>
        <span style={{ display: 'block', fontSize: 11.5, color: 'var(--fg-muted)', fontWeight: 400, marginTop: 1 }}>{forklaring}</span>
      </span>
    </button>
  );

  return (
    <div ref={vardRef} style={{ position: 'relative', borderTop: '1px solid var(--border)' }}>
      {oppen && (
        <div ref={menyRef} role="menu" aria-label="Konto och inställningar" style={sbMeny}>
          <Rad ikon="trash" text="Papperskorg" forklaring="Återställ borttagna offerter" onClick={val(onOpenTrash)}/>
          {admin && (
            <React.Fragment>
              <div style={sbGruppRubrik}>Administration</div>
              <Rad ikon="user" text="Användare" forklaring="Konton, roller och behörighet" onClick={val(onOpenUsers)}/>
              <Rad ikon="file" text="Produktblad" forklaring="PDF-blad som kan bifogas offerter" onClick={val(onOpenProductAssets)}/>
            </React.Fragment>
          )}
          <div style={sbSkiljare}/>
          <Rad ikon="lock" text="Byt lösenord" forklaring="Du loggas ut från andra enheter" onClick={val(onOpenChangePassword)}/>
          <Rad ikon="logout" text="Logga ut" forklaring={user?.email || ''} onClick={val(onLogout)} fara/>
        </div>
      )}
      {/* e.detail === 0 betyder att klicket kom från Enter eller mellanslag,
          inte från musen — då, och bara då, flyttas fokus in i menyn. */}
      <button ref={knappRef} type="button" onClick={(e) => { setMedTangent(e.detail === 0); setOppen(o => !o); }}
        aria-haspopup="menu" aria-expanded={oppen}
        style={{ ...sbKontoRad, background: oppen ? 'var(--ulab-cream)' : 'transparent' }}
        onMouseEnter={e => { if (!oppen) e.currentTarget.style.background = 'var(--ulab-cool-grey)'; }}
        onMouseLeave={e => { if (!oppen) e.currentTarget.style.background = 'transparent'; }}>
        <span style={sbAvatar}>{initialer}</span>
        <span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>
          <span style={sbNamn}>{user?.name || '—'}</span>
          {/* Rollen står med, så det syns direkt om ett konto är admin — admin
              går förbi alla ägarregler, och det ska inte behöva gissas. */}
          <span style={sbRoll}>{admin ? 'Administratör' : 'Säljare'}</span>
        </span>
        <span style={{ flex: 'none', display: 'flex', color: 'var(--fg-muted)', transform: oppen ? 'rotate(90deg)' : 'rotate(-90deg)', transition: 'transform .12s' }}>
          <Icon name="chevron" size={16}/>
        </span>
      </button>
    </div>
  );
}

const sbKontoRad = {
  width: '100%', display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px',
  border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', textAlign: 'left',
  transition: 'background .12s',
};
const sbAvatar = {
  flex: 'none', width: 32, height: 32, borderRadius: 4, background: 'var(--ulab-navy)',
  color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
  fontSize: 12, fontWeight: 700, letterSpacing: '.02em',
};
const sbNamn = {
  display: 'block', fontSize: 13, fontWeight: 600, color: 'var(--ulab-navy)',
  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
};
const sbRoll = { display: 'block', fontSize: 11.5, color: 'var(--fg-muted)', marginTop: 1 };
const sbMeny = {
  position: 'absolute', bottom: 'calc(100% + 6px)', left: 8, right: 8, zIndex: 50,
  background: '#fff', border: '1px solid var(--border-strong)', borderRadius: 6,
  boxShadow: 'var(--shadow-modal)', padding: 6,
};
const sbMenyRad = {
  width: '100%', display: 'flex', alignItems: 'flex-start', gap: 10, padding: '8px 10px',
  background: 'transparent', border: 'none', borderRadius: 4, cursor: 'pointer',
  fontFamily: 'var(--font-sans)', textAlign: 'left', transition: 'background .1s',
};
const sbGruppRubrik = {
  fontSize: 10, fontWeight: 700, letterSpacing: '.14em', textTransform: 'uppercase',
  color: 'var(--fg-subtle)', padding: '10px 10px 4px',
};
const sbSkiljare = { height: 1, background: 'var(--border)', margin: '6px 4px' };
// Beloppet på en rad i listan. Listan laddas som sammanfattningar med
// totalNet/totalMonthly från servern — men när en offert öppnas byts raden ut
// mot hela offerten, som inte har de fälten. Då räknas beloppet fram här i
// stället (samma calcOffer som dokumentet), så det syns även medan man
// redigerar. Finansierade offerter visar månadsbeloppet, inte köpsumman.
function sbBelopp(o) {
  if (o._summary || !o.lines) return { net: o.totalNet || 0, manad: o.totalMonthly || 0, lage: o.pricingMode || 'kontant' };
  const c = calcOffer(o);
  const lage = o.pricingMode || 'kontant';
  return { net: lage === 'hyra' ? 0 : c.net, manad: lage === 'kontant' ? c.monthlyServiceTotal : c.totaltPerManad, lage };
}
function sbBeloppText(o, kr) {
  const b = sbBelopp(o);
  if (b.lage === 'hyra') return b.manad ? kr(b.manad) + '/mån' : '';
  return b.net ? kr(b.net) : '';
}
function sbSortBelopp(o) {
  const b = sbBelopp(o);
  return b.lage === 'hyra' ? b.manad * 48 : b.net;
}
const valStil = {
  flex:1, minWidth:0, fontFamily:'var(--font-sans)', fontSize:11, padding:'6px 6px',
  border:'1px solid var(--border-strong)', borderRadius:4, color:'var(--ulab-navy)', background:'#fff',
};
window.Sidebar = Sidebar;
