/* Модуль «График работ» — гантт из works (даты, статус, зависимости FS/SS/FF). Данные/права из общего стора. Цены/деньги тут не показываются (роль-агностично). */ window.RD = window.RD || { core: {}, modules: {} }; window.RD.modules.Schedule = function Schedule() { var store = RD.core.useStore(); var C = RD.core, h = React.createElement; var srRef = React.useState(false); var showRejected = srRef[0], setShowRejected = srRef[1]; var works = (store.raw.works || []).filter(function (w) { return showRejected || !w.rejected; }); var proj = store.raw.project || {}; var todayD = proj.todayDay != null ? proj.todayDay : 11; // горизонт: max end var maxEnd = works.reduce(function (m, w) { return Math.max(m, w.end || 0); }, 1); var totalDays = maxEnd + 2; var scaleRef = React.useState('day'); var scale = scaleRef[0], setScale = scaleRef[1]; var viewRef = React.useState('gantt'); var ganttView = viewRef[0], setGanttView = viewRef[1]; var dayW = ({ day: 22, '3day': 12, week: 9, decade: 6, month: 3.2, quarter: 1.4 })[scale] || 22, rowH = 34, barTop = 7, barH = 20, labelW = 250, headerH = 34; var sX = function (d) { return d * dayW; }; var gTotalW = totalDays * dayW; // drag-перетаскивание баров → каскадный пересчёт (как ganttDrag* монолита) var dgRef = React.useState(null); var dragWorks = dgRef[0], setDragWorks = dgRef[1]; var dragMeta = React.useRef(null); React.useEffect(function () { function move(e) { var dm = dragMeta.current; if (!dm) return; var dd = Math.round((e.clientX - dm.x0) / dayW); if (dd === dm.moved) return; dm.moved = dd; var wks; if (dm.edge === 'right') { var ne = Math.max(dm.start0, dm.end0 + dd); wks = (store.raw.works || []).map(function (w) { return w.id === dm.id ? Object.assign({}, w, { end: ne }) : Object.assign({}, w); }); } else if (dm.edge === 'left') { var nst = Math.max(0, Math.min(dm.end0, dm.start0 + dd)); wks = (store.raw.works || []).map(function (w) { return w.id === dm.id ? Object.assign({}, w, { start: nst }) : Object.assign({}, w); }); } else { var ns = Math.max(0, dm.start0 + dd), du = dm.end0 - dm.start0; wks = (store.raw.works || []).map(function (w) { return w.id === dm.id ? Object.assign({}, w, { start: ns, end: ns + du }) : Object.assign({}, w); }); } wks = C.propagateSchedule(wks, dm.id, lockSet); dm.result = wks; setDragWorks(wks); } function up() { var dm = dragMeta.current; if (!dm) return; dragMeta.current = null; var res = dm.result, moved = dm.moved; setDragWorks(null); if (moved !== 0 && res) { if (!store.perms.schedule) { store.showToast('Недостаточно прав: роль «' + store.perms.role + '» не двигает график', 'warn'); return; } store.pushHist(); var nd = Object.assign({}, store.raw); nd.works = res; store.replaceEntity('works', res); store.showToast('Работа сдвинута на ' + (moved > 0 ? '+' : '') + moved + ' дн · пересчитано ' + (C._lastMoves || 0) + ' работ', 'ok'); } } window.addEventListener('mousemove', move); window.addEventListener('mouseup', up); return function () { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); }; }, [store]); function startDrag(w, e) { dragMeta.current = { id: w.id, x0: e.clientX, start0: w.start, end0: w.end, moved: 0, result: null, edge: null }; e.preventDefault(); e.stopPropagation(); } function startResize(w, e, edge) { dragMeta.current = { id: w.id, x0: e.clientX, start0: w.start, end0: w.end, moved: 0, result: null, edge: edge }; e.preventDefault(); e.stopPropagation(); } var pfRef = React.useState(false); var planFact = pfRef[0], setPlanFact = pfRef[1]; var blRef = React.useState(false); var showBase = blRef[0], setShowBase = blRef[1]; var wdRef = React.useState(null); var workDrawer = wdRef[0], setWorkDrawer = wdRef[1]; var lmRef = React.useState(false); var linkMode = lmRef[0], setLinkMode = lmRef[1]; var lfRef = React.useState(null); var linkFrom = lfRef[0], setLinkFrom = lfRef[1]; var ltRef = React.useState('FS'); var linkType = ltRef[0], setLinkType = ltRef[1]; var hovRef = React.useState(null); var hoverW = hovRef[0], setHoverW = hovRef[1]; var scnRef = React.useState(null); var scenario = scnRef[0], setScenario = scnRef[1]; var lockRef = React.useState([]); var locked = lockRef[0], setLocked = lockRef[1]; var lockSet = {}; locked.forEach(function (id) { lockSet[id] = 1; }); var lyRef = React.useState({ pay: false, del: false, labor: false, cash: false }); var layers = lyRef[0], setLayers = lyRef[1]; function toggleLayer(k) { var n = Object.assign({}, layers); n[k] = !n[k]; setLayers(n); } var selRef = React.useState([]); var sel = selRef[0], setSel = selRef[1]; var peRef = React.useState(null); var pendingEdit = peRef[0], setPendingEdit = peRef[1]; var prRef = React.useState(''); var peReason = prRef[0], setPeReason = prRef[1]; function toggleSel(id) { setSel(sel.indexOf(id) >= 0 ? sel.filter(function (x) { return x !== id; }) : sel.concat([id])); } function clearSel() { setSel([]); } function bulkShift(delta) { if (!sel.length) return; if (!store.perms.schedule) { store.showToast('Недостаточно прав: роль «' + store.perms.role + '» не двигает график', 'warn'); return; } store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return sel.indexOf(w.id) >= 0 ? Object.assign({}, w, { start: Math.max(0, w.start + delta), end: Math.max(1, w.end + delta) }) : Object.assign({}, w); }); nd = C.propagateSchedule(nd, null, lockSet); store.replaceEntity('works', nd); store.showToast('Сдвинуто ' + sel.length + ' работ на ' + (delta > 0 ? '+' : '') + delta + ' дн · каскад пересчитан', 'ok'); } function bulkBrigade() { if (!sel.length) return; store.showToast('Назначение бригады для ' + sel.length + ' работ — в проде выбор из ресурсов', 'ok'); } function editWorkField(id, field, val) { if (!store.perms.schedule) { store.showToast('Недостаточно прав: роль «' + store.perms.role + '» не правит график', 'warn'); return; } var w0 = (store.raw.works || []).find(function (x) { return x.id === id; }); if (!w0) return; var patch = {}; if (field === 'start') { var ns = parseInt(val, 10); if (isNaN(ns) || ns < 0) return; var du = w0.end - w0.start; patch = { start: ns, end: ns + du }; } else if (field === 'dur') { var d = Math.max(1, parseInt(val, 10) || 1); patch = { end: w0.start + d - 1 }; } else if (field === 'name') { if (!('' + val).trim()) return; patch = { name: ('' + val).trim() }; } else return; // правка факта/прошлого → подтверждение var isFact = (w0.status === 'done' || w0.status === 'in_progress'); var inPast = (patch.start != null && patch.start < todayD); if (isFact || inPast) { setPendingEdit({ id: id, field: field, patch: patch, name: w0.name, reason: isFact ? 'правка факта идущей/завершённой работы' : 'начало в прошлом (раньше «сегодня»)' }); return; } store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return w.id === id ? Object.assign({}, w, patch) : Object.assign({}, w); }); nd = C.propagateSchedule(nd, null, lockSet); store.replaceEntity('works', nd); store.showToast('Изменено · каскад пересчитан', 'ok'); } function commitPendingEdit(reasonText) { var pe = pendingEdit; if (!pe) return; store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return w.id === pe.id ? Object.assign({}, w, pe.patch) : Object.assign({}, w); }); nd = C.propagateSchedule(nd, null, lockSet); store.replaceEntity('works', nd); setPendingEdit(null); store.showToast('Подтверждено' + (reasonText ? ' · причина: ' + reasonText : '') + ' · запись в журнал', 'ok'); } function projEnd(wks) { return Math.max.apply(null, (wks || []).filter(function (w) { return !w.rejected; }).map(function (w) { return w.end; })); } function enterScenario() { setScenario({ end0: projEnd(store.raw.works), snap: JSON.stringify(store.raw.works) }); store.showToast('Режим «что-если»: меняйте график, сравнивайте срок; примите или отбросьте', 'ok'); } function discardScenario() { if (scenario && scenario.snap) { store.replaceEntity('works', JSON.parse(scenario.snap)); } setScenario(null); store.showToast('Сценарий отброшен', 'warn'); } function acceptScenario() { setScenario(null); store.showToast('Сценарий принят как новый план · чекпоинт', 'ok'); } function toggleLock(id) { var has = locked.indexOf(id) >= 0; setLocked(has ? locked.filter(function (x) { return x !== id; }) : locked.concat([id])); store.showToast('Дата работы ' + (has ? 'разблокирована' : 'зафиксирована (констрейнт «не двигать»)'), 'ok'); } function applyWorks(nd, primaryId) { var res = C.propagateSchedule(nd, null, lockSet); store.replaceEntity('works', res); return res; } function setDepType(workId, predId, t) { store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return w.id === workId ? Object.assign({}, w, { deps: (w.deps || []).map(function (d) { return d.f === predId ? Object.assign({}, d, { t: t }) : d; }) }) : Object.assign({}, w); }); applyWorks(nd, workId); store.showToast('Тип связи: ' + t + ' · график пересчитан', 'ok'); } function setDepLag(workId, predId, v) { store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return w.id === workId ? Object.assign({}, w, { deps: (w.deps || []).map(function (d) { return d.f === predId ? Object.assign({}, d, { lag: v }) : d; }) }) : Object.assign({}, w); }); applyWorks(nd, workId); store.showToast('Лаг связи: ' + (v > 0 ? '+' + v : v) + ' дн · пересчитано', 'ok'); } function removeDep(workId, predId) { store.pushHist(); var nd = (store.raw.works || []).map(function (w) { return w.id === workId ? Object.assign({}, w, { deps: (w.deps || []).filter(function (d) { return d.f !== predId; }) }) : Object.assign({}, w); }); applyWorks(nd, workId); store.showToast('Связь удалена · пересчитано', 'ok'); } function chooseXor(keepId, dropId) { store.pushHist(); var nd = (store.raw.works || []).map(function (w) { if (w.id === keepId) return Object.assign({}, w, { rejected: false, status: w.status === 'alt' ? 'planned' : w.status }); if (w.id === dropId) return Object.assign({}, w, { rejected: true, status: 'alt' }); return Object.assign({}, w); }); store.replaceEntity('works', C.propagateSchedule(nd, null, lockSet)); store.showToast('Выбран вариант · альтернатива отклонена, график пересчитан', 'ok'); } function reaches(wks, from, target) { var byId = {}; wks.forEach(function (w) { byId[w.id] = w; }); var stack = [from], seen = {}; while (stack.length) { var cur = stack.pop(); if (cur === target) return true; if (seen[cur]) continue; seen[cur] = 1; (byId[cur] && byId[cur].deps || []).forEach(function (d) { stack.push(d.f); }); } return false; } function pickForLink(id) { if (!linkFrom) { setLinkFrom(id); store.showToast('Предшественник выбран · кликните зависимую работу', 'ok'); return; } if (linkFrom === id) { setLinkFrom(null); store.showToast('Связь отменена', 'warn'); return; } var wks0 = store.raw.works || [], target = wks0.find(function (w) { return w.id === id; }); if ((target.deps || []).some(function (d) { return d.f === linkFrom; })) { setLinkMode(false); setLinkFrom(null); store.showToast('Такая связь уже есть', 'warn'); return; } if (reaches(wks0, linkFrom, id)) { setLinkMode(false); setLinkFrom(null); store.showToast('Нельзя — возникнет цикл зависимостей', 'warn'); return; } if (reaches(wks0, id, linkFrom)) { setLinkMode(false); setLinkFrom(null); store.showToast('Избыточная связь: уже зависит через цепочку', 'warn'); return; } store.pushHist(); var nd = wks0.map(function (w) { return w.id === id ? Object.assign({}, w, { deps: (w.deps || []).concat([{ f: linkFrom, t: linkType, lag: 0 }]) }) : Object.assign({}, w); }); nd = C.propagateSchedule(nd, null); store.replaceEntity('works', nd); setLinkMode(false); setLinkFrom(null); store.showToast('Связь ' + linkType + ' добавлена · график пересчитан', 'ok'); } function chainOf(wid) { var byId = {}; (store.raw.works || []).forEach(function (w) { byId[w.id] = w; }); var up = {}, down = {}; (function climb(id) { (byId[id] && byId[id].deps || []).forEach(function (d) { if (!up[d.f]) { up[d.f] = 1; climb(d.f); } }); })(wid); (function desc(id) { (store.raw.works || []).forEach(function (w) { if ((w.deps || []).some(function (d) { return d.f === id; }) && !down[w.id]) { down[w.id] = 1; desc(w.id); } }); })(wid); return { up: up, down: down }; } var baseRef = React.useRef(null); function toggleBaseline() { if (!baseRef.current) { baseRef.current = (store.raw.works || []).map(function (w) { return { id: w.id, start: w.start, end: w.end }; }); store.showToast('Базовый план зафиксирован (baseline)', 'ok'); setShowBase(true); } else setShowBase(!showBase); } if (dragWorks) { var dwById = {}; dragWorks.forEach(function (w) { dwById[w.id] = w; }); works = works.map(function (w) { return dwById[w.id] || w; }); } // критический путь: работы с нулевым резервом (упрощённо — самая длинная цепочка по FS) var byId = {}; works.forEach(function (w) { byId[w.id] = w; }); var crit = {}; // прямой проход: ES; обратный: LS; slack=0 → крит. var ES = {}, EF = {}, dur = {}; works.forEach(function (w) { dur[w.id] = (w.end - w.start + 1); }); works.forEach(function (w) { var es = 0; (w.deps || []).forEach(function (d) { var p = byId[d.f]; if (!p) return; var lag = d.lag || 0; var c = d.t === 'SS' ? (ES[d.f] || p.start) + lag : d.t === 'FF' ? (EF[d.f] || (p.end + 1)) - dur[w.id] + lag : (EF[d.f] || (p.end + 1)) + lag; if (c > es) es = c; }); ES[w.id] = es || w.start; EF[w.id] = (es || w.start) + dur[w.id]; }); var projEnd = works.reduce(function (m, w) { return Math.max(m, EF[w.id]); }, 0); var LS = {}, LF = {}; works.slice().reverse().forEach(function (w) { LF[w.id] = projEnd; }); for (var pass = 0; pass < works.length; pass++) { works.forEach(function (w) { (w.deps || []).forEach(function (d) { var p = byId[d.f]; if (!p) return; var lag = d.lag || 0; var lf = d.t === 'SS' ? (LS[w.id] != null ? LS[w.id] : w.start) - lag + dur[d.f] : (LS[w.id] != null ? LS[w.id] : w.start) - lag; if (LF[d.f] == null || lf < LF[d.f]) LF[d.f] = lf; }); }); } works.forEach(function (w) { LS[w.id] = (LF[w.id] != null ? LF[w.id] : projEnd) - dur[w.id]; if ((LS[w.id] - (ES[w.id] != null ? ES[w.id] : w.start)) <= 0) crit[w.id] = true; }); var stColor = function (w) { return w.status === 'done' ? 'var(--rd-good)' : w.status === 'in_progress' ? 'var(--rd-accent)' : w.status === 'alt' ? '#cbb994' : '#aeb0cf'; }; var vi = {}; works.forEach(function (w, i) { vi[w.id] = i; }); // svg-связи var links = []; works.forEach(function (w) { (w.deps || []).forEach(function (d) { if (vi[d.f] == null) return; var p = byId[d.f], pi = vi[d.f], si = vi[w.id]; var sy = pi * rowH + barTop + barH / 2, ty = si * rowH + barTop + barH / 2; var pX1 = (p.end + 1) * dayW, pX0 = p.start * dayW, cX0 = w.start * dayW, cX1 = (w.end + 1) * dayW; var sx = d.t === 'SS' ? pX0 : pX1, tx = (d.t === 'FF') ? cX1 : cX0; var isC = crit[p.id] && crit[w.id], col = isC ? 'var(--rd-bad)' : '#bcbcca', stub = 7; links.push({ d: 'M ' + sx + ' ' + sy + ' L ' + (sx + stub) + ' ' + sy + ' L ' + (sx + stub) + ' ' + ty + ' L ' + tx + ' ' + ty, col: col, w: isC ? 1.7 : 1.1 }); }); }); var ticks = []; for (var t2 = 0; t2 <= totalDays; t2 += 7) ticks.push(t2); // ===== ДОРОЖКИ-СЛОИ (этапы → оплаты/поставки/раб.сила/cash) ===== var allW = (store.raw.works || []).filter(function (w) { return !w.rejected; }); var stageMap = {}; allW.forEach(function (w) { var st = w.stage || '—'; if (!stageMap[st]) stageMap[st] = { name: st, start: w.start, end: w.end }; else { stageMap[st].start = Math.min(stageMap[st].start, w.start); stageMap[st].end = Math.max(stageMap[st].end, w.end); } }); var stages = Object.keys(stageMap).map(function (k) { return stageMap[k]; }); var stColors = ['#3357e6', '#c2790a', '#0a7ea4', '#2f8f5b', '#7a5af0', '#9a6a06', '#5f6b8f']; stages.forEach(function (s, i) { s.color = stColors[i % stColors.length]; }); // суммы по этапам — из payments (по полю stage), иначе пропорция function stageAmt(name) { var ps = (store.raw.payments || []).filter(function (p) { return p.stage === name; }); return ps.reduce(function (a, p) { return a + (p.amount || 0); }, 0); } var payEv = [], delEv = []; stages.forEach(function (s) { var amt = stageAmt(s.name), adv = Math.round(amt * 0.4), comp = amt - adv; delEv.push({ day: Math.max(0, s.start - 2), color: s.color, text: 'Поставка', sub: '→ старт «' + s.name + '» (−2д)' }); if (adv) payEv.push({ day: s.start, color: s.color, type: 'Аванс', amt: adv, sub: 'старт «' + s.name + '»' }); if (comp) payEv.push({ day: Math.min(totalDays - 1, s.end + 1), color: s.color, type: 'По завершении', amt: comp, sub: 'сдача «' + s.name + '»' }); }); var paySumVis = payEv.reduce(function (a, e) { return a + e.amt; }, 0); // рабочая сила по дням (trade × crew) var laborDay = []; var laborPeak = 0; for (var d2 = 0; d2 < totalDays; d2++) { var tot = 0, seg = {}; allW.forEach(function (w) { if (d2 >= w.start && d2 <= w.end) { var c = w.crew || 1; tot += c; seg[w.trade || '—'] = (seg[w.trade || '—'] || 0) + c; } }); laborDay.push({ d: d2, tot: tot, seg: seg }); if (tot > laborPeak) laborPeak = tot; } // resource leveling: перегрузка по специальности (норма 4) var TRADE_CAP = 4, overloads = {}; laborDay.forEach(function (pd) { Object.keys(pd.seg).forEach(function (tr) { if (pd.seg[tr] > TRADE_CAP) { overloads[tr] = Math.max(overloads[tr] || 0, pd.seg[tr]); } }); }); var levelKeys = Object.keys(overloads); var levelText = levelKeys.length ? ('Перегрузка: ' + levelKeys.map(function (tr) { return tr + ' до ' + overloads[tr] + ' чел (норма ' + TRADE_CAP + ')'; }).join(' · ')) : ''; // cash-flow накопительно var cashCum = [], cum = 0; var cashTotal = paySumVis || 1; for (var d3 = 0; d3 < totalDays; d3++) { cum += payEv.filter(function (e) { return e.day === d3; }).reduce(function (a, e) { return a + e.amt; }, 0); cashCum.push(cum); } // weekend bands (день 0 = вт, упрощённо: сб/вс каждые 7) var weekends = []; for (var d4 = 0; d4 < totalDays; d4++) { var dow = (d4 + 2) % 7; if (dow === 5 || dow === 6) weekends.push(d4); } var moneyShort = function (n) { return n >= 1000 ? Math.round(n / 1000) + 'к' : '' + n; }; var laneHeadStyle = { display: 'flex', alignItems: 'center', gap: 8, height: 26, background: 'var(--rd-surface-3)', borderTop: '1px solid var(--rd-border)', borderBottom: '1px solid var(--rd-border)', padding: '0 12px', fontSize: 10.5, fontWeight: 700, letterSpacing: .3, color: 'var(--rd-text-2)', position: 'sticky', left: 0 }; var LAYER_DEFS = [['pay', 'Оплаты'], ['del', 'Поставки'], ['labor', 'Раб.сила'], ['cash', 'Финансы']]; return h('div', { className: 'rd-main-content', style: { background: 'var(--rd-surface)' } }, h('div', { style: { height: 46, flex: 'none', borderBottom: '1px solid var(--rd-border)', display: 'flex', alignItems: 'center', padding: '0 16px', gap: 12, background: 'var(--rd-surface-2)' } }, h('div', { style: { fontSize: 13, fontWeight: 600 } }, 'График работ'), h('div', { className: 'mono', style: { fontSize: 11.5, color: 'var(--rd-text-muted)' } }, works.length + ' работ · ' + links.length + ' связей · крит. путь ' + Object.keys(crit).length), h('div', { style: { display: 'flex', padding: 2, background: 'var(--rd-surface-3)', borderRadius: 7, gap: 2 } }, [['gantt', 'Гантт'], ['table', 'Таблица']].map(function (v) { return h('button', { key: v[0], onClick: function () { setGanttView(v[0]); }, style: { padding: '4px 10px', border: 'none', borderRadius: 6, background: ganttView === v[0] ? 'var(--rd-surface)' : 'transparent', color: ganttView === v[0] ? 'var(--rd-text)' : 'var(--rd-text-muted)', fontSize: 11, fontWeight: ganttView === v[0] ? 600 : 500, cursor: 'pointer', boxShadow: ganttView === v[0] ? '0 1px 2px rgba(0,0,0,.08)' : 'none' } }, v[1]); })), h('div', { style: { flex: 1 } }), h('button', { onClick: function () { setShowRejected(!showRejected); }, title: 'Показать/скрыть отклонённые (XOR-альтернативы)', style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: showRejected ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: showRejected ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: showRejected ? 600 : 500, cursor: 'pointer' } }, showRejected ? '⊘ Скрыть отклонённые' : '⊘ Показать отклонённые'), h('button', { onClick: function () { setPlanFact(!planFact); }, style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: planFact ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: planFact ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: planFact ? 600 : 500, cursor: 'pointer' } }, 'План/Факт'), h('button', { onClick: toggleBaseline, style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: showBase ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: showBase ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: showBase ? 600 : 500, cursor: 'pointer' } }, 'Baseline'), h('div', { style: { display: 'flex', gap: 2 } }, [['day', 'Дни'], ['week', 'Нед'], ['month', 'Мес'], ['quarter', 'Кв']].map(function (s) { return h('button', { key: s[0], onClick: function () { setScale(s[0]); }, style: { padding: '5px 8px', border: '1px solid var(--rd-border)', borderRadius: 6, background: scale === s[0] ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: scale === s[0] ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, cursor: 'pointer' } }, s[1]); })), h('button', { onClick: function () { setLinkMode(!linkMode); setLinkFrom(null); store.showToast(linkMode ? 'Режим связывания выключен' : 'Режим связи: кликните предшественника, затем зависимую', 'ok'); }, style: { padding: '5px 10px', border: '1px solid var(--rd-border)', borderRadius: 7, background: linkMode ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: linkMode ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 11, fontWeight: linkMode ? 600 : 500, cursor: 'pointer' } }, '⛓ Связать'), scenario ? h('span', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, h('span', { className: 'mono', style: { fontSize: 11, fontWeight: 700, color: projEnd(works) > scenario.end0 ? 'var(--rd-bad)' : 'var(--rd-good)' } }, (function () { var d = projEnd(works) - scenario.end0; return (d > 0 ? '+' + d : d) + ' дн к сроку'; })()), h('button', { onClick: acceptScenario, style: { padding: '5px 9px', border: 'none', borderRadius: 7, background: 'var(--rd-good)', color: '#fff', fontSize: 11, fontWeight: 600, cursor: 'pointer' } }, 'Принять'), h('button', { onClick: discardScenario, style: { padding: '5px 9px', border: '1px solid var(--rd-border)', borderRadius: 7, background: 'var(--rd-surface)', color: 'var(--rd-text-2)', fontSize: 11, cursor: 'pointer' } }, 'Отбросить')) : h('button', { onClick: enterScenario, 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' } }, '⎇ Что-если'), linkMode ? h('div', { style: { display: 'flex', gap: 2 } }, ['FS', 'SS', 'FF'].map(function (t) { return h('button', { key: t, onClick: function () { setLinkType(t); }, style: { padding: '5px 7px', border: '1px solid var(--rd-border)', borderRadius: 6, background: linkType === t ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: linkType === t ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 10.5, fontWeight: 600, cursor: 'pointer' } }, t); })) : null, h('span', { style: { fontSize: 10, color: 'var(--rd-text-muted)', marginLeft: 4 } }, 'Слои:'), LAYER_DEFS.map(function (l) { return h('button', { key: l[0], onClick: function () { toggleLayer(l[0]); }, style: { padding: '4px 8px', border: '1px solid ' + (layers[l[0]] ? 'var(--rd-accent)' : 'var(--rd-border)'), borderRadius: 6, background: layers[l[0]] ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: layers[l[0]] ? 'var(--rd-accent)' : 'var(--rd-text-muted)', fontSize: 10.5, fontWeight: layers[l[0]] ? 600 : 500, cursor: 'pointer' } }, l[1]); }) ), ganttView === 'table' ? (function () { // все столбцы из БД: собираем объединение ключей по всем работам var keys = []; (store.raw.works || []).forEach(function (w) { Object.keys(w).forEach(function (k) { if (keys.indexOf(k) < 0) keys.push(k); }); }); var editable = { name: 1, start: 1 }; var fmt = function (v) { if (v == null) return ''; if (Array.isArray(v)) return v.map(function (d) { return d && d.t ? (d.t + (d.lag ? (d.lag > 0 ? '+' + d.lag : d.lag) : '') + '←' + d.f) : JSON.stringify(d); }).join(' · '); if (typeof v === 'object') return JSON.stringify(v); return '' + v; }; var colW = function (k) { return k === 'name' ? 'minmax(170px,1.4fr)' : k === 'deps' ? '150px' : (k === 'compare' ? '220px' : '92px'); }; var grid = '44px ' + keys.map(colW).join(' '); return h('div', { className: 'rd-gantt-wrap', style: { overflow: 'auto' } }, h('div', { style: { padding: '6px 12px', fontSize: 10.5, color: 'var(--rd-text-muted)' } }, 'Все столбцы из БД: ' + keys.length + ' · строк: ' + works.length + (store.perms.schedule ? ' · правка: имя, начало' : ' · только просмотр (роль ' + store.perms.role + ')')), h('div', { style: { minWidth: 60 + keys.length * 96 } }, 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, zIndex: 2 } }, [h('div', { key: 'n', style: { padding: '8px 8px' } }, '№')].concat(keys.map(function (k) { return h('div', { key: k, style: { padding: '8px 8px' } }, k + (editable[k] && store.perms.schedule ? ' ✎' : '')); }))), works.map(function (w, i) { return h('div', { key: w.id, style: { display: 'grid', gridTemplateColumns: grid, borderBottom: '1px solid var(--rd-border)', fontSize: 11.5, alignItems: 'center', background: w.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) { if (editable[k] && store.perms.schedule) return h('input', { key: k, defaultValue: w[k], onBlur: function (e) { if (('' + e.target.value) !== ('' + w[k])) editWorkField(w.id, k, e.target.value); }, style: { padding: '5px 8px', border: 'none', background: 'transparent', fontSize: 11.5, color: 'var(--rd-text)', width: '100%', fontFamily: k === 'name' ? 'inherit' : 'IBM Plex Mono, monospace' } }); return h('div', { key: k, className: (typeof w[k] === 'number' || k === 'deps' || k === 'id') ? 'mono' : '', style: { padding: '5px 8px', color: 'var(--rd-text-2)', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, fmt(w[k])); }))); }) ) ); })() : h('div', { className: 'rd-gantt-wrap' }, h('div', { style: { width: labelW + gTotalW, position: 'relative' } }, h('div', { style: { display: 'flex', position: 'sticky', top: 0, zIndex: 5, height: headerH, background: 'var(--rd-surface-2)', borderBottom: '1px solid var(--rd-border)' } }, h('div', { style: { position: 'sticky', left: 0, width: labelW, flex: 'none', background: 'var(--rd-surface-2)', borderRight: '1px solid var(--rd-border)', display: 'flex', alignItems: 'center', padding: '0 12px', fontSize: 10.5, fontWeight: 600, color: 'var(--rd-text-muted)', zIndex: 6 } }, 'РАБОТА · ЗАВИСИМОСТИ'), h('div', { style: { position: 'relative', flex: 1 } }, ticks.map(function (d) { return h('div', { key: d, className: 'mono', style: { position: 'absolute', left: sX(d), top: 0, height: headerH, borderLeft: '1px solid var(--rd-border)', paddingLeft: 4, fontSize: 10, color: 'var(--rd-text-muted)', display: 'flex', alignItems: 'center' } }, 'д' + d); })) ), // тело h('div', { style: { position: 'relative', height: works.length * rowH } }, // выходные (затенение) weekends.map(function (d) { return h('div', { key: 'we' + d, style: { position: 'absolute', left: labelW + sX(d), top: 0, width: dayW, height: works.length * rowH, background: 'rgba(120,120,120,.06)', zIndex: 0 } }); }), // строки-лейблы works.map(function (w, i) { return h('div', { key: w.id, style: { position: 'absolute', top: i * rowH, left: 0, width: labelW + gTotalW, height: rowH, borderBottom: '1px solid var(--rd-border-soft, var(--rd-border))', background: i % 2 ? 'var(--rd-surface-2)' : 'var(--rd-surface)' } }, h('div', { onMouseEnter: function () { setHoverW(w.id); }, onMouseLeave: function () { setHoverW(null); }, style: { position: 'sticky', left: 0, width: labelW, height: '100%', background: linkFrom === w.id ? 'var(--rd-accent-soft)' : (sel.indexOf(w.id) >= 0 ? 'var(--rd-accent-soft)' : (i % 2 ? 'var(--rd-surface-2)' : 'var(--rd-surface)')), borderRight: '1px solid var(--rd-border)', padding: '0 8px 0 6px', display: 'flex', alignItems: 'center', gap: 6, zIndex: 3 } }, h('span', { onClick: function (e) { e.stopPropagation(); toggleSel(w.id); }, title: 'Выбрать для массовой операции', style: { width: 15, height: 15, flex: 'none', borderRadius: 4, border: '1.5px solid ' + (sel.indexOf(w.id) >= 0 ? 'var(--rd-accent)' : 'var(--rd-border)'), background: sel.indexOf(w.id) >= 0 ? 'var(--rd-accent)' : 'transparent', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' } }, sel.indexOf(w.id) >= 0 ? '✓' : ''), h('div', { onClick: function () { if (linkMode) pickForLink(w.id); else setWorkDrawer(w.id); }, style: { cursor: 'pointer', flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center' } }, h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, h('span', { style: { width: 7, height: 7, borderRadius: '50%', background: stColor(w), flex: 'none' } }), h('span', { style: { fontSize: 11.5, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, w.name), crit[w.id] ? h('span', { style: { fontSize: 8, fontWeight: 700, color: 'var(--rd-bad)', background: 'var(--rd-bad-soft, #fbeaea)', padding: '1px 4px', borderRadius: 3, flex: 'none' } }, 'КП') : null), h('div', { className: 'mono', style: { fontSize: 9.5, color: 'var(--rd-text-muted)', paddingLeft: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (w.deps || []).length ? (w.deps || []).map(function (d) { return d.t + (d.lag ? (d.lag > 0 ? '+' + d.lag : d.lag) : '') + '←' + d.f; }).join(' · ') : 'старт')) ) ); }), // svg-связи h('svg', { style: { position: 'absolute', left: labelW, top: 0, width: gTotalW, height: works.length * rowH, overflow: 'visible', pointerEvents: 'none', zIndex: 1 } }, links.map(function (lk, i) { return h('path', { key: i, d: lk.d, style: { fill: 'none', stroke: lk.col, strokeWidth: lk.w } }); }) ), // бары works.map(function (w, i) { var x = sX(w.start), bw = (w.end - w.start + 1) * dayW; return h('div', { key: w.id, onMouseDown: function (e) { startDrag(w, e); }, title: w.name + ' · д' + w.start + '–д' + w.end, style: { position: 'absolute', left: labelW + x, top: i * rowH + barTop, width: bw, height: barH, cursor: 'grab', background: stColor(w), borderRadius: 5, display: 'flex', alignItems: 'center', padding: '0 6px', zIndex: 2, boxShadow: '0 1px 2px rgba(0,0,0,.12)', border: crit[w.id] ? '1.5px solid var(--rd-bad)' : 'none' } }, h('span', { className: 'mono', style: { fontSize: 8.5, color: '#fff', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden' } }, 'д' + w.start + '–' + w.end), h('span', { onMouseDown: function (e) { startResize(w, e, 'left'); }, style: { position: 'absolute', left: -2, top: 0, width: 8, height: '100%', cursor: 'ew-resize' } }), h('span', { onMouseDown: function (e) { startResize(w, e, 'right'); }, style: { position: 'absolute', right: -2, top: 0, width: 8, height: '100%', cursor: 'ew-resize' } })); }), // baseline-призрак (если зафиксирован) showBase && baseRef.current ? baseRef.current.map(function (bw, idx) { var i = works.findIndex(function (w) { return w.id === bw.id; }); if (i < 0) return null; return h('div', { key: 'bl' + bw.id, title: 'baseline', style: { position: 'absolute', left: labelW + sX(bw.start), top: i * rowH + barTop - 4, width: (bw.end - bw.start + 1) * dayW, height: 3, background: 'var(--rd-text-faint)', borderRadius: 2, zIndex: 1, opacity: .7 } }); }) : null, // план/факт: фактический бар из w.af/w.ae под плановым planFact ? works.map(function (w, i) { if (w.af == null || w.ae == null) return null; var dev = w.ae - w.end; return h('div', { key: 'f' + w.id, title: 'факт д' + w.af + '–д' + w.ae + (dev ? ' (' + (dev > 0 ? '+' : '') + dev + 'д)' : ''), style: { position: 'absolute', left: labelW + sX(w.af), top: i * rowH + barTop + barH - 2, width: (w.ae - w.af + 1) * dayW, height: 5, background: dev > 0 ? 'var(--rd-bad)' : dev < 0 ? 'var(--rd-good)' : 'var(--rd-accent)', borderRadius: 3, zIndex: 3 } }); }) : null, // линия сегодня h('div', { style: { position: 'absolute', left: labelW + sX(todayD), top: 0, height: works.length * rowH, width: 2, background: 'rgba(207,59,59,.45)', zIndex: 4 } }) ), // resource-leveling баннер levelText ? h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, padding: '7px 12px', background: '#fdf6e7', borderTop: '1px solid #f0e3c0', borderBottom: '1px solid #f0e3c0', position: 'sticky', left: 0 } }, h('span', { style: { fontSize: 9.5, fontWeight: 700, color: '#9a6a06', background: '#fbeaca', padding: '2px 7px', borderRadius: 5, flex: 'none' } }, 'RESOURCE LEVELING'), h('span', { style: { fontSize: 11, color: '#7d5a13', flex: 1, lineHeight: 1.35 } }, levelText), h('button', { onClick: function () { enterScenario(); store.showToast('Выровняйте пики в режиме «что-если»', 'ok'); }, style: { flex: 'none', fontSize: 11, fontWeight: 600, color: '#9a6a06', background: '#fbeaca', border: '1px solid #ecd49a', borderRadius: 7, padding: '5px 10px', cursor: 'pointer' } }, 'Выровнять →')) : null, // ДОРОЖКА: ОПЛАТЫ layers.pay ? h('div', null, h('div', { style: laneHeadStyle }, '◆ ОПЛАТЫ · ' + payEv.length + ' · Σ ' + moneyShort(paySumVis) + ' ₽'), h('div', { style: { position: 'relative', height: 44, background: 'var(--rd-surface)' } }, weekends.map(function (d) { return h('div', { key: 'pw' + d, style: { position: 'absolute', left: labelW + sX(d), top: 0, width: dayW, height: 44, background: 'rgba(120,120,120,.06)' } }); }), payEv.map(function (e, i) { return h('div', { key: i, title: e.type + ' ' + moneyShort(e.amt) + '₽ · ' + e.sub, style: { position: 'absolute', left: labelW + sX(e.day), top: 8 + (i % 2) * 16, transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 4, padding: '2px 6px', background: e.color, color: '#fff', borderRadius: 5, fontSize: 9, fontWeight: 600, whiteSpace: 'nowrap', zIndex: 2 } }, '◆ ' + moneyShort(e.amt) + 'к'); }), h('div', { style: { position: 'absolute', left: labelW + sX(todayD), top: 0, height: 44, width: 2, background: 'rgba(207,59,59,.3)' } }))) : null, // ДОРОЖКА: ПОСТАВКИ layers.del ? h('div', null, h('div', { style: laneHeadStyle }, '▲ ПОСТАВКИ · ' + delEv.length), h('div', { style: { position: 'relative', height: 34, background: 'var(--rd-surface)' } }, delEv.map(function (e, i) { return h('div', { key: i, title: e.text + ' ' + e.sub, style: { position: 'absolute', left: labelW + sX(e.day), top: 8, transform: 'translateX(-50%)', padding: '2px 6px', background: e.color, color: '#fff', borderRadius: 5, fontSize: 9, fontWeight: 600, whiteSpace: 'nowrap' } }, '▲ ' + e.text); }), h('div', { style: { position: 'absolute', left: labelW + sX(todayD), top: 0, height: 34, width: 2, background: 'rgba(207,59,59,.3)' } }))) : null, // ДОРОЖКА: РАБОЧАЯ СИЛА (гистограмма) layers.labor ? h('div', null, h('div', { style: laneHeadStyle }, '▦ РАБОЧАЯ СИЛА · пик ' + laborPeak + ' чел'), h('div', { style: { position: 'relative', height: 50, background: 'var(--rd-surface)' } }, laborDay.map(function (pd) { return pd.tot ? h('div', { key: 'l' + pd.d, title: 'д' + pd.d + ': ' + pd.tot + ' чел', style: { position: 'absolute', left: labelW + sX(pd.d), bottom: 0, width: Math.max(1, dayW - 1), height: Math.round(pd.tot / laborPeak * 44), background: pd.tot > TRADE_CAP ? 'var(--rd-bad)' : 'var(--rd-accent)', opacity: .8 } }) : null; }), h('div', { style: { position: 'absolute', left: labelW + sX(todayD), top: 0, height: 50, width: 2, background: 'rgba(207,59,59,.3)' } }))) : null, // ДОРОЖКА: ФИНАНСИРОВАНИЕ (накопит. cash-flow) layers.cash ? h('div', null, h('div', { style: laneHeadStyle }, '⌁ ФИНАНСИРОВАНИЕ · накопл. ' + moneyShort(cashTotal) + ' ₽'), h('div', { style: { position: 'relative', height: 50, background: 'var(--rd-surface)' } }, h('svg', { style: { position: 'absolute', left: labelW, top: 0, width: gTotalW, height: 50, overflow: 'visible' } }, h('path', { d: cashCum.map(function (v, d) { return (d === 0 ? 'M' : 'L') + ' ' + sX(d) + ' ' + (46 - v / cashTotal * 42); }).join(' '), style: { fill: 'none', stroke: 'var(--rd-accent)', strokeWidth: 2 } })), h('div', { style: { position: 'absolute', left: labelW + sX(todayD), top: 0, height: 50, width: 2, background: 'rgba(207,59,59,.3)' } }))) : null ) ), sel.length ? h('div', { style: { position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 8, background: 'var(--rd-text)', color: 'var(--rd-surface)', padding: '9px 14px', borderRadius: 11, boxShadow: '0 8px 28px rgba(0,0,0,.3)', zIndex: 70 } }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, 'Выбрано: ' + sel.length), h('button', { onClick: function () { bulkShift(-1); }, style: { height: 28, padding: '0 10px', border: 'none', borderRadius: 7, background: 'rgba(255,255,255,.15)', color: '#fff', fontSize: 12, cursor: 'pointer' } }, '−1 день'), h('button', { onClick: function () { bulkShift(1); }, style: { height: 28, padding: '0 10px', border: 'none', borderRadius: 7, background: 'rgba(255,255,255,.15)', color: '#fff', fontSize: 12, cursor: 'pointer' } }, '+1 день'), h('button', { onClick: function () { bulkShift(3); }, style: { height: 28, padding: '0 10px', border: 'none', borderRadius: 7, background: 'rgba(255,255,255,.15)', color: '#fff', fontSize: 12, cursor: 'pointer' } }, '+3 дня'), h('button', { onClick: bulkBrigade, style: { height: 28, padding: '0 10px', border: 'none', borderRadius: 7, background: 'rgba(255,255,255,.15)', color: '#fff', fontSize: 12, cursor: 'pointer' } }, 'Бригада'), h('button', { onClick: clearSel, style: { height: 28, padding: '0 8px', border: 'none', background: 'none', color: 'rgba(255,255,255,.6)', fontSize: 12, cursor: 'pointer' } }, 'Снять')) : null, workDrawer ? (function () { var w = (store.raw.works || []).find(function (x) { return x.id === workDrawer; }); if (!w) return null; var brigades = store.raw.brigades || [], sections = store.raw.sections || []; var succ = (store.raw.works || []).filter(function (x) { return (x.deps || []).some(function (d) { return d.f === w.id; }); }).map(function (x) { return x.name; }); return h('div', { onClick: function () { setWorkDrawer(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: 380, 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 } }, w.name), h('div', { style: { fontSize: 11.5, color: 'var(--rd-text-muted)' } }, 'д' + w.start + '–д' + w.end + ' · ' + C.statusMeta(w.status).label + (crit[w.id] ? ' · крит. путь' : ''))), h('button', { onClick: function () { setWorkDrawer(null); }, style: { border: 'none', background: 'none', fontSize: 20, color: 'var(--rd-text-muted)', cursor: 'pointer' } }, '×')), h('div', { style: { padding: 16 } }, h('div', { style: { fontSize: 10, fontWeight: 600, color: 'var(--rd-text-muted)', marginBottom: 7, letterSpacing: .4 } }, 'ЗАВИСИМОСТИ (ПРЕДШЕСТВЕННИКИ)'), h('div', { style: { marginBottom: 14 } }, (w.deps || []).length ? (w.deps || []).map(function (d) { var p = (store.raw.works || []).find(function (x) { return x.id === d.f; }); return h('div', { key: d.f, style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 0', borderBottom: '1px solid var(--rd-border)' } }, h('span', { style: { flex: 1, fontSize: 11.5, color: 'var(--rd-text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, p ? p.name : d.f), ['FS', 'SS', 'FF'].map(function (tt) { return h('button', { key: tt, onClick: function () { setDepType(w.id, d.f, tt); }, style: { padding: '3px 6px', border: 'none', borderRadius: 5, fontSize: 9.5, fontWeight: 700, cursor: 'pointer', background: d.t === tt ? 'var(--rd-accent)' : 'var(--rd-surface-3)', color: d.t === tt ? '#fff' : 'var(--rd-text-2)' } }, tt); }), h('button', { onClick: function () { setDepLag(w.id, d.f, (d.lag || 0) - 1); }, style: { width: 20, height: 20, border: '1px solid var(--rd-border)', borderRadius: 5, background: 'var(--rd-surface)', cursor: 'pointer', fontSize: 12 } }, '−'), h('span', { className: 'mono', style: { fontSize: 11, minWidth: 26, textAlign: 'center' } }, (d.lag > 0 ? '+' : '') + (d.lag || 0) + 'д'), h('button', { onClick: function () { setDepLag(w.id, d.f, (d.lag || 0) + 1); }, style: { width: 20, height: 20, border: '1px solid var(--rd-border)', borderRadius: 5, background: 'var(--rd-surface)', cursor: 'pointer', fontSize: 12 } }, '+'), h('button', { onClick: function () { removeDep(w.id, d.f); }, title: 'Удалить связь', style: { width: 20, height: 20, border: 'none', borderRadius: 5, background: 'var(--rd-bad-soft,#fbeaea)', color: 'var(--rd-bad)', cursor: 'pointer', fontSize: 12 } }, '×')); }) : h('span', { style: { fontSize: 12, color: 'var(--rd-text-2)' } }, 'старт проекта')), h('div', { style: { fontSize: 10, fontWeight: 600, color: 'var(--rd-text-muted)', marginBottom: 7, letterSpacing: .4 } }, 'ПРЕЕМНИКИ'), h('div', { style: { fontSize: 12, color: 'var(--rd-text-2)', marginBottom: 14 } }, succ.length ? succ.join(' · ') : '—'), w.trade ? h('div', null, h('div', { style: { fontSize: 10, fontWeight: 600, color: 'var(--rd-text-muted)', marginBottom: 7, letterSpacing: .4 } }, 'БРИГАДА / СПЕЦИАЛЬНОСТЬ'), h('div', { style: { fontSize: 12, color: 'var(--rd-text-2)', marginBottom: 14 } }, w.trade + (w.crew ? ' · ' + w.crew + ' чел' : ''))) : null, h('button', { onClick: function () { toggleLock(w.id); }, style: { width: '100%', height: 36, border: '1px solid ' + (locked.indexOf(w.id) >= 0 ? 'var(--rd-accent)' : 'var(--rd-border)'), borderRadius: 8, background: locked.indexOf(w.id) >= 0 ? 'var(--rd-accent-soft)' : 'var(--rd-surface)', color: locked.indexOf(w.id) >= 0 ? 'var(--rd-accent)' : 'var(--rd-text-2)', fontSize: 12.5, fontWeight: 500, cursor: 'pointer' } }, locked.indexOf(w.id) >= 0 ? '🔒 Дата зафиксирована — разблокировать' : '🔓 Зафиксировать дату (не двигать каскадом)'), w.excl ? (function () { var alt = (store.raw.works || []).find(function (x) { return x.id === w.excl; }); return h('div', { style: { marginTop: 12, padding: '11px 12px', border: '1px dashed var(--rd-bad)', borderRadius: 8, background: 'var(--rd-bad-soft,#fbeaea)' } }, h('div', { style: { fontSize: 10, fontWeight: 700, color: 'var(--rd-bad)', marginBottom: 5, letterSpacing: .4 } }, 'XOR · ВЗАИМОИСКЛЮЧЕНИЕ — нужен один вариант'), h('div', { style: { fontSize: 11.5, color: 'var(--rd-text-2)', lineHeight: 1.4, marginBottom: 9 } }, w.rejected ? ('Сейчас выбран «' + (alt ? alt.name : '') + '». Сравните и выберите этот вместо него?') : ('Альтернатива: «' + (alt ? alt.name : '') + '». Выбран этот вариант.')), (function () { function attrs(wk) { if (wk && wk.compare) { var cp = wk.compare; return { cost: (typeof cp.cost === 'number' ? '~' + C.money0(cp.cost) + ' ₽' : (cp.cost || '—')), dur: cp.dur || '—', pros: cp.pros || '—', cons: cp.cons || '—' }; } return { cost: '—', dur: '—', pros: 'нет данных сравнения в БД', cons: '—' }; } var aThis = attrs(w), aAlt = attrs(alt); var cell = { padding: '5px 7px', fontSize: 10.5, lineHeight: 1.3, verticalAlign: 'top', borderTop: '1px solid var(--rd-border)' }; var hd = { padding: '5px 7px', fontSize: 10, fontWeight: 700, color: 'var(--rd-text-muted)', textAlign: 'left' }; return h('table', { style: { width: '100%', borderCollapse: 'collapse', marginBottom: 10, background: 'var(--rd-surface)', borderRadius: 6 } }, h('thead', null, h('tr', null, h('th', { style: hd }, ''), h('th', { style: Object.assign({}, hd, { color: 'var(--rd-accent)' }) }, '▸ ' + w.name.replace(/Тёплый пол:\s*/i, '')), h('th', { style: hd }, alt ? alt.name.replace(/Тёплый пол:\s*/i, '') : '—'))), h('tbody', null, [['Срок', aThis.dur, aAlt.dur], ['Стоимость', aThis.cost, aAlt.cost], ['Плюсы', aThis.pros, aAlt.pros], ['Минусы', aThis.cons, aAlt.cons], ['План', 'д' + w.start + '–д' + w.end, alt ? ('д' + alt.start + '–д' + alt.end) : '—']].map(function (r, i) { return h('tr', { key: i }, h('td', { style: Object.assign({}, cell, { fontWeight: 600, color: 'var(--rd-text-muted)', whiteSpace: 'nowrap' }) }, r[0]), h('td', { style: Object.assign({}, cell, { color: 'var(--rd-text)' }) }, r[1]), h('td', { style: Object.assign({}, cell, { color: 'var(--rd-text-2)' }) }, r[2])); }))); })(), h('button', { onClick: function () { chooseXor(w.id, w.excl); setWorkDrawer(null); }, style: { width: '100%', height: 32, border: 'none', borderRadius: 7, background: 'var(--rd-accent)', color: '#fff', fontSize: 12, fontWeight: 600, cursor: 'pointer', marginBottom: 6 } }, w.rejected ? '✓ Выбрать этот (' + w.name.replace(/Тёплый пол:\s*/i, '') + ')' : '✓ Оставить этот (' + w.name.replace(/Тёплый пол:\s*/i, '') + ')'), alt ? h('button', { onClick: function () { chooseXor(w.excl, w.id); setWorkDrawer(null); }, style: { width: '100%', height: 32, border: '1px solid var(--rd-accent)', borderRadius: 7, background: 'transparent', color: 'var(--rd-accent)', fontSize: 12, fontWeight: 600, cursor: 'pointer' } }, '⇄ Выбрать «' + alt.name.replace(/Тёплый пол:\s*/i, '') + '» вместо этого') : null); })() : null ) ) ); })() : null ); };