Files
site/WESP_REL/static/js/pages/recipes-operations-controller.js
2026-07-17 12:57:18 +03:00

337 lines
12 KiB
JavaScript

export function createRecipesOperationsController({
notyf,
getSelectedRecipe,
setSelectedRecipe,
getSelectedPeriod,
setSelectedPeriod,
getSelectedDispenser,
getCurrentDispenserType,
showRecipeEdit,
loadRecipes,
loadPeriods,
ensureComponentsLoaded,
setUnloadingLinkBroken,
initializeUnloadingLinkButton,
updateTripPercentFieldState,
setDryMatterMode,
loadIngredients,
loadUnloadingGroups,
recalculateWeights,
updateTotalValues,
}) {
let copiedRecipeData = null;
let moveRecipeInFlight = false;
async function deleteRecipe(recipeId) {
const periodId = getSelectedPeriod();
const dispenserId = getSelectedDispenser();
const isMill =
typeof getCurrentDispenserType === "function" && getCurrentDispenserType() === "mill";
/** В контексте периода кормораздатчика удаляем только связь с периодом — иначе DELETE /api/recipes глобально убирает рецепт со всех оборудований. */
const unlinkFromPeriodOnly = Boolean(periodId && dispenserId && !isMill);
const msg = unlinkFromPeriodOnly
? "Удалить этот рейс из выбранного периода? (Рецепт останется в базе, если используется ещё где-то.)"
: "Вы уверены, что хотите удалить этот рецепт? Он будет удалён для всех привязок.";
const ok = globalThis.WespKioskDialog
? await globalThis.WespKioskDialog.confirm(msg)
: globalThis.confirm(msg);
if (!ok) return;
try {
const url = unlinkFromPeriodOnly
? `/api/feed_dispensers/${encodeURIComponent(dispenserId)}/periods/${encodeURIComponent(periodId)}/recipes/${encodeURIComponent(recipeId)}`
: `/api/recipes/${encodeURIComponent(recipeId)}`;
const response = await fetch(url, { method: "DELETE" });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Ошибка при удалении рейса");
}
const result = await response.json().catch(() => ({}));
notyf.success(result.message || (unlinkFromPeriodOnly ? "Рейс удалён из периода" : "Рейс удален"));
if (String(getSelectedRecipe() ?? "") === String(recipeId)) {
setSelectedRecipe(null);
showRecipeEdit(false);
}
if (getSelectedPeriod()) {
await loadRecipes(getSelectedPeriod());
}
if (getSelectedDispenser()) {
await loadPeriods(getSelectedDispenser());
}
} catch (error) {
console.error("Ошибка при удалении рейса:", error);
notyf.error(error.message, { fallback: "Ошибка при удалении рейса" });
}
}
async function moveRecipe(recipeId, fromIndex, toIndex) {
const selectedPeriod = getSelectedPeriod();
if (!selectedPeriod || fromIndex === toIndex) return;
if (
typeof document !== "undefined" &&
document.body.classList.contains("recipe-period-transfer-active")
) {
notyf.error(
"Сначала завершите перенос в другой период (кнопка «Вставить здесь») или отмените его (Escape, отпустите вне списка)."
);
return;
}
if (moveRecipeInFlight) return;
if (!applyOptimisticRecipeReorder(fromIndex, toIndex)) {
return;
}
moveRecipeInFlight = true;
try {
const response = await fetch(`/api/recipes/${encodeURIComponent(recipeId)}/move`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from_index: fromIndex, to_index: toIndex, period_id: selectedPeriod }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
await loadRecipes(selectedPeriod, { skipRecipesSkeleton: true });
throw new Error(errorData.message || "Ошибка при перемещении рецепта");
}
const listEl = document.getElementById("recipesList");
if (listEl) {
reindexRecipeListItems(listEl);
}
} catch (error) {
console.error("Ошибка при перемещении рейса:", error);
notyf.error(error.message, { fallback: "Ошибка при перемещении рейса" });
} finally {
moveRecipeInFlight = false;
}
}
function reindexRecipeListItems(listEl) {
const items = listEl.querySelectorAll(":scope > .list-item[data-recipe-id]");
items.forEach((item, index) => {
item.dataset.index = String(index);
item.querySelectorAll("[data-index]").forEach((el) => {
el.dataset.index = String(index);
});
const upBtn = item.querySelector('[data-action="move-recipe-up"]');
const downBtn = item.querySelector('[data-action="move-recipe-down"]');
if (upBtn) upBtn.disabled = index === 0;
if (downBtn) downBtn.disabled = index === items.length - 1;
});
}
function applyOptimisticRecipeReorder(fromIndex, toIndex) {
const listEl = document.getElementById("recipesList");
if (!listEl) return false;
const items = [...listEl.querySelectorAll(":scope > .list-item[data-recipe-id]")];
if (
fromIndex < 0 ||
toIndex < 0 ||
fromIndex >= items.length ||
toIndex >= items.length ||
fromIndex === toIndex
) {
return false;
}
const [moved] = items.splice(fromIndex, 1);
items.splice(toIndex, 0, moved);
items.forEach((item) => listEl.appendChild(item));
reindexRecipeListItems(listEl);
return true;
}
async function moveRecipeUp(recipeId, currentIndex) {
if (currentIndex === 0) {
notyf.error("Рейс уже находится в начале списка");
return;
}
try {
await moveRecipe(recipeId, currentIndex, currentIndex - 1);
} catch (error) {
console.error("Ошибка при перемещении рейса:", error);
notyf.error(error.message, { fallback: "Ошибка при перемещении рейса" });
}
}
async function moveRecipeDown(recipeId, currentIndex) {
const totalRecipes = document.querySelectorAll("#recipesList .list-item").length;
if (currentIndex >= totalRecipes - 1) {
notyf.error("Рейс уже находится в конце списка");
return;
}
try {
await moveRecipe(recipeId, currentIndex, currentIndex + 1);
} catch (error) {
console.error("Ошибка при перемещении рейса:", error);
notyf.error(error.message, { fallback: "Ошибка при перемещении рейса" });
}
}
async function pasteRecipe(periodId) {
if (!copiedRecipeData) {
notyf.error("Буфер пуст. Сначала скопируйте рейс.");
return;
}
try {
setSelectedPeriod(periodId);
setSelectedRecipe(null);
await ensureComponentsLoaded();
const recipeName = document.getElementById("recipeName");
if (recipeName) {
recipeName.value = `${copiedRecipeData.name} (копия)`;
}
const headsCount = document.getElementById("headsCount");
if (headsCount) {
const h =
copiedRecipeData.heads_count ??
copiedRecipeData.headsPerTrip ??
copiedRecipeData.heads_per_trip ??
"";
headsCount.value = h === "" || h == null ? "" : String(h);
}
const mixingTime = document.getElementById("mixingTime");
if (mixingTime) {
const mt = copiedRecipeData.mixing_time ?? copiedRecipeData.mixingTime;
mixingTime.value = mt === "" || mt == null ? "" : String(mt);
}
const tripPercent = document.getElementById("tripPercent");
if (tripPercent) {
const tp = copiedRecipeData.trip_percent ?? copiedRecipeData.tripPercent;
tripPercent.value = tp === "" || tp == null ? 100 : String(tp);
}
const ingredientsTableBody = document.getElementById("ingredientsTableBody");
if (ingredientsTableBody) {
ingredientsTableBody.innerHTML = "";
}
const unloadingGroupsTableBody = document.getElementById("unloadingGroupsTableBody");
if (unloadingGroupsTableBody) {
unloadingGroupsTableBody.innerHTML = "";
}
setUnloadingLinkBroken(Boolean(copiedRecipeData.unloading_link_broken));
initializeUnloadingLinkButton();
updateTripPercentFieldState();
setDryMatterMode(false);
if (copiedRecipeData.ingredients && copiedRecipeData.ingredients.length > 0) {
await loadIngredients(copiedRecipeData.ingredients);
}
const copiedGroups =
copiedRecipeData.unloading_groups?.length > 0
? copiedRecipeData.unloading_groups
: copiedRecipeData.unloadingGroups;
if (copiedGroups && copiedGroups.length > 0) {
await loadUnloadingGroups(copiedGroups);
}
showRecipeEdit(true);
setTimeout(() => {
Promise.resolve(recalculateWeights()).catch((err) => console.error("Ошибка при расчете:", err));
}, 100);
setTimeout(() => {
Promise.resolve(updateTotalValues()).catch((err) => console.error("Ошибка при расчете:", err));
}, 100);
notyf.success("Рейс вставлен из буфера");
} catch (error) {
console.error("Ошибка при вставке рейса:", error);
notyf.error("Ошибка при вставке рейса");
}
}
async function copyRecipe(recipeId) {
if (!recipeId) {
notyf.error("Выберите рейс для копирования");
return;
}
try {
const response = await fetch(`/api/recipes/${recipeId}`);
if (!response.ok) {
throw new Error("Ошибка при загрузке рейса");
}
const recipe = await response.json();
copiedRecipeData = {
name: recipe.name,
heads_count:
recipe.heads_count ??
recipe.headsPerTrip ??
recipe.heads_per_trip ??
0,
mixing_time: recipe.mixing_time ?? recipe.mixingTime ?? 0,
trip_percent: recipe.trip_percent ?? recipe.tripPercent ?? 100,
ingredients: (recipe.ingredients || []).map((ing) => ({
component_id: ing.component_id,
weight_per_head: ing.weight_per_head,
amount: ing.amount,
dry_matter: ing.dry_matter,
order: ing.order,
})),
unloading_link_broken: Boolean(
recipe.unloading_link_broken ?? recipe.unloadingLinkBroken ?? false
),
unloading_groups: (recipe.unloading_groups || recipe.unloadingGroups || []).map((group) => ({
name: group.name,
distribution_type: group.distribution_type ?? group.distributionType ?? "percent",
value: group.value,
weight: group.weight,
order: group.order,
})),
};
updatePasteButtons();
notyf.success("Рейс скопирован");
} catch (error) {
console.error("Ошибка при копировании рейса:", error);
notyf.error("Ошибка при копировании рейса");
}
}
function updatePasteButtons() {
const pasteButtons = document.querySelectorAll(".paste-recipe-btn");
const hasData = copiedRecipeData !== null;
pasteButtons.forEach((button) => {
button.disabled = !hasData;
if (hasData) {
button.title = `Вставить рейс "${copiedRecipeData.name}"`;
} else {
button.title = "Вставить рейс из буфера";
}
});
}
return {
deleteRecipe,
moveRecipe,
moveRecipeUp,
moveRecipeDown,
pasteRecipe,
copyRecipe,
updatePasteButtons,
};
}