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.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) => `
${recipePlanBadgesRowHtml(recipe)}
${cowIconSvg()}
${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) => `
`
)
.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)}
${cowIconSvg()}
${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,
};
}