export function createRecipesOperationsController({ notyf, getSelectedRecipe, setSelectedRecipe, getSelectedPeriod, setSelectedPeriod, getSelectedDispenser, getCurrentDispenserType, showRecipeEdit, loadRecipes, loadPeriods, selectPeriod, persistRecipe, toggleUnloadingBlocks, resetDeletedEntities, 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 { if (typeof selectPeriod === "function") { await selectPeriod(periodId); } else { setSelectedPeriod(periodId); } setSelectedRecipe(null); if (typeof resetDeletedEntities === "function") { resetDeletedEntities(); } 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)); if (typeof toggleUnloadingBlocks === "function") { toggleUnloadingBlocks(); } 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); await Promise.all([ Promise.resolve(recalculateWeights()).catch((err) => console.error("Ошибка при расчете:", err)), Promise.resolve(updateTotalValues?.()).catch((err) => console.error("Ошибка при расчете:", err)), ]); if (typeof persistRecipe !== "function") { throw new Error("Сохранение рейса недоступно"); } await persistRecipe(true); notyf.success("Рейс вставлен из буфера"); } catch (error) { console.error("Ошибка при вставке рейса:", error); notyf.error(error.message || "Ошибка при вставке рейса"); } } 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 ?? ing.weightPerHead, amount: ing.amount ?? ing.tripWeight, dry_matter: ing.dry_matter ?? ing.dryMatter, dry_matter_per_head: ing.dry_matter_per_head ?? ing.dryMatterPerHead, 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, }; }