// pages.jsx — Тиждень, Об'єкти, Команда // ============ ТИЖДЕНЬ ============ function WeekPage({ tasks, onOpenTask, onToggle }) { const [filter, setFilter] = React.useState("all"); // all | mine const weekDays = window.WEEK_DAYS; const team = window.DATA.TEAM; const me = team[0]; // ОК const filtered = filter === "mine" ? tasks.filter(t => t.assignee === me.id) : tasks; // Групуємо по днях const byDay = {}; weekDays.forEach(d => byDay[d.day] = []); filtered.forEach(t => { if (byDay[t.day]) byDay[t.day].push(t); }); const myCount = tasks.filter(t => t.assignee === me.id).length; const totalCount = tasks.length; const doneCount = filtered.filter(t => t.status === "done").length; const lateCount = filtered.filter(t => t.status === "late").length; return (

Тиждень

тиждень 22 · 25–29 травня 2026 {filtered.length} {window.plural(filtered.length, "задача", "задачі", "задач")} {doneCount > 0 && <>{doneCount} виконано} {lateCount > 0 && <>{lateCount} прострочено}
{/* Фільтр */}
{weekDays.map(d => { const dayTasks = byDay[d.day] || []; return (
{d.day} травня {d.name} {d.isToday && Сьогодні}
{dayTasks.length > 0 ? `${dayTasks.length} ${window.plural(dayTasks.length, "задача", "задачі", "задач")}` : "—"}
{dayTasks.length > 0 && (
{dayTasks.map(t => )}
)}
); })}
); } // ============ ОБ'ЄКТИ ============ function ObjectsPage({ tasks, onOpenTask, onToggle, setPage, onOpenProject }) { const objects = window.DATA.OBJECTS; const stages = window.DATA.STAGES; const [openObj, setOpenObj] = React.useState(null); const [pickerOpen, setPickerOpen] = React.useState(false); const [creating, setCreating] = React.useState(null); // загальна статистика const activeCount = objects.filter(o => o.activeStage !== "supervision").length; const supervisionCount = objects.filter(o => o.activeStage === "supervision").length; return (

Об'єкти

{objects.length} в портфелі {activeCount} {window.plural(activeCount, "у проєктуванні", "у проєктуванні", "у проєктуванні")} {supervisionCount} на авт. нагляді
{objects.map(o => ( setOpenObj(openObj === o.id ? null : o.id)} onOpenTask={onOpenTask} onToggleTask={onToggle} onOpenProject={onOpenProject} /> ))}
{pickerOpen && ( setPickerOpen(false)} onPick={(t) => { setPickerOpen(false); setCreating(t); }} /> )} {creating && ( setCreating(null)} onCreated={() => { setCreating(null); }} /> )}
); } function ObjectCard({ obj, tasks, stages, isOpen, onToggle, onOpenTask, onToggleTask, onOpenProject }) { const objTasks = tasks.filter(t => t.obj === obj.id); const activeStageData = window.getStage(obj.activeStage); const correctionStages = window.DATA.CORRECTION_STAGES; const [ccOpen, setCcOpen] = React.useState(false); const handleCcConfirm = ({ grade, scope, basis, added, removed }) => { const newHistory = [...(obj.gradeHistory || []), { value: grade, kind: "calculated", date: window.SYS_DATE, by: "ok", basis }]; const newTasks = added.map(tt => ({ id: "t-" + Math.random().toString(36).slice(2, 8), title: tt.title, obj: obj.id, stage: "expertise", assignee: tt.assigneeRole === "gip" ? (obj.team?.[0] || null) : null, section: tt.section, est: tt.estDays + " дн", status: "todo", templateTaskId: tt.id, deadlineOffset: tt.deadlineOffset, subs: [], })); const plan = { ...(obj.plan || {}) }; plan.expertise = Math.max(0, (plan.expertise || 0) + added.length - removed.length); if (window.__erpStore && window.__erpStore.updateObject) { window.__erpStore.updateObject(obj.id, { grade, expertiseScope: scope, gradeKind: "calculated", gradeHistory: newHistory, plan, }, newTasks); } setCcOpen(false); }; // Загальний прогрес const totalDone = objTasks.filter(t => t.status === "done").length; const totalPlanned = Object.values(obj.plan || {}).reduce((s, n) => s + n, 0); const progressPct = totalPlanned > 0 ? Math.round((totalDone / totalPlanned) * 100) : 0; // Чи є коригування (план або задачі) const hasCorrections = correctionStages.some(s => { const planned = obj.plan?.[s.id] || 0; const hasTasks = objTasks.some(t => t.stage === s.id); return planned > 0 || hasTasks; }); const buildInfo = (stage) => { const list = objTasks.filter(t => t.stage === stage.id); const done = list.filter(t => t.status === "done").length; const live = list.filter(t => t.status === "live").length; const late = list.filter(t => t.status === "late").length; const total = list.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, live, late, total, planned, isCompleted, isActive, isSkipped }; }; const mainStageInfo = stages.map(buildInfo); const correctionStageInfo = correctionStages.map(buildInfo); const stageTasks = isOpen ? objTasks.filter(t => t.stage === obj.activeStage) : []; return (
{/* Header */}
{obj.code}
{obj.grade}
{obj.name}
{obj.client} {obj.address} {obj.contract}
Зараз
{activeStageData?.name}
до {obj.deadline}
{onOpenProject && ( )}
{/* Загальний прогрес */}
{totalDone} з {totalPlanned} задач {progressPct}%
{/* Основна стрічка етапів */}
Основний цикл
{mainStageInfo.map((info, idx) => ( {idx > 0 &&
}
))}
{/* Коригування — показуємо тільки якщо є */} {hasCorrections && (
Коригування
{correctionStageInfo.map((info, idx) => ( {idx > 0 &&
}
))}
)} {/* Розгорнутий блок — клас наслідків + задачі активного етапу */} {isOpen && (
setCcOpen(true)} />
Задачі етапу «{activeStageData?.name}» {stageTasks.length} {window.plural(stageTasks.length, "задача", "задачі", "задач")}
{stageTasks.length === 0 ?
На цьому етапі задач ще не створено.
:
{stageTasks.map(t => )}
}
)} {ccOpen && ( setCcOpen(false)} onConfirm={handleCcConfirm} /> )}
); } function StageChip({ info, expertiseScope }) { const { stage, done, live, late, planned, isCompleted, isActive, isSkipped } = info; const isEstimateOnly = stage.id === "expertise" && expertiseScope === "estimate"; const isStrengthOnly = stage.id === "expertise" && expertiseScope === "strength"; return (
0 ? "has-late" : ""}`}>
{stage.short} {isCompleted && } {isActive && !isCompleted && }
{planned > 0 ? {done}/{planned} : }
{isEstimateOnly &&
тільки кошторис
} {isStrengthOnly &&
МОС + кошторис
} {late > 0 &&
{late} прострочено
} {live > 0 &&
{live} у роботі
}
); } function StageIcon({ kind }) { if (kind === "done") return ( ); return null; } // ============ АДМІНІСТРАЦІЯ ============ function AdminPage({ tasks, onOpenTask, onToggle }) { const adminTasks = tasks.filter(t => !t.obj); const active = adminTasks.filter(t => t.status !== "done"); const done = adminTasks.filter(t => t.status === "done"); // Сортування по даті const sorted = [...active].sort((a, b) => a.day.localeCompare(b.day)); return (

Адміністративні

задачі поза проєктами {active.length} {window.plural(active.length, "активна", "активні", "активних")} {done.length > 0 && <>{done.length} виконано}

Найближчі

офіс · фінанси · HR
{sorted.length === 0 ?
Адміністративних задач немає.
:
{sorted.map(t => ( ))}
}
{done.length > 0 && (

Виконано

{done.length}
{done.map(t => ( ))}
)}
); } // ============ КОМАНДА ============ function TeamPage({ tasks }) { const team = window.DATA.TEAM; const [sort, setSort] = React.useState("role"); // role | name | load const sorted = React.useMemo(() => { const copy = [...team]; if (sort === "name") copy.sort((a, b) => a.name.localeCompare(b.name, "uk")); else if (sort === "load") copy.sort((a, b) => b.load - a.load); // role — default order return copy; }, [team, sort]); const totalActive = tasks.filter(t => t.status !== "done").length; return (

Команда

{team.length} {window.plural(team.length, "людина", "людини", "людей")} у штаті {totalActive} активних задач
Сортувати:
Працівник
Активні задачі
Завантаження
Телефон
Email
{sorted.map(p => { const myActive = tasks.filter(t => t.assignee === p.id && t.status !== "done").length; const myLate = tasks.filter(t => t.assignee === p.id && t.status === "late").length; return (
{p.initials}
{p.name}
{p.role}
{myActive} {myLate > 0 && · {myLate} прострочено}
= 90 ? "is-high" : ""}`} style={{width: p.load + "%"}} >
= 90 ? "is-high" : ""}`}>{p.load}%
{p.phone}
e.stopPropagation()}>{p.email}
); })}
); } window.WeekPage = WeekPage; window.ObjectsPage = ObjectsPage; window.AdminPage = AdminPage; window.TeamPage = TeamPage;