// phase2-pages.jsx — KPI/1:1 · Підрядники · Шаблони задач // ============ KPI ТА 1:1 ============ function KpiPage({ role }) { const team = window.DATA.TEAM; const oneOnOnes = window.ONE_ON_ONES; const [openPerson, setOpenPerson] = React.useState(null); const [tab, setTab] = React.useState("kpi"); const upcoming = oneOnOnes.filter(o => o.status === "scheduled"); const past = oneOnOnes.filter(o => o.status === "done"); return (

KPI та 1:1 розмови

цілі команди, оцінка ефективності, регулярні розмови квартал Q2 2026
{tab === "kpi" && (
{team.map(p => { const goals = window.KPI_GOALS[p.id] || []; const onTrack = goals.filter(g => g.trend === "on_track" || g.trend === "exceeding").length; return (
setOpenPerson(openPerson === p.id ? null : p.id)}>
{p.initials}
{p.name}
{p.role}
{goals.length > 0 ? ( <>
{onTrack} /{goals.length}
у графіку
) : (
немає KPI
)}
{openPerson === p.id && goals.length > 0 && (
{goals.map(g => )}
)}
); })}
)} {tab === "1on1" && ( <> {upcoming.length > 0 && (

Заплановано

{upcoming.length}
{upcoming.map(o => )}
)}

Минулі розмови

{past.length}
{past.map(o => )}
)}
); } function KpiGoalRow({ goal }) { const trend = window.KPI_TREND_LABELS[goal.trend]; const pct = goal.inverse ? Math.max(0, Math.min(100, (1 - (goal.current - goal.target) / goal.target) * 100)) : Math.max(0, Math.min(100, goal.current / goal.target * 100)); const formatVal = (v) => { if (goal.unit === "₴") return window.formatMoney(v) + " ₴"; return v.toLocaleString("uk-UA") + goal.unit; }; return (
{goal.title}
{formatVal(goal.current)} з {formatVal(goal.target)}
{trend.label}
); } function OneOnOneRow({ oneon }) { const person = window.getTeam(oneon.who); const isUpcoming = oneon.status === "scheduled"; return (
{person?.initials}
{person?.name}
{window.formatDate(oneon.date)} · {oneon.duration} хв · {oneon.topics.length} {window.plural(oneon.topics.length, "тема", "теми", "тем")}
{oneon.notes && (
{oneon.notes}
)}
{isUpcoming ? заплановано : проведено } {oneon.nextSteps.length > 0 && (
{oneon.nextSteps.length} наст. кроків
)}
); } // ============ ПІДРЯДНИКИ ============ function SuppliersPage({ role }) { const suppliers = window.DATA.SUPPLIERS; const cats = window.SUPPLIER_CATEGORIES; const [filter, setFilter] = React.useState("all"); const [openId, setOpenId] = React.useState(null); let shown = suppliers; if (filter !== "all") shown = shown.filter(s => s.category === filter); const totalPaid = suppliers.reduce((s, x) => s + x.totalPaid, 0); const activeCount = suppliers.reduce((s, x) => s + x.activeProjects, 0); return (

Підрядники

{suppliers.length} {window.plural(suppliers.length, "контрагент", "контрагенти", "контрагентів")} {activeCount} активних проєктів виплачено за весь час {window.formatMoney(totalPaid)} ₴
Активних
{suppliers.filter(s => s.activeProjects > 0).length}
зараз працюємо
Високий рейтинг
{suppliers.filter(s => s.rating >= 4).length}
4-5 зірок
Категорій
{Object.keys(cats).length}
типів послуг
Сер. чек
{window.formatMoney(Math.round(suppliers.reduce((s,x) => s+x.avgCost, 0) / suppliers.length))}
за одну послугу
{Object.entries(cats).map(([id, c]) => ( ))}
{shown.map(s => { const cat = cats[s.category]; return ( setOpenId(s.id)}> ); })}
Підрядник Категорія Рейтинг Проєктів Сер. чек Загалом виплачено
{s.name}
з {window.formatDate(s.since)}
{cat.label} {[1,2,3,4,5].map(n => )} {s.completedProjects} {s.activeProjects > 0 && +{s.activeProjects}} {window.formatMoney(s.avgCost)} ₴ {window.formatMoney(s.totalPaid)} ₴
{openId && s.id === openId)} onClose={() => setOpenId(null)} />}
); } function SupplierDrawer({ supplier, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const cat = window.SUPPLIER_CATEGORIES[supplier.category]; return ( <>
); } // ============ ШАБЛОНИ ЗАДАЧ ============ function TaskTemplatesPage({ role }) { const templates = window.DATA.TASK_TEMPLATES; const cats = window.TEMPLATE_CATEGORIES; const [openId, setOpenId] = React.useState(null); return (

Шаблони задач

{templates.length} {window.plural(templates.length, "шаблон", "шаблони", "шаблонів")} для нових об'єктів і регулярних процесів
Як це працює: створюючи новий об'єкт, обираєш шаблон — і автоматично створюються всі типові задачі по етапах з оцінкою часу. Економить 1-2 дні роботи на старті проєкту.
{templates.map(t => (
setOpenId(t.id)}>
{cats[t.category]}
{t.name}
{t.description}
{t.stages.reduce((s, st) => s + st.tasks.length, 0)}задач
{t.stages.length}{window.plural(t.stages.length, "етап", "етапи", "етапів")}
{t.estimatedDays}{window.plural(t.estimatedDays, "день", "дні", "днів")}
))}
{openId && t.id === openId)} onClose={() => setOpenId(null)} />}
); } function TemplateModal({ template, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const totalTasks = template.stages.reduce((s, st) => s + st.tasks.length, 0); return ( <>
{template.name}
{template.description}
Задач
{totalTasks}
Етапів
{template.stages.length}
Тривалість
{template.estimatedDays}дн.
{template.objectGrade &&
Клас
{template.objectGrade}
}
{template.stages.map((st, i) => (

{window.getStage(st.stage)?.name || st.stage}

{st.tasks.length} {window.plural(st.tasks.length, "задача", "задачі", "задач")}
{st.tasks.map((t, j) => ( ))}
Задача Розділ Оцінка
{j + 1}
{t.title}
{t.section} {t.est}
))}
); } window.KpiPage = KpiPage; window.SuppliersPage = SuppliersPage; window.TaskTemplatesPage = TaskTemplatesPage;