/* ctor-settings.jsx — настройки Конструктора (подпроект 4, план 2026-10-05-ctor-settings.md), разнесённые по смыслу
   (спека 2026-10-06-ctor-economy-design.md): «Экономика» — цены и налоги, коэффициенты, договор и оплаты, контроль сметы, доли (исполнитель / прораб / компания);
   «Процессы» — календарь работ, доставка на объект; «Бухгалтерия» — реквизиты компании, КП (тексты коммерческого предложения), нумерация документов.
   Каждый пункт меню — своя панель (StgPricesPanel, StgCoefsPanel, StgContractPanel, StgControlPanel, StgSharesPanel, StgCalendarPanel,
   StgDeliveryPanel, StgCompanyPanel, StgOfferPanel, StgNumberingPanel); все правят одно и то же состояние настроек App и одну публикацию
   (unit settings). Прежняя общая панель StgSettingsPanel осталась обёрткой над всеми карточками, но в меню не подключена.
   Данные — window.ObitelSettings (lk/settings-store.js, ключ obitel_settings_v1); раздел публикуемый:
   правки автосохраняются в черновик (App в «Конструктор проектов.html»), «Опубликовать» делает их рабочими.
   Classic-babel скрипты Конструктора делят одну область видимости — все имена верхнего уровня с префиксом stg. */
const { useState: stgUseState, useEffect: stgUseEffect } = React;

const STG_TAX = [{ v: 'usn', l: 'УСН, без НДС' }, { v: 'vat20', l: 'С НДС 20 %' }];
const STG_RESERVE_SHOW = [{ v: 'line', l: 'Строкой в сводке' }, { v: 'inside', l: 'Вшить в цены работ' }];
const STG_ROUNDING = [{ v: 0, l: 'Нет' }, { v: 10, l: 'До 10 ₽' }, { v: 100, l: 'До 100 ₽' }, { v: 1000, l: 'До 1000 ₽' }];
const STG_SCHEDULE = [{ v: 'phase', l: 'По фазам' }, { v: 'stage', l: 'По этапам' }];
const STG_WEEKDAYS = [[1, 'пн'], [2, 'вт'], [3, 'ср'], [4, 'чт'], [5, 'пт'], [6, 'сб'], [0, 'вс']]; // [Date.getDay(), подпись]
const STG_DOCS = [['estimate', 'Сметы'], ['contract', 'Договоры'], ['act', 'Акты']];
const STG_LOGO_MAX = 300 * 1024;
/* производственный календарь РФ: праздники без переносов выходных — недостающее владелец добавляет вручную */
const STG_HOLIDAYS_MD = ['01-01', '01-02', '01-03', '01-04', '01-05', '01-06', '01-07', '01-08', '02-23', '03-08', '05-01', '05-09', '06-12', '11-04'];
const STG_YEARS = [2026, 2027];
/* условия сложности — поля анкеты объекта (их же читает смета: Calc2 / ObitelSettings.complexity) */
const STG_FIELDS = [
  { id: 'ceilingApprox', label: 'Потолки, м', kind: 'number', ops: [['>', 'выше'], ['≥', 'не ниже'], ['<', 'ниже'], ['=', 'равны']], def: 3 },
  { id: 'objectType', label: 'Тип объекта', kind: 'enum', ops: [['=', 'это']], options: [['new_build', 'Новостройка'], ['secondary', 'Вторичка']] },
  { id: 'wallMaterial', label: 'Материал стен', kind: 'enum', ops: [['has', 'включает']], options: () => ((window.ConstructorData && window.ConstructorData.WALL_MATERIALS) || []).map((w) => [w.id, w.label]) },
];

const stgFieldById = (id) => STG_FIELDS.find((f) => f.id === id) || STG_FIELDS[0];
const stgOptions = (f) => (typeof f.options === 'function' ? f.options() : f.options || []);
const stgIsoDate = (y, md) => y + '-' + md;
const stgFmtDate = (iso) => {
  const d = new Date(iso + 'T00:00:00');
  return isNaN(d) ? iso : d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' }).replace(' г.', '');
};
const stgUid = (p) => p + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);

/* ───────── примитивы ───────── */

/* optional — пустое поле = «не задано»: onChange(null) (пороги контроля, наценки по подвидам); без него пустое поле ничего не меняет */
function StgNum({ value, onChange, min = 0, max, step = 1, suffix, width = 88, optional, placeholder }) {
  const [txt, setTxt] = stgUseState(String(value == null ? '' : value));
  const [focus, setFocus] = stgUseState(false);
  stgUseEffect(() => { if (!focus) setTxt(String(value == null ? '' : value)); }, [value, focus]);
  const commit = (raw) => {
    setTxt(raw);
    if (raw === '') { if (optional) onChange(null); return; }
    let n = Number(String(raw).replace(',', '.'));
    if (!isFinite(n)) return;
    if (min != null) n = Math.max(min, n);
    if (max != null) n = Math.min(max, n);
    onChange(n);
  };
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
      <input type="number" min={min} max={max} step={step} value={txt} placeholder={placeholder} style={{ ...ctorStyles.input, width, padding: '8px 10px' }}
        onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} onChange={(e) => commit(e.target.value)} />
      {suffix && <span style={{ fontSize: 12.5, color: 'var(--text-tertiary)', whiteSpace: 'nowrap' }}>{suffix}</span>}
    </span>
  );
}

function StgText({ value, onChange, placeholder, width, maxLength }) {
  return <input type="text" value={value == null ? '' : value} placeholder={placeholder} maxLength={maxLength} style={{ ...ctorStyles.input, ...(width ? { width } : {}) }} onChange={(e) => onChange(e.target.value)} />;
}

function StgSeg({ value, options, onChange }) {
  return (
    <div style={ctorStyles.segmentedTrack} role="group">
      {options.map((o) => <button key={String(o.v)} type="button" onClick={() => onChange(o.v)} style={ctorStyles.segmentedBtn(value === o.v)}>{o.l}</button>)}
    </div>
  );
}

function StgSwitch({ value, onChange, label }) {
  return (
    <button type="button" role="switch" aria-checked={!!value} aria-label={label} onClick={() => onChange(!value)}
      style={{ width: 34, height: 21, borderRadius: 12, background: value ? '#5aad6e' : '#e4ddd2', position: 'relative', transition: 'background .2s', cursor: 'pointer', flexShrink: 0, border: 'none', padding: 0 }}>
      <span style={{ width: 17, height: 17, borderRadius: '50%', background: '#fff', position: 'absolute', top: 2, left: value ? 15 : 2, transition: 'left .2s cubic-bezier(.32,.72,0,1)', boxShadow: '0 1px 2px rgba(0,0,0,.15)', pointerEvents: 'none' }} />
    </button>
  );
}

function StgCard({ title, where, children }) {
  return (
    <section style={{ ...ctorStyles.card, display: 'flex', flexDirection: 'column', gap: 18 }}>
      <header>
        <h2 style={{ margin: 0, fontSize: 18, fontWeight: 700, letterSpacing: '-.02em', color: 'var(--foreground)' }}>{title}</h2>
        <p style={{ margin: '5px 0 0', fontSize: 13, lineHeight: 1.5, color: 'var(--text-tertiary)', maxWidth: 760 }}>{where}</p>
      </header>
      {children}
    </section>
  );
}

function StgRow({ children }) { return <div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'flex-start' }}>{children}</div>; }

function StgField({ label, hint, children, width }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6, ...(width ? { width } : {}) }}>
      <span style={ctorStyles.fieldLabel}>{label}</span>
      {children}
      {hint && <span style={{ fontSize: 11.5, lineHeight: 1.45, color: 'var(--text-tertiary)', maxWidth: 300 }}>{hint}</span>}
    </div>
  );
}

function StgSub({ title, hint, children }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10, paddingTop: 16, borderTop: '1px solid var(--border-subtle, #ece5da)' }}>
      <div>
        <div style={ctorStyles.sectionLabel}>{title}</div>
        {hint && <div style={{ fontSize: 12.5, lineHeight: 1.5, color: 'var(--text-tertiary)', marginTop: 3, maxWidth: 760 }}>{hint}</div>}
      </div>
      {children}
    </div>
  );
}

function StgIconBtn({ onClick, disabled, title, children }) {
  return (
    <button type="button" onClick={onClick} disabled={disabled} title={title} aria-label={title}
      style={{ ...ctorStyles.iconBtn, opacity: disabled ? 0.3 : 1, cursor: disabled ? 'not-allowed' : 'pointer' }}>{children}</button>
  );
}

function StgAddBtn({ onClick, children }) {
  return <button type="button" onClick={onClick} style={{ ...ctorStyles.btnGhost, alignSelf: 'flex-start', padding: '7px 13px' }}>{Icon.plus}{children}</button>;
}

const stgSelectStyle = () => ({ ...ctorStyles.input, width: 'auto', minWidth: 120, padding: '8px 10px' });

/* ───────── 1. Цены и налоги ───────── */

function StgRegions({ v, patch }) {
  const setRegion = (id, p) => patch((cur) => ({ regions: cur.regions.map((r) => (r.id === id ? { ...r, ...p } : r)) }));
  const add = () => patch((cur) => ({ regions: [...cur.regions, { id: stgUid('r'), name: 'Новый регион', coef: 1 }] }));
  const del = (id) => patch((cur) => (cur.regions.length > 1 && id !== cur.defaultRegion ? { regions: cur.regions.filter((r) => r.id !== id) } : {}));
  return (
    <StgSub title="Региональные коэффициенты" hint="Регион сделки задаёт коэффициент к цене работ. Он виден строкой в сводке сметы; материалы не меняются. Регион выбирают в сводке сметы, без выбора берётся «по умолчанию».">
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {v.regions.map((r) => (
          <div key={r.id} style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
            <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: 'var(--text-secondary)', width: 120, cursor: 'pointer' }}>
              <input type="radio" name="stg-default-region" checked={v.defaultRegion === r.id} onChange={() => patch({ defaultRegion: r.id })} />
              по умолчанию
            </label>
            <StgText value={r.name} onChange={(x) => setRegion(r.id, { name: x })} width={260} placeholder="Название региона" />
            <StgNum value={r.coef} min={0.01} max={10} step={0.01} suffix="коэф." onChange={(x) => setRegion(r.id, { coef: x })} />
            <StgIconBtn onClick={() => del(r.id)} disabled={v.regions.length <= 1 || v.defaultRegion === r.id} title={v.defaultRegion === r.id ? 'Регион по умолчанию удалить нельзя' : 'Удалить регион'}>{Icon.x}</StgIconBtn>
          </div>
        ))}
      </div>
      <StgAddBtn onClick={add}>Добавить регион</StgAddBtn>
    </StgSub>
  );
}

function StgComplexity({ v, patch }) {
  const rule = (id, p) => patch((cur) => ({ complexity: cur.complexity.map((c) => (c.id === id ? { ...c, ...p } : c)) }));
  const setField = (c, fieldId) => {
    const f = stgFieldById(fieldId);
    rule(c.id, { field: f.id, op: f.ops[0][0], value: f.kind === 'number' ? f.def : (stgOptions(f)[0] || [''])[0] });
  };
  const add = () => patch((cur) => ({ complexity: [...cur.complexity, { id: stgUid('cx'), label: 'Новое правило', field: 'ceilingApprox', op: '>', value: 3, coef: 1.1, on: true }] }));
  const del = (id) => patch((cur) => ({ complexity: cur.complexity.filter((c) => c.id !== id) }));
  return (
    <StgSub title="Коэффициенты сложности" hint="Применяются к цене работ, когда условие по карточке объекта выполнено: «×1,1» — это +10 %. Каждое включённое правило видно отдельной строкой в сводке сметы. Пока правило выключено, суммы смет не меняются.">
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {v.complexity.length === 0 && <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Правил нет. Добавьте, например, «Потолки выше 3 м ×1,1».</div>}
        {v.complexity.map((c) => {
          const f = stgFieldById(c.field);
          return (
            <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', opacity: c.on ? 1 : 0.65 }}>
              <StgSwitch value={c.on} onChange={(x) => rule(c.id, { on: x })} label={'Включить правило «' + c.label + '»'} />
              <StgText value={c.label} onChange={(x) => rule(c.id, { label: x })} width={200} placeholder="Название" />
              <select value={f.id} onChange={(e) => setField(c, e.target.value)} style={stgSelectStyle()} aria-label="Поле анкеты">
                {STG_FIELDS.map((x) => <option key={x.id} value={x.id}>{x.label}</option>)}
              </select>
              <select value={c.op} onChange={(e) => rule(c.id, { op: e.target.value })} style={{ ...stgSelectStyle(), minWidth: 90 }} aria-label="Условие">
                {f.ops.map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
              </select>
              {f.kind === 'number'
                ? <StgNum value={c.value} min={0} step={0.1} width={78} onChange={(x) => rule(c.id, { value: x })} />
                : <select value={c.value} onChange={(e) => rule(c.id, { value: e.target.value })} style={stgSelectStyle()} aria-label="Значение">
                  {stgOptions(f).map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
                </select>}
              <StgNum value={c.coef} min={0.01} max={10} step={0.01} suffix="×" width={78} onChange={(x) => rule(c.id, { coef: x })} />
              <StgIconBtn onClick={() => del(c.id)} title="Удалить правило">{Icon.x}</StgIconBtn>
            </div>
          );
        })}
      </div>
      <StgAddBtn onClick={add}>Добавить правило</StgAddBtn>
    </StgSub>
  );
}

/* наценка на материалы по подвидам библиотеки (MaterialsData.SUBTYPES): пусто — нет наценки, берётся закупочная, как раньше */
function StgMaterialMarkup({ v, patch }) {
  const subtypes = (window.MaterialsData && window.MaterialsData.SUBTYPES) || [];
  const map = v.materialMarkup || {};
  const set = (id, x) => patch((cur) => {
    const next = { ...(cur.materialMarkup || {}) };
    if (x == null) delete next[id]; else next[id] = x;
    return { materialMarkup: next };
  });
  return (
    <StgSub title="Наценка на материалы по подвидам" hint="Цена материала клиенту: если у позиции библиотеки есть своя цена клиента — она; если нет — закупочная цена × (1 + наценка подвида). Пока наценка не задана, берётся закупочная, как раньше. Пустое поле — без наценки.">
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {subtypes.length === 0 && <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Подвиды материалов не загрузились (constructor/materials-data.js).</div>}
        {subtypes.map((t) => (
          <div key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
            <span style={{ width: 200, display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13.5, fontWeight: 600, color: 'var(--text-secondary)' }}>
              <span aria-hidden="true">{t.icon}</span>{t.name}
            </span>
            <StgNum value={map[t.id]} optional min={0} max={500} step={0.5} suffix="%" placeholder="нет" onChange={(x) => set(t.id, x)} />
            {t.hint && <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{t.hint}</span>}
          </div>
        ))}
      </div>
    </StgSub>
  );
}

/* резерв на непредвиденное: процент от работ (с коэффициентами), строкой в сводке или вшитый в цену работ */
function StgReserve({ v, patch }) {
  const r = v.reserve || { pct: 0, show: 'line' };
  const set = (p) => patch((cur) => ({ reserve: { ...(cur.reserve || { pct: 0, show: 'line' }), ...p } }));
  return (
    <StgSub title="Резерв на непредвиденное" hint="Процент от стоимости работ (с коэффициентами). Входит в «Итого клиенту», скидка считается после резерва. 0 % — резерва нет.">
      <StgRow>
        <StgField label="Размер резерва">
          <StgNum value={r.pct} min={0} max={100} step={0.5} suffix="%" onChange={(x) => set({ pct: x })} />
        </StgField>
        <StgField label="Как показывать клиенту" hint="Строкой: в сводке сметы отдельная строка «Резерв на непредвиденное». Вшить: суммы работ в сводке сразу с резервом, отдельной строки нет; итог тот же.">
          <StgSeg value={r.show === 'inside' ? 'inside' : 'line'} options={STG_RESERVE_SHOW} onChange={(x) => set({ show: x })} />
        </StgField>
      </StgRow>
    </StgSub>
  );
}

function StgPrices({ v, patch }) {
  return (
    <StgCard title="Цены и налоги" where="Читает калькулятор: сводка и печать сметы, цена работ без своей наценки, цена материалов без своей цены клиента, резерв на непредвиденное, суммы сделок в воронке.">
      <StgRow>
        <StgField label="Налог" hint="В сводке сметы: строка «в т.ч. НДС 20 %» или «без НДС (УСН)». Итог от режима налога не растёт — НДС выделяется из него.">
          <StgSeg value={v.tax} options={STG_TAX} onChange={(x) => patch({ tax: x })} />
        </StgField>
        <StgField label="Округление цены клиента" hint="Цена клиента за единицу работы округляется до выбранного шага.">
          <StgSeg value={v.rounding} options={STG_ROUNDING} onChange={(x) => patch({ rounding: x })} />
        </StgField>
      </StgRow>
      <StgRow>
        <StgField label="Наценка по умолчанию" hint="Для работ без своей наценки: цена клиента = себестоимость + эта наценка.">
          <StgNum value={v.markup} min={0} max={500} suffix="%" onChange={(x) => patch({ markup: x })} />
        </StgField>
        <StgField label="Индексация себестоимости по умолчанию" hint="Значение для новых работ каталога; у каждой работы можно задать свою.">
          <StgNum value={v.costIndex} min={0} max={100} step={0.5} suffix="%/мес" onChange={(x) => patch({ costIndex: x })} />
        </StgField>
      </StgRow>
      <StgMaterialMarkup v={v} patch={patch} />
      <StgReserve v={v} patch={patch} />
    </StgCard>
  );
}

/* ───────── 1б. Коэффициенты ───────── */

function StgCoefs({ v, patch }) {
  return (
    <StgCard title="Коэффициенты" where="Читает калькулятор: региональный коэффициент и правила сложности умножают цену работ; каждый виден отдельной строкой в сводке сметы. Материалы не меняются.">
      <StgRegions v={v} patch={patch} />
      <StgComplexity v={v} patch={patch} />
    </StgCard>
  );
}

/* ───────── 2. Договор и оплаты ───────── */

function StgContract({ v, patch }) {
  const stuck = (k) => (x) => patch((cur) => ({ stuckDays: { ...cur.stuckDays, [k]: x } }));
  return (
    <StgCard title="Договор и оплаты" where="Подставляется в новые договоры сделки и проекта. Уже оформленные договоры не меняются. Дни «застряла» читает воронка менеджера и руководителя.">
      <StgRow>
        <StgField label="Аванс по договору подряда" hint="Процент от стоимости работ.">
          <StgNum value={v.advancePct} min={0} max={100} suffix="%" onChange={(x) => patch({ advancePct: x })} />
        </StgField>
        <StgField label="Аванс за дизайн-проект" hint="Процент от суммы договора на ДП: подставляется, когда менеджер указал сумму договора.">
          <StgNum value={v.dpAdvancePct} min={0} max={100} suffix="%" onChange={(x) => patch({ dpAdvancePct: x })} />
        </StgField>
        <StgField label="Срок оплаты счёта">
          <StgNum value={v.invoiceDays} min={0} max={90} suffix="дн" onChange={(x) => patch({ invoiceDays: x })} />
        </StgField>
      </StgRow>
      <StgField label="Срок действия сметы" hint="Дней с отправки сметы клиенту. Менеджер может продлить срок конкретной сделке.">
        <StgNum value={v.offerDays} min={1} max={60} suffix="дн" onChange={(x) => patch({ offerDays: x })} />
      </StgField>
      <StgField label="Схема платежей" hint="По фазам: счёт при закрытии фазы работ. По этапам: счёт по каждому этапу.">
        <StgSeg value={v.paySchedule} options={STG_SCHEDULE} onChange={(x) => patch({ paySchedule: x })} />
      </StgField>
      <StgField label="Оплата открывает этап" hint="Новые проекты: работа следующей фазы (этапа) не начнётся, пока не подтверждена оплата предыдущей, аванса и выставленных счетов за материалы и доплаты. Подтверждает бухгалтер; руководитель может снять блокировку с причиной. Идущие проекты не меняются.">
        <StgSwitch value={v.payGate !== false} onChange={(x) => patch({ payGate: x })} label="Не начинать без оплаты" />
      </StgField>
      <StgSub title="Когда подсказать менеджеру позвонить" hint="Сигналы в «Ждёт ответа клиенту» на «Сегодня» менеджера. Считаются рабочие часы: пн–пт 9:00–19:00, рабочий день — 10 часов.">
        <StgRow>
          <StgField label="Смету не открыли" hint="С отправки клиенту. До этого — «позвоните сегодня»."><StgNum value={v.signals.notOpenedWh} min={1} max={200} suffix="раб. ч" onChange={(x) => patch((cur) => ({ signals: { ...cur.signals, notOpenedWh: x } }))} /></StgField>
          <StgField label="Открыл смету и молчит" hint="С последнего открытия сметы."><StgNum value={v.signals.silentWh} min={1} max={200} suffix="раб. ч" onChange={(x) => patch((cur) => ({ signals: { ...cur.signals, silentWh: x } }))} /></StgField>
        </StgRow>
      </StgSub>
      <StgSub title="Договор подряда простым языком" hint="Кабинет клиента, окно «Документы»: «Главное в 7 пунктах». Цена, срок, оплаты, переносы, приёмка и гарантия собираются из сметы и процесса; текст про расторжение — отсюда, как в договоре.">
        <StgField label="Расторжение" hint="Пусто — в кабинете стоит пример с пометкой «Пример —».">
          <textarea value={v.terminate || ''} onChange={(e) => patch({ terminate: e.target.value })} rows={3} maxLength={600} placeholder="Как расторгнуть договор и что при этом оплачивается" style={{ ...ctorStyles.input, resize: 'vertical', lineHeight: 1.45, width: '100%' }} />
        </StgField>
      </StgSub>
      <StgSub title="«Застряла» — сколько дней ждать клиента" hint="Через сколько дней без ответа сделка попадёт в «застрявшие» у менеджера и руководителя.">
        <StgRow>
          <StgField label="Договор у клиента"><StgNum value={v.stuckDays.contract} min={1} max={60} suffix="дн" onChange={stuck('contract')} /></StgField>
          <StgField label="Смета у клиента"><StgNum value={v.stuckDays.estimate} min={1} max={60} suffix="дн" onChange={stuck('estimate')} /></StgField>
          <StgField label="Этап ДП у клиента"><StgNum value={v.stuckDays.dp} min={1} max={60} suffix="дн" onChange={stuck('dp')} /></StgField>
        </StgRow>
      </StgSub>
    </StgCard>
  );
}

/* ───────── 3. Календарь работ ───────── */

function StgCalendar({ v, patch }) {
  const [newDate, setNewDate] = stgUseState('');
  const [year, setYear] = stgUseState(STG_YEARS[0]);
  const [note, setNote] = stgUseState('');
  const holidays = v.holidays.slice().sort();
  const toggleDay = (d) => patch((cur) => {
    const on = cur.workdays.includes(d);
    if (on && cur.workdays.length <= 1) return {}; // график без рабочих дней не построить
    return { workdays: on ? cur.workdays.filter((x) => x !== d) : [...cur.workdays, d] };
  });
  const addHoliday = (iso) => patch((cur) => (cur.holidays.includes(iso) ? {} : { holidays: [...cur.holidays, iso].sort() }));
  const delHoliday = (iso) => patch((cur) => ({ holidays: cur.holidays.filter((x) => x !== iso) }));
  const fillYear = () => {
    const have = new Set(v.holidays);
    const missing = STG_HOLIDAYS_MD.map((md) => stgIsoDate(year, md)).filter((d) => !have.has(d));
    patch((cur) => ({ holidays: Array.from(new Set([...cur.holidays, ...missing])).sort() }));
    setNote(missing.length ? 'Добавлено дат: ' + missing.length + '. Переносы выходных добавьте вручную.' : 'Все праздники ' + year + ' года уже в списке.');
  };
  return (
    <StgCard title="Календарь работ" where="Читает график сметы (работы переносятся на рабочие дни, праздники пропускаются) и даты доставок на объект.">
      <StgField label="Рабочие дни недели" hint="По умолчанию пн–пт, как график считался раньше. Выберите субботу, если бригады работают.">
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          {STG_WEEKDAYS.map(([d, l]) => {
            const on = v.workdays.includes(d);
            return (
              <button key={d} type="button" aria-pressed={on} onClick={() => toggleDay(d)}
                style={{ ...ctorStyles.segmentedBtn(on), width: 46, justifyContent: 'center', border: '1px solid ' + (on ? 'var(--ink)' : 'var(--glass-border)'), background: on ? 'var(--ink)' : 'var(--glass-bg-strong)', color: on ? '#fff' : 'var(--text-secondary)' }}>{l}</button>
            );
          })}
        </div>
      </StgField>
      <StgSub title="Праздники и нерабочие дни" hint="Эти даты график пропускает.">
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
          <input type="date" value={newDate} onChange={(e) => setNewDate(e.target.value)} style={{ ...ctorStyles.input, width: 170 }} aria-label="Дата" />
          <button type="button" disabled={!newDate} onClick={() => { addHoliday(newDate); setNewDate(''); }}
            style={{ ...ctorStyles.btnGhost, padding: '8px 13px', opacity: newDate ? 1 : 0.4, cursor: newDate ? 'pointer' : 'not-allowed' }}>{Icon.plus}Добавить</button>
          <span style={{ width: 12 }} />
          <select value={year} onChange={(e) => setYear(+e.target.value)} style={{ ...stgSelectStyle(), minWidth: 90 }} aria-label="Год производственного календаря">
            {STG_YEARS.map((y) => <option key={y} value={y}>{y}</option>)}
          </select>
          <button type="button" onClick={fillYear} style={{ ...ctorStyles.btnGhost, padding: '8px 13px' }}>Производственный календарь РФ {year}</button>
        </div>
        {note && <div style={{ fontSize: 12.5, color: 'var(--text-secondary)' }}>{note}</div>}
        {holidays.length === 0
          ? <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Дат нет. Добавьте вручную или заполните производственным календарём.</div>
          : (
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {holidays.map((iso) => (
                <span key={iso} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 6px 4px 11px', borderRadius: 980, background: 'var(--secondary)', fontSize: 12.5, fontWeight: 600, color: 'var(--text-secondary)' }}>
                  {stgFmtDate(iso)}
                  <button type="button" onClick={() => delHoliday(iso)} title="Убрать дату" aria-label={'Убрать ' + stgFmtDate(iso)} style={{ ...ctorStyles.iconBtnSm, width: 20, height: 20 }}>{Icon.x}</button>
                </span>
              ))}
            </div>
          )}
      </StgSub>
      <StgRow>
        <StgField label="Шумные работы с" hint="Подсказка в задачах: шумные работы — только в эти часы.">
          <input type="time" value={v.noisyFrom} onChange={(e) => patch({ noisyFrom: e.target.value })} style={{ ...ctorStyles.input, width: 130 }} aria-label="Шумные работы с" />
        </StgField>
        <StgField label="до">
          <input type="time" value={v.noisyTo} onChange={(e) => patch({ noisyTo: e.target.value })} style={{ ...ctorStyles.input, width: 130 }} aria-label="Шумные работы до" />
        </StgField>
      </StgRow>
    </StgCard>
  );
}

/* ───────── 4. Компания и документы ───────── */

function StgCompany({ v, patch }) {
  const [logoErr, setLogoErr] = stgUseState('');
  const onLogo = (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    if (!/^image\//.test(f.type)) { setLogoErr('Нужен файл изображения (PNG, JPG, SVG).'); return; }
    if (f.size > STG_LOGO_MAX) { setLogoErr('Файл больше 300 КБ — уменьшите логотип.'); return; }
    const r = new FileReader();
    r.onload = () => { setLogoErr(''); patch({ logo: String(r.result) }); };
    r.onerror = () => setLogoErr('Не удалось прочитать файл.');
    r.readAsDataURL(f);
  };
  const T = (key, label, width, placeholder) => (
    <StgField label={label} width={width}><StgText value={v[key]} onChange={(x) => patch({ [key]: x })} placeholder={placeholder} /></StgField>
  );
  return (
    <StgCard title="Реквизиты компании" where="Попадает в шапку и подвал печатной сметы: логотип и реквизиты. Пустые поля в печати пропускаются.">
      <StgRow>
        {T('name', 'Название', 280)}
        {T('inn', 'ИНН', 150)}
        {T('kpp', 'КПП', 130)}
        {T('ogrn', 'ОГРН', 170)}
      </StgRow>
      <StgRow>
        {T('address', 'Адрес', 420)}
        {T('phone', 'Телефон', 190, '+7 …')}
        {T('site', 'Сайт', 220)}
      </StgRow>
      <StgField label="Банковские реквизиты" hint="Банк, БИК, расчётный и корреспондентский счета — одной строкой или несколькими." width={640}>
        <textarea value={v.bank || ''} onChange={(e) => patch({ bank: e.target.value })} rows={2} style={{ ...ctorStyles.input, resize: 'vertical', lineHeight: 1.45 }} />
      </StgField>
      <StgField label="Логотип" hint="PNG, JPG или SVG, до 300 КБ.">
        <div style={{ display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' }}>
          {v.logo ? <img src={v.logo} alt="Логотип компании" style={{ maxHeight: 56, maxWidth: 200, objectFit: 'contain', borderRadius: 8, border: '1px solid var(--border-subtle, #ece5da)', background: '#fff', padding: 4 }} /> : <span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Логотип не загружен</span>}
          <label style={{ ...ctorStyles.btnGhost, cursor: 'pointer' }}>
            {v.logo ? 'Заменить' : 'Загрузить'}
            <input type="file" accept="image/*" onChange={onLogo} style={{ display: 'none' }} />
          </label>
          {v.logo && <button type="button" onClick={() => patch({ logo: '' })} style={ctorStyles.btnGhost}>Убрать</button>}
        </div>
        {logoErr && <span style={{ fontSize: 12.5, color: '#b91c1c' }}>{logoErr}</span>}
      </StgField>
    </StgCard>
  );
}

/* ───────── 4в. КП ───────── */

const STG_OBJECT_TYPES = [{ v: 'new_build', l: 'Новостройка' }, { v: 'secondary', l: 'Вторичка' }, { v: 'house', l: 'Дом' }];

/* список строк: добавить / изменить / удалить (преимущества и гарантии КП) */
function StgOfferList({ title, hint, items, onChange, addLabel, placeholder }) {
  const set = (i, x) => onChange(items.map((t, j) => (j === i ? x : t)));
  return (
    <StgSub title={title} hint={hint}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {items.map((t, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <StgText value={t} onChange={(x) => set(i, x)} placeholder={placeholder} width={620} />
            <StgIconBtn onClick={() => onChange(items.filter((_, j) => j !== i))} title="Удалить строку">{Icon.x}</StgIconBtn>
          </div>
        ))}
        {!items.length && <span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Пока пусто — в КП этот блок не показывается.</span>}
      </div>
      <StgAddBtn onClick={() => onChange([...items, ''])}>{addLabel}</StgAddBtn>
    </StgSub>
  );
}

function StgOfferCase({ c, onChange, onRemove }) {
  return (
    <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'flex-start', padding: 14, borderRadius: 12, border: '1px solid var(--border-subtle, #ece5da)' }}>
      {c.photo
        ? <img src={c.photo} alt={c.title} style={{ width: 150, height: 110, objectFit: 'cover', borderRadius: 10, border: '1px solid var(--border-subtle, #ece5da)' }} />
        : <div style={{ width: 150, height: 110, borderRadius: 10, background: '#f4efe7', color: 'var(--text-tertiary)', fontSize: 12.5, display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>Фото не добавлено</div>}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12, flex: 1, minWidth: 280 }}>
        <StgRow>
          <StgField label="Название" width={340}><StgText value={c.title} onChange={(x) => onChange({ title: x })} /></StgField>
          <StgField label="Тип объекта"><StgSeg value={c.objectType} options={STG_OBJECT_TYPES} onChange={(x) => onChange({ objectType: x })} /></StgField>
        </StgRow>
        <StgRow>
          <StgField label="Площадь"><StgNum value={c.area} min={1} max={10000} suffix="м²" onChange={(x) => onChange({ area: x })} /></StgField>
          <StgField label="Срок"><StgNum value={c.weeks} min={1} max={520} step={1} suffix="нед" onChange={(x) => onChange({ weeks: Math.round(x) })} /></StgField>
          <StgField label="Фото (ссылка)" hint="Пусто — в КП будет заглушка." width={340}><StgText value={c.photo} onChange={(x) => onChange({ photo: x })} placeholder="https://…" /></StgField>
        </StgRow>
        <StgField label="Описание">
          <textarea value={c.text || ''} onChange={(e) => onChange({ text: e.target.value })} rows={2} style={{ ...ctorStyles.input, resize: 'vertical', lineHeight: 1.45 }} />
        </StgField>
      </div>
      <StgIconBtn onClick={onRemove} title="Удалить кейс">{Icon.x}</StgIconBtn>
    </div>
  );
}

function StgOffer({ v, patch }) {
  const setCase = (id, p) => patch((cur) => ({ cases: cur.cases.map((c) => (c.id === id ? { ...c, ...p } : c)) }));
  const addCase = () => patch((cur) => {
    const n = cur.cases.reduce((m, c) => Math.max(m, Number(String(c.id).replace(/^case-/, '')) || 0), 0) + 1;
    return { cases: [...cur.cases, { id: 'case-' + n, title: '', objectType: 'new_build', area: 50, weeks: 12, text: '', photo: '' }] };
  });
  return (
    <StgCard title="КП" where="Содержимое коммерческого предложения, которое видит клиент до старта работ: о компании, преимущества, гарантии и кейсы. Сейчас здесь тестовые тексты «Пример — …» — замените их на настоящие.">
      <StgField label="О компании" width={760}>
        <textarea value={v.about || ''} onChange={(e) => patch({ about: e.target.value })} rows={4} style={{ ...ctorStyles.input, resize: 'vertical', lineHeight: 1.45 }} />
      </StgField>
      <StgOfferList title="Преимущества" items={v.advantages} onChange={(x) => patch({ advantages: x })} addLabel="Добавить преимущество" placeholder="Например: фиксированная смета" />
      <StgOfferList title="Гарантии" items={v.guarantees} onChange={(x) => patch({ guarantees: x })} addLabel="Добавить гарантию" placeholder="Например: гарантия на работы 3 года" />
      <StgSub title="Кейсы" hint="Выполненные объекты. Без фото в КП показывается заглушка.">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {v.cases.map((c) => <StgOfferCase key={c.id} c={c} onChange={(p) => setCase(c.id, p)} onRemove={() => patch((cur) => ({ cases: cur.cases.filter((x) => x.id !== c.id) }))} />)}
        </div>
        <StgAddBtn onClick={addCase}>Добавить кейс</StgAddBtn>
      </StgSub>
    </StgCard>
  );
}

/* ───────── 4б. Нумерация документов ───────── */

function StgNumbering({ v, patch }) {
  const year = new Date().getFullYear();
  const OS = window.ObitelSettings;
  const num = (kind, p) => patch((cur) => ({ numbering: { ...cur.numbering, [kind]: { ...(cur.numbering || {})[kind], ...p } } }));
  return (
    <StgCard title="Нумерация документов" where="Номер вида «СМ-2026-0001»: префикс, год и сквозной номер. Номер сметы присваивается, когда смета выходит из черновика или впервые уходит в печать. Стартовый номер — с какого начать счёт.">
      <div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {STG_DOCS.map(([kind, label]) => {
            const n = (v.numbering || {})[kind] || { prefix: '', start: 1 };
            return (
              <div key={kind} style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
                <span style={{ width: 90, fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)' }}>{label}</span>
                <StgText value={n.prefix} onChange={(x) => num(kind, { prefix: x })} width={110} maxLength={10} placeholder="Префикс" />
                <StgNum value={n.start} min={1} max={999999} step={1} width={100} suffix="стартовый №" onChange={(x) => num(kind, { start: Math.floor(x) })} />
                <span style={{ fontSize: 12.5, color: 'var(--text-tertiary)' }}>пример: <b style={{ color: 'var(--text-secondary)' }}>{OS ? OS.formatNumber(n.prefix, n.start, year) : ''}</b></span>
              </div>
            );
          })}
        </div>
      </div>
    </StgCard>
  );
}

/* ───────── 5. Доставка на объект ───────── */

function StgDelivery({ v, patch }) {
  return (
    <StgCard title="Доставка на объект" where="Читают проекты и кабинеты закупок, прораба и руководителя: когда по этапу появляется заявка на доставку материалов — по тому условию, что наступит раньше.">
      <StgRow>
        <StgField label="За сколько дней до этапа (N)" hint="Заявка появляется за N дней до старта этапа по графику.">
          <StgNum value={v.daysBefore} min={0} max={60} suffix="дн" onChange={(x) => patch({ daysBefore: Math.round(x) })} />
        </StgField>
        <StgField label="Осталось незакрытых задач (K)" hint="Или когда до этапа осталось K незакрытых задач.">
          <StgNum value={v.tasksBefore} min={0} max={60} suffix="задач" onChange={(x) => patch({ tasksBefore: Math.round(x) })} />
        </StgField>
      </StgRow>
    </StgCard>
  );
}

/* ───────── 6. Контроль сметы ───────── */

function StgControl({ v, patch }) {
  const c = v || {};
  const set = (key) => (x) => patch({ [key]: x });
  const inverted = c.marginTarget != null && c.marginMin != null && c.marginMin > c.marginTarget;
  return (
    <StgCard title="Контроль сметы" where="Читает калькулятор: сводка сметы у менеджера и руководителя (цвет маржи) и кнопка «Отправить клиенту». Пустой порог цели или скидки — контроль по нему выключен. Минимальная маржа по умолчанию 25 %.">
      <StgRow>
        <StgField label="Целевая маржа" hint="Маржа сметы не ниже цели — в сводке зелёная; между минимумом и целью — жёлтая.">
          <StgNum value={c.marginTarget} optional min={0} max={100} step={0.5} suffix="%" placeholder="выкл." onChange={set('marginTarget')} />
        </StgField>
        <StgField label="Минимальная маржа" hint="Ниже минимума маржа красная, а смета не уходит клиенту без согласования руководителя. Пустое поле — 25 %, ноль — маржу не проверять.">
          <StgNum value={c.marginMin} optional min={0} max={100} step={0.5} suffix="%" placeholder="25" onChange={set('marginMin')} />
        </StgField>
        <StgField label="Максимальная скидка без согласования" hint="Скидка выше — смета не уходит клиенту без согласования руководителя. Ноль — согласовывать любую скидку.">
          <StgNum value={c.discountMax} optional min={0} max={100} step={0.5} suffix="%" placeholder="выкл." onChange={set('discountMax')} />
        </StgField>
      </StgRow>
      {inverted && <div style={{ fontSize: 12.5, color: 'var(--warning-strong)' }}>Минимум выше цели: жёлтой зоны не будет — маржа сразу станет красной ниже минимума.</div>}
      <div style={{ fontSize: 12.5, lineHeight: 1.5, color: 'var(--text-tertiary)', maxWidth: 760 }}>
        Руководитель в калькуляторе нажимает «Согласовать» — смета уходит клиенту при тех же или лучших значениях; поднимете скидку выше согласованной — согласование понадобится снова. Согласование видно в истории сметы. С долями маржа — это доля компании.
      </div>
    </StgCard>
  );
}

/* ───────── 7. Доли ───────── */

/* умолчания долей для НОВЫХ смет (ObitelSettings.DEFAULTS.shares); старые сметы не меняются */
const STG_SHARE_MODES = [{ v: 'client', l: 'От цены клиента' }, { v: 'executor', l: 'От цены исполнителя' }];

function StgShares({ v, patch }) {
  const s = v || {};
  const mode = s.mode === 'executor' ? 'executor' : 'client';
  const set = (key) => (x) => patch({ [key]: x });
  const overflow = mode === 'executor' && (Number(s.foreman) || 0) + (Number(s.company) || 0) >= 95;
  // пустое поле движок читает как 60 / 10 / 30 — здесь так же; «от цены клиента»: исполнитель + прораб больше 100 % — компания в минусе
  const orDefault = (x, d) => (x === '' || x == null || !isFinite(Number(x)) ? d : Number(x));
  const over100 = mode === 'client' && orDefault(s.executor, 60) + orDefault(s.foreman, 10) > 100;
  return (
    <StgCard title="Доли" where="Новые сметы берут эти проценты. Старые сметы не меняются.">
      <label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 13.5, fontWeight: 600, color: 'var(--foreground)' }}>
        <StgSwitch value={s.on === true} onChange={(x) => patch({ on: x })} label="Считать доли в новых сметах" />
        Считать доли в новых сметах
      </label>
      <StgRow>
        <StgField label="Способ расчёта">
          <StgSeg value={mode} options={STG_SHARE_MODES} onChange={set('mode')} />
        </StgField>
        <StgField label="Исполнителю"><StgNum value={s.executor} min={0} max={100} step={0.5} suffix="%" onChange={set('executor')} /></StgField>
        <StgField label="Прорабу"><StgNum value={s.foreman} min={0} max={100} step={0.5} suffix="%" onChange={set('foreman')} /></StgField>
        <StgField label="Компании"><StgNum value={s.company} min={0} max={100} step={0.5} suffix="%" onChange={set('company')} /></StgField>
      </StgRow>
      <div style={{ fontSize: 12.5, lineHeight: 1.5, color: 'var(--text-tertiary)', maxWidth: 760 }}>
        {mode === 'client' ? 'Компании остаётся цена клиента минус исполнитель и прораб.' : 'Цена клиента = цена исполнителя ÷ (1 − прораб − компания).'}
      </div>
      {overflow && <div style={{ fontSize: 12.5, color: 'var(--warning-strong)' }}>Прораб + компания ≥ 95 % — цены не пересчитаны</div>}
      {over100 && <div style={{ fontSize: 12.5, color: 'var(--warning-strong)' }}>Исполнитель + прораб &gt; 100 % — компания в минусе</div>}
    </StgCard>
  );
}

/* ───────── панели меню ───────── */

/* patch(block)(объект | (текущий блок) => объект): слияние в блок настроек; чужие ключи настроек не трогаются */
const stgPatchOf = (setSettings, block) => (change) => setSettings((s) => {
  const cur = s[block] || {};
  const p = typeof change === 'function' ? change(cur) : change;
  return p && Object.keys(p).length ? { ...s, [block]: { ...cur, ...p } } : s;
});

function StgNoModule() {
  return <EmptyState icon="settings" title="Настройки недоступны" text="Не загрузился модуль настроек (lk/settings-store.js). Обновите страницу." />;
}

function StgDraftNote() {
  return (
    <p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.5, color: 'var(--text-tertiary)', maxWidth: 760 }}>
      Правки сохраняются в черновик; калькулятор, договоры, воронка и доставки читают их после «Опубликовать». Пока ничего не опубликовано — всё работает как раньше.
    </p>
  );
}

/* одна карточка настроек одного блока как отдельная панель меню */
function StgBlockPanel({ settings, setSettings, block, View }) {
  if (!settings) return <StgNoModule />;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 940 }}>
      <StgDraftNote />
      <View v={settings[block]} patch={stgPatchOf(setSettings, block)} />
    </div>
  );
}

const StgPricesPanel = (p) => <StgBlockPanel {...p} block="prices" View={StgPrices} />;
const StgCoefsPanel = (p) => <StgBlockPanel {...p} block="prices" View={StgCoefs} />;
const StgContractPanel = (p) => <StgBlockPanel {...p} block="contract" View={StgContract} />;
const StgControlPanel = (p) => <StgBlockPanel {...p} block="control" View={StgControl} />;
const StgSharesPanel = (p) => <StgBlockPanel {...p} block="shares" View={StgShares} />;
const StgCalendarPanel = (p) => <StgBlockPanel {...p} block="calendar" View={StgCalendar} />;
const StgDeliveryPanel = (p) => <StgBlockPanel {...p} block="delivery" View={StgDelivery} />;
const StgCompanyPanel = (p) => <StgBlockPanel {...p} block="company" View={StgCompany} />;
const StgNumberingPanel = (p) => <StgBlockPanel {...p} block="company" View={StgNumbering} />;
const StgOfferPanel = (p) => <StgBlockPanel {...p} block="offer" View={StgOffer} />;

/* прежняя общая панель — все карточки подряд; в меню больше не подключена */
function StgSettingsPanel({ settings, setSettings }) {
  if (!settings) return <StgNoModule />;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18, maxWidth: 940 }}>
      <header>
        <h2 style={{ margin: 0, fontSize: 22, fontWeight: 800, letterSpacing: '-.025em', color: 'var(--foreground)' }}>Настройки</h2>
        <div style={{ marginTop: 6 }}><StgDraftNote /></div>
      </header>
      <StgPrices v={settings.prices} patch={stgPatchOf(setSettings, 'prices')} />
      <StgCoefs v={settings.prices} patch={stgPatchOf(setSettings, 'prices')} />
      <StgContract v={settings.contract} patch={stgPatchOf(setSettings, 'contract')} />
      <StgControl v={settings.control} patch={stgPatchOf(setSettings, 'control')} />
      <StgShares v={settings.shares} patch={stgPatchOf(setSettings, 'shares')} />
      <StgCalendar v={settings.calendar} patch={stgPatchOf(setSettings, 'calendar')} />
      <StgDelivery v={settings.delivery} patch={stgPatchOf(setSettings, 'delivery')} />
      <StgCompany v={settings.company} patch={stgPatchOf(setSettings, 'company')} />
      <StgNumbering v={settings.company} patch={stgPatchOf(setSettings, 'company')} />
    </div>
  );
}

Object.assign(window, { StgSettingsPanel, StgPricesPanel, StgCoefsPanel, StgContractPanel, StgControlPanel, StgSharesPanel, StgCalendarPanel, StgDeliveryPanel, StgCompanyPanel, StgNumberingPanel, StgOfferPanel });
