/* Overview: дашборд проекта — KPI, прогресс, ключевые метрики, критический путь. Формат: React.createElement (no-build). Добавь в Shell.NAV чтобы появился в меню. */ window.RD = window.RD || { core: {}, modules: {} }; window.RD.modules.Overview = function Overview() { var store = RD.core.useStore(); var h = React.createElement; var fmt = RD.core; // ФИК��: core — это объект с функциями money(), dayLabel(), etc. // FIX: All data entities are in store.raw (root), not in store.raw.data var tasks = store.raw.tasks || []; var smeta = store.raw.smeta || []; var payments = store.raw.payments || []; var supplies = store.raw.supplies || []; // === Метрики === // Бюджет var totalBudget = smeta.reduce(function (s, item) { return s + ((item.qty || 0) * (item.price || 0)); }, 0); var totalSpent = payments.filter(function (p) { return p.status === 'paid'; }) .reduce(function (s, p) { return s + (p.amount || 0); }, 0); var budgetRemaining = totalBudget - totalSpent; var budgetPercent = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0; // График работ var now = new Date(); var completedTasks = tasks.filter(function (t) { return t.done; }).length; var totalTasks = tasks.length; var schedulePercent = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; var overdueTasks = tasks.filter(function (t) { if (t.done) return false; if (!t.end) return false; var end = new Date(t.end); return end < now; }).length; // Поставки var pendingSupplies = supplies.filter(function (s) { return s.status === 'pending' || s.status === 'ordered'; }).length; var overdueSupplies = supplies.filter(function (s) { if (s.status === 'delivered') return false; if (!s.expectedDate) return false; var exp = new Date(s.expectedDate); return exp < now; }).length; // Оплаты var pendingPayments = payments.filter(function (p) { return p.status === 'pending' || p.status === 'scheduled'; }).length; var overduePayments = payments.filter(function (p) { if (p.status === 'paid') return false; if (!p.dueDate) return false; var due = new Date(p.dueDate); return due < now; }).length; // Критический путь (задачи с просрочкой или высоким приоритетом) var criticalTasks = tasks.filter(function (t) { if (t.done) return false; var isOverdue = t.end && new Date(t.end) < now; var isHighPriority = t.priority === 'high' || t.critical; return isOverdue || isHighPriority; }).slice(0, 5); // === KPI cards === function kpiCard(title, value, subtitle, color, icon) { return h('div', { style: { background: 'var(--rd-surface)', border: '1px solid var(--rd-border)', borderRadius: 12, padding: 16, display: 'flex', flexDirection: 'column', gap: 8 } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, h('div', { style: { width: 36, height: 36, borderRadius: 8, background: color || 'var(--rd-accent-soft)', color: color ? '#fff' : 'var(--rd-accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, fontWeight: 700 } }, icon || '●'), h('div', { style: { flex: 1 } }, h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', fontWeight: 600, letterSpacing: .3 } }, title), h('div', { style: { fontSize: 22, fontWeight: 700, color: 'var(--rd-text)', marginTop: 2 } }, value) ) ), subtitle && h('div', { style: { fontSize: 11, color: 'var(--rd-text-2)' } }, subtitle) ); } // === Секция критического пути === function criticalSection() { if (criticalTasks.length === 0) { return h('div', { style: { padding: 16, textAlign: 'center', color: 'var(--rd-text-muted)', fontSize: 13 } }, '✓ Нет критических задач — проект в рамках графика' ); } return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, criticalTasks.map(function (t, i) { var isOverdue = t.end && new Date(t.end) < now; var statusColor = isOverdue ? 'var(--rd-bad)' : 'var(--rd-warn)'; var statusLabel = isOverdue ? 'Просрочено' : 'Высокий приоритет'; var endLabel = t.end ? fmt.dayLabel(t.end) : 'без срока'; return h('div', { key: i, style: { background: 'var(--rd-surface-2)', border: '1px solid var(--rd-border)', borderRadius: 10, padding: 12, display: 'flex', alignItems: 'center', gap: 10 } }, h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: statusColor, flex: 'none' } }), h('div', { style: { flex: 1 } }, h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--rd-text)' } }, t.label || t.name || 'Задача #' + (i + 1)), h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginTop: 2 } }, statusLabel + ' · срок: ' + endLabel ) ), h('div', { style: { fontSize: 10, fontWeight: 700, color: statusColor, textTransform: 'uppercase', letterSpacing: .5 } }, isOverdue ? '!' : '⚠') ); }) ); } // === Render === return h('div', { style: { flex: 1, overflow: 'auto', padding: 20, display: 'flex', flexDirection: 'column', gap: 20 } }, // Header h('div', null, h('h2', { style: { fontSize: 20, fontWeight: 700, color: 'var(--rd-text)', marginBottom: 6 } }, 'Обзор проекта'), h('p', { style: { fontSize: 13, color: 'var(--rd-text-muted)' } }, 'Ключевые метрики, прогресс и критический путь' ) ), // KPI Grid h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 14 } }, kpiCard( 'БЮДЖЕТ', fmt.money(totalBudget), 'Потрачено: ' + fmt.money(totalSpent) + ' (' + budgetPercent + '%)', budgetPercent > 90 ? 'var(--rd-warn)' : 'var(--rd-accent)', '₽' ), kpiCard( 'ГРАФИК РАБОТ', schedulePercent + '%', completedTasks + ' из ' + totalTasks + ' задач', schedulePercent > 70 ? 'var(--rd-good)' : 'var(--rd-accent)', '⚙' ), kpiCard( 'ПРОСРОЧКИ', overdueTasks, overdueTasks === 0 ? 'Всё в срок' : overdueTasks + ' задач просрочено', overdueTasks > 0 ? 'var(--rd-bad)' : 'var(--rd-good)', '⚠' ), kpiCard( 'ПОСТАВКИ', pendingSupplies, overdueSupplies > 0 ? overdueSupplies + ' поставок просрочено' : 'Ожидают доставки', overdueSupplies > 0 ? 'var(--rd-warn)' : 'var(--rd-info)', '📦' ), kpiCard( 'ОПЛАТЫ', pendingPayments, overduePayments > 0 ? overduePayments + ' оплат просрочено' : 'Запланировано', overduePayments > 0 ? 'var(--rd-bad)' : 'var(--rd-info)', '💳' ), kpiCard( 'ОСТАТОК БЮДЖЕТА', fmt.money(budgetRemaining), budgetRemaining < 0 ? 'Перерасход!' : Math.round((budgetRemaining / totalBudget) * 100) + '% от бюджета', budgetRemaining < 0 ? 'var(--rd-bad)' : 'var(--rd-good)', '💰' ) ), // Критический путь h('div', { style: { background: 'var(--rd-surface)', border: '1px solid var(--rd-border)', borderRadius: 12, overflow: 'hidden' } }, h('div', { style: { padding: '14px 16px', borderBottom: '1px solid var(--rd-border)', display: 'flex', alignItems: 'center', gap: 8 } }, h('div', { style: { fontSize: 14, fontWeight: 700, color: 'var(--rd-text)' } }, 'Критический путь'), criticalTasks.length > 0 && h('div', { style: { fontSize: 10, fontWeight: 700, background: 'var(--rd-bad)', color: '#fff', padding: '3px 7px', borderRadius: 6 } }, criticalTasks.length) ), h('div', { style: { padding: 12 } }, criticalSection()) ), // Прогресс-бары h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 14 } }, // Бюджет прогресс h('div', { style: { background: 'var(--rd-surface)', border: '1px solid var(--rd-border)', borderRadius: 12, padding: 16 } }, h('div', { style: { fontSize: 12, fontWeight: 700, color: 'var(--rd-text)', marginBottom: 8 } }, 'Исполнение бюджета' ), h('div', { style: { width: '100%', height: 24, background: 'var(--rd-surface-3)', borderRadius: 8, overflow: 'hidden', position: 'relative' } }, h('div', { style: { width: Math.min(budgetPercent, 100) + '%', height: '100%', background: budgetPercent > 90 ? 'var(--rd-warn)' : 'var(--rd-accent)', transition: 'width 0.3s' } }), h('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, color: budgetPercent > 50 ? '#fff' : 'var(--rd-text)' } }, budgetPercent + '%') ), h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginTop: 6 } }, fmt.money(totalSpent) + ' из ' + fmt.money(totalBudget) ) ), // График прогресс h('div', { style: { background: 'var(--rd-surface)', border: '1px solid var(--rd-border)', borderRadius: 12, padding: 16 } }, h('div', { style: { fontSize: 12, fontWeight: 700, color: 'var(--rd-text)', marginBottom: 8 } }, 'Выполнение графика' ), h('div', { style: { width: '100%', height: 24, background: 'var(--rd-surface-3)', borderRadius: 8, overflow: 'hidden', position: 'relative' } }, h('div', { style: { width: Math.min(schedulePercent, 100) + '%', height: '100%', background: schedulePercent > 70 ? 'var(--rd-good)' : 'var(--rd-info)', transition: 'width 0.3s' } }), h('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, color: schedulePercent > 50 ? '#fff' : 'var(--rd-text)' } }, schedulePercent + '%') ), h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginTop: 6 } }, completedTasks + ' из ' + totalTasks + ' задач' ) ) ) ); };