// projects-page.jsx — Проєктна діяльність: реєстр проєктів ГО + воркспейс одного // проєкту (вкладки Огляд / Деліверабли / Задачі / Оплати / Команда / Документи) // + картка контракту ГО для розділу «Договори». (function () { const SYS = window.SYS_DATE || "2026-05-29"; const money = (n) => (window.formatMoney ? window.formatMoney(n) : n); const fdate = (iso) => (window.formatDate ? window.formatDate(iso) : iso); const daysTo = (iso) => Math.round((new Date(iso + "T00:00:00") - new Date(SYS + "T00:00:00")) / 86400000); const ENTITY = (k) => (k === "fop" ? "ФОП Підкапка М.І." : "ТОВ «Укрбудпроєкт»"); const ENTITY_SHORT = (k) => (k === "fop" ? "ФОП" : "ТОВ"); // ════════════════════ РЕЄСТР ════════════════════ function ProjectActivityPage({ role, onOpenProject }) { const projects = window.useProjects ? window.useProjects() : (window.DATA.PROJECTS || []); const [filter, setFilter] = React.useState("all"); const [creating, setCreating] = React.useState(false); const canEdit = role === "director" || role === "accountant"; const active = projects.filter(p => p.status === "active"); const closed = projects.filter(p => p.status === "closed" || p.status === "archived"); const shown = filter === "active" ? active : filter === "closed" ? closed : projects; // KPI const fopPaid = projects .filter(p => p.execEntity === "fop") .reduce((a, p) => a + window.projectSummary(p).paid, 0); const dlvLive = active.reduce((a, p) => a + (p.deliverables || []).filter(d => d.status === "in_progress").length, 0); const partners = (window.DATA.NGO_PARTNERS || []).length; return (

Проєктна діяльність

{projects.length} {window.plural(projects.length, "проєкт", "проєкти", "проєктів")} з ГО {active.length} активних · {closed.length} закритих грантові / донорські проєкти
{canEdit && }
Що це. Робота в межах проєкту громадської організації (ГО): контракт живе рівно стільки, скільки сам проєкт донора — коли проєкт закривається, контракт переходить у статус «Закрито» й іде в архів. Виконавцем може бути ФОП або ТОВ; дохід ФОП-проєктів автоматично враховується в «Єдиний податок (ФОП)» (5% + 1% ВЗ).
Активних проєктів
{active.length}
{closed.length} у архіві
Отримано (ФОП, разом)
{money(fopPaid)}
оподатковано 5% + 1% ВЗ
Деліверабли у роботі
{dlvLive}
по активних проєктах
Партнерів-ГО
{partners}
у довіднику
{shown.map(p => onOpenProject(p.id)} />)} {shown.length === 0 &&
Проєктів у цій категорії немає.
}
{creating && window.CreateGoProjectModal && ( setCreating(false)} onCreated={(p) => { setCreating(false); onOpenProject(p.id); }} /> )}
); } function ProjectCard({ project, onOpen }) { const p = project; const partner = window.getNgoPartner(p.partner); const donor = window.getDonor(p.donor); const st = window.PROJECT_STATUS[p.status]; const sum = window.projectSummary(p); return (
{p.code} {ENTITY_SHORT(p.execEntity)} {st.label}

{p.name}

{partner?.short} донор: {donor?.name} {p.scope}
Період {fdate(p.start)} → {p.closedDate ? fdate(p.closedDate) : fdate(p.end)}
{money(sum.total)}
отримано {money(sum.paid)} ₴ · {sum.paidPct}%
{(p.team || []).map(id => { const t = window.getTeam(id); return
{t?.initials}
; })}
деліверабли: {sum.dlvDone}/{sum.dlvTotal} готово оплата {sum.paidPct}%
); } // ════════════════════ ВОРКСПЕЙС ════════════════════ const PRJ_TABS = [ { id: "overview", label: "Огляд" }, { id: "deliverables", label: "Деліверабли" }, { id: "tasks", label: "Задачі" }, { id: "billing", label: "Оплати" }, { id: "team", label: "Команда" }, { id: "docs", label: "Документи" }, ]; function ProjectGoWorkspace({ projId, role, onBack }) { if (window.useProjects) window.useProjects(); // реактивність при змінах стору const project = window.getGoProject(projId); const canEdit = role === "director" || role === "accountant"; const [tab, setTab] = React.useState("overview"); const [tasks, setTasks] = React.useState(() => (project ? (project.tasks || []).map(window.normalizeTask) : [])); const [drawerTask, setDrawerTask] = React.useState(null); const [closing, setClosing] = React.useState(false); const [editing, setEditing] = React.useState(false); const [, forceTick] = React.useState(0); const persist = () => { if (window.ProjectsStore) window.ProjectsStore.save(); forceTick(x => x + 1); }; React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") { if (drawerTask) setDrawerTask(null); else onBack(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onBack, drawerTask]); // зберігаємо зміни задач у самому проєкті + у сторі (між перезавантаженнями) React.useEffect(() => { if (project) { project.tasks = tasks; if (window.ProjectsStore) window.ProjectsStore.save(); } }, [tasks, project]); if (!project) return
Проєкт не знайдено.
; const partner = window.getNgoPartner(project.partner); const donor = window.getDonor(project.donor); const st = window.PROJECT_STATUS[project.status]; const onStatusChange = (taskId, status) => { setTasks(prev => prev.map(x => x.id === taskId ? { ...x, status } : x)); setDrawerTask(d => d && d.id === taskId ? { ...d, status } : d); }; const onTaskPatch = (taskId, patch) => { const apply = (x) => (typeof patch === "function" ? patch(x) : { ...x, ...patch }); setTasks(prev => prev.map(x => x.id === taskId ? apply(x) : x)); setDrawerTask(d => (d && d.id === taskId ? apply(d) : d)); }; return (
{project.code} {ENTITY_SHORT(project.execEntity)} {st.label}

{project.name}

{partner?.short} донор: {donor?.name} {project.scope}
{(project.team || []).map(id => { const t = window.getTeam(id); return
{t?.initials}
; })}
{canEdit && } {canEdit && project.status === "active" && } {canEdit && project.status !== "active" && }
{PRJ_TABS.map(tb => { let c = null; if (tb.id === "tasks") c = tasks.filter(t => t.status !== "done").length; if (tb.id === "deliverables") c = (project.deliverables || []).filter(d => d.status !== "done").length; return ( ); })}
{tab === "overview" && } {tab === "deliverables" && } {tab === "tasks" && } {tab === "billing" && } {tab === "team" && } {tab === "docs" && } {closing && ( setClosing(false)} onConfirm={(note) => { window.closeProject(projId, note); persist(); setClosing(false); }} /> )} {editing && window.CreateGoProjectModal && ( setEditing(false)} onCreated={() => { setEditing(false); persist(); }} /> )} {drawerTask && window.Drawer && ( setDrawerTask(null)} onStatusChange={onStatusChange} onTaskPatch={onTaskPatch} onOpenProject={() => {}} /> )}
); } // ── Підтвердження закриття проєкту ── function CloseProjectConfirm({ project, onCancel, onConfirm }) { const [note, setNote] = React.useState("Усі деліверабли передано, фінальний звіт донору затверджено. Проєкт завершено."); React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onCancel(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onCancel]); return (
Закрити проєкт?
{project.name}

Проєкт перейде у статус «Закрито» з датою {window.formatDate(window.SYS_DATE)} й одразу потрапить в архів. Контракт вважається закритим. Це можна скасувати кнопкою «Відновити».