// dashboard-page.jsx — Головний дашборд директора function DashboardPage({ role, tasks, onNewTask, user }) { const today = window.SYS_DATE || "2026-05-26"; // жива дата з бекенду // Рядок дати/часу — з живої дати + поточного часу браузера (Київ для команди в Україні). const _WD = ["неділя", "понеділок", "вівторок", "середа", "четвер", "п'ятниця", "субота"]; const _MO = ["січня", "лютого", "березня", "квітня", "травня", "червня", "липня", "серпня", "вересня", "жовтня", "листопада", "грудня"]; const _dt = new Date(today + "T00:00:00"); const _now = new Date(); const nowHHMM = String(_now.getHours()).padStart(2, "0") + ":" + String(_now.getMinutes()).padStart(2, "0"); const dateLine = `${_WD[_dt.getDay()]} · ${_dt.getDate()} ${_MO[_dt.getMonth()]} ${_dt.getFullYear()} · ${nowHHMM}`; const team = window.DATA.TEAM; const objects = window.DATA.OBJECTS; const contracts = window.DATA.CONTRACTS; const invoices = window.DATA.INVOICES; const expenses = window.DATA.EXPENSES; const leads = window.DATA.LEADS; const accounts = window.DATA.CASH_ACCOUNTS; const [printDraft, setPrintDraft] = React.useState(null); const [pickInvoice, setPickInvoice] = React.useState(false); const [pickAct, setPickAct] = React.useState(false); const [printAct, setPrintAct] = React.useState(null); // Аналітика const totalBalance = accounts.reduce((s, a) => s + (a.currency === "UAH" ? a.balance : a.balance * 41.5), 0); const overdueInv = invoices.filter(i => i.status === "overdue"); const overdueSum = overdueInv.reduce((s, i) => s + i.amount, 0); const paidSum = invoices.filter(i => i.status === "paid").reduce((s, i) => s + i.amount, 0); const monthlyExpenses = expenses.reduce((s, e) => s + (e.monthlyAmount || 0), 0); const totalSalary = team.reduce((s, p) => { const pay = window.computePayroll(p.id, p.partRate ? 88 : 176, 176); return s + (pay?.net || 0); }, 0); const todayTasks = tasks.filter(t => t.day === window.TODAY.day && t.status !== "done"); const lateTasks = tasks.filter(t => t.status === "late"); const liveTasks = tasks.filter(t => t.status === "live"); const activeLeads = leads.filter(l => { const s = window.leadStage(l); return !s.terminal && !s.won; }); const directActive = activeLeads.filter(l => l.track === "direct"); const tenderActive = activeLeads.filter(l => l.track === "tender"); const pipelineValue = activeLeads.reduce((s, l) => s + l.estimatedValue * (l.probability / 100), 0); const monthlyProfit = paidSum - monthlyExpenses - totalSalary; return (

{window.dashGreeting(user)}

{dateLine} усе під контролем {lateTasks.length > 0 && <> {lateTasks.length} {window.plural(lateTasks.length, "задача потребує", "задачі потребують", "задач потребують")} уваги }
{/* Хедер-блок з ключовими цифрами */}
Баланс компанії
{window.formatMoney(Math.round(totalBalance))}
на {accounts.length} рахунках · оновлено о {nowHHMM}
Виторг за травень +{window.formatMoney(paidSum)} ₴
Витрати + ЗП −{window.formatMoney(monthlyExpenses + Math.round(totalSalary))} ₴
Прибуток +{window.formatMoney(Math.round(monthlyProfit))} ₴
{/* 4 ключові області */}
{role === "director" && window.BirthdayCard && } {/* Сьогодні + команда */}

Сьогодні

Усі задачі →
{todayTasks.length}
задач на сьогодні
0 ? "var(--c-green-deep)" : "var(--ink)"}}>{liveTasks.length}
у роботі зараз
0 ? "var(--late)" : "var(--ink)"}}>{lateTasks.length}
прострочено
{lateTasks.length > 0 && (
Прострочено: {lateTasks[0].title}
)}
{team.map(p => (
{p.initials}
= 90 ? "is-high" : ""}`} style={{width: p.load + "%"}}>
))}
{/* Дебіторка */}

Дебіторка

Деталі →
0 ? "var(--late)" : "var(--ink)"}}> {window.formatMoney(overdueSum)}
{overdueInv.length} прострочених рахунків
{overdueInv.slice(0, 2).map(inv => { const cli = window.getClient(inv.client); return (
{cli?.short}
{inv.title}
+{inv.lateDays} дн.
); })}
{/* Cash flow прогноз */}

Прогноз 4 тижні

Cash flow →
{[ { w: "Цей", val: 60, in: 425000, out: 220000 }, { w: "Наст", val: 75, in: 600000, out: 180000 }, { w: "+2", val: 50, in: 0, out: 250000 }, { w: "+3", val: 90, in: 2940000, out: 220000 }, ].map((m, i) => (
{m.w}
))}
+2.96 млн надходжень очікується
{/* Об'єкти */}

Активні об'єкти

Усі об'єкти →
{objects.slice(0, 3).map(o => { 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; const contract = contracts.find(c => c.obj === o.id); return (
{o.code} {o.name}
{done}/{total}
до {o.deadline}
); })}
{/* Воронка лідів */}

Воронка

Усі ліди →
{window.formatMoney(Math.round(pipelineValue / 1000000 * 10) / 10)}М ₴
прогнозований виторг з {activeLeads.length} лідів у роботі
прямі {directActive.length}
тендери {tenderActive.length}
{/* Команда */}

Команда

Профілі →
{team.length}
людей у штаті
{Math.round(team.reduce((s,p) => s + p.load, 0) / team.length)}%сер. завантаження
{team.filter(p => p.load >= 90).length}перевантажено
{window.DATA.VACATIONS.filter(v => v.status === "pending").length}заявок на відпустку
{/* Швидкі дії */}

Швидкі дії

{pickInvoice && setPickInvoice(false)} onPick={(draft) => { setPickInvoice(false); setPrintDraft(draft); }} />} {printDraft && setPrintDraft(null)} />} {pickAct && window.ActContractPicker && setPickAct(false)} onPick={(draft) => { setPickAct(false); setPrintAct(draft); }} />} {printAct && window.ActPrintDoc && setPrintAct(null)} />}
); } window.DashboardPage = DashboardPage;