// consequence-class.jsx — клас наслідків як рухоме поле об'єкта + неруйнівне до-налаштування // Демонструє логіку, узгоджену в чаті: // • клас попередній (із Завдання) → уточнюється розрахунком (в обидва боки) // • обсяг експертизи ВИВОДИТЬСЯ з класу (а не зашитий у шаблон) // • зміна класу не перестворює проєкт, а пропонує ДЕЛЬТУ і зберігає зроблене // ---- Дельта складу робіт між двома обсягами експертизи ---- window.expertiseDelta = function (oldScope, newScope) { const stageOf = (sc) => window.getStageTemplate(window.EXPERTISE_STAGE_FOR_SCOPE[sc]); const oldIds = (oldScope && stageOf(oldScope)?.taskTemplateIds) || []; const newIds = (newScope && stageOf(newScope)?.taskTemplateIds) || []; const added = newIds.filter(id => !oldIds.includes(id)).map(window.getTaskTemplate).filter(Boolean); const removed = oldIds.filter(id => !newIds.includes(id)).map(window.getTaskTemplate).filter(Boolean); return { added, removed }; }; // ===================================================================== // ПАНЕЛЬ КЛАСУ НАСЛІДКІВ — у розгорнутій картці об'єкта // ===================================================================== function ConsequenceClassPanel({ obj, onRefine }) { const history = obj.gradeHistory || [{ value: obj.grade, kind: "preliminary", date: "", by: "", basis: "—" }]; const last = history[history.length - 1]; const kind = obj.gradeKind || last.kind || "preliminary"; const scope = obj.expertiseScope; const scopeLabel = window.EXPERTISE_SCOPE_LABELS[scope]?.full || "—"; const isSchema = obj.workType === "schema"; const kindMeta = kind === "calculated" ? { label: "Уточнений розрахунком", tone: "live" } : { label: "Попередній — із Завдання на проєктування", tone: "warn" }; if (isSchema) { return (
Клас наслідків
Схема намірів забудови — клас наслідків не визначається (будівельний паспорт без експертизи).
); } return (
Клас наслідків
{obj.grade}
{kindMeta.label}
Обсяг експертизи: {scopeLabel}
Журнал змін
{history.map((h, i) => { const who = window.getTeam(h.by); const prev = i > 0 ? history[i - 1] : null; const changed = prev && prev.value !== h.value; return (
{changed && {prev.value} →} {h.value} {h.kind === "calculated" ? "розрахунок" : "попередній"} {h.date && {window.fmtDate ? window.fmtDate(h.date) : h.date}}
{h.basis}{who ? ` · ${who.initials}` : ""}
); })}
); } window.ConsequenceClassPanel = ConsequenceClassPanel; // ===================================================================== // МОДАЛКА: УТОЧНЕННЯ КЛАСУ → ДЕЛЬТА → ПІДТВЕРДЖЕННЯ // ===================================================================== function ConsequenceClassModal({ obj, tasks, onClose, onConfirm }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const [grade, setGrade] = React.useState(obj.grade); const [complexTechno, setComplexTechno] = React.useState(obj.expertiseScope === "strength"); const [basis, setBasis] = React.useState(""); const oldScope = obj.expertiseScope; const newScope = window.expertiseScopeForGrade(grade, { complexTechno }); const scopeChanged = oldScope !== newScope; const gradeChanged = grade !== obj.grade; const { added, removed } = window.expertiseDelta(oldScope, newScope); // вже наявні задачі експертизи цього об'єкта — щоб показати, що збережеться const objExpertiseTasks = (tasks || []).filter(t => t.obj === obj.id && t.stage === "expertise"); const doneCount = objExpertiseTasks.filter(t => t.status === "done").length; const SC = window.EXPERTISE_SCOPE_LABELS; const canConfirm = (gradeChanged || scopeChanged) && basis.trim().length > 0; const submit = () => { if (!canConfirm) return; onConfirm({ grade, scope: newScope, basis: basis.trim(), added, removed }); }; return ( <>
Уточнення класу наслідків
{obj.code} · {obj.name}
{/* Вибір класу */}
{["СС1", "СС2", "СС3"].map(g => ( ))}
{grade === "СС1" && ( )}
setBasis(e.target.value)} placeholder="Напр.: Розрахунок за ДБН А.2.2-3, додано підземний паркінг" autoFocus />
{/* Прев'ю наслідків */}
Що зміниться у проєкті
{!gradeChanged && !scopeChanged && (
Клас і обсяг експертизи не змінюються.
)} {gradeChanged && (
Клас наслідків {obj.grade} {grade}
)} {scopeChanged && (
Обсяг експертизи {SC[oldScope]?.label || "—"} {SC[newScope]?.label || "—"}
)} {added.length > 0 && (
Додаються задачі ({added.length})
{added.map(t => (
+ {t.title} {t.section}
))}
)} {removed.length > 0 && (
Стають необов'язковими ({removed.length})
{removed.map(t => (
{t.title} {t.section}
))}
Не видаляються автоматично — позначаються як необов'язкові, рішення за ГІПом.
)} {(gradeChanged || scopeChanged) && (
Вже зроблена робота зберігається{doneCount > 0 ? ` (${doneCount} ${window.plural(doneCount, "виконана задача", "виконані задачі", "виконаних задач")} експертизи)` : ""}. Перестворення проєкту не відбувається.
)}
); } window.ConsequenceClassModal = ConsequenceClassModal;