// reports-pages.jsx — Звіти: замовникам · внутрішні · шаблони // ============ ЗВІТИ ЗАМОВНИКАМ ============ function ClientReportsPage({ role }) { const objects = window.DATA.OBJECTS; const contracts = window.DATA.CONTRACTS; const tasks = window.DATA.TASKS; const [openId, setOpenId] = React.useState(null); return (

Звіти замовникам

прогрес проєктів формуються автоматично з задач і платежів
Як це працює: для кожного активного об'єкта формується PDF-звіт з прогресом проєктних робіт, фотофіксацією, графіком платежів і списком зданих документів. Натисніть «Сформувати» для попереднього перегляду, потім «Експорт PDF» для надсилання.
{objects.map(o => { const contract = contracts.find(c => c.obj === o.id); const client = contract ? window.getClient(contract.client) : null; const objTasks = tasks.filter(t => t.obj === o.id); const done = objTasks.filter(t => t.status === "done").length; const total = Object.values(o.plan || {}).reduce((s, n) => s + n, 0); const pct = total > 0 ? Math.round(done / total * 100) : 0; return (
setOpenId(o.id)}>
{o.code}
{o.grade}
{o.name}
{client?.short || "—"}
{done} з {total} задач {pct}%
); })}
{openId && o.id === openId)} onClose={() => setOpenId(null)} />}
); } function ReportPreview({ obj, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const contract = window.DATA.CONTRACTS.find(c => c.obj === obj.id); const client = contract ? window.getClient(contract.client) : null; const tasks = window.DATA.TASKS.filter(t => t.obj === obj.id); const stages = window.DATA.STAGES; const today = "26 травня 2026"; // Прогрес по етапах const stageProgress = stages.map(s => { const list = tasks.filter(t => t.stage === s.id); const done = list.filter(t => t.status === "done").length; const planned = obj.plan?.[s.id] || 0; return { stage: s, done, planned, pct: planned > 0 ? Math.round(done / planned * 100) : 0 }; }).filter(x => x.planned > 0); // Платежі const paid = contract?.schedule.filter(s => s.paidDate) || []; const upcoming = contract?.schedule.filter(s => !s.paidDate) || []; return ( <>
Попередній перегляд звіту
{/* Шапка звіту */}
Звіт про виконання проєктних робіт

{obj.name}

{obj.code} · {obj.address} · клас {obj.grade}
Замовнику:
{client?.short}
станом на {today}
{/* Резюме */}
1. Загальні відомості
Договір№ {contract?.number} від {window.formatDate(contract?.date)}
Сума договору{window.formatMoney(contract?.total)} ₴
Поточна стадія{window.getStage(obj.activeStage)?.name}
Очікуваний дедлайн{obj.deadline}
{/* Прогрес по етапах */}
2. Прогрес виконання
{stageProgress.map(sp => (
{sp.stage.name}
{sp.done}/{sp.planned}
))}
{/* Виконані задачі періоду */}
3. Виконано за звітний період
{tasks.filter(t => t.status === "done").length > 0 ?
    {tasks.filter(t => t.status === "done").slice(0, 8).map(t => (
  • {t.title} · розділ {t.section}
  • ))}
:
За звітний період задач ще не закрито.
}
{/* Фотофіксація (плейсхолдер) */}
4. Фотофіксація з виїздів
Підвантажується з Google Drive ↗
{/* Платежі */}
5. Графік платежів
{contract?.schedule.map((s, i) => ( ))}
ЕтапСумаТермінСтатус
{s.stage} {window.formatMoney(s.amount)} ₴ {window.formatDate(s.dueDate)} {s.paidDate ? сплачено {window.formatDate(s.paidDate)} : очікується }
{/* Підпис */}
Звіт підготувала
Кравченко О.М.
ГІП, ТОВ «Укрбудпроєкт»
Підпис:
{today}
); } // ============ ВНУТРІШНІ ЗВІТИ ============ function InternalReportsPage({ role }) { // Розрахунок загальновиробничих (ЗВВ) та адміністративних (АДМ) витрат const overhead = (type) => { const cats = window.EXPENSE_CATEGORIES.filter(c => c.type === type).map(c => c.id); const items = window.DATA.EXPENSES .filter(e => cats.includes(e.cat)) .map(e => ({ cat: e.cat, monthly: e.monthlyAmount || 0 })); const byCat = {}; items.forEach(i => { byCat[i.cat] = (byCat[i.cat] || 0) + i.monthly; }); const monthly = window.totalMonthlyByType(type); const top = Object.entries(byCat).sort((a, b) => b[1] - a[1])[0]; const topCat = top ? window.getCategory(top[0]) : null; return { monthly, annual: monthly * 12, topLabel: topCat ? topCat.label : "—" }; }; const zvv = overhead("zvv"); const adm = overhead("adm"); const [openReport, setOpenReport] = React.useState(null); if (openReport === "finance") { return setOpenReport(null)} role={role} />; } const pl = window.computeAnnualPL("2026"); const reports = [ { id: "zvv", title: "Загальновиробничі витрати", subtitle: `ЗВВ · надбавка ${window.OVERHEAD_RATES.zvvPct}%`, icon: "🏗", kpis: [ { label: "ЗВВ за місяць", val: `${window.formatMoney(zvv.monthly)} ₴` }, { label: "За рік (прогноз)", val: `${window.formatMoney(zvv.annual)} ₴` }, { label: "Найбільша стаття", val: zvv.topLabel }, ] }, { id: "adm", title: "Адміністративні витрати", subtitle: `АДМ · надбавка ${window.OVERHEAD_RATES.admPct}%`, icon: "🏢", kpis: [ { label: "АДМ за місяць", val: `${window.formatMoney(adm.monthly)} ₴` }, { label: "За рік (прогноз)", val: `${window.formatMoney(adm.annual)} ₴` }, { label: "Найбільша стаття", val: adm.topLabel }, ] }, { id: "monthly", title: "Місячний дашборд", subtitle: "Травень 2026", icon: "📊", kpis: [ { label: "Виторг (виставлено)", val: "5 440 000 ₴", color: "var(--c-green-deep)" }, { label: "Фонд ЗП", val: "247 600 ₴" }, { label: "Витрати", val: "70 333 ₴/міс" }, { label: "Чистий прибуток", val: "+5 122 067 ₴", color: "var(--c-green-deep)" }, ] }, { id: "team", title: "Звіт по команді", subtitle: "Завантаження, ефективність, ЗП", icon: "👥", kpis: [ { label: "Середнє завантаження", val: "67%" }, { label: "Прострочених задач", val: "1", color: "var(--late)" }, { label: "Сер. ефективність", val: "87%" }, ] }, { id: "objects", title: "Звіт по об'єктах", subtitle: "Прогрес, маржа, дедлайни", icon: "🏗", kpis: [ { label: "Активних об'єктів", val: "4" }, { label: "Загальна маржа", val: "59.6%" }, { label: "Найближчий дедлайн", val: "12.06 · УКП-47" }, ] }, { id: "finance", title: "Фінансовий результат (P&L)", subtitle: "Рік · ТОВ · загальна система", icon: "💰", open: "finance", kpis: [ { label: "Дохід (без ПДВ)", val: `${window.formatMoney(pl.revenueNet)} ₴` }, { label: "Чистий прибуток", val: `${window.formatMoney(pl.netProfit)} ₴`, color: "var(--c-green-deep)" }, { label: "Рентабельність", val: `${pl.margin.toFixed(1)}%` }, ] }, { id: "tax", title: "Податки до сплати", subtitle: "Рік · загальна система, платник ПДВ", icon: "🏛", kpis: [ { label: "ПДВ до сплати", val: `${window.formatMoney(pl.vatPayable)} ₴` }, { label: "Податок на прибуток 18%", val: `${window.formatMoney(pl.profitTax)} ₴` }, { label: "ЄСВ (нараховано)", val: `${window.formatMoney(pl.esv)} ₴` }, ] }, { id: "kpi", title: "KPI звіт по команді", subtitle: "Бонусний пул", icon: "🎯", kpis: [ { label: "Бонус до виплати", val: "98 000 ₴" }, { label: "Учасників", val: "3 з 5" }, { label: "Дата виплати", val: "30.05.2026" }, ] }, ]; return (

Внутрішні звіти

для директора, бухгалтерії, керівництва оновлюються щодня
{reports.map(r => (
{r.title}
{r.subtitle}
{r.kpis.map((k, i) => (
{k.label}
{k.val}
))}
))}
); } // ============ ШАБЛОНИ ============ function ReportTemplatesPage({ role }) { const templates = [ { id: "tpl1", name: "Звіт замовнику · повний", category: "client", description: "Прогрес + фото + платежі + підпис ГІП", fields: 12 }, { id: "tpl2", name: "Звіт замовнику · короткий", category: "client", description: "Тільки прогрес і дедлайни", fields: 6 }, { id: "tpl3", name: "Акт виконаних робіт", category: "client", description: "Юридичний документ для підпису", fields: 10 }, { id: "tpl4", name: "Місячний звіт директора", category: "internal", description: "KPI, фінанси, команда", fields: 18 }, { id: "tpl5", name: "Звіт ДПС · ФОП 3 група", category: "tax", description: "Квартальна декларація", fields: 24 }, { id: "tpl6", name: "Авторський нагляд · виїзд", category: "client", description: "Журнал АН з фото і зауваженнями", fields: 8 }, { id: "tpl7", name: "Звіт у банк · підтвердження доходу", category: "external", description: "Довідка для кредиту/іпотеки", fields: 6 }, { id: "tpl8", name: "Кадровий звіт за рік", category: "internal", description: "Звіт по штату для річного зібрання", fields: 14 }, ]; const catLabels = { client: "Замовникам", internal: "Внутрішній", tax: "Податковий", external: "Зовнішній" }; const catTones = { client: "live", internal: "neutral", tax: "warn", external: "neutral" }; return (

Шаблони звітів

{templates.length} {window.plural(templates.length, "шаблон", "шаблони", "шаблонів")} можна копіювати, редагувати, створювати нові
{templates.map(t => ( ))}
Назва Тип Опис Полів
{t.name}
{catLabels[t.category]} {t.description} {t.fields}
); } window.ClientReportsPage = ClientReportsPage; window.InternalReportsPage = InternalReportsPage; window.ReportTemplatesPage = ReportTemplatesPage;