// 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.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 (
{myActive}
{myLate > 0 && · {myLate} прострочено}
= 90 ? "is-high" : ""}`}
style={{width: p.load + "%"}}
>
= 90 ? "is-high" : ""}`}>{p.load}%
{p.phone}
);
})}
);
}
window.WeekPage = WeekPage;
window.ObjectsPage = ObjectsPage;
window.AdminPage = AdminPage;
window.TeamPage = TeamPage;