RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js): - 27 nutrients per TR TS 022/2011 Russian daily intake standards - Exposed as window.BMR.RDI Supplement columns (db.js): - 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene - getSupplementTotalsForDate expanded to sum all new columns Food nutrient display (main.js): - Removed asterisk (*) from daily kcal (guessed tint stays) - getFoodForDate now JOINs all micronutrient columns from products - foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item - calculateRecipeNutrition expanded to 19 micronutrients with cooking loss Daily nutrients section (main.js): - "Нутриенты" section on Today screen between Добавки and Сон - calcDailyNutrients() sums food (on-the-fly from products) + supplements - Sorted by % RDI ascending — most deficient first - Color coded: red < 30%, orange 30-80%, green >= 80% Recipe detail: - "Микронутриенты (на порцию, % РСП)" chips section
This commit is contained in:
parent
03358964e2
commit
885ef27f36
4 changed files with 248 additions and 40 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="day-header">
|
||||
<button class="day-nav" id="prev-day">←</button>
|
||||
|
|
@ -240,7 +238,7 @@ async function renderToday(container) {
|
|||
|
||||
<div class="summary-row">
|
||||
<div class="summary-card">
|
||||
<span class="summary-val">${intake}${hasGuessed ? "*" : ""}</span>
|
||||
<span class="summary-val">${intake}</span>
|
||||
<span class="summary-lbl">ккал приход</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
|
|
@ -282,7 +280,8 @@ async function renderToday(container) {
|
|||
<span class="log-desc">${esc(f.product_name ?? f.note ?? "—")}
|
||||
${f.unit_amount && f.unit_short
|
||||
? `<small>${f.unit_amount} ${f.unit_short}</small>`
|
||||
: f.amount_g ? `<small>${f.amount_g}г</small>` : ""}</span>
|
||||
: f.amount_g ? `<small>${f.amount_g}г</small>` : ""}
|
||||
${foodNutrientLine(f)}</span>
|
||||
<span class="log-kcal">${f.kcal != null ? Math.round(f.kcal) + " кк" : ""}</span>
|
||||
<button class="del-btn" data-table="food" data-id="${f.id}">✕</button>
|
||||
</li>`
|
||||
|
|
@ -337,6 +336,11 @@ async function renderToday(container) {
|
|||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="log-section">
|
||||
<div class="section-header"><h3>Нутриенты</h3></div>
|
||||
<div class="nutrient-strip" id="daily-nutrients"></div>
|
||||
</section>
|
||||
|
||||
<section class="log-section">
|
||||
<div class="section-header"><h3>Сон</h3>
|
||||
<button class="add-btn" id="add-sleep-btn">✎</button></div>
|
||||
|
|
@ -420,6 +424,14 @@ async function renderToday(container) {
|
|||
const strip = container.querySelector(".micro-strip");
|
||||
if (strip) strip.onwheel = e => { e.preventDefault(); strip.scrollLeft += e.deltaY; };
|
||||
|
||||
// Fill daily nutrient totals section (sorted by deficiency, lowest first)
|
||||
const dailyNutEl = container.querySelector("#daily-nutrients");
|
||||
if (dailyNutEl) {
|
||||
const nutrients = calcDailyNutrients(food, suppTotals);
|
||||
dailyNutEl.innerHTML = renderNutrientChips(nutrients)
|
||||
|| `<div class="log-empty" style="padding:6px 14px">нет данных о нутриентах</div>`;
|
||||
}
|
||||
|
||||
container.querySelector("#add-weight-btn").onclick = () => showWeightModal();
|
||||
container.querySelector("#add-food-btn").onclick = () => showFoodModal();
|
||||
container.querySelector("#add-recipe-food-btn").onclick = () => showFoodFromRecipeModal();
|
||||
|
|
@ -1387,6 +1399,24 @@ async function showRecipeDetailModal(recipeId) {
|
|||
<div class="macro"><span class="macro-val">${nutr.carbs}</span><span class="macro-lbl">У</span></div>
|
||||
<div class="macro"><span class="macro-val">${nutr.fiber}</span><span class="macro-lbl">Кл</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sd-section" style="margin-top:8px">
|
||||
<div class="sd-title">Микронутриенты (на порцию, % РСП)</div>
|
||||
<div class="nutrient-strip">${(() => {
|
||||
const RDI = BMR.RDI;
|
||||
const chips = [];
|
||||
for (const [key, rdi] of Object.entries(RDI)) {
|
||||
const val = nutr[key];
|
||||
if (!val) continue;
|
||||
const pct = Math.round(val / rdi * 100);
|
||||
if (pct < 1) continue;
|
||||
const label = NUTRIENT_LABELS[key] || key;
|
||||
const cls = pct < 30 ? "nut-low" : pct < 80 ? "nut-mid" : "nut-ok";
|
||||
chips.push({ pct, html: '<div class="nut-item ' + cls + '">' + label + " " + pct + "%</div>" });
|
||||
}
|
||||
chips.sort((a, b) => a.pct - b.pct);
|
||||
return chips.map(c => c.html).join("");
|
||||
})()}</div>
|
||||
</div>` : ""}
|
||||
|
||||
<button id="rd-log" class="btn-primary full-width" style="margin-top:16px">Записать в дневник</button>
|
||||
|
|
@ -3832,36 +3862,116 @@ async function showBikeModal(bikeId) {
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// РСП — рекомендуемая суточная потребность (взрослый мужчина)
|
||||
const RDA = {
|
||||
vit_a_mcg: 900, vit_c_mg: 90, vit_d_mcg: 15, vit_e_mg: 15, vit_k_mcg: 120,
|
||||
vit_b1_mg: 1.2, vit_b2_mg: 1.3, vit_b3_mg: 16, vit_b6_mg: 1.7,
|
||||
vit_b12_mcg: 2.4, folate_mcg: 400,
|
||||
calcium_mg: 1000, iron_mg: 8, magnesium_mg: 400, zinc_mg: 11,
|
||||
copper_mg: 0.9, selenium_mcg: 55, potassium_mg: 3400, sodium_mg: 1500,
|
||||
};
|
||||
|
||||
const RDA_LABELS = {
|
||||
vit_a_mcg: "A", vit_c_mg: "C", vit_d_mcg: "D", vit_e_mg: "E", vit_k_mcg: "K",
|
||||
vit_b1_mg: "B1", vit_b2_mg: "B2", vit_b3_mg: "B3", vit_b6_mg: "B6",
|
||||
vit_b12_mcg: "B12", folate_mcg: "Фол",
|
||||
calcium_mg: "Ca", iron_mg: "Fe", magnesium_mg: "Mg", zinc_mg: "Zn",
|
||||
copper_mg: "Cu", selenium_mcg: "Se", potassium_mg: "K+", sodium_mg: "Na",
|
||||
// Nutrient labels for display — maps DB column names to short UI labels
|
||||
const NUTRIENT_LABELS = {
|
||||
vit_a_mcg: "A", vit_c_mg: "C", vit_e_mg: "E",
|
||||
vit_b1_mg: "B1", vit_b2_mg: "B2", vit_b3_mg: "B3",
|
||||
vit_b5_mg: "B5", vit_b6_mg: "B6", vit_b7_mcg: "B7",
|
||||
folate_mcg: "B9", vit_b12_mcg: "B12",
|
||||
vit_d_mcg: "D", vit_k_mcg: "K",
|
||||
calcium_mg: "Ca", magnesium_mg: "Mg", zinc_mg: "Zn",
|
||||
iron_mg: "Fe", iodine_mg: "I", selenium_mcg: "Se",
|
||||
molybdenum_mcg: "Mo", manganese_mg: "Mn", copper_mg: "Cu",
|
||||
chromium_mg: "Cr", phosphorus_mg: "P", potassium_mg: "K+",
|
||||
sodium_mg: "Na", carotene_mcg: "b-car",
|
||||
};
|
||||
|
||||
// Build supplement micronutrient strip (existing horizontal chips on Today screen)
|
||||
function buildMicroStrip(totals) {
|
||||
const RDI = BMR.RDI;
|
||||
const items = [];
|
||||
for (const [key, rda] of Object.entries(RDA)) {
|
||||
for (const [key, rdi] of Object.entries(RDI)) {
|
||||
const val = totals[key];
|
||||
if (val == null || val === 0) continue;
|
||||
const pct = Math.round((val / rda) * 100);
|
||||
const pct = Math.round((val / rdi) * 100);
|
||||
const cls = pct >= 100 ? "micro-ok" : pct >= 50 ? "micro-mid" : "micro-low";
|
||||
items.push(`<span class="micro-chip ${cls}">${RDA_LABELS[key]} ${pct}%</span>`);
|
||||
items.push(`<span class="micro-chip ${cls}">${NUTRIENT_LABELS[key] || key} ${pct}%</span>`);
|
||||
}
|
||||
if (items.length === 0) return "";
|
||||
return `<div class="micro-strip">${items.join("")}</div>`;
|
||||
}
|
||||
|
||||
// Per-food-item nutrient line: shows non-zero nutrients as %RDI chips
|
||||
function foodNutrientLine(f) {
|
||||
if (!f.product_id) return ""; // manual entry or recipe — no per-product data
|
||||
const RDI = BMR.RDI;
|
||||
const factor = (f.amount_g || 100) / 100;
|
||||
const items = [];
|
||||
const check = (val, rdi, label) => {
|
||||
if (!val || !rdi) return;
|
||||
const pct = Math.round(val * factor / rdi * 100);
|
||||
if (pct >= 1) items.push(`${label} ${pct}%`);
|
||||
};
|
||||
check(f.vit_c_mg, RDI.vit_c_mg, "C");
|
||||
check(f.vit_a_mcg, RDI.vit_a_mcg, "A");
|
||||
check(f.vit_b1_mg, RDI.vit_b1_mg, "B1");
|
||||
check(f.vit_b2_mg, RDI.vit_b2_mg, "B2");
|
||||
check(f.vit_b3_mg, RDI.vit_b3_mg, "B3");
|
||||
check(f.vit_b6_mg, RDI.vit_b6_mg, "B6");
|
||||
check(f.vit_b12_mcg, RDI.vit_b12_mcg, "B12");
|
||||
check(f.folate_mcg, RDI.folate_mcg, "B9");
|
||||
check(f.vit_d_mcg, RDI.vit_d_mcg, "D");
|
||||
check(f.vit_e_mg, RDI.vit_e_mg, "E");
|
||||
check(f.vit_k_mcg, RDI.vit_k_mcg, "K");
|
||||
check(f.calcium_mg, RDI.calcium_mg, "Ca");
|
||||
check(f.iron_mg, RDI.iron_mg, "Fe");
|
||||
check(f.magnesium_mg, RDI.magnesium_mg, "Mg");
|
||||
check(f.potassium_mg, RDI.potassium_mg, "K+");
|
||||
check(f.zinc_mg, RDI.zinc_mg, "Zn");
|
||||
check(f.copper_mg, RDI.copper_mg, "Cu");
|
||||
check(f.selenium_mcg, RDI.selenium_mcg, "Se");
|
||||
check(f.sodium_mg, RDI.sodium_mg, "Na");
|
||||
if (items.length === 0) return "";
|
||||
return `<div class="food-nutrients">${items.join(", ")}</div>`;
|
||||
}
|
||||
|
||||
// Calculate daily nutrient totals from food (product micronutrients) + supplements
|
||||
// Returns array sorted by %RDI ascending (most deficient first)
|
||||
function calcDailyNutrients(food, suppTotals) {
|
||||
const RDI = BMR.RDI;
|
||||
const totals = {};
|
||||
|
||||
// From food: product micronutrients * (amount_g / 100)
|
||||
for (const f of food) {
|
||||
if (!f.product_id) continue;
|
||||
const factor = (f.amount_g || 100) / 100;
|
||||
for (const key of Object.keys(RDI)) {
|
||||
if (f[key]) {
|
||||
totals[key] = (totals[key] || 0) + f[key] * factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From supplements
|
||||
if (suppTotals) {
|
||||
for (const key of Object.keys(RDI)) {
|
||||
if (suppTotals[key]) {
|
||||
totals[key] = (totals[key] || 0) + suppTotals[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build array sorted by %RDI ascending
|
||||
const result = [];
|
||||
for (const [key, rdi] of Object.entries(RDI)) {
|
||||
const amount = totals[key] || 0;
|
||||
const pct = Math.round(amount / rdi * 100);
|
||||
const label = NUTRIENT_LABELS[key] || key;
|
||||
result.push({ key, label, amount, rdi, pct });
|
||||
}
|
||||
result.sort((a, b) => a.pct - b.pct);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Render nutrient chips from an array of {label, pct} objects
|
||||
function renderNutrientChips(nutrients) {
|
||||
if (!nutrients || nutrients.length === 0) return "";
|
||||
return nutrients.map(n => {
|
||||
const cls = n.pct < 30 ? "nut-low" : n.pct < 80 ? "nut-mid" : "nut-ok";
|
||||
return `<div class="nut-item ${cls}">${n.label} ${n.pct}%</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reusable product search dropdown component
|
||||
// parentEl: element to insert into (must have position:relative or will be set)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue