Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand

1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
   with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.

2. Omega 3-6-9: columns in supplements, in supplement totals query,
   in MICRO_FIELDS form (34 fields total now).

3. Product card micronutrients: searchProducts now fetches all micro
   columns. renderProdDetails shows nutrient chips (sorted by deficiency)
   when product is expanded.

4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
   iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.

5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
   Tap = toggle full view showing all nutrients.

6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
This commit is contained in:
4aiman 2026-05-29 14:47:09 +03:00
commit de5cfd5c21
4 changed files with 99 additions and 28 deletions

View file

@ -1452,6 +1452,11 @@ select {
flex-wrap: wrap;
gap: 6px;
padding: 8px 14px;
cursor: pointer;
}
.nutrient-strip.nutrient-collapsed {
max-height: 36px;
overflow: hidden;
}
.nut-item {
font-size: 12px;

Binary file not shown.

View file

@ -110,7 +110,9 @@ async function searchProducts(query, limit) {
return select(
`SELECT id, name, brand, category, weight_g, kcal_per_100g,
protein_per_100g, fat_per_100g, carbs_per_100g, fiber_per_100g,
composition
composition, nutrients_guessed,
vit_a_mcg, vit_c_mg, vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_e_mg,
calcium_mg, iron_mg, magnesium_mg, potassium_mg, sodium_mg, phosphorus_mg
FROM products
WHERE name_lower LIKE ?1
GROUP BY COALESCE(store_id, id)
@ -152,7 +154,8 @@ async function addProduct(p) {
p.source ?? "manual",
]
);
return r.lastInsertId;
const rows = await select("SELECT id FROM products ORDER BY id DESC LIMIT 1");
return rows[0]?.id;
}
// ---------------------------------------------------------------------------
@ -540,7 +543,10 @@ async function getSupplementTotalsForDate(date) {
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
SUM(COALESCE(sl.dose_g,1) * s.carotene_mcg) as carotene_mcg,
SUM(COALESCE(sl.dose_g,1) * s.omega3_mg) as omega3_mg,
SUM(COALESCE(sl.dose_g,1) * s.omega6_mg) as omega6_mg,
SUM(COALESCE(sl.dose_g,1) * s.omega9_mg) as omega9_mg
FROM supplement_log sl
JOIN supplements s ON sl.supplement_id = s.id
WHERE sl.date = ?1`,
@ -1120,6 +1126,9 @@ async function runMigrations() {
await addSuppCol("lutein_mg", "REAL"); // лютеин — no RDI
await addSuppCol("lycopene_mg", "REAL"); // ликопин — no RDI
await addSuppCol("carotene_mcg", "REAL"); // бета-каротин
await addSuppCol("omega3_mg", "REAL"); // омега-3
await addSuppCol("omega6_mg", "REAL"); // омега-6
await addSuppCol("omega9_mg", "REAL"); // омега-9
console.log("[db.js] recipe migrations done, checking seq repair...");

View file

@ -338,7 +338,7 @@ async function renderToday(container) {
<section class="log-section">
<div class="section-header"><h3>Нутриенты</h3></div>
<div class="nutrient-strip" id="daily-nutrients"></div>
<div class="nutrient-strip nutrient-collapsed" id="daily-nutrients"></div>
</section>
<section class="log-section">
@ -430,6 +430,8 @@ async function renderToday(container) {
const nutrients = calcDailyNutrients(food, suppTotals);
dailyNutEl.innerHTML = renderNutrientChips(nutrients)
|| `<div class="log-empty" style="padding:6px 14px">нет данных о нутриентах</div>`;
// Tap to expand/collapse
dailyNutEl.onclick = () => dailyNutEl.classList.toggle("nutrient-collapsed");
}
container.querySelector("#add-weight-btn").onclick = () => showWeightModal();
@ -893,11 +895,25 @@ function showFoodModal() {
<div id="m-manual-fields">
<div class="form-group">
<label>Или вручную: описание</label>
<input id="m-desc" type="text" placeholder="гречка с молоком" />
<input id="m-desc" type="text" placeholder="Тесто для чебуреков" />
</div>
<div class="form-group">
<label>ккал (примерно)</label>
<input id="m-kcal" type="number" inputmode="numeric" placeholder="350" />
<label>ккал на 100г</label>
<input id="m-kcal" type="number" inputmode="numeric" placeholder="260" />
</div>
<div style="display:flex;gap:8px">
<div class="form-group" style="flex:1">
<label>Б</label>
<input id="m-protein" type="number" inputmode="decimal" step="0.1" placeholder="7" />
</div>
<div class="form-group" style="flex:1">
<label>Ж</label>
<input id="m-fat" type="number" inputmode="decimal" step="0.1" placeholder="4" />
</div>
<div class="form-group" style="flex:1">
<label>У</label>
<input id="m-carbs" type="number" inputmode="decimal" step="0.1" placeholder="48" />
</div>
</div>
</div>
<div class="form-group">
@ -925,9 +941,22 @@ function showFoodModal() {
await DB.addFoodEntry(state.date, time, selectedProduct.id, amountG, null, unitId, unitAmount);
} else {
const desc = overlay.querySelector("#m-desc").value.trim();
const kcal = parseFloat(overlay.querySelector("#m-kcal").value) || null;
if (!desc) return false;
await DB.addFoodManual(state.date, time, desc, kcal);
const kcal = parseFloat(overlay.querySelector("#m-kcal").value) || null;
const protein = parseFloat(overlay.querySelector("#m-protein").value) || null;
const fat = parseFloat(overlay.querySelector("#m-fat").value) || null;
const carbs = parseFloat(overlay.querySelector("#m-carbs").value) || null;
const amount = rawAmount;
if (kcal || protein || fat || carbs) {
// Create a custom product in DB, then log it
const pid = await DB.addProduct({
name: desc, kcal_per_100g: kcal, protein_per_100g: protein,
fat_per_100g: fat, carbs_per_100g: carbs, source: "manual",
});
await DB.addFoodEntry(state.date, time, pid, amount, null);
} else {
await DB.addFoodManual(state.date, time, desc, kcal);
}
}
});
@ -1242,6 +1271,18 @@ async function renderProducts(container) {
function renderProdDetails(p) {
const r = v => v != null ? v : "—";
// Micronutrient chips for this product (per 100g)
const RDI = BMR.RDI;
const microChips = [];
for (const [key, rdi] of Object.entries(RDI)) {
if (!p[key]) continue;
const pct = Math.round(p[key] / rdi * 100);
if (pct < 1) continue;
const label = NUTRIENT_LABELS[key] || key;
const cls = pct < 30 ? "nut-low" : pct < 80 ? "nut-mid" : "nut-ok";
microChips.push({ pct, html: `<div class="nut-item ${cls}">${label} ${pct}%</div>` });
}
microChips.sort((a, b) => a.pct - b.pct);
return `
<div class="prod-details">
<div class="prod-macros">
@ -1250,6 +1291,7 @@ async function renderProducts(container) {
<div class="macro"><span class="macro-val">${r(p.carbs_per_100g)}</span><span class="macro-lbl">У</span></div>
<div class="macro"><span class="macro-val">${r(p.fiber_per_100g)}</span><span class="macro-lbl">Кл</span></div>
</div>
${microChips.length ? `<div class="nutrient-strip" style="padding:4px 0">${microChips.map(c => c.html).join("")}</div>` : ""}
${p.composition ? `<div class="prod-composition">${esc(p.composition)}</div>` : ""}
<button class="btn-primary prod-add-btn" data-pid="${p.id}">Добавить в дневник</button>
</div>`;
@ -3245,25 +3287,40 @@ async function showLogSupplementModal() {
// ---------------------------------------------------------------------------
const MICRO_FIELDS = [
{ key: "vit_a_mcg", label: "Вит A (мкг)" },
{ key: "vit_c_mg", label: "Вит C (мг)" },
{ key: "vit_d_mcg", label: "Вит D (мкг)" },
{ key: "vit_e_mg", label: "Вит E (мг)" },
{ key: "vit_k_mcg", label: "Вит K (мкг)" },
{ key: "vit_b1_mg", label: "B1 (мг)" },
{ key: "vit_b2_mg", label: "B2 (мг)" },
{ key: "vit_b3_mg", label: "B3 (мг)" },
{ key: "vit_b6_mg", label: "B6 (мг)" },
{ key: "vit_b12_mcg", label: "B12 (мкг)" },
{ key: "folate_mcg", label: "Фолат (мкг)" },
{ key: "calcium_mg", label: "Ca (мг)" },
{ key: "iron_mg", label: "Fe (мг)" },
{ key: "magnesium_mg", label: "Mg (мг)" },
{ key: "zinc_mg", label: "Zn (мг)" },
{ key: "copper_mg", label: "Cu (мг)" },
{ key: "selenium_mcg", label: "Se (мкг)" },
{ key: "potassium_mg", label: "K (мг)" },
{ key: "sodium_mg", label: "Na (мг)" },
{ key: "vit_a_mcg", label: "Вит A (мкг РЭ)" },
{ key: "vit_c_mg", label: "Вит C (мг)" },
{ key: "vit_d_mcg", label: "Вит D3 (мкг)" },
{ key: "vit_e_mg", label: "Вит E (мг ТЭ)" },
{ key: "vit_k_mcg", label: "Вит K (мкг)" },
{ key: "vit_b1_mg", label: "B1 (мг)" },
{ key: "vit_b2_mg", label: "B2 (мг)" },
{ key: "vit_b3_mg", label: "B3/PP (мг)" },
{ key: "vit_b5_mg", label: "B5 (мг)" },
{ key: "vit_b6_mg", label: "B6 (мг)" },
{ key: "vit_b7_mcg", label: "B7/биотин (мкг)" },
{ key: "folate_mcg", label: "B9/фолат (мкг)" },
{ key: "vit_b12_mcg", label: "B12 (мкг)" },
{ key: "calcium_mg", label: "Ca (мг)" },
{ key: "magnesium_mg", label: "Mg (мг)" },
{ key: "iron_mg", label: "Fe (мг)" },
{ key: "zinc_mg", label: "Zn (мг)" },
{ key: "copper_mg", label: "Cu (мг)" },
{ key: "selenium_mcg", label: "Se (мкг)" },
{ key: "potassium_mg", label: "K (мг)" },
{ key: "sodium_mg", label: "Na (мг)" },
{ key: "phosphorus_mg", label: "P (мг)" },
{ key: "iodine_mg", label: "Йод (мг)" },
{ key: "chromium_mg", label: "Хром (мг)" },
{ key: "manganese_mg", label: "Mn (мг)" },
{ key: "molybdenum_mcg", label: "Mo (мкг)" },
{ key: "carotene_mcg", label: "b-каротин (мкг)" },
{ key: "omega3_mg", label: "Омега-3 (мг)" },
{ key: "omega6_mg", label: "Омега-6 (мг)" },
{ key: "omega9_mg", label: "Омега-9 (мг)" },
{ key: "silicon_mg", label: "Кремний (мг)" },
{ key: "vanadium_mg", label: "Ванадий (мг)" },
{ key: "lutein_mg", label: "Лютеин (мг)" },
{ key: "lycopene_mg", label: "Ликопин (мг)" },
];
async function renderSupplements(container) {