@@ -0,0 +1,336 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user