// 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 (
);
})}
{/* 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) => (
))}
+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 (
);
})}
{/* Воронка лідів */}
{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;