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

184 lines
7.7 KiB
React
Raw Normal View History

import { useState } from 'react'
import { api } from '../utils/api'
function emptyIng() {
return { source_name_raw: '', quantity_g: '' }
}
function mappingHint(name, learned) {
const n = (name || '').trim().toLowerCase()
if (!n) return null
return (learned || []).find((m) => {
const raw = (m.source_name_raw || '').toLowerCase()
const norm = (m.source_name_normalized || '').toLowerCase()
return raw === n || norm === n || raw.includes(n) || n.includes(norm)
}) || null
}
export function RecipeForm({ defaultName, defaultPortions, defaultIngredients, learned, disabled, submitLabel, onSave, onCancel }) {
const [name, setName] = useState(defaultName || '')
const [portions, setPortions] = useState(defaultPortions != null ? String(defaultPortions) : '1')
const [ings, setIngs] = useState(
(defaultIngredients || []).length
? defaultIngredients.map((i) => ({ source_name_raw: i.source_name_raw || '', quantity_g: i.quantity_g ?? '' }))
: [emptyIng()]
)
const [error, setError] = useState(null)
const [saving, setSaving] = useState(false)
const save = async () => {
const ingredients = ings
.map((i) => ({
source_name_raw: (i.source_name_raw || '').trim(),
quantity_g: i.quantity_g === '' ? null : parseFloat(String(i.quantity_g).replace(',', '.')),
}))
.filter((i) => i.source_name_raw)
if (!name.trim()) {
setError('Rezeptname fehlt')
return
}
if (!ingredients.length) {
setError('Mindestens eine Zutat angeben')
return
}
setSaving(true)
setError(null)
try {
await onSave({
name_raw: name.trim(),
portions: parseFloat(String(portions).replace(',', '.')) || 1,
ingredients,
})
} catch (e) {
setError(e.message)
} finally {
setSaving(false)
}
}
return (
<div style={{ marginTop: 8, padding: 10, background: 'var(--surface2)', borderRadius: 8 }}>
<p style={{ fontSize: 12, margin: '0 0 8px', color: 'var(--text2)' }}>
Zutaten, die schon zugeordnet sind, werden übernommen. Offene erscheinen danach in der Liste zum Zuordnen.
</p>
{error && <div style={{ color: 'var(--danger)', fontSize: 13, marginBottom: 8 }}>{error}</div>}
<input className="form-input" style={{ width: '100%', textAlign: 'left', marginBottom: 8 }} placeholder="Rezeptname" value={name} onChange={(e) => setName(e.target.value)} />
<label style={{ fontSize: 12, color: 'var(--text2)' }}>
Portionen
<input className="form-input" type="number" min="0.1" step="0.1" style={{ width: 80, textAlign: 'left', marginLeft: 8 }} value={portions} onChange={(e) => setPortions(e.target.value)} />
</label>
<p style={{ fontSize: 12, margin: '10px 0 6px', color: 'var(--text2)' }}>Zutaten (Name + Gramm)</p>
{ings.map((ing, i) => {
const mapped = mappingHint(ing.source_name_raw, learned)
return (
<div key={i} style={{ marginBottom: 8 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 80px', gap: 6 }}>
<input className="form-input" style={{ textAlign: 'left' }} placeholder="z. B. Haferflocken" value={ing.source_name_raw} onChange={(e) => setIngs((list) => list.map((x, j) => j === i ? { ...x, source_name_raw: e.target.value } : x))} />
<input className="form-input" type="number" min="0" step="0.1" style={{ textAlign: 'left' }} placeholder="g" value={ing.quantity_g} onChange={(e) => setIngs((list) => list.map((x, j) => j === i ? { ...x, quantity_g: e.target.value } : x))} />
</div>
{ing.source_name_raw.trim() && (
<div style={{ fontSize: 11, color: mapped ? 'var(--accent-dark)' : 'var(--text3)', marginTop: 2 }}>
{mapped ? `zugeordnet: ${mapped.food_name_de || mapped.source_name_raw}` : 'noch offen — nach dem Speichern in der Liste zuordnen'}
</div>
)}
</div>
)
})}
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => setIngs((list) => [...list, emptyIng()])}>Zutat hinzufügen</button>
<div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
{onCancel && (
<button type="button" className="btn btn-secondary" disabled={saving} onClick={onCancel}>Zurück</button>
)}
<button type="button" className="btn btn-primary" style={{ flex: 1 }} disabled={disabled || saving} onClick={save}>
{saving ? 'Speichere…' : (submitLabel || 'Rezept speichern')}
</button>
</div>
</div>
)
}
export default function FoodRecipeEditor({ recipes, learned, onChanged }) {
const [open, setOpen] = useState(false)
const [editing, setEditing] = useState(null)
const [draft, setDraft] = useState(null)
const [error, setError] = useState(null)
const startNew = () => {
setEditing('new')
setDraft({ name: '', portions: '1' })
setError(null)
setOpen(true)
}
const startEdit = (r) => {
setEditing(r)
setDraft(r)
setError(null)
setOpen(true)
}
const save = async (body) => {
if (editing === 'new') await api.createNutritionRecipe(body)
else await api.updateNutritionRecipe(editing.id, body)
setEditing(null)
setDraft(null)
onChanged?.()
}
const remove = async (id) => {
if (!confirm('Rezept wirklich löschen?')) return
try {
await api.deleteNutritionRecipe(id)
if (editing && editing !== 'new' && editing.id === id) setEditing(null)
onChanged?.()
} catch (e) {
setError(e.message)
}
}
return (
<div style={{ marginTop: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<h3 style={{ fontSize: 14, margin: 0 }}>Eigene Rezepte / Listen ({recipes.length})</h3>
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => { setOpen((v) => !v); if (open) setEditing(null) }}>
{open ? 'Einklappen' : 'Anzeigen'}
</button>
</div>
{open && (
<div style={{ marginTop: 8 }}>
{error && <div style={{ color: 'var(--danger)', fontSize: 13, marginBottom: 8 }}>{error}</div>}
<button type="button" className="btn btn-secondary btn-full" onClick={startNew}>Rezept anlegen</button>
{recipes.map((r) => (
<div key={r.id} style={{ borderTop: '1px solid var(--border)', padding: '8px 0', marginTop: 8 }}>
<div style={{ fontWeight: 600 }}>{r.name_raw}</div>
<div style={{ fontSize: 12, color: 'var(--text3)' }}>
{(r.ingredients || []).length} Zutaten · {r.portions || 1} Portionen
{r.source === 'manual' ? ' · selbst angelegt' : ''}
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => startEdit(r)}>Bearbeiten</button>
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => remove(r.id)}>Löschen</button>
</div>
</div>
))}
{editing === 'new' && (
<RecipeForm learned={learned} submitLabel="Rezept anlegen" onSave={save} onCancel={() => setEditing(null)} />
)}
{editing && editing !== 'new' && (
<RecipeForm
key={editing.id}
defaultName={draft?.name_raw}
defaultPortions={draft?.portions}
defaultIngredients={draft?.ingredients}
learned={learned}
submitLabel="Rezept speichern"
onSave={save}
onCancel={() => setEditing(null)}
/>
)}
</div>
)}
</div>
)
}