@@ -0,0 +1,252 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
let observer = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function planDate() {
|
||||
return (
|
||||
global.WespDailyPlanPanel?.getSelectedPlanDate?.() ||
|
||||
global.WespDailyPlanSkipDuration?.todayIso?.() ||
|
||||
new Date().toISOString().slice(0, 10)
|
||||
);
|
||||
}
|
||||
|
||||
function getRecipeId() {
|
||||
return (
|
||||
document.getElementById("recipeEdit")?.dataset?.recipeId ||
|
||||
document.querySelector(".list-item.active[data-recipe-id]")?.dataset?.recipeId ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function notifySuccess(message) {
|
||||
global.WespZootechNotify?.createAdapter?.()?.success?.(message);
|
||||
}
|
||||
|
||||
function notifyError(message, fallback) {
|
||||
global.WespZootechNotify?.createAdapter?.()?.error?.(message, { fallback: fallback || message });
|
||||
}
|
||||
|
||||
async function reloadEditor() {
|
||||
const recipeId = getRecipeId();
|
||||
if (!recipeId) return;
|
||||
if (typeof global.reloadRecipeEditor === "function") {
|
||||
await global.reloadRecipeEditor(recipeId);
|
||||
} else if (typeof global.loadRecipeDetailsWithPlanOverlay === "function") {
|
||||
await global.loadRecipeDetailsWithPlanOverlay();
|
||||
} else if (typeof global.loadRecipeDetails === "function") {
|
||||
await global.loadRecipeDetails(recipeId);
|
||||
}
|
||||
}
|
||||
|
||||
function planBtnHtml(action, title, icon, attrs = "") {
|
||||
return (
|
||||
`<button type="button" class="btn btn-outline-secondary btn-sm recipe-plan-action-btn" ` +
|
||||
`data-action="${action}" title="${escapeHtml(title)}" ${attrs}>` +
|
||||
`<i class="fas ${icon}"></i></button>`
|
||||
);
|
||||
}
|
||||
|
||||
function decorateRow(row) {
|
||||
const actions = row.querySelector(".recipe-table-row-actions");
|
||||
if (!actions || actions.querySelector(".recipe-plan-actions")) return;
|
||||
|
||||
const ingredientId = row.dataset.id;
|
||||
if (!ingredientId) return;
|
||||
|
||||
const skipped = row.classList.contains("ingredient-row--skipped");
|
||||
const replaced = row.classList.contains("ingredient-row--replaced");
|
||||
const select = row.querySelector(".ingredient-select");
|
||||
const componentId = select?.value || "";
|
||||
const label = select?.selectedOptions?.[0]?.textContent?.trim() || "Компонент";
|
||||
const recipeId = getRecipeId();
|
||||
if (!recipeId) return;
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "btn-group recipe-plan-actions me-1";
|
||||
wrap.setAttribute("role", "group");
|
||||
|
||||
if (skipped) {
|
||||
wrap.innerHTML = planBtnHtml(
|
||||
"recipe-plan-unskip-ingredient",
|
||||
"Вернуть в план",
|
||||
"fa-undo",
|
||||
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ingredientId)}"`
|
||||
);
|
||||
} else if (replaced) {
|
||||
wrap.innerHTML = planBtnHtml(
|
||||
"recipe-plan-undo-replace-ingredient",
|
||||
"Отменить замену",
|
||||
"fa-undo",
|
||||
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ingredientId)}"`
|
||||
);
|
||||
} else {
|
||||
wrap.innerHTML =
|
||||
planBtnHtml(
|
||||
"recipe-plan-skip-ingredient",
|
||||
"Убрать из плана",
|
||||
"fa-ban",
|
||||
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ingredientId)}" data-label="${escapeHtml(label)}"`
|
||||
) +
|
||||
(componentId
|
||||
? planBtnHtml(
|
||||
"recipe-plan-replace-ingredient",
|
||||
"Заменить в плане",
|
||||
"fa-exchange-alt",
|
||||
`data-recipe-id="${escapeHtml(recipeId)}" data-ingredient-id="${escapeHtml(ingredientId)}" ` +
|
||||
`data-component-id="${escapeHtml(componentId)}" data-label="${escapeHtml(label)}"`
|
||||
)
|
||||
: "");
|
||||
}
|
||||
|
||||
const deleteBtn = actions.querySelector('[data-action="remove-ingredient"]');
|
||||
if (deleteBtn) actions.insertBefore(wrap, deleteBtn);
|
||||
else actions.appendChild(wrap);
|
||||
}
|
||||
|
||||
function decorateRows() {
|
||||
document.querySelectorAll("#ingredientsTableBody .ingredient-row").forEach(decorateRow);
|
||||
}
|
||||
|
||||
function ensureStyles() {
|
||||
if (document.getElementById("wesp-lab-plan-overlay-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "wesp-lab-plan-overlay-styles";
|
||||
style.textContent =
|
||||
".recipe-plan-action-btn{padding:2px 6px;}" +
|
||||
".recipe-plan-actions .btn+.btn{margin-left:2px;}";
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async function skipIngredient(recipeId, ingredientId, label) {
|
||||
const skip = global.WespDailyPlanSkipDuration;
|
||||
if (!skip?.skipDurationBody) return;
|
||||
const body = await skip.skipDurationBody(planDate(), { recipeId, ingredientId }, "На какой срок убрать компонент?");
|
||||
if (!body) return;
|
||||
const resp = await fetch("/api/daily-plan/skips/ingredients", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.message || "Не удалось убрать компонент");
|
||||
}
|
||||
await reloadEditor();
|
||||
notifySuccess(`«${label}» убран из плана`);
|
||||
}
|
||||
|
||||
async function unskipIngredient(recipeId, ingredientId) {
|
||||
const params = new URLSearchParams({ recipe_id: recipeId, ingredient_id: ingredientId, date: planDate() });
|
||||
const resp = await fetch(`/api/daily-plan/skips/ingredients?${params}`, {
|
||||
method: "DELETE",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.message || "Не удалось вернуть компонент");
|
||||
}
|
||||
await reloadEditor();
|
||||
notifySuccess("Компонент снова в плане");
|
||||
}
|
||||
|
||||
async function openReplace(recipeId, ingredientId, componentId, label) {
|
||||
const replaceModal = global.WespDailyPlanReplaceModal;
|
||||
if (!replaceModal?.open) {
|
||||
notifyError("Модалка замены недоступна");
|
||||
return;
|
||||
}
|
||||
await replaceModal.open({
|
||||
currentName: label,
|
||||
onPick: async (replacementComponentId, replacementName) => {
|
||||
const skip = global.WespDailyPlanSkipDuration;
|
||||
const body = await skip?.skipDurationBody?.(
|
||||
planDate(),
|
||||
{ recipeId, ingredientId, replacementComponentId },
|
||||
"На какой срок заменить компонент?"
|
||||
);
|
||||
if (!body) return;
|
||||
const resp = await fetch("/api/daily-plan/replacements/ingredients", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.message || "Не удалось заменить компонент");
|
||||
}
|
||||
await reloadEditor();
|
||||
notifySuccess(`Компонент заменён на «${replacementName}»`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function undoReplace(recipeId, ingredientId) {
|
||||
const params = new URLSearchParams({ recipe_id: recipeId, ingredient_id: ingredientId, date: planDate() });
|
||||
const resp = await fetch(`/api/daily-plan/replacements/ingredients?${params}`, {
|
||||
method: "DELETE",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.message || "Не удалось отменить замену");
|
||||
}
|
||||
await reloadEditor();
|
||||
notifySuccess("Замена отменена");
|
||||
}
|
||||
|
||||
document.addEventListener("click", async (ev) => {
|
||||
const btn = ev.target.closest("[data-action]");
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
if (!action?.startsWith("recipe-plan-")) return;
|
||||
if (!document.getElementById("recipeEdit")?.contains(btn)) return;
|
||||
ev.preventDefault();
|
||||
|
||||
const recipeId = btn.dataset.recipeId;
|
||||
const ingredientId = btn.dataset.ingredientId;
|
||||
try {
|
||||
if (action === "recipe-plan-skip-ingredient") {
|
||||
await skipIngredient(recipeId, ingredientId, btn.dataset.label || "Компонент");
|
||||
} else if (action === "recipe-plan-unskip-ingredient") {
|
||||
await unskipIngredient(recipeId, ingredientId);
|
||||
} else if (action === "recipe-plan-replace-ingredient") {
|
||||
await openReplace(recipeId, ingredientId, btn.dataset.componentId, btn.dataset.label || "Компонент");
|
||||
} else if (action === "recipe-plan-undo-replace-ingredient") {
|
||||
await undoReplace(recipeId, ingredientId);
|
||||
}
|
||||
} catch (e) {
|
||||
notifyError(e.message, "Ошибка плана");
|
||||
}
|
||||
});
|
||||
|
||||
function watchIngredients() {
|
||||
const tbody = document.getElementById("ingredientsTableBody");
|
||||
if (!tbody || observer) return;
|
||||
observer = new MutationObserver(() => decorateRows());
|
||||
observer.observe(tbody, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
global.WespLabRecipesPlanOverlay = {
|
||||
init() {
|
||||
ensureStyles();
|
||||
watchIngredients();
|
||||
decorateRows();
|
||||
},
|
||||
decorateRows,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => global.WespLabRecipesPlanOverlay.init());
|
||||
} else {
|
||||
global.WespLabRecipesPlanOverlay.init();
|
||||
}
|
||||
})(window);
|
||||
Reference in New Issue
Block a user