АУП norms with star, nutrient half-life table, recipe nutrients in daily strip
RDI/АУП: - rdi_values.norm_type column: 'rdi' or 'aup' - АУП values seeded: omega3 1000mg, EPA/DHA 250mg, lutein 5mg, lycopene 5mg, silicon 30mg, vanadium 0.04mg (all 3 standards) - Nutrient chips show * after label for АУП norms - Supplement form placeholders show RDI/АУП values (with * for АУП) Half-life table: - nutrient_halflife: nutrient_key, min/max/avg days, source - 32 nutrients seeded with whole-body biological half-life data - Sources: PubMed, ICRP, Performance Lab, clinical studies Recipe nutrients in daily strip: - recipes.cached_nutrients TEXT column (JSON) - cacheRecipeNutrients() computes and stores per-serving nutrients - Called on recipe save in editor - calcDailyNutrients() now reads cached_nutrients for recipe food entries - getFoodForDate() JOINs r.cached_nutrients BMR.loadRDI() now loads norm_type alongside values. BMR.RDI_TYPES exposed for star rendering.
This commit is contained in:
parent
c72a22a965
commit
0bafb2b317
3 changed files with 122 additions and 8 deletions
|
|
@ -29,6 +29,7 @@ function estimateRealBmr(weights, foodTotals) {
|
|||
|
||||
// RDI loaded from DB (rdi_standards + rdi_values tables).
|
||||
// Fallback hardcoded RU values used until DB is ready.
|
||||
let RDI_TYPES = {}; // { nutrient_key: "rdi" | "aup" }
|
||||
let 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,
|
||||
|
|
@ -49,14 +50,20 @@ async function loadRDI(standardCode) {
|
|||
if (!std.length) return;
|
||||
const vals = await window.__TAURI__.core.invoke("plugin:sql|select", {
|
||||
db: "sqlite:baldav.db",
|
||||
query: "SELECT nutrient_key, value FROM rdi_values WHERE standard_id = ?1",
|
||||
query: "SELECT nutrient_key, value, norm_type FROM rdi_values WHERE standard_id = ?1",
|
||||
values: [std[0].id],
|
||||
});
|
||||
const loaded = {};
|
||||
for (const v of vals) loaded[v.nutrient_key] = v.value;
|
||||
RDI_TYPES = {};
|
||||
for (const v of vals) {
|
||||
loaded[v.nutrient_key] = v.value;
|
||||
RDI_TYPES[v.nutrient_key] = v.norm_type || "rdi";
|
||||
}
|
||||
if (Object.keys(loaded).length > 0) RDI = loaded;
|
||||
} catch (e) { console.error("[BMR] loadRDI error:", e); }
|
||||
}
|
||||
|
||||
window.BMR = { calculateBmr, getAge, estimateRealBmr, RDI, loadRDI,
|
||||
get rdi() { return RDI; } };
|
||||
window.BMR = { calculateBmr, getAge, estimateRealBmr, loadRDI,
|
||||
get RDI() { return RDI; },
|
||||
get RDI_TYPES() { return RDI_TYPES; },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ async function getFoodForDate(date) {
|
|||
p.calcium_mg, p.iron_mg, p.magnesium_mg, p.zinc_mg, p.copper_mg,
|
||||
p.selenium_mcg, p.potassium_mg, p.sodium_mg,
|
||||
p.phosphorus_mg, p.carotene_mcg,
|
||||
r.name as recipe_name,
|
||||
r.name as recipe_name, r.cached_nutrients,
|
||||
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
|
||||
|
|
@ -944,6 +944,14 @@ window.DB.addRecipeIngredient = addRecipeIngredient;
|
|||
window.DB.deleteRecipeIngredient = deleteRecipeIngredient;
|
||||
window.DB.clearRecipeIngredients = clearRecipeIngredients;
|
||||
window.DB.calculateRecipeNutrition = calculateRecipeNutrition;
|
||||
|
||||
async function cacheRecipeNutrients(recipeId) {
|
||||
const nutr = await calculateRecipeNutrition(recipeId);
|
||||
if (!nutr) return;
|
||||
await execute("UPDATE recipes SET cached_nutrients = ?1 WHERE id = ?2",
|
||||
[JSON.stringify(nutr), recipeId]);
|
||||
}
|
||||
window.DB.cacheRecipeNutrients = cacheRecipeNutrients;
|
||||
window.DB.addFoodFromRecipe = addFoodFromRecipe;
|
||||
window.DB.createGpsSession = createGpsSession;
|
||||
window.DB.addGpsPoint = addGpsPoint;
|
||||
|
|
@ -1213,6 +1221,90 @@ async function runMigrations() {
|
|||
console.log("[db.js] Seeded 3 RDI standards (RU/EU/US)");
|
||||
}
|
||||
|
||||
// Add norm_type to rdi_values (rdi vs aup)
|
||||
const rdiValCols = (await select("PRAGMA table_info(rdi_values)")).map(r => r.name);
|
||||
if (!rdiValCols.includes("norm_type")) {
|
||||
await execute("ALTER TABLE rdi_values ADD COLUMN norm_type TEXT DEFAULT 'rdi'");
|
||||
}
|
||||
|
||||
// Seed АУП values (one-time, check if omega3 already exists)
|
||||
const hasOmega3 = await select("SELECT COUNT(*) as cnt FROM rdi_values WHERE nutrient_key = 'omega3_mg'");
|
||||
if (hasOmega3[0].cnt === 0) {
|
||||
const aupData = {
|
||||
// key: [RU, EU, US] — all marked as 'aup'
|
||||
omega3_mg: [1000, 250, 1600],
|
||||
epa_mg: [250, 250, 500],
|
||||
dha_mg: [250, 250, 500],
|
||||
lutein_mg: [5, 10, 6],
|
||||
lycopene_mg: [5, 5, 5],
|
||||
silicon_mg: [30, 30, 30],
|
||||
vanadium_mg: [0.04, 0.04, 0.04],
|
||||
};
|
||||
for (const [key, vals] of Object.entries(aupData)) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await execute("INSERT OR IGNORE INTO rdi_values (standard_id, nutrient_key, value, norm_type) VALUES (?1, ?2, ?3, 'aup')", [i + 1, key, vals[i]]);
|
||||
}
|
||||
}
|
||||
console.log("[db.js] Seeded АУП values for omega/lutein/lycopene/silicon/vanadium");
|
||||
}
|
||||
|
||||
// Nutrient half-life table (whole-body, in days)
|
||||
await execute(`CREATE TABLE IF NOT EXISTS nutrient_halflife (
|
||||
nutrient_key TEXT PRIMARY KEY,
|
||||
halflife_min_days REAL,
|
||||
halflife_max_days REAL,
|
||||
halflife_avg_days REAL,
|
||||
source TEXT
|
||||
)`);
|
||||
|
||||
const hlCount = await select("SELECT COUNT(*) as cnt FROM nutrient_halflife");
|
||||
if (hlCount[0].cnt === 0) {
|
||||
const hl = [
|
||||
["vit_a_mcg", 50, 180, 115, "fat-soluble, liver stores"],
|
||||
["vit_c_mg", 10, 20, 15, "Performance Lab review"],
|
||||
["vit_d_mcg", 30, 90, 60, "fat tissue, PMC4207933"],
|
||||
["vit_e_mg", 2, 5, 3.5, "ZIO review"],
|
||||
["vit_k_mcg", 1, 3, 1.5, "K1 pool turnover"],
|
||||
["vit_b1_mg", 9, 18, 13.5, "Ariaey-Nejad 1970"],
|
||||
["vit_b2_mg", 2, 7, 4, "limited storage"],
|
||||
["vit_b3_mg", 0.5, 2, 1, "rapid excretion"],
|
||||
["vit_b5_mg", 0.5, 2, 1, "rapid excretion"],
|
||||
["vit_b6_mg", 15, 20, 17.5, "muscle stores"],
|
||||
["vit_b7_mcg", 1, 3, 2, "tissue stores > plasma"],
|
||||
["folate_mcg", 60, 120, 100, "RBC folate ~8 weeks"],
|
||||
["vit_b12_mcg", 200, 770, 400, "liver, PubMed 14010994"],
|
||||
["calcium_mg", 1, 10, 5, "fast exchangeable pool"],
|
||||
["magnesium_mg", 30, 60, 42, "~1000 hours"],
|
||||
["iron_mg", 2000, 5000, 3650, "~10 years, no excretion"],
|
||||
["zinc_mg", 12, 30, 20, "fast pool"],
|
||||
["copper_mg", 13, 33, 23, "NCBI/PMC review"],
|
||||
["selenium_mcg", 50, 150, 100, "Griffith 1976"],
|
||||
["iodine_mg", 40, 80, 66, "ICRP/measured"],
|
||||
["manganese_mg", 20, 50, 37, "clinical data"],
|
||||
["molybdenum_mcg", 1, 3, 2, "plasma ~24-51h"],
|
||||
["chromium_mg", 300, 900, 616, "ICRP"],
|
||||
["phosphorus_mg", 14, 60, 30, "soft tissue pool"],
|
||||
["potassium_mg", 10, 28, 16, "K-42 tracer study"],
|
||||
["sodium_mg", 7, 15, 11, "ICRP"],
|
||||
["omega3_mg", 30, 90, 60, "fat-soluble stored"],
|
||||
["epa_mg", 30, 90, 60, "same pool as omega3"],
|
||||
["dha_mg", 30, 90, 60, "same pool as omega3"],
|
||||
["carotene_mcg", 5, 30, 15, "fat tissue"],
|
||||
["lutein_mg", 10, 30, 20, "macular pigment"],
|
||||
["lycopene_mg", 14, 28, 21, "fat tissue"],
|
||||
];
|
||||
for (const [key, min, max, avg, src] of hl) {
|
||||
await execute("INSERT INTO nutrient_halflife VALUES (?1,?2,?3,?4,?5)", [key, min, max, avg, src]);
|
||||
}
|
||||
console.log("[db.js] Seeded nutrient half-life data (32 nutrients)");
|
||||
}
|
||||
|
||||
// Recipe cached nutrients columns
|
||||
const recipeCols2 = (await select("PRAGMA table_info(recipes)")).map(r => r.name);
|
||||
if (!recipeCols2.includes("cached_nutrients")) {
|
||||
await execute("ALTER TABLE recipes ADD COLUMN cached_nutrients TEXT");
|
||||
}
|
||||
|
||||
console.log("[db.js] recipe migrations done, checking seq repair...");
|
||||
|
||||
// Repair old tracks: assign seq to points where it's NULL, ordered by timestamp.
|
||||
|
|
|
|||
|
|
@ -1820,6 +1820,8 @@ async function showRecipeEditorModal(recipeId) {
|
|||
rid, ing.product_id, ing.custom_name, ing.weight_g, ing.cooking_method_id
|
||||
);
|
||||
}
|
||||
// Cache computed nutrients on the recipe for daily tracking
|
||||
await DB.cacheRecipeNutrients(rid);
|
||||
renderScreen("recipes");
|
||||
});
|
||||
|
||||
|
|
@ -3593,7 +3595,7 @@ function showEditSupplementModal(existing) {
|
|||
if (displayVal && f.mult) displayVal = Math.round(displayVal / f.mult * 1000) / 1000; // mcg→mg for display
|
||||
return `<div class="form-group micro-field">
|
||||
<label>${f.label}</label>
|
||||
<input id="m-${f.key}" type="number" inputmode="decimal" step="any" value="${displayVal}" placeholder="—" />
|
||||
<input id="m-${f.key}" type="number" inputmode="decimal" step="any" value="${displayVal}" placeholder="${(() => { const r = BMR.RDI[f.key]; const t = BMR.RDI_TYPES[f.key]; return r ? r + (t === 'aup' ? '*' : '') : '—'; })()}" />
|
||||
</div>`;
|
||||
}
|
||||
).join("");
|
||||
|
|
@ -4256,6 +4258,17 @@ function calcDailyNutrients(food, suppTotals) {
|
|||
}
|
||||
}
|
||||
|
||||
// From recipes (cached_nutrients JSON in recipes table)
|
||||
for (const f of food) {
|
||||
if (!f.recipe_id || !f.cached_nutrients) continue;
|
||||
try {
|
||||
const rn = typeof f.cached_nutrients === "string" ? JSON.parse(f.cached_nutrients) : f.cached_nutrients;
|
||||
for (const key of Object.keys(RDI)) {
|
||||
if (rn[key]) totals[key] = (totals[key] || 0) + rn[key];
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// From supplements
|
||||
if (suppTotals) {
|
||||
for (const key of Object.keys(RDI)) {
|
||||
|
|
@ -4275,7 +4288,8 @@ function calcDailyNutrients(food, suppTotals) {
|
|||
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 });
|
||||
const normType = BMR.RDI_TYPES[key] || "rdi";
|
||||
result.push({ key, label, amount, rdi, pct, normType });
|
||||
}
|
||||
result.sort((a, b) => a.pct - b.pct);
|
||||
return result;
|
||||
|
|
@ -4286,7 +4300,8 @@ 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>`;
|
||||
const star = n.normType === "aup" ? "*" : "";
|
||||
return `<div class="nut-item ${cls}">${n.label}${star} ${n.pct}%</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue