PageSourceSearch

https://restaurantai.consulting/assets/nutrition-B6pDkfYC.js

js restaurantai.consulting collected 2026-09-24 14:58:49 UTC 11,513 bytes, 1 lines download raw bytes

1import{j as e}from"./vendor-motion-CAS9Mrr8.js";import{b as X,Y as ee,ab as se,d as u,_ as m,$ as p,B as r,aa as te,J as j,K as f,N,O as b,P as v,Q as l,I as R,a0 as ae}from"./index-CxC89PRg.js";import{a}from"./vendor-charts-CCcpKWTT.js";import{B as ie}from"./badge-DXnfPCD0.js";import{U as ne}from"./upgrade-gate-mCiqKCUt.js";import{L as re,I as i}from"./lamplit-sheet-DBfx0skq.js";import{U as le}from"./utensils-Cdrt-tP5.js";import{S as ce}from"./search-Ceq5iq3J.js";import{T as z}from"./trash-2-DAItjZQU.js";import"./vendor-pdf-C9TCjkzL.js";import"./use-tier-access-DycmD0Wc.js";function y({value:o}){return o==null||o===""?e.jsx("span",{style:{color:i.faint},children:"—"}):e.jsx("span",{className:"ledger-stat-value",style:{color:i.full},children:o})}function ve(){return e.jsx(ne,{domain:"nutrition",children:e.jsx(oe,{})})}function oe(){const[,o]=X(),{toast:c}=ee(),w=se(),[t,F]=a.useState(""),[S,C]=a.useState(""),[I,D]=a.useState(""),[$,T]=a.useState({calories:"",protein:"",totalFat:"",totalCarbs:"",sodium:""}),[q,_]=a.useState("100"),[M,H]=a.useState("g"),[U,L]=a.useState("cup"),[k,K]=a.useState(""),[n,Q]=a.useState(null),{data:B}=u({queryKey:["/api/nutrition/status"]}),{data:x=[]}=u({queryKey:["/api/nutrition/ingredients"]}),{data:P=[]}=u({queryKey:["/api/nutrition/measures"]}),{data:A=[]}=u({queryKey:["/api/nutrition/missing"]}),{data:Y=[],isFetching:J}=u({queryKey:["/api/nutrition/search",I],enabled:!!I&&B?.usdaConfigured===!0}),d=()=>{w.invalidateQueries({queryKey:["/api/nutrition/ingredients"]}),w.invalidateQueries({queryKey:["/api/nutrition/missing"]}),w.invalidateQueries({queryKey:["/api/nutrition/measures"]})},h=m({mutationFn:async s=>{const g=s??{refUnit:U,gramsPerRef:Number(k)};return(await p("PUT","/api/nutrition/measures",{ingredientName:t,...g})).json()},onSuccess:s=>{K(""),d(),c({title:`Saved: 1 ${s?.refUnit??U} of ${t} = ${Number(s?.gramsPerRef)} g`})},onError:()=>c({title:"Couldn't save that weight",variant:"destructive"})}),V=m({mutationFn:async s=>{await p("DELETE",`/api/nutrition/measures/${s}`)},onSuccess:d}),O=m({mutationFn:async s=>(await p("POST","/api/nutrition/import",{ingredientName:t,fdcId:s})).json(),onSuccess:s=>{D(""),C(""),Q(s?.measureSuggestion??null),d(),c({title:`Saved facts for ${t}`,description:s?.measure?`Also stored 1 ${s.measure.refUnit} = ${Number(s.measure.gramsPerRef)} g`:void 0})},onError:()=>c({title:"Import failed",variant:"destructive"})}),E=m({mutationFn:async()=>(await p("PUT","/api/nutrition/ingredients",{ingredientName:t,basisQty:q.trim()||"100",basisUnit:M.trim()||"g",source:"manual",...$})).json(),onSuccess:()=>{T({calories:"",protein:"",totalFat:"",totalCarbs:"",sodium:""}),d(),c({title:`Saved facts for ${t}`})},onError:()=>c({title:"Couldn't save those facts",variant:"destructive"})}),W=m({mutationFn:async s=>{await p("DELETE",`/api/nutrition/ingredients/${s}`)},onSuccess:d});return e.jsxs("div",{className:"max-w-4xl mx-auto p-4 space-y-6",style:{paddingTop:"calc(1rem + env(safe-area-inset-top, 0px))"},children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(r,{variant:"ghost",size:"icon",className:"shrink-0 -ml-2",onClick:()=>o("/"),"data-testid":"nutrition-back","aria-label":"Back to dashboard",children:e.jsx(te,{className:"h-5 w-5"})}),e.jsxs("h1",{className:"text-2xl font-semibold flex items-center gap-2",children:[e.jsx(le,{className:"h-6 w-6"})," Nutrition"]})]}),e.jsx("p",{className:"text-sm text-muted-foreground mt-1",children:"Facts per 100 g of each ingredient. Recipe panels are built from these, and a recipe with a gap says so rather than counting the gap as zero."})]}),A.length>0&&e.jsxs(j,{children:[e.jsx(f,{className:"pb-3",children:e.jsxs(N,{className:"text-base",children:["Missing facts (",A.length,")"]})}),e.jsx(b,{className:"space-y-1",children:A.slice(0,25).map(s=>e.jsxs("button",{onClick:()=>{F(s.name),C(s.name)},className:"w-full text-left flex justify-between gap-3 py-1.5 border-b text-sm hover:text-primary","data-testid":`missing-${s.name}`,children:[e.jsx("span",{className:"truncate",children:s.name}),e.jsxs("span",{className:"text-xs text-muted-foreground flex-shrink-0",children:[s.usedIn.length," recipe",s.usedIn.length===1?"":"s"]})]},s.name))})]}),e.jsxs(j,{children:[e.jsx(f,{className:"pb-3",children:e.jsx(N,{className:"text-base",children:"Add facts"})}),e.jsxs(b,{className:"space-y-4",children:[e.jsxs("div",{children:[e.jsx(v,{htmlFor:"nutrition-target",children:"Ingredient"}),e.jsx(l,{id:"nutrition-target",value:t,onChange:s=>F(s.target.value),placeholder:"chicken breast","data-testid":"input-nutrition-target"}),e.jsx("p",{className:"text-[14px] text-muted-foreground mt-1",children:"Use the name exactly as the recipes spell it."})]}),B?.usdaConfigured?e.jsxs("div",{className:"space-y-2",children:[e.jsx(v,{htmlFor:"usda-query",children:"Look up in USDA FoodData Central"}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(l,{id:"usda-query",value:S,onChange:s=>C(s.target.value),placeholder:"chicken breast raw","data-testid":"input-usda-query"}),e.jsx(r,{variant:"outline",onClick:()=>D(S.trim()),disabled:!S.trim()||!t.trim(),"data-testid":"button-usda-search",children:J?e.jsx(R,{className:"h-4 w-4 animate-spin"}):e.jsx(ce,{className:"h-4 w-4"})})]}),Y.map(s=>e.jsxs("button",{onClick:()=>O.mutate(s.fdcId),disabled:O.isPending,className:"w-full text-left py-2 border-b text-sm hover:text-primary","data-testid":`usda-hit-${s.fdcId}`,children:[e.jsx("span",{className:"block truncate",children:s.description}),e.jsxs("span",{className:"text-[14px] text-muted-foreground",children:[s.dataType??"USDA",s.brandOwner?` · ${s.brandOwner}`:""]})]},s.fdcId))]}):e.jsxs("p",{className:"text-xs text-muted-foreground border border-dashed rounded p-3",children:["USDA lookup is off: no ",e.jsx("code",{children:"USDA_FDC_API_KEY"})," is configured on the server. Manual entry below works either way, and lookup lights up as soon as the key is set."]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-end gap-2 flex-wrap",children:[e.jsx(v,{className:"text-xs uppercase tracking-wide text-muted-foreground pb-2",children:"Manual, per"}),e.jsx(l,{value:q,onChange:s=>_(s.target.value),className:"w-16 h-8",inputMode:"decimal","data-testid":"input-basis-qty"}),e.jsx(l,{value:M,onChange:s=>H(s.target.value),className:"w-20 h-8",placeholder:"g","data-testid":"input-basis-unit"})]}),e.jsx("p",{className:"text-[14px] text-muted-foreground mt-1",children:"Store facts in the unit your recipes actually use. A recipe measuring flour in cups needs facts per cup, because converting cups to grams needs a density we will not guess."}
1),e.jsx("div",{className:"grid grid-cols-2 sm:grid-cols-5 gap-2 mt-2",children:[["calories","kcal"],["protein","protein g"],["totalFat","fat g"],["totalCarbs","carbs g"],["sodium","sodium mg"]].map(([s,g])=>e.jsx(l,{value:$[s],onChange:G=>T(Z=>({...Z,[s]:G.target.value})),placeholder:g,inputMode:"decimal","data-testid":`input-manual-${s}`},s))}),n&&e.jsxs("div",{className:"mt-3 border rounded p-3","data-testid":"measure-suggestion",children:[e.jsxs("p",{className:"text-sm",children:["That entry has no household measure. USDA's"," ",e.jsx("span",{className:"font-medium",children:n.description})," lists"," ",e.jsxs("span",{className:"font-medium",children:["1 ",n.refUnit," = ",n.gramsPerRef," g"]}),"."]}),e.jsx("p",{className:"text-[14px] text-muted-foreground mt-1",children:"Different USDA record, so only you can say it's the same ingredient."}),e.jsx(r,{size:"sm",variant:"outline",className:"mt-2",onClick:()=>{L(n.refUnit),h.mutate({refUnit:n.refUnit,gramsPerRef:n.gramsPerRef}),Q(null)},"data-testid":"button-accept-measure",children:"Use it"})]}),e.jsxs("div",{className:"mt-4 border-t pt-3",children:[e.jsx(v,{className:"text-xs uppercase tracking-wide text-muted-foreground",children:"Weigh one"}),e.jsx("p",{className:"text-[14px] text-muted-foreground mt-1 mb-2",children:"A recipe in cups can't be costed against a price per pound, and can't use facts per 100 g, without knowing what a cup of THIS ingredient weighs. Put one on the scale once and both start working. A USDA import fills this in automatically when USDA has measured it."}),e.jsxs("div",{className:"flex items-end gap-2 flex-wrap",children:[e.jsx("span",{className:"text-sm pb-2",children:"1"}),e.jsx(l,{value:U,onChange:s=>L(s.target.value),className:"w-20 h-8",placeholder:"cup","data-testid":"input-ref-unit"}),e.jsx("span",{className:"text-sm pb-2",children:"weighs"}),e.jsx(l,{value:k,onChange:s=>K(s.target.value),className:"w-24 h-8",inputMode:"decimal",placeholder:"125","data-testid":"input-grams-per-ref"}),e.jsx("span",{className:"text-sm pb-2",children:"g"}),e.jsx(r,{size:"sm",variant:"outline",className:"h-8",onClick:()=>h.mutate(void 0),disabled:!t.trim()||!k.trim()||h.isPending,"data-testid":"button-save-measure",children:h.isPending?e.jsx(R,{className:"h-4 w-4 animate-spin"}):"Save weight"})]})]}),e.jsxs(r,{className:"mt-2",onClick:()=>E.mutate(),disabled:!t.trim()||E.isPending,"data-testid":"button-save-manual",children:[E.isPending?e.jsx(R,{className:"h-4 w-4 animate-spin mr-1"}):e.jsx(ae,{className:"h-4 w-4 mr-1"}),"Save facts"]})]})]})]}),P.length>0&&e.jsxs(j,{children:[e.jsx(f,{className:"pb-3",children:e.jsxs(N,{className:"text-base",children:["Measured weights (",P.length,")"]})}),e.jsx(b,{className:"divide-y",children:P.map(s=>e.jsxs("div",{className:"flex items-center gap-3 py-2 text-sm","data-testid":`measure-${s.id}`,children:[e.jsxs("span",{className:"flex-1 truncate",children:["1 ",s.refUnit," of ",s.ingredientName," = ",Number(s.gramsPerRef)," g"]}),e.jsx(ie,{variant:"outline",className:"text-[13px]",children:s.source}),e.jsx(r,{size:"sm",variant:"ghost",className:"h-8 w-8 p-0",onClick:()=>V.mutate(s.id),"aria-label":"Remove measured weight",children:e.jsx(z,{className:"h-3.5 w-3.5"})})]},s.id))})]}),x.length===0?e.jsxs(j,{children:[e.jsx(f,{className:"pb-3",children:e.jsx(N,{className:"text-base",children:"On file (0)"})}),e.jsx(b,{children:e.jsx("p",{className:"text-xs text-muted-foreground text-center py-3",children:"Nothing on file yet."})})]}):e.jsx(re,{title:"On File",meta:`${x.length} ${x.length===1?"INGREDIENT":"INGREDIENTS"}`,testid:"nutrition-library",folio:e.jsxs(e.Fragment,{children:[e.jsx("span",{children:"AS SUPPLIED — NOTHING INFERRED"}),e.jsx("span",{children:"A GAP READS AS A GAP, NEVER AS ZERO"})]}),children:e.jsx("div",{children:x.map(s=>e.jsxs("div",{className:"paper-entry flex items-center gap-3 py-2.5","data-testid":`fact-${s.id}`,children:[e.jsxs("div",{className:"flex-1 min-w-0",children:[e.jsx("div",{className:"truncate font-medium",style:{color:i.full},children:s.ingredientName}),e.jsxs("p",{className:"text-[13px] mt-0.5",style:{color:i.mid},children:[e.jsx(y,{value:s.calories})," kcal · ",e.jsx(y,{value:s.protein})," g protein ·"," ",e.jsx(y,{value:s.totalFat})," g fat · ",e.jsx(y,{value:s.totalCarbs})," g carbs",e.jsx("span",{style:{color:i.faint},children:" · per "}),e.jsx("span",{className:"ledger-stat-value",style:{color:i.mid},children:s.basisQty}),s.basisUnit]})]}),e.jsx("span",{className:"text-[12px] px-1.5 py-0 border shrink-0",style:{color:i.mid,borderColor:"rgba(41,34,26,0.28)",borderRadius:"2px"},children:s.source}),e.jsx(r,{size:"sm",variant:"ghost",className:"h-8 w-8 p-0 hover:bg-black/5",style:{color:i.mid},onClick:()=>W.mutate(s.id),"aria-label":"Remove facts",children:e.jsx(z,{className:"h-3.5 w-3.5"})})]},s.id))})})]})}export{ve as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.