mitai-jinkendo/frontend/src/components/NutritionFoodMap.jsx
Lars 919c77fcf8
Some checks failed
Deploy Development / deploy (push) Failing after 52s
Build Test / pytest-backend (push) Successful in 3s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Failing after 9s
feat: BLS-Stammdaten, FDDB-Mapping und Item-Tagebuch (#106)
Katalog, lernendes Mapping ohne KI, optionale Items und Import-Policy.
Playwright-Smoke und Issue-Audit um Ernährung/Zuordnen/API ergänzt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 14:35:57 +02:00

145 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import { api } from '../utils/api'
export default function NutritionFoodMap({ onChanged }) {
const [unmapped, setUnmapped] = useState([])
const [learned, setLearned] = useState([])
const [error, setError] = useState(null)
const [query, setQuery] = useState({})
const [hits, setHits] = useState({})
const [saving, setSaving] = useState(null)
const load = async () => {
try {
const [u, m] = await Promise.all([api.listUnmappedFoods(), api.listMyFoodMappings()])
setUnmapped(u)
setLearned(m)
} catch (e) {
setError(e.message)
}
}
useEffect(() => { load() }, [])
const search = async (key, q) => {
setQuery((s) => ({ ...s, [key]: q }))
if (!q || q.length < 2) {
setHits((s) => ({ ...s, [key]: [] }))
return
}
try {
const rows = await api.searchBlsFoods(q)
setHits((s) => ({ ...s, [key]: rows }))
} catch (e) {
setError(e.message)
}
}
const assign = async (sourceName, foodId, key) => {
setSaving(key)
setError(null)
try {
await api.upsertMyFoodMapping({ source_name: sourceName, food_id: foodId })
setHits((s) => ({ ...s, [key]: [] }))
await load()
onChanged?.()
} catch (e) {
setError(e.message)
} finally {
setSaving(null)
}
}
const remove = async (id) => {
if (!confirm('Zuordnung wirklich löschen?')) return
try {
await api.deleteMyFoodMapping(id)
await load()
onChanged?.()
} catch (e) {
setError(e.message)
}
}
return (
<div className="card section-gap">
<div className="card-title">Lebensmittel zuordnen</div>
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginBottom: 12 }}>
Einmal bestätigt, bleibt die Zuordnung erhalten und gilt für spätere Importe.
Du kannst sie jederzeit ändern oder löschen. Keine automatische KI-Zuordnung.
</p>
{error && <div style={{ color: 'var(--danger)', fontSize: 13, marginBottom: 10 }}>{error}</div>}
<h3 style={{ fontSize: 14, margin: '12px 0 8px' }}>Offen ({unmapped.length})</h3>
{unmapped.length === 0 && <p className="muted">Keine ungemappten Bezeichner.</p>}
{unmapped.map((u) => {
const key = u.source_name_normalized
return (
<div key={key} style={{ borderTop: '1px solid var(--border)', padding: '10px 0' }}>
<div style={{ fontWeight: 600 }}>{u.source_name_raw}</div>
<div style={{ fontSize: 12, color: 'var(--text3)' }}>
{u.count}× · {u.first_date} {u.last_date}
</div>
<input
className="form-input"
style={{ marginTop: 6 }}
placeholder="BLS-Code oder Name suchen…"
value={query[key] || ''}
onChange={(e) => search(key, e.target.value)}
/>
{(hits[key] || []).map((h) => (
<button
key={h.id}
type="button"
className="btn btn-secondary"
style={{ marginTop: 6, marginRight: 6 }}
disabled={saving === key}
onClick={() => assign(u.source_name_raw, h.id, key)}
>
{h.bls_code ? `${h.bls_code} · ` : ''}{h.name_de}
{h.catalog_kind !== 'official_bls' ? ' (manuell)' : ''}
</button>
))}
</div>
)
})}
<h3 style={{ fontSize: 14, margin: '20px 0 8px' }}>Gelernt ({learned.length})</h3>
{learned.map((m) => (
<div key={m.id} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '8px 0', borderTop: '1px solid var(--border)' }}>
<div>
<div style={{ fontWeight: 500 }}>{m.source_name_raw}</div>
<div style={{ fontSize: 12, color: 'var(--text3)' }}>
{m.bls_code ? `${m.bls_code} · ` : ''}{m.food_name_de}
{m.catalog_kind !== 'official_bls' ? ' (manuell)' : ''}
</div>
</div>
<button type="button" className="btn btn-secondary" onClick={() => remove(m.id)}>Löschen</button>
</div>
))}
</div>
)
}
export function DayMarkButtons({ date, markType, onChanged }) {
const setMark = async (type) => {
try {
if (markType === type) await api.deleteNutritionDayMark(date)
else await api.putNutritionDayMark(date, { mark_type: type })
onChanged?.()
} catch (e) {
alert(e.message)
}
}
return (
<span style={{ display: 'inline-flex', gap: 4 }}>
<button type="button" className="btn btn-secondary" style={{ fontSize: 11, padding: '4px 8px' }} onClick={() => setMark('fasting')}>
{markType === 'fasting' ? '✓ Fasten' : 'Fasten'}
</button>
<button type="button" className="btn btn-secondary" style={{ fontSize: 11, padding: '4px 8px' }} onClick={() => setMark('incomplete')}>
{markType === 'incomplete' ? '✓ Lücke' : 'Lücke'}
</button>
</span>
)
}