);
}
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)} ₴
}
);
}
// ============ РОЛІ ============
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, "роль", "ролі", "ролей")}хто що бачить і може робити
Як це працює: кожен співробітник має одну з ролей. Роль визначає, які модулі він бачить,
які дані може редагувати, видаляти, експортувати. Директор має повний доступ; інші — обмежений
за принципом «потрібно знати».