/* Общий стор: гидрация из RenoDB + авто-персист + производные. A1/A2: dataRef (актуальное состояние), функциональные set, replaceEntity. */ window.RD = window.RD || { core: {}, modules: {} }; (function (C) { var Ctx = React.createContext(null); C.StoreContext = Ctx; C.useStore = function () { return React.useContext(Ctx); }; C.StoreProvider = function (props) { var init = { loading: true, role: 'Администратор', user: null, view: 'estimate', theme: 'light', data: {}, toast: null }; var ref = React.useReducer(function (s, a) { return Object.assign({}, s, a); }, init); var state = ref[0], set = ref[1]; var dataRef = React.useRef(state.data); // A2: всегда актуальные данные dataRef.current = state.data; var saveTimer = React.useRef(null); var hist = React.useRef([]); var redo = React.useRef([]); var toastTimer = React.useRef(null); function showToast(text, kind) { set({ toast: { text: text, kind: kind || 'ok' } }); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(function () { set({ toast: null }); }, 3400); } function pushHist() { hist.current.push(JSON.stringify(dataRef.current)); if (hist.current.length > 40) hist.current.shift(); redo.current = []; } function undo() { if (!hist.current.length) { showToast('История пуста', 'warn'); return; } redo.current.push(JSON.stringify(dataRef.current)); var d = JSON.parse(hist.current.pop()); dataRef.current = d; set({ data: d }); showToast('Отменено', 'ok'); } function redoFn() { if (!redo.current.length) { showToast('Нечего вернуть', 'warn'); return; } hist.current.push(JSON.stringify(dataRef.current)); var d = JSON.parse(redo.current.pop()); dataRef.current = d; set({ data: d }); showToast('Возвращено', 'ok'); } React.useEffect(function () { RenoDB.getProject().then(function (d) { // Normalize works deps if (Array.isArray(d.works)) d.works = d.works.map(function (w) { return Object.assign({}, w, { deps: C.normalizeDeps(w.deps) }); }); // FIX #1: Bind spec_items to sections.rows (currently sections.rows are empty) if (Array.isArray(d.sections) && Array.isArray(d.spec_items)) { d.sections = d.sections.map(function (s) { var sectionItems = d.spec_items.filter(function (item) { // Group spec_items by category → section mapping var catMap = { 'Оборудование': 's3', 'Материалы': 's3', 'Работы': 's3' }; return catMap[item.category] === s.id; }).map(function (item, idx) { // Transform spec_item to estimate row format return { id: item.id, kind: 'material', name: item.name + (item.brand ? ' (' + item.brand + ')' : ''), unit: item.unit || 'шт', qty: item.qty || 0, price: 0, // spec_items don't have price, will be 0 cost: 0, status: item.status || 'planned', pct: 0, rejected: false, note: item.note || '' }; }); return Object.assign({}, s, { rows: sectionItems.length > 0 ? sectionItems : s.rows || [] }); }); } // FIX #2: Create aliases for legacy field names used by Overview.jsx d.tasks = d.works || []; d.smeta = []; if (Array.isArray(d.sections)) { d.sections.forEach(function (s) { (s.rows || []).forEach(function (r) { d.smeta.push(r); }); }); } d.supplies = d.deliveries || []; dataRef.current = d; set({ loading: false, data: d }); }); }, []); // A1: единый метод замены сущности целиком (для каскада/групп) function replaceEntity(entity, list, persistIds) { pushHist(); var nd = Object.assign({}, dataRef.current); nd[entity] = list; dataRef.current = nd; set({ data: nd }); var ids = persistIds || list.map(function (x) { return x.id; }); clearTimeout(saveTimer.current); saveTimer.current = setTimeout(function () { ids.forEach(function (id) { var row = list.find(function (x) { return x.id === id; }); if (row) RenoDB.update(entity, id, row); }); }, 800); } // правка одной сущности function update(entity, id, patch) { pushHist(); var d = dataRef.current; var list = d[entity]; if (Array.isArray(list)) { var n = list.map(function (x) { return x.id === id ? Object.assign({}, x, patch) : x; }); var nd = Object.assign({}, d); nd[entity] = n; dataRef.current = nd; set({ data: nd }); } clearTimeout(saveTimer.current); saveTimer.current = setTimeout(function () { RenoDB.update(entity, id, patch); }, 800); } function createEntity(entity, obj) { pushHist(); var d = dataRef.current; var list = (d[entity] || []).slice(); list.push(obj); var nd = Object.assign({}, d); nd[entity] = list; dataRef.current = nd; set({ data: nd }); RenoDB.create(entity, obj); } function setView(v) { set({ view: v }); } function setTheme(t) { set({ theme: t }); document.body.setAttribute('data-theme', t); try { localStorage.setItem('rd_theme', t); } catch (e) {} } function setRole(r) { set({ role: r }); try { localStorage.setItem('rd_role', r); } catch (e) {} } function login(u) { set({ user: u.name, role: u.role }); try { localStorage.setItem('rd_role', u.role); localStorage.setItem('rd_user', u.name); } catch (e) {} } function logout() { set({ user: null }); try { localStorage.removeItem('rd_user'); } catch (e) {} } var cfg = state.data.config || {}; var api = { state: state, raw: state.data, config: cfg, loading: state.loading, perms: C.perms(state.role, cfg), vatRate: C.vatRate(cfg), update: update, create: createEntity, replaceEntity: replaceEntity, setView: setView, setTheme: setTheme, setRole: setRole, login: login, logout: logout, toast: state.toast, showToast: showToast, undo: undo, redo: redoFn, pushHist: pushHist }; return React.createElement(Ctx.Provider, { value: api }, props.children); }; })(window.RD.core);