/* standards-app.jsx — the «Standarde / Стандарты» page. * * Reads data/defaults.json over GET /api/defaults, lets the manager change the * hardware and material standards, the Gola profile height and the standard * service prices, and writes them back with PUT /api/defaults (docs/api.md). * Presentation only: the shape of the standards and the merge over the built-in * fallback live in import-core.js, where they are unit tested. */ const { useState, useEffect } = React; const IC = window.ImportCore; /* ---------------- i18n ---------------- */ const I18N = { ro: { title: "Standarde", sub: "Valorile implicite pe care pagina de import le completează automat.", hwTitle: "Feronerie standard", matTitle: "Materiale standard", golaTitle: "Profil Gola", svcTitle: "Preț servicii standard", code: "Cod", name: "Denumire", clear: "Golește", balamale: "Balamale", placute: "Plăcuțe", picioruse: "Picioare", ghidaje: "Ghidaje", spate: "Spate (HDF/ДВП)", gola: "Profil Gola", golaH: "Înălțime profil (mm)", golaHHint: "Folosită la conversia m² → m.l. pe pagina de import.", montare: "Montare prefilat", livrare: "Livrare", proiectare: "Proiectare", tehnolog: "Tehnolog", transport: "Transport", svcHint: "Lasă câmpul gol dacă serviciul nu are preț standard.", save: "Salvează", saving: "Se salvează…", saved: "Standardele au fost salvate.", loading: "Se încarcă…", dbDown: "Baza de date indisponibilă — valorile afișate sunt cele implicite, salvarea nu va funcționa.", errSave: "Salvarea a eșuat", errTimeout: "Baza de date nu răspunde.", emptyHint: "Gol = nu există standard pentru această poziție.", brandTitle: "Feronerie pe brand", brandHint: "Brandul ales pe pagina de import decide ce balamale, plăcuțe și ghidaje se completează. Gol = se folosește feroneria standard de mai sus.", extraTitle: "Articole suplimentare", extraHint: "Dacă un element din proiectul PRO100 conține fragmentul, articolul intră în feronerie cu cantitatea elementului. Ghidajele («Направляющие») nu sunt afectate de aceste reguli: ele se stabilesc în cardul «Feronerie pe brand».", extraMatch: "Fragment din denumirea PRO100", extraMatchPh: "ex. Kargo", extraAdd: "Adaugă articol", extraDel: "Șterge", extraEmpty: "Nu există articole suplimentare.", colorsTitle: "Culori implicite", colorsHint: "Culoarea pe care pagina de import o pune pe rândurile de feronerie de acest tip; căutarea în bază filtrează după ea. Gol = fără culoare.", colorOther: "Alte accesorii", colorNone: "—", colorPh: "ex. Антрацит", }, en: { title: "Standards", sub: "The default values the import page fills in automatically.", hwTitle: "Standard hardware", matTitle: "Standard materials", golaTitle: "Gola profile", svcTitle: "Standard service prices", code: "Code", name: "Name", clear: "Clear", balamale: "Hinges", placute: "Mounting plates", picioruse: "Legs", ghidaje: "Slides", spate: "Back (HDF)", gola: "Gola profile", golaH: "Profile height (mm)", golaHHint: "Used for the m² → linear metre conversion on the import page.", montare: "Pre-installed mounting", livrare: "Delivery", proiectare: "Design", tehnolog: "Technologist", transport: "Transport", svcHint: "Leave a field empty when the service has no standard price.", save: "Save", saving: "Saving…", saved: "Standards saved.", loading: "Loading…", dbDown: "Database unavailable — the values shown are the built-in defaults and saving will not work.", errSave: "Save failed", errTimeout: "The database is not responding.", emptyHint: "Empty = there is no standard for this position.", brandTitle: "Hardware per brand", brandHint: "The brand chosen on the import page decides which hinges, plates and slides are prefilled. Empty = the standard hardware above is used.", extraTitle: "Extra articles", extraHint: "When an element of the PRO100 project contains the fragment, the article is added to the hardware with the element's count. Slides («Направляющие») are not affected by these rules: they are set in the «Hardware per brand» card.", extraMatch: "Fragment of the PRO100 name", extraMatchPh: "e.g. Kargo", extraAdd: "Add article", extraDel: "Delete", extraEmpty: "No extra articles yet.", colorsTitle: "Default colours", colorsHint: "The colour the import page puts on hardware rows of this kind; the base search filters by it. Empty = no colour.", colorOther: "Other accessories", colorNone: "—", colorPh: "e.g. Антрацит", }, ru: { title: "Стандарты", sub: "Значения по умолчанию, которые страница импорта подставляет автоматически.", hwTitle: "Стандартная фурнитура", matTitle: "Стандартные материалы", golaTitle: "Профиль Gola", svcTitle: "Стандартные цены услуг", code: "Код", name: "Название", clear: "Очистить", balamale: "Петли", placute: "Планки", picioruse: "Ножки", ghidaje: "Направляющие", spate: "Задняя стенка (ДВП)", gola: "Профиль Gola", golaH: "Высота профиля (мм)", golaHHint: "Используется для пересчёта м² → м.п. на странице импорта.", montare: "Сборка предустан.", livrare: "Доставка", proiectare: "Проектирование", tehnolog: "Технолог", transport: "Транспорт", svcHint: "Оставьте поле пустым, если у услуги нет стандартной цены.", save: "Сохранить", saving: "Сохранение…", saved: "Стандарты сохранены.", loading: "Загрузка…", dbDown: "База недоступна — показаны встроенные значения, сохранение не сработает.", errSave: "Не удалось сохранить", errTimeout: "База не отвечает.", emptyHint: "Пусто = стандарта для этой позиции нет.", brandTitle: "Фурнитура по брендам", brandHint: "Бренд, выбранный на странице импорта, решает, какие петли, планки и направляющие подставятся. Пусто = берётся стандартная фурнитура выше.", extraTitle: "Дополнительные артикулы", extraHint: "Если элемент проекта PRO100 содержит фрагмент, артикул попадёт в фурнитуру с количеством элемента. Элементы «Направляющие» эти правила не трогают: они задаются в карточке «Фурнитура по брендам».", extraMatch: "Фрагмент названия из PRO100", extraMatchPh: "напр. Kargo", extraAdd: "Добавить артикул", extraDel: "Удалить", extraEmpty: "Дополнительных артикулов пока нет.", colorsTitle: "Цвета по умолчанию", colorsHint: "Цвет, который страница импорта ставит строкам фурнитуры этого вида; поиск в базе фильтруется по нему. Пусто = без цвета.", colorOther: "Другая фурнитура", colorNone: "—", colorPh: "напр. Антрацит", }, }; const DB_CAT = window.DbCat; // Which picker category each standard searches. An empty string searches the // whole base: neither plates nor guides have a category of their own. const HW_FIELDS = [ { key: "balamale", category: DB_CAT.balamale }, { key: "placute", category: "" }, { key: "picioruse", category: DB_CAT.picioruse }, { key: "ghidaje", category: "" }, ]; // Kinds that differ per hardware brand (hw_brands in data/defaults.json). const BRAND_FIELDS = ["balamale", "placute", "ghidaje"]; // Same list as the import page's brand select; catalogs.js is not loaded here. const BRANDS = (window.CATALOGS && window.CATALOGS.FERONERIE_BRANDS) || ["Blum", "Hettich", "DTC"]; const EMPTY_REF = { code: "", name: "" }; /* Extra rows are keyed by a page-local id, so deleting one row does not hand its open picker to the next. The id never reaches the server. */ let extraSeq = 0; const withExtraIds = (list) => (list || []).map((x) => ({ ...x, _id: ++extraSeq })); /* Default colour per hardware kind (colors in data/defaults.json). The choices come from GET /api/colors for the category the kind's picker searches; plates follow the hinges and «other» covers everything else (IC.colorKindOf). */ const COLOR_FIELDS = [ { key: "balamale", label: "balamale", category: DB_CAT.balamale }, { key: "ghidaje", label: "ghidaje", category: "" }, { key: "gola", label: "gola", category: DB_CAT.gola }, { key: "other", label: "colorOther", category: "" }, ]; const MAT_FIELDS = [ { key: "spate", category: DB_CAT.spate }, { key: "gola", category: DB_CAT.gola }, ]; const SVC_FIELDS = [ { key: "montare", unit: "MDL/m²" }, { key: "livrare", unit: "MDL" }, { key: "proiectare", unit: "MDL" }, { key: "tehnolog", unit: "MDL/m²" }, { key: "transport", unit: "MDL" }, ]; /* One standard: code, name, a button that fills them from the base and a button that clears them. An empty pair is a valid standard — it means "no standard". */ function StandardRow({ t, lang, label, category, brand, fallbackCategory, value, onChange }) { const [modalOpen, setModalOpen] = useState(false); return (