// project-workspace.jsx — повноцінний робочий простір ОДНОГО об'єкта (екран ГІПа) // Вкладки: Огляд · Розділи · Етапи й задачі · Графік · Команда · Фінанси · Нагляд // Вкладки Етапи/Графік/Команда/Фінанси/Нагляд — у project-workspace-tabs.jsx const PW_TABS = [ { id: "overview", label: "Огляд" }, { id: "sections", label: "Розділи" }, { id: "stages", label: "Етапи й задачі" }, { id: "gantt", label: "Графік" }, { id: "team", label: "Команда" }, { id: "finance", label: "Фінанси" }, { id: "supervision", label: "Нагляд" }, ]; // ---- Зведення ризиків/уваги через усі дані ERP по об'єкту ---- function pwBuildAlerts(obj, tasks) { const a = []; const objTasks = tasks.filter(t => t.obj === obj.id); objTasks.filter(t => t.status === "late").forEach(t => a.push({ tone: "warn", kind: "Прострочено", title: t.title, meta: window.getTeam(t.assignee)?.name || "" })); window.PM.sectionsFor(obj.id).filter(s => s.status === "remarks").forEach(s => a.push({ tone: "warn", kind: "Зауваження", title: `${s.mark} · ${s.name}`, meta: s.note || "" })); (window.DATA.INVOICES || []).filter(i => i.obj === obj.id && i.status === "overdue").forEach(i => a.push({ tone: "warn", kind: "Дебіторка", title: `Рахунок ${i.number} · ${window.formatMoney(i.amount)} ₴`, meta: `прострочка ${i.lateDays} дн` })); window.PM.permitsFor(obj.id).filter(p => p.status === "pending").forEach(p => a.push({ tone: "amber", kind: p.kind, title: p.title, meta: `${p.body}${p.note ? " · " + p.note : ""}` })); const k = window.PM.contractFor(obj.id); if (k) k.schedule.filter(s => !s.paidDate).forEach(s => { const d = window.PM.daysTo(s.dueDate); if (d >= 0 && d <= 30) a.push({ tone: "blue", kind: "Платіж", title: `${s.stage} · ${window.formatMoney(s.amount)} ₴`, meta: `через ${d} дн · ${window.PM.dm(s.dueDate)}` }); }); (obj.team || []).forEach(id => { const p = window.getTeam(id); if (p && p.kep) { const st = window.kepStatus(p.kep.validUntil); if (st.state !== "active") a.push({ tone: st.state === "expired" ? "warn" : "amber", kind: "КЕП", title: p.name, meta: `${st.label.toLowerCase()} · до ${window.formatDate(p.kep.validUntil)}` }); } }); return a; } // ---- Інфо по етапах (для смужки етапів) ---- function pwStageInfo(obj, tasks, stage) { const list = tasks.filter(t => t.obj === obj.id && t.stage === stage.id); const done = list.filter(t => t.status === "done").length; const late = list.filter(t => t.status === "late").length; const live = list.filter(t => t.status === "live").length; const planned = obj.plan?.[stage.id] || 0; const isCompleted = obj.completedStages?.includes(stage.id) || (planned > 0 && done >= planned); const isActive = stage.id === obj.activeStage; const isSkipped = planned === 0 && !isCompleted && !isActive; return { stage, list, done, late, live, planned, total: list.length, isCompleted, isActive, isSkipped }; } // ======================= ШЕЛЛ ======================= function ProjectWorkspace({ objId, tasks, onOpenTask, onToggleTask, onBack, role }) { const obj = window.getObject(objId); const [tab, setTab] = React.useState("overview"); React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onBack(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onBack]); if (!obj) return
Об'єкт не знайдено.
; const client = window.DATA.CLIENTS?.find(c => c.short && obj.client.includes(c.short)) || null; const gip = window.getTeam(obj.team?.[0]); const scopeLabel = window.EXPERTISE_SCOPE_LABELS?.[obj.expertiseScope]?.label || "—"; const dlDays = obj.deadlineISO ? window.PM.daysTo(obj.deadlineISO) : null; return (
{/* ── Хедер об'єкта ── */}
{obj.code} {obj.grade} {scopeLabel}

{obj.name}

{obj.client} {obj.address} {obj.workType === "reconstruction" ? "Реконструкція" : obj.workType === "schema" ? "Схема намірів" : "Нове будівництво"}
{(obj.team || []).map(id => { const p = window.getTeam(id); return
{p?.initials}
; })}
{/* ── Вкладки ── */}
{PW_TABS.map(tb => { let c = null; if (tb.id === "sections") c = window.PM.sectionsFor(obj.id).length; if (tb.id === "stages") c = tasks.filter(t => t.obj === obj.id && t.status !== "done").length; return ( ); })}
{/* ── Контент ── */} {tab === "overview" && } {tab === "sections" && } {tab === "stages" && window.PWStages && } {tab === "gantt" && window.PWGantt && } {tab === "team" && window.PWTeam && } {tab === "finance" && window.PWFinance && } {tab === "supervision" && window.PWSupervision && }
); } // ======================= СМУЖКА ЕТАПІВ ======================= function PWStageStrip({ obj, tasks }) { const stages = window.DATA.STAGES; const correction = window.DATA.CORRECTION_STAGES || []; const hasCorr = correction.some(s => (obj.plan?.[s.id] || 0) > 0 || tasks.some(t => t.obj === obj.id && t.stage === s.id)); const renderStrip = (arr, label) => (
{label}
{arr.map((s, i) => { const info = pwStageInfo(obj, tasks, s); const isEstimate = s.id === "expertise" && obj.expertiseScope === "estimate"; return ( {i > 0 &&
}
0 ? "has-late" : ""}`}>
{s.short} {info.isCompleted && } {info.isActive && !info.isCompleted && }
{info.planned > 0 ? {info.done}/{info.planned} : }
{isEstimate &&
тільки кошторис
} {info.late > 0 &&
{info.late} прострочено
} {info.late === 0 && info.live > 0 &&
{info.live} у роботі
}
); })}
); return (
Життєвий цикл об'єкта
{renderStrip(stages, "Основний цикл")} {hasCorr && renderStrip(correction, "Коригування")}
); } // ======================= ОГЛЯД ======================= function PWOverview({ obj, tasks, gip, client, dlDays, onOpenTask, onToggleTask, setTab }) { const prog = window.PM.sectionProgress(obj.id); const sum = window.PM.sectionSummary(obj.id); const alerts = pwBuildAlerts(obj, tasks); const k = window.PM.contractFor(obj.id); const paid = k ? k.schedule.filter(s => s.paidDate).reduce((a, s) => a + s.amount, 0) : 0; const total = k ? k.total : 0; const paidPct = total ? Math.round(paid / total * 100) : 0; const objTasks = tasks.filter(t => t.obj === obj.id); const activeTasks = objTasks.filter(t => t.status !== "done").length; const activity = window.PM.activityFor(obj.id); // Наступні віхи: майбутні платежі + дедлайни розділів const milestones = []; if (k) k.schedule.filter(s => !s.paidDate).forEach(s => milestones.push({ date: s.dueDate, kind: "Платіж", title: s.stage, val: `${window.formatMoney(s.amount)} ₴` })); window.PM.sectionsFor(obj.id).filter(s => !window.PM.SECTION_STATUS[s.status].done && s.due).forEach(s => milestones.push({ date: s.due, kind: "Розділ", title: `${s.mark} · ${s.name}`, val: "" })); milestones.sort((a, b) => window.PM.ms(a.date) - window.PM.ms(b.date)); const nextMs = milestones.filter(m => window.PM.daysTo(m.date) >= -3).slice(0, 6); return (
{/* KPI */}
Готовність документації
{prog}%
{sum.sheets} аркушів · {sum.total} розділів
Розділи видано
{sum.issued}/{sum.total}
{sum.remarks > 0 ? {sum.remarks} із зауваженнями : "без зауважень"}
До дедлайну
{dlDays != null ? dlDays : "—"} дн
{obj.activeStage === "supervision" ? "авт. нагляд" : `здача ${obj.deadline}`}
Оплачено за договором
{paidPct}%
{window.formatMoney(paid)} з {window.formatMoney(total)} ₴
{/* Потребує уваги */}
Потребує уваги {alerts.length > 0 && {alerts.length}}
{alerts.length === 0 ?
За об'єктом усе в нормі — ризиків і прострочень немає.
:
{alerts.map((al, i) => (
{al.kind} {al.title} {al.meta && {al.meta}}
))}
}
{/* Наступні віхи */}
Наступні віхи
{nextMs.length === 0 ?
Найближчих віх немає.
: nextMs.map((m, i) => { const d = window.PM.daysTo(m.date); const over = d < 0; return (
{window.PM.dm(m.date)} {over ? `${Math.abs(d)} дн тому` : d === 0 ? "сьогодні" : `через ${d} дн`}
{m.kind} {m.title} {m.val && {m.val}}
); })}
{/* Права колонка */}
Паспорт об'єкта
Шифр
{obj.code}
Клас наслідків
{obj.grade} · {obj.gradeKind === "calculated" ? "розрахунок" : "попередній"}
Замовник
{obj.client}
Договір
{obj.contract}
Активний етап
{window.getStage(obj.activeStage)?.name}
Ревізія
{obj.revision}
ГІП
{gip?.name || "—"}
Активних задач
{activeTasks}
Команда об'єкта
{(obj.team || []).map(id => { const p = window.getTeam(id); const mine = objTasks.filter(t => t.assignee === id && t.status !== "done").length; const late = objTasks.filter(t => t.assignee === id && t.status === "late").length; return (
{p?.initials}
{p?.name}
{p?.role}
{mine} задач {late > 0 && {late} простр.}
); })}
Активність
{activity.map((ev, i) => { const p = window.getTeam(ev.by); return (
{p?.initials}
{p?.name?.split(" ")[0]} {ev.text}
{ev.date}
); })}
); } // ======================= РОЗДІЛИ (склад документації) ======================= function PWSections({ obj, tasks, onOpenTask, onToggleTask }) { const sections = window.PM.sectionsFor(obj.id); const SS = window.PM.SECTION_STATUS; const [filter, setFilter] = React.useState("all"); const [openMark, setOpenMark] = React.useState(null); const sum = window.PM.sectionSummary(obj.id); const filters = [ { id: "all", label: "Усі" }, { id: "active", label: "У роботі" }, { id: "review", label: "Нормоконтроль" }, { id: "remarks", label: "Зауваження" }, { id: "issued", label: "Видано" }, ]; const match = (s) => { if (filter === "all") return true; if (filter === "active") return s.status === "in_progress" || s.status === "not_started"; if (filter === "review") return s.status === "review"; if (filter === "remarks") return s.status === "remarks"; if (filter === "issued") return SS[s.status].done || s.status === "expertise"; return true; }; const rows = sections.filter(match); return (
Склад проєктної документації. Реєстр марок (розділів) з виконавцем, нормоконтролем, ревізією та готовністю. Натисніть на рядок — побачите примітку і задачі по розділу.
{filters.map(f => ( ))}
{sum.total} розділів · {sum.issued} видано · {sum.sheets} аркушів
Марка
Розділ
Виконавець · НК
Статус
Зм.
Арк.
Готовність
Дедлайн
{rows.map(s => { const meta = SS[s.status]; const lead = window.getTeam(s.lead); const checker = window.getTeam(s.checker); const open = openMark === s.mark; const dueDays = s.due ? window.PM.daysTo(s.due) : null; const dueOver = dueDays != null && dueDays < 0 && !meta.done; const secTasks = tasks.filter(t => t.obj === obj.id && t.section === s.mark); return (
setOpenMark(open ? null : s.mark)}>
{s.mark}
{s.name}
{s.note &&
{s.note}
}
{lead?.initials} · {checker?.initials}
{meta.label}
{s.rev > 0 ? Зм.{s.rev} : }
{s.sheets || "—"}
{s.pct}%
{s.due ? window.PM.dm(s.due) : "—"}
{open && (
Виконавець{lead?.name} · {lead?.role}
Нормоконтроль{checker?.name}
Ревізія{s.rev > 0 ? `Зміна ${s.rev}` : "Перша редакція"}
Дедлайн{s.due ? window.formatDate(s.due) : "—"}{dueDays != null && !meta.done ? ` · ${dueDays < 0 ? Math.abs(dueDays) + " дн прострочки" : "через " + dueDays + " дн"}` : ""}
Задачі розділу{secTasks.length > 0 ? ` · ${secTasks.length}` : ""}
{secTasks.length === 0 ?
Окремих задач по розділу ще не створено.
:
{secTasks.map(t => )}
}
)}
); })}
); } window.ProjectWorkspace = ProjectWorkspace; window.PWStageStrip = PWStageStrip;