/* Модуль «Смета» — паритет с монолитом: правка ячеек, фильтр, +строка, стиль грида (Линии/Зебра/Минимал), статус-колонка, скрытие Себест./Маржа по роли (seeMargin), экспорт CSV, клик по строке → drawer позиции. Данные/права из общего стора. */ window.RD = window.RD || { core: {}, modules: {} }; window.RD.modules.Estimate = function Estimate() { var store = RD.core.useStore(); var C = RD.core, money = C.money, h = React.createElement; var sections = store.raw.sections || []; var perms = store.perms, vat = store.vatRate; var fRef = React.useState('all'); var filter = fRef[0], setFilter = fRef[1]; var rwRef = React.useState(false); var raw = rwRef[0], setRaw = rwRef[1]; var gRef = React.useState('lines'); var gridStyle = gRef[0], setGridStyle = gRef[1]; var dRef = React.useState(null); var drawer = dRef[0], setDrawer = dRef[1]; var seeMargin = perms.seeMargin || perms.admin; function editLine(secId, rowId, field, val) { if (!perms.estimate) { store.showToast('Недостаточно прав: роль «' + perms.role + '» не правит смету', 'warn'); return; } var v = Math.max(0, parseFloat(('' + val).replace(',', '.')) || 0); var sec = sections.find(function (s) { return s.id === secId; }); if (!sec) return; var rows = sec.rows.map(function (r) { var o = Object.assign({}, r); if (r.id === rowId) o[field] = v; return o; }); store.update('sections', secId, { rows: rows }); } function chooseEstimateXor(secId, keepId) { var sec = sections.find(function (s) { return s.id === secId; }); if (!sec) return; var grp = (sec.rows.find(function (r) { return r.id === keepId; }) || {}).exclGroup; if (!grp) return; store.pushHist(); var rows = sec.rows.map(function (r) { if (r.exclGroup !== grp) return r; var on = (r.id === keepId); return Object.assign({}, r, { rejected: !on, exclChosen: on, status: on ? (r.status === 'alt' ? 'planned' : r.status) : 'alt' }); }); store.update('sections', secId, { rows: rows }); store.showToast('Вариант выбран в смете · альтернатива исключена из итога', 'ok'); } function addRow() { if (!perms.estimate) { store.showToast('Недостаточно прав для добавления позиции', 'warn'); return; } var sec = sections[0]; if (!sec) return; var row = { id: 'r' + Date.now(), kind: 'work', name: 'Новая позиция', unit: 'шт', qty: 1, price: 0, status: 'planned', pct: 0 }; store.update('sections', sec.id, { rows: (sec.rows || []).concat([row]) }); store.showToast('Добавлена позиция в «' + sec.name + '» · впишите наименование и цену', 'ok'); } function exportCSV() { var rows = []; sections.forEach(function (s) { (s.rows || []).forEach(function (r) { var amt = r.qty * r.price; rows.push([s.name, r.name, r.kind === 'work' ? 'работа' : 'материал', r.unit, r.qty, r.price, Math.round(amt), Math.round(C.lineVat(amt, vat)), Math.round(amt + C.lineVat(amt, vat)), C.statusMeta(r.status).label, (r.pct || 0) + '%']); }); }); var head = ['Раздел', 'Наименование', 'Тип', 'Ед', 'Кол-во', 'Цена', 'Сумма', 'НДС', 'Всего', 'Статус', 'Готовность']; var esc = function (v) { v = ('' + (v == null ? '' : v)).replace(/"/g, '""'); return /[";\n]/.test(v) ? '"' + v + '"' : v; }; var csv = '\uFEFF' + [head.map(esc).join(';')].concat(rows.map(function (r) { return r.map(esc).join(';'); })).join('\r\n'); try { var b = new Blob([csv], { type: 'text/csv;charset=utf-8' }); var u = URL.createObjectURL(b); var a = document.createElement('a'); a.href = u; a.download = 'smeta.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(u); }, 1000); store.showToast('Выгружено: smeta.csv · ' + rows.length + ' строк', 'ok'); } catch (e) { store.showToast('Ошибка экспорта', 'warn'); } } // колонки (динамически: маржа/себест только для seeMargin) var cols = [['no', '№', 40], ['name', 'Наименование', 1], ['unit', 'Ед.', 50], ['qty', 'Кол-во', 70], ['price', 'Цена', 88]]; if (seeMargin) cols.push(['cost', 'Себест.', 88]); cols.push(['amt', 'Сумма ƒ', 104], ['vat', 'НДС ƒ', 92]); if (seeMargin) cols.push(['margin', 'Маржа ƒ', 92]); cols.push(['status', 'Статус', 90], ['pct', 'Готов.', 70]); var gtc = cols.map(function (c) { return c[2] === 1 ? '1fr' : c[2] + 'px'; }).join(' '); var minW = cols.reduce(function (a, c) { return a + (c[2] === 1 ? 180 : c[2]); }, 0); var grand = 0, grandVat = 0; var blocks = sections.map(function (s) { var sub = 0, subVat = 0; var rows = (s.rows || []).filter(function (r) { if (r.rejected && !r.exclGroup) return false; if (filter === 'work') return r.kind === 'work'; if (filter === 'material') return r.kind === 'material'; if (filter === 'unfinished') return r.status !== 'done'; return true; }); (s.rows || []).forEach(function (r) { if (r.rejected) return; var amt = r.qty * r.price; sub += amt; subVat += C.lineVat(amt, vat); }); grand += sub; grandVat += subVat; return { s: s, rows: rows, sub: sub }; }); function rowBg(ri) { return gridStyle === 'zebra' && ri % 2 ? 'var(--rd-surface-2)' : 'var(--rd-surface)'; } function brd() { return gridStyle === 'minimal' ? 'none' : '1px solid var(--rd-border)'; } function cell(txt, st) { return h('div', { style: Object.assign({ padding: '7px 10px', fontSize: 12, borderBottom: brd(), display: 'flex', alignItems: 'center', overflow: 'hidden' }, st) }, txt); } function inp(r, secId, field) { return h('div', { style: { borderBottom: brd() } }, h('input', { defaultValue: r[field], onBlur: function (e) { editLine(secId, r.id, field, e.target.value); }, disabled: !perms.estimate, className: 'mono', style: { width: '100%', height: '100%', minHeight: 30, border: 'none', background: 'transparent', textAlign: 'right', fontSize: 12, color: 'var(--rd-text)', padding: '0 10px', outline: 'none' } })); } var seg = function (k, lbl) { return h('button', { key: k, onClick: function () { setGridStyle(k); }, style: { padding: '5px 9px', border: '1px solid var(--rd-border)', borderRadius: 7, background: gridStyle === k ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: gridStyle === k ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: gridStyle === k ? 600 : 500, cursor: 'pointer' } }, lbl); }; return h('div', { className: 'rd-main-content', style: { background: 'var(--rd-surface)', position: 'relative' } }, h('div', { style: { height: 46, flex: 'none', borderBottom: '1px solid var(--rd-border)', display: 'flex', alignItems: 'center', padding: '0 16px', gap: 10, background: 'var(--rd-surface-2)', flexWrap: 'wrap' } }, h('div', { style: { fontSize: 13, fontWeight: 600 } }, 'Смета'), h('div', { className: 'mono', style: { fontSize: 11.5, color: 'var(--rd-text-muted)' } }, sections.reduce(function (a, s) { return a + (s.rows || []).length; }, 0) + ' позиций · ' + sections.length + ' разд.'), h('button', { onClick: function () { setRaw(!raw); }, style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: raw ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: raw ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: raw ? 600 : 500, cursor: 'pointer' } }, 'Все столбцы из БД'), h('div', { style: { flex: 1 } }), h('div', { style: { display: 'flex', gap: 4 } }, seg('lines', 'Линии'), seg('zebra', 'Зебра'), seg('minimal', 'Минимал')), h('div', { style: { width: 1, height: 18, background: 'var(--rd-border)' } }), ['all', 'work', 'material', 'unfinished'].map(function (f) { return h('button', { key: f, onClick: function () { setFilter(f); }, style: { padding: '5px 9px', border: '1px solid var(--rd-border)', borderRadius: 7, background: filter === f ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: filter === f ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: filter === f ? 600 : 500, cursor: 'pointer' } }, { all: 'Все', work: 'Работы', material: 'Материалы', unfinished: 'Незаверш.' }[f]); }), h('button', { onClick: exportCSV, style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: 'var(--rd-surface)', color: 'var(--rd-text-2)', fontSize: 11, fontWeight: 500, cursor: 'pointer' } }, '↓ CSV'), perms.estimate ? h('button', { onClick: addRow, style: { padding: '5px 11px', border: 'none', borderRadius: 7, background: 'var(--rd-text)', color: 'var(--rd-surface)', fontSize: 11, fontWeight: 600, cursor: 'pointer' } }, '+ Строка') : null ), raw ? (function () { var rows = []; sections.forEach(function (s) { (s.rows || []).forEach(function (r) { rows.push(Object.assign({ раздел: s.name }, r)); }); }); var keys = []; rows.forEach(function (x) { Object.keys(x).forEach(function (k) { if (keys.indexOf(k) < 0) keys.push(k); }); }); var priceK = { price: 1, cost: 1 }; var fmt = function (v) { if (v == null) return ''; if (typeof v === 'object') return JSON.stringify(v); return '' + v; }; var grid = '40px ' + keys.map(function (k) { return k === 'name' ? '180px' : '110px'; }).join(' '); return h('div', { className: 'rd-gantt-wrap', style: { overflow: 'auto', padding: 12 } }, h('div', { style: { fontSize: 10.5, color: 'var(--rd-text-muted)', marginBottom: 6 } }, 'Все столбцы из БД: ' + keys.length + ' · строк: ' + rows.length + (seeMargin ? '' : ' · себест./маржа скрыты по роли')), h('div', { style: { border: '1px solid var(--rd-border)', borderRadius: 8, overflow: 'auto' } }, h('div', { style: { minWidth: 50 + keys.length * 112 } }, h('div', { style: { display: 'grid', gridTemplateColumns: grid, background: 'var(--rd-surface-3)', borderBottom: '1px solid var(--rd-border)', fontSize: 10, fontWeight: 600, color: 'var(--rd-text-muted)', position: 'sticky', top: 0 } }, [h('div', { key: 'n', style: { padding: '7px 8px' } }, '№')].concat(keys.map(function (k) { return h('div', { key: k, style: { padding: '7px 8px' } }, (priceK[k] && !seeMargin) ? (k + ' 🔒') : k); }))), rows.map(function (x, i) { return h('div', { key: i, style: { display: 'grid', gridTemplateColumns: grid, borderBottom: '1px solid var(--rd-border)', fontSize: 11, alignItems: 'center', background: x.rejected ? 'var(--rd-bad-soft,#fbeaea)' : (i % 2 ? 'var(--rd-surface-2)' : 'var(--rd-surface)') } }, [h('div', { key: 'n', className: 'mono', style: { padding: '5px 8px', color: 'var(--rd-text-muted)', fontSize: 10 } }, i + 1)].concat(keys.map(function (k) { var hide = priceK[k] && !seeMargin; return h('div', { key: k, className: (typeof x[k] === 'number') ? 'mono' : '', style: { padding: '5px 8px', color: 'var(--rd-text-2)', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hide ? '—' : fmt(x[k])); }))); }) ))); })() : (sections.length === 0 ? h('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 40, color: 'var(--rd-text-muted)' } }, h('div', { style: { fontSize: 48, marginBottom: 16, opacity: 0.3 } }, '📋'), h('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 8, color: 'var(--rd-text)' } }, 'Смета не загружена'), h('div', { style: { fontSize: 13, marginBottom: 20, textAlign: 'center', maxWidth: 420 } }, 'Для проекта ещё не создана смета. Импортируйте из документов (финальная смета ТСН) или создайте вручную.'), h('div', { style: { display: 'flex', gap: 12 } }, h('button', { onClick: function () { store.showToast('Импорт сметы — в разработке', 'info'); }, style: { padding: '10px 18px', border: '1px solid var(--rd-border)', borderRadius: 8, background: 'var(--rd-surface-2)', color: 'var(--rd-text)', fontSize: 13, fontWeight: 600, cursor: 'pointer' } }, '📄 Импорт из документов'), perms.estimate ? h('button', { onClick: function () { store.showToast('Создание сметы вручную — в разработке', 'info'); }, style: { padding: '10px 18px', border: 'none', borderRadius: 8, background: 'var(--rd-accent)', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer' } }, '+ Создать вручную') : null ) ) : h('div', { className: 'rd-gantt-wrap' }, h('div', { className: 'rd-estimate-grid', style: { minWidth: minW } }, h('div', { style: { display: 'grid', gridTemplateColumns: gtc, position: 'sticky', top: 0, zIndex: 2, background: 'var(--rd-surface-3)', borderBottom: '1px solid var(--rd-border)' } }, cols.map(function (c, i) { return h('div', { key: i, style: { padding: '9px 10px', fontSize: 10.5, fontWeight: 600, color: 'var(--rd-text-muted)', textAlign: (c[0] === 'qty' || c[0] === 'price' || c[0] === 'cost' || c[0] === 'amt' || c[0] === 'vat' || c[0] === 'margin') ? 'right' : 'left' } }, c[1]); })), blocks.map(function (b, bi) { return h(React.Fragment, { key: b.s.id }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 13px', background: 'var(--rd-surface-3)', borderTop: '1px solid var(--rd-border)', borderBottom: '1px solid var(--rd-border)' } }, h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, b.s.name), h('span', { className: 'mono', style: { fontSize: 10.5, color: 'var(--rd-text-muted)' } }, (b.s.rows || []).length + ' поз.'), h('span', { style: { flex: 1 } }), h('span', { className: 'mono', style: { fontSize: 11, color: 'var(--rd-text-2)' } }, 'Σ ' + money(b.sub) + ' ₽')), b.rows.map(function (r, ri) { var amt = r.qty * r.price, cost = (r.cost != null ? r.cost : Math.round(r.price * 0.72)), margin = r.price - cost; var c2 = cols.map(function (col) { var k = col[0]; if (k === 'no') return cell((bi + 1) + '.' + (ri + 1), { color: 'var(--rd-text-muted)', fontFamily: 'var(--rd-mono)', fontSize: 10.5 }); if (k === 'name') return cell(h('span', { onClick: function () { setDrawer({ secId: b.s.id, row: r }); }, style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', opacity: r.rejected ? .5 : 1 } }, h('span', { style: { width: 7, height: 7, borderRadius: 2, background: r.kind === 'material' ? 'var(--rd-accent)' : 'var(--rd-good)', flex: 'none' } }), h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textDecoration: r.rejected ? 'line-through' : 'none' } }, r.name), r.exclGroup ? h('button', { onClick: function (e) { e.stopPropagation(); chooseEstimateXor(b.s.id, r.id); }, title: 'XOR: выбрать этот вариант', style: { flex: 'none', fontSize: 9, fontWeight: 700, padding: '1px 6px', borderRadius: 4, border: 'none', cursor: 'pointer', background: r.exclChosen ? 'var(--rd-accent)' : 'var(--rd-surface-3)', color: r.exclChosen ? '#fff' : 'var(--rd-accent)' } }, r.exclChosen ? 'XOR ✓' : 'выбрать') : null)); if (k === 'unit') return cell(r.unit, { color: 'var(--rd-text-2)', fontSize: 11 }); if (k === 'qty') return inp(r, b.s.id, 'qty'); if (k === 'price') return inp(r, b.s.id, 'price'); if (k === 'cost') return cell(money(cost), { fontFamily: 'var(--rd-mono)', justifyContent: 'flex-end', color: 'var(--rd-text-muted)', fontSize: 11 }); if (k === 'amt') return cell(money(amt), { fontFamily: 'var(--rd-mono)', fontWeight: 600, justifyContent: 'flex-end', background: 'var(--rd-accent-soft)', color: 'var(--rd-text)' }); if (k === 'vat') return cell(money(C.lineVat(amt, vat)), { fontFamily: 'var(--rd-mono)', justifyContent: 'flex-end', color: 'var(--rd-text-muted)', fontSize: 11 }); if (k === 'margin') return cell((margin >= 0 ? '+' : '') + money(margin * r.qty), { fontFamily: 'var(--rd-mono)', justifyContent: 'flex-end', color: margin >= 0 ? 'var(--rd-good)' : 'var(--rd-bad)', fontSize: 11 }); if (k === 'status') return cell(h('span', { style: C.pill(r.status) }, C.statusMeta(r.status).label)); if (k === 'pct') return cell((r.pct || 0) + '%', { color: 'var(--rd-text-2)', fontSize: 11 }); return cell(''); }); return h('div', { key: r.id, style: { display: 'grid', gridTemplateColumns: gtc, background: rowBg(ri) } }, c2); }) ); }) ) )), h('div', { style: { flex: 'none', borderTop: '1px solid var(--rd-border)', background: 'var(--rd-surface-2)', display: 'flex', alignItems: 'center', padding: '0 18px', height: 50, gap: 24 } }, h('div', null, h('span', { style: { fontSize: 11, color: 'var(--rd-text-muted)' } }, 'Итого без НДС '), h('span', { className: 'mono', style: { fontSize: 14, fontWeight: 600 } }, money(grand) + ' ₽')), h('div', null, h('span', { style: { fontSize: 11, color: 'var(--rd-text-muted)' } }, 'НДС ' + Math.round(vat * 100) + '% '), h('span', { className: 'mono', style: { fontSize: 13, color: 'var(--rd-text-2)' } }, money(grandVat) + ' ₽')), h('div', { style: { flex: 1 } }), h('div', null, h('span', { style: { fontSize: 11.5, color: 'var(--rd-text-2)' } }, 'Всего с НДС '), h('span', { className: 'mono', style: { fontSize: 17, fontWeight: 700, color: 'var(--rd-accent)' } }, money(grand + grandVat) + ' ₽')) ), // drawer позиции drawer ? (function () { var r = drawer.row, amt = r.qty * r.price, cost = (r.cost != null ? r.cost : Math.round(r.price * 0.72)); return h('div', { onClick: function () { setDrawer(null); }, style: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,.34)', zIndex: 60, display: 'flex', justifyContent: 'flex-end' } }, h('div', { onClick: function (e) { e.stopPropagation(); }, style: { width: 360, maxWidth: '92vw', height: '100%', background: 'var(--rd-surface)', boxShadow: '-12px 0 40px rgba(0,0,0,.2)', overflow: 'auto' } }, h('div', { style: { padding: 18, borderBottom: '1px solid var(--rd-border)', display: 'flex', alignItems: 'center', gap: 10 } }, h('div', { style: { flex: 1 } }, h('div', { style: { fontSize: 15, fontWeight: 600 } }, r.name), h('div', { style: { fontSize: 11.5, color: 'var(--rd-text-muted)' } }, (r.kind === 'material' ? 'Материал' : 'Работа') + ' · ' + r.unit)), h('button', { onClick: function () { setDrawer(null); }, style: { border: 'none', background: 'none', fontSize: 20, color: 'var(--rd-text-muted)', cursor: 'pointer' } }, '×')), h('div', { style: { padding: 16 } }, h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 9 } }, [['Кол-во', r.qty + ' ' + r.unit], ['Цена', money(r.price) + ' ₽'], ['Сумма', money(amt) + ' ₽'], ['НДС', money(C.lineVat(amt, vat)) + ' ₽'], ['Статус', C.statusMeta(r.status).label], ['Готовность', (r.pct || 0) + '%']] .concat(seeMargin ? [['Себест.', money(cost) + ' ₽'], ['Маржа', money((r.price - cost) * r.qty) + ' ₽']] : []) .map(function (s, i) { return h('div', { key: i, style: { border: '1px solid var(--rd-border)', borderRadius: 9, padding: '9px 11px', background: 'var(--rd-surface-2)' } }, h('div', { style: { fontSize: 10.5, color: 'var(--rd-text-muted)', marginBottom: 3 } }, s[0]), h('div', { className: 'mono', style: { fontSize: 13, fontWeight: 700 } }, s[1])); }))) ) ); })() : null ); };