// «Контроль исполнения поручений» — the main register page.

const MONTH_ORDER = ["январь","февраль","март","апрель","май","июнь","июль","август","сентябрь","октябрь","ноябрь","декабрь"];

function personFor(assignee, dept) {
  if (PEOPLE[assignee]) return PEOPLE[assignee];
  const m = assignee.match(/[А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]\.?/);
  if (m && PEOPLE[m[0]]) return PEOPLE[m[0]];
  // destination phrase (e.g. «В бухгалтерию») → department bot persona
  return { name: assignee, tg: "@" + (DEPT_COLORS[dept] ? "otdel" : "porucheniya") + "_bot", color: DEPT_COLORS[dept] || "#5d6b82" };
}

function todayStr() {
  const t = (typeof effToday === "function") ? effToday() : DEMO_TODAY;
  return `${String(t.d).padStart(2,"0")}.${String(t.m).padStart(2,"0")}.${t.y}`;
}

const YEARS = [2024, 2025, 2026];

function PorucheniyaPage({ intent }) {
  const [tasks, setTasks] = React.useState(() => PORUCHENIYA.map(t => ({ ...t, year: 2025 })));
  const [year, setYear] = React.useState(2025);
  const [yearMenu, setYearMenu] = React.useState(false);
  const [month, setMonth] = React.useState("январь");
  const [group, setGroup] = React.useState("dept");
  const [query, setQuery] = React.useState("");
  const [statusFilter, setStatusFilter] = React.useState("all");
  const [assigneeFilter, setAssigneeFilter] = React.useState("all");
  const [deptFilter, setDeptFilter] = React.useState("all");
  const [yearScope, setYearScope] = React.useState(false); // "Весь год": filters span the whole year instead of the selected month
  const [collapsed, setCollapsed] = React.useState(new Set());
  const [expanded, setExpanded] = React.useState(new Set());
  const [tg, setTg] = React.useState({ open: false, taskId: null });
  const [toasts, setToasts] = React.useState([]);
  const [exportOpen, setExportOpen] = React.useState(false);
  const [newOpen, setNewOpen] = React.useState(false);
  const [tgCfgOpen, setTgCfgOpen] = React.useState(false);
  const [tgInfo, setTgInfo] = React.useState({ configured: false, running: false, username: "", chats: [] });
  const [editTask, setEditTask] = React.useState(null);
  const [reassign, setReassign] = React.useState(null); // task being reassigned, or null
  const [importPrev, setImportPrev] = React.useState(null); // import diff preview, or null
  const [importing, setImporting] = React.useState(false);
  const importInputRef = React.useRef(null);
  const [notesByTask, setNotesByTask] = React.useState({}); // taskId -> [note], loaded lazily on expand
  const dir = useDirectory(); // live people/departments from the DB (re-renders on load)

  // Load the live register from the backend; fall back to bundled mock data if offline.
  const reload = React.useCallback(() => {
    if (!window.API) return;
    window.API.listTasks()
      .then(data => { if (Array.isArray(data)) setTasks(data); })
      .catch(() => {});
  }, []);
  React.useEffect(() => { reload(); }, [reload]);

  // Live updates pushed from the backend (e.g. Telegram button callbacks).
  React.useEffect(() => {
    if (!window.API || !window.API.events) return;
    const es = window.API.events({
      onTask: (task) => {
        setTasks(ts => {
          const i = ts.findIndex(t => t.id === task.id);
          if (i === -1) return [...ts, task];
          const next = ts.slice();
          next[i] = task;
          return next;
        });
      },
      onDeleted: ({ id }) => setTasks(ts => ts.filter(t => t.id !== id)),
      onNote: (note) => setNotesByTask(m => {
        const cur = m[note.taskId];
        if (!cur) return m; // not loaded yet — will be fetched fresh on expand
        if (cur.some(n => n.id === note.id)) return m;
        return { ...m, [note.taskId]: [...cur, note] };
      }),
    });
    return () => { if (es) es.close(); };
  }, []);

  // Telegram bot status (for the banner + config modal).
  const refreshTgStatus = React.useCallback(() => {
    if (window.API && window.API.telegramStatus) {
      window.API.telegramStatus().then(setTgInfo).catch(() => {});
    }
  }, []);
  React.useEffect(() => { refreshTgStatus(); }, [refreshTgStatus]);

  // Load the note/question thread when the Telegram panel opens for a task.
  React.useEffect(() => {
    if (tg.open && tg.taskId != null && window.API && window.API.listNotes && !notesByTask[tg.taskId]) {
      window.API.listNotes(tg.taskId)
        .then(ns => setNotesByTask(m => ({ ...m, [tg.taskId]: Array.isArray(ns) ? ns : [] })))
        .catch(() => {});
    }
  }, [tg.open, tg.taskId]);

  // Server-provided "today" so the overdue calc isn't hardcoded in the UI.
  React.useEffect(() => {
    if (!window.API || !window.API.config) return;
    window.API.config().then(cfg => {
      const m = cfg && cfg.today && String(cfg.today).match(/(\d{4})-(\d{2})-(\d{2})/);
      if (m) window.SERVER_TODAY = { d: +m[3], m: +m[2], y: +m[1] };
    }).catch(() => {});
  }, []);

  // Apply navigation intent (topbar search / overdue bell / employee link).
  // An intent defines a fresh filter view: filters it doesn't mention reset to defaults.
  React.useEffect(() => {
    if (!intent) return;
    setQuery(intent.query != null ? intent.query : "");
    setStatusFilter(intent.status || "all");
    setAssigneeFilter(intent.assignee || "all");
    setDeptFilter(intent.dept || "all");
    if (intent.year) setYear(intent.year);
    if (intent.month) {
      setMonth(intent.month);
      setYearScope(false);
    } else {
      // Topbar search / overdue bell / employee link land on the whole-year view.
      const wantYear = !!intent.allYear || !!intent.query
        || (intent.status && intent.status !== "all")
        || (intent.assignee && intent.assignee !== "all")
        || (intent.dept && intent.dept !== "all");
      setYearScope(!!wantYear);
    }
  }, [intent]);

  // ---- filtering ----
  // Every filter (search / status / assignee / dept) composes uniformly with the
  // current scope: a single month, or the whole year ("Весь год" tab / yearScope).
  // Applying any filter auto-switches to the year scope so matches in other months
  // aren't hidden (e.g. searching "Фролова" while on Январь); clicking a month tab
  // narrows back to that month.
  const yearTasks = tasks.filter(t => t.year === year);
  const monthTasks = yearTasks.filter(t => t.mon === month);
  const passFilter = (t) => {
    if (query && !(t.text.toLowerCase().includes(query.toLowerCase()) || t.assignee.toLowerCase().includes(query.toLowerCase()))) return false;
    if (statusFilter !== "all" && effStatus(t) !== statusFilter) return false;
    if (assigneeFilter !== "all" && t.assignee !== assigneeFilter) return false;
    if (deptFilter !== "all" && t.dept !== deptFilter) return false;
    return true;
  };
  const anyFilter = query.trim() !== "" || statusFilter !== "all" || assigneeFilter !== "all" || deptFilter !== "all";
  const filtered = (yearScope ? yearTasks : monthTasks).filter(passFilter);
  const yearMatch = yearTasks.filter(passFilter); // count for the "Весь год" tab

  const months = MONTH_ORDER;
  const monthCount = (m) => yearTasks.filter(t => t.mon === m && passFilter(t)).length;
  const yearCount = (y) => tasks.filter(t => t.year === y).length;
  const changeYear = (delta) => {
    const idx = YEARS.indexOf(year);
    const ni = Math.min(YEARS.length - 1, Math.max(0, idx + delta));
    setYear(YEARS[ni]);
  };

  const pushToast = (text, icon, color) => {
    const id = Date.now() + Math.random();
    setToasts(ts => [...ts, { id, text, icon, color }]);
    setTimeout(() => setToasts(ts => ts.filter(t => t.id !== id)), 3200);
  };

  const setStatus = (id, status, extra = {}) => {
    setTasks(ts => ts.map(t => t.id === id ? { ...t, status, ...extra } : t));
    // Persist the transition to the backend (best-effort; the UI is already updated).
    if (window.API) window.API.updateTask(id, { status, ...extra }).catch(() => {});
  };

  // ---- Telegram actions ----
  const openTelegram = async (task) => {
    const needsSend = task.status === "draft" || task.status === "in_progress";

    if (needsSend && window.API && window.API.sendTelegram) {
      try {
        await window.API.sendTelegram(task.id); // backend sends + sets 'sent' (SSE updates state)
        const p = personFor(task.assignee, task.dept);
        pushToast(`Отправлено в Telegram → ${p.tg}`, <I.Plane size={12} />, "#2682c0");
        setTg({ open: true, taskId: task.id });
        return;
      } catch (err) {
        const msg = String((err && err.message) || err);
        if (/не настроен/i.test(msg)) {
          pushToast("Telegram-бот не подключён", <I.Alert size={12} />, "var(--warn)");
          setTgCfgOpen(true);
          return;
        }
        // bot is on but e.g. the user hasn't done /start — show why, then open a local preview
        pushToast(msg, <I.Alert size={12} />, "var(--warn)");
        setStatus(task.id, "sent");
        setTg({ open: true, taskId: task.id });
        return;
      }
    }

    // No backend, or the task was already sent: original local behavior.
    if (needsSend) {
      setStatus(task.id, "sent");
      const p = personFor(task.assignee, task.dept);
      pushToast(`Отправлено в Telegram → ${p.tg}`, <I.Plane size={12} />, "#2682c0");
    }
    setTg({ open: true, taskId: task.id });
  };
  const tgAccept = () => {
    const t = tasks.find(x => x.id === tg.taskId);
    if (!t) return;
    if (!window.confirm(`Отметить поручение №${t.num} как «Принято в работу» от имени исполнителя?`)) return;
    setStatus(t.id, "accepted");
    const p = personFor(t.assignee, t.dept);
    pushToast(`${p.name} принял(а) поручение в работу`, <I.Check size={12} />, "#5546b0");
  };
  const tgDone = () => {
    const t = tasks.find(x => x.id === tg.taskId);
    if (!t) return;
    if (!window.confirm(`Отметить поручение №${t.num} как «Выполнено»? Это изменит статус в реестре.`)) return;
    setStatus(t.id, "done", { comment: "выполнено", dateEnd: t.dateEnd || todayStr() });
    pushToast(`Поручение №${t.num} отмечено «Выполнено»`, <I.CheckCircle size={12} />, "var(--ok)");
  };

  const buildMessages = (task) => {
    if (!task) return [];
    const due = task.dateEnd || task.dateStart || "—";
    const msgs = [
      { dir: "in", bot: true, text: "Вам назначено новое поручение 👇", time: "14:30" },
      { dir: "in", bot: true, card: { num: task.num, mon: task.mon, text: task.text, due, dept: task.dept }, keyboard: true, time: "14:30" },
    ];
    if (task.status === "accepted" || task.status === "done") {
      msgs.push({ dir: "out", text: "✅ Принял в работу", time: "14:33" });
      msgs.push({ dir: "in", bot: true, text: "Принято. На сайте статус обновлён на «Принято». Отдел контроля уведомлён.", time: "14:33" });
      msgs.push({ kind: "sync", text: "Синхронизировано с реестром" });
    }
    if (task.status === "done") {
      msgs.push({ dir: "out", text: "✔️ Выполнено", time: "15:10" });
      msgs.push({ dir: "in", bot: true, text: `Отлично! Поручение №${task.num} закрыто и отмечено «Выполнено» в реестре.`, time: "15:10" });
      msgs.push({ kind: "sync", text: "Реестр обновлён · контроль уведомлён" });
    }
    return msgs;
  };

  const activeTask = tasks.find(t => t.id === tg.taskId) || null;
  const activePerson = activeTask ? personFor(activeTask.assignee, activeTask.dept) : null;

  // ---- stats for the current view (month, or the filtered set when searching) ----
  const stat = (arr) => {
    const s = { total: arr.length, done: 0, work: 0, overdue: 0, waiting: 0 };
    arr.forEach(t => {
      const e = effStatus(t);
      if (e === "done") s.done++;
      else if (e === "overdue") s.overdue++;
      else if (e === "sent") s.waiting++;
      else s.work++;
    });
    return s;
  };
  const S = stat(filtered);

  const toggleDept = (d) => {
    const n = new Set(collapsed);
    n.has(d) ? n.delete(d) : n.add(d);
    setCollapsed(n);
  };
  const toggleExpand = (id) => {
    const n = new Set(expanded);
    if (n.has(id)) {
      n.delete(id);
    } else {
      n.add(id);
      if (window.API && window.API.listNotes && !notesByTask[id]) {
        window.API.listNotes(id)
          .then(ns => setNotesByTask(m => ({ ...m, [id]: Array.isArray(ns) ? ns : [] })))
          .catch(() => {});
      }
    }
    setExpanded(n);
  };

  const addTaskNote = async (taskId, text) => {
    if (!text.trim() || !window.API || !window.API.addNote) return;
    try {
      const note = await window.API.addNote(taskId, text.trim());
      // Dedup by id: the SSE 'note' event may have already appended this note
      // (race between the POST response and the live event).
      setNotesByTask(m => {
        const cur = m[taskId] || [];
        if (cur.some(n => n.id === note.id)) return m;
        return { ...m, [taskId]: [...cur, note] };
      });
      pushToast("Заметка добавлена", <I.Check size={12} />, "var(--accent)");
    } catch (e) {
      pushToast(String((e && e.message) || e), <I.Alert size={12} />, "var(--warn)");
    }
  };

  // ---- export ----
  const exportCSV = () => {
    const header = ["№","Дата поручения","Формулировка поручения","Ответственный","Дата начала","Дата окончания","Статус"];
    const esc = (v) => `"${String(v == null ? "" : v).replace(/"/g, '""')}"`;
    const lines = [header.map(esc).join(";")];
    filtered.forEach(t => {
      lines.push([t.num, t.dateOrder, t.text, t.assignee, t.dateStart, t.dateEnd || "", STATUS_META[effStatus(t)].label].map(esc).join(";"));
    });
    const blob = new Blob(["\uFEFF" + lines.join("\r\n")], { type: "text/csv;charset=utf-8" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `Поручения_${month}_${year}.csv`;
    a.click();
    setExportOpen(false);
    pushToast("Выгружено в Excel (CSV)", <I.Download size={12} />, "var(--ok)");
  };
  // Server-side .xlsx export in the register format (one sheet per month).
  // scope "year" → whole year, "month" → current month tab.
  const exportXlsx = (scope) => {
    setExportOpen(false);
    const q = new URLSearchParams();
    q.set("year", year);
    if (scope === "month") q.set("month", month);
    const a = document.createElement("a");
    a.href = "/api/tasks/export?" + q.toString();
    document.body.appendChild(a);
    a.click();
    a.remove();
    pushToast(scope === "month" ? `Выгружен ${month} ${year} (Excel)` : `Выгружен ${year} год (Excel)`, <I.Download size={12} />, "var(--ok)");
  };
  const exportPrint = () => {
    setExportOpen(false);
    setTimeout(() => window.print(), 100);
  };

  // ---- import: pick .xlsx → preview diff → resolve conflicts → apply ----
  const handleImportFile = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file || !window.API) return;
    setExportOpen(false);
    setImporting(true);
    try {
      const prev = await window.API.importPreview(file, year, month);
      setImportPrev(prev);
    } catch (err) {
      pushToast("Импорт: " + String((err && err.message) || err), <I.Alert size={12} />, "var(--err)");
    } finally {
      setImporting(false);
    }
  };
  const onImported = (res) => {
    setImportPrev(null);
    reload();
    pushToast(`Импорт: добавлено ${res.created}, обновлено ${res.updated}`, <I.Check size={12} />, "var(--ok)");
  };

  const createOrder = async (payload) => {
    const fallbackDept = personFor(payload.assignee, "").dept || (PEOPLE[payload.assignee]?.dept) || "Юридический отдел";
    const realDept = PEOPLE[payload.assignee] ? PEOPLE[payload.assignee].dept : fallbackDept;
    const input = { mon: month, year, text: payload.text, assignee: payload.assignee, dept: realDept, dateEnd: payload.due || "" };

    // Persist via the backend (server assigns id and the next number); fall back to a local task if offline.
    let t = null;
    try {
      if (window.API) t = await window.API.createTask(input);
    } catch (e) {
      pushToast("Не удалось сохранить на сервере — создано локально", <I.Alert size={12} />, "var(--warn)");
    }
    if (!t) {
      const maxNum = Math.max(0, ...monthTasks.map(x => x.num || 0));
      t = {
        id: Date.now(),
        mon: month,
        year,
        num: maxNum + 1,
        dateOrder: todayStr(),
        dateStart: todayStr(),
        dateEnd: payload.due || null,
        text: payload.text,
        assignee: payload.assignee,
        dept: realDept,
        status: "draft",
        comment: "",
      };
    }
    setTasks(ts => [...ts, t]);
    setNewOpen(false);
    pushToast(`Создано поручение №${t.num}`, <I.Plus size={12} />, "var(--accent)");
    if (payload.sendNow) {
      setTimeout(() => openTelegram(t), 300);
    }
  };

  const saveOrder = async (id, patch) => {
    setTasks(ts => ts.map(t => t.id === id ? { ...t, ...patch } : t));
    setEditTask(null);
    if (window.API) { try { await window.API.updateTask(id, patch); } catch (e) {} }
    pushToast("Поручение обновлено", <I.Check size={12} />, "var(--accent)");
  };

  const deleteOrder = async (task) => {
    if (!window.confirm(`Удалить поручение №${task.num} без возможности восстановления?`)) return;
    setTasks(ts => ts.filter(t => t.id !== task.id));
    if (window.API && window.API.deleteTask) {
      try { await window.API.deleteTask(task.id); }
      catch (e) { pushToast(String((e && e.message) || e), <I.Alert size={12} />, "var(--warn)"); reload(); return; }
    }
    pushToast(`Поручение №${task.num} удалено`, <I.Trash size={12} />, "var(--err)");
  };

  const cancelOrder = async (task) => {
    if (!window.confirm(`Отменить поручение №${task.num}? Оно останется в реестре со статусом «Отменено».`)) return;
    setStatus(task.id, "cancelled");
    pushToast(`Поручение №${task.num} отменено`, <I.X size={12} />, "var(--ink-400)");
  };

  const reassignOrder = async (id, assignee, dept) => {
    setReassign(null);
    const patch = { assignee };
    if (dept) patch.dept = dept;
    setTasks(ts => ts.map(t => t.id === id ? { ...t, ...patch } : t));
    if (window.API) { try { await window.API.updateTask(id, patch); } catch (e) { pushToast(String((e && e.message) || e), <I.Alert size={12} />, "var(--warn)"); reload(); return; } }
    pushToast(`Ответственный изменён → ${assignee}`, <I.Users size={12} />, "var(--accent)");
  };

  // Group by the departments actually present in the filtered tasks (not just the
  // directory), so imported tasks with an empty/unknown dept aren't hidden — they
  // fall into a "Без отдела" bucket. Known departments keep their directory order.
  const deptOf = (t) => t.dept || "Без отдела";
  const depts = [...new Set(filtered.map(deptOf))].sort((a, b) => {
    const ia = DEPARTMENTS.indexOf(a), ib = DEPARTMENTS.indexOf(b);
    return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
  });
  const canEdit = !window.ME || window.ME.role !== "executor"; // executor: read-only register (own tasks)
  const canDelete = window.ME && window.ME.role === "admin";   // hard delete: admin only

  return (
    <div className="page page-fade">
      <div className="doc-banner">
        <div className="seal"><I.Clipboard size={26} /></div>
        <div style={{ minWidth: 0 }}>
          <h1>Контроль исполнения поручений</h1>
          <div className="sub">Реестр {year} · {yearCount(year)} поручений · АО «Юридический центр»</div>
        </div>

        <div className="year-switch">
          <button className="year-step" onClick={() => changeYear(-1)} disabled={year <= YEARS[0]} title="Предыдущий год"><I.Chevron size={15} style={{ transform: "rotate(90deg)" }} /></button>
          <button className="year-current" onClick={() => setYearMenu(o => !o)}>
            {year} <I.Chevron size={13} style={{ opacity: .6 }} />
          </button>
          <button className="year-step" onClick={() => changeYear(1)} disabled={year >= YEARS[YEARS.length - 1]} title="Следующий год"><I.Chevron size={15} style={{ transform: "rotate(-90deg)" }} /></button>
          {yearMenu && (
            <div className="year-menu" onMouseLeave={() => setYearMenu(false)}>
              {YEARS.map(y => (
                <button key={y} className={y === year ? "active" : ""} onClick={() => { setYear(y); setYearMenu(false); }}>
                  <I.Calendar size={14} /> {y} год
                  <span className="cnt">{yearCount(y)} поруч.</span>
                </button>
              ))}
            </div>
          )}
        </div>

        <div className="tg-status" onClick={() => setTgCfgOpen(true)} title="Настройки Telegram-бота">
          <span className="tg-logo"><I.Plane size={12} /></span>
          <div>
            <div style={{ fontWeight: 600 }}>Telegram-бот</div>
            <div style={{ fontSize: 11, color: "rgba(255,255,255,.55)", display: "flex", alignItems: "center", gap: 5 }}>
              {tgInfo.configured
                ? <><span className="live-dot"></span> подключён{tgInfo.username ? ` · @${tgInfo.username}` : ""}</>
                : <>● не подключён · настроить</>}
            </div>
          </div>
        </div>
      </div>

      <div className="month-tabs">
        <button className={`month-tab ${yearScope ? "active" : ""}`} onClick={() => setYearScope(true)}>
          Весь год<span className="cnt">{yearMatch.length}</span>
        </button>
        {months.map(m => (
          <button key={m} className={`month-tab ${!yearScope && m === month ? "active" : ""}`} onClick={() => { setYearScope(false); setMonth(m); }}>
            {m}<span className="cnt">{monthCount(m)}</span>
          </button>
        ))}
      </div>

      <div className="kpi-strip">
        <MiniKpi n={S.total} label={(anyFilter || yearScope) ? "Всего найдено" : "Всего за месяц"} icon={<I.Clipboard size={12} />} barColor="var(--ink-300)" pct={100} />
        <MiniKpi n={S.done} label="Выполнено" icon={<I.CheckCircle size={12} />} barColor="var(--ok)" pct={S.total ? S.done / S.total * 100 : 0} />
        <MiniKpi n={S.work} label="В работе / принято" icon={<I.Hourglass size={12} />} barColor="var(--warn)" pct={S.total ? S.work / S.total * 100 : 0} />
        <MiniKpi n={S.waiting} label="Ждут в Telegram" icon={<I.Plane size={12} />} barColor="#2682c0" pct={S.total ? S.waiting / S.total * 100 : 0} />
        <MiniKpi n={S.overdue} label="Просрочено" icon={<I.Alert size={12} />} barColor="var(--err)" pct={S.total ? S.overdue / S.total * 100 : 0} />
      </div>

      <div className="orders-toolbar">
        <div className="seg">
          <button className={group === "dept" ? "active" : ""} onClick={() => setGroup("dept")}><I.Building size={13} /> По отделам</button>
          <button className={group === "list" ? "active" : ""} onClick={() => setGroup("list")}><I.Docs size={13} /> Списком</button>
        </div>

        <div className="search" style={{ maxWidth: 240, minWidth: 170 }}>
          <I.Search size={14} />
          <input placeholder="Поиск по тексту и ответственному…" value={query} onChange={e => { setQuery(e.target.value); if (e.target.value.trim()) setYearScope(true); }} />
        </div>

        <select className="fld" style={{ padding: "7px 10px", maxWidth: 200, fontSize: 13 }} value={assigneeFilter} onChange={e => { setAssigneeFilter(e.target.value); if (e.target.value !== "all") setYearScope(true); }} title="Фильтр по ответственному">
          <option value="all">Все ответственные</option>
          {Object.keys(dir.people).map(n => <option key={n} value={n}>{n}</option>)}
        </select>
        <select className="fld" style={{ padding: "7px 10px", maxWidth: 180, fontSize: 13 }} value={deptFilter} onChange={e => { setDeptFilter(e.target.value); if (e.target.value !== "all") setYearScope(true); }} title="Фильтр по отделу">
          <option value="all">Все отделы</option>
          {dir.departments.map(d => <option key={d} value={d}>{d}</option>)}
        </select>

        <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
          {[["all","Все"],["overdue","Просрочено"],["sent","Ждут"],["in_progress","В работе"],["done","Выполнено"]].map(([k,l]) => (
            <button key={k} className={`field ${statusFilter === k ? "active" : ""}`} style={{ padding: "6px 11px" }} onClick={() => { setStatusFilter(k); if (k !== "all") setYearScope(true); }}>{l}</button>
          ))}
        </div>

        {anyFilter && (
          <button className="field" style={{ padding: "6px 11px" }} title="Сбросить фильтры"
            onClick={() => { setQuery(""); setStatusFilter("all"); setAssigneeFilter("all"); setDeptFilter("all"); setYearScope(false); }}>
            <I.X size={12} /> Сбросить
          </button>
        )}

        <div style={{ marginLeft: "auto", display: "flex", gap: 8, position: "relative" }}>
          <div style={{ position: "relative" }}>
            <button className="btn btn-outline" onClick={() => setExportOpen(o => !o)}><I.Download size={14} /> Экспорт <I.Chevron size={13} /></button>
            {exportOpen && (
              <div className="menu-pop" onMouseLeave={() => setExportOpen(false)}>
                <button onClick={() => exportXlsx("year")}><I.FileSpread size={16} style={{ color: "#1f7a4c" }} /> Excel — весь {year} год</button>
                <button onClick={() => exportXlsx("month")}><I.FileSpread size={16} style={{ color: "#1f7a4c" }} /> Excel — {month}</button>
                <button onClick={exportCSV}><I.FileSpread size={16} style={{ color: "var(--ink-500)" }} /> Текущий вид (CSV)</button>
                <button onClick={exportPrint}><I.Printer size={16} style={{ color: "var(--ink-500)" }} /> Печать / PDF</button>
              </div>
            )}
          </div>
          {canEdit && (
            <>
              <button className="btn btn-outline" onClick={() => importInputRef.current && importInputRef.current.click()} disabled={importing}>
                <I.Upload size={14} /> {importing ? "Чтение…" : "Импорт"}
              </button>
              <input ref={importInputRef} type="file" accept=".xlsx,.csv" style={{ display: "none" }} onChange={handleImportFile} />
            </>
          )}
          {canEdit && <button className="btn btn-primary" onClick={() => setNewOpen(true)}><I.Plus size={14} /> Новое поручение</button>}
        </div>
      </div>

      {(anyFilter || yearScope) && (
        <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: "var(--ink-500)", margin: "2px 2px 12px" }}>
          <I.Search size={13} />
          <span>
            {yearScope ? `За весь ${year} год` : `За ${month} ${year}`}{statusFilter !== "all" ? ` · ${({ overdue: "просрочено", sent: "ждут", in_progress: "в работе", done: "выполнено" })[statusFilter] || statusFilter}` : ""}{assigneeFilter !== "all" ? ` · ${assigneeFilter}` : ""}{deptFilter !== "all" ? ` · ${deptFilter}` : ""}{query ? ` · «${query}»` : ""} — найдено {filtered.length}
          </span>
        </div>
      )}

      {/* column header */}
      {filtered.length > 0 && (
      <div className="order-row" style={{ padding: "4px 16px 8px", borderBottom: "none", background: "transparent" }}>
        <div style={{ fontSize: 10.5, color: "var(--ink-300)", textTransform: "uppercase", letterSpacing: ".05em" }}>№</div>
        <div style={{ fontSize: 10.5, color: "var(--ink-300)", textTransform: "uppercase", letterSpacing: ".05em" }}>Формулировка поручения</div>
        <div style={{ fontSize: 10.5, color: "var(--ink-300)", textTransform: "uppercase", letterSpacing: ".05em" }}>Ответственный</div>
        <div className="due-h" style={{ fontSize: 10.5, color: "var(--ink-300)", textTransform: "uppercase", letterSpacing: ".05em" }}>Срок</div>
        <div style={{ fontSize: 10.5, color: "var(--ink-300)", textTransform: "uppercase", letterSpacing: ".05em" }}>Статус</div>
        <div></div>
      </div>
      )}

      {!yearScope && !anyFilter && monthTasks.length === 0 ? (
        <div className="empty-reg">
          <div className="ic"><I.Clipboard size={26} /></div>
          <h3>За {month} {year} поручений пока нет</h3>
          <p>Выберите другой месяц или год в шапке реестра — либо создайте первое поручение, оно сразу уйдёт исполнителю в Telegram.</p>
          {canEdit && <button className="btn btn-accent" onClick={() => setNewOpen(true)}><I.Plus size={14} /> Новое поручение</button>}
        </div>
      ) : filtered.length === 0 ? (
        <div className="card" style={{ padding: "48px", textAlign: "center", color: "var(--ink-400)" }}>
          {(anyFilter || yearScope) ? "По заданным фильтрам поручений не найдено" : "По заданным условиям поручений не найдено"}
        </div>
      ) : null}

      {group === "dept" ? (
        depts.map(d => {
          const list = filtered.filter(t => deptOf(t) === d);
          const done = list.filter(t => effStatus(t) === "done").length;
          const pct = list.length ? Math.round(done / list.length * 100) : 0;
          const isOpen = !collapsed.has(d);
          return (
            <div key={d} className={`dept-group ${isOpen ? "open" : ""}`}>
              <div className="dept-head" onClick={() => toggleDept(d)}>
                <I.Chevron size={16} className="chev" />
                <span className="dept-mark" style={{ background: DEPT_COLORS[d] || "var(--ink-500)" }}><I.Users size={15} /></span>
                <div>
                  <div className="name">{d}</div>
                  <div className="meta">{list.length} поруч. · выполнено {done}</div>
                </div>
                <div className="mini-prog"><i style={{ width: pct + "%" }}></i></div>
                <div className="pct">{pct}%</div>
              </div>
              {isOpen && (
                <div className="dept-body">
                  {list.map(t => (
                    <OrderRow key={t.id} t={t} expanded={expanded.has(t.id)} onExpand={() => toggleExpand(t.id)} onTelegram={() => openTelegram(t)} onEdit={() => setEditTask(t)} onDelete={() => deleteOrder(t)} onCancel={() => cancelOrder(t)} onReassign={() => setReassign(t)} canEdit={canEdit} canDelete={canDelete} showMonth={yearScope} notes={notesByTask[t.id]} onAddNote={addTaskNote} />
                  ))}
                </div>
              )}
            </div>
          );
        })
      ) : (
        <div className="dept-body" style={{ borderRadius: "var(--r-md)", borderTop: "1px solid var(--ink-100)" }}>
          {filtered.map(t => (
            <OrderRow key={t.id} t={t} expanded={expanded.has(t.id)} onExpand={() => toggleExpand(t.id)} onTelegram={() => openTelegram(t)} onEdit={() => setEditTask(t)} onDelete={() => deleteOrder(t)} onCancel={() => cancelOrder(t)} onReassign={() => setReassign(t)} canEdit={canEdit} canDelete={canDelete} showDept showMonth={yearScope} notes={notesByTask[t.id]} onAddNote={addTaskNote} />
          ))}
        </div>
      )}

      <TelegramPanel
        open={tg.open}
        person={activePerson}
        task={activeTask}
        notes={notesByTask[tg.taskId]}
        onClose={() => setTg({ open: false, taskId: null })}
        onAccept={tgAccept}
        onDone={tgDone}
        onSendNote={(text) => addTaskNote(tg.taskId, text)}
      />

      {(newOpen || editTask) && (
        <NewOrderModal
          month={month}
          task={editTask}
          onClose={() => { setNewOpen(false); setEditTask(null); }}
          onCreate={createOrder}
          onSave={saveOrder}
        />
      )}
      {reassign && <ReassignModal task={reassign} onClose={() => setReassign(null)} onApply={reassignOrder} />}
      {importPrev && <ImportModal prev={importPrev} onClose={() => setImportPrev(null)} onApplied={onImported} />}
      {tgCfgOpen && <TelegramConfigModal onClose={() => setTgCfgOpen(false)} onSaved={(st) => setTgInfo(st)} />}

      <Toasts items={toasts} />
    </div>
  );
}

function MiniKpi({ n, label, icon, barColor, pct }) {
  return (
    <div className="kpi-mini">
      <div className="n">{n}</div>
      <div className="l">{icon} {label}</div>
      <div className="bar"><i style={{ width: Math.max(4, pct) + "%", background: barColor }}></i></div>
    </div>
  );
}

// NotesBlock — shared note/question thread for a task (site + Telegram), shown when a row is expanded.
function NotesBlock({ notes, onAdd }) {
  const [text, setText] = React.useState("");
  const list = notes || [];
  const submit = () => { if (text.trim()) { onAdd(text.trim()); setText(""); } };
  return (
    <div style={{ marginTop: 10, borderTop: "1px solid var(--ink-100)", paddingTop: 10 }} onClick={e => e.stopPropagation()}>
      <div style={{ fontSize: 10.5, color: "var(--ink-400)", textTransform: "uppercase", letterSpacing: ".05em", marginBottom: 6 }}>
        Заметки и вопросы{list.length > 0 ? ` · ${list.length}` : ""}
      </div>
      {list.length === 0 && <div style={{ fontSize: 12.5, color: "var(--ink-300)", marginBottom: 8 }}>Пока нет заметок. Вопросы исполнителя из Telegram появятся здесь.</div>}
      <div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 8 }}>
        {list.map(n => (
          <div key={n.id} style={{ fontSize: 12.5, background: n.kind === "question" ? "var(--accent-soft)" : "var(--ink-50)", borderRadius: 8, padding: "6px 10px" }}>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", color: "var(--ink-400)", fontSize: 11, marginBottom: 2 }}>
              {n.kind === "question" ? <I.Help size={11} /> : <I.Edit size={11} />}
              <b style={{ color: "var(--ink-700)" }}>{n.author}</b>
              <span>· {n.source === "telegram" ? "Telegram" : "сайт"}</span>
              <span>· {n.created}</span>
            </div>
            <div style={{ whiteSpace: "pre-wrap" }}>{n.text}</div>
          </div>
        ))}
      </div>
      <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
        <input className="fld" style={{ flex: 1, padding: "6px 10px", fontSize: 12.5 }} placeholder="Заметка или ответ исполнителю…"
          value={text} onChange={e => setText(e.target.value)} onKeyDown={e => { if (e.key === "Enter") submit(); }} />
        <MicButton size={15} title="Надиктовать заметку" onText={(t) => setText(p => (p ? p.trimEnd() + " " : "") + t)} />
        <button className="btn btn-outline" style={{ padding: "6px 12px", fontSize: 12 }} onClick={submit} disabled={!text.trim()}>Добавить</button>
      </div>
    </div>
  );
}

function OrderRow({ t, expanded, onExpand, onTelegram, onEdit, onDelete, onCancel, onReassign, canEdit, canDelete, showDept, showMonth, notes, onAddNote }) {
  const e = effStatus(t);
  const p = personFor(t.assignee, t.dept);
  const due = t.dateEnd || t.dateStart || "—";
  const btnLabel = (t.status === "draft" || t.status === "in_progress")
    ? "Отправить" : t.status === "sent" ? "Ожидает" : t.status === "done" ? "Чат" : "Открыть";
  const btnCls = (t.status === "draft" || t.status === "in_progress") ? "" : t.status === "done" ? "done" : "sent";

  return (
    <div className={`order-row ${e === "overdue" ? "is-overdue" : ""}`}>
      <div className="order-num">{t.num != null ? t.num : "—"}</div>
      <div className="order-main">
        <div className={`order-text ${expanded ? "" : "clamp"}`} onClick={onExpand} title="Нажмите, чтобы развернуть">{t.text}</div>
        <div className="order-meta-line">
          {showMonth && <span className="chip" style={{ textTransform: "capitalize", fontWeight: 500, color: "var(--accent-strong)" }}>{t.mon}</span>}
          <span className="chip"><I.Calendar size={11} /> от {t.dateOrder || "—"}</span>
          {showDept && <span className="chip" style={{ color: DEPT_COLORS[t.dept] }}>● {t.dept}</span>}
          {t.comment && e === "done" && <span className="chip" style={{ color: "var(--ok)" }}><I.Check size={11} /> {t.comment}</span>}
        </div>
        {expanded && canEdit && (
          <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
            <button className="btn btn-ghost" style={{ padding: "4px 10px", fontSize: 12 }} onClick={onEdit}><I.Edit size={13} /> Редактировать</button>
            <button className="btn btn-ghost" style={{ padding: "4px 10px", fontSize: 12 }} onClick={onReassign}><I.Users size={13} /> Сменить ответственного</button>
            {e !== "cancelled" && e !== "done" && (
              <button className="btn btn-ghost" style={{ padding: "4px 10px", fontSize: 12, color: "var(--warn)" }} onClick={onCancel}><I.X size={13} /> Отменить</button>
            )}
            {canDelete && <button className="btn btn-danger" style={{ padding: "4px 10px", fontSize: 12 }} onClick={onDelete}><I.Trash size={13} /> Удалить</button>}
          </div>
        )}
        {expanded && <NotesBlock notes={notes} onAdd={(txt) => onAddNote && onAddNote(t.id, txt)} />}
      </div>
      <div className="order-assignee">
        <Avatar name={p.name} color={p.color} size={30} />
        <div style={{ minWidth: 0 }}>
          <div className="nm">{t.assignee}</div>
          <div className="tg">{p.tg}</div>
        </div>
      </div>
      <div className="order-due">
        <div className="lbl">срок</div>
        {due}
      </div>
      <div><StatusPill status={e} /></div>
      <div className="order-actions">
        <button className={`tg-send-btn ${btnCls}`} onClick={onTelegram} title="Telegram">
          <I.Plane size={13} /> {btnLabel}
        </button>
      </div>
    </div>
  );
}

// ---- Reassign modal: change the responsible person on a task ----
function ReassignModal({ task, onClose, onApply }) {
  const dir = useDirectory(); // active people only (dismissed are filtered out)
  const peopleMap = dir.people || {};
  const [assignee, setAssignee] = React.useState(task.assignee || "");
  const options = Object.keys(peopleMap).map(k => ({ value: k, label: k, sub: peopleMap[k].dept }));
  if (assignee && !peopleMap[assignee]) options.unshift({ value: assignee, label: assignee + " (текущий)" });
  const newDept = (peopleMap[assignee] || {}).dept || "";

  return (
    <div className="tg-overlay open" style={{ background: "rgba(13,22,32,.5)", display: "grid", placeItems: "center", zIndex: 260 }} onClick={onClose}>
      <div className="card" style={{ width: 460, maxWidth: "92vw", boxShadow: "var(--sh-3)" }} onClick={e => e.stopPropagation()}>
        <div className="card-head">
          <div style={{ flex: 1 }}>
            <h3 className="card-title">Сменить ответственного</h3>
            <p className="card-sub">Поручение №{task.num} · сейчас: {task.assignee || "—"}</p>
          </div>
          <button className="icon-btn" style={{ color: "var(--ink-400)" }} onClick={onClose}><I.X size={18} /></button>
        </div>
        <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <div>
            <label className="fld-label">Новый ответственный</label>
            <Combobox options={options} value={assignee} onSelect={setAssignee} placeholder="Начните вводить ФИО…" />
          </div>
          {newDept && newDept !== task.dept && (
            <div style={{ fontSize: 12.5, color: "var(--ink-400)" }}>Отдел поручения изменится на «{newDept}».</div>
          )}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: "0 20px 18px" }}>
          <button className="btn btn-ghost" onClick={onClose}>Отмена</button>
          <button className="btn btn-accent" onClick={() => onApply(task.id, assignee, newDept)} disabled={!assignee || assignee === task.assignee}>
            <I.Users size={14} /> Применить
          </button>
        </div>
      </div>
    </div>
  );
}

// ---- Import resolver: git-style left (DB) / right (file) conflict resolution ----
function ImportModal({ prev, onClose, onApplied }) {
  const conflicts = prev.conflicts || [];
  const news = prev.new || [];
  const [choices, setChoices] = React.useState(() => conflicts.map(c => c.suggest || "file"));
  const [includeNew, setIncludeNew] = React.useState(true);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState("");

  const FIELD = { text: "Формулировка", assignee: "Ответственный", dateEnd: "Срок", status: "Статус" };
  const statusLbl = (code) => (code && STATUS_META[code] ? STATUS_META[code].label : (code || "—"));
  const cellVal = (obj, field) => {
    if (field === "status") return statusLbl(obj.status); // stored step (not derived overdue)
    return (obj[field] || "—");
  };
  const pick = (i, side) => setChoices(cs => cs.map((c, j) => (j === i ? side : c)));
  const fileWins = choices.filter(c => c === "file").length;

  const apply = async () => {
    setBusy(true); setErr("");
    const tasks = [];
    if (includeNew) tasks.push(...news);
    conflicts.forEach((c, i) => { if (choices[i] === "file") tasks.push(c.file); });
    try {
      const r = await window.API.importApply(prev.year, tasks);
      onApplied(r);
    } catch (e) { setErr(String((e && e.message) || e)); setBusy(false); }
  };

  return (
    <div className="tg-overlay open" style={{ background: "rgba(13,22,32,.5)", display: "grid", placeItems: "center", zIndex: 270 }} onClick={onClose}>
      <div className="card" style={{ width: 780, maxWidth: "95vw", maxHeight: "90vh", display: "flex", flexDirection: "column", boxShadow: "var(--sh-3)" }} onClick={e => e.stopPropagation()}>
        <div className="card-head">
          <div style={{ flex: 1 }}>
            <h3 className="card-title">Импорт из Excel · {prev.year}</h3>
            <p className="card-sub">{news.length} новых · {conflicts.length} конфликтов · {prev.same} без изменений</p>
          </div>
          <button className="icon-btn" style={{ color: "var(--ink-400)" }} onClick={onClose}><I.X size={18} /></button>
        </div>

        <div className="card-body" style={{ overflowY: "auto", display: "flex", flexDirection: "column", gap: 14 }}>
          {news.length > 0 && (
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
              <input type="checkbox" checked={includeNew} onChange={e => setIncludeNew(e.target.checked)} />
              <b>Добавить {news.length} новых</b> <span style={{ color: "var(--ink-400)" }}>— есть в файле, нет в базе</span>
            </label>
          )}

          {conflicts.length === 0 ? (
            <div style={{ color: "var(--ink-400)", fontSize: 13 }}>Конфликтов нет — существующие данные совпадают.</div>
          ) : (
            <div>
              <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: ".05em", color: "var(--ink-400)", margin: "2px 0 8px" }}>
                Конфликты — отметьте, где данные актуальнее
              </div>
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                {conflicts.map((c, i) => (
                  <div key={i} style={{ border: "1px solid var(--ink-100)", borderRadius: "var(--r-md)", overflow: "hidden" }}>
                    <div style={{ padding: "6px 10px", background: "var(--ink-50)" }}>
                      <div style={{ fontSize: 12.5, fontWeight: 500 }}>№{c.num} · {c.mon}</div>
                      <div style={{ fontSize: 12, color: "var(--ink-500)", marginTop: 2 }}>{(c.db && c.db.text) || (c.file && c.file.text) || ""}</div>
                    </div>
                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
                      {["db", "file"].map(side => {
                        const sel = choices[i] === side;
                        const obj = side === "db" ? c.db : c.file;
                        return (
                          <div key={side} onClick={() => pick(i, side)}
                            style={{ padding: "8px 10px", cursor: "pointer", borderLeft: side === "file" ? "1px solid var(--ink-100)" : "none", background: sel ? "var(--accent-soft)" : "transparent" }}>
                            <label style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 5, fontSize: 11.5, fontWeight: 600, color: sel ? "var(--accent-strong)" : "var(--ink-500)", cursor: "pointer" }}>
                              <input type="radio" checked={sel} onChange={() => pick(i, side)} /> {side === "db" ? "В базе" : "Из файла"}
                            </label>
                            {c.fields.map(f => (
                              <div key={f} style={{ fontSize: 12, marginBottom: 2 }}>
                                <span style={{ color: "var(--ink-400)" }}>{FIELD[f] || f}: </span>
                                <span style={{ color: "var(--ink-700)" }}>{cellVal(obj, f)}</span>
                              </div>
                            ))}
                          </div>
                        );
                      })}
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
          {err && <div style={{ color: "var(--err)", fontSize: 12.5 }}>{err}</div>}
        </div>

        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "12px 20px", borderTop: "1px solid var(--ink-100)" }}>
          <span style={{ fontSize: 12, color: "var(--ink-400)" }}>Будет применено: {(includeNew ? news.length : 0) + fileWins} · без изменений {prev.same}</span>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-ghost" onClick={onClose}>Отмена</button>
            <button className="btn btn-accent" onClick={apply} disabled={busy} style={busy ? { opacity: 0.4, pointerEvents: "none" } : null}>
              <I.Check size={14} /> {busy ? "Применение…" : "Применить"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- New / edit order modal ----
function NewOrderModal({ month, task, onClose, onCreate, onSave }) {
  const editing = !!task;
  const toInput = (ru) => {
    const m = (ru || "").match(/(\d{2})\.(\d{2})\.(\d{4})/);
    return m ? `${m[3]}-${m[2]}-${m[1]}` : "";
  };
  const [text, setText] = React.useState(editing ? task.text : "");
  const dir = useDirectory();
  const peopleMap = dir.people;
  const [assignee, setAssignee] = React.useState(editing ? task.assignee : (Object.keys(peopleMap)[0] || ""));
  const [due, setDue] = React.useState(editing ? toInput(task.dateEnd) : "");
  const [sendNow, setSendNow] = React.useState(!editing);
  const person = peopleMap[assignee] || PEOPLE[assignee] || personFor(assignee, "");

  const comboOptions = Object.keys(peopleMap).map(k => ({ value: k, label: k, sub: peopleMap[k].dept }));
  if (editing && assignee && !peopleMap[assignee]) comboOptions.unshift({ value: assignee, label: assignee });

  const submit = () => {
    if (!text.trim()) return;
    const ru = due ? due.split("-").reverse().join(".") : "";
    if (editing) {
      onSave(task.id, { text: text.trim(), assignee, dateEnd: ru });
    } else {
      onCreate({ text: text.trim(), assignee, due: ru || null, sendNow });
    }
  };

  return (
    <div className="tg-overlay open" style={{ background: "rgba(13,22,32,.5)", display: "grid", placeItems: "center", zIndex: 250 }} onClick={onClose}>
      <div className="card" style={{ width: 540, maxWidth: "92vw", boxShadow: "var(--sh-3)" }} onClick={e => e.stopPropagation()}>
        <div className="card-head">
          <div style={{ flex: 1 }}>
            <h3 className="card-title">{editing ? `Редактировать поручение №${task.num}` : "Новое поручение"}</h3>
            <p className="card-sub">{editing ? "Изменения сохранятся в реестре" : `В реестр «${month}» · после создания будет отправлено исполнителю в Telegram`}</p>
          </div>
          <button className="icon-btn" style={{ color: "var(--ink-400)" }} onClick={onClose}><I.X size={18} /></button>
        </div>
        <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <div>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
              <label className="fld-label">Формулировка поручения</label>
              <MicButton size={14} title="Надиктовать формулировку" onText={(t) => setText(p => (p ? p.trimEnd() + " " : "") + t)} />
            </div>
            <textarea
              className="fld" rows={3}
              placeholder="Например: Подготовить и направить претензию по договору №…"
              value={text} onChange={e => setText(e.target.value)}
              style={{ resize: "vertical", width: "100%" }}
            />
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
            <div>
              <label className="fld-label">Ответственный</label>
              <Combobox
                options={comboOptions}
                value={assignee}
                onSelect={setAssignee}
                placeholder="Начните вводить ФИО…"
              />
            </div>
            <div>
              <label className="fld-label">Срок исполнения</label>
              <input type="date" className="fld" value={due} onChange={e => setDue(e.target.value)} style={{ width: "100%" }} />
            </div>
          </div>
          {!editing && (
            <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 14px", background: "var(--accent-soft)", borderRadius: "var(--r-md)" }}>
              <Avatar name={person.name} color={person.color} size={34} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 500 }}>{person.name}</div>
                <div style={{ fontSize: 12, color: "var(--accent-strong)" }}>{person.tg}</div>
              </div>
              <label style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, cursor: "pointer" }}>
                <CheckBox on={sendNow} onChange={setSendNow} />
                Отправить в Telegram сразу
              </label>
            </div>
          )}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: "0 20px 18px" }}>
          <button className="btn btn-ghost" onClick={onClose}>Отмена</button>
          <button className="btn btn-accent" onClick={submit} disabled={!text.trim()}>
            {editing ? <><I.Check size={14} /> Сохранить</> : <><I.Plane size={14} /> Создать и назначить</>}
          </button>
        </div>
      </div>
    </div>
  );
}

// ---- Telegram bot config modal (dynamic token, long-polling) ----
function TelegramConfigModal({ onClose, onSaved }) {
  const [info, setInfo] = React.useState({ configured: false, running: false, username: "", chats: [] });
  const [token, setToken] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState("");

  React.useEffect(() => {
    if (window.API && window.API.telegramStatus) {
      window.API.telegramStatus().then(setInfo).catch(() => {});
    }
  }, []);

  const chatSet = new Set((info.chats || []).map(c => (c.username || "").toLowerCase()));
  const people = Object.keys(PEOPLE).map(k => {
    const uname = (PEOPLE[k].tg || "").replace(/^@/, "").toLowerCase();
    return { name: k, tg: PEOPLE[k].tg, registered: chatSet.has(uname) };
  });
  const regCount = people.filter(p => p.registered).length;

  const connect = async () => {
    if (!token.trim() || !window.API) return;
    setBusy(true); setError("");
    try {
      const st = await window.API.telegramConfig(token.trim());
      setInfo(st);
      setToken("");
      onSaved && onSaved(st);
    } catch (e) {
      setError(String((e && e.message) || e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="tg-overlay open" style={{ background: "rgba(13,22,32,.5)", display: "grid", placeItems: "center", zIndex: 260 }} onClick={onClose}>
      <div className="card" style={{ width: 520, maxWidth: "92vw", boxShadow: "var(--sh-3)" }} onClick={e => e.stopPropagation()}>
        <div className="card-head">
          <span className="tg-logo" style={{ width: 26, height: 26 }}><I.Plane size={14} /></span>
          <div style={{ flex: 1 }}>
            <h3 className="card-title">Telegram-бот</h3>
            <p className="card-sub">
              {info.configured
                ? <>Подключён{info.username ? <> как <b>@{info.username}</b></> : null} · чатов: {regCount}/{people.length}</>
                : "Не подключён — вставьте токен от @BotFather"}
            </p>
          </div>
          <button className="icon-btn" style={{ color: "var(--ink-400)" }} onClick={onClose}><I.X size={18} /></button>
        </div>
        <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div>
            <label className="fld-label">Токен бота</label>
            <div style={{ display: "flex", gap: 8 }}>
              <input
                className="fld" style={{ flex: 1 }} type="password" placeholder="123456789:AA…"
                value={token} onChange={e => setToken(e.target.value)}
                onKeyDown={e => { if (e.key === "Enter") connect(); }}
              />
              <button className="btn btn-accent" onClick={connect} disabled={busy || !token.trim()}>
                {busy ? "Проверка…" : info.configured ? "Обновить" : "Подключить"}
              </button>
            </div>
            {error && <div style={{ color: "var(--err)", fontSize: 12.5, marginTop: 6 }}>{error}</div>}
          </div>

          <div style={{ fontSize: 12.5, color: "var(--ink-500)", background: "var(--ink-50)", borderRadius: "var(--r-md)", padding: "10px 12px" }}>
            Режим long-polling: токен задаётся здесь, в рантайме. Чтобы исполнитель получал поручения,
            он должен один раз написать боту <b>/start</b> — так бот узнаёт его chat&nbsp;id.
          </div>

          <div>
            <label className="fld-label">Исполнители · {regCount}/{people.length} подключены</label>
            <div style={{ maxHeight: 220, overflowY: "auto", border: "1px solid var(--ink-100)", borderRadius: "var(--r-md)" }}>
              {people.map(p => (
                <div key={p.name} style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 12px", borderBottom: "1px solid var(--ink-100)" }}>
                  <span style={{ flex: 1, minWidth: 0, fontSize: 13 }}>
                    {p.name} <span style={{ color: "var(--accent-strong)", fontSize: 12 }}>{p.tg}</span>
                  </span>
                  {p.registered
                    ? <span className="pill ok"><I.Check size={11} /> /start выполнен</span>
                    : <span className="pill muted">ждёт /start</span>}
                </div>
              ))}
            </div>
          </div>
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: "0 20px 18px" }}>
          <button className="btn btn-ghost" onClick={onClose}>Закрыть</button>
        </div>
      </div>
    </div>
  );
}

window.PorucheniyaPage = PorucheniyaPage;
