// ferro-app.jsx — Ferrolend КП detail page with click-to-expand manager calc
const { useState, useMemo, useRef, useEffect } = React;

/* ============================ small bits ============================ */
function DownloadIcon() {
  return (
    <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
      <path d="M8 2v8" />
      <path d="M5 7l3 3 3-3" />
      <path d="M2.5 13h11" />
    </svg>
  );
}

function Pill({ children }) {
  return <span className="pill">{children}</span>;
}
function Chevron({ open }) {
  return (
    <svg className={"chev" + (open ? " open" : "")} viewBox="0 0 24 24" width="16" height="16">
      <path d="M8 10l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function Warn({ size = 15 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" style={{ display: "block" }}>
      <path d="M10.4 4.2 2.7 17.6a1.85 1.85 0 0 0 1.6 2.8h15.4a1.85 1.85 0 0 0 1.6-2.8L13.6 4.2a1.85 1.85 0 0 0-3.2 0Z" fill="#f2c233" stroke="#f2c233" strokeWidth="2.6" strokeLinejoin="round" />
      <rect x="10.95" y="8.6" width="2.1" height="5.6" rx="1.05" fill="#5d4708" />
      <circle cx="12" cy="16.6" r="1.25" fill="#5d4708" />
    </svg>
  );
}

// Подсказка "откуда взято значение" — заменяет нативный title. source бывает:
// null/undefined (сработал дефолт), объект {before, match, after} (дословная
// цитата с контекстом — see extractor._ctx_source) или обычная строка
// (описание вроде "рассчитано по проёму ..." — не цитата, просто пояснение).
function SourceTip({ source, defaultText, children }) {
  const [pos, setPos] = useState(null);

  // [БАГ] Раньше позиция считалась через ref.getBoundingClientRect() — но обёртка
  // .stip-anchor это display:contents (нужно, чтобы не ломать вёрстку ячейки
  // таблицы), а у display:contents элементов нет собственного box'а, поэтому
  // getBoundingClientRect() возвращал пустой прямоугольник (0,0) — подсказка
  // улетала в левый верхний угол экрана. Позиционируем прямо от курсора
  // (координаты события) — заодно это и есть то, что просили: рядом с курсором.
  const place = (e) => {
    const x = Math.max(6, Math.min(e.clientX + 12, window.innerWidth - 340));
    const y = Math.min(e.clientY + 16, window.innerHeight - 20);
    setPos({ x, y });
  };
  const hide = () => setPos(null);

  let body;
  if (!source) {
    body = <span className="stip__default">{defaultText || "По умолчанию — в тексте заявки не указано"}</span>;
  } else if (typeof source === "string") {
    body = source.startsWith("рассчитано")
      ? <><span className="stip__label">Рассчитано:</span> {source}</>
      : <><span className="stip__label">Из текста заявки:</span> {source}</>;
  } else {
    body = (
      <>
        <span className="stip__label">Из текста заявки:</span>
        <div className="stip__quote">
          {source.before ? `…${source.before}` : ""}
          <mark>{source.match}</mark>
          {source.after ? `${source.after}…` : ""}
        </div>
      </>
    );
  }

  return (
    <span className="stip-anchor" onMouseEnter={place} onMouseMove={place} onMouseLeave={hide}>
      {children}
      {pos && ReactDOM.createPortal(
        <div className="stip" style={{ left: pos.x, top: pos.y }}>{body}</div>,
        document.body
      )}
    </span>
  );
}

function PinIcon() {
  return (
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 17v5" />
      <path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
    </svg>
  );
}

function BellIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M6 8a6 6 0 0 1 12 0c0 4.2 1.2 5.8 2 6.6.4.4.1 1.4-.5 1.4H4.5c-.6 0-.9-1-.5-1.4C4.8 13.8 6 12.2 6 8Z" />
      <path d="M9.5 18a2.5 2.5 0 0 0 5 0" />
    </svg>
  );
}

/* ============================ changelog / "что нового" ============================ */
const CHANGELOG_SEEN_KEY = "ferro_changelog_seen";

function ChangelogButton() {
  const [open, setOpen] = useState(false);
  const [seen, setSeen] = useState(() => {
    try { return localStorage.getItem(CHANGELOG_SEEN_KEY) || ""; } catch (e) { return ""; }
  });
  const ref = useRef(null);
  const latest = CHANGELOG[0]?.date || "";
  const hasNew = latest && latest > seen;

  useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  const toggle = () => {
    setOpen((o) => {
      const next = !o;
      if (next && latest) {
        try { localStorage.setItem(CHANGELOG_SEEN_KEY, latest); } catch (e) {}
        setSeen(latest);
      }
      return next;
    });
  };

  const fmtShort = (iso) => {
    const d = new Date(iso + "T00:00:00");
    return d.toLocaleDateString("ru-RU", { day: "numeric", month: "short" });
  };

  return (
    <div className={"changelog" + (open ? " changelog--open" : "")} ref={ref}>
      <button type="button" className="btn btn--ghost changelog__btn" onClick={toggle} title="Что нового">
        <BellIcon />
        Что нового
        {hasNew && <span className="changelog__dot" />}
      </button>
      {open && (
        <div className="changelog__menu">
          <div className="changelog__head">Что нового</div>
          <ul className="changelog__list">
            {CHANGELOG.map((c, i) => (
              <li key={i} className="changelog__item">
                <span className="changelog__date">{fmtShort(c.date)}</span>
                <span className="changelog__text">{c.text}</span>
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

/* ============================ position-text info popup ============================ */
function InfoPopup({ x, y, pinned, tab, positionText, fullTextNormalized, fullTextRaw, originalText, hasOpenPosition, reqId, onRawTextDone, onClose, onTogglePin, onSetTab, onMove }) {
  const ref = useRef(null);

  useEffect(() => {
    if (!pinned) return;
    const onDown = (e) => {
      if (ref.current && ref.current.contains(e.target)) return;
      if (e.target.closest(".tablewrap") || e.target.closest(".poscards") || e.target.closest(".infobtn")) return;
      onClose();
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
    // eslint-disable-next-line
  }, [pinned]);

  const startDrag = (e) => {
    e.preventDefault();
    const startX = e.clientX, startY = e.clientY;
    const origX = x, origY = y;
    const onMouseMove = (ev) => {
      const w = ref.current ? ref.current.offsetWidth : 360;
      const nx = Math.max(8, Math.min(origX + (ev.clientX - startX), window.innerWidth - w - 8));
      const ny = Math.max(8, Math.min(origY + (ev.clientY - startY), window.innerHeight - 40));
      onMove(nx, ny);
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onUp);
    };
    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onUp);
  };

  const body = tab === "position"
    ? (hasOpenPosition ? (positionText || "Текст не найден") : "Сначала откройте позицию в таблице")
    : tab === "original"
    ? (originalText || "Оригинал не сохранён — нормализация не применялась")
    : tab === "full"
    ? (fullTextNormalized || "Текст не найден")
    : null; // tab === "raw" — рендерится через RawTextTab ниже

  return ReactDOM.createPortal(
    <div ref={ref} className="infopop" style={{ left: x, top: y }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
      <div className="infopop__head" onMouseDown={startDrag}>
        <button
          type="button"
          className={"infopop__pin" + (pinned ? " infopop__pin--on" : "")}
          title={pinned ? "Открепить" : "Закрепить"}
          onClick={onTogglePin}
        >
          <PinIcon />
        </button>
        <span className="infopop__label">Текст заявки</span>
        <button type="button" className="infopop__close" onClick={onClose}>×</button>
      </div>
      <div className="infopop__tabs">
        <button
          type="button"
          className={"infopop__tab" + (tab === "position" ? " infopop__tab--on" : "")}
          disabled={!hasOpenPosition}
          title={hasOpenPosition ? "Что реально ушло в расчёт для этой позиции (после ИИ-нормализации)" : "Сначала откройте позицию в таблице"}
          onClick={() => onSetTab("position")}
        >Эта позиция</button>
        <button
          type="button"
          className={"infopop__tab" + (tab === "full" ? " infopop__tab--on" : "")}
          title="Нормализованный текст всех позиций заявки — то, что реально ушло в расчёт"
          onClick={() => onSetTab("full")}
        >Вся заявка</button>
        <button
          type="button"
          className={"infopop__tab" + (tab === "raw" ? " infopop__tab--on" : "")}
          title="Заявка как её прислал клиент — можно отредактировать на месте (например дописать то, что видно на чертеже)"
          onClick={() => onSetTab("raw")}
        >Оригинал</button>
        {originalText && (
          <button
            type="button"
            className={"infopop__tab" + (tab === "original" ? " infopop__tab--on" : "")}
            title="Текст фото до OCR-коррекции"
            onClick={() => onSetTab("original")}
          >Фото до OCR</button>
        )}
      </div>
      <div className="infopop__body">
        {tab === "raw" && reqId ? (
          <RawTextTab reqId={reqId} text={fullTextRaw} onDone={onRawTextDone} />
        ) : body}
      </div>
    </div>,
    document.body
  );
}

function ReprocessBtn({ reqId, onDone }) {
  const [state, setState] = useState('idle'); // idle | running | done | error
  const run = async () => {
    if (state === 'running') return;
    setState('running');
    try {
      await apiFetch('POST', `/requests/${reqId}/reprocess`);
      // Poll until ready
      for (let i = 0; i < 60; i++) {
        await new Promise(r => setTimeout(r, 2000));
        const d = await apiFetch('GET', `/requests/${reqId}`);
        if (d.status === 'ready') { setState('done'); onDone(); setTimeout(() => setState('idle'), 2000); return; }
        if (d.status === 'error') { setState('error'); setTimeout(() => setState('idle'), 3000); return; }
      }
      setState('error');
      setTimeout(() => setState('idle'), 3000);
    } catch (e) {
      setState('error');
      setTimeout(() => setState('idle'), 3000);
    }
  };
  const icon = state === 'running' ? '⟳' : state === 'done' ? '✓' : state === 'error' ? '✕' : '⟳';
  const title = state === 'running' ? 'Пересчёт…' : state === 'done' ? 'Готово' : state === 'error' ? 'Ошибка' : 'Пересчитать заявку';
  return (
    <button
      className={`reprocess-btn reprocess-btn--${state}`}
      onClick={run}
      title={title}
      disabled={state === 'running'}
    ><span className="reprocess-btn__icon">{icon}</span></button>
  );
}

// Менеджер редактирует текст заявки ПРЯМО НА МЕСТЕ (карандашик → текст становится
// редактируемым тут же, без отдельного попапа/блока) — вписывает то, чего не
// написал клиент, но видно на приложенном чертеже/фото, РОВНО туда, где в тексте
// описана нужная дверь (дописывание в конец не годится: при нескольких позициях
// непонятно, к какой из них относится добавленное). Галочка слева от карандашика
// сохраняет и сразу пересчитывает заявку заново (см. /api/requests/{id}/raw-text).
function RawTextTab({ reqId, text, onDone }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(text || "");
  const [state, setState] = useState("idle"); // idle | saving | done | error

  useEffect(() => {
    if (!editing) setDraft(text || "");
  }, [text, editing]);

  const save = async () => {
    if (state === "saving") return;
    setState("saving");
    try {
      await apiFetch("PATCH", `/requests/${reqId}/raw-text`, { text: draft });
      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 2000));
        const d = await apiFetch("GET", `/requests/${reqId}`);
        if (d.status === "ready") { setState("done"); setEditing(false); onDone(); setTimeout(() => setState("idle"), 1500); return; }
        if (d.status === "error") { setState("error"); setTimeout(() => setState("idle"), 3000); return; }
      }
      setState("error");
      setTimeout(() => setState("idle"), 3000);
    } catch (e) {
      setState("error");
      setTimeout(() => setState("idle"), 3000);
    }
  };

  const toggleEdit = () => {
    if (editing) setDraft(text || "");
    setEditing((e) => !e);
  };

  return (
    <div className="rawedit">
      <div className="rawedit__toolbar">
        {editing && (
          <button type="button" className="rawedit__icon" title="Сохранить и пересчитать" onClick={save} disabled={state === "saving"}>
            {state === "saving" ? "…" : state === "error" ? "✕" : "✓"}
          </button>
        )}
        <button type="button" className="rawedit__icon" title={editing ? "Отменить" : "Редактировать текст заявки"} onClick={toggleEdit}>✎</button>
      </div>
      {editing ? (
        <textarea
          className="rawedit__textarea"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          rows={16}
          autoFocus
        />
      ) : (
        <div className="rawedit__display">{text || "Текст не найден"}</div>
      )}
    </div>
  );
}

// editable money cell used in the manager panel
function MoneyField({ label, value, onChange, accent, onRemove, hint }) {
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(String(value));
  useEffect(() => { if (!editing) setRaw(String(value)); }, [value, editing]);
  return (
    <div className={"mfield" + (accent ? " mfield--accent" : "")}>
      {onRemove && (
        <button
          className="mfield__rm"
          onClick={(e) => { e.stopPropagation(); onRemove(); }}
          tabIndex={-1}
          title="Убрать"
        />
      )}
      <span className="mfield__label">{label}</span>
      <div className="mfield__valwrap">
        {editing ? (
          <input
            className="mfield__input"
            type="text"
            inputMode="numeric"
            autoFocus
            value={raw}
            onChange={(e) => setRaw(e.target.value.replace(/[^\d.-]/g, ""))}
            onBlur={() => { setEditing(false); onChange(parseFloat(raw) || 0); }}
            onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setRaw(String(value)); setEditing(false); } }}
          />
        ) : (
          <button className="mfield__val" onClick={() => setEditing(true)} title="Редактировать">
            {RUB(value)}
          </button>
        )}
        {!editing && hint && <span className="mfield__hint">{hint}</span>}
      </div>
    </div>
  );
}

// editable text cell used directly in the KP table — click-to-edit, same pattern as MoneyField:
// shows plain wrapping text until clicked, then swaps to a borderless underlined input.
function CellInput({ value, placeholder, onSave }) {
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(value || "");
  useEffect(() => { if (!editing) setRaw(value || ""); }, [value, editing]);
  if (editing) {
    return (
      <input
        className="kcell__input"
        autoFocus
        value={raw}
        placeholder={placeholder}
        onClick={(e) => e.stopPropagation()}
        onChange={(e) => setRaw(e.target.value)}
        onBlur={() => { setEditing(false); if (raw !== (value || "")) onSave(raw); }}
        onKeyDown={(e) => {
          if (e.key === "Enter") e.target.blur();
          if (e.key === "Escape") { setRaw(value || ""); setEditing(false); }
        }}
      />
    );
  }
  return (
    <button
      type="button"
      className={"kcell__val" + (!value && placeholder ? " kcell__val--empty" : "")}
      onClick={(e) => { e.stopPropagation(); setEditing(true); }}
    >
      {value || placeholder || ""}
    </button>
  );
}

// Примечание — как CellInput, но в режиме показа (не редактирования) рисует
// каждый кусочек автосборки отдельным span со своей подсказкой (откуда в
// тексте заявки взят именно этот кусочек). Если менеджер переписал текст
// вручную (note !== noteAuto) — покусочная разбивка больше не соответствует
// содержимому, показываем как обычный текст без подсказок по кусочкам.
function NoteCell({ pos, onSave }) {
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(pos.note || "");
  useEffect(() => { if (!editing) setRaw(pos.note || ""); }, [pos.note, editing]);
  if (editing) {
    return (
      <input
        className="kcell__input"
        autoFocus
        value={raw}
        onClick={(e) => e.stopPropagation()}
        onChange={(e) => setRaw(e.target.value)}
        onBlur={() => { setEditing(false); if (raw !== (pos.note || "")) onSave(raw); }}
        onKeyDown={(e) => {
          if (e.key === "Enter") e.target.blur();
          if (e.key === "Escape") { setRaw(pos.note || ""); setEditing(false); }
        }}
      />
    );
  }
  const blocks = pos.noteBlocksDetail || [];
  const isAuto = pos.note === pos.noteAuto && blocks.length > 0;
  return (
    <button
      type="button"
      className={"kcell__val" + (!pos.note ? " kcell__val--empty" : "")}
      onClick={(e) => { e.stopPropagation(); setEditing(true); }}
    >
      {isAuto
        ? blocks.map((b, i) => (
            <SourceTip key={b.id + i} source={b.source}>
              {b.text}{i < blocks.length - 1 ? " " : ""}
            </SourceTip>
          ))
        : (pos.note || "")}
    </button>
  );
}

// editable integer (mm) cell used directly in the KP table — same click-to-edit pattern as CellInput.
function NumberCell({ value, placeholder, onSave }) {
  const dispVal = value === "" || value == null ? "" : String(value);
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(dispVal);
  useEffect(() => { if (!editing) setRaw(dispVal); }, [value, editing]);
  if (editing) {
    return (
      <input
        className="kcell__input kcell__val--num"
        inputMode="numeric"
        autoFocus
        value={raw}
        placeholder={placeholder}
        onClick={(e) => e.stopPropagation()}
        onChange={(e) => setRaw(e.target.value.replace(/[^\d]/g, ""))}
        onBlur={() => { setEditing(false); if (raw !== dispVal) onSave(raw === "" ? null : parseInt(raw, 10)); }}
        onKeyDown={(e) => {
          if (e.key === "Enter") e.target.blur();
          if (e.key === "Escape") { setRaw(dispVal); setEditing(false); }
        }}
      />
    );
  }
  return (
    <button
      type="button"
      className={"kcell__val kcell__val--num" + (!dispVal && placeholder ? " kcell__val--empty" : "")}
      onClick={(e) => { e.stopPropagation(); setEditing(true); }}
    >
      {dispVal || placeholder || ""}
    </button>
  );
}

// editable cell with a dropdown of preset options + free text.
// Custom-built (not the native <datalist>) because the browser's native suggestion popup
// can't be styled — no control over width or text wrapping, so long option labels (door
// types, lock/handle names) get clipped or force a huge box. The list is rendered through
// a portal into document.body and positioned with getBoundingClientRect, so it isn't
// clipped by .tablewrap's overflow and can be made wider than the (narrow) table cell.
// Editing always starts from an empty input so every option is visible immediately
// (a pre-filled exact match would otherwise leave nothing else to pick).
// A touched-ref distinguishes "clicked away without picking anything" (revert, no save)
// from "deliberately cleared" (save empty).
function DatalistCell({ value, options, onSave }) {
  const dispVal = value === "—" ? "" : value || "";
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState("");
  const [rect, setRect] = useState(null);
  const touchedRef = useRef(false);
  const wrapRef = useRef(null);
  const dropRef = useRef(null);

  const commit = (keep) => {
    setEditing(false);
    if (!touchedRef.current) return;
    if (keep !== dispVal) onSave(keep);
  };

  useEffect(() => {
    if (!editing) return;
    const place = () => {
      const el = wrapRef.current;
      if (el) setRect(el.getBoundingClientRect());
    };
    place();
    const onDown = (e) => {
      if (wrapRef.current?.contains(e.target) || dropRef.current?.contains(e.target)) return;
      commit(raw);
    };
    document.addEventListener("mousedown", onDown);
    window.addEventListener("scroll", place, true);
    window.addEventListener("resize", place);
    return () => {
      document.removeEventListener("mousedown", onDown);
      window.removeEventListener("scroll", place, true);
      window.removeEventListener("resize", place);
    };
    // eslint-disable-next-line
  }, [editing, raw]);

  if (editing) {
    const filtered = raw
      ? options.filter((o) => o.toLowerCase().includes(raw.toLowerCase()))
      : options;
    return (
      <span ref={wrapRef} style={{ display: "block" }}>
        <input
          className="kcell__input"
          autoFocus
          value={raw}
          onClick={(e) => e.stopPropagation()}
          onChange={(e) => { touchedRef.current = true; setRaw(e.target.value); }}
          onKeyDown={(e) => {
            if (e.key === "Enter") commit(raw);
            if (e.key === "Escape") { touchedRef.current = false; commit(dispVal); }
          }}
        />
        {rect && ReactDOM.createPortal(
          <div
            ref={dropRef}
            className="kdrop"
            style={{ top: rect.bottom + 4, left: rect.left, minWidth: Math.max(rect.width, 260) }}
            onMouseDown={(e) => e.preventDefault()}
          >
            {filtered.length === 0 && <div className="kdrop__empty">Нет совпадений — впишите вручную</div>}
            {filtered.map((o) => (
              <button
                key={o}
                type="button"
                className="kdrop__item"
                onClick={() => { touchedRef.current = true; commit(o); }}
              >
                {o}
              </button>
            ))}
          </div>,
          document.body
        )}
      </span>
    );
  }
  return (
    <span
      role="button"
      tabIndex={0}
      className={"kcell__val" + (!dispVal ? " kcell__val--empty" : "")}
      onClick={(e) => {
        e.stopPropagation();
        setRaw("");
        touchedRef.current = false;
        setEditing(true);
      }}
    >
      {dispVal || "—"}
    </span>
  );
}

/* ============================ add-field dropdown ============================ */
function AddFieldDropdown({ fields, onAdd }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  return (
    <div className={"mgr__add" + (open ? " mgr__add--open" : "")} ref={ref}>
      <button className="mgr__add-btn" onClick={() => setOpen(o => !o)}>
        Добавить
      </button>
      {open && (
        <div className="mgr__add-menu">
          {fields.map(f => (
            <button
              key={f.key}
              className="mgr__add-item"
              onClick={() => { onAdd(f.key); setOpen(false); }}
            >
              {f.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// Редактируемая формула стекла (высота×ширина, мм + цена/м²) под полем
// "Стоимость стекла" — правка любого из трёх значений уходит на бэкенд
// (patchAndRecalc), который пересчитывает наценку по той же формуле, что и
// при автоматическом разборе заявки (см. pricing.calc_glazing), поэтому
// сумма и подсказка ниже всегда соответствуют введённым числам. Размер
// пишется прямо в текстовое поле "остекление" — значит автоматически
// попадает и в примечание клиенту (buildNoteFromBackend читает то же поле).
function GlassFormulaEditor({ pos, glassHint, onChange }) {
  const dims = useMemo(() => parseGlazingDims(pos.glazingText), [pos.glazingText]);
  const [h, setH] = useState(dims.h);
  const [w, setW] = useState(dims.w);
  const [rate, setRate] = useState(pos.glazingPriceOverride || parseGlazingRate(glassHint));
  useEffect(() => { setH(dims.h); setW(dims.w); }, [dims.h, dims.w]);
  useEffect(() => { setRate(pos.glazingPriceOverride || parseGlazingRate(glassHint)); }, [pos.glazingPriceOverride, glassHint]);

  const commitDims = () => {
    if (!h || !w || (h === dims.h && w === dims.w)) return;
    onChange({ glazing: withGlazingDims(pos.glazingText, h, w) });
  };
  const commitRate = () => {
    const val = parseFloat(rate) || 0;
    if (val === (+pos.glazingPriceOverride || 0)) return;
    onChange({ glazing_price_override: val || null });
  };
  const numInput = (value, setValue, onBlur, placeholder, title, allowDot) => (
    <input
      type="text"
      inputMode="numeric"
      value={value}
      placeholder={placeholder}
      title={title}
      onChange={(e) => setValue(e.target.value.replace(allowDot ? /[^\d.]/g : /[^\d]/g, ""))}
      onBlur={onBlur}
      onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}
      style={{ width: 40, font: "inherit", fontSize: 11, border: "none", borderBottom: "1px dashed var(--muted)", outline: "none", background: "none", color: "inherit", textAlign: "center" }}
    />
  );
  return (
    <div className="mfield__hint" style={{ display: "flex", alignItems: "center", gap: 3, flexWrap: "wrap" }}>
      {numInput(h, setH, commitDims, "выс.", "Высота стекла, мм")}
      <span>×</span>
      {numInput(w, setW, commitDims, "шир.", "Ширина стекла, мм")}
      <span>мм ·</span>
      {numInput(rate, setRate, commitRate, "по прайсу", "Цена за м² (вручную, иначе по прайсу)", true)}
      <span>₽/м²</span>
    </div>
  );
}

/* ============================ manager panel ============================ */
function ManagerPanel({ pos, calc, onField, onDel, density, idx, priceHint, glassHint, lockHint, onGlazingChange }) {
  const m = pos.mgr;
  const [pending, setPending] = useState(new Set());

  const doorHint = useMemo(() => {
    if (priceHint) return priceHint;
    if (!+m.doors) return undefined;
    const parts = MGR_GROUPS[0].fields
      .filter(f => f.key !== "doors" && +m[f.key] > 0)
      .map(f => `+${RUB(+m[f.key])} ${f.label.toLowerCase()}`);
    return parts.length ? parts.join('  ') : undefined;
  }, [priceHint, m]);

  const areaM2 = useMemo(() => {
    const w = +pos.korobW, h = +pos.korobH;
    return (w > 0 && h > 0) ? w * h / 1e6 : null;
  }, [pos.korobW, pos.korobH]);

  const steelHint = useMemo(() => {
    if (!areaM2 || !+m.naicenkaStal) return undefined;
    const rate = Math.round(+m.naicenkaStal / areaM2);
    return `${pos.korobW}×${pos.korobH} × ${NUM(rate)} ₽/м²`;
  }, [areaM2, m.naicenkaStal, pos.korobW, pos.korobH]);

  const colorHint = useMemo(() => {
    if (!areaM2 || !+m.naicenkaColor) return undefined;
    const rate = Math.round(+m.naicenkaColor / areaM2);
    return `${pos.korobW}×${pos.korobH} × ${NUM(rate)} ₽/м²`;
  }, [areaM2, m.naicenkaColor, pos.korobW, pos.korobH]);

  const handleChange = (key, val) => {
    onField(key, val);
    if (val === 0) setPending(prev => { const s = new Set(prev); s.delete(key); return s; });
  };

  return (
    <div className="mgr" data-density={density}>
      <div className="mgr__head">
        <div className="mgr__title">
          <span className="mgr__dot" />
          {`Расчёт стоимости · позиция №${idx}`}
        </div>
        <div className="mgr__actions">
          <button className="lnk lnk--danger" onClick={onDel}>Удалить позицию</button>
        </div>
      </div>

      {pos.alerts && pos.alerts.length > 0 && (
        <div className="mgr__alert">
          <span className="wmark">!</span>
          <span>{pos.alerts.join(" · ")}</span>
        </div>
      )}

      <div className="mgr__grid">
        {MGR_GROUPS.map((group) => {
          const visibleFields = group.fields.filter(f => +m[f.key] !== 0 || pending.has(f.key));
          const hiddenFields  = group.fields.filter(f => +m[f.key] === 0 && !pending.has(f.key));
          return (
            <div key={group.key} className="mgr__group">
              <div className="mgr__group-cap">{group.label}</div>
              <div className="mgr__fields">
                {visibleFields.map(f => (
                  <React.Fragment key={f.key}>
                    <MoneyField
                      label={f.label}
                      value={m[f.key]}
                      onChange={(v) => handleChange(f.key, v)}
                      onRemove={() => handleChange(f.key, 0)}
                      hint={f.key === "doors" ? doorHint : f.key === "naicenkaStal" ? steelHint : f.key === "naicenkaColor" ? colorHint : f.key === "glass" ? glassHint : f.key === "lockMain" ? lockHint : undefined}
                    />
                    {f.key === "glass" && (
                      <GlassFormulaEditor pos={pos} glassHint={glassHint} onChange={onGlazingChange} />
                    )}
                  </React.Fragment>
                ))}
                {hiddenFields.length > 0 && (
                  <AddFieldDropdown
                    fields={hiddenFields}
                    onAdd={(key) => setPending(prev => new Set([...prev, key]))}
                  />
                )}
              </div>
            </div>
          );
        })}
      </div>

      <div className="mgr__totals">
        <div className="mgr__tot">
          <span>ИТОГО за двери</span>
          <b>{RUB(calc.itogoIzdelie)}</b>
        </div>
        <div className="mgr__tot">
          <span>Итого монтаж</span>
          <b>{RUB(calc.itogoMontazh)}</b>
        </div>
        {calc.itogoUslugi !== 0 && (
          <div className="mgr__tot">
            <span>Итого услуги</span>
            <b>{RUB(calc.itogoUslugi)}</b>
          </div>
        )}
        <div className="mgr__tot mgr__tot--grand">
          <span>Итого по позиции</span>
          <b>{RUB(calc.lineTotal)}</b>
        </div>
      </div>
    </div>
  );
}

/* ============================ main app ============================ */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  reveal: "inline",
  mgrTheme: "soft",
  density: "regular",
  accent: "#5e6ad2",
  layout: "top",
  brand: "left",
  zview: "cards",
}/*EDITMODE-END*/;

function useIsMobile() {
  const [isMobile, setIsMobile] = useState(() => window.matchMedia("(max-width: 768px)").matches);
  useEffect(() => {
    const mq = window.matchMedia("(max-width: 768px)");
    const onChange = (e) => setIsMobile(e.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);
  return isMobile;
}

function App() {
  const t = TWEAK_DEFAULTS;
  const isMobile = useIsMobile();
  const [positions, setPositions] = useState([]);
  const [reqMeta, setReqMeta] = useState(null);
  const [loading, setLoading] = useState(false);
  const [extras, setExtras] = useState({
    takelazh: 0,
    dostavka: 0,
    srokKp: "3 рабочих дня",
    srokIzg: "15–20 рабочих дней",
    srokMont: "3–5 рабочих дней",
  });
  const [openId, setOpenId] = useState(null);
  const [page, setPage] = useState("list");
  const [fromPage, setFromPage] = useState("list");
  const [zayavkaText, setZayavkaText] = useState("");
  const [reconcileData, setReconcileData] = useState(null);
  const [alertsHidden, setAlertsHidden] = useState(false);
  const [managers, setManagers] = useState([]);
  const [infoPopup, setInfoPopup] = useState(null); // { x, y, pinned, tab: "position" | "full" }
  const [installOn, setInstallOn] = useState(true);

  // кнопка "Текст заявки" в шапке открывает/закрывает подсказку; вкладка "Эта позиция"
  // следует за текущей открытой строкой (openId), "Вся заявка" — исходный текст запроса целиком.
  const toggleInfoPopup = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    setInfoPopup((cur) => {
      if (cur) return null;
      const popupWidth = Math.min(360, window.innerWidth - 24);
      const x = Math.max(12, Math.min(rect.left, window.innerWidth - popupWidth - 12));
      return { x, y: rect.bottom + 6, pinned: true, tab: openId ? "position" : "full" };
    });
  };
  const toggleInfoPin = () => {
    setInfoPopup((cur) => {
      if (!cur) return cur;
      return cur.pinned ? null : { ...cur, pinned: true };
    });
  };
  const setInfoTab = (tab) => {
    setInfoPopup((cur) => (cur ? { ...cur, tab } : cur));
  };

  useEffect(() => {
    apiFetch("GET", "/managers").then(setManagers).catch((e) => console.error("load managers error", e));
  }, []);

  const setManager = (managerKey) => {
    setReqMeta((m) => (m ? { ...m, manager_key: managerKey } : m));
    apiFetch("PATCH", `/requests/${reqMeta.id}/manager`, { manager_key: managerKey })
      .catch((e) => console.error("save manager error", e));
  };

  // Переключатель "с монтажем/без монтажа" на весь КП — не пересчитывает и не
  // трогает сами суммы монтажа у позиций, только скрывает их в calcPos (см.
  // ferro-data.jsx) и на сервере при экспорте/письме (см. include_install в
  // main.py) — переключать туда-обратно можно без потери цифр.
  const toggleInstall = () => {
    const next = !installOn;
    setInstallOn(next);
    apiFetch("PATCH", `/requests/${reqId}/install`, { include_install: next })
      .catch((e) => console.error("save install toggle error", e));
  };

  const loadRequest = async (reqId) => {
    setLoading(true);
    try {
      const data = await apiFetch("GET", `/requests/${reqId}`);
      setReqMeta(data);
      setInstallOn((data.include_install ?? 1) != 0);
      setPositions((data.positions || []).map((p) => backendToPos(p, data.mode)));
      // Авторасчёт такелажа: 1400 однопольная, 2000 двупольная, +1000 если фрамуга
      let tkSingle = 0, tkDouble = 0, tkFramuga = 0;
      (data.positions || []).forEach(p => {
        if (p.kind === 'closer') return;
        const q = p.quantity || 1;
        if ((p.sash || '').includes('2')) tkDouble += q; else tkSingle += q;
        if (p.transom) tkFramuga += q;
      });
      const takelazh = tkSingle * 1400 + tkDouble * 2000 + tkFramuga * 1000;
      const tkParts = [];
      if (tkSingle)  tkParts.push(`${tkSingle}×1 400`);
      if (tkDouble)  tkParts.push(`${tkDouble}×2 000`);
      if (tkFramuga) tkParts.push(`${tkFramuga}×1 000 (фр.)`);
      const takelazh_hint = tkParts.join(' + ');
      const dzone = DELIVERY_ZONES.find(z => z.label === data.delivery_zone);
      const dostavka = dzone?.cost || 0;
      const dostavka_hint = data.delivery_zone || "";
      setExtras((e) => ({ ...e, takelazh, takelazh_hint, dostavka, dostavka_hint }));
      setPage("kp");
      setAlertsHidden(false);
      setOpenId(null);
    } catch (e) {
      console.error("load request error", e);
    } finally {
      setLoading(false);
    }
  };

  const sumIzdelie = useMemo(() => positions.reduce((s, p) => s + calcPos(p, installOn).itogoIzdelie, 0), [positions, installOn]);
  const sumUslugi  = useMemo(() => positions.reduce((s, p) => s + calcPos(p, installOn).itogoUslugi,  0), [positions, installOn]);
  const sumMontazh = useMemo(() => positions.reduce((s, p) => s + calcPos(p, installOn).itogoMontazh, 0), [positions, installOn]);
  const total = sumIzdelie + sumUslugi + sumMontazh + (+extras.takelazh || 0) + (+extras.dostavka || 0);

  // generic field setter for plain (non-mgr_json) DB columns edited directly in the table
  const setPosField = (id, displayKey, dbField, val) => {
    setPositions((ps) => ps.map((p) => (p.id === id ? { ...p, [displayKey]: val } : p)));
    apiFetch("PATCH", `/positions/${id}`, { data: { [dbField]: val } })
      .catch((e) => console.error("save field error", e));
  };

  // patches given DB fields then re-prices the position (door price + all price-dependent
  // markups: цвет/сталь/доводчик/порог/ручка/замок/остекление/фрамуга). mgr_json overrides
  // are preserved — backendToPos() re-merges them on top of the freshly computed defaults.
  const patchAndRecalc = async (id, patch) => {
    try {
      await apiFetch("PATCH", `/positions/${id}`, { data: patch });
      const res = await apiFetch("POST", `/positions/${id}/recalc-price`);
      if (res && res.position) {
        const fresh = backendToPos(res.position, reqMeta.mode);
        setPositions((ps) => ps.map((p) => (p.id === id ? fresh : p)));
      }
    } catch (e) {
      console.error("patch+recalc error", e);
    }
  };

  // same as setPosField, but for fields that affect price (RAL/порог) — re-prices after saving.
  const setPosFieldAndRecalc = (id, displayKey, dbField, val) => {
    setPositions((ps) => ps.map((p) => (p.id === id ? { ...p, [displayKey]: val } : p)));
    patchAndRecalc(id, { [dbField]: val });
  };

  // короб ↔ проём: всегда зависимы (проём = короб + 20мм по высоте / + 40мм по ширине).
  // Правка любой стороны автоматически пересчитывает другую.
  const SIZE_PAIRS = {
    proemH: { self: "height_proem", pairDisplay: "korobH", pairDb: "height",       offset: -20 },
    proemW: { self: "width_proem",  pairDisplay: "korobW", pairDb: "width",        offset: -40 },
    korobH: { self: "height",       pairDisplay: "proemH", pairDb: "height_proem", offset: 20 },
    korobW: { self: "width",        pairDisplay: "proemW", pairDb: "width_proem",  offset: 40 },
  };
  const setSizeField = (id, kind, val) => {
    const cfg = SIZE_PAIRS[kind];
    const pairVal = val == null ? null : val + cfg.offset;
    setPositions((ps) => ps.map((p) => (p.id === id
      ? { ...p, [kind]: val, ...(pairVal != null ? { [cfg.pairDisplay]: pairVal } : {}) }
      : p)));
    const patch = { [cfg.self]: val };
    if (pairVal != null) patch[cfg.pairDb] = pairVal;
    patchAndRecalc(id, patch);
  };

  // система антипаники — одно устройство на замок+ручку: выбор любого из 3 вариантов
  // в любом из двух полей зеркалится в другое. Обычные замки/ручки независимы.
  const setLockOrHandle = (id, kind, val) => {
    const isLock = kind === "zamok";
    const selfDb = isLock ? "lock_" : "handle";
    const otherDisplay = isLock ? "ruchka" : "zamok";
    const otherDb = isLock ? "handle" : "lock_";
    const mirror = window.ANTIPANIC_VALUES.includes(val);

    setPositions((ps) => ps.map((p) => (p.id === id
      ? { ...p, [kind]: val, ...(mirror ? { [otherDisplay]: val } : {}) }
      : p)));

    const patch = { [selfDb]: val };
    if (mirror) patch[otherDb] = val;
    patchAndRecalc(id, patch);
  };

  // Довод.: упрощённый Да/Нет. "Да" пишет стандартную модель, "Нет" — "без доводчика".
  // Конкретная модель (если была) теряется при переключении на "Нет" — это осознанное огрубление поля.
  const setDovodchik = (id, isYes) => {
    const val = isYes ? "Доводчик рычажный" : "без доводчика";
    setPositions((ps) => ps.map((p) => (p.id === id ? { ...p, dovodchik: isYes } : p)));
    patchAndRecalc(id, { closer: val });
  };

  // Примечание: ручной текст полностью заменяет автособранный. Пустое значение — возврат к автосборке.
  const setNote = (id, val) => {
    setPositions((ps) => ps.map((p) => (p.id === id ? { ...p, note: val || p.noteAuto } : p)));
    apiFetch("PATCH", `/positions/${id}`, { data: { note_override: val || null } })
      .catch((e) => console.error("save note error", e));
  };

  // saves "Тип дверей" and re-prices the position from the price list (door price + all
  // type-dependent markups). Existing manual mgr_json overrides on other fields are kept —
  // backendToPos() re-merges them on top of the freshly computed defaults.
  // [ПРАВИЛО] EI-90/EIS-90 в прайсе нет — при выборе в списке сразу подставляется EI-60/EIS-60
  // (как и при автораспознавании из текста заявки, см. extractor.detect_type/parser._eiclass_map),
  // с тем же предупреждением менеджеру — иначе клиент увидит в КП класс, который мы не поставляем.
  const saveTypeAndRecalc = (id, val) => {
    let resolvedVal = val;
    let extraAlert = null;
    const m = /(EIS?)-90\b/.exec(val || "");
    if (m) {
      resolvedVal = (val || "").replace(/EIS?-90\b/, m[1] + "-60");
      extraAlert = `Запрошен класс ${m[0]} — в прайсе только EI-60, требуется добровольная сертификация/уточнение`;
    }
    setPositions((ps) => ps.map((p) => (p.id === id ? { ...p, doorType: resolvedVal || "—" } : p)));
    const patch = { type: resolvedVal };
    if (extraAlert) {
      const pos = positions.find((p) => p.id === id);
      const alerts = (pos && pos.alerts) || [];
      if (!alerts.includes(extraAlert)) {
        patch.manager_alerts = JSON.stringify([...alerts, extraAlert]);
      }
    }
    patchAndRecalc(id, patch);
  };

  const setField = (id, key, val) => {
    setPositions((ps) => {
      const updated = ps.map((p) => p.id === id ? { ...p, mgr: { ...p.mgr, [key]: val } } : p);
      const pos = updated.find((p) => p.id === id);
      if (pos) {
        apiFetch("PATCH", `/positions/${id}`, { data: { mgr_json: JSON.stringify(pos.mgr) } })
          .catch((e) => console.error("save mgr error", e));
      }
      return updated;
    });
  };

  const delPosition = async (id) => {
    try {
      await apiFetch("DELETE", `/positions/${id}`);
    } catch (e) {
      console.error("delete position error", e);
    }
    setPositions((ps) => ps.filter((p) => p.id !== id));
    setOpenId((o) => (o === id ? null : o));
  };

  // пустая позиция в конец списка — нумерация (i+1 в рендере) идёт по порядку в массиве,
  // не по id, так что вставка в конец никогда не сбивает номера предыдущих позиций.
  const addPosition = async () => {
    try {
      const res = await apiFetch("POST", `/requests/${reqMeta.id}/positions`);
      if (res && res.position) {
        const fresh = backendToPos(res.position, reqMeta.mode);
        setPositions((ps) => [...ps, fresh]);
        setOpenId(fresh.id);
      }
    } catch (e) {
      console.error("add position error", e);
    }
  };

  const toggle = (id) => setOpenId((o) => (o === id ? null : id));

  const alertPositions = positions.filter((p) => p.alerts && p.alerts.length);

  const reqId = reqMeta ? reqMeta.id : "";
  const reqStatus = reqMeta ? statusLabel(reqMeta.status) : "";
  const reqDate = reqMeta ? fmtDate(reqMeta.created_at) : "";
  const calcVerified = reqMeta
    ? (reqMeta.calc_verified === null || reqMeta.calc_verified === undefined ? null : !!reqMeta.calc_verified)
    : null;
  const [verifyPrompt, setVerifyPrompt] = useState(false);
  const [pendingDownload, setPendingDownload] = useState(null);

  const saveVerified = async (val) => {
    setVerifyPrompt(false);
    setReqMeta(m => m ? { ...m, calc_verified: val } : m);
    await apiFetch('PATCH', `/requests/${reqId}/verify`, { verified: val }).catch(() => {});
    if (pendingDownload) { window.open(pendingDownload, '_blank'); setPendingDownload(null); }
  };

  const downloadWithVerify = (url) => {
    setPendingDownload(url);
    setVerifyPrompt(true);
  };

  return (
    <div className="app" data-mgr-theme={t.mgrTheme} data-layout={t.layout} style={{ "--accent": t.accent }}>
      <main className="main">
        <header className="top">
          <div className="brand brand--bar">
            <div className="brand__logo">F</div>
            <div className="brand__name">Ferrolend</div>
          </div>
          <nav className="topnav">
            <button
              className={"btn btn--ghost" + (page === "list" ? " btn--nav-on" : "")}
              onClick={() => setPage("list")}
            >Заявки</button>
            <button
              className={"btn btn--ghost" + (page === "new" ? " btn--nav-on" : "")}
              onClick={() => setPage("new")}
            >Новый расчёт</button>
            <button
              className={"btn btn--ghost" + (page === "tender" ? " btn--nav-on" : "")}
              onClick={() => setPage("tender")}
            >Тендер</button>
            <a className="btn btn--ghost" href="/corrections" target="_blank">Правки</a>
          </nav>
          <div className="top__sp" />
          <ChangelogButton />
          {page === "kp" && (
            <>
              <button
                type="button"
                className={"btn btn--ghost infobtn" + (infoPopup ? " btn--nav-on" : "")}
                onClick={toggleInfoPopup}
              >Текст заявки</button>
              <div className="dl-btns">
                <button className="btn btn--ghost" onClick={() => downloadWithVerify(`/api/requests/${reqId}/kp?takelazh=${+extras.takelazh || 0}&dostavka=${+extras.dostavka || 0}`)}><DownloadIcon />Excel</button>
                <button className="btn btn--ghost" onClick={() => downloadWithVerify(`/api/requests/${reqId}/pdf`)}><DownloadIcon />PDF</button>
                {verifyPrompt && (
                  <div className="verify-popup">
                    <span>Расчёт правильный?</span>
                    <button className="verify-popup__yes" onClick={() => saveVerified(true)}>✓ Да</button>
                    <button className="verify-popup__no"  onClick={() => saveVerified(false)}>✕ Нет</button>
                  </div>
                )}
              </div>
              <button className="btn btn--solid">Отправить клиенту</button>
            </>
          )}
        </header>

        {loading && (
          <div style={{ padding: 48, color: "var(--muted)", textAlign: "center" }}>Загрузка…</div>
        )}

        {!loading && page === "list" && (
          <ZayavkiPage onOpen={(id) => { setFromPage("list"); loadRequest(id); }} onNew={() => setPage("new")} />
        )}
        {!loading && page === "new" && (
          <NewCalcPage
            onCalc={(id) => { setFromPage("new"); loadRequest(id); }}
            text={zayavkaText}
            onTextChange={setZayavkaText}
            onReconcile={(data) => { setReconcileData(data); setFromPage("new"); setPage("reconcile"); }}
          />
        )}
        {!loading && page === "tender" && <TenderCheckPage />}
        {!loading && page === "reconcile" && (
          <ReconcilePage data={reconcileData} onBack={() => setPage(fromPage)} />
        )}

        {!loading && page === "kp" && (
          <>
            <div className="dochead" data-screen-label="Коммерческое предложение">
              <div className="dochead__left">
                <button className="back" onClick={() => setPage(fromPage)}>Назад</button>
                <ReprocessBtn reqId={reqId} onDone={() => loadRequest(reqId)} />
                <div>
                  <h1 className="ttl">Коммерческое предложение №{reqId}</h1>
                  <div className="dochead__meta">
                    <Pill>{reqStatus}</Pill>
                    {calcVerified === true  && <span className="pill pill--ok">Расчёт верный</span>}
                    {calcVerified === false && <span className="pill pill--err">Ошибки в расчёте</span>}
                    <span>Поставка и монтаж металлических дверей</span>
                    <span className="sep">·</span>
                    <span>{positions.length} позиций</span>
                    <span className="sep">·</span>
                    <span>{reqDate}</span>
                    <span className="sep">·</span>
                    <span className="mgrsel">
                      Менеджер:
                      <select
                        className="mgrsel__input"
                        value={reqMeta?.manager_key || ""}
                        onChange={(e) => setManager(e.target.value)}
                      >
                        {managers.map((m) => (
                          <option key={m.key} value={m.key}>{m.name}</option>
                        ))}
                      </select>
                    </span>
                    <span className="sep">·</span>
                    <button
                      type="button"
                      className={"btn btn--ghost" + (installOn ? " btn--nav-on" : "")}
                      onClick={toggleInstall}
                      title="Включить/выключить монтаж в расчёте КП — переключается без потери сумм"
                    >{installOn ? "Монтаж: включён" : "Монтаж: выключен"}</button>
                  </div>
                </div>
              </div>
              <div className="dochead__total">
                <span>Итого с НДС</span>
                <b>{RUB(total)}</b>
              </div>
            </div>

            {!alertsHidden && alertPositions.length > 0 && (
              <div className="alertbar">
                <div className="alertbar__head">
                  <span className="alertbar__icon"><Warn size={17} /></span>
                  <b>{`Алгоритм рассчитал коммерческое предложение — ${alertPositions.length === 1 ? "1 предупреждение" : alertPositions.length + " предупреждения"}`}</b>
                  <span className="alertbar__sub">видно только менеджеру</span>
                  <button className="alertbar__close" onClick={() => setAlertsHidden(true)}>Понятно</button>
                </div>
                <ul className="alertbar__list">
                  {alertPositions.map((p) => (
                    <li key={p.id}>
                      <button className="alertbar__link" onClick={() => setOpenId(p.id)}>
                        Позиция {positions.indexOf(p) + 1}
                      </button>
                      <span>{p.alerts.join(" · ")}</span>
                    </li>
                  ))}
                </ul>
              </div>
            )}

            <div className="hintrow">Нажмите на позицию — раскроется расчёт стоимости</div>

            <>
              {!isMobile && (
              <div className="tablewrap">
                <table className="kp">
                  <thead>
                    <tr className="hgrp">
                      <th className="c-idx" rowSpan={2}>№</th>
                      <th className="c-proj" rowSpan={2}>Проект</th>
                      <th className="c-type" rowSpan={2}>Тип дверей</th>
                      <th className="c-grp" colSpan={2}>Размер по проёму</th>
                      <th className="c-grp" colSpan={2}>Размер по коробке</th>
                      <th className="c-num" rowSpan={2}>Кол-во</th>
                      <th className="c-attr" rowSpan={2}>Раб. створка</th>
                      <th className="c-attr" rowSpan={2}>Нал.</th>
                      <th className="c-attr" rowSpan={2}>Открыв.</th>
                      <th className="c-attr" rowSpan={2}>RAL</th>
                      <th className="c-attr" rowSpan={2}>Порог</th>
                      <th className="c-grp" colSpan={2}>Фурнитура</th>
                      <th className="c-attr" rowSpan={2}>Довод.</th>
                      <th className="c-note" rowSpan={2}>Примечание</th>
                      <th className="c-money" rowSpan={2}>Цена за изделие</th>
                      <th className="c-money" rowSpan={2}>Итого за изделия</th>
                      <th className="c-money" rowSpan={2}>Демонтаж</th>
                      <th className="c-money" rowSpan={2}>Монтажные работы</th>
                      <th className="c-money" rowSpan={2}>Итого за монтаж. работы</th>
                    </tr>
                    <tr className="hsub">
                      <th className="c-sz">высота</th>
                      <th className="c-sz">ширина</th>
                      <th className="c-sz">высота</th>
                      <th className="c-sz">ширина</th>
                      <th className="c-fur">замок</th>
                      <th className="c-fur">ручка</th>
                    </tr>
                  </thead>
                  <tbody>
                    {positions.map((p, i) => {
                      const calc = calcPos(p, installOn);
                      const open = openId === p.id;
                      return (
                        <React.Fragment key={p.id}>
                          <tr
                            className={"krow" + (open ? " krow--open" : "")}
                            onClick={() => toggle(p.id)}
                          >
                            <td className="c-idx">{i + 1}</td>
                            <td className="c-proj">
                              <SourceTip source={p.fieldSources.проект}>
                                <CellInput
                                  value={p.proekt === "—" ? "" : p.proekt}
                                  placeholder="—"
                                  onSave={(v) => setPosField(p.id, "proekt", "project_code", v || "—")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-type">
                              <SourceTip source={p.fieldSources.тип}>
                                <div className="nm">
                                  {p.alerts.length > 0 && (
                                    <span className="wmark" title={p.alerts.join("\n")}><Warn /></span>
                                  )}
                                  <DatalistCell
                                    value={p.doorType}
                                    options={p.kind === "closer" ? window.CLOSER_TYPE_OPTIONS : window.DOOR_TYPE_OPTIONS}
                                    onSave={(v) => saveTypeAndRecalc(p.id, v)}
                                  />
                                </div>
                              </SourceTip>
                            </td>
                            <td className="c-sz">
                              <SourceTip source={p.fieldSources.размер_проем}>
                                <NumberCell
                                  value={p.proemH}
                                  onSave={(v) => setSizeField(p.id, "proemH", v)}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-sz">
                              <SourceTip source={p.fieldSources.размер_проем}>
                                <NumberCell
                                  value={p.proemW}
                                  onSave={(v) => setSizeField(p.id, "proemW", v)}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-sz">
                              <SourceTip source={p.fieldSources.размер_короб}>
                                <NumberCell
                                  value={p.korobH}
                                  onSave={(v) => setSizeField(p.id, "korobH", v)}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-sz">
                              <SourceTip source={p.fieldSources.размер_короб}>
                                <NumberCell
                                  value={p.korobW}
                                  onSave={(v) => setSizeField(p.id, "korobW", v)}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-num">
                              <NumberCell
                                value={p.qty}
                                onSave={(v) => setPosField(p.id, "qty", "quantity", Math.max(1, v || 1))}
                              />
                            </td>
                            <td className="c-attr">
                              <SourceTip source={p.fieldSources.раб_створка}>
                                <NumberCell
                                  value={p.rabStvorka === "—" ? null : p.rabStvorka}
                                  placeholder="—"
                                  onSave={(v) => setPosField(p.id, "rabStvorka", "active_leaf_width", v)}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-attr">
                              <SourceTip source={p.fieldSources.наличник}>
                                <DatalistCell
                                  value={p.nalichnik}
                                  options={window.NALICHNIK_OPTIONS}
                                  onSave={(v) => setPosField(p.id, "nalichnik", "nalichnik", v || "без наличника")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-attr">
                              <SourceTip source={p.fieldSources.сторона}>
                                <DatalistCell
                                  value={p.opening}
                                  options={window.OPENING_OPTIONS}
                                  onSave={(v) => setPosField(p.id, "opening", "side", v || "?")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-attr c-ral">
                              <SourceTip source={p.fieldSources.ral}>
                                <CellInput
                                  value={p.ral === "—" ? "" : p.ral}
                                  placeholder="—"
                                  onSave={(v) => setPosFieldAndRecalc(p.id, "ral", "color", v || "—")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-attr">
                              <SourceTip source={p.fieldSources.порог}>
                                <DatalistCell
                                  value={p.porog}
                                  options={window.POROG_OPTIONS}
                                  onSave={(v) => setPosFieldAndRecalc(p.id, "porog", "threshold", v || "без порога")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-fur">
                              <SourceTip source={p.fieldSources.замок} defaultText="По умолчанию/из ручки — в тексте заявки не указано отдельной фразой">
                                <DatalistCell
                                  value={p.zamok}
                                  options={window.ZAMOK_OPTIONS}
                                  onSave={(v) => setLockOrHandle(p.id, "zamok", v || "—")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-fur">
                              <SourceTip source={p.fieldSources.ручка}>
                                <DatalistCell
                                  value={p.ruchka}
                                  options={window.RUCHKA_OPTIONS}
                                  onSave={(v) => setLockOrHandle(p.id, "ruchka", v || "—")}
                                />
                              </SourceTip>
                            </td>
                            <td className="c-attr">
                              <SourceTip source={p.fieldSources.доводчик} defaultText="По умолчанию/по типу двери — в тексте заявки не указано отдельной фразой">
                              {p.kind === "closer" ? "—" : (
                                <>
                                  <select
                                    className="cell-select"
                                    value={p.dovodchik ? "да" : "нет"}
                                    onClick={(e) => e.stopPropagation()}
                                    onChange={(e) => setDovodchik(p.id, e.target.value === "да")}
                                  >
                                    <option value="да">Да</option>
                                    <option value="нет">Нет</option>
                                  </select>
                                  {p.dovodchik && p.dovodchikType && (
                                    <div className="c-attr__sub">{p.dovodchikType}, {p.dovodchikCount} шт.</div>
                                  )}
                                </>
                              )}
                              </SourceTip>
                            </td>
                            <td className="c-note">
                              <NoteCell pos={p} onSave={(v) => setNote(p.id, v)} />
                            </td>
                            <td className="c-money">
                              {RUB(calc.priceEach)}
                            </td>
                            <td className="c-money c-money--strong">{RUB(calc.itogoIzdelie)}</td>
                            <td className="c-money">{RUB(calc.demontazh)}</td>
                            <td className="c-money">{RUB(calc.montazhRaboty)}</td>
                            <td className="c-money c-money--strong">{RUB(calc.itogoMontazh)}</td>
                          </tr>
                          {open && (
                            <tr className="mgr-row">
                              <td colSpan={22}>
                                <ManagerPanel
                                  pos={p}
                                  calc={calc}
                                  idx={i + 1}
                                  density={t.density}
                                  onField={(k, v) => setField(p.id, k, v)}
                                  onDel={() => delPosition(p.id)}
                                  priceHint={p.priceHint}
                                  glassHint={p.glassHint}
                                  lockHint={p.lockHint}
                                  onGlazingChange={(patch) => patchAndRecalc(p.id, patch)}
                                />
                              </td>
                            </tr>
                          )}
                        </React.Fragment>
                      );
                    })}
                  </tbody>
                </table>
                <button className="addpos" onClick={addPosition}>+ Добавить позицию</button>
              </div>
              )}

              {isMobile && (
              <div className="poscards">
                {positions.map((p, i) => {
                  const calc = calcPos(p, installOn);
                  const open = openId === p.id;
                  return (
                    <React.Fragment key={p.id}>
                      <div
                        className={"poscard" + (open ? " poscard--open" : "")}
                        onClick={() => toggle(p.id)}
                      >
                        <div className="poscard__top">
                          <span className="poscard__idx">{i + 1}</span>
                          <div className="poscard__type">
                            {p.alerts.length > 0 && (
                              <span className="wmark" title={p.alerts.join("\n")}><Warn /></span>
                            )}
                            {p.doorType}
                          </div>
                          <div className="poscard__total">{RUB(calc.itogoIzdelie)}</div>
                        </div>
                        <div className="poscard__meta">
                          <span>{p.proemH}×{p.proemW} мм</span>
                          <span>· {p.qty} шт</span>
                          {p.ral && <span>· RAL {p.ral}</span>}
                          {p.porog && <span>· {p.porog}</span>}
                        </div>
                        {p.proekt && p.proekt !== "—" && <div className="poscard__proj">{p.proekt}</div>}
                      </div>
                      {open && (
                        <div className="poscard-detail">
                          <ManagerPanel
                            pos={p}
                            calc={calc}
                            idx={i + 1}
                            density={t.density}
                            onField={(k, v) => setField(p.id, k, v)}
                            onDel={() => delPosition(p.id)}
                            priceHint={p.priceHint}
                            glassHint={p.glassHint}
                            lockHint={p.lockHint}
                            onGlazingChange={(patch) => patchAndRecalc(p.id, patch)}
                          />
                        </div>
                      )}
                    </React.Fragment>
                  );
                })}
                <button className="addpos" onClick={addPosition}>+ Добавить позицию</button>
              </div>
              )}

              <KpFooter
                sumIzdelie={sumIzdelie}
                sumMontazh={sumMontazh}
                extras={extras}
                setExtras={setExtras}
                total={total}
              />
            </>
          </>
        )}
      </main>
      {infoPopup && (() => {
        const openPos = positions.find((p) => p.id === openId);
        const fullTextNormalized = positions
          .map((p, i) => `[Позиция ${i + 1}]\n${p.positionTextNormalized || "(нет данных)"}`)
          .join("\n\n");
        return (
          <InfoPopup
            x={infoPopup.x}
            y={infoPopup.y}
            pinned={infoPopup.pinned}
            tab={infoPopup.tab}
            positionText={openPos ? openPos.positionTextNormalized : ""}
            fullTextNormalized={fullTextNormalized}
            fullTextRaw={reqMeta ? (reqMeta.raw_text_edited || reqMeta.raw_text) : ""}
            originalText={reqMeta ? reqMeta.raw_text_original || "" : ""}
            hasOpenPosition={!!openPos}
            reqId={reqId}
            onRawTextDone={() => loadRequest(reqId)}
            onClose={() => setInfoPopup(null)}
            onTogglePin={toggleInfoPin}
            onSetTab={setInfoTab}
            onMove={(x, y) => setInfoPopup((c) => (c ? { ...c, x, y } : c))}
          />
        );
      })()}
    </div>
  );
}

/* ============================ КП footer (classic) ============================ */
function FootMoney({ value, onChange, hint }) {
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(String(value));
  useEffect(() => { if (!editing) setRaw(String(value)); }, [value, editing]);
  if (editing) {
    return (
      <input
        className="foot__input"
        autoFocus
        value={raw}
        inputMode="numeric"
        onChange={(e) => setRaw(e.target.value.replace(/[^\d.-]/g, ""))}
        onBlur={() => { setEditing(false); onChange(parseFloat(raw) || 0); }}
        onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setRaw(String(value)); setEditing(false); } }}
      />
    );
  }
  return (
    <span style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 1 }}>
      <button className="foot__val foot__val--edit" onClick={() => setEditing(true)} title="Редактировать">{RUB(value)}</button>
      {hint && <span style={{ fontSize: 10, color: "#9ca3af", lineHeight: 1.3 }}>{hint}</span>}
    </span>
  );
}

function FootText({ value, onChange }) {
  const [editing, setEditing] = useState(false);
  const [raw, setRaw] = useState(value);
  useEffect(() => { if (!editing) setRaw(value); }, [value, editing]);
  if (editing) {
    return (
      <input
        className="foot__input foot__input--text"
        autoFocus
        value={raw}
        onChange={(e) => setRaw(e.target.value)}
        onBlur={() => { setEditing(false); onChange(raw.trim() || value); }}
        onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); if (e.key === "Escape") { setRaw(value); setEditing(false); } }}
      />
    );
  }
  return <button className="foot__val foot__val--edit foot__val--text" onClick={() => setEditing(true)} title="Редактировать">{value}</button>;
}

function KpFooter({ sumIzdelie, sumMontazh, extras, setExtras, total }) {
  return (
    <div className="kpfoot">
      <div className="kpfoot__col">
        <div className="kpfoot__cap">Комплектация</div>
        <ul className="kpfoot__list">
          <li>Сложно-гнутое дверное полотно (лист 1,2 мм / 1,5 мм / 2 мм)</li>
          <li>Дверная коробка (сложный гнуто-сварной профиль)</li>
          <li>Петли с подшипниками</li>
          <li>Стандарт. RAL 7035, 7040, 8017, 9016, порошковое покрытие</li>
          <li>Замок FUARO (возможна замена на аналог)</li>
          <li>Шпингалеты врезные</li>
        </ul>
      </div>
      <div className="kpfoot__col">
        <div className="kpfoot__cap">Сроки</div>
        <ul className="kpfoot__list kpfoot__list--plain">
          <li><b>Срок коммерческого предложения</b> — <FootText value={extras.srokKp} onChange={(v) => setExtras((e) => ({ ...e, srokKp: v }))} /></li>
          <li><b>Срок изготовления</b> — <FootText value={extras.srokIzg} onChange={(v) => setExtras((e) => ({ ...e, srokIzg: v }))} /></li>
          <li><b>Срок монтажных работ</b> — <FootText value={extras.srokMont} onChange={(v) => setExtras((e) => ({ ...e, srokMont: v }))} /></li>
        </ul>
        <p className="kpfoot__note">* В стоимость монтажа изделий входит установка дверей в стеновые проёмы и их герметизация без срезки монтажной пены, отделки откосов, покраски и облицовки.</p>
      </div>
      <div className="kpfoot__col kpfoot__col--totals">
        <div className="foot__row"><span>Итого за изготовление материалов</span><span className="foot__val">{RUB(sumIzdelie)}</span></div>
        <div className="foot__row"><span>Такелаж</span><FootMoney value={extras.takelazh} onChange={(v) => setExtras((e) => ({ ...e, takelazh: v }))} hint={extras.takelazh_hint} /></div>
        <div className="foot__row">
          <span>Итого за монтажные работы</span>
          {sumMontazh === 0
            ? <span style={{color:"#9ca3af",fontSize:13}}>Без монтажных работ</span>
            : <span className="foot__val">{RUB(sumMontazh)}</span>}
        </div>
        <div className="foot__row"><span>Доставка</span><FootMoney value={extras.dostavka} onChange={(v) => setExtras((e) => ({ ...e, dostavka: v }))} hint={extras.dostavka_hint} /></div>
        <div className="foot__row foot__row--grand"><span>Итого за поставку в т.ч. НДС 22%</span><span className="foot__val">{RUB(total)}</span></div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
