337 lines
12 KiB
JavaScript
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,
|
|
};
|
|
}
|