/** * Модалка выбора компонента-замены (похожие + поиск), как в плане на день. */ (function (global) { function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function formatKg(value) { const n = Number(value); if (!Number.isFinite(n)) return "—"; const rounded = Math.round(n * 100) / 100; return String(rounded).replace(/\.?0+$/, ""); } function renderComponentPickList(items, emptyText) { if (!items.length) { return `

${escapeHtml(emptyText)}

`; } return items .map( (item) => `` ) .join(""); } async function fetchComponentAlternatives(componentId, query) { const params = new URLSearchParams({ component_id: componentId, q: query || "", limit: "100", }); const resp = await fetch(`/api/daily-plan/component-alternatives?${params.toString()}`, { headers: { Accept: "application/json" }, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.message || "Не удалось загрузить компоненты"); } return resp.json(); } function dismissOverlays(host, keepSelectors) { host.querySelectorAll(".zt-k-hub-plan-overlay").forEach((el) => { if (keepSelectors.some((sel) => el.matches(sel))) return; el.remove(); }); } /** * @param {object} options * @param {string} options.componentId * @param {string} options.label * @param {HTMLElement} [options.overlayHost] * @param {string[]} [options.keepOverlays] — селекторы overlay, которые не закрывать * @param {string} [options.subtitle] * @param {(replacementComponentId: string, replacementName: string) => void|Promise} options.onPick * @param {(message: string) => void} [options.onError] */ async function openReplaceComponentModal(options) { const { componentId, label, overlayHost = document.body, keepOverlays = [], subtitle = "", onPick, onError = () => {}, } = options || {}; if (!componentId || !onPick) return; const overlay = document.createElement("div"); overlay.className = "zt-k-hub-plan-overlay zt-k-hub-plan-replace"; const subtitleHtml = subtitle ? `

${escapeHtml(subtitle)}

` : ""; overlay.innerHTML = ''; dismissOverlays(overlayHost, keepOverlays); overlay.setAttribute("data-daily-plan-overlay", "1"); overlayHost.appendChild(overlay); const similarEl = overlay.querySelector("[data-replace-similar]"); const resultsEl = overlay.querySelector("[data-replace-results]"); const searchEl = overlay.querySelector("[data-replace-search]"); let catalog = null; const close = () => { if (overlay.isConnected) overlay.remove(); }; function filterCatalog(query) { const q = query.trim().toLowerCase(); if (!q) { return { similar: catalog?.similar || [], items: catalog?.results || [], q: "", }; } const items = (catalog?.results || []).filter((item) => String(item.name || "").toLowerCase().includes(q) ); const similar = items.filter((item) => item.similar); return { similar, items, q }; } function paintLists(query = "") { if (!overlay.isConnected) return; const { similar, items, q } = filterCatalog(query); if (similarEl) { similarEl.innerHTML = renderComponentPickList( similar, q ? "Нет похожих по запросу" : "Нет похожих компонентов" ); } if (resultsEl) { resultsEl.innerHTML = renderComponentPickList( items, q ? "Ничего не найдено" : "Нет доступных компонентов" ); } } async function loadCatalog() { try { catalog = await fetchComponentAlternatives(componentId, ""); paintLists(searchEl?.value.trim() || ""); } catch (err) { if (!overlay.isConnected) return; onError(err.message || "Не удалось загрузить компоненты"); } } overlay.querySelector("[data-replace-cancel]")?.addEventListener("click", close); overlay.addEventListener("click", (event) => { if (event.target === overlay) close(); }); searchEl?.addEventListener("input", () => paintLists(searchEl.value.trim())); overlay.addEventListener("click", async (event) => { const pick = event.target.closest("[data-pick-component-id]"); if (!pick) return; event.preventDefault(); const replacementComponentId = pick.dataset.pickComponentId; const replacementName = pick.dataset.pickComponentName || "Компонент"; if (!replacementComponentId) return; close(); try { await onPick(replacementComponentId, replacementName); } catch (err) { onError(err.message || "Не удалось заменить компонент"); } }); await loadCatalog(); searchEl?.focus(); } global.WespDailyPlanReplaceModal = { open: openReplaceComponentModal, fetchComponentAlternatives, }; })(window);