// ============================================================================
// ЗАЯВКИ v2 — фінальна сторінка (на базі варіанта 2: KPI-картки = вкладки)
//   LsLeadsModule — вміст модуля (вбудовується в основний CRM)
//   LsFinalPage   — самостійна сторінка з сайдбаром і топбаром
// 4 вкладки звернень клієнтів + 2 внутрішні: Документи, Питання менеджерів
// ============================================================================
const { useState: lsfUseState } = React;

function LsCardTab({ t, active, count, alert, alertLabel, onClick }) {
  return (
    <button onClick={onClick} style={{
      display: "flex", flexDirection: "column", gap: 8, padding: "12px 14px", textAlign: "left",
      borderRadius: 10, cursor: "pointer", fontFamily: "inherit", minWidth: 0,
      border: "1px solid " + (active ? "color-mix(in oklab, " + t.color + " 45%, transparent)" : "var(--border-subtle)"),
      background: active ? "color-mix(in oklab, " + t.color + " 9%, var(--bg-panel))" : "var(--bg-panel)",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", minWidth: 0 }}>
        <span style={{
          width: 26, height: 26, borderRadius: 7, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
          background: t.soft, color: t.color,
        }}><LsIcon name={t.icon} size={14}/></span>
        <span style={{ fontSize: 12.5, fontWeight: 500, color: active ? "var(--fg-primary)" : "var(--fg-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.label}</span>
        <span style={{ flex: 1 }}></span>
        <span style={{ fontSize: 21, fontWeight: 600, color: active ? t.color : "var(--fg-primary)", fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{count}</span>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: alert > 0 ? "#FCA5A5" : "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
        {alert > 0 && <span style={{ width: 6, height: 6, borderRadius: "50%", background: "#EF4444", flexShrink: 0 }}></span>}
        {alert > 0 ? alertLabel : "все опрацьовано"}
      </div>
    </button>
  );
}

function LsGroupLabel({ children }) {
  return (
    <div style={{ fontSize: 10, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--fg-muted)", padding: "0 2px" }}>
      {children}
    </div>
  );
}

// ── Воронка товарних заявок: створено → в роботі → в заказ / втрачено ───────
// + перемикач Активні/Архів і розбивка по менеджерах (контроль: хто скільки втрачає).
const LS_TERMINAL = new Set(["converted", "lost", "closed", "done"]);
function LsFunnelStat({ label, value, color, sub }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 2, padding: "6px 12px", minWidth: 0 }}>
      <span style={{ fontSize: 10.5, fontWeight: 500, letterSpacing: "0.04em", textTransform: "uppercase", color: "var(--fg-muted)", whiteSpace: "nowrap" }}>{label}</span>
      <span style={{ fontSize: 20, fontWeight: 700, color: color || "var(--fg-primary)", fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{value}{sub && <span style={{ fontSize: 12, fontWeight: 500, color: "var(--fg-muted)", marginLeft: 5 }}>{sub}</span>}</span>
    </div>
  );
}
// Стан активності дзвінків для заявки (через спільний lsCallActivity).
function lsLeadCallState(l) { const a = (typeof lsCallActivity === "function") ? lsCallActivity(l.phone, l.createdAt) : null; return a ? a.state : "none"; }
function LsLeadFunnel({ leads, archive, onToggleArchive, callFilter, onCallFilter, statusFilter, onStatusFilter, compact }) {
  const [byMgr, setByMgr] = lsfUseState(false);
  const [openFilters, setOpenFilters] = lsfUseState(false); // compact: фільтри/статистика сховані за тапом
  const inWork = leads.filter(l => !LS_TERMINAL.has(l.status)).length;
  const conv = leads.filter(l => l.status === "converted").length;
  const lost = leads.filter(l => l.status === "lost").length;
  const denom = conv + lost;
  const pct = denom ? Math.round((conv / denom) * 100) : 0;
  // «не передзвонили» — рахуємо по АКТИВНИХ (відкритих) заявках: клієнт дзвонив, менеджер не передзвонив
  const missed = leads.filter(l => !LS_TERMINAL.has(l.status) && lsLeadCallState(l) === "missed").length;
  const mgr = {};
  leads.forEach(l => {
    const m = l.manager || "— без менеджера";
    const o = mgr[m] || (mgr[m] = { total: 0, conv: 0, lost: 0, work: 0, missed: 0 });
    o.total++;
    if (l.status === "converted") o.conv++;
    else if (l.status === "lost") o.lost++;
    else if (!LS_TERMINAL.has(l.status)) { o.work++; if (lsLeadCallState(l) === "missed") o.missed++; }
  });
  const mgrs = Object.entries(mgr).sort((a, b) => b[1].lost - a[1].lost || b[1].total - a[1].total);
  // база для лічильників фільтрів = поточний scope (активні / архів)
  const scope = leads.filter(l => archive ? LS_TERMINAL.has(l.status) : !LS_TERMINAL.has(l.status));
  const callCounts = { all: scope.length, missed: 0, ok: 0, none: 0 };
  scope.forEach(l => { const s = lsLeadCallState(l); if (s === "missed") callCounts.missed++; else if (s === "ok") callCounts.ok++; else if (s === "none") callCounts.none++; });
  const callChips = [
    { k: "all", label: "Усі дзвінки", color: "var(--fg-secondary)" },
    { k: "missed", label: "❗ не передзвонили", color: "#EF4444" },
    { k: "ok", label: "📲 передзвонили", color: "#10B981" },
    { k: "none", label: "немає дзвінків", color: "var(--fg-muted)" },
  ];
  const stChips = [{ k: "all", label: "Усі" }, ...Object.entries(LS_PRODUCT_STATUSES).map(([k, v]) => ({ k, label: v.label, dot: v.dot }))]
    .filter(c => c.k === "all" || scope.some(l => l.status === c.k));
  const seg = (val, label) => (
    <button onClick={() => onToggleArchive(val)} style={{
      height: 26, padding: "0 12px", border: 0, borderRadius: 6, cursor: "pointer", fontFamily: "inherit", fontSize: 12, fontWeight: 500,
      background: archive === val ? "var(--accent)" : "transparent", color: archive === val ? "#fff" : "var(--fg-secondary)",
    }}>{label}</button>
  );
  const chipBtn = (active, label, color, count, onClick, dot) => (
    <button onClick={onClick} style={{
      display: "inline-flex", alignItems: "center", gap: 6, height: 26, padding: "0 10px",
      border: "1px solid " + (active ? "var(--accent-ring)" : "var(--border-default)"),
      background: active ? "var(--accent-soft)" : "var(--bg-raised)", color: active ? "var(--fg-primary)" : (color || "var(--fg-secondary)"),
      borderRadius: 999, fontFamily: "inherit", fontSize: 11.5, fontWeight: 500, cursor: "pointer", whiteSpace: "nowrap",
    }}>
      {dot && <span style={{ width: 6, height: 6, borderRadius: "50%", background: dot }}></span>}
      {label}{count != null && <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>{count}</span>}
    </button>
  );
  // ── Компактний (мобільний) режим: вузька стрічка цифр, що горизонтально
  //    скролиться, + кнопка «фільтри», за якою ховаються чипи/сегменти/по менеджерах.
  //    Сам блок у mobile.jsx винесено в зону скролу, тож він зʼїжджає при прогортанні
  //    і не зʼїдає екран зі списком заявок.
  if (compact) {
    const cstat = (label, value, color, sub) => (
      <div style={{ display: "flex", flexDirection: "column", gap: 1, padding: "0 10px", borderRight: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <span style={{ fontSize: 9, fontWeight: 500, letterSpacing: ".03em", textTransform: "uppercase", color: "var(--fg-muted)", whiteSpace: "nowrap" }}>{label}</span>
        <span style={{ fontSize: 16, fontWeight: 700, color: color || "var(--fg-primary)", fontVariantNumeric: "tabular-nums", lineHeight: 1.1, whiteSpace: "nowrap" }}>{value}{sub && <span style={{ fontSize: 10, fontWeight: 500, color: "var(--fg-muted)", marginLeft: 4 }}>{sub}</span>}</span>
      </div>
    );
    const activeFilters = (callFilter && callFilter !== "all") || (archive && statusFilter && statusFilter !== "all") || archive;
    return (
      <div style={{ margin: "0 16px 8px", border: "1px solid var(--border-subtle)", borderRadius: 10, background: "var(--bg-panel)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 0, padding: "8px 6px 8px 0", overflowX: "auto", WebkitOverflowScrolling: "touch" }}>
          {cstat("Усього", leads.length)}
          {cstat("В роботі", inWork, inWork > 0 ? "#F59E0B" : null)}
          {cstat("В заказ", conv, "#10B981", denom ? pct + "%" : null)}
          {cstat("Втрач.", lost, lost > 0 ? "#EF4444" : null)}
          {cstat("Не передзв.", missed, missed > 0 ? "#EF4444" : null)}
          <button onClick={() => setOpenFilters(v => !v)} style={{
            marginLeft: "auto", flexShrink: 0, height: 30, padding: "0 10px", borderRadius: 8, cursor: "pointer", fontFamily: "inherit",
            fontSize: 11.5, fontWeight: 600, border: "1px solid " + (activeFilters || openFilters ? "var(--accent-ring)" : "var(--border-default)"),
            background: activeFilters || openFilters ? "var(--accent-soft)" : "var(--bg-raised)", color: "var(--fg-secondary)",
            display: "inline-flex", alignItems: "center", gap: 5, marginRight: 6,
          }}><LsIcon name="sliders-horizontal" size={13}/>{activeFilters ? "•" : ""}</button>
        </div>
        {openFilters && (
          <div style={{ borderTop: "1px solid var(--border-subtle)", padding: "8px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
            <div style={{ display: "inline-flex", padding: 3, background: "var(--bg-raised)", border: "1px solid var(--border-default)", borderRadius: 8, gap: 3, alignSelf: "flex-start" }}>
              {seg(false, "Активні")}
              {seg(true, "Архів")}
            </div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
              {callChips.map(c => chipBtn(callFilter === c.k, c.label, c.color, callCounts[c.k], () => onCallFilter(callFilter === c.k ? "all" : c.k)))}
              {archive && stChips.map(c => chipBtn(statusFilter === c.k, c.label, null, null, () => onStatusFilter(statusFilter === c.k ? "all" : c.k), c.dot))}
            </div>
            <button onClick={() => setByMgr(v => !v)} style={{
              height: 30, padding: "0 11px", borderRadius: 7, cursor: "pointer", fontFamily: "inherit", fontSize: 12, fontWeight: 500,
              border: "1px solid var(--border-default)", background: byMgr ? "var(--accent-soft)" : "var(--bg-raised)", color: "var(--fg-secondary)",
              display: "inline-flex", alignItems: "center", gap: 6, alignSelf: "flex-start",
            }}><LsIcon name="users" size={13}/>по менеджерах</button>
            {byMgr && (
              <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                {mgrs.map(([name, o]) => (
                  <div key={name} style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 8, fontSize: 11.5 }}>
                    <span style={{ flex: 1, minWidth: 80, color: "var(--fg-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</span>
                    <span style={{ color: "var(--fg-muted)" }}>усього {o.total}</span>
                    <span style={{ color: "#F59E0B" }}>в роб. {o.work}</span>
                    {o.missed > 0 && <span style={{ color: "#EF4444", fontWeight: 600 }}>не передзв. {o.missed}</span>}
                    <span style={{ color: "#10B981" }}>заказ {o.conv}</span>
                    <span style={{ color: o.lost > 0 ? "#EF4444" : "var(--fg-muted)", fontWeight: o.lost > 0 ? 600 : 400 }}>втрач. {o.lost}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  return (
    <div style={{ margin: "0 20px 8px", border: "1px solid var(--border-subtle)", borderRadius: 10, background: "var(--bg-panel)" }}>
      <div style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, padding: "8px 8px 8px 0" }}>
        <LsFunnelStat label="Усього" value={leads.length}/>
        <LsFunnelStat label="В роботі" value={inWork} color={inWork > 0 ? "#F59E0B" : "var(--fg-primary)"}/>
        <LsFunnelStat label="В заказ" value={conv} color="#10B981" sub={denom ? pct + "%" : null}/>
        <LsFunnelStat label="Втрачено" value={lost} color={lost > 0 ? "#EF4444" : "var(--fg-primary)"}/>
        <LsFunnelStat label="Не передзвонили" value={missed} color={missed > 0 ? "#EF4444" : "var(--fg-primary)"}/>
        <span style={{ flex: 1 }}></span>
        <button onClick={() => setByMgr(v => !v)} style={{
          height: 28, padding: "0 11px", borderRadius: 7, cursor: "pointer", fontFamily: "inherit", fontSize: 12, fontWeight: 500,
          border: "1px solid var(--border-default)", background: byMgr ? "var(--accent-soft)" : "var(--bg-raised)", color: "var(--fg-secondary)", marginRight: 8,
          display: "inline-flex", alignItems: "center", gap: 6,
        }}><LsIcon name="users" size={13}/>по менеджерах</button>
        <div style={{ display: "inline-flex", padding: 3, background: "var(--bg-raised)", border: "1px solid var(--border-default)", borderRadius: 8, gap: 3, marginRight: 8 }}>
          {seg(false, "Активні")}
          {seg(true, "Архів")}
        </div>
      </div>
      {/* Фільтри: по активності дзвінків (завжди) + по статусу (в архіві) */}
      <div style={{ borderTop: "1px solid var(--border-subtle)", padding: "8px 12px", display: "flex", flexWrap: "wrap", gap: 6 }}>
        {callChips.map(c => chipBtn(callFilter === c.k, c.label, c.color, callCounts[c.k], () => onCallFilter(callFilter === c.k ? "all" : c.k)))}
        {archive && stChips.length > 1 && <span style={{ width: 1, background: "var(--border-default)", margin: "0 4px" }}></span>}
        {archive && stChips.map(c => chipBtn(statusFilter === c.k, c.label, null, null, () => onStatusFilter(statusFilter === c.k ? "all" : c.k), c.dot))}
      </div>
      {byMgr && (
        <div style={{ borderTop: "1px solid var(--border-subtle)", padding: "8px 14px", display: "flex", flexDirection: "column", gap: 4 }}>
          {mgrs.map(([name, o]) => (
            <div key={name} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 12 }}>
              <span style={{ flex: 1, minWidth: 0, color: "var(--fg-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</span>
              <span style={{ color: "var(--fg-muted)", whiteSpace: "nowrap" }}>усього {o.total}</span>
              <span style={{ color: "#F59E0B", whiteSpace: "nowrap" }}>в роботі {o.work}</span>
              {o.missed > 0 && <span style={{ color: "#EF4444", fontWeight: 600, whiteSpace: "nowrap" }}>не передзв. {o.missed}</span>}
              <span style={{ color: "#10B981", whiteSpace: "nowrap" }}>в заказ {o.conv}</span>
              <span style={{ color: o.lost > 0 ? "#EF4444" : "var(--fg-muted)", fontWeight: o.lost > 0 ? 600 : 400, whiteSpace: "nowrap" }}>втрачено {o.lost}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Модуль (без сайдбара/топбара — вбудовується в основний CRM) ────────────
// Термінальні («в архів») статуси для гарантій і документів.
const LS_WARR_DONE = new Set(["done", "closed"]);
const LS_DOC_DONE  = new Set(["sent"]);
// Перемикач Активні / Архів для вкладок гарантій і документів (виконані/надіслані ховаємо
// в архів, щоб не займали місце, але одним кліком доступні).
function LsArchiveToggle({ archive, onToggle, activeN, archiveN }) {
  const btn = (on, label, n) => (
    <button onClick={() => onToggle(on)} style={{
      height: 30, padding: "0 13px", borderRadius: 8, fontFamily: "inherit", fontSize: 12.5, fontWeight: 500, cursor: "pointer",
      display: "inline-flex", alignItems: "center", gap: 7,
      background: archive === on ? "var(--accent-soft)" : "var(--bg-raised)",
      color: archive === on ? "#C7D2FE" : "var(--fg-secondary)",
      border: "1px solid " + (archive === on ? "var(--accent-ring)" : "var(--border-default)"),
    }}>
      {label}
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, opacity: 0.8 }}>{n}</span>
    </button>
  );
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 18px 0" }}>
      {btn(false, "Активні", activeN)}
      {btn(true, "Архів", archiveN)}
    </div>
  );
}
function LsLeadsModule({ initialTab = "product", userName, userRole, openLeadId, onConsumeOpenLead }) {
  const [tab, setTab] = lsfUseState(initialTab);
  const [records, setRecords] = lsfUseState([]);
  const [loading, setLoading] = lsfUseState(true);
  const [managers, setManagers] = lsfUseState([]);
  const [mgrFilter, setMgrFilter] = lsfUseState("all"); // фільтр по менеджеру (лише owner)
  const [docModal, setDocModal] = lsfUseState(false);
  const [newModal, setNewModal] = lsfUseState(false);
  const [editing, setEditing] = lsfUseState(null);
  const [selProduct, setSelProduct] = lsfUseState(null);
  const [prodArchive, setProdArchive] = lsfUseState(false); // вкладка product: Активні / Архів
  const [warrArchive, setWarrArchive] = lsfUseState(false); // вкладка warranty: Активні / Архів
  const [docsArchive, setDocsArchive] = lsfUseState(false); // вкладка docs: Активні / Архів
  const [prodCallFilter, setProdCallFilter] = lsfUseState("all");   // all|missed|ok|none
  const [prodStatusFilter, setProdStatusFilter] = lsfUseState("all"); // фільтр статусу (в архіві)
  const [, setCallsTick] = lsfUseState(0); // ре-рендер коли підтягнулись дзвінки (бейджі активності)
  const [refreshing, setRefreshing] = lsfUseState(false);

  const reload = () => lsApi.list().then(rs => { setRecords(rs); setLoading(false); }).catch(e => { console.warn("заявки:", e.message); setLoading(false); });
  // Ручне оновлення: нові заявки/статуси + менеджери + дзвінки, без перезапуску PWA.
  const refreshAll = () => {
    setRefreshing(true);
    const pCalls = (window.API && window.API.get)
      ? window.API.get("/api/calls/recent?days=60").then(d => { if (d && d.calls) { window._crmCalls = { data: d.calls, ts: Date.now() }; setCallsTick(t => t + 1); } }).catch(() => {})
      : Promise.resolve();
    Promise.all([
      lsApi.list().then(rs => setRecords(rs)).catch(() => {}),
      lsApi.managers().then(setManagers).catch(() => {}),
      pCalls,
    ]).finally(() => setRefreshing(false));
  };
  React.useEffect(() => {
    reload(); lsApi.managers().then(setManagers);
    // дзвінки для бейджів/фільтрів активності — з персистентної БД (історія > 7 днів),
    // фолбек на живий кеш Ringostat якщо БД ще порожня
    const setCalls = arr => { window._crmCalls = { data: arr || [], ts: Date.now() }; setCallsTick(t => t + 1); };
    if (window.API && window.API.get) {
      window.API.get("/api/calls/recent?days=60")
        .then(d => { if (d && d.calls && d.calls.length) setCalls(d.calls); else if (window.API.getCalls) return window.API.getCalls().then(x => setCalls(x.calls)); })
        .catch(() => { if (window.API.getCalls) window.API.getCalls().then(x => setCalls(x.calls)).catch(() => {}); });
    }
  }, []);

  // Глибоке відкриття заявки з дошки «Що зробити» (App → openLeadId): щойно записи
  // завантажились — знаходимо заявку, перемикаємо на її вкладку й відкриваємо картку.
  React.useEffect(() => {
    if (openLeadId == null || !records.length) return;
    const rec = records.find(r => Number(r.id) === Number(openLeadId));
    if (rec) {
      setTab(rec.type || "product");
      if ((rec.type || "product") === "product") { setProdArchive(LS_TERMINAL.has(rec.status)); setSelProduct(rec); }
      else setEditing(rec);
    }
    if (onConsumeOpenLead) onConsumeOpenLead();
  }, [openLeadId, records]);

  // Розмежування доступу: менеджер бачить ЛИШЕ свої заявки; керівник (owner) — усі,
  // з опційним фільтром по конкретному менеджеру.
  const isOwner = userRole === "owner";
  const sameMgr = (l) => String(l.manager || "").trim().toLowerCase() === String(userName || "").trim().toLowerCase();
  const scopedRecords = !isOwner
    ? records.filter(sameMgr)
    : (mgrFilter === "all" ? records : records.filter(l => String(l.manager || "") === mgrFilter));
  const byType = (t) => scopedRecords.filter(r => r.type === t);
  const products = byType("product"), warranty = byType("warranty"), returns = byType("return");
  // вкладка product: показуємо або активні, або архів (термінальні) — щоб активні не засмічувались
  let productsView = products.filter(l => prodArchive ? LS_TERMINAL.has(l.status) : !LS_TERMINAL.has(l.status));
  if (prodArchive && prodStatusFilter !== "all") productsView = productsView.filter(l => l.status === prodStatusFilter);
  if (prodCallFilter !== "all") productsView = productsView.filter(l => lsLeadCallState(l) === prodCallFilter);
  const questions = byType("question"), docs = byType("docs"), mgrq = byType("mgrq");
  // Гарантії/документи: Активні (не термінальні) або Архів (виконані/надіслані).
  const warrantyView = warranty.filter(l => warrArchive ? LS_WARR_DONE.has(l.status) : !LS_WARR_DONE.has(l.status));
  const docsView     = docs.filter(l => docsArchive ? LS_DOC_DONE.has(l.status) : !LS_DOC_DONE.has(l.status));

  const docsPending = docs.filter(d => d.status === "pending").length;
  const mgrqOpen = mgrq.filter(q => q.status === "open").length;

  const STATUS_MAPS = { product: LS_PRODUCT_STATUSES, warranty: LS_WARRANTY_STATUSES, return: LS_RETURN_STATUSES, question: LS_QUESTION_STATUSES, docs: LS_DOC_STATUSES, mgrq: LS_MGRQ_STATUSES };
  const statusLabel = (type, key) => { const m = STATUS_MAPS[type]; return m && m[key] ? m[key].label : key; };
  const ev = (a) => ({ a, u: userName || "" });

  const sendDoc   = (id)        => lsApi.update(id, { status: "sent", __event: ev("Статус → Надіслано") }).then(reload);
  const answerMgr = (id, reply) => lsApi.update(id, { status: "answered", reply, replyBy: userName || "Ви", __event: ev("Відповідь менеджеру") }).then(reload);
  // дії над будь-якою заявкою
  const changeStatus = (id, status) => {
    const cur = records.find(r => r.id === id);
    const patch = { status, __event: ev("Статус → " + statusLabel(cur ? cur.type : "", status)) };
    if (status === "in_sc" && !(cur && cur.inScDate)) patch.inScDate = Date.now(); // старт 14-денного відліку
    return lsApi.update(id, patch).then(reload);
  };
  const removeLead   = (id)         => lsApi.remove(id).then(() => { setEditing(null); reload(); });

  // Кілька файлів: ллємо послідовно; перший fileId зберігаємо в data (легасі-поле для чипа в столі)
  const uploadAllFiles = async (id, files) => {
    let firstId = null;
    for (const file of files || []) {
      try {
        const up = await lsApi.uploadFile(id, file);
        if (up && up.fileId && firstId == null) firstId = up.fileId;
      } catch (e) { console.warn("файл «" + file.name + "»:", e.message); }
    }
    return firstId;
  };
  const fileMeta = (rest, files) => {
    if (!files.length) return;
    rest.fileName = files[0].name + (files.length > 1 ? " +" + (files.length - 1) : "");
    rest.fileSize = files[0].sizeLabel;
  };

  const editLead     = (id, patch)  => {
    const { _file, _files, ageMin, ts, updatedStr, history, ...rest } = patch; // не писати службові/файли/історію у data
    const files = (_files && _files.length) ? _files : (_file ? [_file] : []);
    fileMeta(rest, files);
    rest.__event = ev("Відредаговано");
    return lsApi.update(id, rest)
      .then(() => uploadAllFiles(id, files))
      .then(fid => (fid ? lsApi.update(id, { fileId: fid }) : null))
      .then(() => { setEditing(null); reload(); })
      .catch(e => console.warn("редагування:", e.message));
  };

  // Документ: створити запис → завантажити файли (якщо є) → зберегти fileId.
  const addDoc = (d) => {
    const { _file, _files, ...rest } = d;
    const files = (_files && _files.length) ? _files : (_file ? [_file] : []);
    const body = { type: "docs", status: rest.status || "pending", source: "crm", manager: rest.manager || userName || "", _user: userName, _action: "Створено документ", ...rest };
    fileMeta(body, files);
    return lsApi.create(body)
      .then(res => (files.length && res && res.id)
        ? uploadAllFiles(res.id, files).then(fid => (fid ? lsApi.update(res.id, { fileId: fid }) : null))
        : null)
      .then(() => { setTab("docs"); reload(); })
      .catch(e => console.warn("документ:", e.message));
  };

  // «Нова заявка» — створює на бекенді й перемикає на вкладку типу.
  // Файли (фото гарантії тощо) заливаються ПІСЛЯ створення — як у документах.
  const createLead = (type, f) => {
    if (type === "docs") return addDoc(f);
    const files = (f._files && f._files.length) ? f._files : (f._file ? [f._file] : []);
    const body = { type, source: f.source || "call", client: f.client || "Без імені", phone: f.phone || "", manager: f.manager || userName || "", _user: userName, _action: "Створено заявку" };
    if (type === "product")  Object.assign(body, { status: "new",     product: f.product || "", bot: "awaiting", note: f.note || "", price: f.sitePrice != null ? f.sitePrice : null, sitePresence: f.sitePresence || "" });
    if (type === "warranty") Object.assign(body, { status: "new",     product: f.product || "", serial: f.serial || "", problem: f.problem || "", docId: f.docId || "", ttn: f.ttn || "", orderId: parseInt(f.orderId, 10) || null });
    if (type === "return")   Object.assign(body, { status: "review",  product: f.product || "", reason: f.reason || "", purchased: f.purchased || "", day: 1, refund: parseInt(f.refund, 10) || 0, complete: true });
    if (type === "question") Object.assign(body, { status: "open",    question: f.question || "" });
    if (type === "mgrq")     Object.assign(body, { status: "open",    question: f.question || "", orderId: parseInt(f.orderId, 10) || null });
    fileMeta(body, files);
    // Гарантія з фото: відкладаємо пост у топік — ПЕРШЕ фото стане основним
    // повідомленням (фото з підписом-заявкою), а не окремим текстом
    const defer = type === "warranty" && files.length > 0;
    if (defer) body._deferTopic = true;
    lsApi.create(body)
      .then(res => {
        if (!(files.length && res && res.id)) return null;
        return uploadAllFiles(res.id, files)
          // страховка: якщо фото не залилось — пустий PUT створить текстове повідомлення
          .then(() => (defer ? lsApi.update(res.id, {}) : null));
      })
      .then(() => { setTab(type); reload(); })
      .catch(e => console.warn("створення заявки:", e.message));
  };

  const defs = {
    product:  { search: "Пошук: товар, клієнт, телефон…", statuses: LS_PRODUCT_STATUSES,  data: productsView },
    warranty: { search: "Пошук: товар, серійний номер…",   statuses: LS_WARRANTY_STATUSES, data: warranty },
    return:   { search: "Пошук: товар, клієнт…",           statuses: LS_RETURN_STATUSES,   data: returns },
    question: { search: "Пошук у питаннях…",               statuses: LS_QUESTION_STATUSES, data: questions },
    docs:     { search: "Пошук: документ, замовлення…",    statuses: LS_DOC_STATUSES,      data: docs },
    mgrq:     { search: "Пошук у питаннях менеджерів…",    statuses: LS_MGRQ_STATUSES,     data: mgrq },
  };
  const def = defs[tab];
  const warrActive = warranty.filter(l => ["new", "wait_client", "in_sc", "wait_return"].includes(l.status)).length;
  const counts = {
    product:  { count: products.length,  alert: products.filter(l => l.status === "new").length,   alertLabel: products.filter(l => l.status === "new").length + " нові без відповіді" },
    warranty: { count: warranty.length,  alert: warrActive, alertLabel: warrActive + " в роботі" },
    return:   { count: returns.length,   alert: returns.filter(l => l.day >= 12).length,           alertLabel: "строк спливає" },
    question: { count: questions.length, alert: questions.filter(l => l.status === "open").length, alertLabel: questions.filter(l => l.status === "open").length + " без відповіді" },
  };

  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0, minHeight: 0, background: "var(--bg-base)", position: "relative" }}>
      {/* Картки-вкладки: 4 клієнтські + 2 внутрішні */}
      <div style={{ display: "flex", gap: 14, padding: "14px 20px 8px", flexShrink: 0, alignItems: "stretch" }}>
        <div style={{ flex: 4, display: "flex", flexDirection: "column", gap: 6, minWidth: 0 }}>
          <LsGroupLabel>Звернення клієнтів</LsGroupLabel>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, flex: 1 }}>
            {Object.values(LS_TYPES).map(t => (
              <LsCardTab key={t.key} t={t} active={tab === t.key}
                count={counts[t.key].count} alert={counts[t.key].alert} alertLabel={counts[t.key].alertLabel}
                onClick={() => setTab(t.key)}/>
            ))}
          </div>
        </div>
        <div style={{ width: 1, background: "var(--border-default)", margin: "18px 0 2px" }}></div>
        <div style={{ flex: 2, display: "flex", flexDirection: "column", gap: 6, minWidth: 0 }}>
          <LsGroupLabel>Внутрішні — для керівника</LsGroupLabel>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 8, flex: 1 }}>
            <LsCardTab t={LS_INTERNAL_TYPES.docs} active={tab === "docs"}
              count={docs.length} alert={docsPending} alertLabel={docsPending + " очіку" + (docsPending === 1 ? "є" : "ють") + " відправки"}
              onClick={() => setTab("docs")}/>
            <LsCardTab t={LS_INTERNAL_TYPES.mgrq} active={tab === "mgrq"}
              count={mgrq.length} alert={mgrqOpen} alertLabel={mgrqOpen + " без відповіді"}
              onClick={() => setTab("mgrq")}/>
          </div>
        </div>
      </div>

      {/* Тулбар вкладки */}
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 20px", flexWrap: "wrap" }}>
        <LsSearch placeholder={def.search} width={190}/>
        <LsStatusChips key={"chips-" + tab + "-" + def.data.length} map={def.statuses} leads={def.data}/>
        <div style={{ flex: 1 }}></div>
        {tab === "docs" && (
          <span onClick={() => setDocModal(true)} style={{ display: "inline-flex" }}>
            <LsButton variant="secondary" icon="file-plus">Додати документ</LsButton>
          </span>
        )}
        {isOwner && (
          <select value={mgrFilter} onChange={e => setMgrFilter(e.target.value)} title="Фільтр по менеджеру" style={{
            height: 32, padding: "0 10px", borderRadius: 7, fontFamily: "inherit", fontSize: 12.5, outline: "none",
            background: "var(--bg-raised)", color: mgrFilter !== "all" ? "var(--accent)" : "var(--fg-secondary)",
            border: "1px solid " + (mgrFilter !== "all" ? "var(--accent)" : "var(--border-default)"), cursor: "pointer",
          }}>
            <option value="all">Усі менеджери</option>
            {[...new Set(records.map(r => (r.manager || "").trim()).filter(Boolean))].sort().map(m => <option key={m} value={m}>{m}</option>)}
          </select>
        )}
        <LsButton variant="ghost" icon="arrow-up-down" style={{ padding: "0 8px" }} title="Сортування: найновіші"></LsButton>
        <span onClick={refreshing ? undefined : refreshAll} style={{ display: "inline-flex" }}>
          <LsButton variant="secondary" icon="refresh-cw" title="Оновити заявки">{refreshing ? "Оновлення…" : "Оновити"}</LsButton>
        </span>
        <span onClick={() => setNewModal(true)} style={{ display: "inline-flex" }}>
          <LsButton variant="primary" icon="plus">Нова заявка</LsButton>
        </span>
      </div>

      {/* Воронка + перемикач Активні/Архів — лише для товарних заявок */}
      {tab === "product" && <LsLeadFunnel leads={products} archive={prodArchive} onToggleArchive={setProdArchive}
        callFilter={prodCallFilter} onCallFilter={setProdCallFilter} statusFilter={prodStatusFilter} onStatusFilter={setProdStatusFilter}/>}
      {tab === "warranty" && <LsArchiveToggle archive={warrArchive} onToggle={setWarrArchive}
        activeN={warranty.length - warranty.filter(l => LS_WARR_DONE.has(l.status)).length} archiveN={warranty.filter(l => LS_WARR_DONE.has(l.status)).length}/>}
      {tab === "docs" && <LsArchiveToggle archive={docsArchive} onToggle={setDocsArchive}
        activeN={docs.length - docs.filter(l => LS_DOC_DONE.has(l.status)).length} archiveN={docs.filter(l => LS_DOC_DONE.has(l.status)).length}/>}

      {/* Стіл вкладки (+ картка запиту товару праворуч) */}
      <div style={{ flex: 1, display: "flex", minHeight: 0, borderTop: "1px solid var(--border-subtle)" }}>
        <div style={{ flex: 1, overflowY: "auto", overflowX: "auto", minWidth: 0 }}>
          {tab === "product"  && <LsProductTable leads={productsView} userName={userName} selectedId={editing?.id} onEdit={setEditing} onStatus={changeStatus}/>}
          {tab === "warranty" && <LsWarrantyTable leads={warrantyView} onEdit={setEditing} onStatus={changeStatus}/>}
          {tab === "return"   && <LsReturnTable leads={returns} onEdit={setEditing} onStatus={changeStatus}/>}
          {tab === "question" && <LsQuestionInbox key={questions.length} leads={questions} onEdit={setEditing} onStatus={changeStatus}/>}
          {tab === "docs"     && <LsDocsTable leads={docsView} onSend={sendDoc} onEdit={setEditing} onStatus={changeStatus}/>}
          {tab === "mgrq"     && <LsMgrInbox key={mgrq.filter(q => q.status === "open").length} leads={mgrq} onAnswer={answerMgr} onEdit={setEditing} onStatus={changeStatus}/>}
          <div style={{ padding: "12px 20px", fontSize: 11.5, color: "var(--fg-muted)" }}>
            Показано {def.data.length} із {def.data.length}
          </div>
        </div>
      </div>

      {docModal && <LsAddDocModal managers={managers} onClose={() => setDocModal(false)} onAdd={addDoc}/>}
      {newModal && <LsNewLeadModal managers={managers} userName={userName} onClose={() => setNewModal(false)} onCreate={createLead}/>}
      {editing && <LsNewLeadModal initial={editing} managers={managers} userName={userName} onClose={() => setEditing(null)} onSave={editLead} onDelete={removeLead}/>}
    </div>
  );
}

// ── Самостійна сторінка (для прев'ю поза основним CRM) ─────────────────────
function LsFinalPage({ initialTab = "product" }) {
  return (
    <div style={{ display: "flex", width: "100%", height: "100%", background: "var(--bg-base)", overflow: "hidden", position: "relative" }}>
      <LsSidebar/>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
        <LsTopBar right={
          <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>Сьогодні: <span style={{ color: "var(--fg-secondary)", fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{LS_SUMMARY.total}</span> · {LS_SUMMARY.totalSub}</span>
        }/>
        <LsLeadsModule initialTab={initialTab}/>
      </div>
    </div>
  );
}

Object.assign(window, { LsFinalPage, LsLeadsModule, LsLeadFunnel, LS_TERMINAL });
