Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,185 @@
/**
* Модалка выбора компонента-замены (похожие + поиск), как в плане на день.
*/
(function (global) {
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 `<p class="zt-k-hub-plan-replace__empty">${escapeHtml(emptyText)}</p>`;
}
return items
.map(
(item) =>
`<button type="button" class="zt-k-hub-plan-replace__pick" data-pick-component-id="${escapeHtml(item.id)}" ` +
`data-pick-component-name="${escapeHtml(item.name)}">` +
`<span class="zt-k-hub-plan-replace__pick-name">${escapeHtml(item.name)}</span>` +
`<span class="zt-k-hub-plan-replace__pick-meta">` +
`${escapeHtml(item.type || "—")} · СВ ${formatKg(item.dryMatter)}%</span></button>`
)
.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<void>} 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
? `<p class="zt-k-hub-plan-replace__lead">${escapeHtml(subtitle)}</p>`
: "";
overlay.innerHTML =
'<div class="zt-k-hub-plan-subdialog zt-k-hub-plan-replace__dialog" role="dialog" aria-modal="true">' +
`<h3 class="zt-k-hub-plan-subdialog__title zt-k-hub-plan-replace__title">Заменить «${escapeHtml(label)}»</h3>` +
subtitleHtml +
'<label class="zt-k-hub-plan-replace__search-label">Поиск по всем компонентам</label>' +
'<input type="search" class="form-control zt-k-hub-plan-replace__search" data-replace-search placeholder="Начните вводить название…" autocomplete="off">' +
'<div class="zt-k-hub-plan-replace__section">' +
'<p class="zt-k-hub-plan-replace__section-title">Похожие по параметрам</p>' +
'<div data-replace-similar class="zt-k-hub-plan-replace__list"><div class="zt-k-hub-plan-replace__empty">Загрузка…</div></div></div>' +
'<div class="zt-k-hub-plan-replace__section">' +
'<p class="zt-k-hub-plan-replace__section-title">Все компоненты</p>' +
'<div data-replace-results class="zt-k-hub-plan-replace__list"><div class="zt-k-hub-plan-replace__empty">Загрузка…</div></div></div>' +
'<div class="zt-k-hub-plan-subdialog__actions zt-k-hub-plan-replace__actions">' +
'<button type="button" class="btn btn-secondary" data-replace-cancel>Отмена</button></div></div>';
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);