Fix dedup: run BEFORE version check, RDI standards from DB
Dedup fix: - Dedup SQL now runs BEFORE seed version check (was inside "if new version" block, never executed when version matched). 16675→8330 products. - UNIQUE INDEX on (name_lower, source, COALESCE(store_id,'')) prevents future dupes - Errors propagated via map_err (no more silent failures) RDI standards from DB (not hardcoded): - rdi_standards table: RU, EU, US - rdi_values table: nutrient_key + value per standard - 26 nutrients seeded for all 3 standards - BMR.loadRDI(code) reads from DB, replaces hardcoded RDI object - Settings: "Стандарт РСП" dropdown (РФ/ЕС/США) - Profile stores selected standard, loaded at init - Fallback: hardcoded RU values until DB ready
This commit is contained in:
parent
01427941d4
commit
a0c7dfb5e2
4 changed files with 148 additions and 61 deletions
|
|
@ -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; } };
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">Нормы нутриентов</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Стандарт РСП</div>
|
||||
<div class="settings-item-desc">Пересчитает % нутриентов</div>
|
||||
</div>
|
||||
<select id="s-rdi-standard" style="width:80px;padding:6px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg)">
|
||||
<option value="RU">РФ</option>
|
||||
<option value="EU">ЕС</option>
|
||||
<option value="US">США</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">Карты</div>
|
||||
<div class="settings-item">
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -120,21 +120,7 @@ fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
|
|||
};
|
||||
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::<i64>().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<String, String> {
|
|||
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<String, String> {
|
|||
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::<i64>().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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue