// phase1-pages.jsx — Ліди, Бюджети об'єктів, Ролі, Компанія // ============ ЛІДИ ============ function LeadsPage({ role }) { const leads = window.DATA.LEADS; const [track, setTrack] = React.useState("direct"); const [openId, setOpenId] = React.useState(null); const st = (l) => window.leadStage(l); const trackLeads = leads.filter(l => st(l).track === track); const stages = window.leadStagesFor(track); const flowStages = stages.filter(s => !s.terminal); const inFlow = trackLeads.filter(l => !st(l).terminal && !st(l).won); const wonList = trackLeads.filter(l => st(l).won); const lostList = trackLeads.filter(l => st(l).lost); const pipelineValue = inFlow.reduce((s, l) => s + l.estimatedValue * (l.probability / 100), 0); const flowTotal = inFlow.reduce((s, l) => s + l.estimatedValue, 0); const awaitingCount = trackLeads.filter(l => l.awaitingReview).length; const byStage = {}; stages.forEach(s => { byStage[s.key] = trackLeads.filter(l => l.stage === s.key); }); const maxStage = Math.max(1, ...flowStages.map(s => byStage[s.key].length)); return (

Ліди

{trackLeads.length} {window.plural(trackLeads.length, "картка", "картки", "карток")} {inFlow.length} у роботі прогноз виторгу {window.formatMoney(Math.round(pipelineValue))} ₴
{Object.keys(window.LEAD_TRACKS).map(k => { const meta = window.LEAD_TRACKS[k]; const count = leads.filter(l => st(l).track === k && !st(l).terminal && !st(l).won).length; return ( ); })}
У роботі
{inFlow.length}
на {window.formatMoney(Math.round(flowTotal / 1000))} тис ₴ контрактів
Прогноз виторгу
{window.formatMoney(Math.round(pipelineValue / 1000))} тис ₴
з урахуванням ймовірності
{track === "tender" ? (
Чекають розгляду
{awaitingCount}
черга за ціною
) : (
Договори
{wonList.length}
готові до підписання
)}
{flowStages.map(s => (
{s.label} {byStage[s.key].length}
))}
{track === "tender" && (
Статуси тендерів синхронізуються з zakupivli.pro. Останнє оновлення: 07.06.2026 09:12. демо · автосинк після інтеграції API
)}
{flowStages.map(s => byStage[s.key].length > 0 && (
{s.label}{byStage[s.key].length}
{byStage[s.key].map(l => setOpenId(l.id)} />)}
))} {wonList.length > 0 && (
Договір{wonList.length}
{wonList.map(l => setOpenId(l.id)} />)}
)} {lostList.length > 0 && (
Закрито без угоди{lostList.length}
{lostList.map(l => setOpenId(l.id)} />)}
)}
{openId && l.id === openId)} onClose={() => setOpenId(null)} />}
); } function LeadCard({ lead, onOpen }) { const stage = window.leadStage(lead); const owner = window.getTeam(lead.owner); const cls = stage.won ? "lead-hot" : stage.lost ? "lead-lost" : "lead-warm"; const isTender = lead.track === "tender"; return (
{stage.label} {lead.grade} {lead.awaitingReview && черга за ціною}
{lead.probability}%
ймовірність
{lead.name}
{lead.client}
{window.LEAD_TYPE[lead.type]} {lead.source} {isTender && lead.externalId && {lead.externalId}}
{isTender ? "наша ставка / очік." : "оцінка контракту"}
{window.formatMoney(isTender && lead.ourBid ? lead.ourBid : lead.estimatedValue)}
{isTender ? "статус" : "наступний крок"}
{lead.nextAction}
{owner ? owner.initials : ""}
); } function LeadDrawer({ lead, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const stage = window.leadStage(lead); const owner = window.getTeam(lead.owner); const isTender = lead.track === "tender"; const company = window.getCompany(lead.companyId); const flowStages = window.leadStagesFor(lead.track).filter(s => !s.terminal); return (
); } // ============ БЮДЖЕТИ ОБ'ЄКТІВ ============ function BudgetsPage({ role, tasks }) { const objects = window.DATA.OBJECTS; const contracts = window.DATA.CONTRACTS; const expenses = window.DATA.EXPENSES; const allocations = window.allocateOverheadToObjects(); const team = window.DATA.TEAM; // Розрахунок: на кожен об'єкт - план vs факт const objectBudgets = objects.map(o => { const contract = contracts.find(c => c.obj === o.id); if (!contract) return null; // План — з кошторису (як у estimates) const estimatedLabor = Math.round(contract.total * 0.25); const plannedAdm = Math.round(estimatedLabor * window.OVERHEAD_RATES.admPct / 100); const plannedZvv = Math.round(estimatedLabor * window.OVERHEAD_RATES.zvvPct / 100); const plannedTotal = estimatedLabor + plannedAdm + plannedZvv; // Факт — на скільки реально витрачено const directExpenses = expenses.filter(e => e.obj === o.id).reduce((s, e) => s + e.amount, 0); const allocation = allocations.find(a => a.contract.id === contract.id); const monthsActive = 6; // спрощено — припустимо 6 місяців роботи const factAdm = (allocation?.monthlyAdm || 0) * monthsActive; const factZvv = (allocation?.monthlyZvv || 0) * monthsActive + directExpenses; const factLabor = Math.round(estimatedLabor * 0.85); // спрощено — поки 85% з плану const factTotal = factLabor + factAdm + factZvv; return { obj: o, contract, plannedLabor: estimatedLabor, plannedAdm, plannedZvv, plannedTotal, factLabor, factAdm, factZvv, factTotal, directExpenses, varAbs: factTotal - plannedTotal, varPct: ((factTotal - plannedTotal) / plannedTotal) * 100, }; }).filter(Boolean); const totalPlanned = objectBudgets.reduce((s, b) => s + b.plannedTotal, 0); const totalFact = objectBudgets.reduce((s, b) => s + b.factTotal, 0); const totalVar = totalFact - totalPlanned; return (

Бюджети об'єктів

план vs факт по витратах {objectBudgets.length} активних об'єктів {totalVar < 0 ? економія {window.formatMoney(Math.abs(totalVar))} ₴ : перевитрата {window.formatMoney(totalVar)} ₴ }
Запланований бюджет
{window.formatMoney(Math.round(totalPlanned / 1000))}k ₴
по всіх активних
Фактично витрачено
{window.formatMoney(Math.round(totalFact / 1000))}k ₴
накопиченим підсумком
0 ? "is-warn" : ""}`}>
Відхилення
0 ? "var(--late)" : "var(--c-green-deep)"}}> {totalVar > 0 ? "+" : ""}{Math.round(totalVar / totalPlanned * 100)}%
{totalVar > 0 ? "перевитрата" : "економія"}
Об'єктів у нормі
{objectBudgets.filter(b => b.varPct <= 5).length} з {objectBudgets.length}
відхилення ≤ 5%
{objectBudgets.map(b => )}
); } function BudgetRow({ budget }) { const [open, setOpen] = React.useState(false); const { obj, contract, plannedTotal, factTotal, plannedLabor, plannedAdm, plannedZvv, factLabor, factAdm, factZvv, varAbs, varPct } = budget; const factPctOfPlan = (factTotal / plannedTotal) * 100; const isOver = varPct > 5; const isWarning = varPct > 0 && varPct <= 5; const isGood = varPct <= 0; // Маржа: контракт - фактичні витрати const marginFact = contract.total - factTotal; const marginPlanned = contract.total - plannedTotal; return (
setOpen(!open)}>
{obj.code}
{obj.name}
договір {window.formatMoney(contract.total)} ₴
План {window.formatMoney(plannedTotal)} ₴
Факт {window.formatMoney(factTotal)} ₴
{factPctOfPlan > 100 && (
)}
{Math.round(factPctOfPlan)}%
Маржа
0 ? "var(--c-green-deep)" : "var(--late)"}}> {window.formatMoney(marginFact)}
{Math.round(marginFact / contract.total * 100)}% від договору
{open && (
Стаття Запланувалось Витрачено Залишок % від плану
Усього {window.formatMoney(plannedTotal)} ₴ {window.formatMoney(factTotal)} ₴ 0 ? "var(--late)" : "var(--c-green-deep)"}}> {varAbs > 0 ? "−" : "+"}{window.formatMoney(Math.abs(varAbs))} ₴ {Math.round(factPctOfPlan)}%
)}
); } function BudgetLine({ name, tag, planned, fact, hint }) { const pct = (fact / planned) * 100; const isOver = pct > 100; const remaining = planned - fact; return (
{tag}
{name}
{hint}
{window.formatMoney(planned)} ₴ {window.formatMoney(fact)} ₴ {remaining > 0 ? window.formatMoney(remaining) : "−" + window.formatMoney(Math.abs(remaining))} ₴ 90 ? "var(--ink)" : "var(--ink-2)"}}> {Math.round(pct)}%
{Math.round(pct)}%
); } // ============ РОЛІ ============ function RolesPage({ role }) { const roles = window.DATA.ROLES; const team = window.DATA.TEAM; const cat = window.PERMISSIONS_CATALOG; const [openId, setOpenId] = React.useState(null); return (

Ролі та права доступу

{roles.length} {window.plural(roles.length, "роль", "ролі", "ролей")} хто що бачить і може робити
Як це працює: кожен співробітник має одну з ролей. Роль визначає, які модулі він бачить, які дані може редагувати, видаляти, експортувати. Директор має повний доступ; інші — обмежений за принципом «потрібно знати».
{roles.map(r => { const members = r.members.map(id => window.getTeam(id)).filter(Boolean); return (
setOpenId(r.id)}>
{r.label}
{r.description}
{Object.entries(r.permissions).map(([key, val]) => (
{ key === "read" ? "Перегляд" : key === "write" ? "Редагування" : key === "delete" ? "Видалення" : key === "export" ? "Експорт" : key } {val === "*" ? "Усе" : Array.isArray(val) ? `${val.length} ${window.plural(val.length, "модуль", "модулі", "модулів")}` : "—"}
))}
Призначено
{members.length > 0 ?
{members.slice(0, 4).map((m, i) => ( {m.initials[0]} ))} {members.length > 4 && +{members.length - 4}}
: нікого }
); })}
{openId && r.id === openId)} catalog={cat} onClose={() => setOpenId(null)} />}
); } function RolePermissionsModal({ role, catalog, onClose }) { React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const hasAccess = (group) => role.permissions.read === "*" || (Array.isArray(role.permissions.read) && role.permissions.read.includes(group)); return ( <>
); } function PermDot({ on }) { return on ? : ·; } // ============ КОМПАНІЯ ============ function CompanyPage({ role }) { return (

Налаштування компанії

реквізити, печатка, КЕП, підписи
Назва
ТОВ «Укрбудпроєкт»
ЄДРПОУ
42158963
Працівників
{window.DATA.TEAM.length}
Активних об'єктів
{window.DATA.OBJECTS.length}

Реквізити

Редагувати →
Повна назва
Товариство з обмеженою відповідальністю «Укрбудпроєкт»
ЄДРПОУ
42158963
Юридична адреса
04070, м. Київ, вул. Хорива 12, оф. 305
Поштова адреса
Та сама
Директор
Кравченко Олена Михайлівна
Email
info@ukrbudproiekt.ua
Сайт
ukrbudproiekt.ua
Телефон
+380 67 412 38 90

Банківські реквізити

Усі рахунки →
{window.DATA.CASH_ACCOUNTS.slice(0, 2).map(a => (
{a.label}
{a.bank}
{a.primary && основний}
{a.iban}
))}

КЕП і підписи

Дія.Підпис →
КЕП директора
Кравченко О.М. · діє до 12.03.2027
активний
Печатка ТОВ
Електронна · для договорів
активна

Документи компанії

Витяг з ЄДРотримано 12.03.2024
Статутчинна редакція
Ліцензія на проєктуваннядіє до 15.08.2028
Свідоцтво платника ЄП3 група, 5%
); } window.LeadsPage = LeadsPage; window.BudgetsPage = BudgetsPage; window.RolesPage = RolesPage; window.CompanyPage = CompanyPage;