2026-09-12 16:14:24 +02:00
import { useEffect , useMemo , useRef , useState } from 'react'
2026-09-12 14:35:57 +02:00
import { api } from '../utils/api'
2026-09-12 15:31:20 +02:00
import FoodSearchModal from './FoodSearchModal'
2026-09-12 16:59:24 +02:00
import FoodNutrientFields , { foodCreatePayload } from './FoodNutrientFields'
import FoodRecipeEditor , { RecipeForm } from './FoodRecipeEditor'
2026-09-12 15:31:20 +02:00
2026-09-12 16:14:24 +02:00
const PERIODS = [
{ days : 14 , label : '14 Tage' } ,
{ days : 28 , label : '4 Wochen' } ,
{ days : 90 , label : '90 Tage' } ,
{ days : 0 , label : 'Alle' } ,
]
function formatDay ( iso ) {
const s = String ( iso || '' ) . slice ( 0 , 10 )
if ( s . length < 10 ) return ''
const [ y , m , d ] = s . split ( '-' )
return ` ${ d } . ${ m } . ${ String ( y ) . slice ( 2 ) } `
}
2026-09-12 15:31:20 +02:00
function suggestQuery ( raw ) {
let s = ( raw || '' ) . replace ( /^\s*[!]?\s*\d+(?:[.,]\d+)?\s*(?:g|kg|ml|l|stück|stk)?\s*/i , '' ) . trim ( )
s = s . replace ( /^!+/ , '' ) . trim ( )
const comma = s . indexOf ( ',' )
if ( comma > 2 ) s = s . slice ( 0 , comma ) . trim ( )
return s
}
function RecipePickModal ( { recipes , sourceName , onPick , onClose } ) {
const [ q , setQ ] = useState ( suggestQuery ( sourceName ) )
const filtered = recipes . filter ( ( r ) => {
const hay = ` ${ r . name _raw || '' } ${ r . name _normalized || '' } ` . toLowerCase ( )
return ! q . trim ( ) || hay . includes ( q . trim ( ) . toLowerCase ( ) )
} )
return (
< div
style = { {
position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.45)' ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' , zIndex : 20000 , padding : 16 ,
} }
onClick = { onClose }
>
< div
role = "dialog"
aria - modal = "true"
onClick = { ( e ) => e . stopPropagation ( ) }
style = { {
width : '100%' , maxWidth : 480 , maxHeight : 'min(88vh, 560px)' ,
background : 'var(--surface)' , borderRadius : 16 ,
boxShadow : '0 8px 32px rgba(0,0,0,0.18)' ,
display : 'flex' , flexDirection : 'column' ,
} }
>
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , padding : '14px 16px' , borderBottom : '1px solid var(--border)' } } >
< h2 className = "card-title" style = { { margin : 0 , fontSize : 16 } } > Eigenes Rezept wählen < / h2 >
< button type = "button" className = "btn btn-secondary" onClick = { onClose } > Schließen < / button >
< / div >
< div style = { { padding : '12px 16px' } } >
2026-09-12 16:14:24 +02:00
< input className = "form-input" style = { { width : '100%' , textAlign : 'left' } } autoFocus placeholder = "Rezeptname filtern" value = { q } onChange = { ( e ) => setQ ( e . target . value ) } / >
2026-09-12 15:31:20 +02:00
< / div >
< div style = { { overflowY : 'auto' , padding : '0 16px 16px' , flex : 1 } } >
2026-09-12 16:14:24 +02:00
{ filtered . length === 0 && < p style = { { fontSize : 13 , color : 'var(--text3)' } } > Kein passendes Rezept . < / p > }
2026-09-12 15:31:20 +02:00
{ filtered . map ( ( r ) => (
2026-09-12 16:14:24 +02:00
< button key = { r . id } type = "button" className = "btn btn-secondary btn-full" style = { { marginTop : 8 , justifyContent : 'flex-start' , textAlign : 'left' , height : 'auto' , padding : '10px 12px' } } onClick = { ( ) => onPick ( r . id ) } >
2026-09-12 15:31:20 +02:00
< span >
< strong style = { { display : 'block' } } > { r . name _raw } < / strong >
2026-09-12 16:14:24 +02:00
< span style = { { fontSize : 12 , color : 'var(--text3)' } } > { ( r . ingredients || [ ] ) . length } Zutaten < / span >
2026-09-12 15:31:20 +02:00
< / span >
< / button >
) ) }
< / div >
< / div >
< / div >
)
}
2026-09-12 14:35:57 +02:00
2026-09-12 16:59:24 +02:00
function InlineCreate ( { defaultName , disabled , learned , allowRecipe , onCreateFood , onCreateRecipe } ) {
const [ kind , setKind ] = useState ( allowRecipe ? null : 'food' )
2026-09-12 16:14:24 +02:00
const [ name , setName ] = useState ( defaultName || '' )
2026-09-12 16:59:24 +02:00
const [ macros , setMacros ] = useState ( { kcal : '0' , protein _g : '0' , fat _g : '0' , carbs _g : '0' } )
const [ extras , setExtras ] = useState ( [ ] )
const [ servingG , setServingG ] = useState ( '100' )
if ( ! kind ) {
return (
< div style = { { marginTop : 8 , padding : 10 , background : 'var(--surface2)' , borderRadius : 8 } } >
< p style = { { fontSize : 12 , margin : '0 0 8px' , color : 'var(--text2)' } } > Lebensmittel ( ein Eintrag im Katalog ) oder Rezept ( mehrere Zutaten ) ? < / p >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 } } >
< button type = "button" className = "btn btn-primary" disabled = { disabled } onClick = { ( ) => setKind ( 'food' ) } > Lebensmittel < / button >
{ allowRecipe && (
< button type = "button" className = "btn btn-secondary" disabled = { disabled } onClick = { ( ) => setKind ( 'recipe' ) } > Rezept < / button >
) }
< / div >
< / div >
)
}
if ( kind === 'recipe' ) {
return (
< RecipeForm
defaultName = { defaultName }
learned = { learned }
disabled = { disabled }
submitLabel = "Rezept anlegen und zuordnen"
onCancel = { ( ) => setKind ( null ) }
onSave = { onCreateRecipe }
/ >
)
}
2026-09-12 16:14:24 +02:00
return (
< div style = { { marginTop : 8 , padding : 10 , background : 'var(--surface2)' , borderRadius : 8 } } >
2026-09-12 16:59:24 +02:00
< p style = { { fontSize : 12 , margin : '0 0 8px' , color : 'var(--text2)' } } > Neuer Katalogeintrag . Salz : Makros 0. Öl / Supplement : Stoffe wie EPA dazusuchen . < / p >
2026-09-12 16:14:24 +02:00
< input className = "form-input" style = { { width : '100%' , textAlign : 'left' , marginBottom : 8 } } value = { name } onChange = { ( e ) => setName ( e . target . value ) } placeholder = "Name" / >
2026-09-12 16:59:24 +02:00
< FoodNutrientFields
macros = { macros }
setMacros = { setMacros }
extras = { extras }
setExtras = { setExtras }
servingG = { servingG }
setServingG = { setServingG }
/ >
< div style = { { display : 'flex' , gap : 8 , marginTop : 8 } } >
< button type = "button" className = "btn btn-secondary" disabled = { disabled } onClick = { ( ) => setKind ( null ) } > Zurück < / button >
< button
type = "button"
className = "btn btn-primary"
style = { { flex : 1 } }
disabled = { disabled || ! name . trim ( ) }
onClick = { ( ) => onCreateFood ( foodCreatePayload ( name , macros , extras , servingG ) ) }
>
Anlegen und zuordnen
< / button >
2026-09-12 16:14:24 +02:00
< / div >
< / div >
)
}
export default function NutritionFoodMap ( { onChanged , onMapped } ) {
2026-09-12 14:35:57 +02:00
const [ unmapped , setUnmapped ] = useState ( [ ] )
const [ learned , setLearned ] = useState ( [ ] )
2026-09-12 15:31:20 +02:00
const [ recipes , setRecipes ] = useState ( [ ] )
2026-09-12 14:35:57 +02:00
const [ error , setError ] = useState ( null )
2026-09-12 15:31:20 +02:00
const [ notice , setNotice ] = useState ( null )
2026-09-12 14:35:57 +02:00
const [ saving , setSaving ] = useState ( null )
2026-09-12 15:31:20 +02:00
const [ searchFor , setSearchFor ] = useState ( null )
const [ recipeFor , setRecipeFor ] = useState ( null )
2026-09-12 16:14:24 +02:00
const [ createFor , setCreateFor ] = useState ( null )
const [ filter , setFilter ] = useState ( 'all' )
const [ sinceDays , setSinceDays ] = useState ( 28 )
const [ totalOpen , setTotalOpen ] = useState ( 0 )
const [ q , setQ ] = useState ( '' )
const [ visible , setVisible ] = useState ( 80 )
const [ showLearned , setShowLearned ] = useState ( false )
2026-09-12 15:31:20 +02:00
const [ importing , setImporting ] = useState ( false )
2026-09-12 15:49:14 +02:00
const [ busy , setBusy ] = useState ( false )
2026-09-12 15:31:20 +02:00
const listRef = useRef ( null )
2026-09-12 15:49:14 +02:00
const bundleRef = useRef ( null )
const loadGen = useRef ( 0 )
2026-09-12 16:14:24 +02:00
const askedSuggest = useRef ( new Set ( ) )
2026-09-12 14:35:57 +02:00
const load = async ( ) => {
2026-09-12 15:49:14 +02:00
const gen = ++ loadGen . current
2026-09-12 14:35:57 +02:00
try {
2026-09-12 16:14:24 +02:00
const [ u , m , r , counts ] = await Promise . all ( [
api . listUnmappedFoods ( sinceDays ) ,
2026-09-12 15:31:20 +02:00
api . listMyFoodMappings ( ) ,
api . listNutritionRecipes ( ) . catch ( ( ) => [ ] ) ,
2026-09-12 16:14:24 +02:00
api . listUnmappedFoodCount ( sinceDays ) . catch ( ( ) => null ) ,
2026-09-12 15:31:20 +02:00
] )
2026-09-12 15:49:14 +02:00
if ( gen !== loadGen . current ) return
2026-09-12 16:14:24 +02:00
askedSuggest . current = new Set ( )
setUnmapped ( Array . isArray ( u ) ? u : [ ] )
setTotalOpen ( Number ( counts ? . total ) || ( Array . isArray ( u ) ? u . length : 0 ) )
2026-09-12 14:35:57 +02:00
setLearned ( m )
2026-09-12 15:31:20 +02:00
setRecipes ( Array . isArray ( r ) ? r : [ ] )
2026-09-12 14:35:57 +02:00
} catch ( e ) {
2026-09-12 15:49:14 +02:00
if ( gen !== loadGen . current ) return
2026-09-12 14:35:57 +02:00
setError ( e . message )
}
}
2026-09-12 16:14:24 +02:00
useEffect ( ( ) => { load ( ) } , [ sinceDays ] )
const filtered = useMemo ( ( ) => {
const term = q . trim ( ) . toLowerCase ( )
return unmapped . filter ( ( u ) => {
if ( filter === 'suggested' && ! ( u . suggestions || [ ] ) . length ) return false
if ( filter === 'none' && ( u . suggestions || [ ] ) . length ) return false
if ( ! term ) return true
const hay = ` ${ u . source _name _raw || '' } ${ u . source _name _normalized || '' } ` . toLowerCase ( )
return hay . includes ( term )
} )
} , [ unmapped , filter , q ] )
2026-09-12 14:35:57 +02:00
2026-09-12 16:14:24 +02:00
const shown = filtered . slice ( 0 , visible )
useEffect ( ( ) => {
const need = [ ]
const seen = new Set ( )
const take = ( u ) => {
const key = u . source _name _normalized
if ( ! key || askedSuggest . current . has ( key ) || seen . has ( key ) ) return
seen . add ( key )
need . push ( u )
}
shown . forEach ( take )
if ( need . length < 80 ) {
for ( const u of unmapped ) {
take ( u )
if ( need . length >= 80 ) break
}
}
if ( ! need . length ) return
need . forEach ( ( u ) => askedSuggest . current . add ( u . source _name _normalized ) )
const names = need . map ( ( u ) => u . source _name _raw ) . filter ( Boolean )
if ( ! names . length ) return
api . suggestFoodsBatch ( names , 3 ) . then ( ( packed ) => {
if ( ! packed || typeof packed !== 'object' ) return
setUnmapped ( ( list ) => list . map ( ( u ) => {
const hit = packed [ u . source _name _raw ]
return hit ? { ... u , ... hit } : u
} ) )
} ) . catch ( ( ) => {
need . forEach ( ( u ) => askedSuggest . current . delete ( u . source _name _normalized ) )
} )
} , [ visible , filter , q , unmapped . length ] )
const assign = async ( row , foodId , extras = { } , foodMeta = { } ) => {
const sourceName = row . source _name _raw
2026-09-12 15:31:20 +02:00
setSaving ( sourceName )
setError ( null )
2026-09-12 14:35:57 +02:00
try {
2026-09-12 16:14:24 +02:00
const res = await api . upsertMyFoodMapping ( {
2026-09-12 15:49:14 +02:00
source _name : sourceName ,
food _id : foodId ,
grams _per _unit : extras . grams _per _unit || null ,
source _unit : extras . source _unit || null ,
} )
2026-09-12 15:31:20 +02:00
setSearchFor ( null )
2026-09-12 16:14:24 +02:00
setCreateFor ( null )
setUnmapped ( ( list ) => list . filter ( ( x ) => x . source _name _normalized !== row . source _name _normalized ) )
setLearned ( ( list ) => [ {
id : res . mapping _id ,
source _name _raw : sourceName ,
food _name _de : res . food _name _de || foodMeta . name _de ,
bls _code : res . bls _code || foodMeta . bls _code ,
catalog _kind : res . catalog _kind || foodMeta . catalog _kind ,
} , ... list ] )
setNotice ( ` Gespeichert: ${ suggestQuery ( sourceName ) || sourceName } ` )
onMapped ? . ( )
2026-09-12 14:35:57 +02:00
} catch ( e ) {
setError ( e . message )
2026-09-12 15:31:20 +02:00
} finally {
setSaving ( null )
2026-09-12 14:35:57 +02:00
}
}
2026-09-12 16:14:24 +02:00
const createAndAssign = async ( row , body ) => {
setSaving ( row . source _name _raw )
setError ( null )
try {
const food = await api . createUserFood ( body )
await assign ( row , food . id , { } , { name _de : food . name _de , catalog _kind : food . catalog _kind } )
} catch ( e ) {
setError ( e . message )
setSaving ( null )
}
}
2026-09-12 16:59:24 +02:00
const createRecipeAndAssign = async ( row , body ) => {
setSaving ( row . source _name _raw )
setError ( null )
try {
const rec = await api . createNutritionRecipe ( { ... body , source : 'manual' } )
await api . applyNutritionRecipe ( rec . id , row . source _name _raw )
setCreateFor ( null )
setSearchFor ( null )
setNotice ( ` Rezept angelegt. Offene Zutaten erscheinen in der Liste. ` )
await load ( )
onMapped ? . ( )
} catch ( e ) {
setError ( e . message )
throw e
} finally {
setSaving ( null )
}
}
2026-09-12 15:31:20 +02:00
const applyRecipe = async ( sourceName , recipeId ) => {
setSaving ( sourceName )
2026-09-12 14:35:57 +02:00
setError ( null )
try {
2026-09-12 15:31:20 +02:00
await api . applyNutritionRecipe ( recipeId , sourceName )
setRecipeFor ( null )
2026-09-12 16:59:24 +02:00
setNotice ( 'Rezept verknüpft. Offene Zutaten erscheinen in der Liste.' )
await load ( )
2026-09-12 16:14:24 +02:00
onMapped ? . ( )
2026-09-12 14:35:57 +02:00
} catch ( e ) {
setError ( e . message )
} finally {
setSaving ( null )
}
}
const remove = async ( id ) => {
if ( ! confirm ( 'Zuordnung wirklich löschen?' ) ) return
try {
await api . deleteMyFoodMapping ( id )
2026-09-12 16:14:24 +02:00
setLearned ( ( list ) => list . filter ( ( x ) => x . id !== id ) )
2026-09-12 14:35:57 +02:00
await load ( )
onChanged ? . ( )
} catch ( e ) {
setError ( e . message )
}
}
2026-09-12 15:49:14 +02:00
const exportBundle = async ( ) => {
setBusy ( true )
setError ( null )
try {
await api . exportFoodKnowledge ( )
setNotice ( 'Zuordnungen und Listen als JSON heruntergeladen.' )
} catch ( e ) {
setError ( e . message )
} finally {
setBusy ( false )
}
}
const importBundle = async ( file ) => {
if ( ! file ) return
setBusy ( true )
setError ( null )
try {
const res = await api . importFoodKnowledge ( file )
await load ( )
onChanged ? . ( )
const skip = res . mappings _skipped ? ` , ${ res . mappings _skipped } ohne Katalogtreffer ` : ''
2026-09-12 16:14:24 +02:00
setNotice ( ` ${ res . mappings || 0 } Zuordnungen und ${ ( res . inserted || 0 ) + ( res . updated || 0 ) } Listen übernommen ${ skip } . ` )
2026-09-12 15:49:14 +02:00
} catch ( e ) {
setError ( e . message )
} finally {
setBusy ( false )
}
}
2026-09-12 15:31:20 +02:00
const importLists = async ( file ) => {
if ( ! file ) return
setImporting ( true )
setError ( null )
try {
const res = await api . importFddbLists ( file )
await load ( )
onChanged ? . ( )
2026-09-12 16:14:24 +02:00
setNotice ( ` ${ res . recipes } Listen importiert, ${ res . items _linked || 0 } Tagebuchzeilen verknüpft. ` )
2026-09-12 15:31:20 +02:00
} catch ( e ) {
setError ( e . message )
} finally {
setImporting ( false )
}
}
2026-09-12 16:14:24 +02:00
const withSuggest = unmapped . filter ( ( u ) => ( u . suggestions || [ ] ) . length ) . length
2026-09-12 14:35:57 +02:00
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 } } >
2026-09-12 16:14:24 +02:00
Zuerst die letzten Wochen zuordnen . Ältere Namen ( z . B . Getreide , das du nicht mehr isst ) bleiben unter „ Alle “ liegen und müssen nicht gemappt werden .
2026-09-12 14:35:57 +02:00
< / p >
{ error && < div style = { { color : 'var(--danger)' , fontSize : 13 , marginBottom : 10 } } > { error } < / div > }
2026-09-12 15:31:20 +02:00
{ notice && < div style = { { fontSize : 13 , color : 'var(--accent-dark)' , marginBottom : 10 } } > { notice } < / div > }
2026-09-12 14:35:57 +02:00
2026-09-12 16:14:24 +02:00
< input ref = { listRef } type = "file" accept = ".csv,text/csv" style = { { display : 'none' } } onChange = { ( e ) => { const f = e . target . files ? . [ 0 ] ; e . target . value = '' ; if ( f ) importLists ( f ) } } / >
< button type = "button" className = "btn btn-secondary btn-full" disabled = { importing } onClick = { ( ) => listRef . current ? . click ( ) } >
2026-09-12 15:31:20 +02:00
{ importing ? 'Importiere Listen…' : 'FDDB-Listen / Rezepte importieren' }
< / button >
2026-09-12 16:59:24 +02:00
< FoodRecipeEditor recipes = { recipes } learned = { learned } onChanged = { ( ) => { load ( ) ; onChanged ? . ( ) } } / >
2026-09-12 16:14:24 +02:00
< input ref = { bundleRef } type = "file" accept = ".json,application/json" style = { { display : 'none' } } onChange = { ( e ) => { const f = e . target . files ? . [ 0 ] ; e . target . value = '' ; if ( f ) importBundle ( f ) } } / >
2026-09-12 15:49:14 +02:00
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 , marginTop : 8 } } >
2026-09-12 16:14:24 +02:00
< button type = "button" className = "btn btn-secondary" disabled = { busy } onClick = { exportBundle } > Zuordnungen & Listen exportieren < / button >
< button type = "button" className = "btn btn-secondary" disabled = { busy } onClick = { ( ) => bundleRef . current ? . click ( ) } > Zuordnungen & Listen importieren < / button >
< / div >
< h3 style = { { fontSize : 14 , margin : '16px 0 8px' } } >
Offen ( { filtered . length } { sinceDays && totalOpen > unmapped . length ? ` von ${ totalOpen } ` : filtered . length !== unmapped . length ? ` / ${ unmapped . length } ` : '' } )
< / h3 >
< p style = { { fontSize : 12 , color : 'var(--text3)' , margin : '0 0 8px' } } > { withSuggest } mit Vorschlag < / p >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 6 , marginBottom : 8 } } >
{ PERIODS . map ( ( p ) => (
< button
key = { p . days }
type = "button"
className = { sinceDays === p . days ? 'btn btn-primary' : 'btn btn-secondary' }
style = { { fontSize : 12 } }
onClick = { ( ) => { setSinceDays ( p . days ) ; setVisible ( 80 ) ; setQ ( '' ) } }
>
{ p . label }
< / button >
) ) }
< / div >
< input className = "form-input" style = { { width : '100%' , textAlign : 'left' , marginBottom : 8 } } placeholder = "Offene Liste filtern…" value = { q } onChange = { ( e ) => { setQ ( e . target . value ) ; setVisible ( 80 ) } } / >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 6 , marginBottom : 8 } } >
{ [ [ 'all' , 'Alle Treffer' ] , [ 'suggested' , 'Mit Vorschlag' ] , [ 'none' , 'Ohne Vorschlag' ] ] . map ( ( [ id , label ] ) => (
< button key = { id } type = "button" className = { filter === id ? 'btn btn-primary' : 'btn btn-secondary' } style = { { fontSize : 12 } } onClick = { ( ) => { setFilter ( id ) ; setVisible ( 80 ) } } > { label } < / button >
) ) }
2026-09-12 15:49:14 +02:00
< / div >
2026-09-12 15:31:20 +02:00
2026-09-12 16:14:24 +02:00
{ shown . length === 0 && (
< p className = "muted" >
{ sinceDays && unmapped . length === 0 && totalOpen > 0
? ` In diesem Zeitraum ist nichts Offen. ${ totalOpen } ältere Namen liegen unter „Alle“ — die brauchst du nicht, wenn du sie nicht mehr isst. `
: 'Keine offenen Bezeichner in diesem Filter.' }
< / p >
) }
{ shown . map ( ( u ) => {
const key = u . source _name _normalized
const suggestions = u . suggestions || [ ]
const best = suggestions [ 0 ]
const busyRow = saving === u . source _name _raw
2026-09-12 14:35:57 +02:00
return (
< div key = { key } style = { { borderTop : '1px solid var(--border)' , padding : '10px 0' } } >
2026-09-12 16:14:24 +02:00
< div style = { { display : 'flex' , justifyContent : 'space-between' , gap : 8 , alignItems : 'flex-start' } } >
< div >
< div style = { { fontWeight : 600 } } > { suggestQuery ( u . source _name _raw ) || u . source _name _raw } < / div >
< div style = { { fontSize : 12 , color : 'var(--text3)' } } >
{ u . kind === 'recipe_ingredient' ? 'Rezeptzutat' : ` ${ u . count } × ` }
{ u . variant _count > 1 ? ` · ${ u . variant _count } Mengen ` : '' }
{ u . last _date ? ` · zuletzt ${ formatDay ( u . last _date ) } ` : '' }
< / div >
< / div >
{ ( u . ambiguous || suggestions . length > 1 ) && (
< span style = { { fontSize : 11 , padding : '2px 8px' , borderRadius : 999 , background : 'var(--surface2)' , color : 'var(--danger)' , whiteSpace : 'nowrap' } } >
mehrere möglich
< / span >
) }
2026-09-12 14:35:57 +02:00
< / div >
2026-09-12 16:14:24 +02:00
{ best && (
< div style = { { marginTop : 8 , display : 'flex' , flexWrap : 'wrap' , gap : 8 , alignItems : 'center' } } >
< span style = { { fontSize : 13 } } >
→ < strong > { best . name _de } < / strong >
< span style = { { color : 'var(--text3)' } } > { best . bls _code ? ` · ${ best . bls _code } ` : ' · manuell' } < / span >
< / span >
< button type = "button" className = "btn btn-primary" disabled = { busyRow } onClick = { ( ) => assign ( u , best . id , { } , best ) } >
Bestätigen
< / button >
< / div >
) }
{ suggestions . slice ( 1 ) . map ( ( s ) => (
< div key = { s . id } style = { { marginTop : 6 , display : 'flex' , flexWrap : 'wrap' , gap : 8 , alignItems : 'center' } } >
< span style = { { fontSize : 12 , color : 'var(--text2)' } } > oder { s . name _de } { s . bls _code ? ` · ${ s . bls _code } ` : '' } < / span >
< button type = "button" className = "btn btn-secondary" disabled = { busyRow } onClick = { ( ) => assign ( u , s . id , { } , s ) } > Übernehmen < / button >
< / div >
) ) }
{ ! best && < p style = { { fontSize : 12 , color : 'var(--text3)' , margin : '8px 0 0' } } > Kein Katalogvorschlag — selbst anlegen . < / p > }
2026-09-12 15:31:20 +02:00
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 , marginTop : 8 } } >
2026-09-12 16:14:24 +02:00
< button type = "button" className = "btn btn-secondary" disabled = { busyRow } onClick = { ( ) => setCreateFor ( createFor === key ? null : key ) } >
2026-09-12 16:59:24 +02:00
{ createFor === key ? 'Anlegen schließen' : ( u . kind === 'recipe_ingredient' ? 'Neu anlegen' : 'Lebensmittel oder Rezept' ) }
2026-09-12 14:35:57 +02:00
< / button >
2026-09-12 16:14:24 +02:00
< button type = "button" className = "btn btn-secondary" disabled = { busyRow } onClick = { ( ) => setSearchFor ( u ) } > Mehr suchen < / button >
2026-09-12 15:31:20 +02:00
{ u . kind !== 'recipe_ingredient' && u . matching _recipe _id && (
2026-09-12 16:14:24 +02:00
< button type = "button" className = "btn btn-secondary" disabled = { busyRow } onClick = { ( ) => applyRecipe ( u . source _name _raw , u . matching _recipe _id ) } > Als Rezept < / button >
2026-09-12 15:31:20 +02:00
) }
{ u . kind !== 'recipe_ingredient' && recipes . length > 0 && (
2026-09-12 16:14:24 +02:00
< button type = "button" className = "btn btn-secondary" disabled = { busyRow } onClick = { ( ) => setRecipeFor ( u ) } > Rezept wählen < / button >
2026-09-12 15:31:20 +02:00
) }
< / div >
2026-09-12 16:14:24 +02:00
{ createFor === key && (
2026-09-12 16:59:24 +02:00
< InlineCreate
defaultName = { suggestQuery ( u . source _name _raw ) || u . source _name _raw }
disabled = { busyRow }
learned = { learned }
allowRecipe = { u . kind !== 'recipe_ingredient' }
onCreateFood = { ( body ) => createAndAssign ( u , body ) }
onCreateRecipe = { ( body ) => createRecipeAndAssign ( u , body ) }
/ >
2026-09-12 16:14:24 +02:00
) }
2026-09-12 14:35:57 +02:00
< / div >
)
} ) }
2026-09-12 16:14:24 +02:00
{ visible < filtered . length && (
< button type = "button" className = "btn btn-secondary btn-full" style = { { marginTop : 8 } } onClick = { ( ) => setVisible ( ( n ) => n + 80 ) } >
Weitere { Math . min ( 80 , filtered . length - visible ) } zeigen
< / button >
) }
2026-09-12 14:35:57 +02:00
2026-09-12 16:14:24 +02:00
< h3 style = { { fontSize : 14 , margin : '20px 0 8px' } } >
Gelernt ( { learned . length } ) { ' ' }
< button type = "button" className = "btn btn-secondary" style = { { fontSize : 11 , padding : '2px 8px' } } onClick = { ( ) => setShowLearned ( ( v ) => ! v ) } >
{ showLearned ? 'Einklappen' : 'Anzeigen' }
< / button >
< / h3 >
{ showLearned && learned . map ( ( m ) => (
2026-09-12 14:35:57 +02:00
< 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)' } } >
2026-09-12 15:31:20 +02:00
→ { m . food _name _de } { m . bls _code ? ` · ${ m . bls _code } ` : '' }
2026-09-12 14:35:57 +02:00
{ m . catalog _kind !== 'official_bls' ? ' (manuell)' : '' }
< / div >
< / div >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => remove ( m . id ) } > Löschen < / button >
< / div >
) ) }
2026-09-12 15:31:20 +02:00
{ searchFor && (
< FoodSearchModal
title = { ` Suchen: ${ searchFor . source _name _raw } ` }
initialQuery = { suggestQuery ( searchFor . source _name _raw ) }
2026-09-12 15:49:14 +02:00
quantityHint = { searchFor . sample _quantity _raw || searchFor . source _name _raw }
2026-09-12 16:59:24 +02:00
allowRecipe = { searchFor . kind !== 'recipe_ingredient' }
learned = { learned }
2026-09-12 15:31:20 +02:00
onClose = { ( ) => setSearchFor ( null ) }
2026-09-12 16:14:24 +02:00
onSelect = { ( food , extras ) => assign ( searchFor , food . id , extras , food ) }
2026-09-12 16:59:24 +02:00
onCreateRecipe = { searchFor . kind === 'recipe_ingredient' ? undefined : ( body ) => createRecipeAndAssign ( searchFor , body ) }
2026-09-12 15:31:20 +02:00
/ >
) }
{ recipeFor && (
2026-09-12 16:14:24 +02:00
< RecipePickModal recipes = { recipes } sourceName = { recipeFor . source _name _raw } onClose = { ( ) => setRecipeFor ( null ) } onPick = { ( id ) => applyRecipe ( recipeFor . source _name _raw , id ) } / >
2026-09-12 15:31:20 +02:00
) }
2026-09-12 14:35:57 +02:00
< / 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 (
2026-09-12 15:31:20 +02:00
< span style = { { display : 'inline-flex' , gap : 8 } } >
2026-09-12 14:35:57 +02:00
< 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 >
)
}