// ============================================================================ // Данные экрана «Цех» — план цеха + двух-фазный учёт перемещений ПФ между // участками. Прототип, синхронизация с реальными PROCESSES / NOMENCLATURE // будет позже (Этап B). // // Источник истины процессов: // ~/.claude/projects/.../memory/manufacturing_path.md — сводная карта // .tmp/manufacturing_flow.md — версия для git'а (после переезда в docs/) // .tmp/manufacturing_path_questions.md — открытые вопросы // ============================================================================ // Все участки и подэтапы цеха, в которых может «жить» позиция. const CECH_STAGES = { tpa: { name: 'ТПА', short: 'ТПА', tone: 'mold' }, lak: { name: 'Лакировка', short: 'лак', tone: 'coat' }, metal: { name: 'Металлизация', short: 'металл.', tone: 'metal' }, al_cast: { name: 'Литьё AL', short: 'AL · литьё', tone: 'cast' }, al_mech: { name: 'Мех. обработка', short: 'AL · мех.', tone: 'cast' }, al_gult: { name: 'Голтовка', short: 'AL · голт.', tone: 'cast' }, buffer: { name: 'Буфер производства', short: 'буфер', tone: 'buffer' }, asm: { name: 'Сборка', short: 'сборка', tone: 'asm' }, // Сторонняя обработка (round-trip): деталь уезжает к подрядчику с одного // из обработочных этапов (лак/металл/голтовка) и ВОЗВРАЩАЕТСЯ обратно в // цех (буфер/сборка/склад). Не финальная зона, не обработочный этап // завода — промежуточный mid-route stage. Граф разрешённых переходов // (туда и обратно) — в cech_route_editor.jsx::RE_DEFAULT_GRAPH. outsource: { name: 'Сторонняя обработка', short: 'аутсорс', tone: 'outsource' }, sklad2: { name: 'Склад №2', short: 'склад №2', tone: 'w2' }, sklad3: { name: 'Склад №3', short: 'склад №3', tone: 'w2' }, }; // Маппинг слов маршрута из спеки (поле BomRow.route, например // «ТПА → Лакировка → Металлизация → Склад №2») в ключи CECH_STAGES. // Ключи нормализуются: lower-case + схлопывание пробелов, чтобы // «Литьё AL» / «литьё al» / «Литье AL» давали один и тот же результат. // «Закупка» намеренно НЕ маппится — покупное в цех не идёт (см. routeToStages). const ROUTE_WORD_TO_STAGE = { 'тпа': 'tpa', 'лакировка': 'lak', 'лак': 'lak', 'металлизация': 'metal', 'металл': 'metal', 'метал': 'metal', // Канон этапа металлизации — «Металлизация» (выше). Синонимы «ВАК/вакуум…» // убраны 2026-06-25 как мёртвые: ни один маршрут спек их не использовал // (проверено на живой БД — 0 узлов), а сам термин путал отчёты. Если на // заводе вернётся написание «ВАК» в маршруте — добавить алиас обратно сюда. 'литье al': 'al_cast', 'литьё al': 'al_cast', 'al литье': 'al_cast', 'al литьё': 'al_cast', 'литье': 'al_cast', 'литьё': 'al_cast', 'мех обр': 'al_mech', 'мех обработка': 'al_mech', 'мех': 'al_mech', 'механообработка':'al_mech', 'голтовка': 'al_gult', 'галтовка': 'al_gult', 'буфер': 'buffer', 'буфер производства': 'buffer', 'сборка': 'asm', 'аутсорс': 'outsource', 'outsource': 'outsource', 'сторонняя обработка':'outsource', 'сторонняя': 'outsource', 'кооперация': 'outsource', 'субподряд': 'outsource', 'склад 2': 'sklad2', 'склад №2': 'sklad2', 'склад 3': 'sklad3', 'склад №3': 'sklad3', }; // Нормализация одного слова маршрута: lower-case, убрать точки, // схлопнуть пробелы. «Мех.обр» → «мех обр»; «Склад №2» оставляем № // (он есть в ключах), но «Склад N2»/«склад 2» тоже сводятся. function _normRouteWord(word) { if (!word) return ''; return String(word) .toLowerCase() .replace(/\./g, ' ') // «мех.обр» → «мех обр» .replace(/\s+/g, ' ') // схлопнуть пробелы .trim(); } // routeToStages(routeStr): парсит строковый маршрут спеки в УПОРЯДОЧЕННЫЙ // массив ключей этапов цеха. Разделители — стрелки (→, ->, =>) либо «/». // 'ТПА → Склад №2' → ['tpa','sklad2'] // 'Литьё AL → Мех.обр → Голтовка → Лак → Метал → Склад №2' // → ['al_cast','al_mech','al_gult','lak','metal','sklad2'] // 'Закупка' → [] (покупное, не цех) // Неизвестные слова пропускаются (не ломают парс). Дубли подряд схлопываются. // Возвращает [] для пустого/нераспознанного маршрута — вызывающая сторона // трактует это как «маршрут не задан» и сохраняет старое поведение. function routeToStages(routeStr) { if (!routeStr || typeof routeStr !== 'string') return []; const parts = routeStr.split(/→|->|=>|\/|⟶|➝|➔/); const out = []; for (const raw of parts) { const key = _normRouteWord(raw); if (!key) continue; if (key === 'закупка' || key === 'покупное' || key === 'покупка') continue; const stage = ROUTE_WORD_TO_STAGE[key]; if (!stage) continue; // незнакомое слово — пропускаем if (out.length > 0 && out[out.length - 1] === stage) continue; // дубль подряд out.push(stage); } return out; } // resolveItemRoute(opts): достаёт строковый маршрут для запускаемой партии // из доступных источников, в порядке приоритета. Возвращает строку route // (или null если нигде не нашлось). Парсинг в массив этапов — отдельно, // через routeToStages. Источники (первый непустой выигрывает): // 1) opts.routeStr — явная строка (если вызывающий уже её знает) // 2) opts.bomRow.route — поле строки BOM (основной кейс: FG-PF, где // каждая internal-строка несёт свой маршрут) // 3) opts.item.route — поле самой NOMENCLATURE-позиции ПФ // 4) spec[sku].route — маршрут на уровне объекта-спеки ПФ // 5) первая internal-строка spec[sku].bom с непустым route // Чисто read-only по отношению к данным. function resolveItemRoute(opts) { opts = opts || {}; if (typeof opts.routeStr === 'string' && opts.routeStr.trim()) return opts.routeStr; if (opts.bomRow && typeof opts.bomRow.route === 'string' && opts.bomRow.route.trim()) { return opts.bomRow.route; } if (opts.item && typeof opts.item.route === 'string' && opts.item.route.trim()) { return opts.item.route; } const sku = opts.sku || (opts.item && opts.item.sku) || null; if (sku && typeof window !== 'undefined') { const spec = (window.SPECIFICATIONS || {})[sku]; if (spec && typeof spec === 'object' && !Array.isArray(spec)) { if (typeof spec.route === 'string' && spec.route.trim()) return spec.route; } // первая internal-строка спеки с маршрутом const tree = (typeof window.getSpecBom === 'function') ? window.getSpecBom(sku) : (Array.isArray(spec) ? spec : (spec && spec.bom) || []); const flat = []; const walk = (rows) => { for (const r of (rows || [])) { if (!r) continue; flat.push(r); if (Array.isArray(r.children)) walk(r.children); } }; walk(tree); const withRoute = flat.find(r => typeof r.route === 'string' && r.route.trim()); if (withRoute) return withRoute.route; } return null; } // Стартовый реестр позиций в Цехе — пустой. Партии материализуются здесь // только когда пользователь явно запускает их через «Запуск в производство» // (см. v1/screens/launch_flow.jsx → window.CECH_STATE.items.push). До этого // Цех пустой, никаких demo-табличек. const CECH_INITIAL_ITEMS = []; // Размер партии по умолчанию для перемещения между участками. const CECH_BATCH = 500; // Стартовое состояние Цеха — пустое. items[] наполняется только запусками // партий через launch_flow.jsx (window.CECH_STATE.items.push + диспатч // 'cech:state-changed') и/или backend-snapshot (manufacturingBatches + // cechTransfers, см. data_server_sync.jsx::_applyServerSnapshot). function makeCechInitialState() { return { items: [], shipmentSeq: 200 }; } // routeProgress(item, currentStage): где находится партия на своём // маршруте. Возвращает { idx, total, next, done } или null если у item // нет привязанного маршрута (item.route пустой/отсутствует). // idx — индекс currentStage в маршруте (0-based), -1 если не на маршруте // total — длина маршрута // next — ключ следующего этапа по маршруту (null если currentStage финальный // или не на маршруте) // done — true если currentStage — последний этап маршрута // Чисто read-only, ничего не мутирует. Цех использует для подсветки // «следующего ожидаемого этапа» и индикатора прогресса. function routeProgress(item, currentStage) { const route = item && Array.isArray(item.route) ? item.route : null; if (!route || route.length === 0) return null; const idx = route.indexOf(currentStage); const total = route.length; if (idx < 0) return { idx: -1, total, next: null, done: false }; const next = idx < total - 1 ? route[idx + 1] : null; return { idx, total, next, done: idx === total - 1 }; } // nextRouteStage(item, fromStage): следующий по маршруту этап ИЗ fromStage, // или null. Тонкая обёртка над routeProgress — удобно для подсветки drop-зоны. function nextRouteStage(item, fromStage) { const p = routeProgress(item, fromStage); return p ? p.next : null; } Object.assign(window, { CECH_STAGES, CECH_INITIAL_ITEMS, CECH_BATCH, makeCechInitialState, ROUTE_WORD_TO_STAGE, routeToStages, routeProgress, nextRouteStage, resolveItemRoute, });