// ============================================================================
//  Прайси — вкладка закупівельних прайсів постачальників
//  Desktop only. Mock data — replace with API calls.
// ============================================================================
(function () {
const { useState, useEffect, useRef, useMemo } = React;

// ── Constants ────────────────────────────────────────────────────────────────
const TARGET_MARGIN = 10;
const CUR_SYM = { UAH: "₴", USD: "$", EUR: "€" };

// Курс — ГЛОБАЛЬНИЙ (сервер, задає власник, бачать всі пристрої/менеджери).
// localStorage — лише кеш для миттєвого старту до відповіді сервера.
function loadRates() {
  try {
    const s = JSON.parse(localStorage.getItem("crm_rates") || "{}");
    return { UAH: 1, USD: Number(s.USD) || 41.6, EUR: Number(s.EUR) || 45.2 };
  } catch { return { UAH: 1, USD: 41.6, EUR: 45.2 }; }
}
let RATES = loadRates();
const toUAH = (price, cur) => Math.round(price * (RATES[cur] || 1));
// Підтягнути серверний курс (при завантаженні вкладки). Розсилає crm-rates-updated → ререндер.
function syncRatesFromServer() {
  fetch("/api/settings/rates").then(r => r.json()).then(j => {
    if (!j || !j.ok || !j.rates) return;
    const u = Number(j.rates.USD), e = Number(j.rates.EUR);
    if (!(u > 0) || !(e > 0)) return;
    if (u === RATES.USD && e === RATES.EUR) return;
    RATES.USD = u; RATES.EUR = e;
    localStorage.setItem("crm_rates", JSON.stringify({ USD: u, EUR: e }));
    window.dispatchEvent(new Event("crm-rates-updated"));
  }).catch(() => {});
}
syncRatesFromServer();

// ── Formatters ────────────────────────────────────────────────────────────────
const fmtBareP  = n => Math.round(n).toLocaleString("uk-UA").replace(/ /g," ").replace(/,/g," ");
const fmtCurP   = (n, cur="UAH") => n == null ? "—" : fmtBareP(n) + " " + (CUR_SYM[cur]||"₴");
const fmtUAHP   = n => fmtCurP(n,"UAH");
const fmtSignedP = (n, cur="UAH") => n==null ? "—" : (n>=0?"+":"−") + fmtBareP(Math.abs(n)) + " " + (CUR_SYM[cur]||"₴");
const fmtPctP   = n => n==null ? "—" : n.toLocaleString("uk-UA",{minimumFractionDigits:1,maximumFractionDigits:1}).replace(".",",")+"%";
const fmtPctSignedP = n => n==null ? "—" : (n>=0?"+":"−")+Math.abs(n).toLocaleString("uk-UA",{maximumFractionDigits:1}).replace(".",",")+"%";

function marginColorP(absMargin, pct) {
  if (absMargin < 0) return "var(--danger)";
  if (pct < TARGET_MARGIN) return "var(--warning)";
  return "var(--success)";
}

// ── Авто-ціноутворення (рекомендація ціни на сайт) ─────────────────────────────
// Ступені маржі (markup на ЗАКУП). Відкалібровано 02.07 по 28 живих кейсах Фокстрота.
// targetPct — «хотілка» коли конкурента нема; minPct/minAbs — ПІД, нижче не опускаємось.
const PRICE_TIERS = [
  { max: 1000,     targetPct: 0.18, minPct: 0.10, minAbs: 60   },
  { max: 2500,     targetPct: 0.10, minPct: 0.04, minAbs: 120  },
  { max: 5000,     targetPct: 0.10, minPct: 0.04, minAbs: 195  },
  { max: 15000,    targetPct: 0.07, minPct: 0.03, minAbs: 250  },
  { max: 30000,    targetPct: 0.05, minPct: 0.03, minAbs: 600  },
  { max: Infinity, targetPct: 0.04, minPct: 0.025, minAbs: 1200 },
];
const pTierP = buy => PRICE_TIERS.find(t => buy < t.max) || PRICE_TIERS[PRICE_TIERS.length - 1];
const NO_HOT_PCT = 0.10;   // нема даних Hotline → закуп +10%, і все
const DUMP_PCT   = 0.10;   // демпінг і нікого вигідно перебити → закуп +10%
// Запобіжник «не той товар»: мін. Hotline дає націнку понад 500% (×6 від закупки)
// І різниця >1500₴ — майже напевно пошук зматчив ІНШИЙ товар (кейс: колонка за
// 1170₴ ↔ hi-fi за 166 800₴). Легальні надприбуткові кейси бувають до ~500% (слова юзера).
const HOT_MISMATCH_X   = 6;
const HOT_MISMATCH_ABS = 1500;
const hotMismatchP = (buy, cheapest) => buy > 0 && cheapest > buy * HOT_MISMATCH_X && (cheapest - buy) > HOT_MISMATCH_ABS;

// Основний склад (Новоселиця/Чернівці) — ЗАВЖДИ пріоритетне джерело, якщо товар у наявності,
// навіть коли інший постачальник дешевший (бізнес-правило). Гейт по назві — стійко до cbPrefix.
const isMainStockP = sup => /новосел|чернів|чернов|основн/i.test(((PRICE_SUPS[sup] || {}).label || "") + " " + (sup || ""));

// Крок підрізу: найбільше …49/…99 СТРОГО НИЖЧЕ price. Якщо конкурент рівно на сітці
// (…00/…50) — це просто price−1 (11950→11949, а не 11899 — не віддаємо зайві 50 грн).
// Великий запас → g=100 (лише …99); дешеве <1000 → …9 (g=10).
function underStepP(price, big) {
  const g = price < 1000 ? 10 : (big ? 100 : 50);
  return Math.floor(price / g) * g - 1;
}
// Найбільше …99 ≤ x (для «−100 і до …99»): 9277→9199, 4780→4699.
const down99P = x => Math.floor((x + 1) / 100) * 100 - 1;
// Penny-кластер: ≥3 ціни поспіль у смузі до ~1% (макс 35 грн) з низу лесенки = «шопи-боти».
// (калібрування 02.07: 3526/3527/3560 і 3649/3656/3691 — юзер бачить це як один кластер)
function pennyClusterP(comp) {
  if (comp.length < 3) return null;
  let i = 1;
  while (i < comp.length && comp[i] - comp[i - 1] <= Math.min(35, Math.max(5, Math.round(comp[i - 1] * 0.01)))) i++;
  if (i < 3) return null;
  return { clusterMin: comp[0], clusterMax: comp[i - 1], nextReal: i < comp.length ? comp[i] : null };
}

// Головна рекомендація. buy — закуп у грн; hotOffers — лесенка Hotline [{price, us}]; currentPrice — ціна на сайті зараз.
// Повертає { price, floor, target, cheapest, profit, marginPct, mode, reason }.
function recommendPriceP(buy, hotOffers, currentPrice) {
  buy = Math.round(Number(buy) || 0);
  const cur = Math.round(Number(currentPrice) || 0);
  const tier = pTierP(buy);
  const floor  = Math.round(buy + Math.max(buy * tier.minPct, tier.minAbs));
  const target = Math.round(buy + Math.max(buy * tier.targetPct, tier.minAbs));
  const wrap = (price, mode, reason) => {
    price = Math.max(Math.round(price), floor);
    // Тай-брейк: рівно збіглись із конкурентом (4199 = megadom 4199) → пірнаємо до …5 (4195),
    // якщо це не пробиває пол. Це «під нього», а не «поруч із ним».
    while (comp.includes(price)) {
      const q = Math.floor((price - 1) / 5) * 5;
      if (q < floor) break;
      price = q;
    }
    return { price, floor, target, cheapest: (comp[0] ?? null), profit: price - buy,
             marginPct: price > 0 ? ((price - buy) / price) * 100 : 0, mode, reason };
  };

  const comp = (hotOffers || []).filter(o => !o.us && Number(o.price) > 0).map(o => Math.round(o.price)).sort((a, b) => a - b);
  if (!buy) return { price: null, floor, target, cheapest: null, profit: null, marginPct: null, mode: "skip", reason: "Немає закупки" };
  if (!comp.length) return wrap(underStepP(Math.round(buy * (1 + NO_HOT_PCT)), false), "review", "Немає даних Hotline — закуп +10%");

  const cheapest = comp[0];
  // Лесенка схожа на ІНШИЙ товар → не використовуємо її взагалі: закуп +10% і review
  if (hotMismatchP(buy, cheapest)) {
    return wrap(underStepP(Math.round(buy * (1 + NO_HOT_PCT)), false), "review",
      `Мін. Hotline ${fmtUAHP(cheapest)} при закупці ${fmtUAHP(buy)} — схоже, знайшовся ІНШИЙ товар; закуп +10%`);
  }
  // «Хвіст»-бот: конкурент на 1-2 грн НИЖЧЕ нашої поточної ціни = слідкує за нами,
  // гнатися безглуздо (переставить). Ігноруємо його при виборі кого перебивати.
  const isTailBot = c => cur > 0 && c < cur && (cur - c) <= 2;

  // 1) Демпінг (≥2 конкурента нижче закупки): не воюємо внизу — встаємо під найближчого,
  //    кого ВИГІДНО перебити (≥ пол); нікого → закуп +10% до …99.
  const belowBuy = comp.filter(c => c < buy).length;
  if (cheapest < buy && belowBuy >= 2) {
    for (const c of comp) {
      if (c <= buy || isTailBot(c)) continue;
      const p = underStepP(c, false);
      if (p >= floor) return wrap(p, "review", `Нижче закупки ${belowBuy} — не воюємо, під найближчого вигідного ${fmtUAHP(c)}`);
    }
    return wrap(down99P(Math.round(buy * (1 + DUMP_PCT))), "review", `Нижче закупки ${belowBuy} — не воюємо, закуп +10%`);
  }

  // 2) Мало пропозицій (≤2 конкурента) і ми вже №1: не тулитись впритул до №2 по тонких
  //    даних — обережний відступ −100 і до …99 (9377→9199, 8714→8599), нижче поточної не падаємо.
  if (comp.length <= 2 && cur > 0 && cur < cheapest) {
    let p = Math.max(down99P(cheapest - 100), floor);
    if (cur > p) p = cur;
    return wrap(p, "auto", `Мало пропозицій (${comp.length}) — обережно, −100 під ${fmtUAHP(cheapest)}`);
  }

  // 3) Penny-кластер низько + дорожчий реальний вище → виходимо з війни:
  //    −100 під наступного РЕАЛЬНОГО і до …99 (краща маржа), якщо це ≥ пола.
  const cl = pennyClusterP(comp);
  if (cl && cl.nextReal && (cl.nextReal - cl.clusterMax) > Math.max(100, cl.clusterMax * 0.025)) {
    const underNext = down99P(cl.nextReal - 100);
    if (underNext >= floor) {
      return wrap(underNext, "auto", `Кластер ботів ${fmtUAHP(cl.clusterMin)}–${fmtUAHP(cl.clusterMax)} — виходимо з війни, −100 під ${fmtUAHP(cl.nextReal)}`);
    }
    // інакше — кластер це фактично верх ринку: падаємо у звичайну логіку (мінімальний підріз)
  }

  // 4) Перебиваємо найдешевшого, кого можна з прибутком (≥ пол); інакше встаємо під наступного
  for (let i = 0; i < comp.length; i++) {
    const c = comp[i];
    // За «хвостом» не ганяємось, АЛЕ лише коли пропуск щось дає: наступний реальний ≥100 вище
    // (5088→5207 skip ✓; 4098→4106 — пропускати безглуздо, б'ємось як завжди).
    if (isTailBot(c) && comp[i + 1] && comp[i + 1] - c >= 100) continue;
    const big = (c - buy) / buy > 0.20;     // великий запас → агресивніший крок
    const p = underStepP(c, big);
    if (p >= floor) {
      return wrap(p, "auto", c === cheapest ? `Перебиваємо найдешевшого ${fmtUAHP(c)}`
        : isTailBot(cheapest) ? `«Хвіст»-бот ${fmtUAHP(cheapest)} — ігноруємо, під ${fmtUAHP(c)}`
        : `#1 (${fmtUAHP(cheapest)}) нижче пола — під ${fmtUAHP(c)}`);
    }
  }
  // 5) Нікого не перебити з прибутком → пол
  return wrap(floor, "review", "Усі конкуренти нижче пола — ставимо пол");
}

// ── Supplier color palette ────────────────────────────────────────────────────
const PRICE_PALETTE = ["#3B82F6","#F59E0B","#8B5CF6","#14B8A6","#F43F5E","#10B981","#6366F1","#F97316","#EC4899","#06B6D4"];
function genSupColor(str) {
  if (!str) return PRICE_PALETTE[0];
  let h = 0;
  for (let i=0;i<str.length;i++) h = str.charCodeAt(i)+((h<<5)-h);
  return PRICE_PALETTE[Math.abs(h) % PRICE_PALETTE.length];
}
function fmtLastSync(iso) {
  if (!iso) return "ніколи";
  const d = new Date(iso), now = new Date();
  const isToday = d.toDateString()===now.toDateString();
  const hm = `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`;
  if (isToday) return `Сьогодні, ${hm}`;
  const diff = Math.floor((now-d)/86400000);
  if (diff===1) return `Вчора, ${hm}`;
  if (diff<7)   return `${diff} днів тому`;
  return d.toLocaleDateString("uk-UA");
}
function parseCurCode(raw) {
  const s = String(raw ?? "").toLowerCase();
  if (s==="usd"||s==="$"||s==="дол") return "USD";
  if (s==="eur"||s==="€"||s==="євро") return "EUR";
  return "UAH";
}

// ── Dynamic supplier data (populated from API) ────────────────────────────────
let PRICE_SUPS = {};    // cbPrefix → { label, color, cur, source, last, positions, changed, status, author }
let SUP_ORDER_P = [];   // [cbPrefix, ...]

// ── Sparkline history (deterministic, from price) ─────────────────────────────
function seriesFromP(seed, n, base, vol, drift) {
  let s = seed % 2147483647; if (s<=0) s+=2147483646;
  const rnd = () => (s=(s*16807)%2147483647)/2147483647;
  const out=[]; let v=base;
  for (let i=0;i<n;i++) { v+=(rnd()-.5)*vol+drift; out.push(Math.max(base*.7,Math.round(v/10)*10)); }
  return out;
}

function deriveOffer(o, pid, idx) {
  const uah=toUAH(o.price,o.cur);
  const prevUah=o.prev!=null?toUAH(o.prev,o.cur):null;
  const delta=prevUah!=null?uah-prevUah:0;
  const deltaPct=prevUah?(delta/prevUah)*100:0;
  const change=delta>0?"up":delta<0?"down":"same";
  return { ...o, sup:o.cbPrefix||o.sup, uah, prevUah, delta, deltaPct, change,
    hist: seriesFromP(pid*100+idx*7+3, 30, uah*1.03, uah*.02, -uah*.0012) };
}

function deriveProducts(rawList) {
  return rawList.map((p, idx) => {
    const offers = p.offers.map((o,i) => deriveOffer({ ...o, sup:o.cbPrefix }, idx+1, i));
    if (!offers.length) return null;
    const inStock = offers.filter(o=>o.stock);
    const pool = inStock.length ? inStock : offers;
    const best = pool.reduce((a,b)=>b.uah<a.uah?b:a, pool[0]);
    const prices = offers.map(o=>o.uah);
    const spread = prices.length>1 ? Math.max(...prices)-Math.min(...prices) : 0;
    const our = p.our ?? null;
    const margin = our!=null ? our-best.uah : null;
    const marginPct = our!=null&&our>0 ? (margin/our)*100 : null;
    const anyUp=offers.some(o=>o.change==="up"), anyDown=offers.some(o=>o.change==="down");
    const allOut=inStock.length===0;
    const marginRisk=margin!=null&&best.change==="up"&&marginPct<TARGET_MARGIN;
    return { id:idx+1, key:p.key||p.art||p.name||String(idx+1),
      art:p.art||"", name:p.name||"", cat:p.cat||"", our,
      offers, best, spread, margin, marginPct, bestChange:best.change,
      anyUp, anyDown, allOut, marginRisk, supCount:offers.length,
      inStockCount:inStock.length, isNew:false, bestHist:best.hist,
      matchType:p.matchType||"name", manual:!!p.manual,
      mdm:!!p.mdm, ref:!!p.ref,
      offerIds:p.offerIds||offers.map(o=>o.offerId).filter(Boolean),
      confidence:p.confidence||null };
  }).filter(Boolean);
}

let PRICE_PRODUCTS = [];

function calcSummary(products) {
  return [
    { key:"all",  label:"Всього позицій",    value:products.length, tone:"neutral" },
    { key:"up",   label:"Подорожчало",       value:products.filter(p=>p.anyUp).length, tone:"danger", pulse:true },
    { key:"down", label:"Подешевшало",       value:products.filter(p=>p.anyDown).length, tone:"success" },
    { key:"new",  label:"Нові позиції",      value:0, tone:"info" },
    { key:"out",  label:"Немає в наявності", value:products.filter(p=>p.allOut).length, tone:"muted" },
    { key:"risk", label:"Маржа під загрозою",value:products.filter(p=>p.marginRisk).length, tone:"warning" },
    { key:"check",label:"Потребують перевірки",value:products.filter(p=>p.confidence?.level==="low").length, tone:"warning" },
  ];
}
let PRICE_SUMMARY = calcSummary([]);
let PRICE_DIGEST = null;

// ── Procurement chips ─────────────────────────────────────────────────────────
function SupplierChipP({ sup, size="md", star, truncate, label }) {
  // Фолбек: якщо постачальника ще нема в PRICE_SUPS (напр. статус прайсів не довантажився
  // або постачальник неактивний) — беремо назву з самого офера (supplierName), інакше cbPrefix.
  // Так імʼя постачальника показується ЗАВЖДИ (раніше повертали null → на мобілці зникали назви).
  const s = PRICE_SUPS[sup] || { label: label || sup || "—", color: genSupColor(sup || "") };
  const sm = size==="sm";
  const bg = s.color+"26"; // ~15% opacity hex
  return (
    <span title={truncate?s.label:undefined} style={{ display:"inline-flex", alignItems:"center", gap:5,
      height:sm?18:20, padding:sm?"0 7px":"0 8px", borderRadius:999,
      background:bg, color:s.color, fontSize:sm?10.5:11, fontWeight:600, whiteSpace:"nowrap",
      ...(truncate ? { minWidth:0, maxWidth:"100%" } : {}) }}>
      {star && <Icon name="star" size={sm?9:10} style={{ flexShrink:0 }}/>}
      <span style={{ width:5, height:5, borderRadius:"50%", background:s.color, flexShrink:0 }}/>
      <span style={truncate ? { overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" } : undefined}>{s.label}</span>
    </span>
  );
}

function SupplierInlineP({ sup, label }) {
  const s = PRICE_SUPS[sup] || { label: label || sup || "—", color: "var(--fg-secondary)" };
  return <span style={{ color:s.color, fontWeight:600 }}>{s.label}</span>;
}

function BestBadgeP({ size="md" }) {
  const sm = size==="sm";
  return (
    <span style={{ display:"inline-flex", alignItems:"center", gap:4,
      height:sm?18:20, padding:sm?"0 7px 0 6px":"0 8px 0 6px", borderRadius:999,
      background:"rgba(16,185,129,.14)", color:"var(--success)",
      fontSize:sm?10:10.5, fontWeight:700, whiteSpace:"nowrap" }}>
      <Icon name="star" size={sm?10:11}/> найдешевший
    </span>
  );
}

// ── Точність зіставлення: бейдж + tooltip ──────────────────────────────────────
const CONF_META = {
  high:   { color:"var(--success)", soft:"rgba(16,185,129,.14)", icon:"check-circle",   label:"Точний збіг" },
  medium: { color:"#3B82F6",        soft:"rgba(59,130,246,.14)", icon:"tag",            label:"За моделлю" },
  low:    { color:"var(--warning)", soft:"rgba(245,158,11,.14)", icon:"alert-triangle", label:"За назвою" },
  manual: { color:"var(--accent)",  soft:"var(--accent-soft)",   icon:"link",           label:"Підтверджено" },
  none:   { color:"var(--fg-muted)",soft:"transparent",          icon:null,             label:"" },
};

function confDetailP(p) {
  if (p.confidence?.level==="manual") return "звʼязано вручну";
  if (p.matchType==="article") return "збіг артикула · " + (p.art||"");
  if (p.matchType==="model")   return "модельний токен";
  return "лише за назвою";
}

function ConfidenceTipP({ p, m, pos }) {
  const c = p.confidence;
  const Row = ({ label, value }) => (
    <div style={{ display:"flex", alignItems:"baseline", gap:8 }}>
      <span style={{ color:"var(--fg-muted)", flexShrink:0 }}>{label}</span>
      <span style={{ flex:1, height:1, alignSelf:"center", background:"var(--border-subtle)" }}/>
      <span style={{ color:"var(--fg-primary)", fontWeight:500, textAlign:"right" }}>{value}</span>
    </div>
  );
  return (
    <div style={{ position:"fixed", left:pos.left, top:pos.top, zIndex:90, width:252, padding:"12px 13px",
      background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:10, boxShadow:"var(--shadow-2)", pointerEvents:"none" }}>
      <div style={{ display:"flex", alignItems:"center", gap:7, marginBottom:8 }}>
        <Icon name={m.icon} size={14} color={m.color}/>
        <span style={{ fontSize:12.5, fontWeight:600, color:m.color }}>{c.label}</span>
        {c.pct!=null && <><div style={{ flex:1 }}/><span style={{ fontFamily:"var(--font-mono)", fontSize:12.5, fontWeight:700, color:m.color }}>{c.pct}%</span></>}
      </div>
      {c.pct!=null && (
        <div style={{ height:4, background:"var(--bg-base)", borderRadius:2, overflow:"hidden", marginBottom:10 }}>
          <div style={{ height:"100%", width:`${c.pct}%`, background:m.color, borderRadius:2 }}/>
        </div>
      )}
      <div style={{ display:"flex", flexDirection:"column", gap:6, fontSize:11.5 }}>
        <Row label="Склеєно за" value={confDetailP(p)}/>
        <Row label="Офери в групі" value={`${p.supCount} пост.`}/>
      </div>
      {c.level==="low" && (
        <div style={{ marginTop:10, paddingTop:9, borderTop:"1px solid var(--border-subtle)", fontSize:11, color:"var(--warning)", display:"flex", gap:6, alignItems:"flex-start", lineHeight:1.4 }}>
          <Icon name="alert-triangle" size={12} color="var(--warning)" style={{ flexShrink:0, marginTop:1 }}/> Низька впевненість — перевірте привʼязку вручну.
        </div>
      )}
    </div>
  );
}

function ConfidenceBadgeP({ p, iconOnly=false, showPct=true, size="sm", interactive=true }) {
  const c = p.confidence; if (!c || c.level==="none") return null;
  const m = CONF_META[c.level]; if (!m) return null;
  const [hov, setHov] = useState(false);
  const [pos, setPos] = useState(null);
  const sm = size==="sm";
  const show = e => {
    if (!interactive) return;
    const r = e.currentTarget.getBoundingClientRect();
    setPos({ left:Math.min(r.left, window.innerWidth-264), top:r.bottom+6 });
    setHov(true);
  };
  return (
    <span style={{ position:"relative", display:"inline-flex", flexShrink:0 }} onMouseEnter={show} onMouseLeave={()=>setHov(false)}>
      <span style={{ display:"inline-flex", alignItems:"center", gap:iconOnly?0:4,
        height:sm?19:22, padding:iconOnly?0:(sm?"0 7px":"0 9px"),
        width:iconOnly?(sm?19:22):"auto", justifyContent:"center",
        borderRadius:999, background:m.soft, color:m.color,
        fontSize:sm?10:11, fontWeight:600, whiteSpace:"nowrap", cursor:interactive?"help":"default" }}>
        <Icon name={m.icon} size={sm?11:12}/>
        {!iconOnly && <span>{m.label}{showPct && c.pct!=null ? ` · ${c.pct}%` : ""}</span>}
      </span>
      {hov && pos && <ConfidenceTipP p={p} m={m} pos={pos}/>}
    </span>
  );
}

// Бейдж стану товару: MDM (заблокований — чорний список) / REF (відновлений).
function FlagBadgeP({ mdm, refurb, size="sm" }) {
  if (!mdm && !refurb) return null;
  const sm = size==="sm";
  const m = mdm
    ? { label:"MDM · заблокований", color:"var(--danger)",  soft:"rgba(244,63,94,.13)", icon:"lock", title:"Заблокований пристрій (MDM) — не вязати, не ставити джерелом, не на вітрину" }
    : { label:"REF · відновлений",  color:"var(--warning)", soft:"rgba(245,158,11,.13)", icon:"refresh-ccw", title:"Відновлений (REF) — лише ручна привʼязка, ніколи не авто-джерело" };
  return (
    <span title={m.title} style={{ display:"inline-flex", alignItems:"center", gap:4, flexShrink:0,
      height:sm?19:22, padding:sm?"0 7px":"0 9px", borderRadius:999, background:m.soft, color:m.color,
      fontSize:sm?10:11, fontWeight:700, whiteSpace:"nowrap", cursor:"help" }}>
      <Icon name={m.icon} size={sm?11:12}/>{m.label}
    </span>
  );
}

// Дефолтний постачальник-джерело для картки: REF ніколи не береться автоматично.
// Якщо best-офер REF — беремо найдешевший НЕ-REF (пріоритет «в наявності»); якщо всі REF — лишаємо best.
function defaultSourceSupP(product) {
  if (!product || !product.best) return null;
  if (!product.best.ref) return product.best.sup;
  const nonRef = (product.offers || []).filter(o => !o.ref);
  if (!nonRef.length) return product.best.sup;
  const inStock = nonRef.filter(o => o.stock);
  const pool = inStock.length ? inStock : nonRef;
  return pool.reduce((a, b) => (b.uah < a.uah ? b : a), pool[0]).sup;
}

// Маленький REF-чип біля постачальника в рядках оферів
function RefChipP() {
  return (
    <span title="REF — відновлений: джерелом лише вручну" style={{ display:"inline-flex", alignItems:"center", gap:3, flexShrink:0,
      height:18, padding:"0 6px", borderRadius:999, background:"rgba(245,158,11,.13)", color:"var(--warning)",
      fontSize:9.5, fontWeight:700, whiteSpace:"nowrap" }}>
      <Icon name="refresh-ccw" size={10}/> REF
    </span>
  );
}

function ChangeBadgeP({ change, pct }) {
  if (change==="same") return <span style={{ color:"var(--fg-disabled)" }}>—</span>;
  const map = {
    up:   { color:"var(--danger)",  bg:"rgba(244,63,94,.13)",  icon:"arrow-up-right" },
    down: { color:"var(--success)", bg:"rgba(16,185,129,.13)", icon:"arrow-down-right" },
    new:  { color:"#3B82F6",        bg:"rgba(59,130,246,.13)", icon:"sparkle" },
  };
  const c = map[change];
  return (
    <span style={{ display:"inline-flex", alignItems:"center", gap:3, height:19, padding:"0 7px 0 5px", borderRadius:999,
      background:c.bg, color:c.color, fontSize:10.5, fontWeight:700, fontFamily:"var(--font-mono)", whiteSpace:"nowrap" }}>
      <Icon name={c.icon} size={11}/>
      {change==="new"?"нове":fmtPctSignedP(pct)}
    </span>
  );
}

const SOURCE_META_P = {
  telegram: { icon:"send",             label:"Telegram", color:"#229ED9" },
  excel:    { icon:"file-spreadsheet", label:"Excel",    color:"#1F8A5B" },
  api:      { icon:"plug-zap",         label:"API",      color:"var(--accent)" },
};
function SourceBadgeP({ source, size="md" }) {
  const m = SOURCE_META_P[source]||{ icon:"pencil", label:"Вручну", color:"var(--fg-muted)" };
  const sm = size==="sm";
  return (
    <span style={{ display:"inline-flex", alignItems:"center", gap:5, height:sm?18:22,
      padding:sm?"0 7px":"0 9px", borderRadius:999, background:"var(--bg-base)",
      border:"1px solid var(--border-default)", color:"var(--fg-secondary)",
      fontSize:sm?10.5:11.5, fontWeight:500, whiteSpace:"nowrap" }}>
      <Icon name={m.icon} size={sm?11:13} color={m.color}/> {m.label}
    </span>
  );
}

function StockDotP({ stock, qty }) {
  return (
    <span style={{ display:"inline-flex", alignItems:"center", gap:6, fontSize:11.5, color:stock?"var(--fg-secondary)":"var(--fg-muted)" }}>
      <span style={{ width:7, height:7, borderRadius:"50%", background:stock?"var(--success)":"var(--fg-disabled)", flexShrink:0 }}/>
      {stock ? <span style={{ fontFamily:"var(--font-mono)" }}>{qty} шт</span> : "немає"}
    </span>
  );
}

// ── Sparkline ─────────────────────────────────────────────────────────────────
function SparklineP({ data, w=58, h=22 }) {
  if (!data||data.length<2) return <span style={{ color:"var(--fg-disabled)" }}>—</span>;
  const min=Math.min(...data), max=Math.max(...data), span=max-min||1;
  const pts=data.map((v,i)=>[(i/(data.length-1))*(w-2)+1, h-2-((v-min)/span)*(h-4)]);
  const d=pts.map((p,i)=>(i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)).join(" ");
  const down=data[data.length-1]<=data[0];
  const stroke=down?"var(--success)":"var(--danger)";
  return (
    <svg width={w} height={h} style={{ display:"block", overflow:"visible" }}>
      <path d={d} fill="none" stroke={stroke} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={pts[pts.length-1][0]} cy={pts[pts.length-1][1]} r="1.8" fill={stroke}/>
    </svg>
  );
}

function PriceHistoryChartP({ series, height=150 }) {
  const w=392, h=height, padL=6, padR=6, padT=14, padB=18;
  const min=Math.min(...series), max=Math.max(...series), span=max-min||1;
  const x=i=>padL+(i/(series.length-1))*(w-padL-padR);
  const y=v=>padT+(1-(v-min)/span)*(h-padT-padB);
  const line=arr=>arr.map((v,i)=>(i?"L":"M")+x(i).toFixed(1)+" "+y(v).toFixed(1)).join(" ");
  const area=line(series)+` L${x(series.length-1).toFixed(1)} ${(h-padB).toFixed(1)} L${x(0).toFixed(1)} ${(h-padB).toFixed(1)} Z`;
  const down=series[series.length-1]<=series[0];
  const col=down?"var(--success)":"var(--danger)";
  return (
    <svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display:"block" }}>
      <defs>
        <linearGradient id="pricesFill" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={col} stopOpacity="0.16"/>
          <stop offset="100%" stopColor={col} stopOpacity="0"/>
        </linearGradient>
      </defs>
      {[0,.5,1].map(t=>(
        <line key={t} x1={padL} x2={w-padR} y1={padT+t*(h-padT-padB)} y2={padT+t*(h-padT-padB)} stroke="var(--border-subtle)" strokeWidth="1"/>
      ))}
      <path d={area} fill="url(#pricesFill)"/>
      <path d={line(series)} fill="none" stroke={col} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={x(series.length-1)} cy={y(series[series.length-1])} r="3" fill={col} stroke="var(--bg-panel)" strokeWidth="1.5"/>
    </svg>
  );
}

// ── SummaryBar ────────────────────────────────────────────────────────────────
function SummaryBar({ active, onPick, summaryData }) {
  const data = summaryData || PRICE_SUMMARY;
  const tone = { neutral:"var(--fg-primary)", muted:"var(--fg-muted)", danger:"var(--danger)", warning:"var(--warning)", success:"var(--success)", info:"#3B82F6" };
  return (
    <div style={{ display:"flex", gap:10, padding:"16px 24px 0" }}>
      {data.map(t => {
        const isOn = active===t.key && t.key!=="all";
        return (
          <button key={t.key} onClick={()=>onPick(isOn?"all":t.key)} style={{
            flex:1, textAlign:"left", padding:"12px 14px", borderRadius:10, cursor:"pointer",
            background:"var(--bg-panel)", fontFamily:"inherit",
            border:`1px solid ${isOn?"var(--accent)":"var(--border-subtle)"}`,
            boxShadow:isOn?"0 0 0 3px var(--accent-soft)":"none",
            transition:"border-color 120ms, box-shadow 120ms",
          }}
            onMouseEnter={e=>{ if(!isOn) e.currentTarget.style.borderColor="var(--border-strong)"; }}
            onMouseLeave={e=>{ if(!isOn) e.currentTarget.style.borderColor="var(--border-subtle)"; }}>
            <div style={{ display:"flex", alignItems:"center", gap:7, marginBottom:8 }}>
              <span style={{ fontFamily:"var(--font-mono)", fontSize:22, fontWeight:700, color:tone[t.tone] }}>{fmtBareP(t.value)}</span>
              {t.pulse && t.value>0 && <span style={{ width:6, height:6, borderRadius:"50%", background:"var(--danger)", animation:"prices-blink 1.1s ease-in-out infinite", display:"inline-block" }}/>}
            </div>
            <div style={{ fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.3 }}>{t.label}</div>
          </button>
        );
      })}
    </div>
  );
}

// ── DigestBanner ──────────────────────────────────────────────────────────────
function DigestBanner({ digest, onPick, onDismiss, onOpenList }) {
  if (!digest) return null;
  const counts = digest.counts || {};
  const seg = [
    { key:"new",  label:"нових",      value:counts.new||0,  color:"#3B82F6",       icon:"sparkle" },
    { key:"up",   label:"подорожчало",value:counts.up||0,   color:"var(--danger)",  icon:"arrow-up-right" },
    { key:"down", label:"подешевшало",value:counts.down||0, color:"var(--success)", icon:"arrow-down-right" },
    { key:"out",  label:"зникли",     value:counts.out||0,  color:"var(--fg-muted)",icon:"package-x" },
  ];
  const supInfo = PRICE_SUPS[digest.sup];
  return (
    <div style={{ display:"flex", alignItems:"center", gap:14, margin:"12px 24px 0", padding:"11px 14px",
      background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:10,
      borderLeft:"3px solid var(--accent)" }}>
      <div style={{ width:30, height:30, borderRadius:8, background:"var(--accent-soft)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}>
        <Icon name="history" size={16} color="var(--accent)"/>
      </div>
      <div style={{ minWidth:0 }}>
        <div style={{ fontSize:12.5, color:"var(--fg-primary)", fontWeight:600 }}>
          Останнє завантаження {supInfo && <span style={{ color:supInfo.color, fontWeight:600 }}>· {supInfo.label}</span>}
        </div>
        <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:1 }}>{digest.when} · оновлено</div>
      </div>
      <div style={{ display:"flex", gap:8, marginLeft:6 }}>
        {seg.map(s=>(
          <button key={s.key} onClick={()=>onPick(s.key)} style={{
            display:"inline-flex", alignItems:"center", gap:6, height:30, padding:"0 11px", borderRadius:8,
            background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit" }}>
            <Icon name={s.icon} size={13} color={s.color}/>
            <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, fontSize:13, color:s.color }}>{s.value}</span>
            <span style={{ fontSize:11.5, color:"var(--fg-secondary)" }}>{s.label}</span>
          </button>
        ))}
      </div>
      <div style={{ flex:1 }}/>
      <button onClick={onOpenList} style={{ background:"transparent", border:0, color:"var(--accent)", cursor:"pointer", fontSize:12, fontFamily:"inherit", whiteSpace:"nowrap" }}>Деталі завантаження</button>
      <button onClick={onDismiss} style={{ width:28, height:28, border:0, borderRadius:6, background:"transparent", color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}>
        <Icon name="x" size={15}/>
      </button>
    </div>
  );
}

// ── MarginRiskStrip ───────────────────────────────────────────────────────────
function MarginRiskStrip({ count, onShow }) {
  if (!count) return null;
  return (
    <div style={{ display:"flex", alignItems:"center", gap:12, margin:"12px 24px 0", padding:"11px 16px",
      background:"rgba(244,63,94,.10)", border:"1px solid rgba(244,63,94,.30)", borderRadius:10 }}>
      <Icon name="trending-up" size={18} color="var(--danger)" style={{ flexShrink:0 }}/>
      <div style={{ minWidth:0 }}>
        <span style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>
          {count} {count===1?"товар подорожчав":"товари подорожчали"} у найдешевшого постачальника
        </span>
        <span style={{ fontSize:12, color:"var(--fg-secondary)", marginLeft:6 }}>— маржа впала нижче цільових {TARGET_MARGIN}%</span>
      </div>
      <div style={{ flex:1 }}/>
      <Button size="sm" variant="danger" leftIcon="alert-triangle" onClick={onShow}>Переглянути ризик</Button>
    </div>
  );
}

// ── RateEditorBtn ─────────────────────────────────────────────────────────────
// Курс глобальний: зберігається НА СЕРВЕРІ (лише власник), локалка — кеш.
function RateEditorBtn() {
  const [open, setOpen] = useState(false);
  const [usd, setUsd] = useState("");
  const [eur, setEur] = useState("");
  const [err, setErr] = useState("");
  const isOwner = localStorage.getItem("crm_user_role") === "owner";

  const openModal = () => { syncRatesFromServer(); setUsd(String(RATES.USD)); setEur(String(RATES.EUR)); setErr(""); setOpen(true); };

  const save = () => {
    const u = parseFloat(usd), e = parseFloat(eur);
    if (!u || !e || u <= 0 || e <= 0) return;
    setErr("");
    fetch("/api/settings/rates", { method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ USD: u, EUR: e }) })
      .then(r => r.json())
      .then(j => {
        if (!j || !j.ok) { setErr((j && j.error) || "Не збережено"); return; }
        RATES.USD = u; RATES.EUR = e;
        localStorage.setItem("crm_rates", JSON.stringify({ USD: u, EUR: e }));
        window.dispatchEvent(new Event("crm-rates-updated"));
        setOpen(false);
      })
      .catch(e2 => setErr(e2.message));
  };

  return (
    <>
      <button onClick={openModal} title="Редагувати курс валют"
        style={{ display:"flex", alignItems:"center", gap:5, padding:"0 10px", height:34, borderRadius:8,
          border:"1px solid var(--border-default)", background:"var(--bg-panel)",
          color:"var(--fg-secondary)", fontSize:12.5, cursor:"pointer", fontFamily:"inherit", flexShrink:0 }}>
        <Icon name="coins" size={13}/>
        <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-primary)", fontWeight:600 }}>${RATES.USD}</span>
      </button>
      {open && (
        <div style={{ position:"fixed", inset:0, background:"rgba(0,0,0,.5)", zIndex:9000,
          display:"flex", alignItems:"center", justifyContent:"center" }}
          onClick={()=>setOpen(false)}>
          <div style={{ background:"var(--bg-panel)", border:"1px solid var(--border-default)",
            borderRadius:14, padding:24, minWidth:280, boxShadow:"0 12px 40px rgba(0,0,0,.35)" }}
            onClick={e=>e.stopPropagation()}>
            <div style={{ fontSize:15, fontWeight:600, color:"var(--fg-primary)", marginBottom:6 }}>Курс валют</div>
            <div style={{ fontSize:11.5, color:"var(--fg-tertiary)", marginBottom:14 }}>
              {isOwner ? "Глобальний — застосується у всіх менеджерів на всіх пристроях" : "Курс задає власник — у вас лише перегляд"}
            </div>
            <div style={{ display:"flex", flexDirection:"column", gap:12 }}>
              {[["USD","$",usd,setUsd],["EUR","€",eur,setEur]].map(([code,sym,val,set])=>(
                <label key={code} style={{ fontSize:12.5, color:"var(--fg-secondary)", display:"block" }}>
                  {code} ({sym}) → грн
                  <input type="number" min="1" step="0.1" value={val} onChange={e=>set(e.target.value)} disabled={!isOwner}
                    style={{ display:"block", marginTop:5, width:"100%", boxSizing:"border-box",
                      height:38, padding:"0 12px", borderRadius:8, border:"1px solid var(--border-default)",
                      background:"var(--bg-base)", color:"var(--fg-primary)", opacity: isOwner ? 1 : .6,
                      fontSize:15, fontFamily:"var(--font-mono)", outline:"none" }}/>
                </label>
              ))}
            </div>
            {err && <div style={{ fontSize:12, color:"var(--danger)", marginTop:10 }}>{err}</div>}
            <div style={{ display:"flex", gap:8, marginTop:20, justifyContent:"flex-end" }}>
              <Button size="sm" variant="ghost" onClick={()=>setOpen(false)}>Скасувати</Button>
              {isOwner && <Button size="sm" variant="primary" onClick={save}>Зберегти</Button>}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

// ── Toolbar ───────────────────────────────────────────────────────────────────
function PricesToolbar({ query, onQuery, onUpload, selCount, onBulkClear }) {
  if (selCount>0) {
    return (
      <div style={{ display:"flex", alignItems:"center", gap:12, padding:"12px 24px", borderBottom:"1px solid var(--border-subtle)", background:"var(--accent-soft)" }}>
        <span style={{ fontSize:13, fontWeight:600, color:"var(--accent)" }}>Вибрано {selCount}</span>
        <div style={{ width:1, height:18, background:"var(--border-default)" }}/>
        <Button size="sm" variant="secondary" leftIcon="shopping-cart">Сформувати замовлення</Button>
        <Button size="sm" variant="secondary" leftIcon="link">Призначити постачальника</Button>
        <Button size="sm" variant="secondary" leftIcon="download">Експорт CSV</Button>
        <div style={{ flex:1 }}/>
        <Button size="sm" variant="ghost" onClick={onBulkClear}>Скасувати</Button>
      </div>
    );
  }
  return (
    <div style={{ display:"flex", alignItems:"center", gap:10, padding:"12px 24px", borderBottom:"1px solid var(--border-subtle)" }}>
      <div style={{ position:"relative", flex:"0 0 420px" }}>
        <Icon name="search" size={15} style={{ position:"absolute", left:13, top:"50%", transform:"translateY(-50%)", color:"var(--accent)" }}/>
        <input value={query} onChange={e=>onQuery(e.target.value)} autoFocus
          placeholder="Знайти товар під замовлення — назва або артикул…"
          style={{ width:"100%", height:38, boxSizing:"border-box", padding:"0 12px 0 38px",
            background:"var(--bg-panel)", color:"var(--fg-primary)",
            border:"1px solid var(--border-default)", borderRadius:9, fontSize:13, outline:"none", fontFamily:"inherit" }}
          onFocus={e=>e.target.style.borderColor="var(--accent)"}
          onBlur={e=>e.target.style.borderColor="var(--border-default)"}/>
      </div>
      <span style={{ fontSize:11.5, color:"var(--fg-muted)", display:"flex", alignItems:"center", gap:5 }}>
        <Icon name="info" size={12}/> ціна закупу — у валюті прайсу
      </span>
      <div style={{ flex:1 }}/>
      <RateEditorBtn/>
      <Button variant="primary" leftIcon="upload" onClick={onUpload}>Завантажити прайс</Button>
    </div>
  );
}

// ── FilterBar ─────────────────────────────────────────────────────────────────
function getFilterDefs() {
  return {
    sup:    { label:"Постачальник", icon:"truck",        options:SUP_ORDER_P.map(k=>({ key:k, label:PRICE_SUPS[k]?.label||k })) },
    match:  { label:"Зіставлення",  icon:"layers",       options:[{key:"multi",label:"2+ постачальники"},{key:"single",label:"Лише 1 постачальник"}] },
    conf:   { label:"Точність",     icon:"shield-check", options:[{key:"high",label:"Точний збіг"},{key:"medium",label:"За моделлю"},{key:"low",label:"За назвою"},{key:"manual",label:"Підтверджено"}] },
    change: { label:"Зміна ціни",   icon:"trending-up",  options:[{key:"up",label:"Подорожчало"},{key:"down",label:"Подешевшало"},{key:"new",label:"Нові позиції"},{key:"risk",label:"Маржа під загрозою"}] },
    stock:  { label:"Наявність",    icon:"package",      options:[{key:"in",label:"В наявності"},{key:"out",label:"Немає в наявності"}] },
    cur:    { label:"Валюта",       icon:"coins",        options:[{key:"UAH",label:"Гривня ₴"},{key:"USD",label:"Долар $"},{key:"EUR",label:"Євро €"}] },
  };
}

function FilterPillP({ group, def, sel, onToggle, open, setOpen }) {
  const count = Object.keys(sel).filter(k=>sel[k]).length;
  const on = count>0;
  return (
    <div style={{ position:"relative" }}>
      <button onClick={()=>setOpen(open===group?null:group)} style={{
        display:"inline-flex", alignItems:"center", gap:7, height:32, padding:"0 11px", borderRadius:8, cursor:"pointer", fontFamily:"inherit",
        background:on?"var(--accent-soft)":"var(--bg-panel)",
        border:`1px solid ${on||open===group?"var(--accent)":"var(--border-default)"}`,
        color:on?"var(--accent)":"var(--fg-secondary)", fontSize:12.5, fontWeight:500, whiteSpace:"nowrap" }}>
        <Icon name={def.icon} size={14}/> {def.label}
        {on && <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, fontSize:11, background:"var(--accent)", color:"#fff", borderRadius:999, minWidth:16, height:16, display:"inline-flex", alignItems:"center", justifyContent:"center", padding:"0 4px" }}>{count}</span>}
        <Icon name="chevron-down" size={13} style={{ transform:open===group?"rotate(180deg)":"none", transition:"transform 150ms", opacity:.6 }}/>
      </button>
      {open===group && (
        <>
          <div onClick={()=>setOpen(null)} style={{ position:"fixed", inset:0, zIndex:8 }}/>
          <div style={{ position:"absolute", top:"calc(100% + 6px)", left:0, minWidth:210, background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:10, padding:6, boxShadow:"var(--shadow-2)", zIndex:9 }}>
            {def.options.map(o=>(
              <label key={o.key} style={{ display:"flex", alignItems:"center", gap:9, padding:"7px 8px", borderRadius:7, cursor:"pointer", fontSize:13, color:"var(--fg-secondary)" }}
                onMouseEnter={e=>e.currentTarget.style.background="var(--bg-hover)"}
                onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
                <input type="checkbox" checked={!!sel[o.key]} onChange={()=>onToggle(group,o.key)} style={{ accentColor:"var(--accent)", width:15, height:15, flexShrink:0 }}/>
                {group==="sup" ? <SupplierChipP sup={o.key} size="sm"/> : <span>{o.label}</span>}
              </label>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

function FilterBarP({ filters, onToggle, onReset, count, total }) {
  const [open, setOpen] = useState(null);
  const FILTER_DEFS_P = getFilterDefs();
  const activeChips = [];
  Object.keys(FILTER_DEFS_P).forEach(group => {
    const def = FILTER_DEFS_P[group];
    Object.keys(filters[group]||{}).filter(k=>filters[group][k]).forEach(k=>{
      const opt = def.options.find(o=>o.key===k);
      activeChips.push({ group, key:k, label:opt?opt.label:k, sup:group==="sup"?k:null });
    });
  });
  const anyActive = activeChips.length>0;
  return (
    <div style={{ display:"flex", alignItems:"center", gap:8, padding:"10px 24px", borderBottom:"1px solid var(--border-subtle)", flexWrap:"wrap" }}>
      <Icon name="sliders-horizontal" size={15} color="var(--fg-muted)" style={{ marginRight:2 }}/>
      {Object.keys(FILTER_DEFS_P).map(group=>(
        <FilterPillP key={group} group={group} def={FILTER_DEFS_P[group]} sel={filters[group]||{}} onToggle={onToggle} open={open} setOpen={setOpen}/>
      ))}
      {anyActive && <div style={{ width:1, height:20, background:"var(--border-default)", margin:"0 2px" }}/>}
      {activeChips.map(c=>(
        <span key={c.group+c.key} style={{ display:"inline-flex", alignItems:"center", gap:6, height:26, padding:"0 5px 0 9px", borderRadius:999, background:"var(--bg-raised)", border:"1px solid var(--border-default)", fontSize:12, color:"var(--fg-secondary)" }}>
          {c.sup ? <SupplierChipP sup={c.sup} size="sm"/> : c.label}
          <button onClick={()=>onToggle(c.group,c.key)} style={{ width:17, height:17, border:0, borderRadius:"50%", background:"transparent", color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}>
            <Icon name="x" size={12}/>
          </button>
        </span>
      ))}
      {anyActive && <button onClick={onReset} style={{ background:"transparent", border:0, color:"var(--accent)", cursor:"pointer", fontSize:12, fontFamily:"inherit", padding:"0 4px" }}>Скинути</button>}
      <div style={{ flex:1 }}/>
      <span style={{ fontSize:12, color:"var(--fg-muted)", whiteSpace:"nowrap" }}>
        <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-secondary)", fontWeight:600 }}>{count}</span> з {total}
      </span>
    </div>
  );
}

// ── Table ─────────────────────────────────────────────────────────────────────
const PCOLS = [["exp",26],["chk",24],["art",90],["name","flex"],["best",162],["change",74],["stock",80],["our",84],["margin",106],["trend",54],["act",58]];
const PCOL_LABEL = { art:"Артикул", name:"Назва товару", best:"Найкращий закуп", change:"Зміна", stock:"Наявн.", our:"Наша ціна", margin:"Маржа", trend:"30д" };
const cellP = w => w==="flex" ? { flex:"1 1 0", minWidth:130 } : { flex:`0 0 ${w}px` };
const PRIGHT = ["our","margin"];

function PTableHeader() {
  return (
    <div style={{ display:"flex", gap:10, alignItems:"center", padding:"9px 20px", borderBottom:"1px solid var(--border-subtle)", background:"var(--bg-panel)", position:"sticky", top:0, zIndex:2 }}>
      {PCOLS.map(([k,w])=>(
        <span key={k} style={{ ...cellP(w), fontSize:10.5, fontWeight:600, letterSpacing:".04em", textTransform:"uppercase", color:"var(--fg-muted)", textAlign:PRIGHT.includes(k)?"right":k==="trend"||k==="change"?"center":"left" }}>{PCOL_LABEL[k]||""}</span>
      ))}
    </div>
  );
}

function GroupRowP({ p, selected, checked, expanded, onSelect, onCheck, onToggle, onLink, onUnlink, dense }) {
  const mColor = marginColorP(p.margin, p.marginPct);
  const pad = dense ? "7px 20px" : "11px 20px";
  const [hover, setHover] = useState(false);
  const isLow = p.confidence?.level==="low";
  return (
    <>
      <div onClick={()=>onSelect(p)} style={{
        display:"flex", gap:10, alignItems:"center", padding:pad,
        borderBottom:expanded?"1px solid var(--border-default)":"1px solid var(--border-subtle)", cursor:"pointer",
        boxShadow:isLow?"inset 3px 0 0 var(--warning)":"none",
        background:selected?"var(--bg-active)":"transparent" }}
        onMouseEnter={e=>{ setHover(true); if(!selected) e.currentTarget.style.background="var(--bg-hover)"; }}
        onMouseLeave={e=>{ setHover(false); if(!selected) e.currentTarget.style.background="transparent"; }}>
        <span style={{ ...cellP(26), display:"flex", justifyContent:"center" }} onClick={e=>{ e.stopPropagation(); if(p.supCount>1) onToggle(p.id); }}>
          {p.supCount>1
            ? <Icon name="chevron-right" size={15} color="var(--fg-muted)" style={{ transform:expanded?"rotate(90deg)":"none", transition:"transform 150ms" }}/>
            : <span style={{ width:4, height:4, borderRadius:"50%", background:"var(--fg-disabled)" }}/>}
        </span>
        <span style={cellP(24)} onClick={e=>e.stopPropagation()}>
          <input type="checkbox" checked={checked} onChange={()=>onCheck(p.id)} style={{ accentColor:"var(--accent)", width:15, height:15 }}/>
        </span>
        <span style={{ ...cellP(90), fontFamily:"var(--font-mono)", fontSize:12, color:"var(--fg-muted)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{p.art}</span>
        <span style={{ ...cellP("flex"), minWidth:0 }}>
          <span style={{ display:"flex", alignItems:"center", gap:8 }}>
            <span style={{ fontSize:13, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }} title={p.name}>{p.name}</span>
            {p.isNew && <span style={{ flexShrink:0, fontSize:9.5, fontWeight:700, color:"#3B82F6", background:"rgba(59,130,246,.13)", borderRadius:4, padding:"1px 5px" }}>NEW</span>}
            <ConfidenceBadgeP p={p}/>
          </span>
          <span style={{ fontSize:11, color:"var(--fg-muted)" }}>{p.cat}</span>
        </span>
        <span style={{ ...cellP(162), display:"flex", alignItems:"center", gap:7, minWidth:0 }}>
          <span style={{ fontFamily:"var(--font-mono)", fontSize:13, fontWeight:700, color:"var(--fg-primary)", whiteSpace:"nowrap" }}>{fmtCurP(p.best.price, p.best.cur)}</span>
          <SupplierChipP sup={p.best.sup} size="sm" label={p.best.supplierName}/>
        </span>
        <span style={{ ...cellP(74), display:"flex", justifyContent:"center" }}>
          <ChangeBadgeP change={p.bestChange} pct={p.best.deltaPct}/>
        </span>
        <span style={{ ...cellP(80), fontSize:12, color:"var(--fg-secondary)" }}>
          {p.allOut
            ? <span style={{ display:"inline-flex", alignItems:"center", gap:6, color:"var(--fg-muted)" }}><span style={{ width:7, height:7, borderRadius:"50%", background:"var(--fg-disabled)" }}/> немає</span>
            : <span style={{ display:"inline-flex", alignItems:"center", gap:6 }}><span style={{ width:7, height:7, borderRadius:"50%", background:"var(--success)" }}/><span style={{ fontFamily:"var(--font-mono)" }}>{p.inStockCount}/{p.supCount}</span></span>}
        </span>
        <span style={{ ...cellP(84), fontFamily:"var(--font-mono)", fontSize:12.5, color:"var(--fg-secondary)", textAlign:"right" }}>{fmtUAHP(p.our)}</span>
        <span style={{ ...cellP(106), textAlign:"right" }}>
          <span style={{ display:"inline-flex", alignItems:"center", gap:5, justifyContent:"flex-end" }}>
            {p.marginRisk && <Icon name="alert-triangle" size={12} color="var(--danger)"/>}
            <span style={{ fontFamily:"var(--font-mono)", fontSize:12.5, fontWeight:600, color:mColor }}>{p.margin!=null ? fmtSignedP(p.margin) : "—"}</span>
          </span>
          <div style={{ fontFamily:"var(--font-mono)", fontSize:11, color:mColor, opacity:.8 }}>{p.marginPct!=null ? fmtPctP(p.marginPct) : ""}</div>
        </span>
        <span style={{ ...cellP(54), display:"flex", justifyContent:"center" }}>
          <SparklineP data={p.bestHist}/>
        </span>
        <span style={{ ...cellP(58), display:"flex", justifyContent:"flex-end", alignItems:"center", gap:4, color:"var(--fg-muted)" }} onClick={e=>e.stopPropagation()}>
          {hover && onLink && (
            <button onClick={()=>onLink(p)} title="Привʼязати товар" style={{
              width:26, height:26, border:0, borderRadius:6, background:"var(--bg-raised)",
              color:"var(--fg-secondary)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}
              onMouseEnter={e=>{ e.currentTarget.style.color="var(--accent)"; }}
              onMouseLeave={e=>{ e.currentTarget.style.color="var(--fg-secondary)"; }}>
              <Icon name="link" size={14}/>
            </button>
          )}
          <Icon name="chevron-right" size={15}/>
        </span>
      </div>
      {expanded && p.offers.map((o,i)=>(
        <SubRowP key={`${o.sup}-${i}`} o={o} p={p} isBest={o.sup===p.best.sup} last={i===p.offers.length-1} onUnlink={onUnlink}/>
      ))}
    </>
  );
}

function SubRowP({ o, p, isBest, onUnlink }) {
  const [hover, setHover] = useState(false);
  const [confirm, setConfirm] = useState(false);
  const canUnlink = onUnlink && p && p.supCount>1;
  return (
    <div onMouseEnter={()=>setHover(true)} onMouseLeave={()=>{ setHover(false); setConfirm(false); }}
      style={{ display:"flex", gap:10, alignItems:"center", padding:"8px 20px", borderBottom:"1px solid var(--border-subtle)", background:"var(--bg-base)" }}>
      <span style={cellP(26)}/>
      <span style={cellP(24)}/>
      <span style={{ ...cellP(90), display:"flex", justifyContent:"flex-end", paddingRight:4 }}>
        <span style={{ width:14, borderLeft:"1px solid var(--border-strong)", borderBottom:"1px solid var(--border-strong)", height:9, marginTop:-8, borderBottomLeftRadius:4 }}/>
      </span>
      <span style={{ ...cellP("flex"), display:"flex", alignItems:"center", gap:8, minWidth:0 }}>
        <SupplierChipP sup={o.sup} size="md" label={o.supplierName}/>
        {isBest && <BestBadgeP size="sm"/>}
        {o.offerName && <span style={{ fontSize:11.5, color:"var(--fg-secondary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis", minWidth:0 }} title={o.offerArt?`${o.offerName}\nарт: ${o.offerArt}`:o.offerName}>{o.offerName}</span>}
        {o.offerArt && <span style={{ fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--fg-muted)", whiteSpace:"nowrap", flexShrink:0 }}>{o.offerArt}</span>}
        <span style={{ fontSize:11, color:"var(--fg-muted)", flexShrink:0 }}>{PRICE_SUPS[o.sup].cur!=="UAH"?`≈ ${fmtUAHP(o.uah)}`:""}</span>
      </span>
      <span style={{ ...cellP(162), fontFamily:"var(--font-mono)", fontSize:13, fontWeight:600, color:isBest?"var(--success)":"var(--fg-primary)" }}>{fmtCurP(o.price,o.cur)}</span>
      <span style={{ ...cellP(74), display:"flex", justifyContent:"center" }}>
        <ChangeBadgeP change={o.change} pct={o.deltaPct}/>
      </span>
      <span style={cellP(80)}><StockDotP stock={o.stock} qty={o.qty}/></span>
      <span style={cellP(84)}/>
      <span style={{ ...cellP(106), display:"flex", justifyContent:"flex-end" }}>
        {o.stock && (
          <button onClick={e=>e.stopPropagation()} style={{
            display:"inline-flex", alignItems:"center", gap:5, height:26, padding:"0 10px", borderRadius:7,
            background:"var(--bg-raised)", border:"1px solid var(--border-default)", color:"var(--fg-secondary)",
            fontSize:11.5, fontFamily:"inherit", cursor:"pointer" }}>
            <Icon name="shopping-cart" size={12}/> Замовити
          </button>
        )}
      </span>
      <span style={{ ...cellP(54), display:"flex", justifyContent:"center" }}>
        <SparklineP data={o.hist} w={44} h={18}/>
      </span>
      <span style={{ ...cellP(58), display:"flex", justifyContent:"flex-end", alignItems:"center", position:"relative" }}>
        {canUnlink && hover && !confirm && (
          <button onClick={e=>{ e.stopPropagation(); setConfirm(true); }} title="Відвʼязати від товару" style={{
            width:26, height:26, border:0, borderRadius:6, background:"var(--bg-raised)",
            color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}
            onMouseEnter={e=>{ e.currentTarget.style.color="var(--danger)"; }}
            onMouseLeave={e=>{ e.currentTarget.style.color="var(--fg-muted)"; }}>
            <Icon name="unlink" size={14}/>
          </button>
        )}
        {canUnlink && confirm && (
          <div onClick={e=>e.stopPropagation()} style={{ position:"absolute", right:0, top:"calc(100% + 4px)", zIndex:10,
            display:"flex", flexDirection:"column", gap:6, padding:10, width:190,
            background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:9, boxShadow:"var(--shadow-2)" }}>
            <span style={{ fontSize:11.5, color:"var(--fg-secondary)" }}>Винести цей офер в окрему картку?</span>
            <div style={{ display:"flex", gap:6, justifyContent:"flex-end" }}>
              <Button size="sm" variant="ghost" onClick={()=>setConfirm(false)}>Ні</Button>
              <Button size="sm" variant="danger" onClick={()=>{ setConfirm(false); onUnlink(p, o); }}>Відвʼязати</Button>
            </div>
          </div>
        )}
      </span>
    </div>
  );
}

function EmptyStateP({ query, onReset }) {
  return (
    <div style={{ display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", padding:"72px 24px", gap:14 }}>
      <div style={{ width:56, height:56, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}>
        <Icon name="search-x" size={26} color="var(--fg-muted)"/>
      </div>
      <div style={{ textAlign:"center" }}>
        <div style={{ fontSize:14, fontWeight:600, color:"var(--fg-primary)" }}>{query?`Нічого не знайдено за «${query}»`:"Немає позицій за фільтрами"}</div>
        <div style={{ fontSize:12.5, color:"var(--fg-muted)", marginTop:5 }}>Спробуйте змінити запит або скинути фільтри</div>
      </div>
      <Button variant="secondary" leftIcon="rotate-ccw" onClick={onReset}>Скинути все</Button>
    </div>
  );
}

// ── DetailPanel ───────────────────────────────────────────────────────────────
function DetailPanelP({ product, onClose, onToast, onLink, onBreak, onUnlink, onConfirm }) {
  const [chartOpen, setChartOpen] = useState(true);
  const [menu, setMenu] = useState(false);
  const isMdm = !!product.mdm;
  const [boundSup, setBoundSup] = useState(defaultSourceSupP(product));
  const [siteSup, setSiteSup] = useState(null);     // реальний постачальник товару на сайті (cbPrefix)
  const [sellPrice, setSellPrice] = useState(product.our);
  const [published, setPublished] = useState(false);
  const [siteData, setSiteData] = useState(null);   // { price, presence, url, title, supplierPrefix } | null
  const [siteLoading, setSiteLoading] = useState(false);
  const [siteArticle, setSiteArticle] = useState(""); // артикул, що реально знайшовся на сайті (для публікації)

  useEffect(()=>{
    setChartOpen(true); setMenu(false); setBoundSup(defaultSourceSupP(product)); setSiteSup(null);
    setPublished(false); setSiteData(null); setSiteArticle(""); setSellPrice(product.our);
    // Кандидати-артикули: базовий + артикули ВСІХ постачальників групи (offerArt).
    // Шукаємо кожен на сайті — перший знайдений визначає привʼязку (зазвичай це артикул
    // постачальника, чий SKU збігається з сайтом, напр. Основной Новоселица).
    const isRealSku = a => a && !String(a).includes(" ") && String(a).length <= 30;
    // Товар може мати кілька артикулів в одній клітинці (різні ринки): «PB634-A-CIS, PB634-A-WW».
    // Розбиваємо ЛИШЕ по комі/; (слеш — частина артикула: Philips 65PUS9010/12, HyperX RG/G!)
    // і шукаємо КОЖЕН на сайті окремо (назви з пробілами не чіпаємо).
    const splitArts = s => String(s || "").split(/\s*[,;]\s*/).map(x => x.trim()).filter(Boolean);
    const cand = [];
    const push = a => { for (const s of splitArts(a)) if (isRealSku(s) && !cand.includes(s)) cand.push(s); };
    push(product.artRaw); push(product.art);
    for (const o of (product.offers || [])) push(o.offerArt);
    // Останніми — коди з дужок у назві (фолбек, якщо поле артикула не дало збігу на сайті)
    for (const a of artsFromName(product.name)) if (!cand.includes(a)) cand.push(a);
    if (!cand.length) return;
    let alive = true;
    setSiteLoading(true);
    (async () => {
      for (const a of cand) {
        if (!alive) return;
        try {
          const res = await fetch(`/api/horoshop/product?article=${encodeURIComponent(a)}`).then(r=>r.json());
          if (!alive) return;
          if (res.ok && res.found) {
            setSiteData(res.data);
            setSiteArticle(a);
            setSellPrice(res.data.price ?? product.our);
            if (res.data.supplierPrefix) { setSiteSup(res.data.supplierPrefix); setBoundSup(res.data.supplierPrefix); }
            return;
          }
        } catch {}
      }
    })().finally(()=>{ if (alive) setSiteLoading(false); });
    return () => { alive = false; };
  }, [product.id]);

  const sortedOffers = [...product.offers].sort((a,b)=>a.uah-b.uah);
  // Артикули всіх постачальників групи (для шапки) + який артикул вважати основним (знайдений на сайті → базовий)
  const supArticles = sortedOffers
    .map(o => ({ sup:o.sup, art:String(o.offerArt||"").trim(), label:(PRICE_SUPS[o.sup]||{}).label||o.sup }))
    .filter(a => a.art);
  const headArt = siteArticle || product.artRaw || product.art || "";
  const boundOffer = product.offers.find(o=>o.sup===boundSup)||product.best;
  const buyUAH = boundOffer.uah;
  const liveMargin = sellPrice-buyUAH;
  const liveMarginPct = sellPrice?(liveMargin/sellPrice)*100:0;
  const mColor = marginColorP(liveMargin, liveMarginPct);
  const targetSell = Math.round(buyUAH/(1-TARGET_MARGIN/100));
  const baseSup = siteSup || defaultSourceSupP(product);
  // База для «ціну змінено» — РЕАЛЬНА ціна на сайті (якщо вже завантажили), а не product.our
  // з кешу. Інакше при розбіжності кеш↔сайт система хибно вважає, що ти змінив ціну, і
  // може випадково перезаписати її, коли ти міняв лише постачальника.
  const siteBasePrice = siteData?.price ?? product.our;
  const priceChangedSite = sellPrice!==siteBasePrice;
  const supChangedSite = boundSup!==baseSup;
  const dirty = supChangedSite||priceChangedSite;
  const siteChangeWhat = priceChangedSite&&supChangedSite ? "ціну і джерело" : supChangedSite ? "джерело" : "ціну";
  const siteChangeBadge = priceChangedSite&&supChangedSite ? "ціна, джерело" : supChangedSite ? "джерело" : "ціна";

  const iconBtn = { width:32, height:32, border:0, background:"transparent", color:"var(--fg-secondary)", borderRadius:6, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 };
  const chipBtn = { height:22, padding:"0 8px", borderRadius:6, background:"var(--bg-raised)", border:"1px solid var(--border-default)", color:"var(--fg-secondary)", fontSize:10.5, fontFamily:"inherit", cursor:"pointer", whiteSpace:"nowrap" };

  const publish = () => {
    if (isMdm) { onToast && onToast("⛔ MDM — заблокований пристрій: публікація на сайт заборонена"); return; }
    const pubArt = siteArticle || product.artRaw || product.art || "";
    if (!pubArt) { onToast && onToast("❌ Немає артикулу для оновлення — привʼяжіть товар із артикулом"); return; }
    setPublished(true);
    fetch('/api/horoshop/update-price', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ article: pubArt, price: sellPrice, sourceSup: boundSup }),
    })
      .then(r=>r.json())
      .then(res=>{
        if (res.ok) {
          const supChanged = boundSup && boundSup !== siteSup;
          onToast && onToast(`✅ Оновлено на сайті · ${fmtUAHP(sellPrice)}${supChanged ? ` · джерело: ${PRICE_SUPS[boundSup]?.label||boundSup}` : ""}`);
          setSiteData(d => d ? { ...d, price: sellPrice, supplierPrefix: boundSup } : d);
          setSiteSup(boundSup);
        } else {
          onToast && onToast(`❌ Помилка оновлення: ${res.error || 'невідома'}`);
        }
      })
      .catch(e=>{ onToast && onToast(`❌ ${e.message}`); })
      .finally(()=>setTimeout(()=>setPublished(false), 1600));
  };

  // Уточнення наявності — як команда /find: шукає товар, віддає наявність/ціну і за потреби
  // шле запит постачальнику в Telegram (CRM-проксі /api/requests/check-availability → бот /internal/check).
  const [checking, setChecking] = useState(false);
  // sup (опц.) — cbPrefix офера: «Уточнити» біля конкретного постачальника пінгує САМЕ його
  const checkAvail = (article, sup, offerTitle) => {
    const art = String(article || headArt || product.art || "").trim();
    if (!art) { onToast && onToast("❌ Немає артикулу для перевірки"); return; }
    setChecking(true);
    onToast && onToast(`⏳ Уточнюю наявність · ${art}${sup ? " · " + ((PRICE_SUPS[sup]||{}).label||sup) : ""}…`);
    fetch('/api/requests/check-availability', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ article: art, managerName: (localStorage.getItem('crm_user_name')||''), supplier: sup || undefined, title: offerTitle || product.name || undefined }) })
      .then(r=>r.json())
      .then(res=>{ onToast && onToast(res && res.label ? res.label : (res && res.found ? "⏳ Запит відправлено постачальнику" : "❌ Товар не знайдено")); })
      .catch(e=>{ onToast && onToast(`❌ ${e.message}`); })
      .finally(()=>setChecking(false));
  };

  return (
    <div style={{ position:"fixed", top:0, right:0, width:560, maxWidth:"94vw", height:"100vh",
      background:"var(--bg-panel)", borderLeft:"1px solid var(--border-subtle)",
      boxShadow:"var(--shadow-2)", display:"flex", flexDirection:"column", zIndex:20, overflow:"hidden",
      animation:"slideIn 200ms cubic-bezier(.2,0,0,1)" }}>
      {/* Header */}
      <div style={{ padding:"16px 20px 14px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:10 }}>
          <span style={{ fontSize:11, fontWeight:600, color:"var(--fg-muted)", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:6, padding:"3px 8px" }}>{product.cat}</span>
          {product.isNew && <span style={{ fontSize:10, fontWeight:700, color:"#3B82F6", background:"rgba(59,130,246,.13)", borderRadius:5, padding:"3px 7px" }}>НОВЕ</span>}
          <FlagBadgeP mdm={product.mdm} refurb={product.ref}/>
          <div style={{ flex:1 }}/>
          <button onClick={onClose} style={iconBtn}><Icon name="x" size={18}/></button>
        </div>
        <h2 style={{ fontSize:16, fontWeight:600, margin:0, color:"var(--fg-primary)", lineHeight:1.3 }}>{product.name}</h2>
        <div style={{ display:"flex", alignItems:"center", gap:12, marginTop:5 }}>
          <span style={{ fontFamily:"var(--font-mono)", fontSize:12, color:"var(--fg-muted)" }}>Арт: {headArt || "—"}</span>
          <span style={{ fontSize:12, color:"var(--fg-muted)" }}>{product.supCount} постачальників</span>
        </div>
        {supArticles.length > 0 && (
          <div style={{ display:"flex", flexWrap:"wrap", gap:6, marginTop:8 }}>
            {supArticles.map((a,i)=>(
              <span key={a.sup+"-"+i} title={`${a.label}: ${a.art}`} style={{ display:"inline-flex", alignItems:"center", gap:5, padding:"3px 8px", borderRadius:6, background: a.art===headArt?"var(--accent-soft)":"var(--bg-base)", border:`1px solid ${a.art===headArt?"var(--accent)":"var(--border-subtle)"}` }}>
                <span style={{ width:6, height:6, borderRadius:"50%", background:(PRICE_SUPS[a.sup]||{}).color||"var(--fg-muted)", flexShrink:0 }}/>
                <span style={{ fontSize:11, color:"var(--fg-muted)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis", maxWidth:110 }}>{a.label}</span>
                <span style={{ fontFamily:"var(--font-mono)", fontSize:11, color:"var(--fg-secondary)", whiteSpace:"nowrap" }}>{a.art}</span>
                {a.art===headArt && siteData && <Icon name="check" size={11} color="var(--accent)"/>}
              </span>
            ))}
          </div>
        )}
        <div style={{ display:"flex", gap:8, marginTop:14, position:"relative" }}>
          <Button size="md" variant="primary" leftIcon="search" style={{ flex:1 }} onClick={()=>checkAvail(headArt||product.art)} disabled={checking}>Уточнити наявність</Button>
          <Button size="md" variant="secondary" leftIcon="external-link">Картка</Button>
          <button onClick={()=>setMenu(m=>!m)} style={{ ...iconBtn, background:"var(--bg-raised)", border:"1px solid var(--border-default)", width:36 }}>
            <Icon name="more-horizontal" size={16}/>
          </button>
          {menu && (
            <>
              <div onClick={()=>setMenu(false)} style={{ position:"fixed", inset:0, zIndex:4 }}/>
              <div style={{ position:"absolute", top:"calc(100% + 6px)", right:0, minWidth:210, background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:8, padding:4, boxShadow:"var(--shadow-1)", zIndex:5 }}>
                {[["history","Історія закупки"],["bell","Сповіщення про ціну"],["download","Експорт"]].map(([ic,lbl])=>(
                  <button key={lbl} onClick={()=>setMenu(false)} style={{ display:"flex", alignItems:"center", gap:10, width:"100%", padding:"8px 10px", border:0, borderRadius:6, background:"transparent", cursor:"pointer", textAlign:"left", color:"var(--fg-primary)", fontSize:13, fontFamily:"inherit" }}
                    onMouseEnter={e=>e.currentTarget.style.background="var(--bg-hover)"}
                    onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
                    <Icon name={ic} size={14} color="var(--fg-muted)"/>{lbl}
                  </button>
                ))}
              </div>
            </>
          )}
        </div>
      </div>

      <div style={{ flex:1, overflow:"auto", padding:"16px 20px 28px", display:"flex", flexDirection:"column", gap:18 }}>
        {product.supCount>1 && product.confidence && product.confidence.level!=="none" && (() => {
          const c = product.confidence; const m = CONF_META[c.level]; const isLow = c.level==="low"; const isManual = c.level==="manual";
          return (
            <div style={{ background:"var(--bg-raised)", border:`1px solid ${isLow?"rgba(245,158,11,.32)":"var(--border-subtle)"}`, borderRadius:10, padding:14 }}>
              <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:10 }}>
                <Icon name={m.icon} size={15} color={m.color}/>
                <span style={{ fontSize:13, fontWeight:600, color:m.color }}>{c.label}</span>
                <div style={{ flex:1 }}/>
                {c.pct!=null && <span style={{ fontFamily:"var(--font-mono)", fontSize:13, fontWeight:700, color:m.color }}>{c.pct}%</span>}
              </div>
              <div style={{ height:5, background:"var(--bg-base)", borderRadius:3, overflow:"hidden" }}>
                <div style={{ height:"100%", width:`${c.pct||0}%`, background:m.color, borderRadius:3 }}/>
              </div>
              <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:8 }}>Склеєно за: {confDetailP(product)}</div>
              {isLow && (
                <div style={{ marginTop:10, fontSize:11.5, color:"var(--warning)", display:"flex", gap:6, alignItems:"flex-start", lineHeight:1.4 }}>
                  <Icon name="alert-triangle" size={13} color="var(--warning)" style={{ flexShrink:0, marginTop:1 }}/> Низька впевненість — перевірте привʼязку вручну.
                </div>
              )}
              {isManual
                ? <Button size="sm" variant="danger" leftIcon="unlink" onClick={()=>onBreak&&onBreak(product)} style={{ width:"100%", marginTop:12 }}>Розірвати звʼязок</Button>
                : <div style={{ display:"flex", gap:8, marginTop:12 }}>
                    <Button size="sm" variant="primary" leftIcon="check" onClick={()=>onConfirm&&onConfirm(product)} style={{ flex:1 }}>Підтвердити привʼязку</Button>
                    <Button size="sm" variant="secondary" leftIcon="link" onClick={()=>onLink&&onLink(product)} style={{ flex:"0 0 auto" }}>Привʼязати ще</Button>
                  </div>}
            </div>
          );
        })()}
        {product.marginRisk && (
          <div style={{ display:"flex", gap:10, padding:"10px 14px", background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.32)", borderRadius:10 }}>
            <Icon name="trending-up" size={16} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
            <div>
              <div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Закуп подорожчав</div>
              <div style={{ fontSize:11, color:"var(--fg-secondary)", marginTop:2 }}>Найдешевший постачальник підняв ціну. Маржа впала до {fmtPctP(product.marginPct)}.</div>
            </div>
          </div>
        )}
        {product.allOut && (
          <div style={{ display:"flex", gap:10, padding:"10px 14px", background:"rgba(255,255,255,.04)", border:"1px solid var(--border-default)", borderRadius:10 }}>
            <Icon name="package-x" size={16} color="var(--fg-muted)" style={{ flexShrink:0, marginTop:1 }}/>
            <div style={{ fontSize:12, color:"var(--fg-secondary)" }}>Зараз товару немає в наявності у жодного постачальника.</div>
          </div>
        )}

        {/* На сайті */}
        <div>
          <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:8 }}>
            <span style={{ fontSize:11, fontWeight:600, letterSpacing:".06em", textTransform:"uppercase", color:"var(--fg-muted)" }}>На сайті · Horoshop</span>
            <div style={{ display:"flex", alignItems:"center", gap:8 }}>
              {siteLoading && <Icon name="loader" size={12} style={{ animation:"prices-spin 0.8s linear infinite", color:"var(--fg-muted)" }}/>}
              {siteData && !siteLoading && (
                <span style={{ fontSize:11, display:"inline-flex", alignItems:"center", gap:4,
                  color: siteData.presence == 1 || siteData.presence === "+"|| siteData.presence === "є" ? "var(--success)" : "var(--fg-muted)" }}>
                  <span style={{ width:5, height:5, borderRadius:"50%", background:"currentColor" }}/>
                  {siteData.presence == 1 || siteData.presence === "+" || siteData.presence === "є" ? "є на сайті" : "немає на сайті"}
                </span>
              )}
              {!siteData && !siteLoading && (headArt || supArticles.length>0) && <span style={{ fontSize:11, color:"var(--fg-muted)" }}>не знайдено на сайті</span>}
              {published
                ? <span style={{ fontSize:11, color:"var(--success)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="check" size={12}/> оновлено</span>
                : dirty ? <span style={{ fontSize:11, color:"var(--warning)" }}>змінено: {siteChangeBadge}</span> : null}
            </div>
          </div>
          <div style={{ background:"var(--bg-raised)", border:"1px solid var(--border-subtle)", borderRadius:10, padding:14 }}>
            <div style={{ display:"flex", gap:12 }}>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:6 }}>Постачальник-джерело</div>
                <div style={{ position:"relative" }}>
                  <select value={boundSup} onChange={e=>setBoundSup(e.target.value)} style={{
                    width:"100%", boxSizing:"border-box", height:38, padding:"0 30px 0 11px", appearance:"none",
                    background:"var(--bg-base)", color:"var(--fg-primary)", border:"1px solid var(--border-default)",
                    borderRadius:8, fontSize:13, fontFamily:"inherit", fontWeight:600, cursor:"pointer", outline:"none" }}>
                    {sortedOffers.map(o=>(
                      <option key={o.sup} value={o.sup}>{PRICE_SUPS[o.sup]?.label||o.sup} · {fmtCurP(o.price,o.cur)}{o.ref?" · REF":""}{o.stock?"":" (немає)"}</option>
                    ))}
                  </select>
                  <Icon name="chevron-down" size={14} style={{ position:"absolute", right:10, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", pointerEvents:"none" }}/>
                </div>
                <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:6, display:"flex", alignItems:"center", gap:5 }}>
                  закуп <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-secondary)", fontWeight:600 }}>{fmtUAHP(buyUAH)}</span>
                  {boundOffer.cur!=="UAH" && <span>· {fmtCurP(boundOffer.price,boundOffer.cur)}</span>}
                </div>
              </div>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:6 }}>Ціна продажу</div>
                <div style={{ position:"relative" }}>
                  <input type="number" value={sellPrice ?? ""} onChange={e=>setSellPrice(Math.max(0,parseInt(e.target.value,10)||0))} style={{
                    width:"100%", boxSizing:"border-box", height:38, padding:"0 26px 0 11px",
                    background:"var(--bg-base)", color:"var(--fg-primary)", border:"1px solid var(--border-default)",
                    borderRadius:8, fontSize:14, fontFamily:"var(--font-mono)", fontWeight:700, outline:"none" }}
                    onFocus={e=>e.target.style.borderColor="var(--accent)"}
                    onBlur={e=>e.target.style.borderColor="var(--border-default)"}/>
                  <span style={{ position:"absolute", right:11, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", fontSize:13, pointerEvents:"none" }}>₴</span>
                </div>
                <div style={{ display:"flex", gap:6, marginTop:6 }}>
                  <button onClick={()=>setSellPrice(targetSell)} style={chipBtn}>= {TARGET_MARGIN}% маржі</button>
                  {sellPrice!==product.our && <button onClick={()=>setSellPrice(product.our)} style={chipBtn}>↺ {fmtUAHP(product.our)}</button>}
                </div>
              </div>
            </div>
            <div style={{ display:"flex", alignItems:"center", gap:14, marginTop:14, padding:"11px 13px", background:"var(--bg-base)", borderRadius:9, border:`1px solid ${liveMargin<0?"rgba(244,63,94,.3)":"var(--border-subtle)"}` }}>
              <div>
                <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:2 }}>Маржа</div>
                <div style={{ display:"flex", alignItems:"baseline", gap:8, whiteSpace:"nowrap" }}>
                  <span style={{ fontFamily:"var(--font-mono)", fontSize:19, fontWeight:700, color:mColor }}>{fmtSignedP(liveMargin)}</span>
                  <span style={{ fontFamily:"var(--font-mono)", fontSize:13, fontWeight:600, color:mColor }}>{fmtPctP(liveMarginPct)}</span>
                </div>
              </div>
              <div style={{ flex:1 }}>
                <div style={{ display:"flex", justifyContent:"flex-end", fontSize:10.5, color:"var(--fg-muted)", marginBottom:5 }}>ціль {TARGET_MARGIN}%</div>
                <div style={{ height:6, background:"var(--bg-panel)", borderRadius:3, overflow:"hidden" }}>
                  <div style={{ height:"100%", width:`${Math.max(3,Math.min(100,(liveMarginPct/TARGET_MARGIN)*100))}%`, background:liveMarginPct>=TARGET_MARGIN?"var(--success)":liveMargin<0?"var(--danger)":"var(--warning)", borderRadius:3, transition:"width 150ms" }}/>
                </div>
              </div>
            </div>
            <Button variant="primary" leftIcon={isMdm?"lock":published?"check":"refresh-cw"} onClick={publish} disabled={isMdm||(!dirty&&!published)} style={{ width:"100%", marginTop:12 }}>
              {isMdm?"MDM — публікацію заблоковано":published?"Оновлено на сайті":`Оновити ${siteChangeWhat} на сайті`}
            </Button>
            {isMdm && (
              <div style={{ display:"flex", gap:8, marginTop:10, padding:"9px 12px", background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.32)", borderRadius:9, fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>
                <Icon name="lock" size={14} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
                Заблокований пристрій (MDM): не публікуємо на сайт, не ставимо джерелом, не виводимо на вітрину.
              </div>
            )}
            {!isMdm && boundOffer.ref && (
              <div style={{ display:"flex", gap:8, marginTop:10, padding:"9px 12px", background:"rgba(245,158,11,.10)", border:"1px solid rgba(245,158,11,.28)", borderRadius:9, fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>
                <Icon name="alert-triangle" size={14} color="var(--warning)" style={{ flexShrink:0, marginTop:1 }}/>
                Джерело — REF (відновлений). Обрано вручну: автоматично REF джерелом не ставиться.
              </div>
            )}
          </div>
        </div>

        {/* Постачальники */}
        <div>
          <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:8 }}>
            <span style={{ fontSize:11, fontWeight:600, letterSpacing:".06em", textTransform:"uppercase", color:"var(--fg-muted)" }}>Постачальники · {product.supCount}</span>
            <span style={{ fontSize:11, color:"var(--fg-muted)" }}>спред {fmtUAHP(product.spread)}</span>
          </div>
          <div style={{ display:"flex", flexDirection:"column", gap:8 }}>
            {sortedOffers.map(o=>(
              <SupplierCompareRowP key={o.sup} o={o} isBest={o.sup===product.best.sup} isSource={boundSup===o.sup} onMakeSource={()=>setBoundSup(o.sup)}
                disableSource={isMdm} canUnlink={product.supCount>1} onUnlink={()=>onUnlink&&onUnlink(product,o)} onCheck={checkAvail} checking={checking}/>
            ))}
          </div>
        </div>

        {/* Історія закупки */}
        <div>
          <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:8 }}>
            <span style={{ fontSize:11, fontWeight:600, letterSpacing:".06em", textTransform:"uppercase", color:"var(--fg-muted)" }}>Історія закупки · 30 днів</span>
            <button onClick={()=>setChartOpen(o=>!o)} style={{ background:"transparent", border:0, color:"var(--fg-muted)", cursor:"pointer", padding:0 }}>
              <Icon name={chartOpen?"chevron-up":"chevron-down"} size={14}/>
            </button>
          </div>
          {chartOpen && (
            <div style={{ background:"var(--bg-raised)", border:"1px solid var(--border-subtle)", borderRadius:10, padding:"12px 8px 8px" }}>
              <div style={{ display:"flex", justifyContent:"space-between", padding:"0 8px 6px", fontSize:11 }}>
                <span style={{ display:"inline-flex", alignItems:"center", gap:6, color:"var(--fg-secondary)" }}>
                  <span style={{ width:12, height:2, background:"var(--success)", borderRadius:2 }}/> Найкращий закуп, ₴
                </span>
                <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-muted)" }}>{fmtUAHP(Math.min(...product.bestHist))} – {fmtUAHP(Math.max(...product.bestHist))}</span>
              </div>
              <PriceHistoryChartP series={product.bestHist}/>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function SupplierCompareRowP({ o, isBest, isSource, onMakeSource, disableSource, canUnlink, onUnlink, onCheck, checking }) {
  const s = PRICE_SUPS[o.sup];
  const [confirm, setConfirm] = useState(false);
  return (
    <div style={{ padding:"11px 12px", borderRadius:9,
      background:isSource?"var(--accent-soft)":isBest?"rgba(16,185,129,.07)":"var(--bg-base)",
      border:`1px solid ${isSource?"var(--accent)":isBest?"rgba(16,185,129,.28)":"var(--border-subtle)"}` }}>
      <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:8 }}>
        <SupplierChipP sup={o.sup} label={o.supplierName}/>
        {o.ref && <RefChipP/>}
        {isBest && <BestBadgeP size="sm"/>}
        {isSource && <span style={{ fontSize:10, fontWeight:700, color:"var(--accent)", background:"var(--bg-panel)", border:"1px solid var(--accent)", borderRadius:5, padding:"2px 6px", display:"inline-flex", alignItems:"center", gap:3 }}><Icon name="globe" size={10}/> на сайті</span>}
        <div style={{ flex:1 }}/>
        <SourceBadgeP source={s.source} size="sm"/>
        {canUnlink && (
          <div style={{ position:"relative" }}>
            <button onClick={()=>setConfirm(v=>!v)} title="Відвʼязати від товару" style={{
              width:26, height:26, border:0, borderRadius:6, background:"transparent",
              color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}
              onMouseEnter={e=>{ e.currentTarget.style.color="var(--danger)"; }}
              onMouseLeave={e=>{ e.currentTarget.style.color="var(--fg-muted)"; }}>
              <Icon name="unlink" size={14}/>
            </button>
            {confirm && (
              <>
                <div onClick={()=>setConfirm(false)} style={{ position:"fixed", inset:0, zIndex:9 }}/>
                <div style={{ position:"absolute", right:0, top:"calc(100% + 4px)", zIndex:10, width:190,
                  display:"flex", flexDirection:"column", gap:6, padding:10,
                  background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:9, boxShadow:"var(--shadow-2)" }}>
                  <span style={{ fontSize:11.5, color:"var(--fg-secondary)" }}>Винести цей офер в окрему картку?</span>
                  <div style={{ display:"flex", gap:6, justifyContent:"flex-end" }}>
                    <Button size="sm" variant="ghost" onClick={()=>setConfirm(false)}>Ні</Button>
                    <Button size="sm" variant="danger" onClick={()=>{ setConfirm(false); onUnlink&&onUnlink(); }}>Відвʼязати</Button>
                  </div>
                </div>
              </>
            )}
          </div>
        )}
      </div>
      {(o.offerName||o.offerArt) && (
        <div style={{ marginBottom:9, padding:"7px 9px", background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:8 }}>
          <div style={{ fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.35, wordBreak:"break-word" }} title={o.offerName}>{o.offerName||"—"}</div>
          {o.offerArt && <div style={{ fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--fg-muted)", marginTop:3 }}>арт: {o.offerArt}</div>}
        </div>
      )}
      <div style={{ display:"flex", alignItems:"flex-end", gap:10 }}>
        <div style={{ minWidth:0 }}>
          <div style={{ fontFamily:"var(--font-mono)", fontSize:18, fontWeight:700, color:isBest?"var(--success)":"var(--fg-primary)", lineHeight:1.1 }}>
            {fmtCurP(o.price,o.cur)}
          </div>
          {o.cur!=="UAH" && <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:2, fontFamily:"var(--font-mono)" }}>≈ {fmtUAHP(o.uah)}</div>}
        </div>
        <div style={{ flex:1 }}/>
        <div style={{ display:"flex", flexDirection:"column", alignItems:"flex-end", gap:4 }}>
          <ChangeBadgeP change={o.change} pct={o.deltaPct}/>
          <StockDotP stock={o.stock} qty={o.qty}/>
        </div>
      </div>
      <div style={{ display:"flex", alignItems:"center", gap:8, marginTop:10 }}>
        <SparklineP data={o.hist} w={70} h={20}/>
        <span style={{ fontSize:10.5, color:"var(--fg-muted)" }}>30 днів</span>
        <div style={{ flex:1 }}/>
        {isSource
          ? <span style={{ fontSize:11.5, color:"var(--accent)", fontWeight:600, display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="check" size={13}/> джерело для сайту</span>
          : disableSource
            ? <span title="MDM — заблоковано" style={{ fontSize:11, color:"var(--fg-disabled)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="lock" size={12}/> джерело заблоковано</span>
            : <Button size="sm" variant="secondary" leftIcon="globe" onClick={onMakeSource}>Зробити джерелом</Button>}
        <Button size="sm" variant={isBest?"primary":"ghost"} leftIcon="search" onClick={()=>onCheck&&onCheck(o.offerArt, o.sup, o.offerName)} disabled={checking}>Уточнити</Button>
      </div>
    </div>
  );
}

// ── Модалка «Привʼязати товар» (десктоп) ─────────────────────────────────────
function LinkResultRowP({ p, selected, onSelect }) {
  return (
    <button onClick={()=>onSelect(p)} style={{
      display:"flex", alignItems:"center", gap:11, width:"100%", textAlign:"left", padding:"9px 12px", borderRadius:9, cursor:"pointer", fontFamily:"inherit",
      background:selected?"var(--accent-soft)":"transparent", border:`1px solid ${selected?"var(--accent)":"transparent"}` }}
      onMouseEnter={e=>{ if(!selected) e.currentTarget.style.background="var(--bg-hover)"; }}
      onMouseLeave={e=>{ if(!selected) e.currentTarget.style.background="transparent"; }}>
      <span style={{ width:16, height:16, borderRadius:"50%", flexShrink:0, border:`2px solid ${selected?"var(--accent)":"var(--border-strong)"}`, background:selected?"var(--accent)":"transparent", display:"flex", alignItems:"center", justifyContent:"center" }}>
        {selected && <Icon name="check" size={10} color="#fff"/>}
      </span>
      <span style={{ fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--fg-muted)", flex:"0 0 84px", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{p.art||"—"}</span>
      <span style={{ flex:1, minWidth:0, fontSize:12.5, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{p.name}</span>
      <span style={{ flexShrink:0 }}><SupplierChipP sup={p.best.sup} size="sm" label={p.best.supplierName}/></span>
      <span style={{ flex:"0 0 70px", textAlign:"right", fontFamily:"var(--font-mono)", fontSize:12, color:"var(--fg-secondary)" }}>{fmtCurP(p.best.price, p.best.cur)}</span>
    </button>
  );
}

function LinkPickerModalP({ product, onClose, onLink }) {
  const [q, setQ] = useState("");
  const [sel, setSel] = useState(null);
  const results = useMemo(()=>{
    const t = q.trim().toLowerCase();
    return PRICE_PRODUCTS.filter(p=>p.id!==product.id &&
      (!t || p.name.toLowerCase().includes(t) || (p.art||"").toLowerCase().includes(t)))
      .slice(0, 200);
  }, [q, product.id]);
  return (
    <div style={{ position:"fixed", inset:0, zIndex:70, display:"flex", alignItems:"center", justifyContent:"center", padding:24 }}>
      <div onClick={onClose} style={{ position:"absolute", inset:0, background:"rgba(0,0,0,.6)" }}/>
      <div style={{ position:"relative", width:640, maxWidth:"100%", maxHeight:"86vh", display:"flex", flexDirection:"column",
        background:"var(--bg-panel)", border:"1px solid var(--border-default)", borderRadius:14, boxShadow:"var(--shadow-2)", overflow:"hidden" }}>
        <div style={{ display:"flex", alignItems:"center", gap:11, padding:"16px 18px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
          <div style={{ width:34, height:34, borderRadius:9, background:"var(--accent-soft)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}>
            <Icon name="link" size={17} color="var(--accent)"/>
          </div>
          <div style={{ flex:1, minWidth:0 }}>
            <h2 style={{ fontSize:15, fontWeight:600, margin:0, color:"var(--fg-primary)" }}>Привʼязати товар</h2>
            <div style={{ fontSize:12, color:"var(--fg-muted)", marginTop:1 }}>Обʼєднати офери в одну картку</div>
          </div>
          <button onClick={onClose} style={{ width:32, height:32, border:0, borderRadius:7, background:"transparent", color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={18}/></button>
        </div>
        <div style={{ padding:"13px 18px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
          <div style={{ fontSize:10.5, fontWeight:600, letterSpacing:".05em", textTransform:"uppercase", color:"var(--fg-muted)", marginBottom:8 }}>Поточний товар</div>
          <div style={{ display:"flex", alignItems:"center", gap:11, padding:"11px 13px", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:10 }}>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ display:"flex", alignItems:"center", gap:8 }}>
                <span style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{product.name}</span>
                <ConfidenceBadgeP p={product} interactive={false}/>
              </div>
              <div style={{ display:"flex", alignItems:"center", gap:9, marginTop:5 }}>
                <span style={{ fontFamily:"var(--font-mono)", fontSize:11, color:"var(--fg-muted)" }}>{product.art||"—"}</span>
                <span style={{ fontSize:11, color:"var(--fg-muted)" }}>{product.supCount} постачальників</span>
              </div>
            </div>
          </div>
        </div>
        <div style={{ flex:1, overflow:"auto", padding:"16px 18px 8px" }}>
          <div style={{ position:"relative", marginBottom:10 }}>
            <Icon name="search" size={15} style={{ position:"absolute", left:12, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)" }}/>
            <input value={q} onChange={e=>setQ(e.target.value)} autoFocus placeholder="Знайти товар за назвою або артикулом…" style={{
              width:"100%", height:38, boxSizing:"border-box", padding:"0 12px 0 36px",
              background:"var(--bg-base)", color:"var(--fg-primary)", border:"1px solid var(--border-default)", borderRadius:9, fontSize:13, outline:"none", fontFamily:"inherit" }}
              onFocus={e=>e.target.style.borderColor="var(--accent)"}
              onBlur={e=>e.target.style.borderColor="var(--border-default)"}/>
          </div>
          <div style={{ display:"flex", flexDirection:"column", gap:1 }}>
            {results.length===0
              ? <div style={{ padding:"26px 0", textAlign:"center", fontSize:12.5, color:"var(--fg-muted)" }}>Нічого не знайдено</div>
              : results.map(p=><LinkResultRowP key={p.id} p={p} selected={sel?.id===p.id} onSelect={setSel}/>)}
          </div>
        </div>
        <div style={{ display:"flex", alignItems:"center", gap:10, padding:"13px 18px", borderTop:"1px solid var(--border-subtle)", flexShrink:0 }}>
          <span style={{ fontSize:12, color:"var(--fg-muted)", minWidth:0, flex:1, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>
            {sel ? <>Обʼєднати з <span style={{ color:"var(--fg-secondary)", fontWeight:600 }}>{sel.name}</span></> : "Оберіть товар для привʼязки"}
          </span>
          <Button variant="ghost" onClick={onClose}>Скасувати</Button>
          <Button variant="primary" leftIcon="link" disabled={!sel} onClick={()=>onLink(product, sel)}>Привʼязати</Button>
        </div>
      </div>
    </div>
  );
}

// ── Режим «Перевірка привʼязок» (десктоп) ────────────────────────────────────
function SuggestionSideP({ x }) {
  return (
    <div style={{ flex:1, minWidth:0 }}>
      <div style={{ fontSize:13, color:"var(--fg-primary)", lineHeight:1.35, display:"-webkit-box", WebkitLineClamp:2, WebkitBoxOrient:"vertical", overflow:"hidden" }} title={x.name}>{x.name}</div>
      <div style={{ display:"flex", alignItems:"center", gap:7, marginTop:7, flexWrap:"wrap" }}>
        {x.art && <span style={{ fontFamily:"var(--font-mono)", fontSize:11, color:"var(--fg-muted)" }}>{x.art}</span>}
        {(x.suppliers||[]).map((s,i)=><SupplierChipP key={i} sup={s} size="sm"/>)}
      </div>
    </div>
  );
}

function SuggestionCardP({ pair, onConfirm, onDismiss }) {
  return (
    <div style={{ background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:12, padding:16 }}>
      <div style={{ display:"flex", alignItems:"center", gap:10 }}>
        <SuggestionSideP x={pair.a}/>
        <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:5, flexShrink:0, padding:"0 6px" }}>
          <Icon name="arrow-left-right" size={16} color="var(--fg-muted)"/>
          <span style={{ display:"inline-flex", alignItems:"center", gap:3, height:19, padding:"0 8px", borderRadius:999, background:"rgba(245,158,11,.14)", color:"var(--warning)", fontSize:10.5, fontWeight:700, whiteSpace:"nowrap" }}>~{pair.sim}%</span>
        </div>
        <SuggestionSideP x={pair.b}/>
      </div>
      {pair.note && (
        <div style={{ display:"flex", alignItems:"center", gap:7, marginTop:12, padding:"8px 11px", borderRadius:8, background:"rgba(245,158,11,.1)", border:"1px solid rgba(245,158,11,.3)", fontSize:12, color:"var(--warning)", fontWeight:500 }}>
          <Icon name="alert-triangle" size={13} style={{ flexShrink:0 }}/>{pair.note}
        </div>
      )}
      <div style={{ display:"flex", alignItems:"center", gap:8, marginTop:14, justifyContent:"flex-end" }}>
        <Button size="sm" variant="ghost" leftIcon="x" onClick={()=>onDismiss(pair)}>Відхилити</Button>
        <Button size="sm" variant="primary" leftIcon="link" onClick={()=>onConfirm(pair)}>Привʼязати</Button>
      </div>
    </div>
  );
}

function ReviewView({ suggestions, loading, onReload, onConfirm, onDismiss }) {
  return (
    <div style={{ flex:1, display:"flex", flexDirection:"column", minHeight:0 }}>
      <div style={{ display:"flex", alignItems:"center", gap:10, padding:"14px 24px", borderBottom:"1px solid var(--border-subtle)" }}>
        <Icon name="git-compare" size={16} color="var(--accent)"/>
        <span style={{ fontSize:13.5, fontWeight:600, color:"var(--fg-primary)" }}>Можливі склейки за назвою</span>
        <span style={{ fontSize:12, color:"var(--fg-muted)" }}>схожі назви · підтвердьте або відхиліть</span>
        <div style={{ flex:1 }}/>
        <Button size="sm" variant="ghost" leftIcon="refresh-cw" onClick={onReload}>Оновити</Button>
      </div>
      <div style={{ flex:1, overflow:"auto", padding:"16px 24px 28px", display:"flex", flexDirection:"column", gap:10 }}>
        {loading ? (
          <div style={{ display:"flex", alignItems:"center", justifyContent:"center", padding:"60px 0", color:"var(--fg-muted)", gap:10 }}>
            <Icon name="loader" size={22} style={{ animation:"prices-spin 0.8s linear infinite" }}/> Пошук можливих склейок…
          </div>
        ) : !suggestions || suggestions.length===0 ? (
          <div style={{ display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", padding:"64px 24px", gap:12 }}>
            <div style={{ width:54, height:54, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}>
              <Icon name="check-circle" size={24} color="var(--success)"/>
            </div>
            <div style={{ fontSize:14, fontWeight:600, color:"var(--fg-primary)" }}>Немає можливих склейок</div>
            <div style={{ fontSize:12.5, color:"var(--fg-muted)" }}>Усе перевірено або немає схожих товарів</div>
          </div>
        ) : suggestions.map(pair=>(
          <SuggestionCardP key={pair.pairId} pair={pair} onConfirm={onConfirm} onDismiss={onDismiss}/>
        ))}
      </div>
    </div>
  );
}

// ── PricesMode (картки прайс-листів) ─────────────────────────────────────────
const STATUS_META_P = {
  ok:      { color:"var(--success)", soft:"rgba(16,185,129,.13)", icon:"check-circle",  label:"Актуальний" },
  parsing: { color:"var(--accent)",  soft:"var(--accent-soft)",   icon:"loader",         label:"Обробка…" },
  error:   { color:"var(--danger)",  soft:"rgba(244,63,94,.13)",  icon:"alert-octagon",  label:"Помилка" },
  stale:   { color:"var(--warning)", soft:"rgba(245,158,11,.13)", icon:"clock",          label:"Застарів" },
};

function StatCellP({ label, value, color }) {
  return (
    <div style={{ flex:1, padding:"10px 12px" }}>
      <div style={{ fontFamily:"var(--font-mono)", fontSize:17, fontWeight:700, color:color||"var(--fg-primary)" }}>{value}</div>
      <div style={{ fontSize:10.5, color:"var(--fg-muted)", letterSpacing:".03em", textTransform:"uppercase", marginTop:2 }}>{label}</div>
    </div>
  );
}

function PriceListCard({ sup, status, onUpload, onOpenBroken, onAudit }) {
  const s = PRICE_SUPS[sup];
  const st = STATUS_META_P[status]||STATUS_META_P.ok;
  const bg = s.color+"29";
  const [diffOpen, setDiffOpen] = useState(false);
  const [diffData, setDiffData] = useState(null);
  const toggleDiff = () => {
    const next = !diffOpen; setDiffOpen(next);
    if (next && !diffData) {
      fetch(`/api/price-cache/diff/${s.cbPrefix||sup}`).then(r=>r.json())
        .then(j=>{ if(j&&j.ok) setDiffData(j.diff||{changed:[],new:[],out:[]}); else setDiffData({changed:[],new:[],out:[]}); })
        .catch(()=>setDiffData({changed:[],new:[],out:[]}));
    }
  };
  const fmtPr = n => n==null?"—":fmtBareP(n);
  const diffSect = (title, color, rows, render) => rows&&rows.length>0 && (
    <div>
      <div style={{ fontSize:10, fontWeight:700, letterSpacing:".04em", textTransform:"uppercase", color, marginBottom:5 }}>{title} · {rows.length}</div>
      <div style={{ display:"flex", flexDirection:"column", gap:4 }}>{rows.slice(0,60).map(render)}{rows.length>60&&<div style={{fontSize:10.5,color:"var(--fg-muted)"}}>…ще {rows.length-60}</div>}</div>
    </div>
  );
  return (
    <div style={{ background:"var(--bg-panel)", border:`1px solid ${status==="error"?"rgba(244,63,94,.3)":"var(--border-subtle)"}`, borderRadius:12, padding:18, display:"flex", flexDirection:"column", gap:14 }}>
      <div style={{ display:"flex", alignItems:"flex-start", gap:10 }}>
        <div style={{ width:40, height:40, borderRadius:10, background:bg, display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}>
          <Icon name="tags" size={19} color={s.color}/>
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontSize:15, fontWeight:600, color:"var(--fg-primary)" }}>{s.label}</div>
          <div style={{ display:"flex", alignItems:"center", gap:8, marginTop:5 }}>
            <SourceBadgeP source={s.source} size="sm"/>
            <span style={{ fontSize:11, color:"var(--fg-muted)", fontFamily:"var(--font-mono)" }}>{s.cur} {CUR_SYM[s.cur]}</span>
          </div>
        </div>
        <span style={{ display:"inline-flex", alignItems:"center", gap:5, height:22, padding:"0 9px", borderRadius:999, background:st.soft, color:st.color, fontSize:11, fontWeight:600, whiteSpace:"nowrap" }}>
          <Icon name={st.icon} size={12} style={status==="parsing"?{ animation:"prices-spin 0.8s linear infinite" }:undefined}/> {st.label}
        </span>
      </div>
      {status==="error" ? (
        <div style={{ display:"flex", gap:9, padding:"10px 12px", background:"rgba(244,63,94,.1)", border:"1px solid rgba(244,63,94,.28)", borderRadius:9 }}>
          <Icon name="file-x" size={15} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
          <div style={{ fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>Не вдалося розпізнати файл. Перевірте відповідність колонок.</div>
        </div>
      ) : (
        <div style={{ display:"flex", gap:0, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:9, overflow:"hidden" }}>
          <StatCellP label="Позицій" value={fmtBareP(s.positions)}/>
          <div style={{ width:1, background:"var(--border-subtle)" }}/>
          <StatCellP label="Змінено" value={s.changed>0?fmtBareP(s.changed):"—"} color={s.changed>0?"var(--warning)":"var(--fg-muted)"}/>
        </div>
      )}
      {s.diff && ((s.diff.new||0)+(s.diff.up||0)+(s.diff.down||0)+(s.diff.out||0))>0 && (
        <button onClick={toggleDiff} title="Показати, що саме змінилось" style={{ display:"flex", alignItems:"center", flexWrap:"wrap", gap:"4px 12px", fontSize:11.5, marginTop:-4, width:"100%", border:0, background:"transparent", padding:0, cursor:"pointer", fontFamily:"inherit", textAlign:"left" }}>
          {(s.diff.new||0)>0 && <span style={{ color:"var(--info)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="sparkle" size={11}/> +{s.diff.new} нових</span>}
          {((s.diff.up||0)+(s.diff.down||0))>0 && <span style={{ color:"var(--warning)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="arrow-up-down" size={11}/> {(s.diff.up||0)+(s.diff.down||0)} змінили ціну</span>}
          {(s.diff.out||0)>0 && <span style={{ color:"var(--fg-muted)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="package-x" size={11}/> {s.diff.out} зникло</span>}
          <span style={{ flex:1 }}/>
          <Icon name={diffOpen?"chevron-up":"chevron-down"} size={13} color="var(--fg-muted)"/>
        </button>
      )}
      {diffOpen && (
        <div style={{ background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:9, padding:10, maxHeight:260, overflowY:"auto", display:"flex", flexDirection:"column", gap:10 }}>
          {!diffData && <div style={{ fontSize:11.5, color:"var(--fg-muted)" }}>Завантаження…</div>}
          {diffData && ((diffData.changed?.length||0)+(diffData.new?.length||0)+(diffData.out?.length||0))===0 && <div style={{ fontSize:11.5, color:"var(--fg-muted)" }}>Деталі недоступні (зʼявляться після наступного синку).</div>}
          {diffData && diffSect("Змінилась ціна", "var(--warning)", diffData.changed, d=>(
            <div key={"c"+d.art} style={{ display:"flex", alignItems:"center", gap:6, fontSize:11.5 }}>
              <span style={{ flex:1, minWidth:0, color:"var(--fg-secondary)", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{d.name||d.art}</span>
              <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-muted)" }}>{fmtPr(d.prev)}→{fmtPr(d.price)}</span>
              <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, color:(d.pct>0?"var(--danger)":"var(--success)"), minWidth:46, textAlign:"right" }}>{d.pct>0?"+":""}{d.pct}%</span>
            </div>
          ))}
          {diffData && diffSect("Нові", "var(--info)", diffData.new, d=>(
            <div key={"n"+d.art} style={{ display:"flex", alignItems:"center", gap:6, fontSize:11.5 }}>
              <span style={{ flex:1, minWidth:0, color:"var(--fg-secondary)", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{d.name||d.art}</span>
              <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-muted)" }}>{fmtPr(d.price)}</span>
            </div>
          ))}
          {diffData && diffSect("Зникли", "var(--fg-muted)", diffData.out, d=>(
            <div key={"o"+d.art} style={{ display:"flex", alignItems:"center", gap:6, fontSize:11.5, opacity:.75 }}>
              <span style={{ flex:1, minWidth:0, color:"var(--fg-secondary)", textDecoration:"line-through", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{d.name||d.art}</span>
              <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-muted)" }}>{fmtPr(d.price)}</span>
            </div>
          ))}
        </div>
      )}
      {status!=="error" && status!=="parsing" && onAudit && (
        <button onClick={()=>onAudit(sup)} style={{ display:"flex", alignItems:"center", justifyContent:"center", gap:8, width:"100%", height:38, border:0, borderRadius:8, background:"var(--accent)", color:"#fff", fontSize:13, fontWeight:600, fontFamily:"inherit", cursor:"pointer", transition:"background 150ms" }}
          onMouseEnter={e=>e.currentTarget.style.background="var(--accent-hover)"}
          onMouseLeave={e=>e.currentTarget.style.background="var(--accent)"}>
          <Icon name="scan-search" size={16}/> Перевірити повністю
        </button>
      )}
      <div style={{ display:"flex", alignItems:"center", gap:8 }}>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontSize:12, color:status==="stale"?"var(--warning)":"var(--fg-secondary)", display:"flex", alignItems:"center", gap:5 }}>
            <Icon name="clock" size={12}/> {s.last}
          </div>
          <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:2 }}>{s.author}</div>
        </div>
        {status==="error"
          ? <Button size="sm" variant="danger" leftIcon="wrench" onClick={onOpenBroken}>Виправити</Button>
          : <Button size="sm" variant="secondary" leftIcon="upload" onClick={onUpload} disabled={status==="parsing"}>Оновити</Button>}
      </div>
    </div>
  );
}

function AddPriceCard({ onUpload }) {
  const [over, setOver] = useState(false);
  return (
    <div onDragOver={e=>{ e.preventDefault(); setOver(true); }} onDragLeave={()=>setOver(false)}
      onDrop={e=>{ e.preventDefault(); setOver(false); onUpload(); }} onClick={onUpload}
      style={{ border:`1.5px dashed ${over?"var(--accent)":"var(--border-strong)"}`, borderRadius:12,
        background:over?"var(--accent-soft)":"transparent", cursor:"pointer",
        display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", gap:10, minHeight:188, transition:"all 150ms" }}>
      <div style={{ width:44, height:44, borderRadius:11, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}>
        <Icon name="plus" size={22} color={over?"var(--accent)":"var(--fg-muted)"}/>
      </div>
      <div style={{ textAlign:"center" }}>
        <div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Додати прайс</div>
        <div style={{ fontSize:11.5, color:"var(--fg-muted)", marginTop:3 }}>Перетягніть Excel / CSV<br/>або підключіть Telegram-бот</div>
      </div>
    </div>
  );
}

const fmtEtaP = ms => { if(!ms||ms<0) return ""; const s=Math.round(ms/1000); if(s<60) return s+"с"; const m=Math.floor(s/60); return m+"хв"+(s%60?" "+(s%60)+"с":""); };
function PricesModeView({ statuses, onUpload, onOpenBroken, onResetStuck, onReload, onAudit }) {
  const hasStuck = Object.values(statuses).some(s=>s==="parsing");
  // Повний синк усіх прайсів (послідовно) + прогрес-бар з ETA + інфо про авто-синк.
  const [prog, setProg] = useState(null);
  const fastRef = useRef(null);
  const fetchProg = (then) => fetch("/api/price-cache/sync-progress").then(r=>r.json()).then(j=>{ if(j&&j.ok){ setProg(j); then&&then(j); } }).catch(()=>{});
  const startFast = () => { if(fastRef.current) return; fastRef.current = setInterval(()=>fetchProg(jj=>{ if(jj && !jj.running){ clearInterval(fastRef.current); fastRef.current=null; setTimeout(()=>onReload&&onReload(), 800); } }), 1500); };
  useEffect(()=>{
    fetchProg(j=>{ if(j&&j.running) startFast(); });
    const slow = setInterval(()=>{ if(!fastRef.current) fetchProg(); }, 30000);
    return ()=>{ clearInterval(slow); if(fastRef.current){ clearInterval(fastRef.current); fastRef.current=null; } };
  }, []);
  const startSyncAll = () => {
    if(prog&&prog.running) return;
    fetch("/api/price-cache/sync",{method:"POST"}).catch(()=>{});
    setProg(p=>({ ...(p||{}), running:true, total:(p&&p.total)||0, done:0, current:"", etaMs:0 }));
    startFast();
    setTimeout(()=>fetchProg(), 700);
  };
  const syncing = !!(prog&&prog.running);
  const nextAutoIn = (prog&&prog.nextAutoTs)?Math.max(0, prog.nextAutoTs-Date.now()):0;
  const totalOffers = PRICE_PRODUCTS.reduce((a,p)=>a+(p.offers?.length||0),0);
  const uniqCards = PRICE_PRODUCTS.length;
  const agg = SUP_ORDER_P.reduce((a,sup)=>{ const d=(PRICE_SUPS[sup]||{}).diff||{}; a.nw+=d.new||0; a.chg+=(d.up||0)+(d.down||0); a.out+=d.out||0; return a; }, {nw:0,chg:0,out:0});
  const Pill = ({ icon, color, label, value }) => (
    <div style={{ display:"flex", alignItems:"center", gap:8, padding:"9px 13px", background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:10 }}>
      <Icon name={icon} size={15} color={color||"var(--fg-muted)"}/>
      <div><div style={{ fontFamily:"var(--font-mono)", fontSize:16, fontWeight:700, color:color||"var(--fg-primary)", lineHeight:1 }}>{fmtBareP(value)}</div>
      <div style={{ fontSize:10.5, color:"var(--fg-muted)", marginTop:2 }}>{label}</div></div>
    </div>
  );
  return (
    <div style={{ flex:1, overflow:"auto", padding:"20px 24px 32px" }}>
      <HlWorkersPanelP />
      <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:14 }}>
        <h2 style={{ fontSize:15, fontWeight:600, color:"var(--fg-primary)", margin:0 }}>Прайс-листи постачальників</h2>
        <span style={{ fontSize:12, color:"var(--fg-muted)" }}>{SUP_ORDER_P.length} активних джерел</span>
        <div style={{ flex:1 }}/>
        {hasStuck && (
          <Button size="sm" variant="danger" leftIcon="x-circle" onClick={onResetStuck}>
            Скинути застряглі
          </Button>
        )}
        <Button size="sm" variant="primary" leftIcon={syncing?"loader":"refresh-cw"} onClick={startSyncAll} disabled={syncing}>{syncing?"Оновлення…":"Оновити всі прайси"}</Button>
        <Button size="sm" variant="ghost" leftIcon="rotate-cw" onClick={onReload}>Оновити дані</Button>
      </div>
      {/* Прогрес повного синку + інфо про авто-оновлення кожні 2 год */}
      {syncing ? (
        <div style={{ background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:10, padding:"12px 14px", marginBottom:14 }}>
          <div style={{ display:"flex", alignItems:"center", gap:8, fontSize:12.5, marginBottom:8 }}>
            <Icon name="loader" size={14} color="var(--accent)" style={{ animation:"prices-spin 0.8s linear infinite" }}/>
            <span style={{ fontWeight:600, color:"var(--fg-primary)" }}>Оновлення прайсів {prog.done||0}/{prog.total||0}</span>
            {prog.current && <span style={{ color:"var(--fg-muted)" }}>· {prog.current}</span>}
            <span style={{ flex:1 }}/>
            {prog.etaMs>0 && <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-secondary)" }}>~{fmtEtaP(prog.etaMs)} лишилось</span>}
          </div>
          <div style={{ height:7, borderRadius:999, background:"var(--bg-base)", overflow:"hidden" }}>
            <div style={{ height:"100%", borderRadius:999, background:"var(--accent)", width:`${prog.total?Math.round((prog.done/prog.total)*100):4}%`, transition:"width 400ms" }}/>
          </div>
        </div>
      ) : (
        <div style={{ display:"flex", alignItems:"center", gap:6, fontSize:11.5, color:"var(--fg-muted)", marginBottom:14 }}>
          <Icon name="clock" size={12}/>
          Авто-оновлення всіх прайсів кожні 2 год{prog&&prog.nextAutoTs?` · наступне ~через ${fmtEtaP(nextAutoIn)}`:""}
        </div>
      )}
      {/* Зведення: позиції / унікальні картки + сумарна аналітика останнього оновлення */}
      <div style={{ display:"flex", gap:10, flexWrap:"wrap", marginBottom:18 }}>
        <Pill icon="layers" label="всього позицій" value={totalOffers}/>
        <Pill icon="copy-check" color="var(--accent)" label="унікальних карток" value={uniqCards}/>
        <div style={{ width:1, alignSelf:"stretch", background:"var(--border-subtle)", margin:"0 2px" }}/>
        <Pill icon="sparkle" color="var(--info)" label="нових (ост. оновлення)" value={agg.nw}/>
        <Pill icon="arrow-up-down" color="var(--warning)" label="змінили ціну" value={agg.chg}/>
        <Pill icon="package-x" color="var(--fg-muted)" label="зникли з наявності" value={agg.out}/>
      </div>
      <div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill, minmax(330px, 1fr))", gap:14 }}>
        {SUP_ORDER_P.map(sup=>(
          <PriceListCard key={sup} sup={sup} status={statuses[sup]||PRICE_SUPS[sup].status}
            onUpload={()=>onUpload(sup)} onOpenBroken={()=>onOpenBroken(sup)} onAudit={onAudit}/>
        ))}
        <AddPriceCard onUpload={()=>onUpload(null)}/>
      </div>
    </div>
  );
}

// ── UploadWizard ──────────────────────────────────────────────────────────────
const UPLOAD_FILE_COLS = ["Код","Найменування","Опт","Вал.","Залишок","Статус"];
const UPLOAD_ROWS = [
  ["16806119","iPhone 15 Pro Max 256 Nat.Tit","45 200","грн","12","є"],
  ["MRXV3UA","MacBook Air 13 M3 8/256 Midnight","40 650","грн","3","є"],
  ["SM-S928BZ","Galaxy S24 Ultra 12/256 Black","38 000","грн","4","є"],
  ["MTJV3UA","AirPods Pro 2 USB-C","8 050","грн","18","є"],
  ["WH1000XM5","Sony WH-1000XM5 Black","11 800","грн","6","є"],
  ["910-006559","Logitech MX Master 3S","2 980","грн","33","є"],
  ["DJI-MINI4","DJI Mini 4 Pro Fly More","—","грн","0","нема"],
];
const AUTO_MAP_P = { 0:{f:"art",c:98}, 1:{f:"name",c:95}, 2:{f:"price",c:92}, 3:{f:"cur",c:88}, 4:{f:"qty",c:81}, 5:{f:"stock",c:67} };
const FIELD_OPTS_P = [["art","Артикул"],["name","Назва"],["price","Ціна закупу"],["cur","Валюта"],["qty","Кількість"],["stock","Наявність"],["skip","— пропустити —"]];
const UPLOAD_DIFF = [
  { type:"up",   art:"16806119", name:"iPhone 15 Pro Max 256 Nat.Tit",   from:44600, to:45200, cur:"UAH" },
  { type:"down", art:"MRXV3UA",  name:"MacBook Air 13 M3 8/256",          from:41400, to:40650, cur:"UAH" },
  { type:"up",   art:"SM-S928BZ",name:"Galaxy S24 Ultra 12/256",          from:37200, to:38000, cur:"UAH" },
  { type:"down", art:"MTJV3UA",  name:"AirPods Pro 2 USB-C",              from:8350,  to:8050,  cur:"UAH" },
  { type:"new",  art:"WH1000XM5",name:"Sony WH-1000XM5 Black",            from:null,  to:11800, cur:"UAH" },
  { type:"out",  art:"DJI-MINI4",name:"DJI Mini 4 Pro Fly More",          from:41200, to:null,  cur:"UAH" },
];
const DIFF_META_P = {
  up:   { color:"var(--danger)",  icon:"arrow-up-right",   label:"Подорожчало" },
  down: { color:"var(--success)", icon:"arrow-down-right", label:"Подешевшало" },
  new:  { color:"#3B82F6",        icon:"sparkle",          label:"Нові" },
  out:  { color:"var(--fg-muted)",icon:"package-x",        label:"Зникли" },
};

function StepDots({ step, steps }) {
  return (
    <div style={{ display:"flex", alignItems:"center" }}>
      {steps.map((s,i)=>{
        const done=i<step, active=i===step;
        return (
          <React.Fragment key={s}>
            <div style={{ display:"flex", alignItems:"center", gap:8 }}>
              <span style={{ width:22, height:22, borderRadius:"50%", display:"flex", alignItems:"center", justifyContent:"center", fontSize:11, fontWeight:700, fontFamily:"var(--font-mono)", background:done?"var(--success)":active?"var(--accent)":"var(--bg-base)", color:done||active?"#fff":"var(--fg-muted)", border:done||active?"0":"1px solid var(--border-default)" }}>{done?"✓":i+1}</span>
              <span style={{ fontSize:12, fontWeight:active?600:500, color:active?"var(--fg-primary)":"var(--fg-muted)" }}>{s}</span>
            </div>
            {i<steps.length-1 && <div style={{ width:28, height:1, background:"var(--border-default)", margin:"0 12px" }}/>}
          </React.Fragment>
        );
      })}
    </div>
  );
}

function UploadWizard({ sup, startBroken, onClose, onSave }) {
  const [step, setStep] = useState(0);
  const [broken, setBroken] = useState(false);
  const [mapping, setMapping] = useState(()=>{ const m={}; Object.entries(AUTO_MAP_P).forEach(([col,v])=>m[col]=v.f); return m; });
  const steps=["Файл","Колонки","Зміни"];
  const supLabel = sup ? PRICE_SUPS[sup].label : "Новий постачальник";

  useEffect(()=>{ if(startBroken){ setStep(0); setBroken(true); } }, [startBroken]);
  const onFile = kind => { if(kind==="broken") setBroken(true); else { setBroken(false); setStep(1); } };
  const next = () => setStep(s=>Math.min(2,s+1));
  const back = () => { if(broken){ setBroken(false); return; } setStep(s=>Math.max(0,s-1)); };

  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, zIndex:50, display:"flex", alignItems:"center", justifyContent:"center", background:"rgba(0,0,0,.55)", padding:32 }}>
      <div onClick={e=>e.stopPropagation()} style={{ width:740, maxHeight:"88vh", background:"var(--bg-panel)", border:"1px solid var(--border-default)", borderRadius:14, boxShadow:"var(--shadow-2)", display:"flex", flexDirection:"column", overflow:"hidden", animation:"prices-sheetup 220ms cubic-bezier(.2,0,0,1)" }}>
        <div style={{ display:"flex", alignItems:"center", gap:12, padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
          <Icon name="upload" size={18} color="var(--accent)"/>
          <h3 style={{ fontSize:15, fontWeight:600, margin:0, color:"var(--fg-primary)" }}>Завантаження прайсу · {supLabel}</h3>
          <div style={{ flex:1 }}/>
          <button onClick={onClose} style={{ width:32, height:32, border:0, background:"transparent", color:"var(--fg-secondary)", borderRadius:6, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={18}/></button>
        </div>
        {!broken && <div style={{ padding:"14px 20px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}><StepDots step={step} steps={steps}/></div>}
        <div style={{ flex:1, overflow:"auto", padding:20 }}>
          {broken ? (
            <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:16, padding:"20px 16px" }}>
              <div style={{ width:56, height:56, borderRadius:14, background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.3)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="file-x" size={26} color="var(--danger)"/></div>
              <div style={{ textAlign:"center" }}>
                <h3 style={{ fontSize:16, fontWeight:600, color:"var(--fg-primary)", margin:0 }}>Не вдалося прочитати прайс</h3>
                <p style={{ fontSize:12.5, color:"var(--fg-muted)", marginTop:8, maxWidth:380, lineHeight:1.5 }}>Не вдалося розпізнати структуру файлу. Перевірте колонки в налаштуваннях постачальника.</p>
              </div>
              <div style={{ display:"flex", gap:8, width:"100%", maxWidth:400 }}>
                <Button variant="primary" leftIcon="rotate-ccw" style={{ flex:1 }} onClick={()=>{ setBroken(false); setStep(0); }}>Завантажити інший файл</Button>
                <Button variant="secondary" leftIcon="sliders-horizontal" onClick={()=>{ setBroken(false); setStep(1); }}>Вручну</Button>
              </div>
            </div>
          ) : step===0 ? (
            <div style={{ display:"flex", flexDirection:"column", gap:16 }}>
              <div onClick={()=>onFile("ok")} style={{ border:"1.5px dashed var(--border-strong)", borderRadius:14, background:"var(--bg-base)", cursor:"pointer", padding:"46px 24px", display:"flex", flexDirection:"column", alignItems:"center", gap:14 }}>
                <div style={{ width:56, height:56, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="upload-cloud" size={26} color="var(--fg-secondary)"/></div>
                <div style={{ textAlign:"center" }}>
                  <div style={{ fontSize:15, fontWeight:600, color:"var(--fg-primary)" }}>Перетягніть файл прайсу сюди</div>
                  <div style={{ fontSize:12.5, color:"var(--fg-muted)", marginTop:5 }}>Excel (.xlsx, .xls) або .csv · до 20 МБ</div>
                </div>
                <Button variant="secondary" leftIcon="folder-open">Обрати файл</Button>
              </div>
              <div style={{ display:"flex", gap:10 }}>
                <button onClick={()=>onFile("ok")} style={{ flex:1, display:"flex", alignItems:"center", gap:11, padding:"12px 14px", borderRadius:10, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit", textAlign:"left" }}>
                  <div style={{ width:32, height:32, borderRadius:8, background:"rgba(34,158,217,.14)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="send" size={15} color="#229ED9"/></div>
                  <div><div style={{ fontSize:12.5, fontWeight:600, color:"var(--fg-primary)" }}>Файл з Telegram</div><div style={{ fontSize:11, color:"var(--fg-muted)" }}>{sup?PRICE_SUPS[sup].author:"@bot_price"}</div></div>
                </button>
                <button onClick={()=>onFile("broken")} style={{ flex:1, display:"flex", alignItems:"center", gap:11, padding:"12px 14px", borderRadius:10, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit", textAlign:"left" }}>
                  <div style={{ width:32, height:32, borderRadius:8, background:"rgba(244,63,94,.12)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="file-x" size={15} color="var(--danger)"/></div>
                  <div><div style={{ fontSize:12.5, fontWeight:600, color:"var(--fg-primary)" }}>Демо: кривий файл</div><div style={{ fontSize:11, color:"var(--fg-muted)" }}>побачити стан помилки</div></div>
                </button>
              </div>
            </div>
          ) : step===1 ? (
            <div>
              <div style={{ display:"flex", alignItems:"center", gap:8, fontSize:12.5, color:"var(--fg-secondary)", marginBottom:14, padding:"9px 12px", background:"rgba(16,185,129,.08)", border:"1px solid rgba(16,185,129,.22)", borderRadius:9 }}>
                <Icon name="wand-2" size={15} color="var(--success)"/> Колонки визначено автоматично. Перевірте маппінг — змініть за потреби.
              </div>
              <div style={{ border:"1px solid var(--border-subtle)", borderRadius:10, overflow:"hidden" }}>
                <div style={{ display:"flex", background:"var(--bg-panel)", borderBottom:"1px solid var(--border-default)" }}>
                  {UPLOAD_FILE_COLS.map((c,i)=>{
                    const m=AUTO_MAP_P[i]; const low=m&&m.c<75;
                    return (
                      <div key={i} style={{ flex:1, minWidth:0, padding:10, borderRight:i<UPLOAD_FILE_COLS.length-1?"1px solid var(--border-subtle)":"0" }}>
                        <div style={{ fontSize:10.5, color:"var(--fg-muted)", marginBottom:5, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>файл: {c}</div>
                        <div style={{ position:"relative" }}>
                          <select value={mapping[i]} onChange={e=>setMapping({...mapping,[i]:e.target.value})} style={{ width:"100%", boxSizing:"border-box", height:30, padding:"0 22px 0 9px", appearance:"none", background:"var(--bg-raised)", color:"var(--fg-primary)", border:`1px solid ${low?"var(--warning)":"var(--border-default)"}`, borderRadius:7, fontSize:11.5, fontFamily:"inherit", cursor:"pointer", outline:"none", fontWeight:600 }}>
                            {FIELD_OPTS_P.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                          </select>
                          <Icon name="chevron-down" size={12} style={{ position:"absolute", right:6, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", pointerEvents:"none" }}/>
                        </div>
                        {m&&<div style={{ fontSize:10, color:low?"var(--warning)":"var(--success)", marginTop:5, display:"flex", alignItems:"center", gap:3 }}><Icon name={low?"alert-triangle":"check"} size={10}/> {m.c}%</div>}
                      </div>
                    );
                  })}
                </div>
                <div style={{ maxHeight:220, overflow:"auto" }}>
                  {UPLOAD_ROWS.map((row,ri)=>(
                    <div key={ri} style={{ display:"flex", borderBottom:ri<UPLOAD_ROWS.length-1?"1px solid var(--border-subtle)":"0", background:ri%2?"var(--bg-base)":"transparent" }}>
                      {row.map((cell,ci)=>(
                        <div key={ci} style={{ flex:1, minWidth:0, padding:"8px 11px", fontSize:12, fontFamily:[2,4].includes(ci)?"var(--font-mono)":"inherit", color:mapping[ci]==="skip"?"var(--fg-disabled)":ci===1?"var(--fg-primary)":"var(--fg-secondary)", borderRight:ci<row.length-1?"1px solid var(--border-subtle)":"0", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{cell}</div>
                      ))}
                    </div>
                  ))}
                </div>
              </div>
              <div style={{ fontSize:11.5, color:"var(--fg-muted)", marginTop:10, textAlign:"center" }}>Показано 7 із 938 рядків</div>
            </div>
          ) : (
            <div>
              <div style={{ display:"flex", gap:8, marginBottom:14 }}>
                {["all","up","down","new","out"].map(k=>{
                  const on=step===2&&k==="all"||false; const m=DIFF_META_P[k]; const val=k==="all"?938:{up:88,down:41,new:12,out:5}[k];
                  return (
                    <div key={k} style={{ flex:1, padding:"10px 8px", borderRadius:9, textAlign:"left", background:"var(--bg-base)", border:`1px solid ${k==="all"?"var(--accent)":"var(--border-subtle)"}` }}>
                      <div style={{ display:"flex", alignItems:"center", gap:5, marginBottom:5 }}>
                        {m?<Icon name={m.icon} size={13} color={m.color}/>:<Icon name="list" size={13} color="var(--fg-muted)"/>}
                        <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, fontSize:16, color:m?m.color:"var(--fg-primary)" }}>{val}</span>
                      </div>
                      <div style={{ fontSize:10.5, color:"var(--fg-secondary)" }}>{k==="all"?"Всього":m.label}</div>
                    </div>
                  );
                })}
              </div>
              <div style={{ border:"1px solid var(--border-subtle)", borderRadius:10, overflow:"hidden" }}>
                <div style={{ maxHeight:244, overflow:"auto" }}>
                  {UPLOAD_DIFF.map((d,i)=>{
                    const m=DIFF_META_P[d.type];
                    return (
                      <div key={i} style={{ display:"flex", alignItems:"center", gap:12, padding:"10px 14px", borderBottom:i<UPLOAD_DIFF.length-1?"1px solid var(--border-subtle)":"0", background:i%2?"var(--bg-base)":"transparent" }}>
                        <Icon name={m.icon} size={15} color={m.color} style={{ flexShrink:0 }}/>
                        <div style={{ flex:1, minWidth:0 }}>
                          <div style={{ fontSize:12.5, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{d.name}</div>
                          <div style={{ fontSize:11, color:"var(--fg-muted)", fontFamily:"var(--font-mono)" }}>{d.art}</div>
                        </div>
                        <div style={{ display:"flex", alignItems:"center", gap:8, fontFamily:"var(--font-mono)", fontSize:12.5, flexShrink:0 }}>
                          {d.from!=null&&<span style={{ color:"var(--fg-muted)", textDecoration:d.to==null?"none":"line-through" }}>{fmtCurP(d.from,d.cur)}</span>}
                          {d.from!=null&&d.to!=null&&<Icon name="arrow-right" size={12} color="var(--fg-disabled)"/>}
                          {d.to!=null&&<span style={{ color:m.color, fontWeight:600 }}>{fmtCurP(d.to,d.cur)}</span>}
                          {d.to==null&&<span style={{ color:"var(--fg-muted)" }}>зникло</span>}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            </div>
          )}
        </div>
        {!broken && (
          <div style={{ display:"flex", alignItems:"center", gap:10, padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", flexShrink:0 }}>
            {step>0 && <Button variant="ghost" leftIcon="arrow-left" onClick={back}>Назад</Button>}
            <div style={{ flex:1 }}/>
            {step===2&&<span style={{ fontSize:12, color:"var(--fg-muted)", marginRight:4 }}>146 змін · 938 позицій</span>}
            {step<2
              ? <Button variant="primary" leftIcon="arrow-right" onClick={next} disabled={step===0}>Далі</Button>
              : <Button variant="primary" leftIcon="check" onClick={onSave}>Застосувати зміни</Button>}
          </div>
        )}
      </div>
    </div>
  );
}

// ── PricesPage (root) ─────────────────────────────────────────────────────────
// ============================================================================
//  Аудит прайсу постачальника — повноекранний фокус-режим.
//  Відкривається з картки постачальника (режим «Прайси» → «Перевірити повністю»).
//  Дані — РЕАЛЬНІ з прайс-кешу (PRICE_PRODUCTS): закупівля цього постачальника,
//  наша ціна на сайті, інші постачальники на тій же картці, маржа, статус.
//  Лесенка Hotline тягнеться ЛІНИВО по розкриттю рядка (бек: /api/price-audit/hotline).
// ============================================================================
const AUD_STATUS = {
  site:    { color: "var(--success)", soft: "rgba(16,185,129,.13)", icon: "check-circle", label: "є на сайті" },
  missing: { color: "var(--warning)", soft: "rgba(245,158,11,.14)", icon: "circle-plus",  label: "немає на сайті" },
};
function AudStatusBadge({ status, size = "md" }) {
  const m = AUD_STATUS[status] || AUD_STATUS.site, sm = size === "sm";
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, height: sm ? 19 : 22,
      padding: sm ? "0 8px" : "0 9px", borderRadius: 999, background: m.soft, color: m.color,
      fontSize: sm ? 10.5 : 11, fontWeight: 600, whiteSpace: "nowrap" }}>
      <Icon name={m.icon} size={sm ? 11 : 12} /> {m.label}
    </span>
  );
}

// Жорстка нормалізація артикулу для звірки прайс ↔ каталог сайту (latin+цифри)
const audNormArt = s => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
// Артикул часто ховається в НАЗВІ в дужках: «… CR-Scanner Otter (4008050048)».
// Дістаємо такі коди (з усіх дужок), щоб зматчити товар із сайтом, коли поле
// артикула постачальника порожнє/не збігається. Беремо лише «схожі на SKU» токени
// (довжина ≥5, є цифра) — і використовуємо ЛИШЕ як фолбек + тільки при точному
// збігу з каталогом, тож хибних привʼязок не буде.
function artsFromName(name) {
  const out = [];
  for (const chunk of (String(name || "").match(/\(([^)]+)\)/g) || [])) {
    for (const t of chunk.replace(/[()]/g, "").split(/[\s,;/]+/)) {
      const v = t.trim();
      // SKU-подібний: є цифра АБО крапка всередині (кейс «alpha.black»/«edge.black» —
      // артикули без цифр; крапка не зустрічається у звичайних словах, тож «Pink»/«Wi-Fi» не пройдуть)
      if (v.length >= 5 && (/\d/.test(v) || /^[^.]+\.[^.]+/.test(v)) && /^[A-Za-z0-9._-]+$/.test(v)) out.push(v);
    }
  }
  // Фолбек: артикул БЕЗ дужок хвостом назви («DJI Mic Mini 2 … CP.RN.00000529.01» у pro100).
  // SKU-подібний токен з КІНЦЯ: ≥6 симв., ≥2 цифри, є літера/крапка, без одиниць виміру
  // (8/128GB, 1000W, 2TX+1RX не пройдуть: + не в чарсеті, GB/W — стоп-суфікси).
  if (!out.length) {
    const toks = String(name || "").replace(/[()]/g, " ").trim().split(/[\s,;]+/);
    for (let i = toks.length - 1; i >= 0; i--) {
      const v = toks[i].trim().replace(/[.,;:]+$/, "");
      if (v.length >= 6 && (v.match(/\d/g) || []).length >= 2 && /[A-Za-z.]/.test(v)
          && /^[A-Za-z0-9./_-]+$/.test(v)
          && !/(gb|tb|гб|тб|mah|wh|hz|гц|вт|мм|ml|мл)$/i.test(v)) { out.push(v); break; }
    }
  }
  return [...new Set(out)];
}
// Артикул для СТВОРЕННЯ картки: поле прайсу → SKU-код з назви → синтетичний.
// QA-гейт авто-публікації вимагає артикул, а частина товарів у прайсах його не має
// взагалі — таким генеруємо детермінований «AI-XXXXXXX» (хеш нормалізованої назви,
// base36): один і той самий товар завжди дає той самий код, а колізію з чужим
// артикулом сайту ловлять жива перевірка перед enqueue + серверний гейт перезапису.
function synthArtP(name) {
  const s = String(name || "").toLowerCase().replace(/\s+/g, " ").trim();
  let h = 5381;
  for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
  return "AI-" + h.toString(36).toUpperCase();
}
const artForCardP = row => String(row.art || artsFromName(row.name)[0] || synthArtP(row.name));
// Авто-привʼязка «назва → артикул сайту» при створенні картки для товару БЕЗ артикула
// в прайсі (артикул виведено з назви або синтетичний): наступні аудити одразу бачать
// його «на сайті» — без повторної ручної привʼязки. auto:true — MatchWalker знає, що
// це не рішення людини: якщо бинд так і не зматчився з каталогом, запропонує ще раз.
// Бинд з артикулом, якого (ще) немає в каталозі, нешкідливий — аудит вимагає збігу.
function autoBindArtP(row, article) {
  try {
    if (String(row.art || "").trim()) return;   // у прайсі є артикул — матч і так спрацює
    const key = audNameKey(row.name);
    if (!key || !article) return;
    fetch("/api/price-audit/site-bind", { method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ key, article: String(article), auto: true }) }).catch(() => {});
  } catch {}
}
// ── Зіставлення по НАЗВІ (для товарів без артикула) ────────────────────────────
// Стоп-слова й маркетингові/регіональні хвости, що не несуть ідентичності товару.
const AUD_NAME_STOP = new Set(["ua","uah","грн","шт","new","новий","нова","версія","version","global","eu","міжнародна","офіційна","гарантія","рст","оригінал","official"]);
// Назву → значущі токени (літери/цифри, ≥2 символи, без стоп-слів). Кир+лат лишаємо як є.
function audNameTokens(s) {
  return String(s || "").toLowerCase()
    .replace(/[^\p{L}\p{N}]+/gu, " ")
    .split(/\s+/).filter(t => t.length >= 2 && !AUD_NAME_STOP.has(t));
}
// Стабільний ключ прив'язки за назвою (склеєні токени).
const audNameKey = s => audNameTokens(s).join("");
// ── Токени для СХОЖОСТІ (НЕ для ключів привʼязки — audNameKey НЕ чіпати, зламає біндинги!) ──
// Відмінності від audNameTokens (калібровка 04.07 на 30 живих кейсах Богдана):
//  • артикул у дужках хвостом назви ВИРІЗАЄТЬСЯ — фрагменти артикулів (rz04…r3m1 у ВСІЄЇ
//    лінійки Razer) накачували схожість ЧУЖИМ моделям і карали свої (WD X0E vs XHE);
//  • одиночні цифри та «x» ЛИШАЮТЬСЯ — інакше «MX Anywhere 3»≠«3S», «Pocket 4»≠«3»,
//    «2 TB»≠«1 TB», «Barracuda»≠«Barracuda X» були НЕВИДИМІ для порівняння.
function audStripArtParen(s) {
  return String(s || "").replace(/\(([^()]{4,})\)\s*$/, (m, inner) =>
    /\d/.test(inner) && inner.replace(/[^a-z0-9а-яёіїєґ]/gi, "").length >= 5 ? "" : m);
}
function audSimTokens(s) {
  return audStripArtParen(s).toLowerCase()
    .replace(/[^\p{L}\p{N}]+/gu, " ")
    .split(/\s+/).filter(t => t && (t.length >= 2 || /^[0-9x]$/.test(t)) && !AUD_NAME_STOP.has(t));
}
// Версійні слова: їх розрив між назвами = ІНША модель (G535 vs G535 Console, QC vs QC Ultra).
const AUD_VERSION_TOKENS = new Set(["x", "pro", "max", "ultra", "plus", "mini", "lite", "se", "xl", "xxl", "console", "chroma", "elite", "premier", "turbo", "gen", "ii", "iii"]);
const audModelishTok = t => /\d/.test(t) || AUD_VERSION_TOKENS.has(t);
// Кольори для порівняння НАЗВ (компакт-дзеркало серверного словника): точний збіг токена;
// ≥2 різні групи в назві (Rose Gold, Red Velvet/Gold) → колір неоднозначний → не порівнюємо.
const AUD_COLORS = [
  ["black", "onyx", "obsidian", "midnight", "чорний", "черный"],
  ["white", "ivory", "starlight", "mercury", "білий", "белый"],
  ["silver", "platinum", "срібний", "серебристый"],
  ["gold", "golden", "champagne", "золотий", "золотой"],
  ["blue", "navy", "indigo", "sapphire", "azure", "синій", "синий", "блакитний", "голубой"],
  ["red", "crimson", "scarlet", "burgundy", "maroon", "червоний", "красный", "бордовий", "бордовый"],
  ["green", "olive", "khaki", "emerald", "mint", "зелений", "зеленый", "хакі", "хаки"],
  ["gray", "grey", "graphite", "titanium", "anthracite", "charcoal", "сірий", "серый", "графіт", "графит", "титан"],
  ["pink", "rose", "quartz", "рожевий", "розовый"],
  ["purple", "violet", "lavender", "lilac", "фіолетовий", "фиолетовый"],
  ["yellow", "lemon", "жовтий", "желтый"],
  ["beige", "sand", "sandstone", "бежевий", "бежевый"],
  ["brown", "chocolate", "walnut", "коричневий", "коричневый"],
  ["cream", "кремовий", "кремовый", "молочний", "молочный"],
  ["orange", "coral", "помаранчевий", "оранжевий", "оранжевый"],
  ["teal", "cyan", "turquoise", "бірюзовий", "бирюзовый"],
];
function audColorOf(s) {
  const toks = String(s || "").toLowerCase().match(/[a-zа-яёіїєґ]+/g) || [];
  if (!toks.length) return null;
  const hit = new Set();
  for (const g of AUD_COLORS) if (g.some(w => toks.includes(w))) hit.add(g[0]);
  return hit.size === 1 ? [...hit][0] : null;
}
// Розмір з назви («Size 8», «розмір 10») — для кілець/браслетів: інший розмір = інший товар.
function audSizeOf(s) {
  const m = String(s || "").match(/(?:size|розмір|размер)\s*[:#]?\s*(\d{1,2})\b/i);
  return m ? m[1] : null;
}
// Топ-кандидати з каталогу сайту для назви, що не зматчилась артикулом.
// catalog: [{article,name,price,supplier,_set}] (_set — Set токенів назви, прекеш).
// art (опційно) — артикул(и) рядка прайсу. Анти-сміття (кейс Oura/G435/Dyson — пропонувались
// сусідні розміри/кольори/моделі):
//  • інший Size у назві → геть;  • однозначно ІНШИЙ колір → геть;
//  • у прайсі Є артикул, у кандидата ЯВНО інший (без префікс/суфікс-споріднення) → лишаємо
//    лише ДУЖЕ схожі назви (sim ≥ 0.85) — легітимний кейс «різні артикули одного товару
//    в різних постачальників» має майже ідентичну назву і проходить.
function audSiteCandidates(name, catalog, limit = 3, art = "") {
  const q = audSimTokens(name);
  if (q.length < 2 || !Array.isArray(catalog) || !catalog.length) return [];
  const qset = new Set(q);
  const qSize = audSizeOf(name), qColor = audColorOf(audStripArtParen(name));
  const artKeys = String(art || "").split(/[,;]/).map(a => audNormArt(a)).filter(k => k && k.length >= 4);
  const artRelated = (ck) => artKeys.some(a => a === ck || (a.length >= 5 && ck.length >= 5 && (ck.startsWith(a) || a.startsWith(ck) || ck.endsWith(a) || a.endsWith(ck))));
  const scored = [];
  for (const c of catalog) {
    // _simSet — лінивий кеш sim-токенів кандидата (окремо від _set, який живе для audNameKey-потоку)
    const cs = c._simSet || (c._simSet = new Set(audSimTokens(c.name)));
    if (!cs.size) continue;
    let hit = 0, modelQ = 0, modelHit = 0, missModel = 0;
    for (const t of q) {
      const m = audModelishTok(t);
      if (m) modelQ++;
      if (cs.has(t)) { hit++; if (m) modelHit++; }
      else if (m) missModel++;
    }
    let sim = hit / q.length;
    if (modelQ && !modelHit) sim *= 0.25;   // жоден модельний токен не збігся (Z70 vs Z60)
    // «Модельні розриви» — цифро/версійні токени, які є лише з ОДНОГО боку: 3↔3S, 15↔15C,
    // 2TB↔1TB, Pocket 4↔3, G535↔G535 Console, Barracuda↔Barracuda X. Кожен розрив — це
    // майже напевно ІНША модель/обсяг → 100% неможливі, стеля падає з кожним розривом.
    let extraModel = 0;
    for (const t of cs) if (!qset.has(t) && audModelishTok(t)) extraModel++;
    const modelGaps = missModel + extraModel;
    if (modelGaps) sim = Math.min(sim, 0.84 - 0.05 * (modelGaps - 1));
    if (sim < 0.6) continue;
    const cSize = audSizeOf(c.name);
    if (qSize && cSize && qSize !== cSize) continue;
    const cColor = audColorOf(audStripArtParen(c.name));
    if (qColor && cColor && qColor !== cColor) continue;
    if (artKeys.length && c.article) {
      const ck = audNormArt(c.article);
      if (ck && !artRelated(ck)) {
        // ЯВНО чужий артикул: легітимний лише кейс «той самий товар під іншим артикулом
        // в іншого постачальника» — назви тоді збігаються аж до артикула. Тому:
        // будь-який модельний розрив → геть (інша модель тієї ж лінійки, кейс Razer Barracuda);
        // кандидат МАЄ колір, якого нема в запиті → інший колір-варіант (Barracuda Quartz);
        // і, як і раніше, дуже схожа назва (sim ≥ 0.85).
        if (modelGaps) continue;
        if (cColor && !qColor) continue;
        if (sim < 0.85) continue;
      }
    }
    scored.push({ article: c.article, name: c.name, price: c.price, supplier: c.supplier, sim });
  }
  scored.sort((a, b) => b.sim - a.sim);
  return scored.slice(0, limit);
}
// Назва постачальника з сайту → наш cbPrefix (якщо такий постачальник є у прайсах)
function supPrefixByName(name) {
  if (!name) return null;
  const n = String(name).toLowerCase().trim();
  for (const k in PRICE_SUPS) if (String(PRICE_SUPS[k].label || "").toLowerCase().trim() === n) return k;
  return null;
}
// Збираємо позиції одного постачальника з PRICE_PRODUCTS.
// siteMap: Map(нормалізований артикул → ціна на сайті) з /api/catalog/products —
// саме він дає «є на сайті», бо у прайс-кеші власної ціни немає.
function buildAuditRows(cbPrefix, siteMap, siteBinds) {
  const rows = [];
  for (const p of PRICE_PRODUCTS) {
    const mine = p.offers.find(o => o.sup === cbPrefix);
    if (!mine) continue;
    const others = p.offers.filter(o => o.sup !== cbPrefix)
      .map(o => ({ sup: o.sup, price: o.price, cur: o.cur, stock: o.stock, qty: o.qty, uah: o.uah }))
      .sort((a, b) => a.uah - b.uah);
    // Товар може мати кілька артикулів («PB634-A-CIS, PB634-A-WW») — перевіряємо КОЖЕН
    // у siteMap, перший знайдений визначає привʼязку. siteArticle — реальний артикул на
    // сайті (для оновлення ціни/наявності саме по ньому, а не по всьому рядку).
    const artTokens = String(p.art ?? "").split(/\s*[,;]\s*/).map(x => x.trim()).filter(Boolean);
    let siteRec = null, siteArticle = null;
    for (const a of (artTokens.length ? artTokens : [p.art])) {
      const k = audNormArt(a);
      if (siteMap && k && siteMap.has(k)) {
        siteRec = siteMap.get(k);
        // Для оновлення беремо АРТИКУЛ ЯК НА САЙТІ (siteArt) — якщо там «A17260Z1, A1728311»,
        // оновлюємо саме по ньому, інакше Хорошоп не змапить; фолбек на знайдений токен.
        siteArticle = siteRec.siteArt || a;
        break;
      }
    }
    // Фолбек: артикула в полі немає/не збігся — пробуємо код із дужок у назві.
    if (!siteRec && siteMap) {
      for (const a of artsFromName(p.name)) {
        const k = audNormArt(a);
        if (k && siteMap.has(k)) { siteRec = siteMap.get(k); siteArticle = siteRec.siteArt || a; break; }
      }
    }
    // Фолбек 2: РУЧНА прив'язка по назві. Значення: {article} = підтверджено, {dismissed} = «не той».
    let manualBound = false, nameDismissed = false;
    if (siteMap && siteBinds) {
      const b = siteBinds[audNameKey(p.name)];
      if (b && b.dismissed) nameDismissed = true;
      else if (b && b.article && !siteRec) {
        const k = audNormArt(b.article);
        if (k && siteMap.has(k)) { siteRec = siteMap.get(k); siteArticle = siteRec.siteArt || b.article; manualBound = true; }
      }
    }
    const siteOur = siteRec ? siteRec.price : null;
    const siteSupplier = siteRec ? siteRec.supplier : "";
    const siteSupPrefix = supPrefixByName(siteSupplier);
    const our = p.our != null ? p.our : siteOur;
    // «На сайті» = знайдено в каталозі (за артикулом/дужками/прив'язкою), НАВІТЬ якщо ціна 0/порожня
    // (інакше товар із ціною 0 — або щойно прив'язаний — хибно вертався б у «немає на сайті»).
    const onSite = (our != null) || !!siteRec;
    const buy = mine.uah;
    const margin = onSite ? our - buy : null;
    const marginPct = (onSite && our > 0) ? (margin / our) * 100 : null;
    const buyChange = mine.change, buyPct = mine.deltaPct;
    const marginRisk = margin != null && (margin < 0 || (buyChange === "up" && marginPct < TARGET_MARGIN));
    const isSource = !!(p.best && p.best.sup === cbPrefix);
    rows.push({
      id: p.id, key: p.key, art: p.art, name: p.name, cat: p.cat,
      onSite, buy, our, stock: mine.stock, qty: mine.qty, cur: mine.cur,
      buyChange, buyPct, margin, marginPct, marginRisk, isSource,
      siteArticle, siteSupplier, siteSupPrefix, mdm: !!p.mdm, ref: !!p.ref, manualBound, nameDismissed,
      status: onSite ? "site" : "missing", others, supCount: 1 + others.length,
      offerIds: p.offerIds,
    });
  }
  return rows;
}
function auditSummary(rows) {
  return {
    positions: rows.length,
    onSite: rows.filter(r => r.onSite).length,
    missing: rows.filter(r => !r.onSite).length,
    up: rows.filter(r => r.buyChange === "up").length,
    risk: rows.filter(r => r.marginRisk).length,
  };
}
// З оферів Hotline → видимі позиції лесенки:
//  • ми в топ-3      → топ-3 + ще 5 (ранги 1..8)
//  • ми нижче топ-3  → топ-3 + 5 магазинів перед нами і 5 після
//  • нас немає у Hotline (не знайдено / не в наявності) → перші 10
//  між видимими блоками згортаємо «··· ще N ···».
function ladderView(offers) {
  if (!offers || !offers.length) return null;
  const total = offers.length;
  const ourRank = (offers.find(o => o.us) || {}).rank || null;
  const vis = new Set();
  if (!ourRank) {
    for (let r = 1; r <= Math.min(10, total); r++) vis.add(r);
  } else if (ourRank <= 3) {
    for (let r = 1; r <= Math.min(8, total); r++) vis.add(r);
  } else {
    [1, 2, 3].forEach(r => vis.add(r));
    for (let r = ourRank - 5; r <= ourRank + 5; r++) if (r >= 1 && r <= total) vis.add(r);
  }
  const ranks = [...vis].filter(r => r >= 1 && r <= total).sort((a, b) => a - b);
  const out = []; let prev = 0;
  for (const r of ranks) { const o = offers[r - 1]; out.push({ ...o, hiddenBefore: Math.max(0, r - prev - 1) }); prev = r; }
  return { rows: out, total, ourRank, minComp: offers[0].price };
}
const audPlural = n => { const a = n % 10, b = n % 100; if (a === 1 && b !== 11) return "оффер"; if (a >= 2 && a <= 4 && (b < 12 || b > 14)) return "оффери"; return "офферів"; };

// ── Лесенка Hotline ───────────────────────────────────────────────────────────
function AudHotlineLadder({ row, hot, onReload }) {
  if (!hot || hot.loading) {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 6, padding: "6px 4px" }}>
        {[0, 1, 2].map(i => <span key={i} className="skeleton" style={{ height: 28, borderRadius: 7 }} />)}
        <span style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 4 }}>Тягнемо Hotline…</span>
      </div>
    );
  }
  if (hot.error || !hot.found) {
    return (
      <div style={{ display: "flex", gap: 10, padding: "12px 14px", background: "rgba(245,158,11,.10)", border: "1px solid rgba(245,158,11,.28)", borderRadius: 10 }}>
        <Icon name="cloud-off" size={16} color="var(--warning)" style={{ flexShrink: 0, marginTop: 1 }} />
        <div>
          <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>{hot.error ? "Hotline недоступний" : "Товар не знайдено на Hotline"}</div>
          <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginTop: 2, lineHeight: 1.4 }}>Спробуйте перевірити позицію ще раз.</div>
          <button onClick={onReload} style={{ marginTop: 8, display: "inline-flex", alignItems: "center", gap: 5, height: 28, padding: "0 11px", borderRadius: 7, background: "var(--bg-raised)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12, fontFamily: "inherit", cursor: "pointer" }}>
            <Icon name="refresh-cw" size={12} /> Повторити
          </button>
        </div>
      </div>
    );
  }
  const lv = ladderView(hot.offers);
  if (!lv) return <div style={{ fontSize: 12, color: "var(--fg-muted)", padding: "10px 4px" }}>Немає даних Hotline.</div>;
  return (
    <div style={{ background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: "6px 4px", fontFamily: "var(--font-mono)" }}>
      {lv.rows.map((o, i) => (
        <React.Fragment key={i}>
          {o.hiddenBefore > 0 && (
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 12px 5px 46px", color: "var(--fg-disabled)", fontSize: 11, fontFamily: "var(--font-ui)" }}>
              <span style={{ flex: 1, height: 1, background: "var(--border-subtle)" }} />
              ··· ще {o.hiddenBefore} {audPlural(o.hiddenBefore)} ···
              <span style={{ flex: 1, height: 1, background: "var(--border-subtle)" }} />
            </div>
          )}
          <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderRadius: 7, background: o.us ? "var(--accent-soft)" : "transparent" }}>
            <span style={{ width: 24, fontSize: 11.5, color: "var(--fg-muted)", textAlign: "right" }}>{o.rank}.</span>
            <span style={{ flex: 1, fontFamily: "var(--font-ui)", fontSize: 12.5, fontWeight: o.us ? 600 : 400, color: o.us ? "var(--accent)" : "var(--fg-secondary)" }}>
              {o.store || "—"}
              {o.rank === 1 && !o.us && <span style={{ marginLeft: 7 }}><BestBadgeP size="sm" /></span>}
            </span>
            {o.us && <span style={{ fontFamily: "var(--font-ui)", fontSize: 11.5, fontWeight: 600, color: "var(--accent)" }}>← ми</span>}
            <span style={{ fontSize: 12.5, fontWeight: o.us ? 700 : 500, color: o.us ? "var(--accent)" : "var(--fg-primary)", whiteSpace: "nowrap" }}>{fmtUAHP(o.price)}</span>
          </div>
        </React.Fragment>
      ))}
      {row.onSite && row.margin != null && lv.ourRank && (
        <div style={{ display: "flex", alignItems: "center", gap: 10, margin: "6px 8px 4px", padding: "9px 10px 0", borderTop: "1px solid var(--border-subtle)", fontFamily: "var(--font-ui)" }}>
          <span style={{ fontSize: 12, color: "var(--fg-secondary)" }}>Зараз поз. <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{lv.ourRank}/{lv.total}</b></span>
          <span style={{ width: 4, height: 4, borderRadius: "50%", background: "var(--fg-disabled)" }} />
          <span style={{ fontSize: 12, color: "var(--fg-secondary)" }}>маржа <b style={{ fontFamily: "var(--font-mono)", color: marginColorP(row.margin, row.marginPct) }}>{fmtSignedP(row.margin)}</b></span>
        </div>
      )}
    </div>
  );
}

// ── Інші постачальники ──────────────────────────────────────────────────────────
function AudOtherSuppliers({ row, cbPrefix, sourceSup, narrow, onMakeSource, disabled }) {
  const all = [{ sup: cbPrefix, price: row.buy, cur: "UAH", stock: row.stock, qty: row.qty, uah: row.buy }, ...row.others].sort((a, b) => a.uah - b.uah);
  const cheapest = all[0].uah;
  return (
    <div style={{ background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: "4px 0" }}>
      {all.map((o, i) => {
        const isSource = !!sourceSup && o.sup === sourceSup, isCheapest = o.uah === cheapest;
        return (
          <div key={o.sup} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 13px", borderTop: i ? "1px solid var(--border-subtle)" : "none", flexWrap: narrow ? "wrap" : "nowrap" }}>
            {/* Ліва група: чип постачальника + бейджі — стискається й обрізається, не штовхає ціну/кнопку */}
            <div style={{ display: "flex", alignItems: "center", gap: 8, flex: narrow ? "1 1 100%" : "1 1 auto", minWidth: 0, order: 0 }}>
              <SupplierChipP sup={o.sup} size="md" star={isSource} truncate label={o.supplierName} />
              {isCheapest && <span style={{ flexShrink: 0 }}><BestBadgeP size="sm" /></span>}
              {o.cur !== "UAH" && <span style={{ flexShrink: 0, fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)" }}>≈ {fmtUAHP(o.uah)}</span>}
            </div>
            <StockDotP stock={o.stock} qty={o.qty} />
            <span style={{ flexShrink: 0, minWidth: 80, textAlign: "right", fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 600, color: isCheapest ? "var(--success)" : "var(--fg-primary)", whiteSpace: "nowrap" }}>{fmtCurP(o.price, o.cur)}</span>
            <div style={{ flexShrink: 0, minWidth: narrow ? 0 : 130, display: "flex", justifyContent: "flex-end" }}>
              {isSource
                ? <span style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, color: "var(--accent)", fontWeight: 600, whiteSpace: "nowrap" }}><Icon name="check" size={13} /> джерело</span>
                : disabled
                  ? <span title="MDM — заблоковано" style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: "var(--fg-disabled)", whiteSpace: "nowrap" }}><Icon name="lock" size={12} /> MDM</span>
                  : <button onClick={() => onMakeSource(o.sup)} style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 28, padding: "0 10px", borderRadius: 7, background: "var(--bg-base)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12, fontFamily: "inherit", cursor: "pointer", fontWeight: 500, whiteSpace: "nowrap" }}>
                    <Icon name="git-branch-plus" size={12} /> Зробити джерелом
                  </button>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── Швидкі дії (інлайн-редагування ціни + наявності) ─────────────────────────────
// Статуси наявності з Horoshop. ВАЖЛИВО: рядок `presence` має точно збігатися з
// назвою статусу в адмінці — інакше імпорт не змапить наявність. (`id` — на майбутнє.)
const AUD_PRES = [
  { key: "none0", id: 0,  kind: "none",  label: "Статус не вибраний",       presence: "Статус не вибраний" },
  { key: "in",    id: 1,  kind: "in",    label: "В наявності",              presence: "В наявності" },
  { key: "out",   id: 2,  kind: "out",   label: "Немає в наявності",        presence: "Немає в наявності" },
  { key: "wait",  id: 3,  kind: "wait",  label: "Очікується",               presence: "Очікується" },
  { key: "o23",   id: 12, kind: "order", label: "Під замовлення 2-3 дні",   presence: "Під замовлення 2-3 дні" },
  { key: "o37",   id: 10, kind: "order", label: "Під замовлення 3-7 днів",  presence: "Під замовлення 3-7 днів" },
  { key: "o714",  id: 11, kind: "order", label: "Під замовлення 7-14 днів", presence: "Під замовлення 7-14 днів" },
];
const AUD_PRES_BY_KEY = Object.fromEntries(AUD_PRES.map(p => [p.key, p]));
// Назва статусу з сайту → ключ AUD_PRES (для ініціалізації селектора поточним статусом).
function presKeyFromNameP(name) {
  const s = String(name || "").toLowerCase().trim();
  const exact = AUD_PRES.find(p => p.presence.toLowerCase() === s);
  if (exact) return exact.key;
  if (!s) return "in";
  if (s.includes("нема") || s.includes("нет") || s.includes("відсут") || s.includes("отсут")) return "out";
  if (s.includes("очік") || s.includes("ожид")) return "wait";
  if (s.includes("2-3")) return "o23";
  if (s.includes("3-7")) return "o37";
  if (s.includes("7-14")) return "o714";
  if (s.includes("замов") || s.includes("заказ")) return "o23";
  return "in";
}
// Закупка ОБРАНОГО джерела: ціна постачальника sourceSup серед оферів (базовий cbPrefix=row.buy + row.others).
// Різні склади з однаковою назвою мають РІЗНІ ключі sup, тож find однозначний; фолбек — row.buy.
function srcBuyUahP(row, cbPrefix, sourceSup) {
  if (!sourceSup) return row.buy;
  const offers = [{ sup: cbPrefix, uah: row.buy }, ...(row.others || [])];
  const o = offers.find(x => x.sup === sourceSup);
  return (o && Number(o.uah) > 0) ? Number(o.uah) : row.buy;
}
function AudQuickActions({ row, sourceSup, buyUse, onToast, onSaved }) {
  // Закупка для маржі — ОБРАНОГО джерела (sourceSup), а не базового row.buy.
  // Інакше при перемиканні джерела маржа лишалась би рахуватись від найдешевшого.
  const buy = (Number(buyUse) > 0) ? Number(buyUse) : row.buy;
  const [priceVal, setPriceVal] = useState(String(row.our ?? ""));
  const [avail, setAvail] = useState(row.stock ? "in" : "o23");
  const [savedAvail, setSavedAvail] = useState(null); // наявність, яку щойно зберегли (база для «змінено»)
  const [busy, setBusy] = useState(false);
  useEffect(() => { setPriceVal(String(row.our ?? "")); setAvail(row.stock ? "in" : "o23"); setSavedAvail(null); }, [row.id]);
  const num = parseInt(String(priceVal).replace(/\D/g, ""), 10) || 0;
  const margin = num - buy, marginPct = num > 0 ? (margin / num) * 100 : 0;
  const belowBuy = num > 0 && num < buy, changed = num !== row.our;
  const baseSup = row.siteSupPrefix || null;
  const supChanged = !!sourceSup && sourceSup !== baseSup;   // обрали іншого постачальника-джерело
  const baseAvail = savedAvail ?? (row.stock ? "in" : "o23"); // база: щойно збережене АБО вихідне зі stock
  const availChanged = avail !== baseAvail;                  // змінили лише наявність — теж дозволяємо зберегти
  const dirty = changed || supChanged || availChanged;
  const save = async () => {
    if (busy || num <= 0 || belowBuy || !dirty) return; setBusy(true);
    try {
      // Оновлюємо за РЕАЛЬНИМ артикулом на сайті (row.siteArticle), якщо знайдено по ньому —
      // інакше комбінований рядок «PB634-A-CIS, PB634-A-WW» не змапиться в Хорошопі.
      const payload = { article: row.siteArticle || row.art, price: num, presence: (AUD_PRES_BY_KEY[avail] || AUD_PRES_BY_KEY.in).presence };
      if (supChanged) payload.sourceSup = sourceSup;
      const r = await fetch("/api/horoshop/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ products: [payload] }) });
      const j = await r.json().catch(() => ({}));
      if (j && j.ok) { onSaved(row.id, num, row.siteArticle || row.art); setSavedAvail(avail); onToast && onToast(`Збережено на сайті · ${row.art}`); }
      else onToast && onToast("Помилка: " + ((j && (j.error || j.status)) || "не вдалося"));
    } catch { onToast && onToast("Помилка мережі"); } finally { setBusy(false); }
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      <div>
        <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginBottom: 6, display: "flex", alignItems: "center", gap: 5 }}>
          <Icon name="globe" size={12} color="var(--fg-muted)" /> Наша ціна на сайті (Horoshop)
        </div>
        <div style={{ display: "flex", gap: 8 }}>
          <div style={{ position: "relative", flex: 1 }}>
            <input value={priceVal} onChange={e => setPriceVal(e.target.value.replace(/[^\d ]/g, ""))} inputMode="numeric" style={{
              width: "100%", boxSizing: "border-box", height: 40, padding: "0 30px 0 13px", background: "var(--bg-base)", color: "var(--fg-primary)",
              border: `1px solid ${belowBuy ? "var(--danger)" : "var(--border-default)"}`, borderRadius: 8, fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)", outline: "none" }} />
            <span style={{ position: "absolute", right: 13, top: "50%", transform: "translateY(-50%)", fontSize: 14, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>₴</span>
          </div>
          <Button variant="primary" leftIcon={busy ? "loader" : "check"} disabled={num <= 0 || belowBuy || !dirty || busy} onClick={save}>Зберегти</Button>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 8, fontSize: 11.5 }}>
          <span style={{ color: "var(--fg-muted)" }}>закуп {fmtUAHP(buy)}</span>
          <span style={{ width: 3, height: 3, borderRadius: "50%", background: "var(--fg-disabled)" }} />
          <span style={{ color: "var(--fg-muted)" }}>маржа</span>
          <span style={{ fontFamily: "var(--font-mono)", fontWeight: 700, color: marginColorP(margin, marginPct) }}>{num > 0 ? `${fmtSignedP(margin)} · ${fmtPctP(marginPct)}` : "—"}</span>
        </div>
        {belowBuy && (
          <div style={{ display: "flex", gap: 8, marginTop: 8, padding: "8px 11px", background: "rgba(244,63,94,.12)", border: "1px solid rgba(244,63,94,.3)", borderRadius: 8, fontSize: 11.5, color: "var(--fg-secondary)" }}>
            <Icon name="ban" size={14} color="var(--danger)" style={{ flexShrink: 0, marginTop: 1 }} /> Нижче закупу — збереження заблоковано.
          </div>
        )}
      </div>
      <div>
        <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginBottom: 6 }}>Наявність на сайті</div>
        {/* Один select на всі статуси Horoshop — нічого не вилазить, текст не обрізається */}
        <div style={{ position: "relative" }}>
          <select value={avail} onChange={e => setAvail(e.target.value)} style={{
            width: "100%", boxSizing: "border-box", height: 38, padding: "0 30px 0 11px", appearance: "none",
            background: "var(--bg-base)", color: "var(--fg-primary)", border: "1px solid var(--border-default)",
            borderRadius: 8, fontSize: 12.5, fontFamily: "inherit", fontWeight: 600, cursor: "pointer", outline: "none" }}>
            {AUD_PRES.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
          </select>
          <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
        </div>
      </div>
    </div>
  );
}

// ── Картка «немає на сайті» + дії AI ─────────────────────────────────────────────
function AudNewProductCard({ row, supColor, supLabel }) {
  const rec = Math.round(row.buy / (1 - TARGET_MARGIN / 100) / 10) * 10;
  return (
    <div style={{ background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: 14 }}>
      <div style={{ display: "flex", gap: 11 }}>
        <div style={{ width: 40, height: 40, borderRadius: 9, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
          <Icon name="wand-sparkles" size={19} color="var(--accent)" />
        </div>
        <div style={{ fontSize: 12.5, color: "var(--fg-secondary)", lineHeight: 1.45 }}>
          AI створить картку для Horoshop: назва, характеристики, опис і фото. Ціну порахуємо від закупівлі <b style={{ color: supColor }}>{supLabel}</b> + цільова маржа {TARGET_MARGIN}%.
        </div>
      </div>
      <div style={{ display: "flex", marginTop: 13, background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 9, overflow: "hidden" }}>
        {[["Закуп", fmtUAHP(row.buy), null], ["Реком. ціна", fmtUAHP(rec), "var(--accent)"]].map(([l, v, c], i) => (
          <React.Fragment key={l}>
            {i > 0 && <div style={{ width: 1, background: "var(--border-subtle)" }} />}
            <div style={{ flex: 1, padding: "9px 12px" }}>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 700, color: c || "var(--fg-primary)" }}>{v}</div>
              <div style={{ fontSize: 10, color: "var(--fg-muted)", letterSpacing: ".03em", textTransform: "uppercase", marginTop: 2 }}>{l}</div>
            </div>
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

// ── Підказка зіставлення по назві (товар без артикула) ────────────────────────────
// Шукає схожі товари в каталозі сайту й пропонує ПІДТВЕРДИТИ прив'язку (нічого не
// пишеться автоматично). «Не той» гортає до наступного кандидата, далі — ховає блок.
function AudNameMatch({ row, siteCatalog, onBind }) {
  const cands = React.useMemo(() => audSiteCandidates(row.name, siteCatalog, 3, row.art), [row.name, row.art, siteCatalog]);
  const [idx, setIdx] = useState(0);
  // row.nameDismissed — менеджер раніше натиснув «Не той» (збережено на сервері) → не нагадуємо.
  if (row.nameDismissed || !cands.length || idx >= cands.length) return null;
  const c = cands[idx];
  const pct = Math.round(c.sim * 100);
  return (
    <div style={{ background: "var(--bg-raised)", border: "1px solid var(--accent)", borderRadius: 10, padding: 13 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--accent)", marginBottom: 9 }}>
        <Icon name="search" size={13} /> Можливо, це є на сайті {cands.length > 1 ? `· ${idx + 1}/${cands.length}` : ""}
      </div>
      <div style={{ fontSize: 13, color: "var(--fg-primary)", fontWeight: 500, lineHeight: 1.35 }}>{c.name}</div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 5, fontSize: 11.5, color: "var(--fg-muted)", flexWrap: "wrap" }}>
        {c.article && <span style={{ fontFamily: "var(--font-mono)" }}>арт. {c.article}</span>}
        {c.price != null && <span style={{ fontFamily: "var(--font-mono)" }}>{fmtUAHP(c.price)}</span>}
        <span style={{ color: pct >= 80 ? "var(--success)" : "var(--warning)", fontWeight: 600 }}>схожість {pct}%</span>
      </div>
      <div style={{ display: "flex", gap: 8, marginTop: 11 }}>
        <Button variant="primary" size="sm" leftIcon="link" style={{ flex: 1 }} onClick={() => onBind && onBind(row, c)}>Підтвердити прив'язку</Button>
        <button onClick={() => { if (idx + 1 < cands.length) setIdx(idx + 1); else onBind && onBind(row, null, "dismiss"); }} style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 32, padding: "0 12px", borderRadius: 8, background: "var(--bg-base)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12.5, fontFamily: "inherit", cursor: "pointer", fontWeight: 500 }}>
          <Icon name={idx + 1 < cands.length ? "arrow-right" : "x"} size={13} /> Не той
        </button>
      </div>
    </div>
  );
}

// ── Розкриття рядка ──────────────────────────────────────────────────────────────
function AudRowDetail({ row, cbPrefix, narrow, hot, aiStatus, warranty, hotLink, onLoadHot, onEditLink, onToast, onSaved, onAI, siteCatalog, onBind }) {
  // Джерело = реальний постачальник із сайту (якщо його вдалося розпізнати).
  // НЕ підставляємо cbPrefix за замовчуванням — інакше бейдж «джерело» хибно
  // світиться на постачальнику, якого ми просто переглядаємо.
  const [sourceSup, setSourceSup] = useState(row.siteSupPrefix || null);
  useEffect(() => { setSourceSup(row.siteSupPrefix || null); if (!hot) onLoadHot(row); }, [row.id]);
  const s = PRICE_SUPS[cbPrefix] || {};
  const onSite = row.onSite;
  const DetailLabel = ({ icon, children, right }) => (
    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 9 }}>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 11, fontWeight: 600, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--fg-muted)" }}><Icon name={icon} size={13} /> {children}</span>
      {right}
    </div>
  );
  // Поставити одиночну картку В ЧЕРГУ генерації (фон, паралельно) — як у walker'і, але з таблиці аудиту.
  // Сервер ІГНОРУЄ buy/джерело/hot при генерації, але ми кладемо їх у params як контекст:
  // при відкритті готової з лотка «Черга» вони повертаються в майстер → лесенка Hotline,
  // джерело й поле «ціна продажу» доступні одразу на «Перевірці» (як при єдиничній генерації).
  const enqueueCard = () => {
    const sup = sourceSup || row.siteSupPrefix || cbPrefix || "";
    const supLabel = (PRICE_SUPS[sup] || {}).label || sup;
    const offers = [{ sup: cbPrefix, uah: row.buy, price: row.buy, cur: "UAH" }, ...(row.others || [])];
    const off = offers.find(o => o.sup === sup) || offers.find(o => o.sup === cbPrefix);
    const aiModels = (() => { try { return JSON.parse(localStorage.getItem("aiWizardModels") || "null"); } catch { return null; } })();
    const params = {
      article: artForCardP(row).toUpperCase(), name: row.name || "", createdBy: localStorage.getItem("crm_user_name") || "",
      supplierPrefix: sup,
      buy: off ? { uah: off.uah, price: off.price, cur: off.cur, supLabel } : null,
      hot: hot && hot.found ? hot : null,
    };
    if (warranty) params.warranty = warranty;   // тимчасове перевизначення з аудиту (інакше сервер підставить основну постачальника)
    if (aiModels) {
      if (aiModels.text)   { params.provider = aiModels.text.provider; params.model = aiModels.text.model; }
      if (aiModels.vision) { params.visionProvider = aiModels.vision.provider; params.visionModel = aiModels.vision.model; }
      if (aiModels.facts)  { params.factsProvider = aiModels.facts.provider; params.factsModel = aiModels.facts.model; }
      if (aiModels.qa)     { params.qaProvider = aiModels.qa.provider; params.qaModel = aiModels.qa.model; }
    }
    fetch("/api/ai-queue/enqueue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) })
      .then(r => r.json())
      .then(j => { if (j && j.ok) { autoBindArtP(row, params.article); onToast && onToast(`➕ У черзі генерації: ${(row.name || row.art || "").slice(0, 40)} — стеж за лотком «Черга» внизу праворуч`); } else onToast && onToast("❌ Черга: " + ((j && j.error) || "не вдалось")); })
      .catch(e => onToast && onToast("❌ Черга: " + e.message));
  };
  return (
    <div style={{ background: "var(--bg-panel)", borderBottom: "1px solid var(--border-default)", boxShadow: "inset 3px 0 0 var(--accent)", animation: "expandRow 180ms ease" }}>
      <div style={{ display: "grid", gridTemplateColumns: narrow ? "1fr" : "minmax(0,1.15fr) minmax(0,1fr) minmax(0,1fr)", gap: 20, padding: "18px 24px 22px" }}>
        {/* 1 — Лесенка Hotline (для ВСІХ позицій, навіть «немає на сайті») */}
        <div>
          <DetailLabel icon="bar-chart-3" right={
            <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
              {hotLink && <a href={hotLink} target="_blank" rel="noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11.5, color: "var(--accent)", textDecoration: "none" }}><Icon name="external-link" size={12} /> Hotline</a>}
              {/* Закріплене посилання на картку Hotline: наступні перевірки йдуть ОДРАЗУ за ним.
                  Привʼязало не той товар / знайшло не те → сюди вставити правильний URL. */}
              <button onClick={() => onEditLink(row)}
                title={hotLink ? "Змінити збережене посилання на картку Hotline (порожнє — скинути й шукати за назвою)" : "Привʼязати посилання на картку Hotline вручну — далі перевірки йтимуть одразу за ним"}
                style={{ display: "inline-flex", alignItems: "center", gap: 4, height: 22, padding: "0 8px", borderRadius: 6, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", fontSize: 11, fontFamily: "inherit", cursor: "pointer", whiteSpace: "nowrap" }}>
                <Icon name="pencil" size={11} /> {hotLink ? "посилання" : "привʼязати посилання"}
              </button>
            </span>
          }>Лесенка Hotline</DetailLabel>
          <AudHotlineLadder row={row} hot={hot} onReload={() => onLoadHot(row, true)} />
        </div>
        {/* 2 — Інші постачальники */}
        <div>
          <DetailLabel icon="truck" right={<span style={{ fontSize: 11, color: "var(--fg-muted)" }}>{row.supCount} {row.supCount === 1 ? "джерело" : "джерел"}</span>}>Інші постачальники</DetailLabel>
          {row.siteSupplier && (
            <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 11.5, color: "var(--fg-secondary)", flexWrap: "wrap", minWidth: 0 }}>
              <Icon name="store" size={12} color="var(--accent)" style={{ flexShrink: 0 }} /> <span style={{ flexShrink: 0 }}>На сайті зараз:</span>
              {row.siteSupPrefix ? <SupplierChipP sup={row.siteSupPrefix} size="sm" truncate /> : <b style={{ color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}>{row.siteSupplier}</b>}
            </div>
          )}
          <AudOtherSuppliers row={row} cbPrefix={cbPrefix} sourceSup={sourceSup} narrow={narrow} onMakeSource={setSourceSup} disabled={row.mdm} />
          {sourceSup && sourceSup !== cbPrefix && (
            <div style={{ display: "flex", gap: 8, marginTop: 8, padding: "8px 11px", background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 8, fontSize: 11.5, color: "var(--fg-secondary)" }}>
              <Icon name="info" size={13} color="var(--accent)" style={{ flexShrink: 0, marginTop: 1 }} />
              Джерелом призначено <b style={{ color: (PRICE_SUPS[sourceSup] || {}).color }}>{(PRICE_SUPS[sourceSup] || {}).label}</b> — маржа рахується від його закупівлі.
            </div>
          )}
        </div>
        {/* 3 — Дії */}
        <div>
          <DetailLabel icon={row.mdm ? "lock" : onSite ? "zap" : "sparkles"}>{row.mdm ? "MDM — заблоковано" : onSite ? "Швидкі дії" : "Немає на сайті"}</DetailLabel>
          {row.mdm
            ? (
              <div style={{ display: "flex", gap: 10, padding: "12px 14px", background: "rgba(244,63,94,.12)", border: "1px solid rgba(244,63,94,.32)", borderRadius: 10 }}>
                <Icon name="lock" size={16} color="var(--danger)" style={{ flexShrink: 0, marginTop: 1 }} />
                <div style={{ fontSize: 12, color: "var(--fg-secondary)", lineHeight: 1.45 }}>
                  Заблокований пристрій (MDM) — чорний список: не додаємо на сайт, не публікуємо ціну, не ставимо джерелом.
                </div>
              </div>
            )
            : onSite
              ? (
                <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                  {row.manualBound && (
                    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 11px", background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 8, fontSize: 11.5, color: "var(--fg-secondary)" }}>
                      <Icon name="link" size={13} color="var(--accent)" style={{ flexShrink: 0 }} />
                      <span style={{ flex: 1 }}>Прив'язано до сайту вручну (по назві).</span>
                      <button onClick={() => onBind && onBind(row, null)} style={{ border: 0, background: "transparent", color: "var(--danger)", fontSize: 11.5, fontFamily: "inherit", cursor: "pointer", fontWeight: 600, whiteSpace: "nowrap" }}>Відв'язати</button>
                    </div>
                  )}
                  <AudQuickActions row={row} sourceSup={sourceSup} buyUse={srcBuyUahP(row, cbPrefix, sourceSup)} onToast={onToast} onSaved={onSaved} />
                </div>
              )
              : (
                <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
                  {/* Підказка: можливо, товар уже є на сайті (зіставлення по назві) — підтвердь прив'язку */}
                  <AudNameMatch row={row} siteCatalog={siteCatalog} onBind={onBind} />
                  {row.ref && (
                    <div style={{ display: "flex", gap: 8, padding: "9px 12px", background: "rgba(245,158,11,.10)", border: "1px solid rgba(245,158,11,.28)", borderRadius: 9, fontSize: 11.5, color: "var(--fg-secondary)", lineHeight: 1.4 }}>
                      <Icon name="alert-triangle" size={14} color="var(--warning)" style={{ flexShrink: 0, marginTop: 1 }} />
                      REF — відновлений товар: джерело лише з ручним підтвердженням.
                    </div>
                  )}
                  <AudNewProductCard row={row} supColor={s.color} supLabel={s.label} />
                  {aiStatus === "published" ? (
                    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "11px 13px", background: "rgba(16,185,129,.10)", border: "1px solid rgba(16,185,129,.30)", borderRadius: 9, fontSize: 12, color: "var(--fg-secondary)", lineHeight: 1.4 }}>
                      <Icon name="globe" size={15} color="#10B981" style={{ flexShrink: 0 }} /> Товар <b>додано на сайт</b>. Зʼявиться в каталозі — тоді можна редагувати ціну й наявність прямо тут.
                    </div>
                  ) : aiStatus === "done" ? (
                    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "11px 13px", background: "rgba(16,185,129,.10)", border: "1px solid rgba(16,185,129,.30)", borderRadius: 9, fontSize: 12, color: "var(--fg-secondary)", lineHeight: 1.4 }}>
                      <Icon name="check-circle" size={15} color="#10B981" style={{ flexShrink: 0 }} /> Картку <b>згенеровано</b> — відкрий її в лотку «Черга» внизу праворуч і опублікуй на сайт.
                    </div>
                  ) : (aiStatus === "running" || aiStatus === "queued") ? (
                    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "11px 13px", background: "rgba(59,130,246,.10)", border: "1px solid rgba(59,130,246,.30)", borderRadius: 9, fontSize: 12, color: "var(--fg-secondary)", lineHeight: 1.4 }}>
                      <Icon name={aiStatus === "running" ? "loader" : "clock"} size={15} color="var(--accent)" style={{ flexShrink: 0, ...(aiStatus === "running" ? { animation: "spin 1s linear infinite" } : {}) }} />
                      {aiStatus === "running" ? "Генерується…" : "У черзі на генерацію…"} Цей товар уже взяли — не дублюйте.
                    </div>
                  ) : (
                    <>
                      <Button variant="primary" leftIcon="list-plus" size="lg" style={{ width: "100%" }} onClick={enqueueCard}>Додати в чергу генерації</Button>
                      <div style={{ display: "flex", alignItems: "flex-start", gap: 7, fontSize: 11, color: "var(--fg-muted)", lineHeight: 1.4, padding: "0 2px" }}>
                        <Icon name="info" size={13} style={{ flexShrink: 0, marginTop: 1 }} />
                        AI збере картку у фоні — стеж за лотком «Черга» внизу праворуч, готову відкриєш на перевірку. Категорію підбере сам (видно на кроці «Перевірка»).
                      </div>
                      <div style={{ display: "flex", gap: 8 }}>
                        <button onClick={() => onAI(row, sourceSup)} style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, height: 38, borderRadius: 8, background: "var(--bg-raised)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12.5, fontFamily: "inherit", cursor: "pointer", fontWeight: 500 }}>
                          <Icon name="sparkles" size={14} /> Згенерувати зараз
                        </button>
                        <button onClick={() => onAI(row, sourceSup, true)} style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, height: 38, borderRadius: 8, background: "var(--bg-raised)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12.5, fontFamily: "inherit", cursor: "pointer", fontWeight: 500 }}>
                          <Icon name="pencil-line" size={14} /> Вручну
                        </button>
                      </div>
                    </>
                  )}
                </div>
              )}
        </div>
      </div>
    </div>
  );
}

// ── Комірка Hotline у рядку (мін. ринок) ─────────────────────────────────────────
function AudHotCell({ hot }) {
  if (!hot) return <span style={{ fontSize: 11, color: "var(--fg-disabled)" }}>розкрити →</span>;
  if (hot.loading) return <span className="skeleton" style={{ display: "inline-block", width: 56, height: 13 }} />;
  if (hot.error || !hot.found) return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 19, padding: "0 8px", borderRadius: 999, background: "rgba(245,158,11,.13)", color: "var(--warning)", fontSize: 10.5, fontWeight: 600 }}><Icon name="cloud-off" size={11} /> н/д</span>
  );
  const min = hot.offers && hot.offers.length ? hot.offers[0].price : null;
  if (min == null) return <span style={{ color: "var(--fg-disabled)" }}>—</span>;
  return <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, color: "var(--fg-secondary)", whiteSpace: "nowrap" }}>{fmtUAHP(min)}</span>;
}

// ── Рядок таблиці аудиту ─────────────────────────────────────────────────────────
const AUD_COLS = [["exp", 24], ["name", "flex"], ["buy", 116], ["our", 104], ["hot", 120], ["stock", 84], ["status", 150]];
const audCw = w => (w === "flex" ? { flex: "1 1 0", minWidth: 180 } : { flex: `0 0 ${w}px` });
// Бейдж стану товару, якого ще немає на сайті: Новий → У черзі → Генерується → Згенеровано → Додано на сайт.
// Оновлюється в реальному часі у всіх (спільна черга), щоб менеджери не задвоювали генерацію.
const AUD_AI_BADGE = {
  queued:    { t: "У черзі",        c: "#F59E0B", bg: "rgba(245,158,11,.14)",  ic: "clock" },
  running:   { t: "Генерується",    c: "var(--accent)", bg: "rgba(59,130,246,.14)", ic: "loader", spin: true },
  done:      { t: "Згенеровано",    c: "#10B981", bg: "rgba(16,185,129,.14)", ic: "check" },
  published: { t: "Додано на сайт", c: "#10B981", bg: "rgba(16,185,129,.14)", ic: "globe" },
};
function AudAiBadge({ status, isNew }) {
  const m = (status && AUD_AI_BADGE[status]) || (isNew ? { t: "Новий", c: "#3B82F6", bg: "rgba(59,130,246,.13)", ic: "" } : null);
  if (!m) return null;
  return (
    <span style={{ flexShrink: 0, display: "inline-flex", alignItems: "center", gap: 3, fontSize: 9.5, fontWeight: 700, color: m.c, background: m.bg, borderRadius: 4, padding: "1px 5px", whiteSpace: "nowrap" }}>
      {m.ic && <Icon name={m.ic} size={10} style={m.spin ? { animation: "spin 1s linear infinite" } : undefined} />}{m.t}
    </span>
  );
}

function AudRow({ row, cbPrefix, expanded, narrow, hot, aiStatus, warranty, onToggle, onLoadHot, onEditLink, onToast, onSaved, onAI, siteCatalog, onBind }) {
  const [hover, setHover] = useState(false);
  const missing = !row.onSite;
  const leftBar = missing ? "var(--warning)" : (expanded ? "var(--accent)" : null);
  if (narrow) {
    return (
      <div style={{ borderBottom: "1px solid var(--border-subtle)", boxShadow: leftBar ? `inset 3px 0 0 ${leftBar}` : "none" }}>
        <div onClick={() => onToggle(row.id)} style={{ display: "flex", gap: 11, padding: "12px 16px", cursor: "pointer", background: expanded ? "var(--bg-active)" : "transparent" }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <span style={{ flex: 1, minWidth: 0, fontSize: 13, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{row.name}</span>
              {(row.mdm || row.ref) && <FlagBadgeP mdm={row.mdm} refurb={row.ref} size="sm"/>}
              {!row.onSite ? <AudAiBadge status={aiStatus} isNew={true} /> : <AudStatusBadge status={row.status} size="sm" />}
            </div>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)", marginTop: 2 }}>{row.art} · {row.cat}</div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: "4px 16px", marginTop: 9 }}>
              {[["Закупівля", fmtUAHP(row.buy)], ["Наша ціна", row.our != null ? fmtUAHP(row.our) : "—"], ["Hotline", hot && hot.found && hot.offers[0] ? fmtUAHP(hot.offers[0].price) : (hot && hot.loading ? "…" : "—")], ["Наявн.", row.stock ? `${row.qty ?? ""} шт` : "немає"]].map(([l, v]) => (
                <div key={l} style={{ minWidth: 72 }}>
                  <div style={{ fontSize: 10, color: "var(--fg-muted)", textTransform: "uppercase" }}>{l}</div>
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)", marginTop: 1 }}>{v}</div>
                </div>
              ))}
            </div>
          </div>
          <Icon name="chevron-right" size={16} color="var(--fg-muted)" style={{ transform: expanded ? "rotate(90deg)" : "none", transition: "transform 150ms", marginTop: 2 }} />
        </div>
        {expanded && <AudRowDetail row={row} cbPrefix={cbPrefix} narrow={true} hot={hot} aiStatus={aiStatus} warranty={warranty} hotLink={hot && hot.url} onLoadHot={onLoadHot} onEditLink={onEditLink} onToast={onToast} onSaved={onSaved} onAI={onAI} siteCatalog={siteCatalog} onBind={onBind} />}
      </div>
    );
  }
  return (
    <>
      <div onClick={() => onToggle(row.id)} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
        display: "flex", gap: 12, alignItems: "center", padding: "10px 24px", cursor: "pointer",
        borderBottom: "1px solid " + (expanded ? "var(--border-default)" : "var(--border-subtle)"),
        background: expanded ? "var(--bg-active)" : hover ? "var(--bg-hover)" : "transparent",
        boxShadow: leftBar ? `inset 3px 0 0 ${leftBar}` : "none" }}>
        <span style={{ ...audCw(24), display: "flex", justifyContent: "center" }}>
          <Icon name="chevron-right" size={15} color="var(--fg-muted)" style={{ transform: expanded ? "rotate(90deg)" : "none", transition: "transform 150ms" }} />
        </span>
        <span style={{ ...audCw("flex"), minWidth: 0 }}>
          <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ flex: "0 1 auto", minWidth: 0, fontSize: 13, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={row.name}>{row.name}</span>
            {(row.mdm || row.ref) && <FlagBadgeP mdm={row.mdm} refurb={row.ref} size="sm"/>}
            {!row.onSite && <AudAiBadge status={aiStatus} isNew={true} />}
          </span>
          <span style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 1 }}>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)" }}>{row.art}</span>
            <span style={{ fontSize: 11, color: "var(--fg-disabled)" }}>{row.cat}</span>
          </span>
        </span>
        <span style={{ ...audCw(116), display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 2 }}>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 700, color: "var(--fg-primary)", whiteSpace: "nowrap" }}>{fmtUAHP(row.buy)}</span>
          {(row.buyChange === "up" || row.buyChange === "down") && <ChangeBadgeP change={row.buyChange} pct={row.buyPct} />}
        </span>
        <span style={{ ...audCw(104), textAlign: "right" }}>
          {row.our != null ? <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, color: "var(--fg-secondary)", whiteSpace: "nowrap" }}>{fmtUAHP(row.our)}</span> : <span style={{ fontSize: 12, color: "var(--fg-disabled)" }}>—</span>}
          {row.marginRisk && row.our != null && (
            <div style={{ display: "flex", alignItems: "center", gap: 3, justifyContent: "flex-end", marginTop: 1 }}>
              <Icon name="alert-triangle" size={10} color="var(--danger)" />
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--danger)" }}>{fmtPctP(row.marginPct)}</span>
            </div>
          )}
        </span>
        <span style={{ ...audCw(120), display: "flex", justifyContent: "flex-end" }}><AudHotCell hot={hot} /></span>
        <span style={audCw(84)}>{row.onSite ? <StockDotP stock={row.stock} qty={row.qty} /> : <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>пост. {row.stock ? `${row.qty ?? ""} шт` : "—"}</span>}</span>
        <span style={audCw(150)}><AudStatusBadge status={row.status} size="sm" /></span>
      </div>
      {expanded && <AudRowDetail row={row} cbPrefix={cbPrefix} narrow={false} hot={hot} aiStatus={aiStatus} warranty={warranty} hotLink={hot && hot.url} onLoadHot={onLoadHot} onEditLink={onEditLink} onToast={onToast} onSaved={onSaved} onAI={onAI} siteCatalog={siteCatalog} onBind={onBind} />}
    </>
  );
}

// ── Повноекранний екран аудиту ───────────────────────────────────────────────────
// ── Авто-оновлення цін (ТЕСТ, лише для одного постачальника) ────────────────────
// Покроковий обхід товарів, що вже є на сайті: тягне Hotline → рахує рекомендовану
// ціну (recommendPriceP) → показує й чекає підтвердження по КОЖНОМУ товару.
// Публікація — через готовий /api/horoshop/update-price (ціна + sourceSup).
function AutoPriceWalker({ rows, cbPrefix, supLabel, prefetched, supWarranty, onUpdated, onClose, onToast, initialIdx, initialDone, initialSkip, onProgress }) {
  const { useState, useEffect, useRef } = React;
  const list = rows;
  const [idx, setIdx]   = useState(initialIdx || 0);
  const [hot, setHot]   = useState(null);   // {found, offers...} | null=вантажиться
  const [busy, setBusy] = useState(false);
  const [edit, setEdit] = useState("");     // ручна ціна (порожньо = брати рекомендовану)
  const [srcSup, setSrcSup] = useState(cbPrefix); // обране джерело (постачальник, чий закуп беремо)
  const [site, setSite] = useState(null);   // актуальний стан на сайті {found, price, presence, article, url} | null=вантажиться
  const [pres, setPres] = useState("");     // обраний статус наявності (ключ AUD_PRES)
  const [guarMap, setGuarMap] = useState(null); // article(code) → {months,type}
  const [done, setDone] = useState(initialDone || 0);
  const [skip, setSkip] = useState(initialSkip || 0);
  const row = list[idx];

  // Журнал запусків (звіти): кожен обхід = pricing_run типу manual. Лічильники в ref —
  // cleanup ефекту бачить лише стартовий state, а закриватись walker може будь-де.
  const runRef = useRef({ id: null, done: initialDone || 0, skip: initialSkip || 0, idx: initialIdx || 0 });
  useEffect(() => {
    fetch("/api/pricing/run-start", { method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: "manual", supplier: cbPrefix, total: list.length }) })
      .then(r => r.json()).then(j => { if (j && j.ok) runRef.current.id = j.id; }).catch(() => {});
    return () => {
      const rc = runRef.current;
      if (rc.id == null) return;
      fetch("/api/pricing/run-finish", { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ id: rc.id, updated: rc.done, skipped: rc.skip, stopped: rc.idx < list.length ? 1 : 0 }) }).catch(() => {});
    };
  }, []);

  // Персист прогресу обходу → після випадкового закриття можна продовжити з цього ж місця.
  useEffect(() => {
    runRef.current = { ...runRef.current, done, skip, idx };
    onProgress && onProgress({ idx, done, skip, art: (list[idx] && list[idx].art) || "", total: list.length });
  }, [idx, done, skip]);

  // Карта гарантій (один раз) — read-only інфо
  useEffect(() => { fetch("/api/guarantee-map").then(r => r.json()).then(m => setGuarMap(m || {})).catch(() => setGuarMap({})); }, []);

  useEffect(() => {
    if (!row) return;
    let alive = true; setHot(null); setEdit(""); setSite(null); setPres("");
    // дефолтне джерело: основний склад (Новоселиця/Чернівці) в наявності → ЗАВЖДИ він (пріоритет,
    // навіть якщо інший дешевший); інакше — найдешевший у наявності; інакше — найдешевший.
    const offs = [{ sup: cbPrefix, uah: row.buy, stock: row.stock }, ...(row.others || [])].filter(o => Number(o.uah) > 0).sort((a, b) => a.uah - b.uah);
    const inStock = offs.filter(o => o.stock);
    const mainInStock = inStock.find(o => isMainStockP(o.sup));
    setSrcSup((mainInStock || inStock[0] || offs[0] || {}).sup || cbPrefix);
    // Актуальний стан товару на сайті (ціна/статус/реальний артикул для оновлення).
    // Товар може мати кілька артикулів («PB634-A-CIS, PB634-A-WW») — пробуємо КОЖЕН,
    // siteArticle з аудиту першим (покриває ручну привʼязку по назві та код із дужок);
    // перший знайдений на сайті й визначає стан (раніше шукало по всьому рядку → не знаходило).
    (async () => {
      for (const a of siteLookupCands(row)) {
        try {
          const j = await fetch("/api/horoshop/product?article=" + encodeURIComponent(a)).then(r => r.json());
          if (!alive) return;
          if (j && j.ok && j.found) {
            setSite({ found: true, price: j.data.price, presence: j.data.presence, article: j.data.article || a, url: j.data.url });
            setPres(presKeyFromNameP(j.data.presence));
            return;
          }
        } catch {}
      }
      if (alive) { setSite({ found: false }); setPres(presKeyFromNameP("")); }
    })();
    const pre = prefetched && prefetched[row.id];
    if (pre && pre.found && pre.offers) { setHot(pre); return; }
    const key = audNormArt(row.art) || row.key || "";
    fetch("/api/price-audit/hotline?q=" + encodeURIComponent(row.name) + "&key=" + encodeURIComponent(key))
      .then(r => r.json())
      .then(j => { if (alive) setHot(j && j.ok ? { found: !!j.found, offers: j.offers || [], total: j.total || 0, url: j.url } : { found: false, error: true }); })
      .catch(() => { if (alive) setHot({ found: false, error: true }); });
    return () => { alive = false; };
  }, [idx]);

  const overlay = { position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 };
  const card = { width: "100%", maxWidth: 720, maxHeight: "92vh", overflow: "auto", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)" };

  if (!row) {
    return (
      <div style={overlay} onClick={onClose}>
        <div style={card} onClick={e => e.stopPropagation()}>
          <div style={{ padding: 28, textAlign: "center" }}>
            <Icon name="check-circle" size={34} color="var(--success)" />
            <div style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)", marginTop: 12 }}>Готово</div>
            <div style={{ fontSize: 13, color: "var(--fg-secondary)", marginTop: 6 }}>Оновлено: <b>{done}</b> · пропущено: <b>{skip}</b></div>
            <Button variant="primary" style={{ marginTop: 18 }} onClick={onClose}>Закрити</Button>
          </div>
        </div>
      </div>
    );
  }

  // Усі постачальники товару (Фокстрот + інші), відсортовані за ціною; обране джерело = srcSup
  const offers = [{ sup: cbPrefix, uah: row.buy, cur: row.cur, stock: row.stock, qty: row.qty }, ...(row.others || [])].filter(o => Number(o.uah) > 0).sort((a, b) => a.uah - b.uah);
  const srcOffer = offers.find(o => o.sup === srcSup) || offers[0] || { sup: cbPrefix, uah: row.buy };
  const buyUse = srcOffer.uah;
  const rec = hot ? recommendPriceP(buyUse, hot.offers, (site && site.found && site.price != null) ? site.price : row.our) : null;
  const finalPrice = edit !== "" ? Math.round(Number(edit) || 0) : (rec ? rec.price : null);
  const finalProfit = finalPrice != null ? finalPrice - buyUse : null;
  const finalMpct = finalPrice ? (finalProfit / finalPrice) * 100 : null;
  const comp = hot && hot.offers ? hot.offers.filter(o => !o.us && Number(o.price) > 0).map(o => Math.round(o.price)).sort((a, b) => a - b) : [];
  const rankAfter = finalPrice != null && comp.length ? 1 + comp.filter(c => c < finalPrice).length : null;
  const ladder = hot && hot.offers ? hot.offers.slice(0, 8) : [];
  const modeColor = rec && rec.mode === "auto" ? "var(--success)" : "var(--warning)";
  const sitePrice = site && site.found && site.price != null ? site.price : row.our;
  const guarKey = (site && site.article) || row.art;
  const guar = guarMap ? (guarMap[guarKey] || guarMap[row.art]) : null;

  const next = () => { setIdx(i => i + 1); };
  const onSkip = () => { setSkip(s => s + 1); next(); };
  const apply = () => {
    if (finalPrice == null || finalPrice <= 0) { onToast && onToast("❌ Некоректна ціна"); return; }
    const pubArt = (site && site.article) || row.art;
    const presence = (AUD_PRES_BY_KEY[pres] || {}).presence;
    setBusy(true);
    // Гарантія ДЖЕРЕЛА: змінюючи постачальника — міняємо і гарантію на його
    const guar = supWarranty && supWarranty[srcSup] && Number(supWarranty[srcSup].months) > 0
      ? { months: supWarranty[srcSup].months, shopType: supWarranty[srcSup].shopType } : undefined;
    fetch("/api/horoshop/update-price", { method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ article: pubArt, price: finalPrice, sourceSup: srcSup, presence, guarantee: guar,
        // meta → price_history (звіти): стара ціна, закуп, режим/причина рекомендації, хто застосував
        meta: { runId: runRef.current.id, supplier: cbPrefix, name: row.name || "", oldPrice: sitePrice ?? null,
                buy: buyUse, mode: rec ? rec.mode : "manual",
                reason: (edit !== "" ? "ручна ціна · " : "") + ((rec && rec.reason) || ""),
                actor: localStorage.getItem("crm_user_name") || "" } }) })
      .then(r => r.json())
      .then(res => {
        if (res.ok) { onUpdated && onUpdated(row.id, finalPrice, pubArt); setDone(d => d + 1); onToast && onToast(`✅ ${pubArt}: ${fmtUAHP(finalPrice)}${presence ? " · " + presence : ""}`); next(); }
        else { onToast && onToast(`❌ ${row.art}: ${(res.log && res.log.map(l => l.message).filter(Boolean).join("; ")) || res.error || "не оновлено"}`); }
      })
      .catch(e => { onToast && onToast(`❌ ${e.message}`); })
      .finally(() => setBusy(false));   // ← завжди скидаємо busy, інакше після успіху фриз
  };

  // Товару немає на сайті (режим «Повне оновлення») → генеруємо картку через AI-майстер.
  // Прокидаємо префіл (назва/артикул/джерело/закуп/Hotline) + продовження: після публікації крокуємо далі.
  const siteKnown = site != null;
  const isMissing = siteKnown && !site.found;
  const genCard = () => {
    const supLabel = (PRICE_SUPS[srcSup] || {}).label || srcSup;
    const payload = {
      article: artForCardP(row).toUpperCase(), name: row.name || "", supplierPrefix: srcSup,
      buy: { uah: buyUse, price: buyUse, cur: "UAH", supLabel },
      hot: hot && hot.found ? hot : null,
      publishPrice: finalPrice > 0 ? finalPrice : undefined,   // авто-ціна → одразу в «ціна продажу»
      autoSearch: true,
      onPublish: () => { setDone(d => d + 1); next(); },   // опубліковано → наступний товар у walker'і
    };
    if (window._openAddProductAI) { window._openAddProductAI(payload); onToast && onToast(`Майстер AI · джерело: ${supLabel} — шукаю джерела…`); }
    else onToast && onToast("Майстер недоступний на цьому пристрої");
  };
  // Поставити картку В ЧЕРГУ генерації (фон, паралельно) і одразу крокнути далі — не чекаючи ~1 хв.
  const enqueueCard = () => {
    const supLabel = (PRICE_SUPS[srcSup] || {}).label || srcSup;
    const aiModels = (() => { try { return JSON.parse(localStorage.getItem("aiWizardModels") || "null"); } catch { return null; } })();
    // buy/джерело/hot сервер ігнорує при генерації, але кладемо в params як контекст —
    // повернуться в майстер при відкритті готової картки з лотка «Черга» (лесенка+ціна на «Перевірці»).
    const params = {
      article: artForCardP(row).toUpperCase(), name: row.name || "", createdBy: localStorage.getItem("crm_user_name") || "",
      supplierPrefix: srcSup,
      buy: { uah: buyUse, price: buyUse, cur: "UAH", supLabel },
      hot: hot && hot.found ? hot : null,
      publishPrice: finalPrice > 0 ? finalPrice : undefined,   // авто-ціна → підставиться в «ціна продажу» при відкритті з лотка
    };
    if (aiModels) {
      if (aiModels.text)   { params.provider = aiModels.text.provider; params.model = aiModels.text.model; }
      if (aiModels.vision) { params.visionProvider = aiModels.vision.provider; params.visionModel = aiModels.vision.model; }
      if (aiModels.facts)  { params.factsProvider = aiModels.facts.provider; params.factsModel = aiModels.facts.model; }
      if (aiModels.qa)     { params.qaProvider = aiModels.qa.provider; params.qaModel = aiModels.qa.model; }
    }
    fetch("/api/ai-queue/enqueue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) })
      .then(r => r.json())
      .then(j => { if (j && j.ok) { autoBindArtP(row, params.article); onToast && onToast(`➕ У черзі генерації: ${(row.name || row.art || "").slice(0, 40)}`); next(); } else onToast && onToast("❌ Черга: " + ((j && j.error) || "не вдалось")); })
      .catch(e => onToast && onToast("❌ Черга: " + e.message));
  };

  const box = { background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: "10px 12px" };

  return (
    <div style={overlay}>{/* клік по фону НЕ закриває — лише ✕ або «Стоп», щоб не загубити прогрес */}
      <div style={card} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", position: "sticky", top: 0, background: "var(--bg-panel)" }}>
          <Icon name="wand-2" size={17} color="var(--accent)" />
          <div style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)" }}>Авто-оновлення цін · {supLabel}</div>
          <span style={{ fontSize: 11, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>{idx + 1}/{list.length}</span>
          <div style={{ flex: 1 }} />
          <button onClick={onClose} style={{ width: 30, height: 30, border: 0, background: "transparent", color: "var(--fg-secondary)", cursor: "pointer" }}><Icon name="x" size={16} /></button>
        </div>

        <div style={{ padding: 18 }}>
          <div style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)", lineHeight: 1.35 }}>{row.name}</div>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)", marginTop: 3 }}>{row.art}</div>

          <div style={{ display: "flex", gap: 10, marginTop: 14 }}>
            <div style={{ ...box, flex: 1 }}>
              <div style={{ fontSize: 10.5, color: "var(--fg-muted)", textTransform: "uppercase", letterSpacing: ".04em" }}>Закупівля · {(PRICE_SUPS[srcSup] || {}).label || srcSup}</div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 700, color: "var(--fg-primary)", marginTop: 3 }}>{fmtUAHP(buyUse)}</div>
            </div>
            <div style={{ ...box, flex: 1 }}>
              <div style={{ fontSize: 10.5, color: "var(--fg-muted)", textTransform: "uppercase", letterSpacing: ".04em" }}>На сайті зараз</div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 700, color: "var(--fg-primary)", marginTop: 3 }}>{sitePrice != null ? fmtUAHP(sitePrice) : "—"}</div>
              <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 2 }}>{site == null ? "…" : site.found ? ("статус: " + (site.presence || "—")) : "немає на сайті"}</div>
            </div>
          </div>

          {/* Статус наявності (редагується тут же) + гарантія (read-only) — лише для товарів НА сайті */}
          {!isMissing && (
          <div style={{ display: "flex", gap: 10, marginTop: 12, alignItems: "flex-end" }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 10.5, color: "var(--fg-muted)", textTransform: "uppercase", letterSpacing: ".04em", marginBottom: 4 }}>Статус наявності на сайті</div>
              <select value={pres} onChange={e => setPres(e.target.value)} disabled={busy || site == null}
                style={{ width: "100%", boxSizing: "border-box", height: 38, padding: "0 10px", borderRadius: 9, border: "1px solid var(--border-default)", background: "var(--bg-base)", color: "var(--fg-primary)", fontFamily: "inherit", fontSize: 13 }}>
                {AUD_PRES.filter(p => p.kind !== "none").map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
              </select>
            </div>
            <div style={{ ...box, minWidth: 150 }}>
              <div style={{ fontSize: 10.5, color: "var(--fg-muted)", textTransform: "uppercase", letterSpacing: ".04em" }}>Гарантія</div>
              <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--fg-primary)", marginTop: 3 }}>{guar && guar.months ? `${guar.months} міс.${guar.type ? " · " + guar.type : ""}` : "—"}</div>
            </div>
          </div>
          )}

          {/* Усі постачальники товару — клік обирає джерело (закуп → перерахунок ціни) */}
          <div style={{ marginTop: 14 }}>
            <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 6 }}>Постачальники товару · джерело</div>
            <div style={{ ...box, padding: "4px 6px" }}>
              {offers.map((o, i) => {
                const sel = o.sup === srcSup, sinfo = PRICE_SUPS[o.sup] || {};
                return (
                  <div key={i} onClick={() => setSrcSup(o.sup)} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", borderRadius: 6, cursor: "pointer", background: sel ? "var(--accent-soft)" : "transparent" }}>
                    <span style={{ width: 7, height: 7, borderRadius: "50%", background: sinfo.color || "var(--fg-muted)", flexShrink: 0 }} />
                    <span style={{ flex: 1, fontSize: 12.5, color: sel ? "var(--accent)" : "var(--fg-secondary)", fontWeight: sel ? 600 : 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{sinfo.label || o.sup}{isMainStockP(o.sup) ? " · склад (пріоритет)" : i === 0 ? " · найдешевший" : ""}</span>
                    {isMainStockP(o.sup) && o.stock && <Icon name="star" size={12} color="var(--success)" />}
                    {!o.stock && <span style={{ fontSize: 10.5, color: "var(--warning)" }}>немає</span>}
                    {sel && <Icon name="check" size={13} color="var(--accent)" />}
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: sel ? 700 : 500, color: sel ? "var(--accent)" : "var(--fg-primary)" }}>{fmtUAHP(o.uah)}</span>
                  </div>
                );
              })}
            </div>
          </div>

          <div style={{ marginTop: 14 }}>
            <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 6 }}>Конкуренти Hotline</div>
            {!hot ? (
              <div style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--fg-muted)", fontSize: 12.5, padding: "8px 2px" }}>
                <Icon name="loader" size={14} style={{ animation: "spin .8s linear infinite" }} /> Тягнемо Hotline…
              </div>
            ) : !hot.found || !ladder.length ? (
              <div style={{ ...box, color: "var(--warning)", fontSize: 12.5 }}>Немає даних Hotline — рекомендація з цільової націнки. Перевір вручну.</div>
            ) : (
              <div style={{ ...box, fontFamily: "var(--font-mono)", padding: "4px 6px" }}>
                {ladder.map((o, i) => (
                  <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: o.us ? "var(--accent-soft)" : "transparent" }}>
                    <span style={{ width: 20, fontSize: 11, color: "var(--fg-muted)", textAlign: "right" }}>{o.rank}.</span>
                    <span style={{ flex: 1, fontFamily: "var(--font-ui)", fontSize: 12, color: o.us ? "var(--accent)" : "var(--fg-secondary)", fontWeight: o.us ? 600 : 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{o.store || "—"}{o.us ? " ← ми" : ""}</span>
                    <span style={{ fontSize: 12.5, fontWeight: o.us ? 700 : 500, color: o.us ? "var(--accent)" : "var(--fg-primary)" }}>{fmtUAHP(o.price)}</span>
                  </div>
                ))}
              </div>
            )}
          </div>

          {isMissing ? (
            <>
              <div style={{ marginTop: 14, padding: 14, borderRadius: 12, border: "1px solid var(--info)", background: "color-mix(in oklab, var(--info) 8%, transparent)" }}>
                <div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
                  <Icon name="sparkles" size={15} color="var(--info)" style={{ flexShrink: 0, marginTop: 1 }} />
                  <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", lineHeight: 1.45 }}>
                    Товару ще немає на сайті. Постав у <b style={{ color: "var(--fg-primary)" }}>чергу генерації</b> — AI збере картку у фоні (джерело <b style={{ color: "var(--fg-primary)" }}>{(PRICE_SUPS[srcSup] || {}).label || srcSup}</b>, закуп <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{fmtUAHP(buyUse)}</b>), а ти рухайся далі по прайсу. Коли буде готова — відкриєш на перевірку з лотка «Черга».
                  </span>
                </div>
                <Button variant="primary" leftIcon="list-plus" size="lg" style={{ width: "100%", marginTop: 12 }} onClick={enqueueCard} disabled={busy}>Додати в чергу генерації</Button>
                <button onClick={genCard} disabled={busy} style={{ width: "100%", marginTop: 8, height: 36, borderRadius: 8, background: "transparent", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 12.5, fontFamily: "inherit", cursor: "pointer", fontWeight: 500 }}>Або згенерувати зараз у майстрі</button>
              </div>
              <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
                <Button variant="secondary" leftIcon="skip-forward" style={{ flex: 1 }} onClick={onSkip} disabled={busy}>Пропустити</Button>
                <Button variant="ghost" onClick={onClose} disabled={busy}>Стоп</Button>
              </div>
            </>
          ) : (
            <>
          <div style={{ marginTop: 14, padding: 14, borderRadius: 12, border: `1px solid ${modeColor}`, background: "color-mix(in oklab, " + modeColor + " 8%, transparent)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
              <Icon name={rec && rec.mode === "auto" ? "check-circle" : "alert-triangle"} size={14} color={modeColor} />
              <span style={{ fontSize: 12, color: "var(--fg-secondary)" }}>{rec ? rec.reason : "Розрахунок…"}</span>
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 10.5, color: "var(--fg-muted)", textTransform: "uppercase", letterSpacing: ".04em", marginBottom: 4 }}>Рекомендована ціна</div>
                <input type="number" value={edit !== "" ? edit : (rec && rec.price != null ? rec.price : "")} onChange={e => setEdit(e.target.value)} disabled={busy}
                  style={{ width: "100%", boxSizing: "border-box", height: 40, padding: "0 12px", borderRadius: 9, border: "1px solid var(--border-default)", background: "var(--bg-base)", color: "var(--fg-primary)", fontFamily: "var(--font-mono)", fontSize: 17, fontWeight: 700 }} />
              </div>
              <div style={{ textAlign: "right", minWidth: 130 }}>
                <div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Прибуток</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 700, color: marginColorP(finalProfit ?? 0, finalMpct ?? 0) }}>{finalProfit != null ? fmtSignedP(finalProfit) : "—"}</div>
                <div style={{ fontSize: 11.5, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>{finalMpct != null ? fmtPctP(finalMpct) : ""}{rankAfter ? ` · поз. ${rankAfter}` : ""}</div>
              </div>
            </div>
            {rec && <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 8 }}>Пол: {fmtUAHP(rec.floor)} · ціль: {fmtUAHP(rec.target)}</div>}
          </div>

          <div style={{ display: "flex", gap: 8, marginTop: 16 }}>
            <Button variant="primary" leftIcon={busy ? "loader" : "upload"} style={{ flex: 1 }} onClick={apply} disabled={busy || finalPrice == null}>{busy ? "Оновлюю…" : "Оновити на сайті"}</Button>
            <Button variant="secondary" leftIcon="skip-forward" onClick={onSkip} disabled={busy}>Пропустити</Button>
            <Button variant="ghost" onClick={onClose} disabled={busy}>Стоп</Button>
          </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

// Кандидати артикулів для пошуку товару на сайті (walker + авто-ранер).
// СПЕРШУ row.siteArticle — реальний артикул сайту, який аудит уже знайшов
// (за артикулом, за кодом із дужок назви АБО за ручною привʼязкою по назві),
// потім токени з поля артикула прайсу. Без siteArticle товари без артикула
// чи привʼязані по назві хибно падали в «Не знайдено на сайті», хоча аудит
// показував їх «є на сайті».
function siteLookupCands(row) {
  const seen = new Set(), out = [];
  const add = (s) => {
    for (const t of String(s || "").split(/\s*[,;]\s*/)) {
      const v = t.trim();
      if (!v || v.includes(" ") || v.length > 30) continue;
      const k = v.toLowerCase();
      if (!seen.has(k)) { seen.add(k); out.push(v); }
    }
  };
  add(row.siteArticle);
  add(row.art);
  return out;
}

// Прогрес обходу авто-цін у localStorage (per постачальник+режим) — щоб пережити випадкове
// закриття вікна / перезавантаження сторінки й запропонувати «Продовжити з місця».
const _walkKey = (sup, mode) => `autoPriceWalk:${sup}:${mode}`;
function loadWalkProgress(sup, mode) { try { return JSON.parse(localStorage.getItem(_walkKey(sup, mode)) || "null"); } catch { return null; } }
function saveWalkProgress(sup, mode, d) { try { localStorage.setItem(_walkKey(sup, mode), JSON.stringify(d)); } catch {} }
function clearWalkProgress(sup, mode) { try { localStorage.removeItem(_walkKey(sup, mode)); } catch {} }

// ── Авто-оновлення цін (без підтверджень по кожному товару) ─────────────────────
// Іде по товарах сам, потихеньку: Hotline — ЛИШЕ з кешу останньої перевірки (авто не
// чекає воркера), ціна сайту — живцем. Застосовує лише впевнені випадки (mode=auto,
// зміна ≤30%); все спірне (демпінг/нема Hotline/великий стрибок) складає у список
// «на ревью» — по ньому потім можна пройти вручну звичайним walker'ом.
// Кожне застосування → price_history (runId), запуск цілком → pricing_runs.
const RUN_MAX_JUMP = 0.30;   // |зміна| понад 30% від поточної — не чіпаємо авто, на ревью
// dryRun — «Прогноз»: повний прохід із тими самими рішеннями, але БЕЗ жодного запису на сайт
// і без журналу запусків. Для перевірки нового постачальника перед першим бойовим раном.
function AutoPriceRunner({ rows, cbPrefix, supLabel, prefetched, supWarranty, onUpdated, onClose, onReview, onToast, dryRun }) {
  const { useState, useEffect, useRef } = React;
  const [idx, setIdx] = useState(0);
  const [feed, setFeed] = useState([]);           // [{art,name,status,old,next,reason}] — нові згори
  const [counts, setCounts] = useState({ updated: 0, unchanged: 0, review: 0, skipped: 0, failed: 0 });
  const [finished, setFinished] = useState(false);
  const [reviewList, setReviewList] = useState([]);
  const stopRef = useRef(false);

  useEffect(() => {
    let alive = true;
    stopRef.current = false;
    (async () => {
      // Ручні локи: артикули, де ОСТАННЯ зміна ціни — людиною (за 7 дн) → авто НЕ чіпає
      // (основний склад юзер веде вручну, у т.ч. в мінус від закупки — система не перетирає).
      let manualLocks = {};
      try { const ml = await fetch("/api/pricing/manual-locks").then(r => r.json()); if (ml && ml.ok && ml.data) manualLocks = ml.data; } catch {}
      let runId = null;
      if (!dryRun) try {
        const j = await fetch("/api/pricing/run-start", { method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ type: "auto", supplier: cbPrefix, total: rows.length }) }).then(r => r.json());
        if (j && j.ok) runId = j.id;
      } catch {}
      const cnt = { updated: 0, unchanged: 0, review: 0, skipped: 0, failed: 0 };
      const rev = [];
      const actor = "авто · " + (localStorage.getItem("crm_user_name") || "");
      const push = (row, status, extra) => {
        if (!alive) return;
        setCounts({ ...cnt });
        setFeed(f => [{ id: row.id, art: row.art, name: row.name, status, ...(extra || {}) }, ...f].slice(0, 500));
      };
      for (let i = 0; i < rows.length; i++) {
        if (!alive || stopRef.current) break;
        const row = rows[i];
        if (alive) setIdx(i + 1);
        // Ручний лок: людина нещодавно виставила ціну сама → пропускаємо (не перетираємо)
        const _mlk = manualLocks[audNormArt(row.siteArticle || row.art)];
        if (_mlk) {
          const dAgo = Math.max(0, Math.round((Date.now() - _mlk.ts) / 86400000));
          cnt.skipped++; push(row, "skipped", { reason: `Ціна виставлена ВРУЧНУ (${_mlk.actor || "менеджер"}, ${dAgo === 0 ? "сьогодні" : dAgo + " дн тому"}) — авто не чіпає` });
          continue;
        }
        // Джерело закупки: осн. склад у наявності → найдешевший у наявності → найдешевший
        const offs = [{ sup: cbPrefix, uah: row.buy, stock: row.stock }, ...(row.others || [])].filter(o => Number(o.uah) > 0).sort((a, b) => a.uah - b.uah);
        const inStock = offs.filter(o => o.stock);
        const srcOff = inStock.find(o => isMainStockP(o.sup)) || inStock[0] || offs[0];
        const buyUse = srcOff ? srcOff.uah : row.buy;
        const srcSup = srcOff ? srcOff.sup : cbPrefix;
        // Hotline — лише кеш останньої перевірки
        const hot = prefetched[row.id];
        if (!hot || hot.loading || hot.error) { cnt.skipped++; push(row, "skipped", { reason: "Hotline не звірено — запустіть «Перевірити всі ціни»" }); continue; }
        // Живий стан на сайті (ціна + реальний артикул) — siteArticle з аудиту першим,
        // інакше товари з ручною привʼязкою по назві падали в «Не знайдено на сайті»
        let site = null;
        for (const a of siteLookupCands(row)) {
          try {
            const sj = await fetch("/api/horoshop/product?article=" + encodeURIComponent(a)).then(r => r.json());
            if (sj && sj.ok && sj.found) { site = { price: sj.data.price, article: sj.data.article || a, presence: sj.data.presence }; break; }
          } catch {}
        }
        if (!alive || stopRef.current) break;
        if (!site) { cnt.skipped++; push(row, "skipped", { reason: "Не знайдено на сайті" }); continue; }
        const oldPrice = Number(site.price) || row.our || null;
        // ── Синк наявності (обидва напрямки) ─────────────────────────────
        // «Під замовлення …» / «Очікується» виставлені свідомо — їх НЕ чіпаємо ніколи.
        const sitePres = presKeyFromNameP(site.presence);
        const anyStock = offs.some(o => o.stock);
        if (!anyStock) {
          // Ні в кого немає в наявності: ціну НЕ чіпаємо (нема живого джерела);
          // якщо на сайті «В наявності» — знімаємо з наявності (лише статус, ціна та сама).
          if (sitePres === "in" && oldPrice) {
            if (dryRun) { cnt.updated++; push(row, "updated", { old: oldPrice, reason: "ПРОГНОЗ: буде знято з наявності (ні в кого немає)" }); continue; }
            try {
              const res = await fetch("/api/horoshop/update-price", { method: "POST", headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ article: site.article, price: Math.round(oldPrice), presence: "Немає в наявності",
                  meta: { runId, supplier: cbPrefix, name: row.name || "", oldPrice, buy: buyUse, mode: "auto", reason: "Ні в кого немає — знято з наявності", actor } }) }).then(r => r.json());
              if (res && res.ok) { cnt.updated++; onUpdated && onUpdated(row.id, Math.round(oldPrice), site.article); push(row, "updated", { old: oldPrice, reason: "Ні в кого немає в наявності — знято з наявності" }); }
              else { cnt.failed++; push(row, "error", { old: oldPrice, reason: "Не вдалось зняти з наявності" }); }
            } catch (e) { cnt.failed++; push(row, "error", { old: oldPrice, reason: e.message }); }
            await new Promise(r => setTimeout(r, 350));
          } else {
            cnt.skipped++; push(row, "skipped", { reason: "Ні в кого немає в наявності — ціну не чіпаємо" });
          }
          continue;
        }
        // Джерело МАЄ товар, а сайт каже «Немає в наявності»/«Статус не вибраний» →
        // разом із ціною повертаємо «В наявності».
        const presFix = (srcOff && srcOff.stock && (sitePres === "out" || sitePres === "none0")) ? "В наявності" : undefined;
        const rec = recommendPriceP(buyUse, hot.offers || [], oldPrice);
        if (rec.price == null) { cnt.skipped++; push(row, "skipped", { reason: rec.reason || "Немає закупки" }); continue; }
        if (rec.mode !== "auto") { cnt.review++; rev.push(row); push(row, "review", { old: oldPrice, next: rec.price, reason: rec.reason }); continue; }
        // Ціна та сама і наявність ок → без змін. Якщо ж треба повернути «В наявності» —
        // НЕ виходимо: падаємо в апдейт нижче з тією ж ціною + presence.
        if (oldPrice && rec.price === Math.round(oldPrice) && !presFix) { cnt.unchanged++; push(row, "unchanged", { old: oldPrice, reason: "Ціна вже оптимальна · " + rec.reason }); continue; }
        if (oldPrice && Math.abs(rec.price - oldPrice) / oldPrice > RUN_MAX_JUMP) { cnt.review++; rev.push(row); push(row, "review", { old: oldPrice, next: rec.price, reason: `Зміна >${Math.round(RUN_MAX_JUMP * 100)}% — на ревью · ` + rec.reason }); continue; }
        if (dryRun) {
          const upReason = "ПРОГНОЗ: " + (presFix ? "поверне «В наявності» · " : "") + rec.reason + ` · джерело ${(PRICE_SUPS[srcSup] || {}).label || srcSup}`;
          cnt.updated++; push(row, "updated", { old: oldPrice, next: rec.price, reason: upReason });
          continue;
        }
        try {
          // Гарантія ДЖЕРЕЛА: разом зі зміною постачальника на сайт їде і його гарантія
          const guar = supWarranty && supWarranty[srcSup] && Number(supWarranty[srcSup].months) > 0
            ? { months: supWarranty[srcSup].months, shopType: supWarranty[srcSup].shopType } : undefined;
          const upReason = (presFix ? "Повернуто «В наявності» · " : "") + rec.reason;
          const res = await fetch("/api/horoshop/update-price", { method: "POST", headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ article: site.article, price: rec.price, sourceSup: srcSup, presence: presFix, guarantee: guar,
              meta: { runId, supplier: cbPrefix, name: row.name || "", oldPrice, buy: buyUse, mode: "auto", reason: upReason, actor } }) }).then(r => r.json());
          if (res && res.ok) { cnt.updated++; onUpdated && onUpdated(row.id, rec.price, site.article); push(row, "updated", { old: oldPrice, next: rec.price, reason: upReason }); }
          else { cnt.failed++; push(row, "error", { old: oldPrice, next: rec.price, reason: (res && (res.error || (res.log || []).map(l => l.message).filter(Boolean).join("; "))) || "не оновлено" }); }
        } catch (e) { cnt.failed++; push(row, "error", { old: oldPrice, next: rec.price, reason: e.message }); }
        await new Promise(r => setTimeout(r, 350));   // потихеньку — не душимо Horoshop API
      }
      if (runId != null && !dryRun) {
        fetch("/api/pricing/run-finish", { method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ id: runId, ...cnt, stopped: stopRef.current ? 1 : 0 }) }).catch(() => {});
      }
      if (alive) { setCounts({ ...cnt }); setReviewList(rev); setFinished(true); setIdx(rows.length); }
    })();
    return () => { alive = false; stopRef.current = true; };
  }, []);

  const ST = {
    updated:   { icon: "check-circle",   color: "var(--success)", label: "оновлено" },
    unchanged: { icon: "minus-circle",   color: "var(--fg-muted)", label: "без змін" },
    review:    { icon: "eye",            color: "var(--warning)", label: "на ревью" },
    skipped:   { icon: "skip-forward",   color: "var(--fg-muted)", label: "пропущено" },
    error:     { icon: "alert-triangle", color: "var(--danger)",  label: "помилка" },
  };
  const chip = (label, val, color) => (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, color: "var(--fg-secondary)", background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 999, padding: "3px 10px" }}>
      <span style={{ width: 7, height: 7, borderRadius: "50%", background: color }} />{label} <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{val}</b>
    </span>
  );
  const total = rows.length;
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div style={{ width: "100%", maxWidth: 780, maxHeight: "92vh", display: "flex", flexDirection: "column", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)", overflow: "hidden" }} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <Icon name={dryRun ? "flask-conical" : "zap"} size={18} color={finished ? "var(--success)" : "var(--accent)"} />
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>{dryRun ? "Прогноз авто-цін (нічого не застосовується)" : "Авто-оновлення цін"} · {supLabel}</div>
          {dryRun && <span style={{ fontSize: 10.5, fontWeight: 700, padding: "2px 8px", borderRadius: 999, background: "rgba(245,158,11,.14)", color: "var(--warning)", border: "1px solid rgba(245,158,11,.35)" }}>DRY-RUN</span>}
          <div style={{ flex: 1 }} />
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--fg-secondary)" }}>{Math.min(idx, total)}/{total}</span>
        </div>
        <div style={{ padding: "10px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <div style={{ height: 6, background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 3, overflow: "hidden", marginBottom: 9 }}>
            <div style={{ height: "100%", width: `${total ? Math.round(Math.min(idx, total) / total * 100) : 0}%`, background: finished ? "var(--success)" : "var(--accent)", borderRadius: 3, transition: "width 250ms" }} />
          </div>
          <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
            {chip("оновлено", counts.updated, "var(--success)")}
            {chip("без змін", counts.unchanged, "var(--fg-muted)")}
            {chip("на ревью", counts.review, "var(--warning)")}
            {chip("пропущено", counts.skipped, "var(--fg-muted)")}
            {counts.failed > 0 && chip("помилок", counts.failed, "var(--danger)")}
          </div>
        </div>
        <div style={{ flex: 1, overflow: "auto", minHeight: 180 }}>
          {feed.length === 0 && (
            <div style={{ padding: "40px 18px", textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>
              <Icon name="loader" size={22} color="var(--accent)" style={{ animation: "spin .8s linear infinite" }} />
              <div style={{ marginTop: 10 }}>Стартую обхід…</div>
            </div>
          )}
          {feed.map((f, i) => {
            const st = ST[f.status] || ST.skipped;
            return (
              <div key={feed.length - i} style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "8px 18px", borderBottom: "1px solid var(--border-subtle)", opacity: f.status === "unchanged" || f.status === "skipped" ? .65 : 1 }}>
                <Icon name={st.icon} size={15} color={st.color} style={{ flexShrink: 0, marginTop: 2 }} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 }}>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)", flexShrink: 0 }}>{f.art}</span>
                    <span style={{ fontSize: 12.5, color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={f.name}>{f.name}</span>
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginTop: 2 }}>
                    {f.old != null && f.next != null && f.status !== "unchanged"
                      ? <span style={{ fontFamily: "var(--font-mono)" }}>{fmtUAHP(f.old)} → <b style={{ color: st.color }}>{fmtUAHP(f.next)}</b> · </span>
                      : f.old != null ? <span style={{ fontFamily: "var(--font-mono)" }}>{fmtUAHP(f.old)} · </span> : null}
                    <span style={{ color: "var(--fg-muted)" }}>{f.reason}</span>
                  </div>
                </div>
                <span style={{ fontSize: 10.5, color: st.color, flexShrink: 0, marginTop: 3, textTransform: "uppercase", letterSpacing: ".03em" }}>{st.label}</span>
              </div>
            );
          })}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 18px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          {finished
            ? <span style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}><Icon name="check-circle" size={13} color="var(--success)" /> {dryRun ? "Прогноз готовий — на сайт НІЧОГО не записано. Якщо все ок — запускай «Оновлення авто»." : `Готово${stopRef.current ? " (зупинено)" : ""}. Все записано у «Звіти».`}</span>
            : <span style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>{dryRun ? "Рахую прогноз — на сайт нічого не пишеться…" : "Оновлюю потихеньку… можна зупинити будь-коли — прогрес не втрачається."}</span>}
          <div style={{ flex: 1 }} />
          {!finished && <Button variant="danger" leftIcon="square" onClick={() => { stopRef.current = true; }}>Стоп</Button>}
          {finished && reviewList.length > 0 && <Button variant="primary" leftIcon="eye" onClick={() => onReview && onReview(reviewList)}>Пройти ревью ({reviewList.length})</Button>}
          {finished && <Button variant="secondary" onClick={onClose}>Закрити</Button>}
        </div>
      </div>
    </div>
  );
}

// ── «Повернути в наявність» — панель для вкладки ТОВАРИ (експортується window'ом) ────
// Товари, що на САЙТІ «немає в наявності», але Є в наявності у постачальників:
// (1) скан Hotline САМЕ по цих позиціях → (2) «розумне повернення» через AutoPriceRunner:
// актуальна ціна за авто-моделлю + гарантія джерела + повернення «В наявності» (presFix).
// items: [{id, article, name, sitePrice, offers:[{sup,uah,price,cur,stock}]}]
function ReturnStockPanel({ items, onDone, onToast }) {
  const { useState, useEffect, useRef } = React;
  const [hotMap, setHotMap] = useState({});          // id → результат Hotline (кеш або скан)
  const [scan, setScan] = useState("idle");          // idle | scanning | done
  const [prog, setProg] = useState({ done: 0, total: 0 });
  const [runner, setRunner] = useState(false);
  const [supWarranty, setSupWarranty] = useState({});
  const pollRef = useRef(null);
  useEffect(() => {
    fetch("/api/supplier-warranty").then(r => r.json()).then(j => { const d = (j && (j.data || j.warranty)) || j; if (d && typeof d === "object") setSupWarranty(d); }).catch(() => {});
    return () => { if (pollRef.current) clearInterval(pollRef.current); };
  }, []);
  // Кеш останніх перевірок Hotline — можливо, частина позицій уже звірена і скан не потрібен
  useEffect(() => {
    const keys = items.map(it => audNormArt(it.article)).filter(Boolean);
    if (!keys.length) return;
    fetch("/api/price-audit/cache", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ keys }) })
      .then(r => r.json()).then(j => {
        if (!j || !j.ok || !j.data) return;
        const m = {};
        for (const it of items) { const e = j.data[audNormArt(it.article)]; if (e) m[it.id] = { loading: false, found: !!e.found, offers: e.offers || [], total: e.total || 0, url: e.url || null, ts: e.ts, cached: true }; }
        if (Object.keys(m).length) { setHotMap(h => ({ ...m, ...h })); setScan(s => s === "idle" ? "done" : s); }
      }).catch(() => {});
  }, [items.length]);
  const startScan = () => {
    if (scan === "scanning" || !items.length) return;
    const scanItems = items.map(it => ({ id: it.id, q: it.name, key: audNormArt(it.article) })).filter(it => it.q);
    setScan("scanning"); setProg({ done: 0, total: scanItems.length });
    fetch("/api/price-audit/scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: scanItems, cbPrefix: "return" }) })
      .then(r => r.json()).then(j => {
        if (!j || !j.ok) { setScan("done"); return; }
        pollRef.current = setInterval(() => {
          fetch("/api/price-audit/scan/" + j.jobId).then(r => r.json()).then(st => {
            if (!st || !st.ok) { clearInterval(pollRef.current); pollRef.current = null; setScan("done"); return; }
            setProg({ done: st.done, total: st.total });
            // Помилка воркера (blocked/timeout/offline) НЕ перезаписує раніше ЗНАЙДЕНИЙ результат:
            // інакше скан із заблокованим воркером «з'їдав» кешовані ціни (кейс 180 → 70).
            setHotMap(h => { const n = { ...h }; for (const id in st.results) { const r = st.results[id]; const prev = h[id]; n[id] = (r.error && prev && prev.found) ? prev : { loading: false, found: !!r.found, offers: r.offers || [], total: r.total || 0, url: r.url || null, err: r.error || null }; } return n; });
            if (st.done >= st.total) { clearInterval(pollRef.current); pollRef.current = null; setScan("done"); }
          }).catch(() => {});
        }, 2000);
      }).catch(() => setScan("done"));
  };
  // Рядки для AutoPriceRunner: buy=0 (фіктивний «свій» офер відсіється фільтром uah>0),
  // ВСІ реальні постачальники в others → джерело закупки/гарантія/presFix — як в аудиті.
  const rows = items.map(it => ({
    id: it.id, art: it.article, name: it.name, siteArticle: it.article, onSite: true,
    our: it.sitePrice, buy: 0, stock: false, qty: null,
    others: (it.offers || []).filter(o => Number(o.uah) > 0).map(o => ({ sup: o.sup, uah: o.uah, price: o.price, cur: o.cur, stock: !!o.stock })),
  }));
  const checkedCount = items.filter(it => { const h = hotMap[it.id]; return h && !h.loading && h.found; }).length;
  const btn = { height: 34, padding: "0 14px", borderRadius: 8, fontFamily: "inherit", fontSize: 12.5, fontWeight: 600, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 7 };
  return (
    <div style={{ margin: "14px 24px 0", padding: "12px 16px", background: "var(--accent-soft)", border: "1px solid var(--accent-ring)", borderRadius: 12 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
        <Icon name="undo-2" size={17} color="var(--accent)" style={{ flexShrink: 0 }} />
        <div style={{ flex: 1, minWidth: 240 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Повернення в наявність · {items.length} поз.</div>
          <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginTop: 2 }}>
            На сайті «немає», але Є у постачальників. Крок 1: скан Hotline по цих позиціях{checkedCount ? ` (звірено ${checkedCount}/${items.length})` : ""}. Крок 2: розумне повернення — актуальна ціна + гарантія джерела + «В наявності». Без Hotline позиція буде пропущена.
          </div>
        </div>
        {scan === "scanning" ? (
          <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", display: "inline-flex", alignItems: "center", gap: 8 }}>
            <Icon name="loader" size={14} color="var(--accent)" style={{ animation: "spin .8s linear infinite" }} />
            Hotline {prog.done}/{prog.total}
          </span>
        ) : (
          <button onClick={startScan} disabled={!items.length}
            style={{ ...btn, border: "1px solid var(--border-default)", background: "var(--bg-panel)", color: "var(--fg-secondary)" }}>
            <Icon name="scan-line" size={14} /> {checkedCount ? "Пересканувати" : "Сканувати Hotline"} ({items.length})
          </button>
        )}
        <button onClick={() => setRunner(true)} disabled={!checkedCount || scan === "scanning"}
          title="Пройде всі позиції: впевнена ціна застосується разом із поверненням «В наявності», спірні — на ревью; гарантія джерела підтягнеться"
          style={{ ...btn, border: 0, background: checkedCount ? "var(--accent)" : "var(--border-default)", color: "#fff", opacity: (!checkedCount || scan === "scanning") ? .6 : 1 }}>
          <Icon name="zap" size={14} /> Розумне повернення ({checkedCount})
        </button>
      </div>
      {/* Не звірені з Hotline: привʼязати посилання вручну АБО повернути «як є» зі старою ціною */}
      {scan === "done" && (() => {
        const unmatched = items.filter(it => { const h = hotMap[it.id]; return !(h && h.found); });
        if (!unmatched.length) return null;
        const errCount = unmatched.filter(it => { const h = hotMap[it.id]; return h && h.err; }).length;
        const rescanOne = (it) => {
          setHotMap(h => ({ ...h, [it.id]: { loading: true } }));
          fetch("/api/price-audit/scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [{ id: it.id, q: it.name, key: audNormArt(it.article) }], cbPrefix: "return" }) })
            .then(r => r.json()).then(j => {
              if (!j || !j.ok) return;
              const iv = setInterval(() => {
                fetch("/api/price-audit/scan/" + j.jobId).then(r => r.json()).then(st => {
                  if (!st || !st.ok) { clearInterval(iv); return; }
                  if (st.done >= st.total) { clearInterval(iv); const r = st.results[it.id]; setHotMap(h => ({ ...h, [it.id]: r ? { loading: false, found: !!r.found, offers: r.offers || [], total: r.total || 0, url: r.url || null, err: r.error || null } : { loading: false, found: false } })); }
                }).catch(() => clearInterval(iv));
              }, 2000);
            }).catch(() => setHotMap(h => ({ ...h, [it.id]: { loading: false, found: false } })));
        };
        const editLink = (it) => {
          const url = window.prompt("Посилання на картку Hotline для «" + it.name.slice(0, 60) + "» (порожнє — скинути):", "");
          if (url == null) return;
          fetch("/api/price-audit/link", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: audNormArt(it.article), url: url.trim() }) })
            .then(r => r.json()).then(j => { if (j && j.ok) { onToast && onToast(url.trim() ? "Посилання збережено — перевіряю…" : "Посилання скинуто"); if (url.trim()) rescanOne(it); } })
            .catch(() => onToast && onToast("Помилка мережі"));
        };
        // Повернення «як є»: наявність + СТАРА ціна сайту, без Hotline. mode:'return' в історії —
        // НЕ ставить ручний лок (щойно Hotline зʼявиться, авто-ран зможе підтягнути ціну).
        const returnAsIs = async (list) => {
          let ok = 0, fail = 0;
          for (const it of list) {
            if (!(Number(it.sitePrice) > 0)) { fail++; continue; }
            try {
              const res = await fetch("/api/horoshop/update-price", { method: "POST", headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ article: it.article, price: Math.round(it.sitePrice), presence: "В наявності",
                  meta: { supplier: "повернення", name: it.name, oldPrice: it.sitePrice, mode: "return", reason: "Повернуто в наявність БЕЗ Hotline (стара ціна)", actor: localStorage.getItem("crm_user_name") || "" } }) }).then(r => r.json());
              if (res && res.ok) ok++; else fail++;
            } catch { fail++; }
            await new Promise(r => setTimeout(r, 300));
          }
          onToast && onToast(`Повернуто «як є»: ${ok}${fail ? ` · помилок/без ціни: ${fail}` : ""}`);
          onDone && onDone();
        };
        return (
          <div style={{ marginTop: 12, borderTop: "1px dashed var(--border-default)", paddingTop: 10 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginBottom: 8 }}>
              <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Не звірені з Hotline · {unmatched.length}</span>
              {errCount > 0 && <span style={{ fontSize: 11, color: "var(--warning)" }} title="Воркер упав/заблокований/таймаут — «Пересканувати» часто добирає">⚠ помилок скану: {errCount}</span>}
              <span style={{ fontSize: 11, color: "var(--fg-muted)", flex: 1, minWidth: 200 }}>привʼяжи посилання (якщо товар на Hotline є) або поверни «як є» — наявність + стара ціна, ціну потім підтягне авто-ран</span>
              <button onClick={() => returnAsIs(unmatched)} style={{ ...btn, height: 30, fontSize: 11.5, border: "1px solid var(--border-default)", background: "var(--bg-panel)", color: "var(--fg-secondary)" }}>
                <Icon name="undo-2" size={12} /> Повернути всі як є ({unmatched.filter(it => Number(it.sitePrice) > 0).length})
              </button>
            </div>
            <div style={{ maxHeight: 220, overflow: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
              {unmatched.map(it => {
                const h = hotMap[it.id];
                const minBuy = (it.offers || []).filter(o => o.stock && Number(o.uah) > 0).reduce((m, o) => Math.min(m, o.uah), Infinity);
                const margin = Number(it.sitePrice) > 0 && minBuy < Infinity ? Math.round(it.sitePrice - minBuy) : null;
                return (
                  <div key={it.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 8, fontSize: 12 }}>
                    {h && h.loading
                      ? <Icon name="loader" size={12} color="var(--accent)" style={{ animation: "spin .8s linear infinite", flexShrink: 0 }} />
                      : <Icon name={h && h.err ? "alert-triangle" : "search-x"} size={12} color={h && h.err ? "var(--warning)" : "var(--fg-muted)"} style={{ flexShrink: 0 }} />}
                    <span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--fg-primary)" }} title={it.name}>{it.name}</span>
                    <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)", flexShrink: 0 }}>{Number(it.sitePrice) > 0 ? fmtUAHP(it.sitePrice) : "без ціни"}</span>
                    {margin != null && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: margin < 0 ? "var(--danger)" : "var(--fg-muted)", flexShrink: 0 }} title={`Маржа від мін. закупки в наявності (${fmtUAHP(minBuy)})`}>{margin >= 0 ? "+" : ""}{margin} ₴</span>}
                    <button onClick={() => editLink(it)} title="Привʼязати посилання на картку Hotline і перевірити ще раз" style={{ height: 24, padding: "0 8px", borderRadius: 6, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", fontSize: 10.5, fontFamily: "inherit", cursor: "pointer", flexShrink: 0 }}>✏️ лінк</button>
                    <button onClick={() => returnAsIs([it])} disabled={!(Number(it.sitePrice) > 0)} title={Number(it.sitePrice) > 0 ? "Повернути в наявність зі старою ціною сайту" : "На сайті нема ціни — поверни вручну через картку"} style={{ height: 24, padding: "0 8px", borderRadius: 6, border: 0, background: Number(it.sitePrice) > 0 ? "var(--accent)" : "var(--border-default)", color: "#fff", fontSize: 10.5, fontFamily: "inherit", cursor: "pointer", flexShrink: 0 }}>↩ як є</button>
                  </div>
                );
              })}
            </div>
          </div>
        );
      })()}
      {runner && (
        <AutoPriceRunner rows={rows} cbPrefix="повернення" supLabel="Повернення в наявність"
          prefetched={hotMap} supWarranty={supWarranty} onUpdated={() => {}} onToast={onToast}
          onClose={() => { setRunner(false); onDone && onDone(); }}
          onReview={(rws) => { setRunner(false); onDone && onDone(); onToast && onToast(`Спірні ціни (${rws.length}) — на ревью в аудиті постачальника`); }} />
      )}
    </div>
  );
}
window.ReturnStockPanel = ReturnStockPanel;

// ── Прив'язка схожих: товари без артикул-матчу ↔ кандидати на сайті за назвою ────
// Покроковий обхід: клік по кандидату = прив'язати й далі. «Назад» повертає до минулої
// дії (видно, ЩО зробили) — можна скасувати або перепривʼязати (кейс «випадково не те»).
function MatchWalker({ items: itemsProp, supLabel, onBind, onClose }) {
  const { useState } = React;
  const [items] = useState(() => itemsProp);   // знімок на момент відкриття (прив'язки не зсувають список)
  const [idx, setIdx] = useState(0);
  const [hist, setHist] = useState([]);        // [{idx, action: 'bind'|'dismiss'|'skip', cand}]
  const [busy, setBusy] = useState(false);
  const [done, setDone] = useState(0);
  const it = items[idx];
  const lastHere = hist.length && hist[hist.length - 1].idx === idx ? hist[hist.length - 1] : null;

  const act = async (action, cand) => {
    if (busy || !it) return;
    setBusy(true);
    try {
      if (action === "bind") await onBind(it.row, cand);
      else if (action === "dismiss") await onBind(it.row, null, "dismiss");
      if (action === "bind") setDone(d => d + 1);
      setHist(h => [...h, { idx, action, cand: cand || null }]);
      setIdx(i => i + 1);
    } finally { setBusy(false); }
  };
  // Назад: повертаємось до РЯДКА минулої дії; банер показує, що було зроблено.
  const goBack = () => {
    if (busy || !hist.length) return;
    const last = hist[hist.length - 1];
    setHist(h => h.slice(0, -1));
    if (last.action === "bind") setDone(d => Math.max(0, d - 1));
    // скасовуємо серверну дію (bind/dismiss) — рядок знову «нічийний», можна вибрати заново
    if (last.action === "bind" || last.action === "dismiss") onBind(items[last.idx].row, null);
    setIdx(last.idx);
  };

  const card = { width: "100%", maxWidth: 680, maxHeight: "92vh", display: "flex", flexDirection: "column", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)", overflow: "hidden" };
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div style={card} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <Icon name="link" size={17} color="var(--accent)" />
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Прив'язка схожих · {supLabel}</div>
          <div style={{ flex: 1 }} />
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--fg-secondary)" }}>{Math.min(idx + 1, items.length)}/{items.length}</span>
        </div>
        {!it ? (
          <div style={{ padding: 34, textAlign: "center" }}>
            <Icon name="check-circle" size={30} color="var(--success)" />
            <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)", marginTop: 10 }}>Готово</div>
            <div style={{ fontSize: 12.5, color: "var(--fg-secondary)", marginTop: 5 }}>Прив'язано: <b>{done}</b> з {items.length}</div>
            <Button variant="primary" style={{ marginTop: 16 }} onClick={onClose}>Закрити</Button>
          </div>
        ) : (
          <>
            <div style={{ flex: 1, overflow: "auto", padding: "14px 18px", display: "flex", flexDirection: "column", gap: 12 }}>
              {lastHere && (
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 12px", borderRadius: 10, background: "color-mix(in oklab, var(--warning) 12%, transparent)", border: "1px solid var(--warning)", fontSize: 12, color: "var(--fg-primary)" }}>
                  <Icon name="undo-2" size={14} color="var(--warning)" />
                  Минулу дію скасовано{lastHere.action === "bind" && lastHere.cand ? <> (було прив'язано до «{String(lastHere.cand.name).slice(0, 45)}»)</> : lastHere.action === "dismiss" ? " (було приховано)" : ""} — вибери заново.
                </div>
              )}
              <div style={{ background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: "10px 12px" }}>
                <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", marginBottom: 5 }}>Товар постачальника</div>
                <div style={{ fontSize: 13.5, color: "var(--fg-primary)", fontWeight: 500 }}>{it.row.name}</div>
                <div style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 3, fontFamily: "var(--font-mono)" }}>
                  {it.row.art ? it.row.art + " · " : "без артикула · "}закуп {fmtUAHP(it.row.buy)}{it.row.stock ? " · в наявності" : " · немає"}
                </div>
              </div>
              <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)" }}>Схожі на сайті — клікни, щоб прив'язати</div>
              {it.cands.map((c, i) => (
                <button key={i} disabled={busy} onClick={() => act("bind", c)}
                  style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", borderRadius: 10, cursor: "pointer", textAlign: "left", fontFamily: "inherit",
                    background: "var(--bg-base)", border: "1px solid var(--border-default)" }}
                  onMouseEnter={e => e.currentTarget.style.borderColor = "var(--accent)"} onMouseLeave={e => e.currentTarget.style.borderColor = "var(--border-default)"}>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700, color: c.sim >= 0.85 ? "var(--success)" : "var(--warning)", flexShrink: 0, width: 40 }}>{Math.round(c.sim * 100)}%</span>
                  <span style={{ minWidth: 0, flex: 1 }}>
                    <span style={{ display: "block", fontSize: 12.5, color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{c.name}</span>
                    <span style={{ fontSize: 11, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>{c.article}{c.price ? " · " + fmtUAHP(c.price) : ""}</span>
                  </span>
                  <Icon name="link" size={14} color="var(--accent)" />
                </button>
              ))}
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 18px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
              <Button variant="secondary" size="sm" leftIcon="undo-2" disabled={!hist.length || busy} onClick={goBack}>Назад</Button>
              <div style={{ flex: 1 }} />
              <Button variant="secondary" size="sm" leftIcon="eye-off" disabled={busy} onClick={() => act("dismiss")} title="Це НЕ той товар — більше не пропонувати ці збіги">Немає збігу</Button>
              <Button variant="secondary" size="sm" leftIcon="skip-forward" disabled={busy} onClick={() => act("skip")}>Пропустити</Button>
              <Button variant="danger" size="sm" onClick={onClose}>Стоп</Button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ── Вибір товарів для авто-додавання (галочки + фільтр наявності) ────────────────
// Дефолт: ЛИШЕ товари, що є в наявності хоч в одного постачальника (їх реально продавати).
// «Немає в наявності» — окремий режим: завести картки на сайт зі статусом «Немає в наявності».
function AutoAddPicker({ rows, supLabel, cbPrefix, prefetched, onStart, onClose }) {
  const { useState, useMemo } = React;
  // наявність у БУДЬ-КОГО з постачальників (джерело обирається серед них)
  const hasStock = r => !!(r.stock && r.buy > 0) || (r.others || []).some(o => o.stock && Number(o.uah) > 0);
  // Пріоритет ЧЕРГИ додавання (оптимізація витрат AI: спершу те, на чому реально заробимо):
  //   tier 0 — джерело = ОСНОВНИЙ склад у наявності (тверде правило: завжди першим),
  //   tier 1 — закупка НИЖЧА за мін. Hotline → сортуємо за «зазором» (більший заробіток першим),
  //   tier 2 — Hotline не звірений/не знайдено (зазор невідомий),
  //   tier 3 — закупка ВИЩА за мін. Hotline (нема нормального постачальника) → в кінець.
  // Зазор = (мін. Hotline − закупка джерела) / закупка. Порядок вибору їде в чергу AI як є.
  const metaOf = useMemo(() => {
    const m = new Map();
    for (const r of rows) {
      const offs = [{ sup: cbPrefix, uah: r.buy, stock: r.stock }, ...(r.others || [])].filter(o => Number(o.uah) > 0).sort((a, b) => a.uah - b.uah);
      const inStock = offs.filter(o => o.stock);
      const srcOff = inStock.find(o => isMainStockP(o.sup)) || inStock[0] || offs[0];
      const buyUse = srcOff ? Number(srcOff.uah) : 0;
      const isMain = !!(srcOff && srcOff.stock && isMainStockP(srcOff.sup));
      const hot = prefetched && prefetched[r.id];
      let hotMin = (hot && hot.found && hot.offers && hot.offers.length) ? (Number(hot.offers[0].price) || null) : null;
      // Підозрілий матч (націнка >500% — схоже, Hotline знайшов інший товар) → зазор невідомий
      if (hotMin != null && hotMismatchP(buyUse, hotMin)) hotMin = null;
      const gap = (hotMin != null && buyUse > 0) ? (hotMin - buyUse) / buyUse : null;
      const tier = isMain ? 0 : gap == null ? 2 : gap >= 0 ? 1 : 3;
      m.set(r.id, { buyUse, isMain, hotMin, gap, tier });
    }
    return m;
  }, [rows, prefetched]);
  const byPriority = (list) => [...list].sort((a, b) => {
    const A = metaOf.get(a.id) || {}, B = metaOf.get(b.id) || {};
    return (A.tier - B.tier) || ((B.gap ?? -9) - (A.gap ?? -9));
  });
  const [flt, setFlt] = useState("stock");   // stock | supstock | nostock | all
  const groups = useMemo(() => ({
    stock: byPriority(rows.filter(hasStock)),
    supstock: byPriority(rows.filter(r => r.stock && r.buy > 0)),   // в наявності САМЕ в цього постачальника (чий аудит)
    nostock: byPriority(rows.filter(r => !hasStock(r))),
    all: byPriority(rows),
  }), [rows, metaOf]);
  // Другий вимір фільтра — зазор/джерело; комбінується з фільтром наявності (перетин).
  const [gflt, setGflt] = useState("any");   // any | good | pos | main | nohot | neg
  const GAP_PRED = {
    any:   () => true,
    g20:   m => m.gap != null && m.gap >= 0.20,
    g15:   m => m.gap != null && m.gap >= 0.15 && m.gap < 0.20,
    g10:   m => m.gap != null && m.gap >= 0.10 && m.gap < 0.15,
    g05:   m => m.gap != null && m.gap >= 0.05 && m.gap < 0.10,
    g00:   m => m.gap != null && m.gap >= 0 && m.gap < 0.05,
    main:  m => !!m.isMain,
    nohot: m => m.gap == null,
    neg:   m => m.gap != null && m.gap < 0,
  };
  const byGap = (list, g) => (list || []).filter(r => (GAP_PRED[g] || GAP_PRED.any)(metaOf.get(r.id) || {}));
  const visible = byGap(groups[flt] || rows, gflt);
  const [sel, setSel] = useState(() => new Set(groups.stock.map(r => r.id)));
  const toggle = id => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  // зміна будь-якого фільтра = вибрано все з нового зрізу (наявність × зазор)
  const switchFlt = k => { setFlt(k); setSel(new Set(byGap(groups[k], gflt).map(r => r.id))); };
  const switchGflt = g => { setGflt(g); setSel(new Set(byGap(groups[flt], g).map(r => r.id))); };
  const selVisible = visible.filter(r => sel.has(r.id));
  const allOn = selVisible.length === visible.length && visible.length > 0;
  const FLT = [["stock", `В наявності (${groups.stock.length})`], ["supstock", `У ${supLabel} (${groups.supstock.length})`], ["nostock", `Немає в наявності (${groups.nostock.length})`], ["all", `Всі (${rows.length})`]];
  const GFLT = [
    ["any",   "Всі", null],
    ["g20",   "≥20%", "var(--success)"],
    ["g15",   "15–20%", "var(--success)"],
    ["g10",   "10–15%", null],
    ["g05",   "5–10%", null],
    ["g00",   "0–5%", null],
    ["main",  "⭐ Осн. склад", null],
    ["nohot", "Без Hotline", null],
    ["neg",   "Закупка вища", "var(--danger)"],
  ].map(([k, l, c]) => [k, `${l} (${byGap(groups[flt], k).length})`, c]);
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }} onClick={onClose}>
      <div style={{ width: "100%", maxWidth: 720, maxHeight: "92vh", display: "flex", flexDirection: "column", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)", overflow: "hidden" }} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, flexWrap: "wrap" }}>
          <Icon name="sparkles" size={18} color="var(--accent)" />
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Авто-додавання · {supLabel}</div>
          <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>вибрано <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{selVisible.length}</b> з {visible.length}</span>
          <div style={{ flex: 1 }} />
          <button onClick={() => setSel(allOn ? new Set([...sel].filter(id => !visible.some(r => r.id === id))) : new Set([...sel, ...visible.map(r => r.id)]))}
            style={{ height: 28, padding: "0 12px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 12, background: "var(--bg-base)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)" }}>
            {allOn ? "Зняти всі" : "Вибрати всі"}
          </button>
          <button onClick={onClose} style={{ width: 32, height: 32, border: "1px solid var(--border-default)", borderRadius: 8, background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="x" size={16} /></button>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "10px 18px 6px", flexShrink: 0, flexWrap: "wrap" }}>
          {FLT.map(([k, l]) => (
            <button key={k} onClick={() => switchFlt(k)} style={{ height: 26, padding: "0 11px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 11.5,
              background: flt === k ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${flt === k ? "var(--accent)" : "var(--border-default)"}`, color: flt === k ? "var(--accent)" : "var(--fg-secondary)" }}>{l}</button>
          ))}
          {flt === "nostock" && <span style={{ fontSize: 11, color: "var(--warning)" }}>додадуться зі статусом «Немає в наявності»</span>}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "0 18px 10px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, flexWrap: "wrap" }}>
          <span style={{ fontSize: 11, color: "var(--fg-muted)", marginRight: 2 }}>Зазор:</span>
          {GFLT.map(([k, l, c]) => {
            const on = gflt === k;
            const col = c || "var(--accent)";
            return (
              <button key={k} onClick={() => switchGflt(k)} style={{ height: 26, padding: "0 11px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 11.5,
                background: on ? "color-mix(in oklab, " + col + " 14%, transparent)" : "var(--bg-base)", border: `1px solid ${on ? col : "var(--border-default)"}`, color: on ? col : (c || "var(--fg-secondary)") }}>{l}</button>
            );
          })}
        </div>
        <div style={{ flex: 1, overflow: "auto" }}>
          {visible.map(r => {
            const on = sel.has(r.id);
            const st = hasStock(r);
            const mt = metaOf.get(r.id) || {};
            const gapColor = mt.gap == null ? "var(--fg-muted)" : mt.gap < 0 ? "var(--danger)" : mt.gap >= 0.15 ? "var(--success)" : "var(--fg-secondary)";
            const gapLabel = mt.gap == null ? "—" : (mt.gap >= 0 ? "+" : "") + Math.round(mt.gap * 100) + "%";
            return (
              <label key={r.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 18px", borderBottom: "1px solid var(--border-subtle)", cursor: "pointer", opacity: on ? 1 : .55 }}>
                <input type="checkbox" checked={on} onChange={() => toggle(r.id)} style={{ width: 16, height: 16, accentColor: "var(--accent)", flexShrink: 0 }} />
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)", flexShrink: 0, width: 110, overflow: "hidden", textOverflow: "ellipsis" }}>{r.art}</span>
                <span style={{ fontSize: 12.5, color: "var(--fg-primary)", flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={r.name}>{mt.isMain ? "⭐ " : ""}{r.name}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, fontWeight: 600, color: gapColor, flexShrink: 0, width: 48, textAlign: "right" }}
                  title={mt.hotMin != null ? `Мін. Hotline ${fmtUAHP(mt.hotMin)} · закупка ${fmtUAHP(mt.buyUse)}` : "Hotline не звірено — запусти «Перевірити всі ціни»"}>{gapLabel}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--fg-secondary)", flexShrink: 0 }}>{fmtUAHP(r.buy)}</span>
                <span style={{ fontSize: 10.5, color: st ? "var(--success)" : "var(--fg-muted)", flexShrink: 0, width: 64, textAlign: "right" }}>{st ? "в наявн." : "немає"}</span>
              </label>
            );
          })}
          {visible.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>У цьому фільтрі кандидатів немає.</div>}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 18px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>Черга: ⭐ осн. склад → більший зазор до Hotline → без даних → <span style={{ color: "var(--danger)" }}>закупка вища за Hotline</span>. Зніми зайве — далі генерація в черзі → авто-публікація після QA-гейтів.</span>
          <div style={{ flex: 1 }} />
          <Button variant="secondary" onClick={onClose}>Скасувати</Button>
          <Button variant="primary" leftIcon="play" disabled={sel.size === 0} onClick={() => onStart(byPriority(rows.filter(r => sel.has(r.id))))}>Почати ({sel.size})</Button>
        </div>
      </div>
    </div>
  );
}

// ── Авто-ДОДАВАННЯ товарів (batch): off-site кандидати → черга AI-генерації ─────
// Ставить у чергу ПАЧКОЮ з autoPublish: сервер згенерує картку і сам опублікує
// (QA-гейти: назва/опис/≥3 фото/категорія ≥0.75/ціна; фейл → картка чекає в лотку).
// Ціна публікації — з тих самих авто-цін (recommendPriceP по кешу Hotline).
// Беремо ЛИШЕ товари в наявності у джерела — «під замовлення» не авто-додаємо.
function AutoAddRunner({ rows, cbPrefix, supLabel, prefetched, aiMap, onClose, onToast }) {
  // Ідентифікатор ПАРТІЇ — всі картки цього запуску групуються у звіті «AI-картки» (воронка)
  const batchIdRef = React.useRef("b" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5));
  const { useState, useEffect, useRef } = React;
  const [idx, setIdx] = useState(0);
  const [feed, setFeed] = useState([]);
  const [counts, setCounts] = useState({ queued: 0, dup: 0, skipped: 0, failed: 0 });
  const [finished, setFinished] = useState(false);
  const [retrying, setRetrying] = useState(null);   // артикул, що зараз повторюється
  const stopRef = useRef(false);

  // fetch із ЗАХИСТОМ від HTML-відповідей (502/перезавантаження сервера під навантаженням
  // генерації → «Unexpected token '<'») + до 3 спроб із паузою.
  const jfetch = async (url, opts) => {
    let lastErr = null;
    for (let a = 0; a < 3; a++) {
      try {
        const r = await fetch(url, opts);
        const txt = await r.text();
        if (txt.trim().startsWith("<")) throw new Error(`сервер повернув HTML (код ${r.status}) — ймовірно перезавантаження/502`);
        return JSON.parse(txt);
      } catch (e) { lastErr = e; if (a < 2) await new Promise(res => setTimeout(res, 2500 * (a + 1))); }
    }
    throw lastErr;
  };

  // Обробка ОДНОГО товару → {status, reason, price}. Використовують і основний цикл,
  // і кнопка «повторити» на рядках із помилкою (щоб не чекати/не перезапускати все).
  const processRow = async (row) => {
    // Артикул картки: прайс → SKU з назви (вкл. «alpha.black») → синтетичний AI-хеш
    const artUse = artForCardP(row).toUpperCase();
    const k = audNormArt(artUse);
    if (k && aiMap[k]) return { status: "dup", reason: "Вже в роботі (" + aiMap[k] + ")" };
    // ЖИВА перевірка «вже на сайті» (кеш каталогу міг бути порожній після рестарту →
    // аудит хибно показував «нема на сайті» для ВСЬОГО — не ставимо дублі в чергу).
    // Перевіряємо і токени з прайсу, і фінальний артикул картки (виведений з назви/синтетичний).
    try {
      const cands = String(row.art || "").split(/\s*[,;]\s*/).map(x => x.trim()).filter(x => x && !x.includes(" ") && x.length <= 30);
      if (!cands.some(c => c.toUpperCase() === artUse)) cands.push(artUse);
      for (const a of cands) {
        const sj = await jfetch("/api/horoshop/product?article=" + encodeURIComponent(a));
        if (sj && sj.ok && sj.found) {
          // Знайшовся на сайті під виведеним артикулом → одразу привʼязуємо по назві,
          // щоб аудит більше не показував його «нема на сайті»
          autoBindArtP(row, (sj.data && sj.data.article) || a);
          return { status: "dup", reason: "Вже на сайті (жива перевірка) — пропускаю" };
        }
      }
    } catch (e) { return { status: "skipped", reason: "Не вдалось перевірити наявність на сайті: " + e.message }; }
    // Джерело: осн. склад у наявності → найдешевший у наявності → (якщо ніде немає)
    // найдешевший БУДЬ-ЯКИЙ: товар заводиться на сайт зі статусом «Немає в наявності»
    // (режим фільтра «Немає в наявності» у пікері — завести картки наперед).
    const offs = [{ sup: cbPrefix, uah: row.buy, stock: row.stock }, ...(row.others || [])].filter(o => Number(o.uah) > 0).sort((a, b) => a.uah - b.uah);
    const inStock = offs.filter(o => o.stock);
    const srcOff = inStock.find(o => isMainStockP(o.sup)) || inStock[0] || offs[0];
    if (!srcOff) return { status: "skipped", reason: "Немає закупки в жодного постачальника" };
    const presence = srcOff.stock ? "В наявності" : "Немає в наявності";
    const buyUse = srcOff.uah, srcSup = srcOff.sup;
    const hot = prefetched[row.id];
    const rec = recommendPriceP(buyUse, (hot && hot.found && hot.offers) || [], 0);
    if (!rec || !(rec.price > 0)) return { status: "skipped", reason: "Не вдалось порахувати ціну" };
    const aiModels = (() => { try { return JSON.parse(localStorage.getItem("aiWizardModels") || "null"); } catch { return null; } })();
    const params = {
      // Артикул капсом — стандарт магазину (у прайсах постачальників часто дрібними)
      article: artUse, name: row.name || "",
      createdBy: localStorage.getItem("crm_user_name") || "",
      supplierPrefix: srcSup,
      buy: { uah: buyUse, price: buyUse, cur: "UAH", supLabel: (PRICE_SUPS[srcSup] || {}).label || srcSup },
      hot: hot && hot.found ? hot : null,
      autoPublish: true, publishPrice: rec.price, publishPresence: presence,
      batchId: batchIdRef.current,
    };
    if (aiModels) {
      if (aiModels.text)   { params.provider = aiModels.text.provider; params.model = aiModels.text.model; }
      if (aiModels.vision) { params.visionProvider = aiModels.vision.provider; params.visionModel = aiModels.vision.model; }
      if (aiModels.facts)  { params.factsProvider = aiModels.facts.provider; params.factsModel = aiModels.facts.model; }
      if (aiModels.qa)     { params.qaProvider = aiModels.qa.provider; params.qaModel = aiModels.qa.model; }
    }
    try {
      const j = await jfetch("/api/ai-queue/enqueue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) });
      if (j && j.ok && j.duplicate) return { status: "dup", reason: "Вже в черзі (" + (j.status || "") + ")" };
      if (j && j.ok) { autoBindArtP(row, artUse); return { status: "queued", reason: rec.reason, price: rec.price }; }
      return { status: "error", reason: (j && j.error) || "не поставлено в чергу" };
    } catch (e) { return { status: "error", reason: e.message }; }
  };
  const bucketOf = st => st === "queued" ? "queued" : st === "dup" ? "dup" : st === "error" ? "failed" : "skipped";

  useEffect(() => {
    let alive = true;
    stopRef.current = false;
    (async () => {
      const cnt = { queued: 0, dup: 0, skipped: 0, failed: 0 };
      for (let i = 0; i < rows.length; i++) {
        if (!alive || stopRef.current) break;
        const row = rows[i];
        if (alive) setIdx(i + 1);
        const r = await processRow(row);
        cnt[bucketOf(r.status)]++;
        if (alive) {
          setCounts({ ...cnt });
          setFeed(f => [{ art: row.art, name: row.name, status: r.status, reason: r.reason, price: r.price }, ...f].slice(0, 500));
        }
        await new Promise(res => setTimeout(res, 120));
      }
      if (alive) { setCounts({ ...cnt }); setFinished(true); setIdx(rows.length); }
    })();
    return () => { alive = false; stopRef.current = true; };
  }, []);

  // Точковий повтор ОДНОГО товару (рядок із помилкою) — без перезапуску всього процесу.
  const onRetry = async (art) => {
    const row = rows.find(x => x.art === art);
    if (!row || retrying) return;
    setRetrying(art);
    const r = await processRow(row);
    setCounts(c => {
      const n = { ...c, failed: Math.max(0, c.failed - 1) };
      const b = bucketOf(r.status);
      n[b] = (n[b] || 0) + 1;
      return n;
    });
    setFeed(f => [{ art: row.art, name: row.name, status: r.status, reason: r.reason, price: r.price },
                  ...f.filter(x => !(x.art === art && x.status === "error"))].slice(0, 500));
    setRetrying(null);
  };

  const ST = {
    queued:  { icon: "check-circle",   color: "var(--success)", label: "у черзі · авто" },
    dup:     { icon: "copy",           color: "var(--fg-muted)", label: "дубль" },
    skipped: { icon: "skip-forward",   color: "var(--fg-muted)", label: "пропущено" },
    error:   { icon: "alert-triangle", color: "var(--danger)",  label: "помилка" },
  };
  const chip = (label, val, color) => (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, color: "var(--fg-secondary)", background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 999, padding: "3px 10px" }}>
      <span style={{ width: 7, height: 7, borderRadius: "50%", background: color }} />{label} <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{val}</b>
    </span>
  );
  const total = rows.length;
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div style={{ width: "100%", maxWidth: 780, maxHeight: "92vh", display: "flex", flexDirection: "column", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)", overflow: "hidden" }} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <Icon name="sparkles" size={18} color={finished ? "var(--success)" : "var(--accent)"} />
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Авто-додавання товарів · {supLabel}</div>
          <div style={{ flex: 1 }} />
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--fg-secondary)" }}>{Math.min(idx, total)}/{total}</span>
        </div>
        <div style={{ padding: "10px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <div style={{ height: 6, background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 3, overflow: "hidden", marginBottom: 9 }}>
            <div style={{ height: "100%", width: `${total ? Math.round(Math.min(idx, total) / total * 100) : 0}%`, background: finished ? "var(--success)" : "var(--accent)", borderRadius: 3, transition: "width 250ms" }} />
          </div>
          <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
            {chip("у черзі · авто", counts.queued, "var(--success)")}
            {chip("дублі", counts.dup, "var(--fg-muted)")}
            {chip("пропущено", counts.skipped, "var(--fg-muted)")}
            {counts.failed > 0 && chip("помилок", counts.failed, "var(--danger)")}
          </div>
        </div>
        <div style={{ flex: 1, overflow: "auto", minHeight: 160 }}>
          {feed.map((f, i) => {
            const st = ST[f.status] || ST.skipped;
            return (
              <div key={feed.length - i} style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "8px 18px", borderBottom: "1px solid var(--border-subtle)", opacity: f.status === "queued" ? 1 : .7 }}>
                <Icon name={st.icon} size={15} color={st.color} style={{ flexShrink: 0, marginTop: 2 }} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 }}>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)", flexShrink: 0 }}>{f.art}</span>
                    <span style={{ fontSize: 12.5, color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={f.name}>{f.name}</span>
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 2 }}>
                    {f.price != null && <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>ціна {fmtUAHP(f.price)} · </span>}{f.reason}
                  </div>
                </div>
                {f.status === "error" && (
                  <button onClick={() => onRetry(f.art)} disabled={!!retrying} title="Повторити цей товар (не чекаючи решти)"
                    style={{ display: "inline-flex", alignItems: "center", gap: 4, height: 24, padding: "0 9px", borderRadius: 999, cursor: retrying ? "wait" : "pointer", fontFamily: "inherit", fontSize: 11, background: "var(--bg-base)", border: "1px solid var(--danger)", color: "var(--danger)", flexShrink: 0 }}>
                    <Icon name={retrying === f.art ? "loader" : "rotate-cw"} size={11} color="var(--danger)" />повторити
                  </button>
                )}
                <span style={{ fontSize: 10.5, color: st.color, flexShrink: 0, marginTop: 3, textTransform: "uppercase", letterSpacing: ".03em" }}>{st.label}</span>
              </div>
            );
          })}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 18px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <span style={{ fontSize: 12, color: "var(--fg-muted)", lineHeight: 1.4 }}>
            {finished
              ? "Черга генерує у фоні (~1 хв/картка). Публікація авто після QA-гейтів; що не пройшло — чекає в лотку «Черга». Статуси: Звіти → AI-картки."
              : "Ставлю в чергу… генерація і публікація підуть у фоні на сервері."}
          </span>
          <div style={{ flex: 1 }} />
          {!finished && <Button variant="danger" leftIcon="square" onClick={() => { stopRef.current = true; }}>Стоп</Button>}
          {finished && <Button variant="secondary" onClick={onClose}>Закрити</Button>}
        </div>
      </div>
    </div>
  );
}

// ── Звіти: запуски оновлення цін + історія по товару ────────────────────────────
function PricingReports({ cbPrefix, supLabel, onClose }) {
  const { useState, useEffect } = React;
  const [runs, setRuns] = useState(null);       // null = вантажиться
  const [sel, setSel] = useState(null);         // {run, items} — відкритий запуск
  const [tab, setTab] = useState("runs");       // runs | article | cards | usage
  const [artQ, setArtQ] = useState("");
  const [artItems, setArtItems] = useState(null);
  const [cards, setCards] = useState(null);     // журнал AI-генерацій
  const [cardsDays, setCardsDays] = useState(7);      // період журналу
  const [cardsFilter, setCardsFilter] = useState("all"); // фільтр статусу
  const [cardsSup, setCardsSup] = useState("all");     // фільтр постачальника
  const [cardsReason, setCardsReason] = useState(null); // фільтр «причина фейлу» (клік по агрегату)
  const [cardsBatch, setCardsBatch] = useState(null);   // відкрита партія (batch_id)
  // «Дозібрати фото» для тонких галерей (≤2 фото): один товар або пачкою по всіх видимих
  const [refilling, setRefilling] = useState(null);     // артикул у роботі | "bulk"
  const [refillMsg, setRefillMsg] = useState(null);
  const refillAliveRef = React.useRef(true);
  useEffect(() => () => { refillAliveRef.current = false; }, []);
  const refillOne = async (c) => {
    try {
      const j = await fetch("/api/ai-cards/refill-photos", { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ article: c.article, name: c.name }) }).then(r => r.json());
      if (!refillAliveRef.current) return;
      if (j && j.ok) {
        setCards(cs => Array.isArray(cs) ? cs.map(x => x.article === c.article ? { ...x, photos: j.gallery } : x) : cs);
        setRefillMsg(`${c.article}: +${j.added} фото (тепер ${j.gallery})${j.note ? " · " + j.note : ""}`);
      } else setRefillMsg(`${c.article}: ❌ ${(j && j.error) || "не вдалось"}`);
    } catch (e) { if (refillAliveRef.current) setRefillMsg(`${c.article}: ❌ ${e.message}`); }
  };
  // «Заново» — свідома перегенерація картки (force: дедуп і гейт перезапису пропускають;
  // ціна/наявність лишаються поточні з сайту, галерея замінюється новою)
  const regenOne = async (c) => {
    try {
      const j = await fetch("/api/ai-queue/enqueue", { method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ article: c.article, name: c.name, supplierPrefix: c.supplier || "",
          createdBy: localStorage.getItem("crm_user_name") || "", autoPublish: true, force: true, batchId: "regen" }) }).then(r => r.json());
      if (j && j.ok) setRefillMsg(`${c.article}: 🔁 у черзі на перегенерацію — готова картка сама заміниться на сайті (стеж за лотком «Черга»)`);
      else setRefillMsg(`${c.article}: ❌ ${(j && j.error) || "не вдалось поставити в чергу"}`);
    } catch (e) { setRefillMsg(`${c.article}: ❌ ${e.message}`); }
  };
  const refillBulk = async (list) => {
    setRefilling("bulk");
    for (let i = 0; i < list.length; i++) {
      if (!refillAliveRef.current) return;
      setRefillMsg(`Добивка ${i + 1}/${list.length}: ${list[i].article}…`);
      await refillOne(list[i]);
    }
    if (refillAliveRef.current) { setRefilling(null); setRefillMsg(m => (m || "") + " · ✅ пачку завершено"); }
  };
  const [usage, setUsage] = useState(null);     // витрати AI (запити/токени/$)
  const fmtTs = ts => ts ? new Date(ts).toLocaleString("uk-UA", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }) : "—";

  useEffect(() => {
    fetch("/api/pricing/runs?limit=50" + (cbPrefix ? "&supplier=" + encodeURIComponent(cbPrefix) : ""))
      .then(r => r.json()).then(j => setRuns(j && j.ok ? j.runs : []))
      .catch(() => setRuns([]));
  }, []);
  useEffect(() => {
    if (tab === "cards") {
      setCards(null);
      fetch("/api/ai-cards/history?days=" + cardsDays + "&limit=500").then(r => r.json()).then(j => setCards(j && j.ok ? j.items : [])).catch(() => setCards([]));
    }
    if (tab === "usage" && usage === null)
      fetch("/api/ai-usage/summary?days=30").then(r => r.json()).then(j => setUsage(j && j.ok ? j : { error: true })).catch(() => setUsage({ error: true }));
  }, [tab, cardsDays]);
  const openRun = (id) => {
    setSel({ loading: true });
    fetch("/api/pricing/run?id=" + id).then(r => r.json())
      .then(j => setSel(j && j.ok ? { run: j.run, items: j.items } : null))
      .catch(() => setSel(null));
  };
  const searchArt = () => {
    if (!artQ.trim()) return;
    setArtItems("loading");
    fetch("/api/pricing/history?article=" + encodeURIComponent(artQ.trim()))
      .then(r => r.json()).then(j => setArtItems(j && j.ok ? j.items : []))
      .catch(() => setArtItems([]));
  };

  const MODE_CHIP = { auto: ["zap", "var(--success)", "авто"], review: ["eye", "var(--warning)", "ревью"], manual: ["user", "var(--info)", "вручну"] };
  const ItemRow = ({ it }) => {
    const [mi, mc, ml] = MODE_CHIP[it.mode] || MODE_CHIP.manual;
    const diff = it.old_price != null ? it.new_price - it.old_price : null;
    return (
      <div style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "8px 18px", borderBottom: "1px solid var(--border-subtle)", opacity: it.applied ? 1 : .7 }}>
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)", flexShrink: 0, width: 76 }}>{fmtTs(it.ts)}</span>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 }}>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)", flexShrink: 0 }}>{it.article}</span>
            <span style={{ fontSize: 12.5, color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={it.name}>{it.name}</span>
          </div>
          <div style={{ fontSize: 11.5, marginTop: 2 }}>
            <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>
              {it.old_price != null ? fmtUAHP(it.old_price) + " → " : ""}<b style={{ color: it.applied ? (diff == null ? "var(--fg-primary)" : diff >= 0 ? "var(--success)" : "var(--warning)") : "var(--danger)" }}>{fmtUAHP(it.new_price)}</b>
              {diff != null && diff !== 0 && <span style={{ color: "var(--fg-muted)" }}> ({diff > 0 ? "+" : "−"}{fmtBareP(Math.abs(diff))})</span>}
              {it.buy != null && <span style={{ color: "var(--fg-muted)" }}> · закуп {fmtUAHP(it.buy)}</span>}
            </span>
            <span style={{ color: "var(--fg-muted)" }}> · {it.reason || "—"}</span>
            {!it.applied && <span style={{ color: "var(--danger)" }}> · ❌ {it.error || "не застосовано"}</span>}
          </div>
        </div>
        <span title={ml} style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10.5, color: mc, flexShrink: 0, marginTop: 3 }}>
          <Icon name={mi} size={11} color={mc} />{ml}{it.actor ? <span style={{ color: "var(--fg-muted)" }}> · {it.actor}</span> : null}
        </span>
      </div>
    );
  };

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }} onClick={onClose}>
      <div style={{ width: "100%", maxWidth: 860, maxHeight: "92vh", display: "flex", flexDirection: "column", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "0 24px 60px rgba(0,0,0,.5)", overflow: "hidden" }} onClick={e => e.stopPropagation()}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
          <Icon name="history" size={18} color="var(--accent)" />
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Звіти по цінах{supLabel ? " · " + supLabel : ""}</div>
          <div style={{ display: "flex", gap: 6, marginLeft: 14, flexWrap: "wrap" }}>
            {[["runs", "Ціни: запуски"], ["article", "Ціни: по товару"], ["cards", "AI-картки"], ["usage", "Витрати AI"]].map(([k, l]) => (
              <button key={k} onClick={() => { setTab(k); setSel(null); }} style={{ height: 28, padding: "0 12px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 12,
                background: tab === k ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${tab === k ? "var(--accent)" : "var(--border-default)"}`, color: tab === k ? "var(--accent)" : "var(--fg-secondary)" }}>{l}</button>
            ))}
          </div>
          <div style={{ flex: 1 }} />
          <button onClick={onClose} style={{ width: 32, height: 32, border: "1px solid var(--border-default)", borderRadius: 8, background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="x" size={16} /></button>
        </div>

        {tab === "article" && (
          <>
            <div style={{ display: "flex", gap: 8, padding: "12px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
              <input value={artQ} onChange={e => setArtQ(e.target.value)} onKeyDown={e => e.key === "Enter" && searchArt()} placeholder="Артикул на сайті (напр. ey5528e0)"
                style={{ flex: 1, height: 36, padding: "0 12px", borderRadius: 8, border: "1px solid var(--border-default)", background: "var(--bg-base)", color: "var(--fg-primary)", fontSize: 13, fontFamily: "var(--font-mono)", outline: "none" }} />
              <Button variant="primary" leftIcon="search" onClick={searchArt}>Історія</Button>
            </div>
            <div style={{ flex: 1, overflow: "auto" }}>
              {artItems === "loading" && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Шукаю…</div>}
              {Array.isArray(artItems) && artItems.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Змін по цьому артикулу за 90 днів немає.</div>}
              {Array.isArray(artItems) && artItems.map(it => <ItemRow key={it.id} it={it} />)}
              {artItems == null && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Введи артикул — покажу всі зміни ціни за 90 днів: коли, з якої на яку, чому і хто.</div>}
            </div>
          </>
        )}

        {tab === "runs" && !sel && (
          <div style={{ flex: 1, overflow: "auto" }}>
            {runs === null && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Вантажу…</div>}
            {Array.isArray(runs) && runs.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Запусків ще не було. Запусти «Оновлення авто» або пройди walker вручну.</div>}
            {Array.isArray(runs) && runs.map(r => (
              <div key={r.id} onClick={() => openRun(r.id)} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 18px", borderBottom: "1px solid var(--border-subtle)", cursor: "pointer" }}
                onMouseEnter={e => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
                <Icon name={r.type === "auto" ? "zap" : "user"} size={15} color={r.type === "auto" ? "var(--success)" : "var(--info)"} />
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 13, color: "var(--fg-primary)", fontWeight: 500 }}>
                    {fmtTs(r.started_at)} · {r.type === "auto" ? "авто" : "вручну"}{r.actor ? ` · ${r.actor}` : ""}
                    {!r.finished_at && <span style={{ color: "var(--warning)" }}> · не завершено</span>}
                    {!!r.stopped && <span style={{ color: "var(--fg-muted)" }}> · зупинено</span>}
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 2, fontFamily: "var(--font-mono)" }}>
                    оновлено {r.updated} · без змін {r.unchanged} · ревью {r.review} · пропущено {r.skipped}{r.failed ? ` · помилок ${r.failed}` : ""} · всього {r.total}
                  </div>
                </div>
                <div style={{ flex: 1 }} />
                <Icon name="chevron-right" size={15} color="var(--fg-muted)" />
              </div>
            ))}
          </div>
        )}

        {tab === "runs" && sel && (
          <>
            <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
              <button onClick={() => setSel(null)} style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 28, padding: "0 10px", borderRadius: 8, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", fontFamily: "inherit", fontSize: 12 }}>
                <Icon name="arrow-left" size={13} /> Всі запуски
              </button>
              {sel.run && <span style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>{fmtTs(sel.run.started_at)} · {sel.run.type === "auto" ? "авто" : "вручну"} · записано змін: <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{(sel.items || []).length}</b></span>}
            </div>
            <div style={{ flex: 1, overflow: "auto" }}>
              {sel.loading && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Вантажу…</div>}
              {sel.items && sel.items.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>У цьому запуску жодної зміни ціни не записано (все «без змін»/пропуски — вони в історію не пишуться).</div>}
              {sel.items && sel.items.map(it => <ItemRow key={it.id} it={it} />)}
            </div>
          </>
        )}

        {tab === "cards" && (() => {
          const SHOP = "https://digital-shop.com.ua";
          const ST = {
            queued:    ["clock", "var(--fg-muted)", "у черзі"],
            running:   ["loader", "var(--accent)", "генерується"],
            done:      ["package", "var(--warning)", "готова · чекає публікації"],
            error:     ["alert-triangle", "var(--danger)", "помилка генерації"],
            published: ["check-circle", "var(--success)", "на сайті"],
            dismissed: ["x", "var(--fg-muted)", "знято"],
          };
          const list = Array.isArray(cards) ? cards : [];
          // «Проблемна» картка: тонка галерея / непевна категорія / не пройшла авто-публікацію
          const isProblem = c => (c.status === "done" || c.status === "published") &&
            ((c.photos != null && c.photos <= 2) || (c.cat_conf != null && c.cat_conf < 0.6) || !!c.publish_error);
          const sups = [...new Set(list.map(c => c.supplier).filter(Boolean))].sort();
          const reasonKey = c => String(c.error || c.publish_error || "").trim().slice(0, 45);
          const matches = c =>
            (cardsSup === "all" || c.supplier === cardsSup) &&
            (!cardsBatch || c.batch_id === cardsBatch) &&
            (!cardsReason || reasonKey(c) === cardsReason);
          const base = list.filter(matches);
          const cnt = k => base.filter(c => k === "all" ? true : k === "work" ? (c.status === "queued" || c.status === "running") : k === "problem" ? isProblem(c) : c.status === k).length;
          const FILTERS = [["all", "Всі"], ["published", "На сайті"], ["done", "Чекають публікації"], ["problem", "⚠ Проблемні"], ["error", "Помилки"], ["work", "В роботі"], ["dismissed", "Зняті"]];
          const visible = base.filter(c => cardsFilter === "all" ? true : cardsFilter === "work" ? (c.status === "queued" || c.status === "running") : cardsFilter === "problem" ? isProblem(c) : c.status === cardsFilter);
          const dur = c => c.done_at && c.ts ? Math.max(1, Math.round((c.done_at - c.ts) / 1000)) : null;
          const totCost = base.reduce((s, c) => s + (Number(c.cost_usd) || 0), 0);
          // Партії (batch_id, запуски «Додавання авто») — воронка поставлено→згенеровано→на сайті
          const batches = (() => {
            const m = new Map();
            for (const c of list) {
              if (!c.batch_id) continue;
              if (cardsSup !== "all" && c.supplier !== cardsSup) continue;
              let b = m.get(c.batch_id);
              if (!b) m.set(c.batch_id, b = { id: c.batch_id, ts: c.ts, sup: c.supplier, total: 0, gen: 0, pub: 0, err: 0, wait: 0, work: 0, cost: 0 });
              b.total++; b.cost += Number(c.cost_usd) || 0; b.ts = Math.min(b.ts, c.ts);
              if (c.status === "published") { b.gen++; b.pub++; }
              else if (c.status === "done") { b.gen++; b.wait++; }
              else if (c.status === "dismissed") b.gen++;
              else if (c.status === "error") b.err++;
              else b.work++;
            }
            return [...m.values()].sort((a, b) => b.ts - a.ts);
          })();
          // Топ причин фейлів (генерація + авто-публікація) — клік фільтрує список
          const reasons = (() => {
            const m = new Map();
            for (const c of list.filter(c => (cardsSup === "all" || c.supplier === cardsSup) && (!cardsBatch || c.batch_id === cardsBatch))) {
              const r = reasonKey(c);
              if (r) m.set(r, (m.get(r) || 0) + 1);
            }
            return [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
          })();
          return (
            <>
              <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "10px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, flexWrap: "wrap" }}>
                {FILTERS.map(([k, l]) => (
                  <button key={k} onClick={() => setCardsFilter(k)} style={{ height: 26, padding: "0 10px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 11.5,
                    background: cardsFilter === k ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${cardsFilter === k ? "var(--accent)" : "var(--border-default)"}`, color: cardsFilter === k ? (k === "problem" ? "var(--warning)" : "var(--accent)") : "var(--fg-secondary)" }}>
                    {l} <b style={{ fontFamily: "var(--font-mono)", fontSize: 10.5 }}>{cnt(k)}</b>
                  </button>
                ))}
                {cardsFilter === "problem" && (() => {
                  const thin = visible.filter(c => c.status === "published" && c.photos != null && c.photos <= 2);
                  return thin.length > 0 ? (
                    <button onClick={() => !refilling && refillBulk(thin)} disabled={!!refilling}
                      title="Дошукати фото всім опублікованим з ≤2 фото: пошук джерел + vision, ~$0.01-0.02 і ~30-60с на товар, опис/назва не чіпаються"
                      style={{ height: 26, padding: "0 11px", borderRadius: 999, cursor: refilling ? "default" : "pointer", fontFamily: "inherit", fontSize: 11.5, background: "var(--accent)", border: "1px solid var(--accent)", color: "#fff", opacity: refilling ? .6 : 1 }}>
                      {refilling === "bulk" ? "Дозбираю…" : `📷 Дозібрати фото всім (${thin.length})`}
                    </button>
                  ) : null;
                })()}
                <div style={{ flex: 1 }} />
                {sups.length > 1 && (
                  <select value={cardsSup} onChange={e => setCardsSup(e.target.value)}
                    style={{ height: 26, padding: "0 6px", borderRadius: 8, border: "1px solid var(--border-default)", background: "var(--bg-base)", color: "var(--fg-secondary)", fontSize: 11.5, fontFamily: "inherit" }}>
                    <option value="all">всі постач.</option>
                    {sups.map(s => <option key={s} value={s}>{(PRICE_SUPS[s] || {}).label || s}</option>)}
                  </select>
                )}
                {[7, 30, 90].map(d => (
                  <button key={d} onClick={() => setCardsDays(d)} style={{ height: 26, padding: "0 9px", borderRadius: 999, cursor: "pointer", fontFamily: "var(--font-mono)", fontSize: 11,
                    background: cardsDays === d ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${cardsDays === d ? "var(--accent)" : "var(--border-default)"}`, color: cardsDays === d ? "var(--accent)" : "var(--fg-secondary)" }}>{d}д</button>
                ))}
                <span style={{ fontSize: 11, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>Σ ≈${totCost.toFixed(2)}</span>
              </div>
              {(cardsBatch || cardsReason) && (
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, flexWrap: "wrap", fontSize: 11.5 }}>
                  {cardsBatch && <span style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "var(--accent)", background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 999, padding: "2px 10px" }}>
                    партія {cardsBatch}<button onClick={() => setCardsBatch(null)} style={{ border: 0, background: "transparent", color: "inherit", cursor: "pointer", padding: 0, fontSize: 12 }}>✕</button></span>}
                  {cardsReason && <span style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "var(--warning)", background: "rgba(245,158,11,.12)", border: "1px solid var(--warning)", borderRadius: 999, padding: "2px 10px" }}>
                    причина: {cardsReason}…<button onClick={() => setCardsReason(null)} style={{ border: 0, background: "transparent", color: "inherit", cursor: "pointer", padding: 0, fontSize: 12 }}>✕</button></span>}
                </div>
              )}
              {refillMsg && (
                <div style={{ padding: "6px 18px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, fontSize: 11.5, color: "var(--fg-secondary)", fontFamily: "var(--font-mono)", display: "flex", alignItems: "center", gap: 8 }}>
                  {refilling && <Icon name="loader" size={12} color="var(--accent)" style={{ animation: "spin 1s linear infinite" }} />}
                  <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{refillMsg}</span>
                  {!refilling && <button onClick={() => setRefillMsg(null)} style={{ border: 0, background: "transparent", color: "var(--fg-muted)", cursor: "pointer", fontSize: 12, padding: 0 }}>✕</button>}
                </div>
              )}
              <div style={{ flex: 1, overflow: "auto" }}>
                {!cardsBatch && batches.length > 0 && (
                  <div style={{ padding: "10px 18px 4px" }}>
                    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", marginBottom: 6 }}>Партії «Додавання авто» — воронка</div>
                    {batches.slice(0, 8).map(b => {
                      const pct = b.total ? Math.round(b.pub / b.total * 100) : 0;
                      return (
                        <div key={b.id} onClick={() => setCardsBatch(b.id)} title="Клік — показати картки лише цієї партії"
                          style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 10px", marginBottom: 5, borderRadius: 9, border: "1px solid var(--border-subtle)", background: "var(--bg-raised)", cursor: "pointer", flexWrap: "wrap" }}>
                          <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)" }}>{fmtTs(b.ts)}</span>
                          <span style={{ fontSize: 11.5, color: "var(--fg-secondary)" }}>{(PRICE_SUPS[b.sup] || {}).label || b.sup}</span>
                          <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--fg-primary)", fontWeight: 600 }}>
                            {b.total} <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>постав.</span> → {b.gen} <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>згенер.</span> → <span style={{ color: "var(--success)" }}>{b.pub}</span> <span style={{ color: "var(--fg-muted)", fontWeight: 400 }}>на сайті</span>
                          </span>
                          {b.wait > 0 && <span style={{ fontSize: 10.5, color: "var(--warning)" }}>{b.wait} чекає публікації</span>}
                          {b.err > 0 && <span style={{ fontSize: 10.5, color: "var(--danger)" }}>{b.err} помилок</span>}
                          {b.work > 0 && <span style={{ fontSize: 10.5, color: "var(--accent)" }}>{b.work} в роботі</span>}
                          <div style={{ flex: 1 }} />
                          <span style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)" }}>{pct}% · ${b.cost.toFixed(2)}</span>
                        </div>
                      );
                    })}
                  </div>
                )}
                {reasons.length > 0 && !cardsReason && (
                  <div style={{ padding: "6px 18px 10px", display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
                    <span style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)" }}>Причини фейлів:</span>
                    {reasons.map(([r, n]) => (
                      <button key={r} onClick={() => setCardsReason(r)} title={"Клік — показати ці картки: " + r}
                        style={{ height: 24, padding: "0 9px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit", fontSize: 10.5, background: "var(--bg-base)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", maxWidth: 320, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                        {r}… <b style={{ fontFamily: "var(--font-mono)" }}>{n}</b>
                      </button>
                    ))}
                  </div>
                )}
                {cards === null && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Вантажу…</div>}
                {Array.isArray(cards) && visible.length === 0 && <div style={{ padding: 30, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Нічого за {cardsDays} дн. з цим фільтром.</div>}
                {visible.map(c => {
                  const st = ST[c.status] || ["package", "var(--fg-muted)", c.status];
                  const d = dur(c);
                  return (
                    <div key={c.job_id} style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "9px 18px", borderBottom: "1px solid var(--border-subtle)" }}>
                      <Icon name={st[0]} size={15} color={st[1]} style={{ flexShrink: 0, marginTop: 2 }} />
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 }}>
                          <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)", flexShrink: 0 }}>{fmtTs(c.ts)}</span>
                          <span onClick={() => { try { navigator.clipboard.writeText(c.article); } catch {} }} title="Клік — скопіювати артикул"
                            style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-secondary)", flexShrink: 0, cursor: "copy", textDecoration: "underline dotted" }}>{c.article}</span>
                          <span style={{ fontSize: 12.5, color: "var(--fg-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={c.name}>{c.name}</span>
                          {!!c.auto && <span style={{ fontSize: 10, color: "var(--success)", border: "1px solid var(--success)", borderRadius: 999, padding: "0 6px", flexShrink: 0 }}>авто</span>}
                          {c.status === "published" && c.article && (
                            <a href={SHOP + "/katalog/search/?q=" + encodeURIComponent(c.article)} target="_blank" rel="noreferrer"
                              style={{ fontSize: 11, color: "var(--accent)", flexShrink: 0, whiteSpace: "nowrap" }}>на сайті ↗</a>
                          )}
                        </div>
                        <div style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 2, lineHeight: 1.5 }}>
                          {c.price != null && <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>ціна {fmtUAHP(c.price)} · </span>}
                          {c.photos != null && <span style={{ fontFamily: "var(--font-mono)", color: c.photos <= 2 ? "var(--warning)" : "var(--fg-muted)", fontWeight: c.photos <= 2 ? 600 : 400 }}>📷{c.photos} · </span>}
                          {c.cat_conf != null && <span style={{ fontFamily: "var(--font-mono)", color: c.cat_conf < 0.6 ? "var(--warning)" : "var(--fg-muted)", fontWeight: c.cat_conf < 0.6 ? 600 : 400 }}>кат. {Math.round(c.cat_conf * 100)}% · </span>}
                          {c.cost_usd > 0 && <span style={{ fontFamily: "var(--font-mono)" }}>≈${Number(c.cost_usd).toFixed(3)} · </span>}
                          {d != null && <span style={{ fontFamily: "var(--font-mono)" }}>{d >= 60 ? Math.round(d / 60) + " хв" : d + " с"} · </span>}
                          {c.created_by && <span>{c.created_by} · </span>}
                          <span style={{ color: "var(--fg-disabled)" }}>{c.supplier}</span>
                          {c.batch_id && <span style={{ color: "var(--fg-disabled)" }}> · {c.batch_id}</span>}
                          {c.error && <div style={{ color: "var(--danger)" }}>помилка: {c.error}</div>}
                          {c.publish_error && <div style={{ color: "var(--warning)" }}>чому не авто-опубліковано: {c.publish_error}</div>}
                        </div>
                      </div>
                      <div style={{ display: "flex", flexDirection: "column", gap: 4, alignItems: "flex-end", flexShrink: 0 }}>
                        <span style={{ fontSize: 10.5, color: st[1], marginTop: 3, textTransform: "uppercase", letterSpacing: ".03em", whiteSpace: "nowrap" }}>{st[2]}</span>
                        {c.status === "published" && c.photos != null && c.photos <= 2 && (
                          <button onClick={() => { if (!refilling) { setRefilling(c.article); refillOne(c).finally(() => refillAliveRef.current && setRefilling(null)); } }} disabled={!!refilling}
                            title="Дошукати фото цьому товару (≈$0.01-0.02, ~30-60с; опис/назву не чіпає)"
                            style={{ height: 24, padding: "0 9px", borderRadius: 7, border: "1px solid var(--accent)", background: refilling === c.article ? "var(--accent)" : "transparent", color: refilling === c.article ? "#fff" : "var(--accent)", cursor: refilling ? "default" : "pointer", fontFamily: "inherit", fontSize: 10.5, whiteSpace: "nowrap", opacity: refilling && refilling !== c.article ? .5 : 1 }}>
                            {refilling === c.article ? "шукаю…" : "📷 Дозібрати"}
                          </button>
                        )}
                        {(c.status === "published" || c.status === "done" || c.status === "error" || c.status === "dismissed") && c.article && (
                          <button onClick={() => regenOne(c)}
                            title="Перегенерувати картку заново (нові джерела/опис/фото). Ціна й наявність лишаться поточні з сайту, стара галерея заміниться новою."
                            style={{ height: 24, padding: "0 9px", borderRadius: 7, border: "1px solid var(--border-default)", background: "transparent", color: "var(--fg-secondary)", cursor: "pointer", fontFamily: "inherit", fontSize: 10.5, whiteSpace: "nowrap" }}>
                            🔁 Заново
                          </button>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            </>
          );
        })()}

        {tab === "usage" && (
          <div style={{ flex: 1, overflow: "auto", padding: "14px 18px" }}>
            {usage === null && <div style={{ padding: 20, textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>Вантажу…</div>}
            {usage && usage.error && <div style={{ padding: 20, textAlign: "center", color: "var(--danger)", fontSize: 13 }}>Не вдалось завантажити</div>}
            {usage && !usage.error && (() => {
              const fmtTok = n => n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e3 ? Math.round(n / 1e3) + "K" : String(n || 0);
              const box = (title, a) => (
                <div style={{ flex: 1, minWidth: 190, background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 12, padding: "12px 14px" }}>
                  <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", marginBottom: 8 }}>{title}</div>
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 20, fontWeight: 700, color: "var(--fg-primary)" }}>${Number(a.llm.usd || 0).toFixed(2)}</div>
                  <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", marginTop: 6, lineHeight: 1.6 }}>
                    LLM-запитів: <b style={{ fontFamily: "var(--font-mono)" }}>{fmtBareP(a.llm.c || 0)}</b><br />
                    токени: <b style={{ fontFamily: "var(--font-mono)" }}>{fmtTok(a.llm.ti)} + {fmtTok(a.llm.tot)}</b><br />
                    пошук (Serper): <b style={{ fontFamily: "var(--font-mono)" }}>{fmtBareP(a.search.c || 0)}</b> запитів
                  </div>
                </div>
              );
              return (
                <>
                  <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
                    {box("Сьогодні", usage.today)}
                    {box(`За ${usage.days} днів`, usage.period)}
                    {box("За весь час", usage.allTime)}
                  </div>
                  <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", margin: "18px 0 8px" }}>По моделях (за {usage.days} днів)</div>
                  {(usage.byModel || []).map((m, i) => (
                    <div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "6px 0", borderBottom: "1px solid var(--border-subtle)", fontSize: 12 }}>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)", flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{m.provider} · {m.model || "—"}</span>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>{fmtBareP(m.c)} зап.</span>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>{fmtTok(m.ti)}+{fmtTok(m.tot)}</span>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)", fontWeight: 600, width: 70, textAlign: "right" }}>${Number(m.usd).toFixed(2)}</span>
                    </div>
                  ))}
                  <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", margin: "18px 0 8px" }}>По днях</div>
                  {(usage.byDay || []).slice(0, 14).map((d, i) => (
                    <div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "5px 0", borderBottom: "1px solid var(--border-subtle)", fontSize: 12 }}>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)", width: 84 }}>{d.day}</span>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>LLM {fmtBareP(d.llm)}</span>
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>пошук {fmtBareP(d.search)}</span>
                      <span style={{ flex: 1 }} />
                      <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)", fontWeight: 600 }}>${Number(d.usd).toFixed(2)}</span>
                    </div>
                  ))}
                </>
              );
            })()}
          </div>
        )}
      </div>
    </div>
  );
}

// Тимчасове перевизначення гарантії на час аудиту. ОСНОВНА гарантія постачальника
// задається в розділі «Постачальники» — тут лише разова правка для поточного додавання.
function SupWarrantyModal({ current, supLabel, baseWarranty, isOverride, onClose, onSave, onReset }) {
  const [months, setMonths] = useState(current && current.months ? String(current.months) : "");
  const [shopType, setShopType] = useState((current && current.shopType) || "Магазин");
  const inp = { width: "100%", boxSizing: "border-box", background: "var(--bg-base)", border: "1px solid var(--border-default)", borderRadius: 8, padding: "10px 12px", color: "var(--fg-primary)", fontSize: 14, fontFamily: "var(--font-mono)", outline: "none" };
  const lbl = { fontSize: 11, fontWeight: 500, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", marginBottom: 6, display: "block" };
  const save = () => onSave(Math.round(Number(String(months).replace(/[^\d]/g, "")) || 0), shopType);
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.6)", zIndex: 9900, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div onClick={e => e.stopPropagation()} style={{ width: "100%", maxWidth: 430, background: "var(--bg-base)", border: "1px solid var(--border-default)", borderRadius: 16, padding: 20, display: "flex", flexDirection: "column", gap: 14 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
          <Icon name="shield-check" size={18} color="var(--warning)" />
          <div style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)" }}>Гарантія тимчасово — {supLabel}</div>
        </div>
        <div style={{ fontSize: 12, color: "var(--fg-muted)", lineHeight: 1.45, marginTop: -4 }}>
          Тільки для цього аудиту (не змінює постачальника). <b>Постійну</b> гарантію задайте в розділі «Постачальники».
          {baseWarranty && baseWarranty.months ? <span> Зараз основна: <b style={{ color: "var(--fg-secondary)" }}>{baseWarranty.months} міс{baseWarranty.shopType ? ` · ${baseWarranty.shopType}` : ""}</b>.</span> : <span> Основну ще не задано.</span>}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
          <div>
            <label style={lbl}>Місяців</label>
            <input value={months} onChange={e => setMonths(e.target.value)} inputMode="numeric" placeholder="напр. 12" style={inp} />
          </div>
          <div>
            <label style={lbl}>Тип</label>
            <select value={shopType} onChange={e => setShopType(e.target.value)} style={{ ...inp, fontFamily: "inherit" }}>
              {["Магазин", "Виробник"].map(v => <option key={v} value={v}>{v}</option>)}
            </select>
          </div>
        </div>
        <div style={{ display: "flex", gap: 10, justifyContent: "flex-end", alignItems: "center" }}>
          {isOverride && <button onClick={onReset} style={{ marginRight: "auto", fontSize: 12, color: "var(--accent)", background: "transparent", border: 0, cursor: "pointer", fontFamily: "inherit" }}>↺ Повернути основну</button>}
          <Button variant="secondary" size="md" onClick={onClose}>Скасувати</Button>
          <Button variant="primary" size="md" leftIcon="check" onClick={save}>Застосувати тимчасово</Button>
        </div>
      </div>
    </div>
  );
}

function AuditScreen({ cbPrefix, isMobile, onClose, onToast }) {
  const s = PRICE_SUPS[cbPrefix] || { label: cbPrefix, color: "var(--accent)", source: "google_sheets", last: "—" };
  // ТЕСТ: авто-оновлення цін поки лише для Фокстрота (гейт по назві — стійко до cbPrefix на сервері)
  const isAutoPriceSupplier = /фокстрот|foxtrot/i.test((s.label || "") + " " + (cbPrefix || ""));
  const [autoWalk, setAutoWalk] = useState(null);   // null | "onsite" (лише товари на сайті) | "full" (всі, off-site → AI-генерація)
  const [autoRun, setAutoRun] = useState(false);    // авто-оновлення цін (без підтверджень, зі звітом)
  const [autoAdd, setAutoAdd] = useState(false);    // false | "pick" (вибір галочками) | rows[] (запущений раннер)
  const [reviewRows, setReviewRows] = useState(null); // список «на ревью» після авто-рана → walker по ньому
  const [reportsOpen, setReportsOpen] = useState(false); // звіти: запуски + історія по товару
  const [matchOpen, setMatchOpen] = useState(false);     // walker прив'язки схожих (назва ↔ сайт)
  const [walkInit, setWalkInit] = useState(null);   // {idx,done,skip} з якого монтуємо walker (продовження)
  const [resumeAsk, setResumeAsk] = useState(null); // {mode, saved} — діалог «Продовжити чи спочатку»
  const [siteMap, setSiteMap] = useState(null);   // Map(нормарт → ціна сайту); null = ще вантажиться
  const [siteCatalog, setSiteCatalog] = useState([]); // [{article,name,price,supplier,_set}] — для пошуку по назві
  const [siteBinds, setSiteBinds] = useState({});  // ручні прив'язки назва→артикул сайту (підтверджені)
  const [rows, setRows] = useState(() => buildAuditRows(cbPrefix, null, null));
  const [aiMap, setAiMap] = useState({});         // нормарт → статус черги генерації (queued|running|done|published)
  const [catalogNonce, setCatalogNonce] = useState(0); // бамп → перечитати каталог сайту (після публікації картки)
  const [supWarranty, setSupWarranty] = useState({});  // cbPrefix → {months, shopType} — ОСНОВНА гарантія (з розділу «Постачальники»)
  const [warrModal, setWarrModal] = useState(false);   // редактор тимчасового перевизначення
  const [warrOverride, setWarrOverride] = useState(null); // тимчасово на цей аудит (не змінює основну)
  const loadWarranty = () => fetch("/api/supplier-warranty").then(r => r.json()).then(d => { if (d && d.ok) setSupWarranty(d.data || {}); }).catch(() => {});
  useEffect(() => { loadWarranty(); }, []);
  const baseWarranty = supWarranty[cbPrefix] || null;                    // основна (постачальника)
  const effWarranty = warrOverride !== null ? warrOverride : baseWarranty; // що реально підставляється
  const [scanState, setScanState] = useState("idle"); // idle | scanning | done
  const [scanProg, setScanProg] = useState({ done: 0, total: 0 });
  const pollRef = useRef(null);
  const [filter, setFilter] = useState("all");
  const [expanded, setExpanded] = useState(null);
  const [hotById, setHotById] = useState({});
  const [refreshing, setRefreshing] = useState(false);
  const [conc, setConc] = useState(2); // паралельність вкладок воркера (з налаштувань сервера)
  useEffect(() => { fetch("/api/hl-worker/status").then(r => r.json()).then(d => { if (d && d.ok && d.concurrency) setConc(d.concurrency); }).catch(() => {}); }, []);
  const sum = useMemo(() => auditSummary(rows), [rows]);
  // Кандидати на прив'язку за назвою: товар без артикул-матчу на сайті + схожі назви в каталозі,
  // ще без рішення (не прив'язані і не приховані). Для кнопки «Прив'язати схожі (N)».
  const matchItems = useMemo(() => {
    if (!siteCatalog.length) return [];
    const out = [];
    for (const r of rows) {
      if (r.onSite || !(r.buy > 0)) continue;
      const key = audNameKey(r.name);
      const b = key ? siteBinds[key] : null;
      // Рішення людини (ручний бинд/dismiss) — не чіпаємо. АВТО-бинд, що так і не
      // зматчився з каталогом (r.onSite=false, ми тут), — пропонуємо привʼязати ще раз.
      if (b && (b.dismissed || (b.article && !b.auto))) continue;
      const cands = audSiteCandidates(r.name, siteCatalog, 5, r.art);
      if (cands.length) out.push({ row: r, cands });
    }
    return out;
  }, [rows, siteCatalog, siteBinds]);
  const etaMin = Math.max(1, Math.round(sum.positions * 2.5 / Math.max(1, conc) / 60));

  // Тягнемо каталог сайту один раз → будуємо мапу артикулів → перебудовуємо рядки зі статусом «є на сайті»
  useEffect(() => {
    let alive = true;
    fetch("/api/catalog/products").then(r => r.json()).then(j => {
      if (!alive) return;
      const m = new Map();
      // На сайті теж буває кілька артикулів в одному полі через кому («A17260Z1, A1728311»).
      // Індексуємо і склеєний ключ, і КОЖЕН токен окремо (по комі/;, НЕ по слешу — слеш є
      // частиною артикула: Philips 65PUS9010/12). Тоді match спрацює, скільки б артикулів не
      // мав постачальник чи сайт.
      const addKeys = (artStr, val) => {
        val = { ...val, siteArt: String(artStr || "").trim() }; // оригінальний артикул на сайті (для оновлення саме по ньому)
        const whole = audNormArt(artStr); if (whole) m.set(whole, val);
        for (const t of String(artStr || "").split(/[,;]+/)) { const k = audNormArt(t); if (k && k !== whole) m.set(k, val); }
      };
      const cat = [];  // плаский каталог для пошуку по назві (товари + модифікації)
      const pushCat = (art, name, price, supplier) => {
        const nm = String(name || "").trim();
        if (nm) cat.push({ article: String(art || "").trim(), name: nm, price: price || null, supplier: supplier || "", _set: new Set(audNameTokens(nm)) });
      };
      if (j && j.ok && Array.isArray(j.data)) {
        for (const cp of j.data) {
          addKeys(cp.article, { price: cp.price || null, supplier: cp.supplier || "" });
          pushCat(cp.article, cp.name, cp.price, cp.supplier);
          (cp.modifications || []).forEach(v => { addKeys(v.article, { price: v.price || cp.price || null, supplier: v.supplier || cp.supplier || "" }); pushCat(v.article, v.name || cp.name, v.price || cp.price, v.supplier || cp.supplier); });
        }
      }
      setSiteCatalog(cat);
      setSiteMap(m);
    }).catch(() => { if (alive) setSiteMap(new Map()); });
    return () => { alive = false; };
  }, [cbPrefix, catalogNonce]);

  // Підтягуємо ручні прив'язки назва→артикул (підтверджені раніше) — раз на монтування.
  useEffect(() => {
    let alive = true;
    fetch("/api/price-audit/site-bindings").then(r => r.json()).then(j => { if (alive && j && j.ok) setSiteBinds(j.data || {}); }).catch(() => {});
    return () => { alive = false; };
  }, []);

  // Перебудова рядків коли прийшла мапа сайту/прив'язки (статуси/наша ціна/маржа)
  useEffect(() => { if (siteMap) setRows(buildAuditRows(cbPrefix, siteMap, siteBinds)); }, [siteMap, siteBinds, cbPrefix]);

  // Статуси спільної черги генерації (новий → у черзі → генерується → готово → додано на сайт).
  // Реал-тайм: WS-подія `ai-queue-change` (з App.jsx) → перечитуємо чергу І каталог (щоб
  // щойно доданий товар став редагованим). Дедуп: двоє менеджерів бачать той самий статус.
  useEffect(() => {
    let alive = true;
    const loadAi = () => fetch("/api/ai-queue/list").then(r => r.json()).then(d => {
      if (!alive || !d || !d.ok) return;
      const m = {};
      for (const j of (d.jobs || [])) { const k = j.normArt || audNormArt(j.article); if (k) m[k] = j.status; }
      for (const p of (d.published || [])) { const k = audNormArt(p.art); if (k) m[k] = "published"; }
      setAiMap(m);
    }).catch(() => {});
    loadAi();
    const onChange = () => { loadAi(); setCatalogNonce(n => n + 1); };  // публікація → оновити й каталог
    window.addEventListener("ai-queue-change", onChange);
    const t = setInterval(loadAi, 15000);   // фолбек, якщо WS відвалився
    return () => { alive = false; window.removeEventListener("ai-queue-change", onChange); clearInterval(t); };
  }, [cbPrefix]);
  // Статус генерації для рядка: onSite/published мають пріоритет (товар уже/щойно на сайті).
  const aiStatusOf = (row) => {
    const k = audNormArt(row.art);
    if (!k) return null;
    if (row.onSite) return null;            // вже є на сайті (з каталогу) — звичайний режим
    return aiMap[k] || null;                // queued | running | done | published | undefined(новий)
  };

  const lkey = row => audNormArt(row.art) || row.key || "";
  const loadHot = (row, force) => {
    if (!force && hotById[row.id]) return;
    setHotById(h => ({ ...h, [row.id]: { loading: true } }));
    fetch("/api/price-audit/hotline?q=" + encodeURIComponent(row.name) + "&key=" + encodeURIComponent(lkey(row)))
      .then(r => r.json())
      .then(j => setHotById(h => ({ ...h, [row.id]: j && j.ok ? { loading: false, found: !!j.found, offers: j.offers || [], total: j.total || 0, url: j.url || null, error: !!j.error } : { loading: false, error: true } })))
      .catch(() => setHotById(h => ({ ...h, [row.id]: { loading: false, error: true } })));
  };
  const onEditLink = (row) => {
    const cur = (hotById[row.id] && hotById[row.id].url) || "";
    const url = window.prompt("Посилання на картку Hotline для цієї позиції (порожнє — скинути):", cur);
    if (url == null) return;
    fetch("/api/price-audit/link", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: lkey(row), url: url.trim() }) })
      .then(r => r.json())
      .then(j => { if (j && j.ok) { onToast && onToast(url.trim() ? "Посилання збережено — перевіряю…" : "Посилання скинуто"); loadHot(row, true); } else onToast && onToast("Помилка: " + ((j && j.error) || "")); })
      .catch(() => onToast && onToast("Помилка мережі"));
  };
  const doRefresh = () => {
    setRefreshing(true);
    fetch(`/api/price-cache/sync/${cbPrefix}`, { method: "POST" })
      .then(() => onToast && onToast("Оновлення прайсу запущено"))
      .catch(() => {})
      .finally(() => setTimeout(() => setRefreshing(false), 1600));
  };
  // Підтягнути каталог сайту з Horoshop (для товарів, доданих в іншій сесії/деінде) — щоб
  // вони перестали хибно бути «немає на сайті». Чекаємо рефреш → бампаємо nonce (siteMap перебудується).
  const [catRefreshing, setCatRefreshing] = useState(false);
  const doCatalogRefresh = () => {
    setCatRefreshing(true);
    fetch("/api/catalog/refresh", { method: "POST" })
      .then(r => r.json())
      .then(j => { onToast && onToast(j && j.ok ? `Каталог оновлено · ${j.count} товарів` : "Не вдалось оновити каталог"); setCatalogNonce(n => n + 1); })
      .catch(() => onToast && onToast("Помилка оновлення каталогу"))
      .finally(() => setCatRefreshing(false));
  };
  const onSaved = (id, newPrice, article) => {
    setRows(rs => rs.map(r => r.id === id ? { ...r, our: newPrice, margin: newPrice - r.buy, marginPct: newPrice > 0 ? ((newPrice - r.buy) / newPrice) * 100 : null } : r));
    // Патчимо siteMap у памʼяті (та сама ціна, що сервер записав у кеш каталогу) — щоб при будь-якій
    // перебудові рядків у цій сесії ціна НЕ відкотилась до старої. Мутуємо спільний val-обʼєкт
    // (усі ключі артикула вказують на нього), повертаємо ту саму мапу → зайвого ререндеру нема.
    const k = audNormArt(article);
    if (k) setSiteMap(m => { if (m && m.has(k)) m.get(k).price = newPrice; return m; });
  };
  // Ручна прив'язка товару (без артикула) до товару на сайті.
  // cand = {article,name,...} → підтвердити; action="dismiss" → «не той» (зберегти, щоб не нагадувати);
  // cand=null без action → зняти будь-яке рішення (відв'язати / показати підказку знову).
  const onBind = (row, cand, action) => {
    const key = audNameKey(row.name);
    if (!key) { onToast && onToast("❌ Порожня назва — нема за що прив'язати"); return Promise.resolve(); }
    const dismiss = action === "dismiss";
    const body = dismiss ? { key, dismissed: true } : { key, article: cand ? cand.article : "" };
    return fetch("/api/price-audit/site-bind", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })
      .then(r => r.json())
      .then(j => {
        if (!j || !j.ok) { onToast && onToast("❌ " + ((j && j.error) || "не вдалось зберегти")); return; }
        setSiteBinds(b => {
          const nb = { ...b };
          if (dismiss) nb[key] = { dismissed: true, ts: Date.now() };
          else if (cand) nb[key] = { article: cand.article, ts: Date.now() };
          else delete nb[key];
          return nb;
        });
        onToast && onToast(dismiss ? "Підказку приховано для цього товару" : (cand ? `🔗 Прив'язано до «${String(cand.name).slice(0, 40)}»` : "Скинуто"));
      })
      .catch(e => onToast && onToast("❌ " + e.message));
  };
  // Додавання картки з аудиту: відкриваємо вкладку «Товари» (TestPanel) з префілом артикула/назви/джерела.
  // Джерело: явно обране «Зробити джерелом» → постачальник сайту → за замовч. постачальник цього прайсу (cbPrefix).
  // manual=true — лише префіл форми без авто-пошуку джерел (режим «створити вручну»).
  const onAI = async (row, srcSup, manual) => {
    const sup = srcSup || row.siteSupPrefix || cbPrefix || "";
    const supLabel = (PRICE_SUPS[sup] || {}).label || sup;
    // Дедуп прямої генерації: «застовпити» артикул, щоб інші не задвоїли під час роботи в майстрі.
    let claimId = null;
    if (row.art) {
      try {
        const cj = await fetch("/api/ai-queue/claim", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ article: row.art, name: row.name, createdBy: localStorage.getItem("crm_user_name") || "" }) }).then(r => r.json());
        if (cj && cj.duplicate) {
          const st = cj.status === "running" ? "генерується" : cj.status === "queued" ? "у черзі" : cj.status === "published" ? "вже на сайті" : "в роботі";
          onToast && onToast(`⚠ «${(row.name || row.art).slice(0, 40)}» уже ${st}${cj.createdBy ? ` (${cj.createdBy})` : ""} — не дублюйте`);
          return;
        }
        claimId = cj && cj.jobId;
      } catch {}
    }
    // Закуп саме обраного джерела (для розрахунку маржі у майстрі)
    const offers = [{ sup: cbPrefix, uah: row.buy, price: row.buy, cur: "UAH" }, ...(row.others || [])];
    const off = offers.find(o => o.sup === sup) || offers.find(o => o.sup === cbPrefix);
    const payload = {
      article: artForCardP(row).toUpperCase(), name: row.name || "", supplierPrefix: sup,
      buy: off ? { uah: off.uah, price: off.price, cur: off.cur, supLabel } : null,
      hot: hotById[row.id] || null,
      autoSearch: !manual,
      claimId,                                            // звільниться при закритті майстра, якщо не опублікують
      // гарантія: тимчасове перевизначення (якщо задане) → інакше основна постачальника-джерела
      warranty: warrOverride !== null ? warrOverride : (supWarranty[sup] || supWarranty[cbPrefix] || null),
    };
    // Відкриваємо майстер ОВЕРЛЕЄМ поверх поточної вкладки (аудиту), без переходу на «Товари»
    if (window._openAddProductAI) window._openAddProductAI(payload);
    onToast && onToast(manual
      ? `Відкрито майстер додавання${sup ? ` · джерело: ${supLabel}` : ""}`
      : `Майстер AI${sup ? ` · джерело: ${supLabel}` : ""} — шукаю джерела…`);
  };

  // Список позицій обходу (той самий, що рендериться у walker) — для resume-розрахунку.
  const walkRows = (mode) => mode === "full"
    ? rows.filter(r => r.buy > 0 && !r.mdm)
    : rows.filter(r => r.onSite && r.buy > 0);
  // Старт обходу: якщо є збережений прогрес (idx>0) — спершу спитати «Продовжити / спочатку».
  const startWalk = (mode) => {
    const saved = loadWalkProgress(cbPrefix, mode);
    const total = walkRows(mode).length;
    if (saved && saved.idx > 0 && saved.idx < total) { setResumeAsk({ mode, saved }); return; }
    clearWalkProgress(cbPrefix, mode); setWalkInit({ idx: 0, done: 0, skip: 0 }); setAutoWalk(mode);
  };
  const beginWalk = (mode, init) => { setWalkInit(init); setAutoWalk(mode); setResumeAsk(null); };
  // «Продовжити»: шукаємо збережений артикул у поточному списку (порядок міг змінитись після оновлення прайсу).
  const resumeWalk = () => {
    const { mode, saved } = resumeAsk; const list = walkRows(mode);
    let idx = saved.idx;
    if (saved.art) { const f = list.findIndex(r => r.art === saved.art); if (f >= 0) idx = f; }
    idx = Math.min(Math.max(0, idx), Math.max(0, list.length - 1));
    beginWalk(mode, { idx, done: saved.done || 0, skip: saved.skip || 0 });
  };

  // Batch-скан Hotline: фоновий job на бекенді (паралельні вкладки) + опитування прогресу.
  // Скан живе на сервері — можна закрити аудит, він триватиме; при поверненні відновлюємо прогрес.
  const pollJob = (jobId) => {
    if (pollRef.current) clearInterval(pollRef.current);
    pollRef.current = setInterval(() => {
      fetch("/api/price-audit/scan/" + jobId).then(r => r.json()).then(st => {
        if (!st || !st.ok) { clearInterval(pollRef.current); pollRef.current = null; setScanState("done"); return; }
        setScanProg({ done: st.done, total: st.total });
        setHotById(h => {
          const n = { ...h };
          for (const id in st.results) { const r = st.results[id]; n[id] = { loading: false, found: !!r.found, offers: r.offers || [], total: r.total || 0, url: r.url || null, error: !!r.error, ts: Date.now() }; }
          return n;
        });
        if (st.done >= st.total) { clearInterval(pollRef.current); pollRef.current = null; setScanState("done"); }
      }).catch(() => {});
    }, 2000);
  };
  // Охоплення скану Hotline: не дьоргати воркерів по товарах, яких НІ В КОГО немає.
  //  mine — в наявності у ЦЬОГО постачальника; any — хоч у когось із постачальників; all — всі.
  const [scanScope, setScanScope] = useState(() => localStorage.getItem("audScanScope") || "mine");
  const scopeFilter = (r) => scanScope === "mine" ? !!r.stock
    : scanScope === "any" ? (!!r.stock || (r.others || []).some(o => o.stock))
    : true;
  const scopedRows = rows.filter(scopeFilter);
  const setScope = (v) => { setScanScope(v); try { localStorage.setItem("audScanScope", v); } catch {} };
  const runScan = (items) => {
    if (scanState === "scanning" || !items.length) return;
    setScanState("scanning"); setScanProg({ done: 0, total: items.length });
    setHotById(h => { const n = { ...h }; items.forEach(it => { if (!n[it.id] || !n[it.id].found) n[it.id] = { loading: true }; }); return n; });
    fetch("/api/price-audit/scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items, cbPrefix }) })
      .then(r => r.json())
      .then(j => { if (!j || !j.ok) { setScanState("done"); return; } pollJob(j.jobId); })
      .catch(() => setScanState("done"));
  };
  const startScan = () => runScan(scopedRows.map(r => ({ id: r.id, q: r.name, key: lkey(r) })).filter(it => it.q));
  // Позиції, де ціна НЕ витягнулась (помилка воркера/блокування/не знайдено) — часто друга
  // спроба дає результат (інший воркер, минуле вікно перевірки Hotline).
  const failedRows = scopedRows.filter(r => { const h = hotById[r.id]; return h && !h.loading && (h.error || !h.found); });
  const startScanFailed = () => runScan(failedRows.map(r => ({ id: r.id, q: r.name, key: lkey(r) })).filter(it => it.q));
  useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []);

  // Відновлення при відкритті: (1) збережені результати останньої перевірки з кешу,
  // (2) якщо скан цього постачальника ще триває на сервері — відновлюємо прогрес і опитування.
  useEffect(() => {
    if (!siteMap) return;
    const r = buildAuditRows(cbPrefix, siteMap, siteBinds);
    const keyToIds = {};
    r.forEach(row => { const k = lkey(row); if (k) (keyToIds[k] = keyToIds[k] || []).push(row.id); });
    const keys = Object.keys(keyToIds);
    if (keys.length) {
      fetch("/api/price-audit/cache", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ keys }) })
        .then(res => res.json()).then(j => {
          if (!j || !j.ok || !j.data) return;
          const upd = {}; let any = false;
          for (const k in j.data) { const e = j.data[k]; (keyToIds[k] || []).forEach(id => { upd[id] = { loading: false, found: !!e.found, offers: e.offers || [], total: e.total || 0, url: e.url || null, ts: e.ts, cached: true }; any = true; }); }
          if (any) { setHotById(h => ({ ...upd, ...h })); setScanState(s => s === "idle" ? "done" : s); }
        }).catch(() => {});
    }
    fetch("/api/price-audit/active/" + encodeURIComponent(cbPrefix)).then(res => res.json()).then(a => {
      if (a && a.ok && a.jobId) { setScanState("scanning"); setScanProg({ done: a.done || 0, total: a.total || 0 }); pollJob(a.jobId); }
    }).catch(() => {});
  }, [siteMap]);

  const FILTERS = [
    { key: "all", label: "Всі", count: rows.length },
    { key: "site", label: "На сайті", count: sum.onSite },
    { key: "missing", label: "Немає на сайті", count: sum.missing, tone: "warning" },
    { key: "up", label: "Подорожчало", count: sum.up, tone: "danger" },
    { key: "risk", label: "Маржа під загрозою", count: sum.risk, tone: "warning" },
  ];
  const pred = { all: () => true, site: r => r.onSite, missing: r => !r.onSite, up: r => r.buyChange === "up", risk: r => r.marginRisk }[filter] || (() => true);
  const visible = rows.filter(pred);
  const toggle = id => setExpanded(e => e === id ? null : id);
  const sourceCount = rows.filter(r => r.isSource).length;

  const Stat = ({ value, label, color, pulse }) => (
    <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 19, fontWeight: 700, color: color || "var(--fg-primary)", lineHeight: 1 }}>{fmtBareP(value)}</span>
        {pulse && value > 0 && <span className="pulse-dot" />}
      </div>
      <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>{label}</span>
    </div>
  );

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 50, background: "var(--bg-base)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <header style={{ background: "var(--bg-panel)", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, paddingTop: isMobile ? "env(safe-area-inset-top)" : 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 24px 12px" }}>
          <div style={{ width: 40, height: 40, borderRadius: 10, background: s.color + "29", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="tags" size={20} color={s.color} />
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <h1 style={{ fontSize: 17, fontWeight: 600, color: "var(--fg-primary)", margin: 0, whiteSpace: "nowrap" }}>{s.label}</h1>
              <span style={{ fontSize: 13, color: "var(--fg-muted)" }}>Аудит прайсу</span>
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 5, flexWrap: "wrap" }}>
              <SourceBadgeP source={s.source} size="sm" />
              <span style={{ fontSize: 11.5, color: "var(--fg-muted)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="clock" size={12} /> прайс: {s.last}</span>
              <span style={{ fontSize: 11.5, color: "var(--fg-secondary)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="star" size={12} color="var(--success)" /> джерело для <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{sourceCount}</b> позицій</span>
              <button onClick={() => setWarrModal(true)} title="Гарантія, що підставляється в товар. Основну задають у розділі «Постачальники»; тут — тимчасово на цей аудит." style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, color: warrOverride !== null ? "var(--warning)" : (effWarranty ? "var(--fg-secondary)" : "var(--accent)"), background: effWarranty || warrOverride !== null ? "var(--bg-raised)" : "transparent", border: "1px solid " + (warrOverride !== null ? "var(--warning)" : (effWarranty ? "var(--border-default)" : "var(--accent)")), borderRadius: 999, padding: "2px 9px", cursor: "pointer", fontFamily: "inherit" }}>
                <Icon name="shield-check" size={12} color={warrOverride !== null ? "var(--warning)" : (effWarranty ? "var(--success)" : "var(--accent)")} />
                {effWarranty && effWarranty.months ? `Гарантія: ${effWarranty.months} міс${effWarranty.shopType ? ` · ${effWarranty.shopType}` : ""}${warrOverride !== null ? " · тимчасово" : ""}` : "Гарантія: не задано"}
              </button>
            </div>
          </div>
          <div style={{ flex: 1 }} />
          <button onClick={onClose} title="Закрити аудит" style={{ width: 36, height: 36, border: "1px solid var(--border-default)", borderRadius: 8, background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="x" size={18} />
          </button>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 22, padding: "0 24px 14px", flexWrap: "wrap" }}>
          <Stat value={sum.positions} label="позицій у прайсі" />
          <div style={{ width: 1, height: 30, background: "var(--border-subtle)" }} />
          <Stat value={sum.onSite} label="на сайті" color="var(--success)" />
          <Stat value={sum.missing} label="нових / немає" color="var(--info)" />
          <Stat value={sum.up} label="подорожчало" color="var(--danger)" pulse />
          <Stat value={sum.risk} label="маржа під загрозою" color="var(--warning)" />
          <div style={{ flex: 1, minWidth: 20 }} />
          <Button variant="secondary" leftIcon={refreshing ? "loader" : "refresh-cw"} onClick={doRefresh} disabled={refreshing}>{refreshing ? "Оновлення…" : "Оновити прайс"}</Button>
          <Button variant="secondary" leftIcon={catRefreshing ? "loader" : "rotate-cw"} onClick={doCatalogRefresh} disabled={catRefreshing} title="Підтягнути товари, додані на сайт в іншій сесії">{catRefreshing ? "Каталог…" : "Оновити каталог"}</Button>
          {/* Авто-функції доступні для ВСІХ постачальників (раніше — лише Фокстрот-тест) */}
          <Button variant="primary" leftIcon="zap" onClick={() => setAutoRun(true)} disabled={siteMap === null || sum.onSite === 0} title="Само пройде всі товари на сайті: впевнені ціни застосує, спірні складе на ревью. Джерело: осн. склад у наявності → найдешевший у наявності; зміна джерела тягне його гарантію.">Оновлення авто</Button>
          <Button variant="secondary" leftIcon="flask-conical" onClick={() => setAutoRun("dry")} disabled={siteMap === null || sum.onSite === 0} title="Той самий прохід і ті самі рішення, але НІЧОГО не застосовується — просто звіт «що буде змінено». Для перевірки нового постачальника перед першим бойовим раном.">Прогноз</Button>
          <Button variant="primary" leftIcon="sparkles" onClick={() => {
            // Каталог порожній = кеш не прогрітий після рестарту → «нема на сайті» для ВСЬОГО.
            // Не даємо поставити в чергу сотні дублів існуючих товарів.
            if (!siteCatalog.length) { onToast && onToast("⛔ Каталог сайту не завантажений (0 товарів) — натисни «Оновити каталог» і спробуй знову"); return; }
            setAutoAdd("pick");
          }} disabled={siteMap === null || sum.missing === 0} title="Товари, яких немає на сайті: спершу вибір галочками → черга AI-генерації → авто-публікація після QA-гейтів (ціна з авто-цін)">Додавання авто</Button>
          <Button variant="secondary" leftIcon="wand-2" onClick={() => startWalk("full")} disabled={siteMap === null || sum.positions === 0} title="Покроковий обхід із підтвердженням по кожному товару (off-site → AI-картка)">Оновлення вручну</Button>
          {matchItems.length > 0 && (
            <Button variant="secondary" leftIcon="link" onClick={() => setMatchOpen(true)} title="Товари без артикул-матчу, але зі схожою назвою на сайті — пройтись і прив'язати кліком (є «Назад» для виправлення)">
              Прив'язати схожі ({matchItems.length})
            </Button>
          )}
          <Button variant="secondary" leftIcon="history" onClick={() => setReportsOpen(true)} title="Запуски оновлень + історія цін по товару (90 днів)">Звіти</Button>
          <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 3 }}>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <select value={scanScope} onChange={e => setScope(e.target.value)} disabled={scanState === "scanning"}
                title="Які позиції ганяти по Hotline: не дьоргаємо воркерів по товарах, яких ні в кого немає в наявності"
                style={{ height: 34, padding: "0 8px", borderRadius: 8, background: "var(--bg-base)", color: "var(--fg-secondary)", border: "1px solid var(--border-default)", fontFamily: "inherit", fontSize: 12, cursor: "pointer", maxWidth: 210 }}>
                <option value="mine">В наявності у постачальника ({rows.filter(r => r.stock).length})</option>
                <option value="any">В наявності хоч у когось ({rows.filter(r => r.stock || (r.others || []).some(o => o.stock)).length})</option>
                <option value="all">Всі позиції ({rows.length})</option>
              </select>
              {scanState === "done" && failedRows.length > 0 && (
                <Button variant="secondary" leftIcon="rotate-ccw" onClick={startScanFailed}
                  title="Повторна перевірка ЛИШЕ позицій без ціни (помилка воркера/блокування/не знайдено) — з другої спроби часто витягується">
                  Повторити невдалі ({failedRows.length})
                </Button>
              )}
              <Button variant="primary" leftIcon={scanState === "scanning" ? "loader" : scanState === "done" ? "rotate-cw" : "scan-line"} onClick={startScan} disabled={scanState === "scanning" || !scopedRows.length}>{scanState === "scanning" ? "Сканування…" : scanState === "done" ? "Пересканувати" : `Перевірити ціни (${scopedRows.length})`}</Button>
            </div>
            {siteMap === null
              ? <span style={{ fontSize: 11, color: "var(--fg-muted)", display: "inline-flex", alignItems: "center", gap: 4 }}><Icon name="loader" size={11} style={{ animation: "spin .8s linear infinite" }} /> звіряємо з сайтом…</span>
              : scanState === "idle" && <span style={{ fontSize: 11, color: "var(--fg-muted)", display: "inline-flex", alignItems: "center", gap: 4 }}><Icon name="timer" size={11} /> Hotline ~{etaMin} хв · {conc} {conc === 1 ? "вкладка" : "вкладки"}</span>}
          </div>
        </div>
        {scanState === "scanning" && (
          <div style={{ padding: "10px 24px 12px", borderTop: "1px solid var(--border-subtle)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 7 }}>
              <Icon name="loader" size={14} color="var(--accent)" style={{ animation: "spin .8s linear infinite" }} />
              <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Сканування Hotline… <span style={{ fontWeight: 400, color: "var(--fg-muted)" }}>({conc} {conc === 1 ? "вкладка" : "вкладки"} паралельно)</span></span>
              <div style={{ flex: 1 }} />
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, color: "var(--fg-primary)", fontWeight: 600 }}>{scanProg.done} / {scanProg.total}</span>
            </div>
            <div style={{ height: 6, background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 3, overflow: "hidden" }}>
              <div style={{ height: "100%", width: `${scanProg.total ? Math.round(scanProg.done / scanProg.total * 100) : 0}%`, background: "var(--accent)", borderRadius: 3, transition: "width 300ms" }} />
            </div>
          </div>
        )}
        {rows.length > 0 && (
          <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "11px 24px", borderTop: "1px solid var(--border-subtle)", flexWrap: "wrap" }}>
            <Icon name="list-filter" size={15} color="var(--fg-muted)" />
            {FILTERS.map(f => {
              const on = filter === f.key, c = { warning: "var(--warning)", danger: "var(--danger)" }[f.tone];
              return (
                <button key={f.key} onClick={() => setFilter(f.key)} style={{ display: "inline-flex", alignItems: "center", gap: 7, height: 30, padding: "0 12px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit",
                  background: on ? (c ? `color-mix(in oklab, ${c} 16%, transparent)` : "var(--accent-soft)") : "var(--bg-base)",
                  border: `1px solid ${on ? (c || "var(--accent)") : "var(--border-default)"}`, color: on ? (c || "var(--accent)") : "var(--fg-secondary)", fontSize: 12.5, fontWeight: 500, whiteSpace: "nowrap" }}>
                  {f.label}<span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700, color: on ? "inherit" : "var(--fg-muted)" }}>{f.count}</span>
                </button>
              );
            })}
          </div>
        )}
      </header>

      {siteMap === null ? (
        <div style={{ flex: 1, overflow: "auto", display: "flex", alignItems: "center", justifyContent: "center", padding: 32 }}>
          <div style={{ textAlign: "center", color: "var(--fg-muted)" }}>
            <Icon name="loader" size={30} color="var(--accent)" style={{ animation: "spin .8s linear infinite" }} />
            <div style={{ fontSize: 13.5, marginTop: 12 }}>Звіряємо прайс із сайтом…</div>
          </div>
        </div>
      ) : rows.length === 0 ? (
        <div style={{ flex: 1, overflow: "auto", display: "flex", alignItems: "center", justifyContent: "center", padding: 32 }}>
          <div style={{ textAlign: "center", color: "var(--fg-muted)", maxWidth: 420 }}>
            <Icon name="package-x" size={30} style={{ opacity: 0.5 }} />
            <div style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-secondary)", marginTop: 12 }}>Немає позицій у прайсі {s.label}</div>
            <div style={{ fontSize: 12.5, marginTop: 6, lineHeight: 1.5 }}>Оновіть прайс постачальника або перевірте, що файл завантажено.</div>
          </div>
        </div>
      ) : (
        <div style={{ flex: 1, overflow: "auto" }}>
          {/* Ціни Hotline ще не звірені — ненавʼязливе запрошення, таблиця вже доступна */}
          {scanState === "idle" && (
            <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 24px", background: "var(--accent-soft)", borderBottom: "1px solid var(--border-subtle)", flexWrap: "wrap" }}>
              <Icon name="info" size={16} color="var(--accent)" style={{ flexShrink: 0 }} />
              <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", flex: 1, minWidth: 200 }}>
                Товари показано. Ціни Hotline ще не звірені — колонку «Hotline» можна заповнити перевіркою (~{etaMin} хв).
              </span>
              <Button variant="primary" size="sm" leftIcon="scan-line" onClick={startScan}>Перевірити всі ціни</Button>
            </div>
          )}
          {!isMobile && (
            <div style={{ display: "flex", gap: 12, alignItems: "center", padding: "9px 24px", borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-panel)", position: "sticky", top: 0, zIndex: 2 }}>
              {AUD_COLS.map(([k, w]) => (
                <span key={k} style={{ ...audCw(w), fontSize: 10.5, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)", display: "flex", justifyContent: ["buy", "our", "hot"].includes(k) ? "flex-end" : "flex-start" }}>
                  {{ name: "Товар", buy: "Закупівля", our: "Наша ціна", hot: "Hotline", stock: "Наявн.", status: "Статус" }[k] || ""}
                </span>
              ))}
            </div>
          )}
          {visible.map(row => (
            <AudRow key={row.id} row={row} cbPrefix={cbPrefix} expanded={expanded === row.id} narrow={isMobile} aiStatus={aiStatusOf(row)} warranty={effWarranty}
              hot={hotById[row.id]} onToggle={toggle} onLoadHot={loadHot} onEditLink={onEditLink} onToast={onToast} onSaved={onSaved} onAI={onAI} siteCatalog={siteCatalog} onBind={onBind} />
          ))}
          {visible.length === 0 && (
            <div style={{ padding: "60px 24px", textAlign: "center", color: "var(--fg-muted)" }}>
              <Icon name="filter-x" size={26} style={{ opacity: 0.5 }} />
              <div style={{ fontSize: 13, marginTop: 10 }}>За цим фільтром позицій немає.</div>
            </div>
          )}
          <div style={{ height: 40 }} />
        </div>
      )}
      {autoWalk && (
        <AutoPriceWalker
          rows={walkRows(autoWalk)}                              // full: усі з закупом окрім MDM; onsite: лише на сайті
          initialIdx={walkInit ? walkInit.idx : 0}
          initialDone={walkInit ? walkInit.done : 0}
          initialSkip={walkInit ? walkInit.skip : 0}
          onProgress={(snap) => {
            if (snap.idx >= snap.total) clearWalkProgress(cbPrefix, autoWalk);              // дійшли до кінця — чистимо
            else if (snap.idx > 0 || snap.done > 0 || snap.skip > 0) saveWalkProgress(cbPrefix, autoWalk, snap);
          }}
          cbPrefix={cbPrefix} supLabel={s.label}
          prefetched={hotById} supWarranty={supWarranty} onUpdated={onSaved} onClose={() => setAutoWalk(null)} onToast={onToast} />
      )}
      {autoRun && (
        <AutoPriceRunner rows={walkRows("onsite")} cbPrefix={cbPrefix} supLabel={s.label} dryRun={autoRun === "dry"}
          prefetched={hotById} supWarranty={supWarranty} onUpdated={onSaved} onToast={onToast}
          onClose={() => setAutoRun(false)}
          onReview={(rws) => { setAutoRun(false); setReviewRows(rws); }} />
      )}
      {autoAdd === "pick" && (
        <AutoAddPicker rows={rows.filter(r => !r.onSite && r.buy > 0 && !r.mdm)} supLabel={s.label}
          cbPrefix={cbPrefix} prefetched={hotById}
          onClose={() => setAutoAdd(false)} onStart={(sel) => setAutoAdd(sel)} />
      )}
      {Array.isArray(autoAdd) && autoAdd.length > 0 && (
        <AutoAddRunner rows={autoAdd} cbPrefix={cbPrefix} supLabel={s.label}
          prefetched={hotById} aiMap={aiMap} onToast={onToast} onClose={() => setAutoAdd(false)} />
      )}
      {reviewRows && (
        <AutoPriceWalker rows={reviewRows} initialIdx={0} initialDone={0} initialSkip={0}
          cbPrefix={cbPrefix} supLabel={s.label + " · ревью"} prefetched={hotById} supWarranty={supWarranty} onUpdated={onSaved}
          onClose={() => setReviewRows(null)} onToast={onToast} />
      )}
      {reportsOpen && <PricingReports cbPrefix={cbPrefix} supLabel={s.label} onClose={() => setReportsOpen(false)} />}
      {matchOpen && <MatchWalker items={matchItems} supLabel={s.label} onBind={onBind} onClose={() => setMatchOpen(false)} />}
      {resumeAsk && (
        <div style={{ position: "fixed", inset: 0, zIndex: 70, background: "rgba(0,0,0,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }} onClick={() => setResumeAsk(null)}>
          <div style={{ width: "100%", maxWidth: 430, background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, padding: 24, boxShadow: "0 24px 60px rgba(0,0,0,.5)" }} onClick={e => e.stopPropagation()}>
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <Icon name="history" size={18} color="var(--accent)" />
              <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Продовжити обхід?</div>
            </div>
            <div style={{ fontSize: 13, color: "var(--fg-secondary)", marginTop: 10, lineHeight: 1.5 }}>
              Минулого разу зупинився на позиції <b style={{ color: "var(--fg-primary)" }}>№{resumeAsk.saved.idx + 1}</b>{resumeAsk.saved.total ? ` з ${resumeAsk.saved.total}` : ""}{resumeAsk.saved.art ? <> · <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{resumeAsk.saved.art}</span></> : null}. Оновлено: <b>{resumeAsk.saved.done || 0}</b> · пропущено: <b>{resumeAsk.saved.skip || 0}</b>.
            </div>
            <div style={{ display: "flex", gap: 8, marginTop: 18 }}>
              <Button variant="primary" leftIcon="play" style={{ flex: 1 }} onClick={resumeWalk}>Продовжити з №{resumeAsk.saved.idx + 1}</Button>
              <Button variant="secondary" leftIcon="rotate-ccw" onClick={() => { clearWalkProgress(cbPrefix, resumeAsk.mode); beginWalk(resumeAsk.mode, { idx: 0, done: 0, skip: 0 }); }}>Спочатку</Button>
            </div>
          </div>
        </div>
      )}
      {warrModal && <SupWarrantyModal current={effWarranty} supLabel={s.label} baseWarranty={baseWarranty} isOverride={warrOverride !== null}
        onClose={() => setWarrModal(false)}
        onReset={() => { setWarrOverride(null); setWarrModal(false); onToast && onToast("Повернено основну гарантію постачальника"); }}
        onSave={(months, shopType) => { setWarrOverride({ months, shopType }); setWarrModal(false); onToast && onToast(months ? `🛡 Гарантія тимчасово: ${months} міс (тільки цей аудит)` : "Тимчасово без гарантії"); }} />}
    </div>
  );
}

function PricesPage({ isMobile }) {
  const [view, setView] = useState("products");
  const [audit, setAudit] = useState(null); // cbPrefix постачальника для повноекранного аудиту
  const [query, setQuery] = useState("");
  const [active, setActive] = useState("all");
  const [selected, setSelected] = useState(null);
  const [linkTarget, setLinkTarget] = useState(null);
  const [suggestions, setSuggestions] = useState(null);
  const [sugLoading, setSugLoading] = useState(false);
  const [expanded, setExpanded] = useState({});
  const [checked, setChecked] = useState({});
  const [digestOn, setDigestOn] = useState(true);
  const [filters, setFilters] = useState({ sup:{}, match:{}, conf:{}, change:{}, stock:{}, cur:{} });
  const [wizard, setWizard] = useState(null);
  const [statuses, setStatuses] = useState({});
  const [toast, setToast] = useState(null);
  const [loading, setLoading] = useState(true);
  const [summary, setSummary] = useState(calcSummary([]));
  const [digest, setDigest] = useState(null);
  const [tick, setTick] = useState(0);
  const [pageLimit, setPageLimit] = useState(300);
  const [filterOpen, setFilterOpen] = useState(false);
  const pollRef = useRef(null);

  // ── Завантаження з API ──────────────────────────────────────────────────────
  const loadData = () => {
    Promise.all([
      fetch("/api/price-cache/status").then(r=>r.json()).catch(()=>null),
      fetch("/api/price-cache/products").then(r=>r.json()).catch(()=>null),
    ]).then(([statusRes, productsRes]) => {
      try {
        // Постачальники
        if (statusRes?.ok && Array.isArray(statusRes.data)) {
          const newSups = {};
          const newOrder = [];
          statusRes.data.forEach(s => {
            if (!s.cbPrefix) return; // пропускаємо записи без cbPrefix
            newSups[s.cbPrefix] = {
              label:     s.supplierName || s.cbPrefix,
              color:     genSupColor(s.cbPrefix),
              cur:       parseCurCode(s.currency),
              source:    "google_sheets",
              last:      fmtLastSync(s.lastSync),
              positions: s.positions ?? 0,
              changed:   s.changed ?? 0,
              status:    s.status ?? "empty",
              author:    "Google Sheets",
              diff:      s.diff ?? null,
            };
            newOrder.push(s.cbPrefix);
          });
          PRICE_SUPS = newSups;
          SUP_ORDER_P = newOrder;

          // Дайджест — останній синкований постачальник
          const lastSynced = statusRes.data.filter(s=>s.lastSyncTs&&s.cbPrefix).sort((a,b)=>b.lastSyncTs-a.lastSyncTs)[0];
          if (lastSynced) {
            setDigest({
              sup:    lastSynced.cbPrefix,
              when:   fmtLastSync(lastSynced.lastSync),
              counts: lastSynced.diff ?? { new:0, up:0, down:0, out:0 },
            });
          }

          // Статуси для PricesModeView
          const newStatuses = {};
          statusRes.data.forEach(s => { if (s.cbPrefix) newStatuses[s.cbPrefix] = s.status; });
          setStatuses(newStatuses);
        }

        // Товари
        if (productsRes?.ok && Array.isArray(productsRes.data)) {
          PRICE_PRODUCTS = deriveProducts(productsRes.data);
          setSummary(calcSummary(PRICE_PRODUCTS));
        }
      } catch(e) {
        console.error("[PricesPage] loadData error:", e);
      }

      setLoading(false);
      setTick(t=>t+1);
    }).catch(e => {
      console.error("[PricesPage] fetch error:", e);
      setLoading(false);
    });
  };

  useEffect(() => {
    loadData();
    return () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };
  }, []);

  useEffect(() => {
    const onRates = () => { RATES = loadRates(); setTick(t => t + 1); };
    window.addEventListener("crm-rates-updated", onRates);
    return () => window.removeEventListener("crm-rates-updated", onRates);
  }, []);

  const toggleFilter = (group, key) => { setFilters(f=>({ ...f, [group]:{ ...f[group], [key]:!f[group][key] } })); setPageLimit(300); };
  const resetFilters = () => { setFilters({ sup:{}, match:{}, conf:{}, change:{}, stock:{}, cur:{} }); setActive("all"); setQuery(""); setPageLimit(300); };

  const list = useMemo(()=>{
    const raw = query.trim().toLowerCase();
    const q = raw.length >= 3 ? raw : ""; // пошук від 3 символів (по 1 букві забагато позицій)
    const supSel   = Object.keys(filters.sup).filter(k=>filters.sup[k]);
    const matchSel = Object.keys(filters.match||{}).filter(k=>filters.match[k]);
    const chSel    = Object.keys(filters.change).filter(k=>filters.change[k]);
    const stSel    = Object.keys(filters.stock).filter(k=>filters.stock[k]);
    const curSel   = Object.keys(filters.cur).filter(k=>filters.cur[k]);
    const confSel  = Object.keys(filters.conf||{}).filter(k=>filters.conf[k]);
    return PRICE_PRODUCTS.filter(p=>{
      if (q && !(p.name.toLowerCase().includes(q)||p.art.toLowerCase().includes(q))) return false;
      if (active==="up"   && !p.anyUp)       return false;
      if (active==="down" && !p.anyDown)     return false;
      if (active==="new"  && !p.isNew)       return false;
      if (active==="out"  && !p.allOut)      return false;
      if (active==="risk" && !p.marginRisk)  return false;
      if (active==="check"&& p.confidence?.level!=="low") return false;
      // Зіставлення: кількість постачальників
      if (matchSel.includes("multi") && !matchSel.includes("single") && p.supCount < 2) return false;
      if (matchSel.includes("single") && !matchSel.includes("multi") && p.supCount >= 2) return false;
      if (confSel.length && !confSel.includes(p.confidence?.level)) return false;
      if (supSel.length && !p.offers.some(o=>supSel.includes(o.sup))) return false;
      if (chSel.length) {
        const ok=chSel.some(c=>c==="up"?p.anyUp:c==="down"?p.anyDown:c==="new"?p.isNew:c==="risk"?p.marginRisk:false);
        if (!ok) return false;
      }
      if (stSel.length) {
        const wantIn=stSel.includes("in"), wantOut=stSel.includes("out");
        if (wantIn&&!wantOut&&p.allOut) return false;
        if (wantOut&&!wantIn&&!p.allOut) return false;
      }
      if (curSel.length && !p.offers.some(o=>curSel.includes(o.cur))) return false;
      return true;
    });
  }, [query, active, filters, tick]);

  const riskCount = PRICE_PRODUCTS.filter(p=>p.marginRisk).length;
  const selCount  = Object.values(checked).filter(Boolean).length;
  const showToast = msg => { setToast(msg); setTimeout(()=>setToast(null), 3200); };

  // ── Ручна привʼязка/відвʼязка товарів ───────────────────────────────────────
  const postLink = (path, body, okMsg) =>
    fetch(`/api/price-cache/${path}`, { method:"POST", headers:{ "Content-Type":"application/json" }, body:JSON.stringify(body) })
      .then(r=>r.json())
      .then(res=>{
        if (res.ok) { showToast(okMsg); setSelected(null); setLinkTarget(null); loadData(); }
        else showToast(`Помилка: ${res.error||"невідома"}`);
      })
      .catch(e=>showToast(`Помилка: ${e.message}`));

  const doLink = (product, target) => {
    const ids = [...new Set([...(product.offerIds||[]), ...(target.offerIds||[])])];
    if (ids.length < 2) { showToast("Недостатньо оферів для привʼязки"); return; }
    postLink("link", { offerIds: ids }, "Товари привʼязано");
  };
  const doUnlink = (product, offer) => {
    if (!offer?.offerId) { showToast("Немає ідентифікатора офера"); return; }
    postLink("unlink", { offerId: offer.offerId }, "Офер відвʼязано");
  };
  const doBreak = (product) => {
    postLink("break", { offerIds: product.offerIds||[] }, "Звʼязок розірвано");
  };
  const doConfirm = (product) => {
    if ((product.offerIds||[]).length < 2) { showToast("Нема що підтверджувати"); return; }
    postLink("link", { offerIds: product.offerIds }, "Привʼязку підтверджено");
  };
  const doDismiss = (pair) => {
    fetch("/api/price-cache/dismiss", { method:"POST", headers:{ "Content-Type":"application/json" },
      body:JSON.stringify({ offerIdsA:pair.a.offerIds, offerIdsB:pair.b.offerIds }) })
      .then(r=>r.json()).then(res=>{ if(!res.ok) showToast(`Помилка: ${res.error||"невідома"}`); })
      .catch(e=>showToast(`Помилка: ${e.message}`));
  };

  // ── Перевірка привʼязок (підказки «можливі склейки») ─────────────────────────
  const loadSuggestions = () => {
    setSugLoading(true);
    fetch("/api/price-cache/suggestions").then(r=>r.json())
      .then(res=>setSuggestions(res.ok && Array.isArray(res.data) ? res.data : []))
      .catch(()=>setSuggestions([]))
      .finally(()=>setSugLoading(false));
  };
  useEffect(()=>{ if (view==="review" && suggestions===null) loadSuggestions(); }, [view]);
  const confirmPair = (pair) => {
    setSuggestions(s=>(s||[]).filter(x=>x.pairId!==pair.pairId));
    const ids = [...new Set([...(pair.a.offerIds||[]), ...(pair.b.offerIds||[])])];
    fetch("/api/price-cache/link", { method:"POST", headers:{ "Content-Type":"application/json" }, body:JSON.stringify({ offerIds:ids }) })
      .then(r=>r.json()).then(res=>{ if(res.ok){ showToast("Товари привʼязано"); loadData(); } else showToast(`Помилка: ${res.error||"невідома"}`); })
      .catch(e=>showToast(`Помилка: ${e.message}`));
  };
  const dismissSug = (pair) => {
    setSuggestions(s=>(s||[]).filter(x=>x.pairId!==pair.pairId));
    showToast("Підказку відхилено");
    doDismiss(pair);
  };

  const openWizard = (sup, broken=false) => setWizard({ sup, broken });
  // Поллінг поки є хоча б один parsing — зупиняється сам коли всі done
  const startPolling = () => {
    if (pollRef.current) return; // вже поллінгуємо
    let attempts = 0;
    const MAX = 60; // максимум 60 спроб × 5с = 5 хвилин
    pollRef.current = setInterval(() => {
      attempts++;
      fetch("/api/price-cache/status").then(r=>r.json()).then(res => {
        if (!res?.ok || !Array.isArray(res.data)) return;
        const stillParsing = res.data.some(s => s.status === "parsing");
        // Оновлюємо статуси в UI
        const newStatuses = {};
        res.data.forEach(s => { if (s.cbPrefix) newStatuses[s.cbPrefix] = s.status; });
        setStatuses(newStatuses);
        if (!stillParsing || attempts >= MAX) {
          clearInterval(pollRef.current);
          pollRef.current = null;
          loadData(); // фінальний повний reload
        }
      }).catch(() => {});
    }, 5000);
  };

  const saveWizard = () => {
    const sup = wizard?.sup;
    setWizard(null);
    showToast("Прайс оновлено — дані оновлюються…");
    if (sup) {
      setStatuses(s=>({ ...s, [sup]:"parsing" }));
      fetch(`/api/price-cache/sync/${sup}`, { method:"POST" })
        .then(() => startPolling())
        .catch(()=>{});
    }
  };

  // Рефреш статусу після ручного синку
  const handleSyncSupplier = (cbPrefix) => {
    setStatuses(s=>({ ...s, [cbPrefix]:"parsing" }));
    fetch(`/api/price-cache/sync/${cbPrefix}`, { method:"POST" })
      .then(() => startPolling())
      .catch(()=>{});
  };

  const filterCount = Object.values(filters).reduce((a,g)=>a+Object.values(g).filter(Boolean).length,0);
  const resetAll = () => { resetFilters(); setActive("all"); setQuery(""); };

  // ── Mobile branch ────────────────────────────────────────────────────────────
  if (isMobile) {
    return (
      <div style={{ flex:1, display:"flex", flexDirection:"column", minHeight:0, overflow:"hidden", position:"relative" }}>
        {audit && <AuditScreen cbPrefix={audit} isMobile={true} onClose={()=>setAudit(null)} onToast={showToast}/>}
        {loading ? (
          <div style={{ flex:1, display:"flex", alignItems:"center", justifyContent:"center", flexDirection:"column", gap:10 }}>
            <Icon name="loader" size={24} style={{ animation:"spin 0.8s linear infinite", color:"var(--fg-muted)" }}/>
            <span style={{ fontSize:13, color:"var(--fg-muted)" }}>Завантаження…</span>
          </div>
        ) : (
          <>
            <MHeaderP view={view} onView={v=>{setView(v);setSelected(null);}} onUpload={()=>setWizard({sup:null,broken:false})} reviewCount={suggestions?.length||0}/>
            {view==="review" ? (
              <ReviewViewMP suggestions={suggestions} loading={sugLoading} onConfirm={confirmPair} onDismiss={dismissSug}/>
            ) : view==="products" ? (
              <ProductsViewP
                list={list} total={PRICE_PRODUCTS.length}
                active={active} onPick={k=>setActive(a=>a===k?"all":k)}
                query={query} onQuery={q=>{setQuery(q);setPageLimit(300);}}
                onFilter={()=>setFilterOpen(true)} filterCount={filterCount}
                digestOn={digestOn} digest={digest}
                onDigest={k=>setActive(k)} onDismissDigest={()=>setDigestOn(false)}
                riskCount={riskCount} summaryData={summary}
                onOpen={setSelected} onResetAll={resetAll} pageLimit={pageLimit}
                onMorePage={()=>setPageLimit(l=>l+150)}/>
            ) : (
              <ListsViewP
                statuses={statuses}
                onSync={handleSyncSupplier}
                onUpload={sup=>setWizard({sup,broken:false})}
                onOpenBroken={sup=>setWizard({sup,broken:true})}
                onReload={loadData}
                onAudit={setAudit}
                onResetStuck={()=>{
                  fetch("/api/price-cache/reset-stuck",{method:"POST"})
                    .then(r=>r.json())
                    .then(res=>{showToast(res.message||"Скинуто");setTimeout(loadData,500);})
                    .catch(()=>showToast("Помилка скидання"));
                }}/>
            )}
          </>
        )}
        {filterOpen && (
          <FilterSheetMP filters={filters} onToggle={toggleFilter} onReset={resetFilters}
            onClose={()=>setFilterOpen(false)} count={list.length} total={PRICE_PRODUCTS.length}/>
        )}
        {selected && (
          <DetailSheetMP product={selected} onClose={()=>setSelected(null)} onToast={showToast}
            onLink={setLinkTarget} onBreak={doBreak} onUnlink={doUnlink} onConfirm={doConfirm}/>
        )}
        {linkTarget && (
          <LinkPickerSheetMP product={linkTarget} onClose={()=>setLinkTarget(null)} onLink={doLink}/>
        )}
        {wizard && (
          <UploadSheetMP sup={wizard.sup} startBroken={wizard.broken}
            onClose={()=>setWizard(null)} onSave={saveWizard}/>
        )}
        {toast && <ToastMP msg={toast}/>}
      </div>
    );
  }

  // ── Desktop branch ────────────────────────────────────────────────────────────
  return (
    <div style={{ flex:1, display:"flex", flexDirection:"column", minHeight:0, overflow:"hidden" }}>
      {audit && <AuditScreen cbPrefix={audit} isMobile={false} onClose={()=>setAudit(null)} onToast={showToast}/>}
      {/* Custom TopBar */}
      <div style={{ height:56, display:"flex", alignItems:"center", padding:"0 24px", gap:16, borderBottom:"1px solid var(--border-subtle)", background:"var(--bg-panel)", flexShrink:0 }}>
        <h1 style={{ fontSize:16, fontWeight:600, color:"var(--fg-primary)", margin:0 }}>Прайси</h1>
        <span style={{ fontSize:12, color:"var(--fg-muted)" }}>Закупівля</span>
        <div style={{ display:"flex", gap:2, marginLeft:8, padding:3, background:"var(--bg-base)", borderRadius:9, border:"1px solid var(--border-subtle)" }}>
          {[{ key:"products", label:"Товари", icon:"list" },{ key:"lists", label:"Прайси", icon:"files" },{ key:"review", label:"Перевірка", icon:"git-compare" }].map(t=>{
            const on=view===t.key;
            const badge = t.key==="review" && suggestions && suggestions.length>0 ? suggestions.length : null;
            return (
              <button key={t.key} onClick={()=>{ setView(t.key); setSelected(null); }} style={{
                display:"inline-flex", alignItems:"center", gap:7, height:30, padding:"0 14px", border:0, borderRadius:7,
                background:on?"var(--bg-raised)":"transparent", color:on?"var(--fg-primary)":"var(--fg-muted)",
                fontSize:12.5, fontWeight:600, cursor:"pointer", fontFamily:"inherit",
                boxShadow:on?"var(--shadow-1)":"none" }}>
                <Icon name={t.icon} size={14}/> {t.label}
                {badge!=null && <span style={{ fontFamily:"var(--font-mono)", fontSize:10.5, fontWeight:700, background:"var(--warning)", color:"#1a1a1a", borderRadius:999, minWidth:16, height:16, display:"inline-flex", alignItems:"center", justifyContent:"center", padding:"0 4px" }}>{badge}</span>}
              </button>
            );
          })}
        </div>
        <div style={{ flex:1 }}/>
        <Button size="sm" variant="ghost" leftIcon="refresh-cw" onClick={loadData}>Оновити</Button>
      </div>

      {/* Content */}
      {loading ? (
        <div style={{ flex:1, display:"flex", alignItems:"center", justifyContent:"center", flexDirection:"column", gap:12, color:"var(--fg-muted)" }}>
          <Icon name="loader" size={28} style={{ animation:"prices-spin 0.8s linear infinite" }}/>
          <span style={{ fontSize:13 }}>Завантаження прайсів…</span>
          <span style={{ fontSize:11.5 }}>Якщо це перший запуск — натисніть «Синхронізувати» в режимі Прайси</span>
        </div>
      ) : view==="review" ? (
        <ReviewView suggestions={suggestions} loading={sugLoading} onReload={loadSuggestions}
          onConfirm={confirmPair} onDismiss={dismissSug}/>
      ) : view==="lists" ? (
        <PricesModeView
          statuses={statuses}
          onUpload={handleSyncSupplier}
          onOpenBroken={sup=>openWizard(sup,true)}
          onReload={loadData}
          onAudit={setAudit}
          onResetStuck={()=>{
            fetch("/api/price-cache/reset-stuck",{method:"POST"})
              .then(r=>r.json())
              .then(res=>{ showToast(res.message||"Скинуто"); setTimeout(loadData, 500); })
              .catch(()=>showToast("Помилка скидання"));
          }}/>
      ) : (
        <>
          <SummaryBar active={active} onPick={k=>{ setActive(k); setSelected(null); }} summaryData={summary}/>
          {digest && digestOn && <DigestBanner digest={digest} onPick={k=>setActive(k)} onDismiss={()=>setDigestOn(false)} onOpenList={()=>setView("lists")}/>}
          <MarginRiskStrip count={riskCount} onShow={()=>setActive("risk")}/>
          <div style={{ marginTop:16, display:"flex", flexDirection:"column", flex:1, minHeight:0, borderTop:"1px solid var(--border-subtle)" }}>
            <PricesToolbar query={query} onQuery={q=>{ setQuery(q); setPageLimit(300); }} onUpload={()=>setView("lists")} selCount={selCount} onBulkClear={()=>setChecked({})}/>
            {selCount===0 && <FilterBarP filters={filters} onToggle={toggleFilter} onReset={resetFilters} count={list.length} total={PRICE_PRODUCTS.length}/>}
            <div style={{ flex:1, minWidth:0, display:"flex", flexDirection:"column", minHeight:0 }}>
              <div style={{ flex:1, overflow:"auto" }}>
                <PTableHeader/>
                {PRICE_PRODUCTS.length===0 ? (
                  <div style={{ display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", padding:"72px 24px", gap:14 }}>
                    <div style={{ width:56, height:56, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}>
                      <Icon name="package-x" size={26} color="var(--fg-muted)"/>
                    </div>
                    <div style={{ textAlign:"center" }}>
                      <div style={{ fontSize:14, fontWeight:600, color:"var(--fg-primary)" }}>Кеш прайсів порожній</div>
                      <div style={{ fontSize:12.5, color:"var(--fg-muted)", marginTop:5 }}>Перейдіть у вкладку «Прайси» і натисніть «Оновити» поруч з постачальником</div>
                    </div>
                    <Button variant="secondary" leftIcon="files" onClick={()=>setView("lists")}>Перейти до Прайсів</Button>
                  </div>
                ) : list.length===0 ? (
                  <EmptyStateP query={query} onReset={resetFilters}/>
                ) : list.slice(0, pageLimit).map((p, i)=>(
                  <GroupRowP key={`${i}-${p.art||p.name}`} p={p}
                    selected={selected?.id===p.id}
                    checked={!!checked[p.id]}
                    expanded={!!expanded[p.id]}
                    onSelect={setSelected}
                    onCheck={id=>setChecked(c=>({ ...c, [id]:!c[id] }))}
                    onToggle={id=>setExpanded(x=>({ ...x, [id]:!x[id] }))}
                    onLink={setLinkTarget} onUnlink={doUnlink}
                    dense={false}/>
                ))}
                {list.length > pageLimit && (
                  <div style={{ padding:"14px 24px", textAlign:"center" }}>
                    <Button variant="secondary" leftIcon="chevrons-down" onClick={()=>setPageLimit(l=>l+300)}>
                      Показати ще ({list.length - pageLimit} з {list.length})
                    </Button>
                  </div>
                )}
              </div>
              <div style={{ display:"flex", alignItems:"center", gap:8, padding:"8px 24px", borderTop:"1px solid var(--border-subtle)", fontSize:11.5, color:"var(--fg-muted)", background:"var(--bg-panel)", flexShrink:0 }}>
                <Icon name="list" size={13}/> показано {Math.min(list.length, pageLimit)} з {list.length} · всього {PRICE_PRODUCTS.length}
                <div style={{ flex:1 }}/>
                {digest && <span>оновлено {digest.when.toLowerCase()}</span>}
              </div>
            </div>
          </div>
        </>
      )}

      {selected && <DetailPanelP product={selected} onClose={()=>setSelected(null)} onToast={showToast}
        onLink={setLinkTarget} onBreak={doBreak} onUnlink={doUnlink} onConfirm={doConfirm}/>}
      {linkTarget && <LinkPickerModalP product={linkTarget} onClose={()=>setLinkTarget(null)} onLink={doLink}/>}
      {wizard && <UploadWizard sup={wizard.sup} startBroken={wizard.broken} onClose={()=>setWizard(null)} onSave={saveWizard}/>}
      {toast && (
        <div style={{ position:"fixed", bottom:24, left:"50%", transform:"translateX(-50%)", zIndex:60, display:"flex", alignItems:"center", gap:10, padding:"12px 18px", background:"var(--bg-raised)", border:"1px solid var(--border-default)", borderRadius:10, boxShadow:"var(--shadow-2)", animation:"prices-sheetup 200ms ease" }}>
          <Icon name="check-circle" size={17} color="var(--success)"/> <span style={{ fontSize:13, color:"var(--fg-primary)" }}>{toast}</span>
        </div>
      )}
    </div>
  );
}

// ── Mobile components (адаптовано з m-parts/m-products/m-detail/m-lists.jsx) ──

function ToastMP({ msg }) {
  if (!msg) return null;
  return (
    <div style={{ position:"absolute", bottom:24, left:"50%", transform:"translateX(-50%)", zIndex:50,
      display:"flex", alignItems:"center", gap:10, padding:"11px 16px", borderRadius:11, maxWidth:"88%",
      background:"var(--bg-raised)", border:"1px solid var(--border-default)", boxShadow:"var(--shadow-2)",
      animation:"toastInM 220ms cubic-bezier(.2,0,0,1)", whiteSpace:"nowrap" }}>
      <Icon name="check-circle" size={16} color="var(--success)"/>
      <span style={{ fontSize:12.5, color:"var(--fg-primary)", fontWeight:500 }}>{msg}</span>
    </div>
  );
}

function BottomSheetP({ onClose, heightPct=92, children, padGrab=true }) {
  const [drag, setDrag] = useState(0);
  const start = useRef(null);
  const onDown = e => { start.current = (e.touches?e.touches[0].clientY:e.clientY); };
  const onMove = e => { if(start.current==null) return; const y=(e.touches?e.touches[0].clientY:e.clientY); setDrag(Math.max(0,y-start.current)); };
  const onUp   = () => { if(drag>110) onClose(); setDrag(0); start.current=null; };
  return (
    <div style={{ position:"absolute", inset:0, zIndex:30, display:"flex", flexDirection:"column", justifyContent:"flex-end" }}>
      <div onClick={onClose} style={{ position:"absolute", inset:0, background:"rgba(0,0,0,.6)", animation:"fade 160ms" }}/>
      <div style={{ position:"relative", height:`${heightPct}%`, maxHeight:"94%", background:"var(--bg-panel)",
        borderTopLeftRadius:18, borderTopRightRadius:18, borderTop:"1px solid var(--border-default)",
        display:"flex", flexDirection:"column", overflow:"hidden", transform:`translateY(${drag}px)`,
        transition:start.current==null?"transform 200ms cubic-bezier(.2,0,0,1)":"none",
        animation:"msheetUp 240ms cubic-bezier(.2,0,0,1)" }}>
        <div onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp}
          onTouchStart={onDown} onTouchMove={onMove} onTouchEnd={onUp}
          style={{ padding:padGrab?"10px 0 6px":0, cursor:"grab", flexShrink:0 }}>
          <div style={{ width:36, height:4, borderRadius:2, background:"var(--border-strong)", margin:"0 auto" }}/>
        </div>
        {children}
      </div>
    </div>
  );
}

function MHeaderP({ view, onView, onUpload, reviewCount }) {
  const tabs = [{key:"products",label:"Товари",icon:"list"},{key:"lists",label:"Прайси",icon:"files"},{key:"review",label:"Перевірка",icon:"git-compare"}];
  return (
    <div style={{ paddingTop:"env(safe-area-inset-top, 0px)", background:"var(--bg-base)", flexShrink:0 }}>
    <div style={{ padding:"6px 16px 8px" }}>
      <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:10 }}>
        <h1 style={{ fontSize:16, fontWeight:600, color:"var(--fg-primary)", margin:0 }}>Прайси</h1>
        <span style={{ fontSize:12, color:"var(--fg-muted)" }}>Закупівля</span>
        <div style={{ flex:1 }}/>
        <button onClick={onUpload} style={{ display:"inline-flex", alignItems:"center", gap:6, height:34, padding:"0 12px", borderRadius:9, border:0, background:"var(--accent)", color:"#fff", fontFamily:"inherit", fontSize:12.5, fontWeight:600, cursor:"pointer" }}>
          <Icon name="upload" size={15}/> Прайс
        </button>
      </div>
      <div style={{ display:"flex", gap:3, padding:3, background:"var(--bg-panel)", borderRadius:11, border:"1px solid var(--border-subtle)" }}>
        {tabs.map(t=>{
          const on=view===t.key;
          const badge = t.key==="review" && reviewCount>0 ? reviewCount : null;
          return (
            <button key={t.key} onClick={()=>onView(t.key)} style={{ flex:1, display:"inline-flex", alignItems:"center", justifyContent:"center", gap:5, height:36, border:0, borderRadius:8,
              background:on?"var(--bg-raised)":"transparent", color:on?"var(--fg-primary)":"var(--fg-muted)",
              fontSize:12.5, fontWeight:600, cursor:"pointer", fontFamily:"inherit", boxShadow:on?"var(--shadow-1)":"none" }}>
              <Icon name={t.icon} size={15}/> {t.label}
              {badge!=null && <span style={{ fontFamily:"var(--font-mono)", fontSize:10, fontWeight:700, background:"var(--warning)", color:"#1a1a1a", borderRadius:999, minWidth:15, height:15, display:"inline-flex", alignItems:"center", justifyContent:"center", padding:"0 4px" }}>{badge}</span>}
            </button>
          );
        })}
      </div>
    </div>
    </div>
  );
}

function SummaryChipsMp({ active, onPick, summaryData }) {
  const tone = { neutral:"var(--fg-primary)", muted:"var(--fg-muted)", danger:"var(--danger)", warning:"var(--warning)", success:"var(--success)", info:"#3B82F6" };
  return (
    <div style={{ display:"flex", gap:8, overflowX:"auto", padding:"0 16px 2px", scrollbarWidth:"none" }}>
      {(summaryData||[]).map(t=>{
        const on = active===t.key && t.key!=="all";
        return (
          <button key={t.key} onClick={()=>onPick(active===t.key?"all":t.key)} style={{ flexShrink:0, textAlign:"left", padding:"10px 13px", borderRadius:12, cursor:"pointer", background:"var(--bg-panel)", fontFamily:"inherit", minWidth:100,
            border:`1px solid ${on?"var(--accent)":"var(--border-subtle)"}`,
            boxShadow:on?"0 0 0 3px var(--accent-soft)":"none" }}>
            <div style={{ display:"flex", alignItems:"center", gap:6 }}>
              <span style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:19, fontWeight:700, color:tone[t.tone]||"var(--fg-primary)" }}>{fmtBareP(t.value)}</span>
              {t.pulse && t.value>0 && <span style={{ width:6, height:6, borderRadius:"50%", background:"var(--danger)", animation:"blink 1.1s ease-in-out infinite", display:"inline-block" }}/>}
            </div>
            <div style={{ fontSize:11, color:"var(--fg-secondary)", marginTop:3, lineHeight:1.25 }}>{t.label}</div>
          </button>
        );
      })}
    </div>
  );
}

function DigestMP({ digest, onOpen, onDismiss }) {
  if (!digest) return null;
  const s = PRICE_SUPS[digest.sup];
  const seg = [
    {key:"new",  v:digest.counts?.new||0,  color:"#3B82F6",      icon:"sparkle"},
    {key:"up",   v:digest.counts?.up||0,   color:"var(--danger)", icon:"arrow-up-right"},
    {key:"down", v:digest.counts?.down||0, color:"var(--success)",icon:"arrow-down-right"},
    {key:"out",  v:digest.counts?.out||0,  color:"var(--fg-muted)",icon:"package-x"},
  ];
  return (
    <div style={{ margin:"0 16px", padding:"11px 12px", background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderLeft:"3px solid var(--accent)", borderRadius:12 }}>
      <div style={{ display:"flex", alignItems:"center", gap:9 }}>
        <div style={{ width:28, height:28, borderRadius:8, background:"var(--accent-soft)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}>
          <Icon name="history" size={15} color="var(--accent)"/>
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontSize:12.5, fontWeight:600, color:"var(--fg-primary)" }}>Останнє завантаження{s ? <span style={{ color:s.color }}> · {s.label}</span> : null}</div>
          <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:1 }}>{digest.when}</div>
        </div>
        <button onClick={onDismiss} style={{ width:26, height:26, border:0, borderRadius:6, background:"transparent", color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}>
          <Icon name="x" size={15}/>
        </button>
      </div>
      <div style={{ display:"flex", gap:7, marginTop:10, overflowX:"auto", scrollbarWidth:"none" }}>
        {seg.map(x=>(
          <button key={x.key} onClick={()=>onOpen(x.key)} style={{ flexShrink:0, display:"inline-flex", alignItems:"center", gap:6, height:30, padding:"0 11px", borderRadius:8, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit" }}>
            <Icon name={x.icon} size={12} color={x.color}/>
            <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, fontSize:12.5, color:x.color }}>{x.v}</span>
            <span style={{ fontSize:11, color:"var(--fg-secondary)" }}>{{new:"нових",up:"дорожче",down:"дешевше",out:"зникли"}[x.key]}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function SearchMP({ value, onChange, onFilter, filterCount }) {
  return (
    <div style={{ display:"flex", gap:8, padding:"0 16px" }}>
      <div style={{ position:"relative", flex:1 }}>
        <Icon name="search" size={15} style={{ position:"absolute", left:12, top:"50%", transform:"translateY(-50%)", color:"var(--accent)" }}/>
        <input value={value} onChange={e=>onChange(e.target.value)} placeholder="Товар під замовлення…" style={{
          width:"100%", height:42, boxSizing:"border-box", padding:"0 12px 0 36px",
          background:"var(--bg-panel)", color:"var(--fg-primary)", border:"1px solid var(--border-default)",
          borderRadius:11, fontSize:14, outline:"none", fontFamily:"inherit" }}/>
      </div>
      <button onClick={onFilter} style={{ position:"relative", width:42, height:42, flexShrink:0, borderRadius:11, cursor:"pointer",
        background:filterCount?"var(--accent-soft)":"var(--bg-panel)",
        border:`1px solid ${filterCount?"var(--accent)":"var(--border-default)"}`,
        color:filterCount?"var(--accent)":"var(--fg-secondary)", display:"flex", alignItems:"center", justifyContent:"center" }}>
        <Icon name="sliders-horizontal" size={17}/>
        {filterCount>0 && <span style={{ position:"absolute", top:-6, right:-6, minWidth:18, height:18, padding:"0 4px", borderRadius:999, background:"var(--accent)", color:"#fff", fontSize:10.5, fontWeight:700, fontFamily:"var(--font-mono)", display:"flex", alignItems:"center", justifyContent:"center", border:"2px solid var(--bg-base)" }}>{filterCount}</span>}
      </button>
    </div>
  );
}

function ProductCardMP({ p, onOpen }) {
  const mColor = marginColorP(p.margin, p.marginPct);
  return (
    <button onClick={()=>onOpen(p)} style={{ width:"100%", textAlign:"left", display:"flex", gap:12, alignItems:"stretch",
      padding:13, borderRadius:12, cursor:"pointer", fontFamily:"inherit",
      background:"var(--bg-panel)",
      boxShadow:p.confidence?.level==="low"?"inset 3px 0 0 var(--warning)":"none",
      border:`1px solid ${p.marginRisk?"rgba(244,63,94,.28)":"var(--border-subtle)"}` }}>
      <div style={{ flex:1, minWidth:0, display:"flex", flexDirection:"column", gap:7 }}>
        <div style={{ display:"flex", alignItems:"flex-start", gap:6 }}>
          <span style={{ flex:1, fontSize:13.5, fontWeight:500, color:"var(--fg-primary)", lineHeight:1.32, display:"-webkit-box", WebkitLineClamp:2, WebkitBoxOrient:"vertical", overflow:"hidden" }}>{p.name}</span>
          {p.isNew && <span style={{ flexShrink:0, fontSize:9, fontWeight:700, color:"#3B82F6", background:"rgba(59,130,246,.13)", borderRadius:4, padding:"2px 5px", letterSpacing:".03em" }}>NEW</span>}
        </div>
        <div style={{ display:"flex", alignItems:"center", gap:8 }}>
          <span style={{ fontFamily:"var(--font-mono)", fontSize:11, color:"var(--fg-muted)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{p.art}</span>
          <span style={{ fontSize:11, color:"var(--fg-disabled)" }}>·</span>
          <span style={{ fontSize:11, color:"var(--fg-muted)", whiteSpace:"nowrap" }}>{p.supCount} пост.</span>
          <ConfidenceBadgeP p={p} interactive={false}/>
        </div>
        <div style={{ display:"flex", alignItems:"center", gap:7, flexWrap:"wrap", marginTop:1 }}>
          <span style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:14.5, fontWeight:700, color:"var(--fg-primary)" }}>{fmtCurP(p.best.price,p.best.cur)}</span>
          <SupplierChipP sup={p.best.sup} size="sm" label={p.best.supplierName}/>
          <ChangeBadgeP change={p.bestChange} pct={p.best.deltaPct}/>
        </div>
      </div>
      <div style={{ display:"flex", flexDirection:"column", alignItems:"flex-end", justifyContent:"space-between", flexShrink:0 }}>
        <div style={{ display:"flex", alignItems:"center", gap:4 }}>
          {p.marginRisk && <Icon name="alert-triangle" size={13} color="var(--danger)"/>}
          {p.allOut
            ? <span style={{ display:"inline-flex", alignItems:"center", gap:4, fontSize:11, color:"var(--fg-muted)" }}><span style={{ width:6, height:6, borderRadius:"50%", background:"var(--fg-disabled)" }}/> немає</span>
            : <span style={{ display:"inline-flex", alignItems:"center", gap:4, fontSize:11, color:"var(--fg-secondary)" }}><span style={{ width:6, height:6, borderRadius:"50%", background:"var(--success)" }}/><span style={{ fontFamily:"var(--font-mono)" }}>{p.inStockCount}/{p.supCount}</span></span>}
        </div>
        <div style={{ height:20, display:"flex", alignItems:"center", margin:"4px 0" }}>
          <SparklineP data={p.bestHist} w={46} h={20}/>
        </div>
        <div style={{ textAlign:"right" }}>
          <div style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:13, fontWeight:600, color:"var(--fg-secondary)" }}>{fmtUAHP(p.our)}</div>
          <div style={{ fontFamily:"var(--font-mono)", fontSize:11.5, fontWeight:600, color:mColor }}>{fmtPctP(p.marginPct)}</div>
        </div>
      </div>
    </button>
  );
}

function ProductsViewP({ list, total, active, onPick, query, onQuery, onFilter, filterCount, digestOn, digest, onDigest, onDismissDigest, riskCount, summaryData, onOpen, onResetAll, pageLimit, onMorePage }) {
  return (
    <div style={{ flex:1, display:"flex", flexDirection:"column", minHeight:0 }}>
      <div style={{ display:"flex", flexDirection:"column", gap:10, paddingBottom:10, flexShrink:0 }}>
        {digestOn && digest && <DigestMP digest={digest} onOpen={onDigest} onDismiss={onDismissDigest}/>}
        <SummaryChipsMp active={active} onPick={onPick} summaryData={summaryData}/>
        <SearchMP value={query} onChange={onQuery} onFilter={onFilter} filterCount={filterCount}/>
        {riskCount>0 && active!=="risk" && (
          <button onClick={()=>onPick("risk")} style={{ display:"flex", alignItems:"center", gap:10, margin:"0 16px", padding:"10px 13px", background:"rgba(244,63,94,.10)", border:"1px solid rgba(244,63,94,.28)", borderRadius:11, cursor:"pointer", fontFamily:"inherit", textAlign:"left" }}>
            <Icon name="trending-up" size={16} color="var(--danger)" style={{ flexShrink:0 }}/>
            <span style={{ flex:1, fontSize:12.5, color:"var(--fg-primary)" }}><b style={{ fontWeight:600 }}>{riskCount}</b> {riskCount===1?"товар подорожчав":"товари подорожчали"} — маржа під загрозою</span>
            <Icon name="chevron-right" size={15} color="var(--danger)"/>
          </button>
        )}
      </div>
      <div style={{ flex:1, overflow:"auto", padding:"0 16px 24px", display:"flex", flexDirection:"column", gap:9, scrollbarWidth:"none" }}>
        <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", padding:"2px 2px 0" }}>
          <span style={{ fontSize:11.5, color:"var(--fg-muted)" }}><span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-secondary)", fontWeight:600 }}>{list.length}</span> з {total}</span>
          {(filterCount>0||query||active!=="all") && <button onClick={onResetAll} style={{ background:"transparent", border:0, color:"var(--accent)", fontFamily:"inherit", fontSize:12, cursor:"pointer", padding:0 }}>Скинути</button>}
        </div>
        {list.length===0 ? (
          <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:12, padding:"56px 24px", textAlign:"center" }}>
            <div style={{ width:52, height:52, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="search-x" size={24} color="var(--fg-muted)"/></div>
            <div>
              <div style={{ fontSize:13.5, fontWeight:600, color:"var(--fg-primary)" }}>{query?`Нічого за «${query}»`:"Немає позицій за фільтрами"}</div>
              <div style={{ fontSize:12, color:"var(--fg-muted)", marginTop:4 }}>Спробуйте змінити запит або скинути фільтри</div>
            </div>
            <Button variant="secondary" leftIcon="rotate-ccw" onClick={onResetAll}>Скинути все</Button>
          </div>
        ) : list.slice(0,pageLimit).map(p=><ProductCardMP key={p.id} p={p} onOpen={onOpen}/>)}
        {list.length>pageLimit && (
          <button onClick={onMorePage} style={{ height:40, border:"1px solid var(--border-default)", background:"var(--bg-panel)", color:"var(--fg-secondary)", borderRadius:10, fontSize:13, fontFamily:"inherit", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center", gap:6 }}>
            <Icon name="chevrons-down" size={14}/> Ще ({list.length-pageLimit})
          </button>
        )}
      </div>
    </div>
  );
}

const M_FILTER_DEFS_P = {
  sup:    { label:"Постачальник", icon:"truck",       get options() { return SUP_ORDER_P.map(k=>({key:k,label:PRICE_SUPS[k]?.label||k,color:PRICE_SUPS[k]?.color})); } },
  conf:   { label:"Точність",     icon:"shield-check",options:[{key:"high",label:"Точний збіг"},{key:"medium",label:"За моделлю"},{key:"low",label:"За назвою"},{key:"manual",label:"Підтверджено"}] },
  change: { label:"Зміна ціни",   icon:"trending-up", options:[{key:"up",label:"Подорожчало"},{key:"down",label:"Подешевшало"},{key:"new",label:"Нові позиції"},{key:"risk",label:"Маржа під загрозою"}] },
  stock:  { label:"Наявність",    icon:"package",     options:[{key:"in",label:"В наявності"},{key:"out",label:"Немає в наявності"}] },
  cur:    { label:"Валюта",       icon:"coins",       options:[{key:"UAH",label:"Гривня ₴"},{key:"USD",label:"Долар $"},{key:"EUR",label:"Євро €"}] },
};

function FilterSheetMP({ filters, onToggle, onReset, onClose, count, total }) {
  const activeCount = Object.values(filters).reduce((a,g)=>a+Object.values(g).filter(Boolean).length,0);
  return (
    <BottomSheetP onClose={onClose} heightPct={84}>
      <div style={{ display:"flex", alignItems:"center", gap:8, padding:"4px 12px 12px 16px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <h3 style={{ fontSize:15, fontWeight:600, margin:0, color:"var(--fg-primary)" }}>Фільтри</h3>
        {activeCount>0 && <span style={{ fontFamily:"var(--font-mono)", fontSize:11, fontWeight:700, background:"var(--accent)", color:"#fff", borderRadius:999, minWidth:18, height:18, display:"inline-flex", alignItems:"center", justifyContent:"center", padding:"0 5px" }}>{activeCount}</span>}
        <div style={{ flex:1 }}/>
        {activeCount>0 && <button onClick={onReset} style={{ background:"transparent", border:0, color:"var(--accent)", fontFamily:"inherit", fontSize:13, cursor:"pointer" }}>Скинути</button>}
        <button onClick={onClose} style={{ width:32, height:32, border:0, background:"transparent", color:"var(--fg-secondary)", borderRadius:6, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={18}/></button>
      </div>
      <div style={{ flex:1, overflow:"auto", padding:"16px 16px 8px", scrollbarWidth:"none" }}>
        {Object.keys(M_FILTER_DEFS_P).map(group=>{
          const def = M_FILTER_DEFS_P[group];
          return (
            <div key={group} style={{ marginBottom:20 }}>
              <div style={{ display:"flex", alignItems:"center", gap:7, marginBottom:10 }}>
                <Icon name={def.icon} size={14} color="var(--fg-muted)"/>
                <span style={{ fontSize:11, fontWeight:600, letterSpacing:".06em", textTransform:"uppercase", color:"var(--fg-muted)" }}>{def.label}</span>
              </div>
              <div style={{ display:"flex", flexWrap:"wrap", gap:8 }}>
                {def.options.map(o=>{
                  const on=!!(filters[group]||{})[o.key];
                  return (
                    <button key={o.key} onClick={()=>onToggle(group,o.key)} style={{ display:"inline-flex", alignItems:"center", gap:7, height:38, padding:"0 14px", borderRadius:10, cursor:"pointer", fontFamily:"inherit",
                      background:on?"var(--accent-soft)":"var(--bg-base)",
                      border:`1px solid ${on?"var(--accent)":"var(--border-default)"}`,
                      color:on?"var(--accent)":"var(--fg-secondary)", fontSize:13, fontWeight:500 }}>
                      {group==="sup" && o.color && <span style={{ width:8, height:8, borderRadius:"50%", background:o.color }}/>}
                      {o.label}
                      {on && <Icon name="check" size={14}/>}
                    </button>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
      <div style={{ padding:14, borderTop:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <Button variant="primary" leftIcon="check" onClick={onClose} style={{ width:"100%" }}>Показати {count} з {total}</Button>
      </div>
    </BottomSheetP>
  );
}

function SupplierCompareRowMP({ o, isBest, isSource, onMakeSource, disableSource, canUnlink, onUnlink, onCheck, checking }) {
  const s = PRICE_SUPS[o.sup];
  const [confirm, setConfirm] = useState(false);
  return (
    <div style={{ padding:"11px 12px", borderRadius:11,
      background:isSource?"var(--accent-soft)":isBest?"rgba(16,185,129,.07)":"var(--bg-base)",
      border:`1px solid ${isSource?"var(--accent)":isBest?"rgba(16,185,129,.28)":"var(--border-subtle)"}` }}>
      <div style={{ display:"flex", alignItems:"center", gap:7, marginBottom:9, flexWrap:"wrap" }}>
        <SupplierChipP sup={o.sup} label={o.supplierName}/>
        {o.ref && <RefChipP/>}
        {isBest && <BestBadgeP size="sm"/>}
        {isSource && <span style={{ fontSize:9.5, fontWeight:700, color:"var(--accent)", background:"var(--bg-panel)", border:"1px solid var(--accent)", borderRadius:5, padding:"2px 6px", display:"inline-flex", alignItems:"center", gap:3 }}><Icon name="globe" size={10}/> на сайті</span>}
        <div style={{ flex:1 }}/>
        <SourceBadgeP source={s?.source||"google_sheets"} size="sm"/>
        {canUnlink && (
          <button onClick={()=>setConfirm(true)} title="Відвʼязати" style={{ width:34, height:34, border:0, borderRadius:8, background:"transparent", color:"var(--fg-muted)", cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}>
            <Icon name="unlink" size={15}/>
          </button>
        )}
      </div>
      {canUnlink && confirm && (
        <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:9, padding:"9px 11px", background:"var(--bg-base)", border:"1px solid var(--border-default)", borderRadius:9 }}>
          <span style={{ flex:1, fontSize:11.5, color:"var(--fg-secondary)" }}>Винести в окрему картку?</span>
          <Button size="sm" variant="ghost" onClick={()=>setConfirm(false)}>Ні</Button>
          <Button size="sm" variant="danger" onClick={()=>{ setConfirm(false); onUnlink&&onUnlink(); }}>Відвʼязати</Button>
        </div>
      )}
      {(o.offerName||o.offerArt) && (
        <div style={{ marginBottom:9, padding:"7px 9px", background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:8 }}>
          <div style={{ fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.35, wordBreak:"break-word" }}>{o.offerName||"—"}</div>
          {o.offerArt && <div style={{ fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--fg-muted)", marginTop:3 }}>арт: {o.offerArt}</div>}
        </div>
      )}
      <div style={{ display:"flex", alignItems:"flex-end", gap:10 }}>
        <div style={{ minWidth:0 }}>
          <div style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:19, fontWeight:700, color:isBest?"var(--success)":"var(--fg-primary)", lineHeight:1.1 }}>{fmtCurP(o.price,o.cur)}</div>
          {o.cur!=="UAH" && <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:2, fontFamily:"var(--font-mono)" }}>≈ {fmtUAHP(o.uah)}</div>}
        </div>
        <div style={{ flex:1 }}/>
        <div style={{ display:"flex", flexDirection:"column", alignItems:"flex-end", gap:5 }}>
          <ChangeBadgeP change={o.change} pct={o.deltaPct}/>
          <StockDotP stock={o.stock} qty={o.qty}/>
          <SparklineP data={o.hist} w={50} h={18}/>
        </div>
      </div>
      <div style={{ display:"flex", justifyContent:"flex-end", gap:8, marginTop:9 }}>
        {!isSource && !disableSource && <button onClick={onMakeSource} style={{ display:"inline-flex", alignItems:"center", gap:5, height:30, padding:"0 11px", borderRadius:8, background:"var(--bg-raised)", border:"1px solid var(--border-default)", color:"var(--fg-secondary)", fontFamily:"inherit", fontSize:11.5, fontWeight:500, cursor:"pointer" }}><Icon name="globe" size={12}/> Джерело</button>}
        {!isSource && disableSource && <span style={{ display:"inline-flex", alignItems:"center", gap:4, height:30, fontSize:11, color:"var(--fg-disabled)" }}><Icon name="lock" size={12}/> MDM</span>}
        <button onClick={()=>onCheck&&onCheck(o.offerArt, o.sup, o.offerName)} disabled={checking} style={{ display:"inline-flex", alignItems:"center", gap:5, height:30, padding:"0 11px", borderRadius:8, background:"var(--accent-soft)", border:"1px solid var(--accent)", color:"var(--accent)", fontFamily:"inherit", fontSize:11.5, fontWeight:600, cursor:checking?"default":"pointer", opacity:checking?0.6:1 }}><Icon name="search" size={12}/> Уточнити</button>
      </div>
    </div>
  );
}

function SectionLabelMP({ children, right }) {
  return (
    <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:9 }}>
      <span style={{ fontSize:11, fontWeight:600, letterSpacing:".06em", textTransform:"uppercase", color:"var(--fg-muted)" }}>{children}</span>
      {right}
    </div>
  );
}

function DetailSheetMP({ product, onClose, onToast, onLink, onBreak, onUnlink, onConfirm }) {
  const isMdm = !!product.mdm;
  const [boundSup, setBoundSup] = useState(defaultSourceSupP(product));
  const [siteSup, setSiteSup] = useState(null);
  const [sellPrice, setSellPrice] = useState(product.our||0);
  const [siteBasePrice, setSiteBasePrice] = useState(product.our||0);   // реальна ціна на сайті — база для «ціну змінено»
  const [published, setPublished] = useState(false);
  const [chartOpen, setChartOpen] = useState(false);
  const [siteArticle, setSiteArticle] = useState("");

  useEffect(()=>{
    setBoundSup(defaultSourceSupP(product)); setSiteSup(null); setSellPrice(product.our||0); setSiteBasePrice(product.our||0); setPublished(false); setSiteArticle("");
    // Кандидати: базовий + артикули всіх постачальників (offerArt); перший знайдений на сайті визначає привʼязку.
    const isRealSku = a => a && !String(a).includes(" ") && String(a).length <= 30;
    const cand = [];
    const push = a => { const s = String(a||"").trim(); if (isRealSku(s) && !cand.includes(s)) cand.push(s); };
    push(product.artRaw); push(product.art);
    for (const o of (product.offers||[])) push(o.offerArt);
    if (!cand.length) return;
    let alive = true;
    (async () => {
      for (const a of cand) {
        if (!alive) return;
        try {
          const res = await fetch(`/api/horoshop/product?article=${encodeURIComponent(a)}`).then(r=>r.json());
          if (!alive) return;
          if (res.ok && res.found) {
            setSiteArticle(a);
            if (res.data?.price) { setSellPrice(res.data.price); setSiteBasePrice(res.data.price); }
            if (res.data?.supplierPrefix){ setSiteSup(res.data.supplierPrefix); setBoundSup(res.data.supplierPrefix); }
            return;
          }
        } catch {}
      }
    })();
    return () => { alive = false; };
  }, [product.id]);

  const sortedOffers = [...product.offers].sort((a,b)=>a.uah-b.uah);
  const supArticles = sortedOffers
    .map(o => ({ sup:o.sup, art:String(o.offerArt||"").trim(), label:(PRICE_SUPS[o.sup]||{}).label||o.sup }))
    .filter(a => a.art);
  const headArt = siteArticle || product.artRaw || product.art || "";
  const boundOffer   = product.offers.find(o=>o.sup===boundSup)||product.best;
  const buyUAH       = boundOffer.uah;
  const liveMargin   = sellPrice-buyUAH;
  const liveMarginPct= sellPrice?(liveMargin/sellPrice)*100:0;
  const mColor       = marginColorP(liveMargin, liveMarginPct);
  const targetSell   = Math.round(buyUAH/(1-TARGET_MARGIN/100));
  const baseSup      = siteSup||defaultSourceSupP(product);
  const priceChangedSite = sellPrice!==siteBasePrice;
  const supChangedSite   = boundSup!==baseSup;
  const dirty        = supChangedSite||priceChangedSite;
  const siteChangeWhat  = priceChangedSite&&supChangedSite ? "ціну і джерело" : supChangedSite ? "джерело" : "ціну";
  const siteChangeBadge = priceChangedSite&&supChangedSite ? "ціна, джерело" : supChangedSite ? "джерело" : "ціна";

  const publish = () => {
    if (isMdm) { onToast && onToast("⛔ MDM — заблокований пристрій: публікація на сайт заборонена"); return; }
    const pubArt = siteArticle || product.artRaw || product.art || "";
    if (!pubArt) { onToast && onToast("❌ Немає артикулу для оновлення"); return; }
    const wasSupChanged = supChangedSite;
    setPublished(true);
    fetch('/api/horoshop/update-price',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({article:pubArt,price:sellPrice,sourceSup:boundSup})})
      .then(r=>r.json())
      .then(res=>{ if(res.ok) setSiteSup(boundSup); onToast&&onToast(res.ok?`✅ Оновлено на сайті · ${fmtUAHP(sellPrice)}${wasSupChanged?` · джерело: ${PRICE_SUPS[boundSup]?.label||boundSup}`:""}`:`❌ Помилка: ${res.error||'невідома'}`); })
      .catch(e=>{ onToast&&onToast(`❌ ${e.message}`); })
      .finally(()=>setTimeout(()=>setPublished(false),1600));
  };

  // Уточнення наявності — як /find (CRM-проксі → бот /internal/check)
  const [checking, setChecking] = useState(false);
  // sup (опц.) — cbPrefix офера: «Уточнити» біля конкретного постачальника пінгує САМЕ його
  const checkAvail = (article, sup, offerTitle) => {
    const art = String(article || headArt || product.art || "").trim();
    if (!art) { onToast && onToast("❌ Немає артикулу для перевірки"); return; }
    setChecking(true);
    onToast && onToast(`⏳ Уточнюю наявність · ${art}${sup ? " · " + ((PRICE_SUPS[sup]||{}).label||sup) : ""}…`);
    fetch('/api/requests/check-availability', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ article: art, managerName: (localStorage.getItem('crm_user_name')||''), supplier: sup || undefined, title: offerTitle || product.name || undefined }) })
      .then(r=>r.json())
      .then(res=>{ onToast && onToast(res && res.label ? res.label : (res && res.found ? "⏳ Запит відправлено постачальнику" : "❌ Товар не знайдено")); })
      .catch(e=>{ onToast && onToast(`❌ ${e.message}`); })
      .finally(()=>setChecking(false));
  };

  const chipBtn = on => ({ display:"inline-flex", alignItems:"center", gap:5, height:32, padding:"0 11px", borderRadius:8, cursor:"pointer", fontFamily:"inherit", fontSize:12, fontWeight:500, background:on?"var(--accent-soft)":"var(--bg-base)", color:on?"var(--accent)":"var(--fg-secondary)", border:`1px solid ${on?"var(--accent)":"var(--border-default)"}` });

  return (
    <BottomSheetP onClose={onClose} heightPct={94} padGrab={false}>
      <div style={{ paddingTop:8 }}>
        <div style={{ width:36, height:4, borderRadius:2, background:"var(--border-strong)", margin:"0 auto 10px" }}/>
      </div>
      <div style={{ padding:"0 16px 14px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:9 }}>
          <span style={{ fontSize:11, fontWeight:600, color:"var(--fg-muted)", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:6, padding:"3px 8px" }}>{product.cat||"Товар"}</span>
          {product.isNew && <span style={{ fontSize:10, fontWeight:700, color:"#3B82F6", background:"rgba(59,130,246,.13)", borderRadius:5, padding:"3px 7px" }}>НОВЕ</span>}
          <FlagBadgeP mdm={product.mdm} refurb={product.ref}/>
          <div style={{ flex:1 }}/>
          <button onClick={onClose} style={{ width:32, height:32, border:0, background:"var(--bg-base)", color:"var(--fg-secondary)", borderRadius:8, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={17}/></button>
        </div>
        <h2 style={{ fontSize:15.5, fontWeight:600, margin:0, color:"var(--fg-primary)", lineHeight:1.32 }}>{product.name}</h2>
        <div style={{ display:"flex", alignItems:"center", gap:12, marginTop:6 }}>
          {headArt && <span style={{ fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--fg-muted)" }}>Арт: {headArt}</span>}
          <span style={{ fontSize:11.5, color:"var(--fg-muted)" }}>{product.supCount} постачальників</span>
        </div>
        {supArticles.length > 0 && (
          <div style={{ display:"flex", flexWrap:"wrap", gap:6, marginTop:8 }}>
            {supArticles.map((a,i)=>(
              <span key={a.sup+"-"+i} style={{ display:"inline-flex", alignItems:"center", gap:5, padding:"3px 7px", borderRadius:6, background:a.art===headArt?"var(--accent-soft)":"var(--bg-base)", border:`1px solid ${a.art===headArt?"var(--accent)":"var(--border-subtle)"}` }}>
                <span style={{ width:6, height:6, borderRadius:"50%", background:(PRICE_SUPS[a.sup]||{}).color||"var(--fg-muted)", flexShrink:0 }}/>
                <span style={{ fontSize:10.5, color:"var(--fg-muted)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis", maxWidth:90 }}>{a.label}</span>
                <span style={{ fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--fg-secondary)", whiteSpace:"nowrap" }}>{a.art}</span>
              </span>
            ))}
          </div>
        )}
      </div>

      <div style={{ flex:1, overflow:"auto", padding:"14px 16px 20px", display:"flex", flexDirection:"column", gap:16, scrollbarWidth:"none" }}>
        {product.supCount>1 && product.confidence && product.confidence.level!=="none" && (() => {
          const c = product.confidence; const m = CONF_META[c.level]; const isLow = c.level==="low"; const isManual = c.level==="manual";
          return (
            <div style={{ background:"var(--bg-raised)", border:`1px solid ${isLow?"rgba(245,158,11,.32)":"var(--border-subtle)"}`, borderRadius:12, padding:13 }}>
              <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:9 }}>
                <Icon name={m.icon} size={15} color={m.color}/>
                <span style={{ fontSize:13, fontWeight:600, color:m.color }}>{c.label}</span>
                <div style={{ flex:1 }}/>
                {c.pct!=null && <span style={{ fontFamily:"var(--font-mono)", fontSize:13, fontWeight:700, color:m.color }}>{c.pct}%</span>}
              </div>
              <div style={{ height:5, background:"var(--bg-base)", borderRadius:3, overflow:"hidden" }}>
                <div style={{ height:"100%", width:`${c.pct||0}%`, background:m.color, borderRadius:3 }}/>
              </div>
              {isLow && (
                <div style={{ marginTop:10, fontSize:11.5, color:"var(--warning)", display:"flex", gap:6, alignItems:"flex-start", lineHeight:1.4 }}>
                  <Icon name="alert-triangle" size={13} color="var(--warning)" style={{ flexShrink:0, marginTop:1 }}/> Низька впевненість — перевірте привʼязку вручну.
                </div>
              )}
              {isManual
                ? <Button variant="danger" leftIcon="unlink" onClick={()=>onBreak&&onBreak(product)} style={{ width:"100%", marginTop:12 }}>Розірвати звʼязок</Button>
                : <div style={{ display:"flex", gap:8, marginTop:12 }}>
                    <Button variant="primary" leftIcon="check" onClick={()=>onConfirm&&onConfirm(product)} style={{ flex:1 }}>Підтвердити</Button>
                    <Button variant="secondary" leftIcon="link" onClick={()=>onLink&&onLink(product)} style={{ flex:"0 0 auto" }}>Привʼязати</Button>
                  </div>}
            </div>
          );
        })()}
        {product.marginRisk && (
          <div style={{ display:"flex", gap:10, padding:"10px 13px", background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.32)", borderRadius:11 }}>
            <Icon name="trending-up" size={16} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
            <div><div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Закуп подорожчав</div>
            <div style={{ fontSize:11.5, color:"var(--fg-secondary)", marginTop:2, lineHeight:1.4 }}>Маржа впала до {fmtPctP(product.marginPct)}.</div></div>
          </div>
        )}
        {product.allOut && (
          <div style={{ display:"flex", gap:10, padding:"10px 13px", background:"rgba(255,255,255,.04)", border:"1px solid var(--border-default)", borderRadius:11 }}>
            <Icon name="package-x" size={16} color="var(--fg-muted)" style={{ flexShrink:0, marginTop:1 }}/>
            <div style={{ fontSize:12, color:"var(--fg-secondary)" }}>Зараз товару немає в наявності у жодного постачальника.</div>
          </div>
        )}

        <div>
          <SectionLabelMP right={published?<span style={{ fontSize:11, color:"var(--success)", display:"inline-flex", alignItems:"center", gap:4 }}><Icon name="check" size={12}/> синхронізовано</span>:dirty?<span style={{ fontSize:11, color:"var(--warning)" }}>змінено: {siteChangeBadge}</span>:null}>На сайті · Horoshop</SectionLabelMP>
          <div style={{ background:"var(--bg-raised)", border:"1px solid var(--border-subtle)", borderRadius:12, padding:13 }}>
            <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:6 }}>Постачальник-джерело</div>
            <div style={{ position:"relative", marginBottom:13 }}>
              <select value={boundSup} onChange={e=>setBoundSup(e.target.value)} style={{ width:"100%", boxSizing:"border-box", height:44, padding:"0 32px 0 12px", appearance:"none", background:"var(--bg-base)", color:"var(--fg-primary)", border:"1px solid var(--border-default)", borderRadius:10, fontSize:13.5, fontFamily:"inherit", fontWeight:600, cursor:"pointer", outline:"none" }}>
                {sortedOffers.map(o=><option key={o.sup} value={o.sup}>{PRICE_SUPS[o.sup]?.label||o.sup} · {fmtCurP(o.price,o.cur)}{o.ref?" · REF":""}{o.stock?"":" (немає)"}</option>)}
              </select>
              <Icon name="chevron-down" size={15} style={{ position:"absolute", right:11, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", pointerEvents:"none" }}/>
            </div>
            <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:6 }}>Ціна продажу на сайті</div>
            <div style={{ position:"relative" }}>
              <input type="text" inputMode="numeric" value={sellPrice?fmtBareP(sellPrice):""} onChange={e=>setSellPrice(Math.max(0,parseInt(e.target.value.replace(/\D/g,""),10)||0))}
                style={{ width:"100%", boxSizing:"border-box", height:50, padding:"0 34px 0 13px",
                  background:"var(--bg-base)", color:"var(--fg-primary)", border:`1px solid ${liveMargin<0?"var(--danger)":"var(--border-default)"}`,
                  borderRadius:10, fontSize:22, fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontWeight:700, outline:"none" }}/>
              <span style={{ position:"absolute", right:13, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", fontSize:18, fontFamily:"var(--font-mono)", pointerEvents:"none" }}>₴</span>
            </div>
            <div style={{ display:"flex", gap:7, marginTop:9, flexWrap:"wrap" }}>
              <button onClick={()=>setSellPrice(targetSell)} style={chipBtn(sellPrice===targetSell)}><Icon name="target" size={12}/> {TARGET_MARGIN}% маржі</button>
              {sellPrice!==(product.our||0) && <button onClick={()=>setSellPrice(product.our||0)} style={chipBtn(false)}><Icon name="rotate-ccw" size={12}/> {fmtUAHP(product.our)}</button>}
            </div>
            <div style={{ display:"flex", alignItems:"center", gap:12, marginTop:13, padding:"11px 13px", background:"var(--bg-base)", borderRadius:10, border:`1px solid ${liveMargin<0?"rgba(244,63,94,.3)":"var(--border-subtle)"}` }}>
              <div style={{ flexShrink:0 }}>
                <div style={{ fontSize:11, color:"var(--fg-muted)", marginBottom:2 }}>Маржа</div>
                <div style={{ display:"flex", alignItems:"baseline", gap:7, whiteSpace:"nowrap" }}>
                  <span style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:18, fontWeight:700, color:mColor }}>{fmtSignedP(liveMargin)}</span>
                  <span style={{ fontFamily:"var(--font-mono)", fontSize:12.5, fontWeight:600, color:mColor }}>{fmtPctP(liveMarginPct)}</span>
                </div>
              </div>
              <div style={{ flex:1 }}>
                <div style={{ display:"flex", justifyContent:"flex-end", fontSize:10.5, color:"var(--fg-muted)", marginBottom:5 }}>ціль {TARGET_MARGIN}%</div>
                <div style={{ height:6, background:"var(--bg-panel)", borderRadius:3, overflow:"hidden" }}>
                  <div style={{ height:"100%", width:`${Math.max(3,Math.min(100,(liveMarginPct/TARGET_MARGIN)*100))}%`, background:liveMarginPct>=TARGET_MARGIN?"var(--success)":liveMargin<0?"var(--danger)":"var(--warning)", borderRadius:3, transition:"width 150ms" }}/>
                </div>
              </div>
            </div>
            <Button variant="primary" leftIcon={isMdm?"lock":published?"check":"refresh-cw"} onClick={publish} disabled={isMdm||(!dirty&&!published)} style={{ width:"100%", marginTop:12 }}>
              {isMdm?"MDM — публікацію заблоковано":published?"Оновлено на сайті":`Оновити ${siteChangeWhat} на сайті`}
            </Button>
            {isMdm && (
              <div style={{ display:"flex", gap:8, marginTop:10, padding:"9px 12px", background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.32)", borderRadius:9, fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>
                <Icon name="lock" size={14} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
                Заблокований пристрій (MDM): не публікуємо на сайт, не ставимо джерелом, не виводимо на вітрину.
              </div>
            )}
            {!isMdm && boundOffer.ref && (
              <div style={{ display:"flex", gap:8, marginTop:10, padding:"9px 12px", background:"rgba(245,158,11,.10)", border:"1px solid rgba(245,158,11,.28)", borderRadius:9, fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>
                <Icon name="alert-triangle" size={14} color="var(--warning)" style={{ flexShrink:0, marginTop:1 }}/>
                Джерело — REF (відновлений). Обрано вручну: автоматично REF джерелом не ставиться.
              </div>
            )}
          </div>
        </div>

        <div>
          <SectionLabelMP right={<span style={{ fontSize:11, color:"var(--fg-muted)" }}>спред {fmtUAHP(product.spread)}</span>}>Постачальники · {product.supCount}</SectionLabelMP>
          <div style={{ display:"flex", flexDirection:"column", gap:8 }}>
            {sortedOffers.map(o=>(
              <SupplierCompareRowMP key={o.sup} o={o} isBest={o.sup===product.best.sup} isSource={boundSup===o.sup} onMakeSource={()=>setBoundSup(o.sup)}
                disableSource={isMdm} canUnlink={product.supCount>1} onUnlink={()=>onUnlink&&onUnlink(product,o)} onCheck={checkAvail} checking={checking}/>
            ))}
          </div>
        </div>

        <div>
          <SectionLabelMP right={<button onClick={()=>setChartOpen(v=>!v)} style={{ background:"transparent", border:0, color:"var(--fg-muted)", cursor:"pointer", padding:0 }}><Icon name={chartOpen?"chevron-up":"chevron-down"} size={15}/></button>}>Історія закупки · 30 днів</SectionLabelMP>
          {chartOpen && (
            <div style={{ background:"var(--bg-raised)", border:"1px solid var(--border-subtle)", borderRadius:12, padding:"12px 8px 8px" }}>
              <div style={{ display:"flex", justifyContent:"space-between", padding:"0 8px 6px", fontSize:11 }}>
                <span style={{ display:"inline-flex", alignItems:"center", gap:6, color:"var(--fg-secondary)" }}><span style={{ width:12, height:2, background:"var(--success)", borderRadius:2 }}/> Найкращий закуп, ₴</span>
                <span style={{ fontFamily:"var(--font-mono)", color:"var(--fg-muted)" }}>{fmtUAHP(Math.min(...product.bestHist))} – {fmtUAHP(Math.max(...product.bestHist))}</span>
              </div>
              <PriceHistoryChartP series={product.bestHist} height={140}/>
            </div>
          )}
        </div>
      </div>

      <div style={{ padding:13, borderTop:"1px solid var(--border-subtle)", flexShrink:0, display:"flex", gap:8 }}>
        <Button variant="secondary" leftIcon="external-link" style={{ flex:"0 0 auto" }}>Картка</Button>
        <Button variant="primary" leftIcon="search" style={{ flex:1 }} onClick={()=>checkAvail(headArt||product.art)} disabled={checking}>Уточнити наявність</Button>
      </div>
    </BottomSheetP>
  );
}

function LinkResultRowMP({ p, selected, onSelect }) {
  return (
    <button onClick={()=>onSelect(p)} style={{
      display:"flex", alignItems:"center", gap:10, width:"100%", textAlign:"left", padding:"11px 12px", borderRadius:11, cursor:"pointer", fontFamily:"inherit",
      background:selected?"var(--accent-soft)":"var(--bg-base)", border:`1px solid ${selected?"var(--accent)":"var(--border-subtle)"}` }}>
      <span style={{ width:18, height:18, borderRadius:"50%", flexShrink:0, border:`2px solid ${selected?"var(--accent)":"var(--border-strong)"}`, background:selected?"var(--accent)":"transparent", display:"flex", alignItems:"center", justifyContent:"center" }}>
        {selected && <Icon name="check" size={11} color="#fff"/>}
      </span>
      <div style={{ flex:1, minWidth:0 }}>
        <div style={{ fontSize:13, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{p.name}</div>
        <div style={{ display:"flex", alignItems:"center", gap:8, marginTop:4 }}>
          <span style={{ fontFamily:"var(--font-mono)", fontSize:11, color:"var(--fg-muted)" }}>{p.art||"—"}</span>
          <SupplierChipP sup={p.best.sup} size="sm" label={p.best.supplierName}/>
          <span style={{ fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--fg-secondary)" }}>{fmtCurP(p.best.price,p.best.cur)}</span>
        </div>
      </div>
    </button>
  );
}

function LinkPickerSheetMP({ product, onClose, onLink }) {
  const [q, setQ] = useState("");
  const [sel, setSel] = useState(null);
  const results = useMemo(()=>{
    const t = q.trim().toLowerCase();
    return PRICE_PRODUCTS.filter(p=>p.id!==product.id &&
      (!t || p.name.toLowerCase().includes(t) || (p.art||"").toLowerCase().includes(t)))
      .slice(0, 150);
  }, [q, product.id]);
  return (
    <BottomSheetP onClose={onClose} heightPct={94} padGrab={false}>
      <div style={{ paddingTop:8 }}>
        <div style={{ width:36, height:4, borderRadius:2, background:"var(--border-strong)", margin:"0 auto 10px" }}/>
      </div>
      <div style={{ padding:"0 16px 13px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <div style={{ display:"flex", alignItems:"center", gap:10 }}>
          <div style={{ width:32, height:32, borderRadius:8, background:"var(--accent-soft)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="link" size={16} color="var(--accent)"/></div>
          <div style={{ flex:1, minWidth:0 }}>
            <h2 style={{ fontSize:15, fontWeight:600, margin:0, color:"var(--fg-primary)" }}>Привʼязати товар</h2>
            <div style={{ fontSize:11.5, color:"var(--fg-muted)", marginTop:1, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{product.name}</div>
          </div>
          <button onClick={onClose} style={{ width:32, height:32, border:0, background:"var(--bg-base)", color:"var(--fg-secondary)", borderRadius:8, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={17}/></button>
        </div>
        <div style={{ position:"relative", marginTop:12 }}>
          <Icon name="search" size={15} style={{ position:"absolute", left:12, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)" }}/>
          <input value={q} onChange={e=>setQ(e.target.value)} placeholder="Знайти товар…" style={{
            width:"100%", height:44, boxSizing:"border-box", padding:"0 12px 0 36px",
            background:"var(--bg-base)", color:"var(--fg-primary)", border:"1px solid var(--border-default)", borderRadius:10, fontSize:14, outline:"none", fontFamily:"inherit" }}/>
        </div>
      </div>
      <div style={{ flex:1, overflow:"auto", padding:"12px 16px", display:"flex", flexDirection:"column", gap:8, scrollbarWidth:"none" }}>
        {results.length===0
          ? <div style={{ padding:"40px 0", textAlign:"center", fontSize:13, color:"var(--fg-muted)" }}>Нічого не знайдено</div>
          : results.map(p=><LinkResultRowMP key={p.id} p={p} selected={sel?.id===p.id} onSelect={setSel}/>)}
      </div>
      <div style={{ padding:13, borderTop:"1px solid var(--border-subtle)", flexShrink:0, display:"flex", gap:8 }}>
        <Button variant="secondary" onClick={onClose} style={{ flex:"0 0 auto" }}>Скасувати</Button>
        <Button variant="primary" leftIcon="link" disabled={!sel} onClick={()=>onLink(product, sel)} style={{ flex:1 }}>Привʼязати</Button>
      </div>
    </BottomSheetP>
  );
}

function SuggestionSideMP({ x, label }) {
  return (
    <div style={{ padding:"10px 12px", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:10 }}>
      <div style={{ fontSize:10, fontWeight:600, letterSpacing:".04em", textTransform:"uppercase", color:"var(--fg-muted)", marginBottom:5 }}>{label}</div>
      <div style={{ fontSize:12.5, color:"var(--fg-primary)", lineHeight:1.35 }}>{x.name}</div>
      <div style={{ display:"flex", alignItems:"center", gap:7, marginTop:6, flexWrap:"wrap" }}>
        {x.art && <span style={{ fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--fg-muted)" }}>{x.art}</span>}
        {(x.suppliers||[]).map((s,i)=><SupplierChipP key={i} sup={s} size="sm"/>)}
      </div>
    </div>
  );
}

function SuggestionCardMP({ pair, onConfirm, onDismiss }) {
  return (
    <div style={{ background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", borderRadius:12, padding:12 }}>
      <div style={{ display:"flex", justifyContent:"center", marginBottom:8 }}>
        <span style={{ display:"inline-flex", alignItems:"center", gap:4, height:20, padding:"0 9px", borderRadius:999, background:"rgba(245,158,11,.14)", color:"var(--warning)", fontSize:11, fontWeight:700 }}>
          <Icon name="arrow-up-down" size={11}/> схожість ~{pair.sim}%
        </span>
      </div>
      <div style={{ display:"flex", flexDirection:"column", gap:8 }}>
        <SuggestionSideMP x={pair.a} label="Товар 1"/>
        <SuggestionSideMP x={pair.b} label="Товар 2"/>
      </div>
      {pair.note && (
        <div style={{ display:"flex", alignItems:"center", gap:7, marginTop:10, padding:"8px 10px", borderRadius:8, background:"rgba(245,158,11,.1)", border:"1px solid rgba(245,158,11,.3)", fontSize:11.5, color:"var(--warning)", fontWeight:500 }}>
          <Icon name="alert-triangle" size={13} style={{ flexShrink:0 }}/>{pair.note}
        </div>
      )}
      <div style={{ display:"flex", gap:8, marginTop:12 }}>
        <Button variant="secondary" leftIcon="x" onClick={()=>onDismiss(pair)} style={{ flex:"0 0 auto" }}>Відхилити</Button>
        <Button variant="primary" leftIcon="link" onClick={()=>onConfirm(pair)} style={{ flex:1 }}>Привʼязати</Button>
      </div>
    </div>
  );
}

function ReviewViewMP({ suggestions, loading, onConfirm, onDismiss }) {
  return (
    <div style={{ flex:1, overflow:"auto", padding:"12px 16px 24px", display:"flex", flexDirection:"column", gap:10, scrollbarWidth:"none" }}>
      <div style={{ fontSize:12, color:"var(--fg-muted)", padding:"0 2px 2px" }}>Можливі склейки за схожими назвами — підтвердьте або відхиліть</div>
      {loading ? (
        <div style={{ display:"flex", alignItems:"center", justifyContent:"center", padding:"50px 0", color:"var(--fg-muted)", gap:10 }}>
          <Icon name="loader" size={22} style={{ animation:"spin 0.8s linear infinite" }}/> Пошук…
        </div>
      ) : !suggestions || suggestions.length===0 ? (
        <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:12, padding:"56px 24px", textAlign:"center" }}>
          <div style={{ width:52, height:52, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="check-circle" size={24} color="var(--success)"/></div>
          <div style={{ fontSize:13.5, fontWeight:600, color:"var(--fg-primary)" }}>Немає можливих склейок</div>
        </div>
      ) : suggestions.map(pair=><SuggestionCardMP key={pair.pairId} pair={pair} onConfirm={onConfirm} onDismiss={onDismiss}/>)}
    </div>
  );
}

// ── Панель воркерів Hotline (онлайн-статус ПК працівників + паралельність вкладок) ──
function HlWorkersPanelP() {
  const [data, setData] = useState(null);   // { online, workers, concurrency, pending }
  const [saving, setSaving] = useState(false);

  const load = () => fetch("/api/hl-worker/status").then(r => r.json()).then(d => { if (d && d.ok) setData(d); }).catch(() => {});
  useEffect(() => { load(); const iv = setInterval(load, 10000); return () => clearInterval(iv); }, []);

  const setConc = (n) => {
    setSaving(true);
    fetch("/api/hl-worker/concurrency", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ concurrency: n }) })
      .then(r => r.json()).then(d => { if (d && d.ok) setData(p => p ? { ...p, concurrency: d.concurrency } : p); })
      .finally(() => setSaving(false));
  };

  // Персональний потолок для одного воркера ("" → авто: глобальний, або 1 для проксі)
  const setWorkerConc = (id, val) => {
    fetch("/api/hl-worker/worker-concurrency", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workerId: id, concurrency: val === "" ? null : Number(val) }) })
      .then(r => r.json()).then(d => { if (d && d.ok) load(); }).catch(() => {});
  };

  const fmtAgo = (ts) => {
    if (!ts) return "—";
    const s = Math.round((Date.now() - ts) / 1000);
    if (s < 60) return s + " с тому";
    if (s < 3600) return Math.round(s / 60) + " хв тому";
    return Math.round(s / 3600) + " год тому";
  };

  const workers = (data && data.workers) || [];
  const conc = (data && data.concurrency) || 2;
  const onlineCount = workers.filter(w => w.online).length;

  return (
    <div style={{ background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 12, padding: 14, marginBottom: 14 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
        <Icon name="monitor" size={16} color="var(--fg-secondary)" />
        <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Воркери Hotline</span>
        <span style={{ fontSize: 12, color: onlineCount ? "var(--success)" : "var(--fg-muted)" }}>
          {onlineCount} онлайн{workers.length ? ` / ${workers.length}` : ""}
        </span>
        <div style={{ flex: 1 }} />
        <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>Паралельних вкладок:</span>
        <div style={{ display: "flex", gap: 4 }}>
          {[1, 2, 3, 4].map(n => {
            const on = conc === n;
            return (
              <button key={n} onClick={() => !saving && setConc(n)} disabled={saving} style={{
                width: 30, height: 28, borderRadius: 7, cursor: saving ? "default" : "pointer", fontFamily: "inherit", fontSize: 13, fontWeight: 700,
                background: on ? "var(--accent)" : "var(--bg-base)", color: on ? "#fff" : "var(--fg-secondary)",
                border: `1px solid ${on ? "var(--accent)" : "var(--border-default)"}`,
              }}>{n}</button>
            );
          })}
        </div>
      </div>
      <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 6, lineHeight: 1.4 }}>
        Скільки сторінок Hotline кожен ПК відкриває одночасно. Менше = безпечніше від анти-бота. Діє на всі воркери.
      </div>
      {workers.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 7, marginTop: 12 }}>
          {workers.map(w => (
            <div key={w.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 11px", background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 9 }}>
              <span style={{ width: 9, height: 9, borderRadius: "50%", flexShrink: 0, background: w.blocked ? "var(--danger)" : w.online ? "var(--success)" : "var(--fg-muted)", boxShadow: w.blocked ? "0 0 0 3px rgba(244,63,94,.18)" : w.online ? "0 0 0 3px rgba(16,185,129,.18)" : "none" }} />
              <div style={{ minWidth: 0, flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{w.name}</div>
                <div style={{ fontSize: 11, color: w.blocked ? "var(--danger)" : "var(--fg-muted)" }}>
                  {w.blocked ? "🚫 IP заблоковано Hotline — перезапусти воркер/інтернет (новий IP) або авто-спроба за 30 хв" : w.online ? "онлайн" : `офлайн · ${fmtAgo(w.lastSeen)}`}{!w.blocked && w.host && w.host !== w.name ? ` · ${w.host}` : ""}
                </div>
              </div>
              {w.online && w.inFlight > 0 && (
                <span style={{ fontSize: 11, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 }}>
                  <Icon name="loader" size={11} style={{ animation: "spin 0.8s linear infinite" }} /> {w.inFlight} активн.
                </span>
              )}
              <select value={w.capOverride != null ? String(w.capOverride) : ""} onChange={e => setWorkerConc(w.id, e.target.value)}
                title="Паралельних вкладок саме для цього воркера (авто = глобальне значення, для проксі = 1)"
                style={{ height: 26, padding: "0 6px", borderRadius: 7, background: "var(--bg-panel)", color: "var(--fg-secondary)", border: "1px solid var(--border-default)", fontFamily: "inherit", fontSize: 11.5, cursor: "pointer" }}>
                <option value="">авто ({w.cap})</option>
                {[1, 2, 3, 4, 5].map(n => <option key={n} value={String(n)}>{n} вкл.</option>)}
              </select>
              <span style={{ fontSize: 11, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }} title="оброблено задач">{w.done || 0}</span>
            </div>
          ))}
        </div>
      )}
      {data && workers.length === 0 && (
        <div style={{ fontSize: 11.5, color: "var(--fg-muted)", marginTop: 10, display: "flex", alignItems: "center", gap: 6 }}>
          <Icon name="alert-circle" size={13} /> Жоден воркер не підключений. Запусти worker\install.cmd на ПК працівника.
        </div>
      )}
    </div>
  );
}

function ListsViewP({ statuses, onSync, onUpload, onOpenBroken, onReload, onResetStuck, onAudit }) {
  const hasStuck = Object.values(statuses).some(s=>s==="parsing");
  const M_ST = {
    ok:     {color:"var(--success)",soft:"rgba(16,185,129,.13)",icon:"check-circle",label:"Актуальний"},
    parsing:{color:"var(--accent)", soft:"var(--accent-soft)",  icon:"loader",      label:"Обробка…"},
    error:  {color:"var(--danger)", soft:"rgba(244,63,94,.13)", icon:"alert-octagon",label:"Помилка"},
    stale:  {color:"var(--warning)",soft:"rgba(245,158,11,.13)",icon:"clock",        label:"Застарів"},
    empty:  {color:"var(--fg-muted)",soft:"var(--bg-raised)",  icon:"circle-dashed",label:"Порожній"},
  };
  return (
    <div style={{ flex:1, overflow:"auto", padding:"4px 16px 24px", scrollbarWidth:"none" }}>
      <HlWorkersPanelP />
      <div style={{ display:"flex", alignItems:"center", gap:8, margin:"4px 2px 12px" }}>
        <span style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Прайс-листи</span>
        <span style={{ fontSize:12, color:"var(--fg-muted)" }}>{SUP_ORDER_P.length} джерел</span>
        <div style={{ flex:1 }}/>
        {hasStuck && <button onClick={onResetStuck} style={{ height:28, padding:"0 10px", border:"1px solid rgba(244,63,94,.3)", background:"rgba(244,63,94,.14)", color:"var(--danger)", borderRadius:8, fontSize:11.5, fontFamily:"inherit", cursor:"pointer" }}>Скинути застряглі</button>}
        <button onClick={onReload} style={{ width:30, height:30, border:"1px solid var(--border-default)", background:"var(--bg-panel)", borderRadius:8, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center", color:"var(--fg-secondary)" }}><Icon name="refresh-cw" size={14}/></button>
      </div>
      <div style={{ display:"flex", flexDirection:"column", gap:12 }}>
        {SUP_ORDER_P.map(sup=>{
          const s = PRICE_SUPS[sup];
          const status = statuses[sup]||s?.status||"empty";
          const st = M_ST[status]||M_ST.ok;
          return (
            <div key={sup} style={{ background:"var(--bg-panel)", border:`1px solid ${status==="error"?"rgba(244,63,94,.3)":"var(--border-subtle)"}`, borderRadius:12, padding:15, display:"flex", flexDirection:"column", gap:13 }}>
              <div style={{ display:"flex", alignItems:"flex-start", gap:10 }}>
                <div style={{ width:38, height:38, borderRadius:10, background:`color-mix(in oklab,${s?.color||"#6366F1"} 16%,transparent)`, display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="tags" size={18} color={s?.color||"var(--accent)"}/></div>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ fontSize:14.5, fontWeight:600, color:"var(--fg-primary)" }}>{s?.label||sup}</div>
                  <div style={{ display:"flex", alignItems:"center", gap:8, marginTop:5 }}>
                    <SourceBadgeP source={s?.source||"google_sheets"} size="sm"/>
                    <span style={{ fontSize:11, color:"var(--fg-muted)", fontFamily:"var(--font-mono)" }}>{s?.cur||"UAH"}</span>
                  </div>
                </div>
                <span style={{ display:"inline-flex", alignItems:"center", gap:5, height:22, padding:"0 9px", borderRadius:999, background:st.soft, color:st.color, fontSize:11, fontWeight:600, whiteSpace:"nowrap", flexShrink:0 }}>
                  <Icon name={st.icon} size={12} style={status==="parsing"?{animation:"spin 0.8s linear infinite"}:undefined}/> {st.label}
                </span>
              </div>
              {status==="parsing" ? (
                <div>
                  <div style={{ display:"flex", justifyContent:"space-between", fontSize:11.5, marginBottom:6 }}><span style={{ color:"var(--fg-secondary)" }}>Оновлення…</span></div>
                  <div style={{ height:6, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:3, overflow:"hidden" }}><div style={{ height:"100%", width:"45%", background:"var(--accent)", transition:"width 200ms" }}/></div>
                </div>
              ) : status==="error" ? (
                <div style={{ display:"flex", gap:9, padding:"10px 12px", background:"rgba(244,63,94,.1)", border:"1px solid rgba(244,63,94,.28)", borderRadius:9 }}>
                  <Icon name="file-x" size={15} color="var(--danger)" style={{ flexShrink:0, marginTop:1 }}/>
                  <div style={{ fontSize:11.5, color:"var(--fg-secondary)", lineHeight:1.4 }}>Не вдалося розпізнати файл. Перевірте налаштування колонок.</div>
                </div>
              ) : (
                <div style={{ display:"flex", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:9, overflow:"hidden" }}>
                  {[["Позицій",s?.positions??0,null],["Змінено",s?.changed??0,(s?.changed||0)>0?"var(--warning)":null]].map(([lbl,val,col])=>(
                    <div key={lbl} style={{ flex:1, padding:"9px 12px" }}>
                      <div style={{ fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:16, fontWeight:700, color:col||"var(--fg-primary)" }}>{fmtBareP(val||0)}</div>
                      <div style={{ fontSize:10, color:"var(--fg-muted)", letterSpacing:".03em", textTransform:"uppercase", marginTop:2 }}>{lbl}</div>
                    </div>
                  ))}
                </div>
              )}
              {status!=="error" && status!=="parsing" && onAudit && (
                <button onClick={()=>onAudit(sup)} style={{ display:"flex", alignItems:"center", justifyContent:"center", gap:8, width:"100%", height:38, border:0, borderRadius:8, background:"var(--accent)", color:"#fff", fontSize:13, fontWeight:600, fontFamily:"inherit", cursor:"pointer" }}>
                  <Icon name="scan-search" size={16}/> Перевірити повністю
                </button>
              )}
              <div style={{ display:"flex", alignItems:"center", gap:8 }}>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ fontSize:12, color:status==="stale"?"var(--warning)":"var(--fg-secondary)", display:"flex", alignItems:"center", gap:5 }}><Icon name="clock" size={12}/> {s?.last||"—"}</div>
                  <div style={{ fontSize:11, color:"var(--fg-muted)", marginTop:2, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{s?.author||"Google Sheets"}</div>
                </div>
                {status==="error"
                  ? <Button size="sm" variant="danger" leftIcon="wrench" onClick={()=>onOpenBroken(sup)}>Виправити</Button>
                  : <Button size="sm" variant="secondary" leftIcon="upload" onClick={()=>onSync(sup)} disabled={status==="parsing"}>Оновити</Button>}
              </div>
            </div>
          );
        })}
        <button onClick={()=>onUpload(null)} style={{ border:"1.5px dashed var(--border-strong)", borderRadius:12, background:"transparent", cursor:"pointer", display:"flex", alignItems:"center", gap:13, padding:16, fontFamily:"inherit", textAlign:"left", width:"100%" }}>
          <div style={{ width:40, height:40, borderRadius:11, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="plus" size={20} color="var(--fg-muted)"/></div>
          <div><div style={{ fontSize:13.5, fontWeight:600, color:"var(--fg-primary)" }}>Додати прайс</div><div style={{ fontSize:11.5, color:"var(--fg-muted)", marginTop:2 }}>Excel / CSV або Telegram-бот</div></div>
        </button>
      </div>
    </div>
  );
}

const MU_FILE_COLS_P = [
  {col:"Код",sample:"16806119",field:"art",conf:98},{col:"Найменування",sample:"iPhone 15 Pro Max 256",field:"name",conf:95},
  {col:"Опт",sample:"45 200",field:"price",conf:92},{col:"Вал.",sample:"грн",field:"cur",conf:88},
  {col:"Залишок",sample:"12",field:"qty",conf:81},{col:"Статус",sample:"є",field:"stock",conf:67},
];
const MU_FIELDS_P = [["art","Артикул"],["name","Назва"],["price","Ціна закупу"],["cur","Валюта"],["qty","Кількість"],["stock","Наявність"],["skip","— пропустити —"]];
const MU_DIFF_P = [
  {type:"up",art:"16806119",name:"iPhone 15 Pro Max 256",from:44600,to:45200},
  {type:"down",art:"MRXV3UA",name:"MacBook Air 13 M3 8/256",from:41400,to:40650},
  {type:"up",art:"SM-S928BZ",name:"Galaxy S24 Ultra 12/256",from:37200,to:38000},
  {type:"new",art:"WH1000XM5",name:"Sony WH-1000XM5 Black",from:null,to:11800},
  {type:"out",art:"DJI-MINI4",name:"DJI Mini 4 Pro Fly More",from:41200,to:null},
];
const MU_DIFF_META_P = {
  up:{color:"var(--danger)",icon:"arrow-up-right",label:"Дорожче"},
  down:{color:"var(--success)",icon:"arrow-down-right",label:"Дешевше"},
  new:{color:"#3B82F6",icon:"sparkle",label:"Нові"},
  out:{color:"var(--fg-muted)",icon:"package-x",label:"Зникли"},
};

function MStepDotsP({ step, broken }) {
  if (broken) return null;
  const steps = ["Файл","Колонки","Зміни"];
  return (
    <div style={{ display:"flex", alignItems:"center", gap:0, padding:"0 4px" }}>
      {steps.map((s,i)=>{
        const done=i<step, active=i===step;
        return (
          <React.Fragment key={s}>
            <div style={{ display:"flex", alignItems:"center", gap:7 }}>
              <span style={{ width:22, height:22, borderRadius:"50%", display:"flex", alignItems:"center", justifyContent:"center", fontSize:11, fontWeight:700, fontFamily:"var(--font-mono)", background:done?"var(--success)":active?"var(--accent)":"var(--bg-base)", color:done||active?"#fff":"var(--fg-muted)", border:done||active?"0":"1px solid var(--border-default)" }}>{done?"✓":i+1}</span>
              <span style={{ fontSize:12, fontWeight:active?600:500, color:active?"var(--fg-primary)":"var(--fg-muted)" }}>{s}</span>
            </div>
            {i<steps.length-1 && <div style={{ flex:1, height:1, background:"var(--border-default)", margin:"0 8px", minWidth:14 }}/>}
          </React.Fragment>
        );
      })}
    </div>
  );
}

function UploadSheetMP({ sup, startBroken, onClose, onSave }) {
  const [step, setStep] = useState(0);
  const [broken, setBroken] = useState(false);
  const [mapping, setMapping] = useState(()=>{ const m={}; MU_FILE_COLS_P.forEach((c,i)=>m[i]=c.field); return m; });
  const [difftab, setDifftab] = useState("all");
  useEffect(()=>{ if(startBroken){setStep(0);setBroken(true);} },[startBroken]);
  const supLabel = sup ? (PRICE_SUPS[sup]?.label||sup) : "Новий постачальник";
  const onFile = kind => { if(kind==="broken") setBroken(true); else { setBroken(false); setStep(1); } };
  const diffRows = difftab==="all" ? MU_DIFF_P : MU_DIFF_P.filter(d=>d.type===difftab);
  const counts = {up:88,down:41,new:12,out:5};

  return (
    <BottomSheetP onClose={onClose} heightPct={94}>
      <div style={{ display:"flex", alignItems:"center", gap:10, padding:"0 14px 12px 16px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}>
        <Icon name="upload" size={17} color="var(--accent)"/>
        <div style={{ flex:1, minWidth:0 }}>
          <h3 style={{ fontSize:14.5, fontWeight:600, margin:0, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>Завантаження прайсу</h3>
          <div style={{ fontSize:11.5, color:"var(--fg-muted)" }}>{supLabel}</div>
        </div>
        <button onClick={onClose} style={{ width:32, height:32, border:0, background:"var(--bg-base)", color:"var(--fg-secondary)", borderRadius:8, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="x" size={17}/></button>
      </div>
      {!broken && <div style={{ padding:"13px 16px", borderBottom:"1px solid var(--border-subtle)", flexShrink:0 }}><MStepDotsP step={step} broken={broken}/></div>}
      <div style={{ flex:1, overflow:"auto", padding:16, scrollbarWidth:"none" }}>
        {broken ? (
          <div style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:16, padding:"12px 4px" }}>
            <div style={{ width:54, height:54, borderRadius:14, background:"rgba(244,63,94,.12)", border:"1px solid rgba(244,63,94,.3)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="file-x" size={25} color="var(--danger)"/></div>
            <div style={{ textAlign:"center" }}>
              <h3 style={{ fontSize:15.5, fontWeight:600, color:"var(--fg-primary)", margin:0 }}>Не вдалося прочитати прайс</h3>
              <p style={{ fontSize:12.5, color:"var(--fg-muted)", marginTop:8, lineHeight:1.5 }}>Перевірте структуру файлу.</p>
            </div>
            <div style={{ width:"100%", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:10, padding:"10px 14px" }}>
              {["Шапка не на першому рядку","Ціни як текст із символом валюти","Обʼєднані клітинки в шапці"].map(t=>(
                <div key={t} style={{ display:"flex", alignItems:"center", gap:9, padding:"6px 0", fontSize:12, color:"var(--fg-secondary)" }}><Icon name="alert-triangle" size={13} color="var(--warning)"/> {t}</div>
              ))}
            </div>
            <Button variant="primary" leftIcon="sliders-horizontal" style={{ width:"100%" }} onClick={()=>{setBroken(false);setStep(1);}}>Зіставити вручну</Button>
          </div>
        ) : step===0 ? (
          <div style={{ display:"flex", flexDirection:"column", gap:14 }}>
            <button onClick={()=>onFile("ok")} style={{ border:"1.5px dashed var(--border-strong)", borderRadius:14, background:"var(--bg-base)", cursor:"pointer", padding:"34px 20px", display:"flex", flexDirection:"column", alignItems:"center", gap:12, fontFamily:"inherit", width:"100%" }}>
              <div style={{ width:52, height:52, borderRadius:14, background:"var(--bg-panel)", border:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", justifyContent:"center" }}><Icon name="upload-cloud" size={24} color="var(--fg-secondary)"/></div>
              <div style={{ textAlign:"center" }}><div style={{ fontSize:14.5, fontWeight:600, color:"var(--fg-primary)" }}>Обрати файл прайсу</div><div style={{ fontSize:12, color:"var(--fg-muted)", marginTop:4 }}>Excel (.xlsx, .xls) або .csv · до 20 МБ</div></div>
            </button>
            <div style={{ display:"flex", alignItems:"center", gap:12 }}><div style={{ flex:1, height:1, background:"var(--border-subtle)" }}/><span style={{ fontSize:11.5, color:"var(--fg-muted)" }}>або джерело</span><div style={{ flex:1, height:1, background:"var(--border-subtle)" }}/></div>
            <button onClick={()=>onFile("ok")} style={{ display:"flex", alignItems:"center", gap:11, padding:"13px 14px", borderRadius:11, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit", textAlign:"left", width:"100%" }}>
              <div style={{ width:34, height:34, borderRadius:9, background:"rgba(34,158,217,.14)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="send" size={16} color="#229ED9"/></div>
              <div style={{ flex:1, minWidth:0 }}><div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Останній файл з Telegram</div><div style={{ fontSize:11.5, color:"var(--fg-muted)" }}>{sup&&PRICE_SUPS[sup]?.author||"@bot_price"}</div></div>
              <Icon name="chevron-right" size={16} color="var(--fg-muted)"/>
            </button>
            <button onClick={()=>onFile("broken")} style={{ display:"flex", alignItems:"center", gap:11, padding:"13px 14px", borderRadius:11, background:"var(--bg-base)", border:"1px solid var(--border-subtle)", cursor:"pointer", fontFamily:"inherit", textAlign:"left", width:"100%" }}>
              <div style={{ width:34, height:34, borderRadius:9, background:"rgba(244,63,94,.12)", display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }}><Icon name="file-x" size={16} color="var(--danger)"/></div>
              <div style={{ flex:1, minWidth:0 }}><div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>Демо: кривий файл</div><div style={{ fontSize:11.5, color:"var(--fg-muted)" }}>побачити стан помилки</div></div>
              <Icon name="chevron-right" size={16} color="var(--fg-muted)"/>
            </button>
          </div>
        ) : step===1 ? (
          <div>
            <div style={{ display:"flex", gap:8, fontSize:12.5, color:"var(--fg-secondary)", marginBottom:14, padding:"9px 12px", background:"rgba(16,185,129,.08)", border:"1px solid rgba(16,185,129,.22)", borderRadius:9 }}>
              <Icon name="wand-2" size={15} color="var(--success)" style={{ flexShrink:0, marginTop:1 }}/> Колонки визначено автоматично. Перевірте маппінг.
            </div>
            <div style={{ display:"flex", flexDirection:"column", gap:8 }}>
              {MU_FILE_COLS_P.map((c,i)=>{
                const low=c.conf<75;
                return (
                  <div key={i} style={{ display:"flex", alignItems:"center", gap:11, padding:"11px 13px", background:"var(--bg-base)", border:"1px solid var(--border-subtle)", borderRadius:10 }}>
                    <div style={{ flex:1, minWidth:0 }}>
                      <div style={{ fontSize:13, fontWeight:600, color:"var(--fg-primary)" }}>{c.col}</div>
                      <div style={{ fontSize:11, color:"var(--fg-muted)", fontFamily:"var(--font-mono)", marginTop:2, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>напр.: {c.sample}</div>
                    </div>
                    <span style={{ display:"inline-flex", alignItems:"center", gap:3, fontSize:10.5, fontWeight:600, color:low?"var(--warning)":"var(--success)", flexShrink:0 }}><Icon name={low?"alert-triangle":"check"} size={11}/> {c.conf}%</span>
                    <Icon name="arrow-right" size={14} color="var(--fg-disabled)" style={{ flexShrink:0 }}/>
                    <div style={{ position:"relative", flexShrink:0, width:122 }}>
                      <select value={mapping[i]} onChange={e=>setMapping({...mapping,[i]:e.target.value})} style={{ width:"100%", boxSizing:"border-box", height:38, padding:"0 26px 0 10px", appearance:"none", background:mapping[i]==="skip"?"var(--bg-panel)":"var(--bg-raised)", color:mapping[i]==="skip"?"var(--fg-muted)":"var(--fg-primary)", border:`1px solid ${low?"var(--warning)":"var(--border-default)"}`, borderRadius:8, fontSize:12, fontFamily:"inherit", fontWeight:600, cursor:"pointer", outline:"none" }}>
                        {MU_FIELDS_P.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                      </select>
                      <Icon name="chevron-down" size={13} style={{ position:"absolute", right:8, top:"50%", transform:"translateY(-50%)", color:"var(--fg-muted)", pointerEvents:"none" }}/>
                    </div>
                  </div>
                );
              })}
            </div>
            <div style={{ fontSize:11.5, color:"var(--fg-muted)", marginTop:12, textAlign:"center" }}>6 колонок · Google Sheets</div>
          </div>
        ) : (
          <div>
            <div style={{ display:"flex", gap:7, marginBottom:14, overflowX:"auto", scrollbarWidth:"none" }}>
              {["all","up","down","new","out"].map(k=>{
                const on=difftab===k; const m=MU_DIFF_META_P[k]; const val=k==="all"?146:counts[k];
                return (
                  <button key={k} onClick={()=>setDifftab(k)} style={{ flexShrink:0, minWidth:72, padding:"9px 11px", borderRadius:9, cursor:"pointer", fontFamily:"inherit", textAlign:"left", background:on?"var(--bg-raised)":"var(--bg-base)", border:`1px solid ${on?"var(--accent)":"var(--border-subtle)"}`, boxShadow:on?"0 0 0 3px var(--accent-soft)":"none" }}>
                    <div style={{ display:"flex", alignItems:"center", gap:5, marginBottom:4 }}>
                      {m?<Icon name={m.icon} size={12} color={m.color}/>:<Icon name="list" size={12} color="var(--fg-muted)"/>}
                      <span style={{ fontFamily:"var(--font-mono)", fontWeight:700, fontSize:15, color:m?m.color:"var(--fg-primary)" }}>{val}</span>
                    </div>
                    <div style={{ fontSize:10.5, color:"var(--fg-secondary)" }}>{k==="all"?"Всього":m.label}</div>
                  </button>
                );
              })}
            </div>
            <div style={{ border:"1px solid var(--border-subtle)", borderRadius:10, overflow:"hidden" }}>
              {diffRows.map((d,i)=>{
                const m=MU_DIFF_META_P[d.type];
                return (
                  <div key={i} style={{ display:"flex", alignItems:"center", gap:11, padding:"11px 13px", borderBottom:i<diffRows.length-1?"1px solid var(--border-subtle)":"0", background:i%2?"var(--bg-base)":"transparent" }}>
                    <Icon name={m.icon} size={15} color={m.color} style={{ flexShrink:0 }}/>
                    <div style={{ flex:1, minWidth:0 }}>
                      <div style={{ fontSize:12.5, color:"var(--fg-primary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{d.name}</div>
                      <div style={{ fontSize:11, color:"var(--fg-muted)", fontFamily:"var(--font-mono)" }}>{d.art}</div>
                    </div>
                    <div style={{ display:"flex", alignItems:"center", gap:6, fontFamily:"var(--font-mono)", fontVariantNumeric:"tabular-nums", fontSize:12, flexShrink:0 }}>
                      {d.from!=null&&<span style={{ color:"var(--fg-muted)", textDecoration:d.to==null?"none":"line-through" }}>{fmtBareP(d.from)}</span>}
                      {d.from!=null&&d.to!=null&&<Icon name="arrow-right" size={11} color="var(--fg-disabled)"/>}
                      {d.to!=null?<span style={{ color:m.color, fontWeight:600 }}>{fmtBareP(d.to)}</span>:<span style={{ color:"var(--fg-muted)" }}>зникло</span>}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        )}
      </div>
      {!broken && (
        <div style={{ display:"flex", alignItems:"center", gap:10, padding:13, borderTop:"1px solid var(--border-subtle)", flexShrink:0 }}>
          {step>0 && <Button variant="ghost" leftIcon="arrow-left" onClick={()=>setStep(s=>Math.max(0,s-1))}>Назад</Button>}
          <div style={{ flex:1 }}/>
          {step===2 && <span style={{ fontSize:11.5, color:"var(--fg-muted)", marginRight:2 }}>146 змін</span>}
          {step<2
            ? <Button variant="primary" leftIcon="arrow-right" onClick={()=>setStep(s=>Math.min(2,s+1))} disabled={step===0}>Далі</Button>
            : <Button variant="primary" leftIcon="check" onClick={onSave}>Застосувати</Button>}
        </div>
      )}
    </BottomSheetP>
  );
}


window.PricesPage = PricesPage;
})();
