253 lines
9.2 KiB
JavaScript
253 lines
9.2 KiB
JavaScript
(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);
|