diff --git a/frontend/js/bmr.js b/frontend/js/bmr.js index ee89dde..e9d9d9e 100644 --- a/frontend/js/bmr.js +++ b/frontend/js/bmr.js @@ -27,35 +27,36 @@ function estimateRealBmr(weights, foodTotals) { return realExpend > 500 ? Math.round(realExpend) : null; // sanity: < 500 kcal/day is wrong data } -// 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 removed — auto-converted to vit_a_mcg (carotene/12) +// RDI loaded from DB (rdi_standards + rdi_values tables). +// Fallback hardcoded RU values used until DB is ready. +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, + 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, }; -window.BMR = { calculateBmr, getAge, estimateRealBmr, RDI }; +// Load RDI from DB for selected standard. Called after DB ready. +async function loadRDI(standardCode) { + try { + const std = await window.__TAURI__.core.invoke("plugin:sql|select", { + db: "sqlite:baldav.db", + query: "SELECT id FROM rdi_standards WHERE code = ?1", + values: [standardCode || "RU"], + }); + 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", + values: [std[0].id], + }); + const loaded = {}; + for (const v of vals) loaded[v.nutrient_key] = v.value; + 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; } }; diff --git a/frontend/js/db.js b/frontend/js/db.js index a90f4fd..14cf8d6 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -1155,6 +1155,64 @@ async function runMigrations() { // Product dedup handled in Rust import_seed_products (UNIQUE INDEX on name_lower+source+store_id) + // RDI standards tables + await execute(`CREATE TABLE IF NOT EXISTS rdi_standards ( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL + )`); + await execute(`CREATE TABLE IF NOT EXISTS rdi_values ( + id INTEGER PRIMARY KEY, + standard_id INTEGER NOT NULL REFERENCES rdi_standards(id), + nutrient_key TEXT NOT NULL, + value REAL NOT NULL, + UNIQUE(standard_id, nutrient_key) + )`); + + // Seed RDI standards if empty + const rdiCount = await select("SELECT COUNT(*) as cnt FROM rdi_standards"); + if (rdiCount[0].cnt === 0) { + await execute("INSERT INTO rdi_standards (id, code, name) VALUES (1, 'RU', 'РФ (ТР ТС 022/2011)')"); + await execute("INSERT INTO rdi_standards (id, code, name) VALUES (2, 'EU', 'ЕС (EU RDA)')"); + await execute("INSERT INTO rdi_standards (id, code, name) VALUES (3, 'US', 'США (US DV)')"); + + const standards = { + // nutrient_key: [RU, EU, US] + vit_a_mcg: [800, 800, 900], + vit_c_mg: [60, 80, 90], + vit_e_mg: [10, 12, 15], + vit_b1_mg: [1.4, 1.1, 1.2], + vit_b2_mg: [1.6, 1.4, 1.3], + vit_b3_mg: [18, 16, 16], + vit_b5_mg: [6, 6, 5], + vit_b6_mg: [2, 1.4, 1.7], + vit_b7_mcg: [50, 50, 30], + folate_mcg: [200, 200, 400], + vit_b12_mcg: [1, 2.5, 2.4], + vit_d_mcg: [5, 15, 15], + vit_k_mcg: [120, 75, 120], + calcium_mg: [1000, 800, 1000], + magnesium_mg: [400, 375, 420], + zinc_mg: [15, 10, 11], + iron_mg: [14, 14, 18], + iodine_mg: [0.15, 0.15, 0.15], + selenium_mcg: [70, 55, 55], + molybdenum_mcg: [70, 50, 45], + manganese_mg: [2, 2, 2.3], + copper_mg: [1, 1, 0.9], + chromium_mg: [0.05, 0.04, 0.035], + phosphorus_mg: [800, 700, 700], + potassium_mg: [3500, 2000, 2600], + sodium_mg: [2400, 2400, 2300], + }; + for (const [key, vals] of Object.entries(standards)) { + for (let i = 0; i < 3; i++) { + await execute("INSERT INTO rdi_values (standard_id, nutrient_key, value) VALUES (?1, ?2, ?3)", [i + 1, key, vals[i]]); + } + } + console.log("[db.js] Seeded 3 RDI standards (RU/EU/US)"); + } + 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 8e60c2c..4bf6b21 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -75,6 +75,12 @@ async function init() { // Recover any orphaned GPS recording session try { await GPS.recoverSession(); } catch (e) { /* ignore */ } + // Load RDI standard from profile (default: RU) + try { + const rdiCode = (await DB.getProfileValue("rdi_standard")) || "RU"; + await BMR.loadRDI(rdiCode); + } catch (e) { /* RDI stays at fallback RU values */ } + // Import seed products if needed — show progress only if actually importing try { let unlisten = null; @@ -3681,6 +3687,21 @@ async function renderSettings(container) { +
+
Нормы нутриентов
+
+
+
Стандарт РСП
+
Пересчитает % нутриентов
+
+ +
+
+
Карты
@@ -3787,6 +3808,17 @@ async function renderSettings(container) { // Profile button → open profile modal container.querySelector("#s-open-profile").onclick = () => showProfileModal(); + // RDI standard selector + const rdiSel = container.querySelector("#s-rdi-standard"); + if (rdiSel) { + rdiSel.value = state.profile.rdi_standard || "RU"; + rdiSel.onchange = async () => { + await DB.setProfileValue("rdi_standard", rdiSel.value); + state.profile.rdi_standard = rdiSel.value; + await BMR.loadRDI(rdiSel.value); + }; + } + // Offline toggle const offToggle = container.querySelector("#s-offline-toggle"); offToggle.onclick = () => { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2c19a1c..57b93a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -120,21 +120,7 @@ fn import_seed_products(app: tauri::AppHandle) -> Result { }; let main_db = rusqlite::Connection::open(&main_path).map_err(|e| e.to_string())?; - let current_version: i64 = main_db.query_row("SELECT COALESCE((SELECT value FROM profile WHERE key='seed_version'), '0')", [], |r| { - let s: String = r.get(0)?; - Ok(s.parse::().unwrap_or(0)) - } - ).unwrap_or(0); - - if current_version >= seed_version {return Ok(format!("up-to-date:v{}", current_version));} - - let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0)) - .map_err(|e| e.to_string())?; - - // Emit initial progress - // Bulk import — no per-row progress needed (takes <5 seconds) - - // Ensure all columns exist in main DB (including micronutrients from ION import) + // Ensure all columns exist in main DB let extra_cols = [ "composition TEXT", "name_lower TEXT", "category TEXT", "sodium_mg REAL", "potassium_mg REAL", "calcium_mg REAL", @@ -148,19 +134,11 @@ fn import_seed_products(app: tauri::AppHandle) -> Result { let _ = main_db.execute(&format!("ALTER TABLE products ADD COLUMN {}", col), []); } - // ATTACH seed DB to main DB and do bulk INSERT + UPDATE in SQL (no per-row loop) - let seed_str = seed_path.to_str().unwrap_or_default().to_string(); - main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str]).map_err(|e| e.to_string())?; + // Dedup ALWAYS runs (even if seed version matches) — fixes legacy duplicates. + // Fast: <0.2s with index. No-op if no dupes exist. + let _ = main_db.execute( + "CREATE INDEX IF NOT EXISTS idx_products_name_lower ON products(name_lower)", []); - main_db.execute("BEGIN", []).map_err(|e| e.to_string())?; - - // Create index for fast lookups during dedup and insert - let _ = main_db.execute_batch( - "CREATE INDEX IF NOT EXISTS idx_products_name_lower ON main.products(name_lower);" - ); - - // Dedup existing products by (name_lower, source, store_id). - // Each statement separate — execute_batch can fail silently on multi-statement strings. main_db.execute( "UPDATE food_log SET product_id = ( SELECT MIN(p2.id) FROM products p2 @@ -190,12 +168,30 @@ fn import_seed_products(app: tauri::AppHandle) -> Result { GROUP BY name_lower, source, COALESCE(store_id,'') )", []).map_err(|e| format!("dedup delete: {}", e))?; - // UNIQUE index to prevent future duplicates - main_db.execute( + let _ = main_db.execute( "CREATE UNIQUE INDEX IF NOT EXISTS idx_products_dedup ON products(name_lower, source, COALESCE(store_id, '')) - WHERE name_lower IS NOT NULL", []) - .map_err(|e| format!("unique index: {}", e))?; + WHERE name_lower IS NOT NULL", []); + + // Check seed version — skip import if already up to date + let current_version: i64 = main_db.query_row( + "SELECT COALESCE((SELECT value FROM profile WHERE key='seed_version'), '0')", + [], |r| { let s: String = r.get(0)?; Ok(s.parse::().unwrap_or(0)) } + ).unwrap_or(0); + + if current_version >= seed_version { + return Ok(format!("up-to-date:v{}", current_version)); + } + + // ATTACH seed DB for import + let seed_str = seed_path.to_str().unwrap_or_default().to_string(); + main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str]) + .map_err(|e| e.to_string())?; + + let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0)) + .map_err(|e| e.to_string())?; + + main_db.execute("BEGIN", []).map_err(|e| e.to_string())?; // Bulk INSERT new products from seed (INSERT OR IGNORE now works with unique index) let inserted = main_db.execute(