import { buildRecipeRowsSkeletonHtml } from "../modules/recipes/recipe-skeleton.js"; import { cowIconSvg, routeIconSvg } from "../wesp-zootech-icons.js"; import { peekPreloadedApiPayload, clearPreloadedApiPayload, clearRecipesBootOverlay, PRELOAD_FEED_DISPENSERS_KEY, PRELOAD_FEED_DISPENSERS_MAX_AGE_MS, } from "../wesp-nav-preload.js"; export function createRecipesDataController({ notyf, getSelectedDispenser, setSelectedDispenser, getSelectedPeriod, setSelectedPeriod, getSelectedRecipe, setSelectedRecipe, setCurrentDispenserType, getCurrentDispenserType, loadRecipeDetails, showRecipeEdit, updatePasteButtons, }) { /** Сравнение id (UUID/число) без гонок при переключении оборудования/периода */ function isSameId(a, b) { return String(a ?? "") === String(b ?? ""); } function recipeSkipBadgeHtml(label, title) { return ( `` + '' + `${label}` ); } function recipeAdjustBadgeHtml(label, title) { return ( `` + '' + `${label}` ); } function recipeSkipRestoreBtn(action, recipeId, title) { return ( `` ); } function dashboardSkipWarningHtml(title = "Сегодня есть отклонения от плана") { return ( `` + '' ); } function recipeSkipExtrasHtml(recipe) { if (recipe.skippedToday) { return ( recipeSkipBadgeHtml("сегодня не кормим", "Сегодня не кормим") + recipeSkipRestoreBtn("unskip-recipe-today", recipe.id, "Вернуть рейс в план") ); } let html = ""; if (recipe.skippedIngredientToday) { html += recipeSkipBadgeHtml("сегодня убрали ингредиент", "Сегодня убрали ингредиент") + recipeSkipRestoreBtn("unskip-ingredient-parts-today", recipe.id, "Вернуть все компоненты в план"); } if (recipe.skippedGroupToday) { html += recipeSkipBadgeHtml("сегодня убрали группу", "Сегодня убрали группу") + recipeSkipRestoreBtn("unskip-group-parts-today", recipe.id, "Вернуть все группы в план"); } if (recipe.adjustedIngredientToday) { html += recipeAdjustBadgeHtml("сегодня изменили норму", "Сегодня изменили норму в плане"); } if (recipe.replacedIngredientToday) { html += recipeSkipBadgeHtml("сегодня заменили компонент", "Сегодня заменили компонент в плане"); } return html; } function recipePlanBadgesRowHtml(recipe) { const badges = recipeSkipExtrasHtml(recipe); if (!badges) return ""; return `
${badges}
`; } /** Верхние 3 списка на десктопе: до 4 карточек без прокрутки; дальше — max-height + scroll */ const DASHBOARD_MAX_LIST_ITEMS = 4; const DASHBOARD_LIST_IDS = new Set(["dispensersList", "periodsList", "recipesList"]); /** Минимум времени показа скелетона (мс), чтобы не мигал при очень быстром ответе API */ const DASHBOARD_SKELETON_MIN_MS = 220; /** Длительность FLIP-перестановки карточек (мс) */ const RECIPES_REORDER_FLIP_MS = 620; const MOBILE_DASHBOARD_MQ = "(max-width: 768px)"; const MOBILE_DASHBOARD_STEPS = ["dispensers", "periods", "recipes"]; function isMobileDashboardViewport() { return globalThis.matchMedia?.(MOBILE_DASHBOARD_MQ)?.matches ?? false; } function getMobileDashboardStepFromBody() { for (const step of MOBILE_DASHBOARD_STEPS) { if (document.body.classList.contains(`dashboard-mobile-step-${step}`)) { return step; } } return "dispensers"; } function getListItemTitle(listId, dataAttr, id) { if (id == null || id === "") return ""; const item = document.querySelector(`#${listId} .list-item[${dataAttr}="${CSS.escape(String(id))}"]`); if (!item) return ""; const heading = item.querySelector("h4"); return heading?.textContent?.replace(/\s+/g, " ").trim() ?? ""; } function updateMobileDashboardNav(step) { const nav = document.getElementById("recipesMobileNav"); const titleEl = document.getElementById("recipesMobileNavTitle"); if (!nav || !titleEl) return; if (!isMobileDashboardViewport() || step === "dispensers") { nav.hidden = true; titleEl.textContent = ""; return; } nav.hidden = false; const dispenserTitle = getListItemTitle("dispensersList", "data-dispenser-id", getSelectedDispenser()); const periodTitle = getListItemTitle("periodsList", "data-period-id", getSelectedPeriod()); if (step === "periods") { titleEl.textContent = dispenserTitle || "Периоды кормления"; return; } if (getCurrentDispenserType() === "mill") { titleEl.textContent = dispenserTitle || "Рецепты"; return; } const parts = [dispenserTitle, periodTitle].filter(Boolean); titleEl.textContent = parts.length > 0 ? parts.join(" · ") : "Рейсы"; } function setMobileDashboardStep(step) { document.body.classList.remove( ...MOBILE_DASHBOARD_STEPS.map((s) => `dashboard-mobile-step-${s}`) ); if (!isMobileDashboardViewport()) { updateMobileDashboardNav("dispensers"); return; } const resolvedStep = MOBILE_DASHBOARD_STEPS.includes(step) ? step : "dispensers"; document.body.classList.add(`dashboard-mobile-step-${resolvedStep}`); updateMobileDashboardNav(resolvedStep); } function mobileDashboardBack() { if (!isMobileDashboardViewport()) return; const step = getMobileDashboardStepFromBody(); if (step === "recipes") { showRecipeEdit(false); if (getCurrentDispenserType() === "mill") { setMobileDashboardStep("dispensers"); } else { setMobileDashboardStep("periods"); } return; } if (step === "periods") { setSelectedDispenser(null); setSelectedPeriod(null); setMobileDashboardStep("dispensers"); document.querySelectorAll("#dispensersList .list-item").forEach((item) => { item.classList.remove("active"); }); } } function hasMobileDashboardStepClass() { return MOBILE_DASHBOARD_STEPS.some((s) => document.body.classList.contains(`dashboard-mobile-step-${s}`) ); } function syncMobileDashboardLayout() { if (!isMobileDashboardViewport()) { document.body.classList.remove( ...MOBILE_DASHBOARD_STEPS.map((s) => `dashboard-mobile-step-${s}`) ); updateMobileDashboardNav("dispensers"); return; } if (document.body.classList.contains("recipe-edit-mobile-open")) { return; } if (!hasMobileDashboardStepClass()) { setMobileDashboardStep("dispensers"); } else { updateMobileDashboardNav(getMobileDashboardStepFromBody()); } } globalThis.addEventListener("resize", () => { syncMobileDashboardLayout(); DASHBOARD_LIST_IDS.forEach((listId) => { const listEl = document.getElementById(listId); if (listEl) scheduleDashboardListLayout(listEl); }); if (!isMobileDashboardViewport()) { document.body.classList.remove("recipe-edit-mobile-open"); const recipeEdit = document.getElementById("recipeEdit"); if (recipeEdit) { recipeEdit.classList.remove("recipe-edit--mobile-active"); } document.documentElement.style.overflow = ""; document.body.style.overflow = ""; } }); function clearRecipesListReorderClasses(listEl) { if (!listEl) return; listEl.classList.remove("recipes-list-reorder-pending", "recipes-list-reorder-settle"); } function captureRecipeCardRects(listEl) { const map = new Map(); if (!listEl) return map; listEl.querySelectorAll(":scope > .list-item[data-recipe-id]").forEach((el) => { map.set(el.dataset.recipeId, el.getBoundingClientRect()); }); return map; } function playRecipesListReorderFlip(listEl, firstRects) { if (!listEl || !firstRects || firstRects.size === 0) { clearRecipesListReorderClasses(listEl); return; } const reduceMotion = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; listEl.classList.remove("recipes-list-reorder-pending"); if (reduceMotion) { clearRecipesListReorderClasses(listEl); return; } const items = [...listEl.querySelectorAll(":scope > .list-item[data-recipe-id]")]; const toAnimate = []; items.forEach((el) => { const id = el.dataset.recipeId; const first = firstRects.get(id); if (!first) return; const last = el.getBoundingClientRect(); const dx = first.left - last.left; const dy = first.top - last.top; if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return; el.style.transition = "none"; el.style.transformOrigin = "center center"; el.style.willChange = "transform"; el.style.boxShadow = "var(--recipe-card-flip-shadow, 0 8px 24px rgba(15, 23, 42, 0.12))"; el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; el.style.zIndex = "2"; el.classList.add("recipe-card-reorder-flip"); toAnimate.push(el); }); if (toAnimate.length === 0) { clearRecipesListReorderClasses(listEl); return; } void listEl.offsetHeight; requestAnimationFrame(() => { const easing = "cubic-bezier(0.33, 1, 0.68, 1)"; const sec = RECIPES_REORDER_FLIP_MS / 1000; toAnimate.forEach((el) => { el.style.transition = `transform ${sec}s ${easing}, box-shadow ${sec}s ${easing}`; el.style.transform = "translate3d(0, 0, 0)"; el.style.boxShadow = ""; }); }); globalThis.setTimeout(() => { toAnimate.forEach((el) => { el.style.transition = ""; el.style.transform = ""; el.style.willChange = ""; el.style.transformOrigin = ""; el.style.zIndex = ""; el.style.boxShadow = ""; el.classList.remove("recipe-card-reorder-flip"); }); clearRecipesListReorderClasses(listEl); }, RECIPES_REORDER_FLIP_MS + 100); } async function awaitDashboardSkeletonMin(skeletonStartedAt, options = {}) { if (options.skip) return; const elapsed = performance.now() - skeletonStartedAt; const rest = DASHBOARD_SKELETON_MIN_MS - elapsed; if (rest > 0) { await new Promise((resolve) => setTimeout(resolve, rest)); } } function getDashboardListScrollCap(listId) { if (isMobileDashboardViewport()) return null; return DASHBOARD_MAX_LIST_ITEMS; } function syncDashboardListScrollCap(listEl) { if (!listEl?.id || !DASHBOARD_LIST_IDS.has(listEl.id)) return; listEl.classList.add("dashboard-list-layout-settling"); listEl.classList.remove("dashboard-list-scroll-cap"); listEl.style.removeProperty("max-height"); const maxItems = getDashboardListScrollCap(listEl.id); if (!maxItems) { requestAnimationFrame(() => { listEl.classList.remove("dashboard-list-layout-settling"); }); return; } const items = listEl.querySelectorAll(":scope > .list-item"); if (items.length <= maxItems) { requestAnimationFrame(() => { listEl.classList.remove("dashboard-list-layout-settling"); }); return; } let capPx = 0; for (let i = 0; i < maxItems; i++) { capPx += items[i].offsetHeight; if (i < maxItems - 1) { const a = items[i].getBoundingClientRect(); const b = items[i + 1].getBoundingClientRect(); capPx += Math.max(0, b.top - a.bottom); } } listEl.style.setProperty("max-height", `${Math.ceil(capPx)}px`, "important"); listEl.classList.add("dashboard-list-scroll-cap"); requestAnimationFrame(() => { listEl.classList.remove("dashboard-list-layout-settling"); }); } function ensureDashboardListScrollObservers() { if (window._dashboardListScrollObserversReady) return; window._dashboardListScrollObserversReady = true; if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver((entries) => { entries.forEach((entry) => { const listEl = entry.target; if (listEl?.id && DASHBOARD_LIST_IDS.has(listEl.id)) { syncDashboardListScrollCap(listEl); } }); }); DASHBOARD_LIST_IDS.forEach((listId) => { const listEl = document.getElementById(listId); if (listEl) observer.observe(listEl); }); } function scheduleDashboardListLayout(listEl) { if (!listEl?.id || !DASHBOARD_LIST_IDS.has(listEl.id)) return; ensureDashboardListScrollObservers(); const run = () => syncDashboardListScrollCap(listEl); requestAnimationFrame(() => { requestAnimationFrame(run); }); } function prefersReducedMotion() { return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; } function getDashboardListSkeletonHtml(listId) { const variantByList = { dispensersList: "dispensers", periodsList: "periods", recipesList: "period", millRecipesList: "mill", }; return buildRecipeRowsSkeletonHtml(variantByList[listId] || "period"); } function markListLoading(listEl, loading) { if (!listEl) return; listEl.classList.toggle("list-container--loading", Boolean(loading)); } function setListSkeleton(listEl, listId) { if (!listEl) return; listEl.innerHTML = getDashboardListSkeletonHtml(listId); markListLoading(listEl, true); scheduleDashboardListLayout(listEl); } function setRecipesColumnIdle(idle) { const recipesCol = document.getElementById("recipesCol"); if (recipesCol) { recipesCol.classList.toggle("dashboard-col--idle", Boolean(idle)); } } function setPeriodsColumnIdle(idle) { const periodsCol = document.getElementById("periodsCol"); if (periodsCol) { periodsCol.classList.toggle("dashboard-col--idle", Boolean(idle)); } } function fadeOutListSkeleton(listEl) { const skel = listEl?.querySelector( ":scope > .dashboard-list-skeleton, :scope > .zt-loading-list", ); if (!skel) return; skel.remove(); } function replaceDashboardListContent(listEl, html) { if (!listEl) return; fadeOutListSkeleton(listEl); listEl.innerHTML = html; markListLoading(listEl, false); scheduleDashboardListLayout(listEl); } function clearStaleDashboardListSkeleton(listEl) { if (!listEl?.querySelector(":scope > .dashboard-list-skeleton, :scope > .zt-loading-list")) return; listEl.innerHTML = ""; markListLoading(listEl, false); scheduleDashboardListLayout(listEl); } async function loadDispensers() { const dispensersList = document.getElementById("dispensersList"); let dispensers = peekPreloadedApiPayload( PRELOAD_FEED_DISPENSERS_KEY, PRELOAD_FEED_DISPENSERS_MAX_AGE_MS, ); const fromPreload = Array.isArray(dispensers); const skeletonStartedAt = performance.now(); if (!fromPreload) { setListSkeleton(dispensersList, "dispensersList"); } try { if (!fromPreload) { const response = await fetch("/api/feed_dispensers"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } dispensers = await response.json(); } const awaitSkeleton = awaitDashboardSkeletonMin(skeletonStartedAt, { skip: fromPreload }); const finishList = async (html) => { await awaitSkeleton; await replaceDashboardListContent(dispensersList, html); if (fromPreload) { clearPreloadedApiPayload(PRELOAD_FEED_DISPENSERS_KEY); } clearRecipesBootOverlay(); }; if (dispensers.length === 0) { await finishList( `
Нет доступных кормораздатчиков
`, ); setMobileDashboardStep("dispensers"); return; } const html = dispensers .map((dispenser) => { const isMill = dispenser.type === "mill"; const deviceIcon = isMill ? "fa-industry" : "fa-tractor"; return `

${dispenser.name}${dispenser.hasSkipToday ? dashboardSkipWarningHtml(`Отклонения от плана: ${dispenser.name}`) : ""}

${!isMill ? `${dispenser.periods ? dispenser.periods.length : 0} периодов` : ""}
${dispenser.operator || "Не указан"} ${dispenser.farm || "Не указана"}
`; }) .join(""); await finishList(html); setMobileDashboardStep("dispensers"); } catch (error) { console.error("Ошибка при загрузке кормораздатчиков:", error); clearPreloadedApiPayload(PRELOAD_FEED_DISPENSERS_KEY); await awaitDashboardSkeletonMin(skeletonStartedAt); await replaceDashboardListContent( dispensersList, `
Ошибка при загрузке кормораздатчиков: ${error.message}
` ); clearRecipesBootOverlay(); notyf.error("Ошибка при загрузке кормораздатчиков"); } } async function selectDispenser(dispenserId) { setSelectedDispenser(dispenserId); setSelectedPeriod(null); showRecipeEdit(false, { skipMobileStep: true }); const dispenserIdStr = String(dispenserId); document.querySelectorAll("#dispensersList .list-item").forEach((item) => { item.classList.toggle("active", item.dataset.dispenserId === dispenserIdStr); }); const periodsListEl = document.getElementById("periodsList"); const recipesListEl = document.getElementById("recipesList"); periodsListEl.innerHTML = ""; markListLoading(periodsListEl, false); recipesListEl.innerHTML = ""; markListLoading(recipesListEl, false); setPeriodsColumnIdle(true); setRecipesColumnIdle(true); scheduleDashboardListLayout(periodsListEl); scheduleDashboardListLayout(recipesListEl); const response = await fetch("/api/feed_dispensers"); const dispensers = await response.json(); const dispenser = dispensers.find((d) => isSameId(d.id, dispenserId)); if (!isSameId(getSelectedDispenser(), dispenserId)) { return; } if (isMobileDashboardViewport()) { setMobileDashboardStep(dispenser && dispenser.type === "mill" ? "recipes" : "periods"); } const periodsCol = document.getElementById("periodsCol"); const recipesCol = document.getElementById("recipesCol"); const recipesHeaderText = document.getElementById("recipesCardHeaderText"); const recipesCardIcon = document.getElementById("recipesCardIcon"); const createRecipeBtn = document.getElementById("createRecipeBtn"); if (dispenser && dispenser.type === "mill") { setCurrentDispenserType("mill"); setPeriodsColumnIdle(true); if (periodsCol) { periodsCol.style.display = "none"; periodsCol.classList.remove("col-md-4"); } if (recipesCol) { recipesCol.classList.remove("col-md-4"); recipesCol.classList.add("col-md-8"); } if (recipesHeaderText) recipesHeaderText.textContent = "Рецепты"; if (recipesCardIcon) { recipesCardIcon.innerHTML = ''; recipesCardIcon.classList.remove("route-icon"); } if (createRecipeBtn) createRecipeBtn.style.display = ""; setRecipesColumnIdle(false); if (!isSameId(getSelectedDispenser(), dispenserId)) return; await loadMillRecipes(dispenserId); } else { setCurrentDispenserType("dispenser"); if (createRecipeBtn) createRecipeBtn.style.display = "none"; setRecipesColumnIdle(true); setPeriodsColumnIdle(false); if (periodsCol) { periodsCol.style.display = ""; periodsCol.classList.add("col-md-4"); } if (recipesCol) { recipesCol.classList.remove("col-md-8"); recipesCol.classList.add("col-md-4"); } if (recipesHeaderText) recipesHeaderText.textContent = "Рейсы"; if (recipesCardIcon) { recipesCardIcon.innerHTML = routeIconSvg(); recipesCardIcon.classList.add("route-icon"); recipesCardIcon.classList.add("me-2"); } if (!isSameId(getSelectedDispenser(), dispenserId)) return; await loadPeriods(dispenserId); } } async function loadMillRecipes(dispenserId) { const recipesList = document.getElementById("recipesList"); setListSkeleton(recipesList, "millRecipesList"); const skeletonStartedAt = performance.now(); try { const response = await fetch(`/api/feed_dispensers/${dispenserId}/recipes`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const recipes = await response.json(); if (!isSameId(getSelectedDispenser(), dispenserId)) { clearStaleDashboardListSkeleton(recipesList); return; } if (recipes.length === 0) { await awaitDashboardSkeletonMin(skeletonStartedAt); await replaceDashboardListContent( recipesList, `
Нет доступных рецептов. Создайте первый рецепт.
` ); return; } const selectedRecipe = getSelectedRecipe(); await awaitDashboardSkeletonMin(skeletonStartedAt); const html = recipes .map( (recipe) => `

${recipe.name}

${recipePlanBadgesRowHtml(recipe)}
${recipe.heads_count ?? recipe.headsPerTrip ?? 0} голов ${recipe.mixing_time ?? recipe.mixingTime ?? 0} мин ${recipe.trip_percent ?? recipe.tripPercent ?? 100}%
` ) .join(""); await replaceDashboardListContent(recipesList, html); } catch (error) { console.error("Ошибка при загрузке рецептов кормоцеха:", error); if (!isSameId(getSelectedDispenser(), dispenserId)) { clearStaleDashboardListSkeleton(recipesList); return; } await awaitDashboardSkeletonMin(skeletonStartedAt); await replaceDashboardListContent( recipesList, `
Ошибка при загрузке рецептов: ${error.message}
` ); notyf.error("Ошибка при загрузке рецептов"); } } async function loadPeriods(dispenserId) { const periodsList = document.getElementById("periodsList"); setPeriodsColumnIdle(false); setListSkeleton(periodsList, "periodsList"); const skeletonStartedAt = performance.now(); try { const response = await fetch(`/api/feed_dispensers/${dispenserId}/periods`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const periods = Array.isArray(data) ? data : []; if (!isSameId(getSelectedDispenser(), dispenserId)) { clearStaleDashboardListSkeleton(periodsList); return; } if (periods.length === 0) { await awaitDashboardSkeletonMin(skeletonStartedAt); await replaceDashboardListContent( periodsList, `
Нет доступных периодов
` ); return; } await awaitDashboardSkeletonMin(skeletonStartedAt); const html = periods .map( (period) => `

${period.name}${period.hasSkipToday ? dashboardSkipWarningHtml(`Отклонения от плана: ${period.name}`) : ""}

${period.recipes && period.recipes.length > 0 ? ` ${period.recipes.length} рейсов ` : ` Нет рейсов `}
${period.recipes && period.recipes.length > 0 ? ` ` : ` `}
` ) .join(""); await replaceDashboardListContent(periodsList, html); updatePasteButtons(); } catch (error) { console.error("Ошибка при загрузке периодов:", error); if (!isSameId(getSelectedDispenser(), dispenserId)) { clearStaleDashboardListSkeleton(periodsList); return; } await awaitDashboardSkeletonMin(skeletonStartedAt); await replaceDashboardListContent( periodsList, `
Ошибка при загрузке периодов: ${error.message}
` ); notyf.error("Ошибка при загрузке периодов"); } } async function selectPeriod(periodId) { setSelectedPeriod(periodId); document.querySelectorAll("#periodsList .list-item").forEach((item) => { item.classList.remove("active"); }); document.querySelector(`[data-period-id="${periodId}"]`)?.classList.add("active"); if (isMobileDashboardViewport()) { setMobileDashboardStep("recipes"); } setRecipesColumnIdle(false); const recipesListClear = document.getElementById("recipesList"); recipesListClear.innerHTML = ""; markListLoading(recipesListClear, false); scheduleDashboardListLayout(recipesListClear); try { await loadRecipes(periodId); } catch (error) { console.error("Ошибка при загрузке рейсов:", error); notyf.error("Ошибка при загрузке рейсов"); } } async function loadRecipes(periodId, options = {}) { if (!periodId) return; const skipRecipesSkeleton = Boolean(options.skipRecipesSkeleton); const recipesListPre = document.getElementById("recipesList"); let skeletonStartedAt = performance.now(); if (recipesListPre) { if (skipRecipesSkeleton) { clearRecipesListReorderClasses(recipesListPre); recipesListPre.classList.add("recipes-list-reorder-pending"); } else { setListSkeleton(recipesListPre, "recipesList"); skeletonStartedAt = performance.now(); } } try { const response = await fetch(`/api/periods/${periodId}/recipes`); if (!response.ok) { throw new Error("Ошибка при загрузке рецептов"); } const recipes = await response.json(); const recipesList = document.getElementById("recipesList"); const selectedRecipe = getSelectedRecipe(); if (!isSameId(getSelectedPeriod(), periodId)) { if (recipesList) clearRecipesListReorderClasses(recipesList); if (!skipRecipesSkeleton) clearStaleDashboardListSkeleton(recipesList); return; } if (recipes.length === 0) { if (!skipRecipesSkeleton) { await awaitDashboardSkeletonMin(skeletonStartedAt); } else if (recipesList) { clearRecipesListReorderClasses(recipesList); } const emptyHtml = `
Нет доступных рейсов
`; if (skipRecipesSkeleton) { recipesList.innerHTML = emptyHtml; markListLoading(recipesList, false); scheduleDashboardListLayout(recipesList); } else { await replaceDashboardListContent(recipesList, emptyHtml); } return; } let firstRects = null; if (skipRecipesSkeleton && recipesList) { firstRects = captureRecipeCardRects(recipesList); } if (!skipRecipesSkeleton) { await awaitDashboardSkeletonMin(skeletonStartedAt); } const html = recipes .map( (recipe, index) => `

${recipe.name}

${recipePlanBadgesRowHtml(recipe)}
${recipe.heads_count ?? recipe.headsPerTrip ?? 0} голов ${recipe.mixing_time ?? recipe.mixingTime ?? 0} мин ${recipe.trip_percent ?? recipe.tripPercent ?? 100}%
` ) .join(""); if (skipRecipesSkeleton) { recipesList.innerHTML = html; markListLoading(recipesList, false); scheduleDashboardListLayout(recipesList); } else { await replaceDashboardListContent(recipesList, html); } if (skipRecipesSkeleton && firstRects && firstRects.size > 0) { /* Третий кадр после innerHTML: к этому моменту уже отработал scheduleDashboardListLayout (2× rAF + sync) */ requestAnimationFrame(() => { requestAnimationFrame(() => { requestAnimationFrame(() => playRecipesListReorderFlip(recipesList, firstRects)); }); }); } else if (skipRecipesSkeleton) { clearRecipesListReorderClasses(recipesList); } } catch (error) { console.error("Ошибка при загрузке рецептов:", error); if (!isSameId(getSelectedPeriod(), periodId)) { return; } if (!skipRecipesSkeleton) { await awaitDashboardSkeletonMin(skeletonStartedAt); } const recipesListErr = document.getElementById("recipesList"); if (recipesListErr) { clearRecipesListReorderClasses(recipesListErr); const errHtml = `
Ошибка при загрузке рейсов
`; if (skipRecipesSkeleton) { recipesListErr.innerHTML = errHtml; markListLoading(recipesListErr, false); scheduleDashboardListLayout(recipesListErr); } else { await replaceDashboardListContent(recipesListErr, errHtml); } } notyf.error("Ошибка при загрузке рецептов"); } } async function selectRecipe(recipeId) { setSelectedRecipe(recipeId); document.querySelectorAll("#recipesList .list-item").forEach((item) => { item.classList.remove("active"); }); document.querySelector(`[data-recipe-id="${recipeId}"]`)?.classList.add("active"); const skeletonStartedAt = performance.now(); showRecipeEdit(true, { loading: true }); await loadRecipeDetails(recipeId); await awaitDashboardSkeletonMin(skeletonStartedAt); showRecipeEdit(true, { loading: false }); } return { loadDispensers, selectDispenser, loadMillRecipes, loadPeriods, selectPeriod, loadRecipes, selectRecipe, getCurrentDispenserType, setMobileDashboardStep, mobileDashboardBack, }; }