diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 8c683bd..c511f76 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -548,6 +548,7 @@ select { .food-edit-item { cursor: pointer; } .food-edit-item:active { background: rgba(255,255,255,0.05); } +.food-edit-item .log-desc { white-space: normal; } /* Sleep info */ .sleep-info { @@ -1437,6 +1438,32 @@ select { font-size: 14px; } +/* Per-food-item nutrient line */ +.food-nutrients { + font-size: 11px; + color: var(--muted); + margin-top: 2px; + white-space: normal; +} + +/* Daily nutrient totals strip — chips sorted by deficiency */ +.nutrient-strip { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 14px; +} +.nut-item { + font-size: 12px; + padding: 3px 8px; + border-radius: 12px; + background: var(--surface); + border: 1px solid var(--border); +} +.nut-low { color: #f44336; border-color: #f44336; } +.nut-mid { color: #ffb74d; border-color: #ffb74d; } +.nut-ok { color: #81c784; border-color: #81c784; } + /* Recipe components */ /* Guessed nutrients indicator — slight tint on food items with estimated micronutrients */ .log-item.guessed { diff --git a/frontend/js/bmr.js b/frontend/js/bmr.js index c7c8a78..902d840 100644 --- a/frontend/js/bmr.js +++ b/frontend/js/bmr.js @@ -27,4 +27,35 @@ function estimateRealBmr(weights, foodTotals) { return realExpend > 500 ? Math.round(realExpend) : null; // sanity: < 500 kcal/day is wrong data } -window.BMR = { calculateBmr, getAge, estimateRealBmr }; +// Russian daily recommended intake (РСП) per ТР ТС 022/2011 +const RDI = { + vit_a_mcg: 800, // ретиноловый эквивалент + vit_c_mg: 60, + vit_e_mg: 10, // токофероловый эквивалент + vit_b1_mg: 1.4, + vit_b2_mg: 1.6, + vit_b3_mg: 18, // ниацин + vit_b5_mg: 6, // пантотеновая + vit_b6_mg: 2, + vit_b7_mcg: 50, // биотин + folate_mcg: 200, // фолиевая (= 0.2 мг = 200 мкг), в БД как folate_mcg + vit_b12_mcg: 1, + vit_d_mcg: 5, + vit_k_mcg: 120, + calcium_mg: 1000, + magnesium_mg: 400, + zinc_mg: 15, + iron_mg: 14, + iodine_mg: 0.15, + selenium_mcg: 70, + molybdenum_mcg: 70, + manganese_mg: 2, + copper_mg: 1, + chromium_mg: 0.05, + phosphorus_mg: 800, + potassium_mg: 3500, + sodium_mg: 2400, + carotene_mcg: 5000, // бета-каротин (нет официальной нормы, ~5мг) +}; + +window.BMR = { calculateBmr, getAge, estimateRealBmr, RDI }; diff --git a/frontend/js/db.js b/frontend/js/db.js index a78e309..fd12329 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -192,8 +192,12 @@ async function getRecipeIngredients(recipeId) { return select( `SELECT ri.*, p.name as product_name, p.kcal_per_100g, p.protein_per_100g, p.fat_per_100g, p.carbs_per_100g, - p.fiber_per_100g, p.vit_a_mcg, p.vit_c_mg, p.vit_b1_mg, - p.vit_b2_mg, + p.fiber_per_100g, + p.vit_a_mcg, p.vit_c_mg, p.vit_d_mcg, p.vit_e_mg, p.vit_k_mcg, + p.vit_b1_mg, p.vit_b2_mg, p.vit_b3_mg, p.vit_b6_mg, + p.vit_b12_mcg, p.folate_mcg, + p.calcium_mg, p.iron_mg, p.magnesium_mg, p.zinc_mg, p.copper_mg, + p.selenium_mcg, p.potassium_mg, p.sodium_mg, cm.name as cooking_method_name FROM recipe_ingredients ri LEFT JOIN products p ON ri.product_id = p.id AND ri.product_id > 0 @@ -263,10 +267,17 @@ async function calculateRecipeNutrition(recipeId) { return cmCache[cmId]; } - const totals = { - kcal: 0, protein: 0, fat: 0, carbs: 0, fiber: 0, - vit_a_mcg: 0, vit_c_mg: 0, vit_b1_mg: 0, vit_b2_mg: 0, - }; + // All micronutrient keys tracked in products table + const microKeys = [ + "vit_a_mcg", "vit_c_mg", "vit_d_mcg", "vit_e_mg", "vit_k_mcg", + "vit_b1_mg", "vit_b2_mg", "vit_b3_mg", "vit_b6_mg", + "vit_b12_mcg", "folate_mcg", + "calcium_mg", "iron_mg", "magnesium_mg", "zinc_mg", "copper_mg", + "selenium_mcg", "potassium_mg", "sodium_mg", + ]; + + const totals = { kcal: 0, protein: 0, fat: 0, carbs: 0, fiber: 0 }; + for (const mk of microKeys) totals[mk] = 0; for (const ing of ingredients) { if (!ing.product_id || ing.product_id === 0) continue; // skip custom ingredients without product data @@ -279,11 +290,8 @@ async function calculateRecipeNutrition(recipeId) { fat: (ing.fat_per_100g ?? 0) * factor, carbs: (ing.carbs_per_100g ?? 0) * factor, fiber: (ing.fiber_per_100g ?? 0) * factor, - vit_a_mcg: (ing.vit_a_mcg ?? 0) * factor, - vit_c_mg: (ing.vit_c_mg ?? 0) * factor, - vit_b1_mg: (ing.vit_b1_mg ?? 0) * factor, - vit_b2_mg: (ing.vit_b2_mg ?? 0) * factor, }; + for (const mk of microKeys) raw[mk] = (ing[mk] ?? 0) * factor; // Apply cooking loss: use ingredient's method, or recipe's default const cmId = ing.cooking_method_id || recipe.cooking_method_id; @@ -295,6 +303,7 @@ async function calculateRecipeNutrition(recipeId) { raw.fat *= (1 - (cm.fat_loss_pct ?? 0) / 100); raw.carbs *= (1 - (cm.carbs_loss_pct ?? 0) / 100); raw.fiber *= (1 - (cm.fiber_loss_pct ?? 0) / 100); + // Vitamin losses: cooking_methods table only has columns for a, c, b1, b2 raw.vit_a_mcg *= (1 - (cm.vit_a_loss_pct ?? 0) / 100); raw.vit_c_mg *= (1 - (cm.vit_c_loss_pct ?? 0) / 100); raw.vit_b1_mg *= (1 - (cm.vit_b1_loss_pct ?? 0) / 100); @@ -307,19 +316,20 @@ async function calculateRecipeNutrition(recipeId) { } // Per serving - return { + const result = { kcal: Math.round(totals.kcal / servings), protein: Math.round(totals.protein / servings * 10) / 10, fat: Math.round(totals.fat / servings * 10) / 10, carbs: Math.round(totals.carbs / servings * 10) / 10, fiber: Math.round(totals.fiber / servings * 10) / 10, - vit_a_mcg: Math.round(totals.vit_a_mcg / servings * 10) / 10, - vit_c_mg: Math.round(totals.vit_c_mg / servings * 10) / 10, - vit_b1_mg: Math.round(totals.vit_b1_mg / servings * 1000) / 1000, - vit_b2_mg: Math.round(totals.vit_b2_mg / servings * 1000) / 1000, servings: servings, totalWeight: ingredients.reduce((s, i) => s + (i.amount_g || 0), 0), }; + // Add all micronutrients per serving (rounded) + for (const mk of microKeys) { + result[mk] = Math.round(totals[mk] / servings * 100) / 100; + } + return result; } // Log a recipe as a food entry — stores calculated nutrition per serving @@ -342,7 +352,13 @@ async function addFoodFromRecipe(date, time, recipeId) { async function getFoodForDate(date) { return select( `SELECT fl.*, p.name as product_name, p.brand as product_brand, - COALESCE(p.nutrients_guessed, 0) as nutrients_guessed, r.name as recipe_name, + COALESCE(p.nutrients_guessed, 0) as nutrients_guessed, + p.vit_a_mcg, p.vit_c_mg, p.vit_d_mcg, p.vit_e_mg, p.vit_k_mcg, + p.vit_b1_mg, p.vit_b2_mg, p.vit_b3_mg, p.vit_b6_mg, + p.vit_b12_mcg, p.folate_mcg, + p.calcium_mg, p.iron_mg, p.magnesium_mg, p.zinc_mg, p.copper_mg, + p.selenium_mcg, p.potassium_mg, p.sodium_mg, + r.name as recipe_name, su.name as unit_name, su.name_short as unit_short FROM food_log fl LEFT JOIN products p ON fl.product_id = p.id @@ -506,7 +522,9 @@ async function getSupplementTotalsForDate(date) { SUM(COALESCE(sl.dose_g,1) * s.vit_b1_mg) as vit_b1_mg, SUM(COALESCE(sl.dose_g,1) * s.vit_b2_mg) as vit_b2_mg, SUM(COALESCE(sl.dose_g,1) * s.vit_b3_mg) as vit_b3_mg, + SUM(COALESCE(sl.dose_g,1) * s.vit_b5_mg) as vit_b5_mg, SUM(COALESCE(sl.dose_g,1) * s.vit_b6_mg) as vit_b6_mg, + SUM(COALESCE(sl.dose_g,1) * s.vit_b7_mcg) as vit_b7_mcg, SUM(COALESCE(sl.dose_g,1) * s.vit_b12_mcg) as vit_b12_mcg, SUM(COALESCE(sl.dose_g,1) * s.folate_mcg) as folate_mcg, SUM(COALESCE(sl.dose_g,1) * s.calcium_mg) as calcium_mg, @@ -516,7 +534,13 @@ async function getSupplementTotalsForDate(date) { SUM(COALESCE(sl.dose_g,1) * s.copper_mg) as copper_mg, SUM(COALESCE(sl.dose_g,1) * s.selenium_mcg) as selenium_mcg, SUM(COALESCE(sl.dose_g,1) * s.potassium_mg) as potassium_mg, - SUM(COALESCE(sl.dose_g,1) * s.sodium_mg) as sodium_mg + SUM(COALESCE(sl.dose_g,1) * s.sodium_mg) as sodium_mg, + SUM(COALESCE(sl.dose_g,1) * s.chromium_mg) as chromium_mg, + SUM(COALESCE(sl.dose_g,1) * s.manganese_mg) as manganese_mg, + SUM(COALESCE(sl.dose_g,1) * s.molybdenum_mcg) as molybdenum_mcg, + SUM(COALESCE(sl.dose_g,1) * s.iodine_mg) as iodine_mg, + SUM(COALESCE(sl.dose_g,1) * s.phosphorus_mg) as phosphorus_mg, + SUM(COALESCE(sl.dose_g,1) * s.carotene_mcg) as carotene_mcg FROM supplement_log sl JOIN supplements s ON sl.supplement_id = s.id WHERE sl.date = ?1`, @@ -1081,6 +1105,22 @@ async function runMigrations() { created_at TEXT DEFAULT (datetime('now')) )`); + // --- Supplement columns migration: add missing micronutrient columns --- + const suppCols = (await select("PRAGMA table_info(supplements)")).map(r => r.name); + const addSuppCol = async (col, type) => { if (!suppCols.includes(col)) await execute(`ALTER TABLE supplements ADD COLUMN ${col} ${type}`); }; + await addSuppCol("vit_b5_mg", "REAL"); + await addSuppCol("vit_b7_mcg", "REAL"); // biotin + await addSuppCol("chromium_mg", "REAL"); + await addSuppCol("manganese_mg", "REAL"); + await addSuppCol("molybdenum_mcg", "REAL"); + await addSuppCol("iodine_mg", "REAL"); + await addSuppCol("phosphorus_mg", "REAL"); + await addSuppCol("silicon_mg", "REAL"); // кремний — no RDI + await addSuppCol("vanadium_mg", "REAL"); // ванадий — no RDI + await addSuppCol("lutein_mg", "REAL"); // лютеин — no RDI + await addSuppCol("lycopene_mg", "REAL"); // ликопин — no RDI + await addSuppCol("carotene_mcg", "REAL"); // бета-каротин + console.log("[db.js] recipe migrations done, checking seq repair..."); // Repair old tracks: assign seq to points where it's NULL, ordered by timestamp. diff --git a/frontend/js/main.js b/frontend/js/main.js index b06067d..f248f58 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -229,8 +229,6 @@ async function renderToday(container) { // Show expenditure even without BMR if there are activities const expend = bmr != null ? bmr + actKcal : actKcal > 0 ? actKcal : null; const balance = expend != null ? intake - expend : null; - const hasGuessed = food.some(f => f.nutrients_guessed); - container.innerHTML = `