mitai-jinkendo/frontend/src/components/NutritionFoodMap.jsx

145 lines
5.0 KiB
React
Raw Normal View History

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>
)
}