320 lines
12 KiB
JavaScript
320 lines
12 KiB
JavaScript
let allMeals = [];
|
||
let currentPlan = [];
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
setupTabs();
|
||
loadMeals();
|
||
setupFilters();
|
||
setupButtons();
|
||
});
|
||
|
||
function setupTabs() {
|
||
document.querySelectorAll(".tab").forEach(tab => {
|
||
tab.addEventListener("click", () => {
|
||
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
|
||
document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
document.getElementById(tab.dataset.tab).classList.add("active");
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadMeals() {
|
||
try {
|
||
const res = await fetch("/api/meals");
|
||
allMeals = await res.json();
|
||
renderMeals(allMeals);
|
||
renderRecipes(allMeals);
|
||
} catch (e) {
|
||
console.error("Ошибка загрузки блюд:", e);
|
||
}
|
||
}
|
||
|
||
function setupFilters() {
|
||
document.querySelectorAll(".filter-btn").forEach(btn => {
|
||
btn.addEventListener("click", () => {
|
||
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
|
||
btn.classList.add("active");
|
||
const type = btn.dataset.type;
|
||
if (type === "all") {
|
||
renderMeals(allMeals);
|
||
} else {
|
||
renderMeals(allMeals.filter(m => m.type === type));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function setupButtons() {
|
||
document.getElementById("btn-generate").addEventListener("click", generatePlan);
|
||
document.getElementById("btn-export").addEventListener("click", exportToExcel);
|
||
}
|
||
|
||
function renderMeals(meals) {
|
||
const container = document.getElementById("meals-list");
|
||
if (meals.length === 0) {
|
||
container.innerHTML = '<div class="empty-state"><p>Нет блюд</p></div>';
|
||
return;
|
||
}
|
||
container.innerHTML = meals.map(m => `
|
||
<div class="meal-card" onclick="showRecipe('${m.id}')">
|
||
<span class="type-badge" data-type="${m.type}">${m.type}</span>
|
||
<h4>${m.name}</h4>
|
||
<div class="kbzu-row">
|
||
<span class="kbzu-item"><strong>${m.calories}</strong> ккал</span>
|
||
<span class="kbzu-item">Б: <strong>${m.protein}</strong>г</span>
|
||
<span class="kbzu-item">Ж: <strong>${m.fat}</strong>г</span>
|
||
<span class="kbzu-item">У: <strong>${m.carbs}</strong>г</span>
|
||
</div>
|
||
<div style="margin-top: 0.8rem; font-size: 0.85rem; color: #718096;">
|
||
⏱ ${m.time_min} мин | 🍽 ${m.servings} порц.
|
||
</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderRecipes(meals) {
|
||
const container = document.getElementById("recipes-list");
|
||
container.innerHTML = meals.map(m => `
|
||
<div class="recipe-card" onclick="showRecipe('${m.id}')">
|
||
<span class="type-badge" data-type="${m.type}">${m.type}</span>
|
||
<h4>${m.name}</h4>
|
||
<div class="kbzu-row">
|
||
<span class="kbzu-item"><strong>${m.calories}</strong> ккал</span>
|
||
<span class="kbzu-item">🍽 ${m.servings} порц.</span>
|
||
</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
async function showRecipe(id) {
|
||
const res = await fetch(`/api/meal/${id}`);
|
||
const meal = await res.json();
|
||
if (!meal) return;
|
||
|
||
document.getElementById("recipe-detail").innerHTML = `
|
||
<h3>${meal.name}</h3>
|
||
<div class="detail-grid">
|
||
<div class="ingredients-list">
|
||
<h4>Ингредиенты (на ${meal.servings} порций)</h4>
|
||
<ul>
|
||
${meal.ingredients.map(i => `<li>${i.name} — ${i.amount} ${i.unit}</li>`).join("")}
|
||
</ul>
|
||
</div>
|
||
<div class="kbzu-table">
|
||
<h4>КБЖУ на порцию</h4>
|
||
<table>
|
||
<tr><th>Калории</th><td><strong>${meal.calories}</strong> ккал</td></tr>
|
||
<tr><th>Белки</th><td><strong>${meal.protein}</strong> г</td></tr>
|
||
<tr><th>Жиры</th><td><strong>${meal.fat}</strong> г</td></tr>
|
||
<tr><th>Углеводы</th><td><strong>${meal.carbs}</strong> г</td></tr>
|
||
</table>
|
||
<p style="margin-top: 1rem; color: #718096; font-size: 0.9rem;">
|
||
⏱ Время приготовления: <strong>${meal.time_min} мин</strong>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.getElementById("recipe-detail").classList.add("active");
|
||
document.getElementById("recipe-detail").scrollIntoView({ behavior: "smooth" });
|
||
}
|
||
|
||
async function generatePlan() {
|
||
const btn = document.getElementById("btn-generate");
|
||
btn.textContent = "⏳ Генерация...";
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
console.log("Генерация плана...");
|
||
const res = await fetch("/api/generate-plan", { method: "POST" });
|
||
console.log("Статус ответа:", res.status);
|
||
if (!res.ok) {
|
||
const errText = await res.text();
|
||
throw new Error("Сервер вернул ошибку " + res.status + ": " + errText);
|
||
}
|
||
const result = await res.json();
|
||
console.log("План получен:", result);
|
||
currentPlan = result.plan;
|
||
|
||
renderWeeklyTotals(result.weekly_totals);
|
||
renderWeekPlan(result.plan);
|
||
|
||
const shoppingRes = await fetch("/api/shopping-list", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ plan: result.plan })
|
||
});
|
||
if (!shoppingRes.ok) {
|
||
throw new Error("Список продуктов: ошибка " + shoppingRes.status);
|
||
}
|
||
const shopping = await shoppingRes.json();
|
||
console.log("Список продуктов:", shopping);
|
||
renderShoppingList(shopping);
|
||
|
||
const timelineRes = await fetch("/api/timeline", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ plan: result.plan })
|
||
});
|
||
if (!timelineRes.ok) {
|
||
throw new Error("Хронология: ошибка " + timelineRes.status);
|
||
}
|
||
const timeline = await timelineRes.json();
|
||
console.log("Хронология:", timeline);
|
||
renderTimeline(timeline);
|
||
|
||
btn.textContent = "🔄 Сгенерировать заново";
|
||
} catch (e) {
|
||
console.error("❌ Ошибка генерации:", e);
|
||
alert("Ошибка: " + e.message);
|
||
btn.textContent = "⚠️ Ошибка: " + e.message;
|
||
}
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function renderWeeklyTotals(totals) {
|
||
document.getElementById("weekly-totals").innerHTML = `
|
||
<h3>📊 Итого за неделю (на 4 человек)</h3>
|
||
<div class="totals-grid">
|
||
<div class="total-item">
|
||
<div class="value">${totals.calories.toLocaleString()}</div>
|
||
<div class="label">ккал всего</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${totals.avg_daily_calories.toLocaleString()}</div>
|
||
<div class="label">ккал в день (среднее)</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${totals.protein}г</div>
|
||
<div class="label">Белки</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${totals.fat}г</div>
|
||
<div class="label">Жиры</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${totals.carbs}г</div>
|
||
<div class="label">Углеводы</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${totals.cost.toLocaleString()} ₸</div>
|
||
<div class="label">Примерная стоимость</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderWeekPlan(plan) {
|
||
const container = document.getElementById("week-plan");
|
||
container.innerHTML = plan.map(day => `
|
||
<div class="day-card">
|
||
<h3>${day.day} <span class="date">${day.date}</span></h3>
|
||
<div class="meal-row">
|
||
<span class="meal-type">🌅 Завтрак</span>
|
||
<span class="meal-name">${day.breakfast.name}</span>
|
||
<span class="meal-kbzu">${day.breakfast.calories} ккал | Б:${day.breakfast.protein}г Ж:${day.breakfast.fat}г У:${day.breakfast.carbs}г</span>
|
||
</div>
|
||
<div class="meal-row">
|
||
<span class="meal-type">☀️ Обед</span>
|
||
<span class="meal-name">${day.lunch.name}</span>
|
||
<span class="meal-kbzu">${day.lunch.calories} ккал | Б:${day.lunch.protein}г Ж:${day.lunch.fat}г У:${day.lunch.carbs}г</span>
|
||
</div>
|
||
<div class="meal-row">
|
||
<span class="meal-type">🌙 Ужин</span>
|
||
<span class="meal-name">${day.dinner.name}</span>
|
||
<span class="meal-kbzu">${day.dinner.calories} ккал | Б:${day.dinner.protein}г Ж:${day.dinner.fat}г У:${day.dinner.carbs}г</span>
|
||
</div>
|
||
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 2px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
|
||
<strong style="color: #667eea;">Итого за день: ${day.daily_total.calories} ккал</strong>
|
||
<span style="color: #718096; font-size: 0.9rem;">Б:${day.daily_total.protein}г | Ж:${day.daily_total.fat}г | У:${day.daily_total.carbs}г | ~${day.daily_total.cost} ₸</span>
|
||
</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderShoppingList(shopping) {
|
||
const container = document.getElementById("shopping-list");
|
||
const summary = document.getElementById("shopping-summary");
|
||
|
||
summary.innerHTML = `
|
||
<h3>🛒 Список продуктов на неделю</h3>
|
||
<div class="totals-grid">
|
||
<div class="total-item">
|
||
<div class="value">${shopping.total_cost.toLocaleString()} ₸</div>
|
||
<div class="label">Общая стоимость</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value">${shopping.budget.toLocaleString()} ₸</div>
|
||
<div class="label">Бюджет</div>
|
||
</div>
|
||
<div class="total-item">
|
||
<div class="value" style="color: ${shopping.remaining >= 0 ? '#48bb78' : '#f56565'}">${shopping.remaining.toLocaleString()} ₸</div>
|
||
<div class="label">Остаток</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const byCategory = {};
|
||
shopping.items.forEach(item => {
|
||
if (!byCategory[item.category]) byCategory[item.category] = [];
|
||
byCategory[item.category].push(item);
|
||
});
|
||
|
||
container.innerHTML = Object.entries(byCategory).map(([cat, items]) => `
|
||
<div class="category-group">
|
||
<h3>${cat}</h3>
|
||
${items.map(p => `
|
||
<div class="product-item">
|
||
<span class="name">${p.name}</span>
|
||
<span class="amount">${p.amount >= 1000 ? (p.amount/1000).toFixed(2) + ' кг' : p.amount + ' г'}</span>
|
||
<span class="cost">${p.cost.toLocaleString()} ₸</span>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderTimeline(timeline) {
|
||
const container = document.getElementById("timeline-list");
|
||
container.innerHTML = timeline.map(item => `
|
||
<div class="timeline-item">
|
||
<span class="day">${item.day}</span>
|
||
<span class="action">
|
||
<strong>${item.action}</strong>
|
||
<div class="details">${item.details}</div>
|
||
${item.ingredients ? `<div class="details" style="font-size: 0.8rem; color: #a0aec0;">📝 Продукты: ${item.ingredients.slice(0, 5).join(", ")}${item.ingredients.length > 5 ? "..." : ""}</div>` : ""}
|
||
</span>
|
||
<span class="time">⏱ ${item.time_min} мин</span>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function exportToExcel() {
|
||
const shopping = document.getElementById("shopping-list");
|
||
if (!shopping || shopping.children.length === 0) {
|
||
alert("Сначала сгенерируйте план питания!");
|
||
return;
|
||
}
|
||
|
||
let csv = "\uFEFF";
|
||
csv += "Категория;Продукт;Количество;Стоимость (₸)\n";
|
||
|
||
document.querySelectorAll(".category-group").forEach(group => {
|
||
const category = group.querySelector("h3").textContent;
|
||
group.querySelectorAll(".product-item").forEach(item => {
|
||
const name = item.querySelector(".name").textContent;
|
||
const amount = item.querySelector(".amount").textContent;
|
||
const cost = item.querySelector(".cost").textContent.replace(" ₸", "");
|
||
csv += `${category};${name};${amount};${cost}\n`;
|
||
});
|
||
});
|
||
|
||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||
const link = document.createElement("a");
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = "spisok_produktov.csv";
|
||
link.click();
|
||
}
|
||
|
||
window.showRecipe = showRecipe;
|