// templates-page.jsx — нова сторінка «Шаблони» з 3 рівнями function TemplatesPage({ role, onCreateFromTemplate, setPage }) { const [tab, setTab] = React.useState("projects"); const [openProject, setOpenProject] = React.useState(null); const [openStage, setOpenStage] = React.useState(null); const [openTask, setOpenTask] = React.useState(null); const [openHr, setOpenHr] = React.useState(null); const [creating, setCreating] = React.useState(null); // PROJECT_TEMPLATE для створення const projectTpl = window.PROJECT_TEMPLATES; const stageTpl = window.STAGE_TEMPLATES; const taskTpl = window.TASK_TEMPLATES; const hrTpl = window.HR_PROCESS_TEMPLATES || []; return (

Шаблони

Три рівні: проєктетапзадача обираєш шаблон проєкту — створюються всі задачі
{tab === "projects" && ( setOpenProject(t)} onCreate={(t) => setCreating(t)} /> )} {tab === "stages" && ( setOpenStage(t)} /> )} {tab === "tasks" && ( setOpenTask(t)} /> )} {tab === "hr" && ( setOpenHr(t)} /> )} {openProject && ( setOpenProject(null)} onCreate={() => { setCreating(openProject); setOpenProject(null); }} /> )} {openStage && setOpenStage(null)} />} {openTask && setOpenTask(null)} />} {openHr && setOpenHr(null)} />} {creating && ( setCreating(null)} onCreated={(newObjectId) => { setCreating(null); onCreateFromTemplate && onCreateFromTemplate(newObjectId); setPage && setPage("objects"); }} /> )}
); } // =========== ТАБ 1: Шаблони проєктів =========== function ProjectTemplates({ templates, onView, onCreate }) { const dirs = window.DIRECTION_LABELS; const WT = window.WORK_TYPE_LABELS; // Групуємо: схема намірів → невиробниче → виробниче → авторський нагляд const groupOf = (t) => { if (t.direction === "supervision") return "supervision"; if (t.workType === "schema") return "schema"; return t.purpose || "other"; }; const groupDefs = [ { key: "schema", label: "Житлова садибна / блокована забудова" }, { key: "non_production", label: "Невиробниче призначення" }, { key: "production", label: "Виробниче призначення" }, { key: "supervision", label: "Авторський нагляд" }, ]; const gradeOrder = (t) => (t.objectGrade === "СС1" ? 0 : 1); const sortFn = (a, b) => ((WT[a.workType]?.order ?? 5) - (WT[b.workType]?.order ?? 5)) || (gradeOrder(a) - gradeOrder(b)); const renderCard = (t) => { const stageCount = t.stageTemplateIds.length; const taskCount = t.stageTemplateIds.reduce((sum, sid) => { const s = window.getStageTemplate(sid); return sum + (s ? s.taskTemplateIds.length : 0); }, 0); const dir = dirs[t.direction]; const wt = WT[t.workType]; return (
{t.gradeLabel && {t.gradeLabel}} {dir.short}
{t.name}
{t.description}
{taskCount}задач
{stageCount}{window.plural(stageCount, "етап", "етапи", "етапів")}
{t.estimatedDays}{window.plural(t.estimatedDays, "день", "дні", "днів")}
); }; return (
{groupDefs.map(g => { const list = templates.filter(t => groupOf(t) === g.key).sort(sortFn); if (list.length === 0) return null; return (

{g.label}

{list.length} {window.plural(list.length, "шаблон", "шаблони", "шаблонів")}
{list.map(renderCard)}
); })}
); } // =========== ТАБ 2: Шаблони етапів =========== function StageTemplates({ templates, onView }) { const byStage = {}; templates.forEach(t => { (byStage[t.stage] = byStage[t.stage] || []).push(t); }); const allStages = [...window.DATA.STAGES, ...(window.DATA.CORRECTION_STAGES || [])]; return (
{Object.entries(byStage).map(([stageId, list]) => { const stage = allStages.find(s => s.id === stageId); return (

{stage?.name || stageId}

{list.length} {window.plural(list.length, "шаблон", "шаблони", "шаблонів")}
{list.map(t => (
onView(t)} style={{cursor: "pointer"}}>
{t.name}
{t.description}
{t.taskTemplateIds.length}задач
))}
); })}
); } // =========== Базові (наскрізні) задачі — застосовуються за правилом =========== function BaselineTasksSection({ onView }) { const baseline = window.BASELINE_TASKS || []; if (baseline.length === 0) return null; return (

Базові (наскрізні) задачі

описані один раз · підставляються за правилом застосовності
Не дублюються в кожному шаблоні етапу. Рушій додає їх при створенні проєкту залежно від типу робіт — тому правило можна змінити в одному місці.
{baseline.map(b => { const tt = window.getTaskTemplate(b.taskTemplateId); if (!tt) return null; const role = window.ASSIGNEE_ROLE_LABELS[tt.assigneeRole]; return (
onView(tt)} style={{cursor: "pointer"}}>
наскрізна
{tt.title}
Застосовується до {window.appliesToLabel(b.appliesTo)}
{b.rationale}
{role &&
{role.label}
}
); })}
); } // =========== ТАБ 3: Шаблони задач =========== function TaskTemplates({ templates, onView }) { const [filterSection, setFilterSection] = React.useState("all"); const sections = ["all", ...Array.from(new Set(templates.map(t => t.section)))]; const filtered = filterSection === "all" ? templates : templates.filter(t => t.section === filterSection); return (
{sections.map(s => ( ))}
{filtered.map(t => { const role = window.ASSIGNEE_ROLE_LABELS[t.assigneeRole]; return ( onView(t)} style={{cursor: "pointer"}}> ); })}
Задача Розділ Виконавець Тривалість Дедлайн Виїзд Залежності Файли
{t.icon || "•"}
{t.title}
{t.description}
{t.section} {role ? {role.label} : "—"} {t.estDays} дн +{t.deadlineOffset} дн {t.requiresSiteVisit ? так : } {t.dependencies?.length || 0} {t.attachments?.length || 0}
); } // =========== МОДАЛКА: ПЕРЕГЛЯД ШАБЛОНУ ПРОЄКТУ =========== function ProjectTemplateModal({ template, onClose, onCreate }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const allStages = [...window.DATA.STAGES, ...(window.DATA.CORRECTION_STAGES || [])]; const stages = template.stageTemplateIds.map(id => window.getStageTemplate(id)).filter(Boolean); const totalTasks = stages.reduce((s, st) => s + st.taskTemplateIds.length, 0); const dir = window.DIRECTION_LABELS[template.direction]; return ( <>
{template.name}
{template.description}
Задач
{totalTasks}
Етапів
{stages.length}
Тривалість
{template.estimatedDays}дн.
Напрямок
{dir.label}
{stages.map((st, i) => { const stage = allStages.find(s => s.id === st.stage); const tasks = st.taskTemplateIds.map(id => window.getTaskTemplate(id)).filter(Boolean); return (

Етап {i + 1}. {stage?.name || st.stage} · {st.name}

{tasks.length} {window.plural(tasks.length, "задача", "задачі", "задач")}
{tasks.map((t, j) => { const r = window.ASSIGNEE_ROLE_LABELS[t.assigneeRole]; return ( ); })}
Задача Розділ Виконавець Оцінка Дедлайн
{j + 1}
{t.title}
{t.section} {r ? {r.label} : "—"} {t.estDays} дн +{t.deadlineOffset} дн
); })}
); } // =========== МОДАЛКА: ШАБЛОН ЕТАПУ =========== function StageTemplateModal({ template, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const allStages = [...window.DATA.STAGES, ...(window.DATA.CORRECTION_STAGES || [])]; const stage = allStages.find(s => s.id === template.stage); const tasks = template.taskTemplateIds.map(id => window.getTaskTemplate(id)).filter(Boolean); return ( <>
{template.name}
Етап: {stage?.name || template.stage} · {template.description}
{tasks.map((t, j) => { const r = window.ASSIGNEE_ROLE_LABELS[t.assigneeRole]; return ( ); })}
Задача Розділ Виконавець Оцінка Дедлайн
{j + 1}
{t.title}
{t.description}
{t.section} {r ? {r.label} : "—"} {t.estDays} дн +{t.deadlineOffset} дн
); } // =========== МОДАЛКА: ШАБЛОН ЗАДАЧІ =========== function TaskTemplateModal({ template, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const r = window.ASSIGNEE_ROLE_LABELS[template.assigneeRole]; const deps = (template.dependencies || []).map(id => window.getTaskTemplate(id)).filter(Boolean); return ( <>
{template.section} {template.requiresSiteVisit && виїзд на майданчик}

{template.title}

{template.description}
Тривалість
{template.estDays}дн
Дедлайн
+{template.deadlineOffset}дн
Виконавець
{r ? {r.label} : "—"}
{template.subtasks && template.subtasks.length > 0 && (

Чек-лист підзадач

{template.subtasks.length} {window.plural(template.subtasks.length, "пункт", "пункти", "пунктів")}
    {template.subtasks.map((s, i) => (
  • {s.title}
  • ))}
)} {deps.length > 0 && (

Залежності

{deps.map(d => (
{d.title} {d.section}
))}
)} {template.attachments && template.attachments.length > 0 && (

Прикріплені шаблони документів

{template.attachments.map((a, i) => (
{a.name} {window.ATTACHMENT_TYPE_LABELS[a.type] || a.type}
))}
)}
); } window.TemplatesPage = TemplatesPage; // =========== ТАБ 4: HR / Команда (кадрові процеси) =========== function HrTemplates({ templates, onView }) { const cats = window.HR_CATEGORY_LABELS || [{ key: "career", label: "Кадрові процеси" }]; const renderCard = (t) => (
onView(t)} style={{cursor: "pointer"}}>
HR
{t.name}
{t.description}
{t.effect}
{t.taskTemplateIds.length}{window.plural(t.taskTemplateIds.length, "задача", "задачі", "задач")}
); return (
Кадрові процеси. Кар'єрні переходи запускаються з картки людини у розділі «Команда» (архітектор → ГАП; інженер-проєктувальник СС1 → ІІ, СС2 → І, СС3 → провідний; надбавка за сертифікат). Решта процесів — прийняття, відпустки, відрядження, звільнення, кваліфікація — стандартні набори задач із дедлайнами та чек-листами під загальну систему оподаткування (ТОВ, платник ПДВ).
{cats.map(cat => { const list = templates.filter(t => (t.category || "career") === cat.key); if (list.length === 0) return null; return (

{cat.label}

{list.length} {window.plural(list.length, "процес", "процеси", "процесів")}
{list.map(renderCard)}
); })}
); } function HrTemplateModal({ template, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const tasks = template.taskTemplateIds.map(id => window.getHrTaskTemplate(id)).filter(Boolean); return ( <>
{template.name}
Кадровий процес · {tasks.length} {window.plural(tasks.length, "задача", "задачі", "задач")}
Тригер{template.trigger}
Ефект{template.effect}

Задачі-супровід

{tasks.length}
{tasks.map((t, i) => { const r = window.ASSIGNEE_ROLE_LABELS[t.assigneeRole]; return (
{i + 1}. {t.title}
{t.description}
{r && {r.label}}
{t.checklist && t.checklist.length > 0 && (
    {t.checklist.map((c, j) => (
  • {c}
  • ))}
)}
); })}
); } // ============ ProjectTemplatePicker (для кнопки "+ Новий об'єкт") ============ function ProjectTemplatePicker({ onClose, onPick }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const templates = window.PROJECT_TEMPLATES; const dirs = window.DIRECTION_LABELS; return ( <>
Створити об'єкт з шаблону
Оберіть тип — задачі і етапи створяться автоматично
{templates.map(t => { const dir = dirs[t.direction]; const stageCount = t.stageTemplateIds.length; const taskCount = t.stageTemplateIds.reduce((sum, sid) => { const s = window.getStageTemplate(sid); return sum + (s ? s.taskTemplateIds.length : 0); }, 0); return ( ); })}
); } window.ProjectTemplatePicker = ProjectTemplatePicker;