268 lines
9.1 KiB
JavaScript
268 lines
9.1 KiB
JavaScript
const http = require("http");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const DATA = path.join(__dirname, "data.json");
|
||
|
||
function readData() {
|
||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { meals: [], products: [], weekly_plan: [], shopping_list: [], timeline: [] }; }
|
||
}
|
||
function writeData(data) {
|
||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||
}
|
||
function body(req) {
|
||
return new Promise((resolve) => {
|
||
let s = "";
|
||
req.on("data", (c) => (s += c));
|
||
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
|
||
});
|
||
}
|
||
|
||
function generateWeeklyPlan() {
|
||
const data = readData();
|
||
const days = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"];
|
||
const breakfasts = data.meals.filter(m => m.type === "завтрак");
|
||
const lunches = data.meals.filter(m => m.type === "обед");
|
||
const dinners = data.meals.filter(m => m.type === "ужин");
|
||
|
||
const plan = [];
|
||
let totalCalories = 0, totalProtein = 0, totalFat = 0, totalCarbs = 0, totalCost = 0;
|
||
|
||
days.forEach((day, i) => {
|
||
const b = breakfasts[i % breakfasts.length];
|
||
const l = lunches[i % lunches.length];
|
||
const d = dinners[i % dinners.length];
|
||
const dayCalories = b.calories + l.calories + d.calories;
|
||
const dayProtein = b.protein + l.protein + d.protein;
|
||
const dayFat = b.fat + l.fat + d.fat;
|
||
const dayCarbs = b.carbs + l.carbs + d.carbs;
|
||
|
||
const dayCost = calculateDayCost([b, l, d], data.products);
|
||
|
||
plan.push({
|
||
day,
|
||
date: new Date(Date.now() + i * 86400000).toISOString().slice(0, 10),
|
||
breakfast: { id: b.id, name: b.name, calories: b.calories, protein: b.protein, fat: b.fat, carbs: b.carbs, time_min: b.time_min },
|
||
lunch: { id: l.id, name: l.name, calories: l.calories, protein: l.protein, fat: l.fat, carbs: l.carbs, time_min: l.time_min },
|
||
dinner: { id: d.id, name: d.name, calories: d.calories, protein: d.protein, fat: d.fat, carbs: d.carbs, time_min: d.time_min },
|
||
daily_total: { calories: dayCalories, protein: dayProtein, fat: dayFat, carbs: dayCarbs, cost: dayCost }
|
||
});
|
||
|
||
totalCalories += dayCalories;
|
||
totalProtein += dayProtein;
|
||
totalFat += dayFat;
|
||
totalCarbs += dayCarbs;
|
||
totalCost += dayCost;
|
||
});
|
||
|
||
const weekly_totals = {
|
||
calories: Math.round(totalCalories),
|
||
protein: Math.round(totalProtein),
|
||
fat: Math.round(totalFat),
|
||
carbs: Math.round(totalCarbs),
|
||
cost: Math.round(totalCost),
|
||
avg_daily_calories: Math.round(totalCalories / 7)
|
||
};
|
||
|
||
return { plan, weekly_totals };
|
||
}
|
||
|
||
function calculateDayCost(meals, products) {
|
||
const productMap = {};
|
||
products.forEach(p => { productMap[p.id] = { ...p }; });
|
||
|
||
meals.forEach(m => {
|
||
m.ingredients.forEach(ing => {
|
||
if (!productMap[ing.product_id]) return;
|
||
const existing = productMap[ing.product_id];
|
||
const amountKg = ing.amount / 1000;
|
||
if (!existing.total_amount) existing.total_amount = 0;
|
||
existing.total_amount += amountKg;
|
||
});
|
||
});
|
||
|
||
let total = 0;
|
||
Object.values(productMap).forEach(p => {
|
||
if (p.total_amount) {
|
||
total += p.total_amount * p.price_per_kg;
|
||
}
|
||
});
|
||
return total;
|
||
}
|
||
|
||
function generateShoppingList(weeklyPlan) {
|
||
const data = readData();
|
||
const productMap = {};
|
||
data.products.forEach(p => { productMap[p.id] = { ...p, total_amount: 0, total_cost: 0 }; });
|
||
|
||
weeklyPlan.forEach(day => {
|
||
[day.breakfast, day.lunch, day.dinner].forEach(meal => {
|
||
const mealData = data.meals.find(m => m.id === meal.id);
|
||
if (!mealData) return;
|
||
mealData.ingredients.forEach(ing => {
|
||
if (productMap[ing.product_id]) {
|
||
productMap[ing.product_id].total_amount += ing.amount;
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
const shoppingList = [];
|
||
let totalCost = 0;
|
||
|
||
Object.values(productMap).forEach(p => {
|
||
if (p.total_amount > 0) {
|
||
const cost = (p.total_amount / 1000) * p.price_per_kg;
|
||
shoppingList.push({
|
||
id: p.id,
|
||
name: p.name,
|
||
category: p.category,
|
||
amount: Math.round(p.total_amount),
|
||
unit: p.total_amount >= 1000 ? "кг" : "г",
|
||
price_per_kg: p.price_per_kg,
|
||
cost: Math.round(cost)
|
||
});
|
||
totalCost += cost;
|
||
}
|
||
});
|
||
|
||
shoppingList.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
||
|
||
return { items: shoppingList, total_cost: Math.round(totalCost), budget: 100000, remaining: 100000 - Math.round(totalCost) };
|
||
}
|
||
|
||
function generateTimeline(weeklyPlan) {
|
||
const data = readData();
|
||
const timeline = [];
|
||
|
||
weeklyPlan.forEach((day, dayIndex) => {
|
||
const prepTime = [];
|
||
[day.breakfast, day.lunch, day.dinner].forEach((meal, mealIdx) => {
|
||
const mealData = data.meals.find(m => m.id === meal.id);
|
||
if (!mealData) return;
|
||
|
||
const mealTypes = ["завтрак", "обед", "ужин"];
|
||
prepTime.push({
|
||
day: day.day,
|
||
date: day.date,
|
||
meal_type: mealTypes[mealIdx],
|
||
dish: mealData.name,
|
||
prep_time_min: mealData.time_min,
|
||
ingredients: mealData.ingredients.map(i => i.name)
|
||
});
|
||
});
|
||
|
||
if (dayIndex === 0) {
|
||
timeline.push({
|
||
day: "Воскресенье (перед неделей)",
|
||
date: new Date(Date.now() - 86400000).toISOString().slice(0, 10),
|
||
action: "Закупка продуктов",
|
||
details: "Купить все продукты по списку на неделю",
|
||
time_min: 90
|
||
});
|
||
}
|
||
|
||
prepTime.forEach(p => timeline.push({
|
||
day: p.day,
|
||
date: p.date,
|
||
action: `Приготовление: ${p.meal_type}`,
|
||
details: p.dish,
|
||
time_min: p.prep_time_min,
|
||
ingredients: p.ingredients
|
||
}));
|
||
});
|
||
|
||
return timeline;
|
||
}
|
||
|
||
http.createServer(async (req, res) => {
|
||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||
|
||
if (req.method === "OPTIONS") {
|
||
res.writeHead(200);
|
||
return res.end();
|
||
}
|
||
|
||
if (req.method === "GET" && req.url === "/api/meals") {
|
||
const data = readData();
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(data.meals));
|
||
}
|
||
|
||
if (req.method === "GET" && req.url === "/api/products") {
|
||
const data = readData();
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(data.products));
|
||
}
|
||
|
||
if (req.method === "GET" && req.url.startsWith("/api/meal/")) {
|
||
const id = req.url.split("/")[3];
|
||
const data = readData();
|
||
const meal = data.meals.find(m => m.id === id);
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(meal || {}));
|
||
}
|
||
|
||
if (req.method === "POST" && req.url === "/api/generate-plan") {
|
||
const result = generateWeeklyPlan();
|
||
const data = readData();
|
||
data.weekly_plan = result.plan;
|
||
writeData(data);
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(result));
|
||
}
|
||
|
||
if (req.method === "GET" && req.url === "/api/plan") {
|
||
const data = readData();
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(data.weekly_plan || []));
|
||
}
|
||
|
||
if (req.method === "POST" && req.url === "/api/shopping-list") {
|
||
const bodyData = await body(req);
|
||
const shoppingList = generateShoppingList(bodyData.plan || []);
|
||
const data = readData();
|
||
data.shopping_list = shoppingList;
|
||
writeData(data);
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(shoppingList));
|
||
}
|
||
|
||
if (req.method === "GET" && req.url === "/api/shopping-list") {
|
||
const data = readData();
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(data.shopping_list || { items: [], total_cost: 0 }));
|
||
}
|
||
|
||
if (req.method === "POST" && req.url === "/api/timeline") {
|
||
const bodyData = await body(req);
|
||
const timeline = generateTimeline(bodyData.plan || []);
|
||
const data = readData();
|
||
data.timeline = timeline;
|
||
writeData(data);
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(timeline));
|
||
}
|
||
|
||
if (req.method === "GET" && req.url === "/api/timeline") {
|
||
const data = readData();
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
return res.end(JSON.stringify(data.timeline || []));
|
||
}
|
||
|
||
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
||
const full = path.join(__dirname, file);
|
||
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||
const ext = path.extname(full);
|
||
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
|
||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||
return res.end(fs.readFileSync(full));
|
||
}
|
||
|
||
res.writeHead(404);
|
||
res.end("Not found");
|
||
}).listen(PORT, () => console.log("Сервер запущен на порту " + PORT));
|