// Employees — real staff directory (window.PEOPLE / API) with their поручения load. function EmployeesPage({ onOpenOrders }) { const [people, setPeople] = React.useState({}); const [tasks, setTasks] = React.useState([]); const [filter, setFilter] = React.useState("Все"); const [addOpen, setAddOpen] = React.useState(false); const [loading, setLoading] = React.useState(true); const load = React.useCallback(() => { if (!window.API) { setLoading(false); return; } setLoading(true); Promise.all([ window.API.people().catch(() => ({})), window.API.listTasks().catch(() => []), ]).then(([p, t]) => { setPeople(p || {}); setTasks(Array.isArray(t) ? t : []); }).finally(() => setLoading(false)); }, []); React.useEffect(() => { load(); }, [load]); const isAdmin = window.ME && window.ME.role === "admin"; const setActive = async (name, active) => { const verb = active ? "Восстановить" : "Уволить"; if (!window.confirm(`${verb} сотрудника «${name}»?`)) return; try { if (active) await window.API.restorePerson(name); else await window.API.dismissPerson(name); load(); if (window.refreshDirectory) window.refreshDirectory(); } catch (e) { alert(String((e && e.message) || e)); } }; const eff = (t) => (typeof effStatus === "function" ? effStatus(t) : t.status); const depts = (typeof DEPARTMENTS !== "undefined") ? DEPARTMENTS : []; const filters = ["Все", ...depts]; const rows = Object.keys(people).map(name => { const p = people[name] || {}; const mine = tasks.filter(t => t.assignee === name); const total = mine.length; const done = mine.filter(t => eff(t) === "done").length; const overdue = mine.filter(t => eff(t) === "overdue").length; const pct = total ? Math.round(done / total * 100) : 0; return { name, dept: p.dept || "—", tg: p.tg || "", color: p.color, total, done, overdue, pct, active: p.active !== false }; }).filter(r => filter === "Все" || r.dept === filter) .sort((a, b) => (a.active === b.active ? b.total - a.total : (a.active ? -1 : 1))); const exportCSV = () => { const header = ["ФИО", "Отдел", "Telegram", "Всего поручений", "Выполнено", "Просрочено"]; const esc = v => `"${String(v == null ? "" : v).replace(/"/g, '""')}"`; const lines = [header.map(esc).join(";")]; rows.forEach(r => lines.push([r.name, r.dept, r.tg, r.total, r.done, r.overdue].map(esc).join(";"))); const blob = new Blob(["" + lines.join("\r\n")], { type: "text/csv;charset=utf-8" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "Сотрудники.csv"; a.click(); }; return (

Сотрудники

{Object.keys(people).length} человек · {depts.length} отделов{loading ? " · загрузка…" : ""}

{filters.map(d => ( ))}
{rows.map(e => (
onOpenOrders && onOpenOrders({ assignee: e.name })} >
{initials(e.name)}
{e.name} {!e.active && Уволен}
{e.dept}{e.tg ? ` · ${e.tg}` : ""}
Выполнено = 70 ? "var(--ok)" : e.pct >= 40 ? "var(--ink-700)" : "var(--warn)" }}>{e.pct}%
= 70 ? "linear-gradient(90deg, #4cb685, var(--ok))" : "linear-gradient(90deg, var(--accent), var(--accent-strong))" }} />
{e.total} поручений · выполнено {e.done} {e.overdue > 0 && · просрочено {e.overdue}}
Поручения {isAdmin && ( e.active ? : )}
))} {rows.length === 0 &&
Нет сотрудников по фильтру
}
{addOpen && setAddOpen(false)} onAdded={() => { setAddOpen(false); load(); if (window.refreshDirectory) window.refreshDirectory(); }} />}
); } function AddPersonModal({ depts, onClose, onAdded }) { const [name, setName] = React.useState(""); const [dept, setDept] = React.useState(depts[0] || ""); const [tg, setTg] = React.useState(""); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(""); const submit = async () => { if (!name.trim() || !window.API) return; setBusy(true); setError(""); try { let handle = tg.trim(); if (handle && !handle.startsWith("@")) handle = "@" + handle; await window.API.addPerson({ name: name.trim(), dept, tg: handle }); onAdded(); } catch (e) { setError(String((e && e.message) || e)); } finally { setBusy(false); } }; return (
e.stopPropagation()}>

Новый сотрудник

Добавится в справочник и станет доступен как ответственный

setName(e.target.value)} />
setTg(e.target.value)} />
{error &&
{error}
}
); } window.EmployeesPage = EmployeesPage;