@@ -3,6 +3,9 @@ import Bring from 'bring-shopping';
33import { db } from '../db/database' ;
44import { convertToSmallestUnit , normalizeUnit } from '../utils/units' ;
55
6+ // Import isIngredientInInventory from server.ts is circular — inline a simple version
7+ const normalizeIngredient = ( s : string ) => s . toLowerCase ( ) . replace ( / \( .* ?\) / g, '' ) . replace ( / [ , \. ] / g, '' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
8+
69const getBringClient = ( ) => {
710 const emailRow = db . prepare ( 'SELECT value FROM settings WHERE key = ?' ) . get ( 'bring_email' ) as any ;
811 const passRow = db . prepare ( 'SELECT value FROM settings WHERE key = ?' ) . get ( 'bring_password' ) as any ;
@@ -18,13 +21,13 @@ export const syncToBring = async () => {
1821 await bring . login ( ) ;
1922 const lists = await bring . loadLists ( ) ;
2023 if ( ! lists || lists . lists . length === 0 ) return ;
21-
24+
2225 const listId = lists . lists [ 0 ] . listUuid ;
2326 const bringItems = await bring . getItems ( listId ) ;
24-
27+
2528 const today = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
2629 const upcomingRecipes = db . prepare ( 'SELECT * FROM planned_recipes WHERE date >= ? AND cooked = 0' ) . all ( today ) ;
27-
30+
2831 const required : Record < string , { amount : number , unit : string , name : string } > = { } ;
2932 upcomingRecipes . forEach ( ( recipe : any ) => {
3033 const ingredients = JSON . parse ( recipe . ingredients ) ;
@@ -39,36 +42,61 @@ export const syncToBring = async () => {
3942 } ) ;
4043 } ) ;
4144
42- const inventory = db . prepare ( 'SELECT name, quantity, unit, min_stock FROM items WHERE quantity > 0' ) . all ( ) as any [ ] ;
43- const allKnownItems = db . prepare ( 'SELECT name, quantity, unit, min_stock FROM items' ) . all ( ) as any [ ] ;
44-
45- const stockLevels : Record < string , { total : number , min : number , unit : string } > = { } ;
45+ const inventory = db . prepare ( 'SELECT name, generic_name, quantity, unit, min_stock, category FROM items WHERE quantity > 0' ) . all ( ) as any [ ] ;
46+ const allKnownItems = db . prepare ( 'SELECT name, generic_name, quantity, unit, min_stock, category FROM items' ) . all ( ) as any [ ] ;
47+
48+ // Min-stock requirements
49+ const stockLevels : Record < string , { total : number , min : number , unit : string , category : string , name : string } > = { } ;
4650 allKnownItems . forEach ( item => {
4751 const key = `${ item . name . toLowerCase ( ) } _${ normalizeUnit ( item . unit ) } ` ;
4852 if ( ! stockLevels [ key ] ) {
49- stockLevels [ key ] = { total : 0 , min : item . min_stock || 0 , unit : normalizeUnit ( item . unit ) } ;
53+ stockLevels [ key ] = { total : 0 , min : item . min_stock || 0 , unit : normalizeUnit ( item . unit ) , category : item . category , name : item . name } ;
5054 }
5155 const smallest = convertToSmallestUnit ( item . quantity , item . unit ) ;
5256 stockLevels [ key ] . total += smallest . amount ;
5357 } ) ;
5458
5559 Object . entries ( stockLevels ) . forEach ( ( [ key , level ] ) => {
5660 if ( level . min > 0 && level . total < level . min ) {
57- const name = key . split ( '_' ) [ 0 ] ;
58- const missing = level . min - level . total ;
59- if ( ! required [ key ] ) {
60- required [ key ] = { amount : 0 , unit : level . unit , name : name . charAt ( 0 ) . toUpperCase ( ) + name . slice ( 1 ) } ;
61+ if ( level . category === 'Gewürze' ) {
62+ // Spices: "1 Packung"
63+ const spiceKey = `${ level . name . toLowerCase ( ) } _Stück` ;
64+ if ( ! required [ spiceKey ] ) {
65+ required [ spiceKey ] = { amount : 1 , unit : 'Stück' , name : level . name } ;
66+ }
67+ } else {
68+ const missing = level . min - level . total ;
69+ if ( ! required [ key ] ) {
70+ required [ key ] = { amount : 0 , unit : level . unit , name : level . name } ;
71+ }
72+ required [ key ] . amount += missing ;
6173 }
62- required [ key ] . amount += missing ;
6374 }
6475 } ) ;
6576
6677 const missingNames = new Set < string > ( ) ;
6778 Object . values ( required ) . forEach ( ( reqIng : any ) => {
79+ // Spices: if in inventory with quantity > 0, always consider available
80+ const matchedInv = inventory . find ( inv => {
81+ const invNorm = normalizeIngredient ( inv . name ) ;
82+ const genNorm = inv . generic_name ? normalizeIngredient ( inv . generic_name ) : '' ;
83+ const reqNorm = normalizeIngredient ( reqIng . name ) ;
84+ return invNorm . includes ( reqNorm ) || reqNorm . includes ( invNorm ) ||
85+ genNorm . includes ( reqNorm ) || reqNorm . includes ( genNorm ) ;
86+ } ) ;
87+
88+ if ( matchedInv ?. category === 'Gewürze' && matchedInv . quantity > 0 ) {
89+ return ; // Spice in stock, skip
90+ }
91+
92+ // Standard unit-aware matching
6893 const matchingItems = inventory . filter ( inv => {
69- const isNameMatch = inv . name . toLowerCase ( ) . includes ( reqIng . name . toLowerCase ( ) ) ||
70- reqIng . name . toLowerCase ( ) . includes ( inv . name . toLowerCase ( ) ) ;
71- if ( ! isNameMatch ) return false ;
94+ const invNorm = normalizeIngredient ( inv . name ) ;
95+ const genNorm = inv . generic_name ? normalizeIngredient ( inv . generic_name ) : '' ;
96+ const reqNorm = normalizeIngredient ( reqIng . name ) ;
97+ const nameMatch = invNorm . includes ( reqNorm ) || reqNorm . includes ( invNorm ) ||
98+ genNorm . includes ( reqNorm ) || reqNorm . includes ( genNorm ) ;
99+ if ( ! nameMatch ) return false ;
72100 const invSmallest = convertToSmallestUnit ( inv . quantity , inv . unit ) ;
73101 return invSmallest . unit === reqIng . unit ;
74102 } ) ;
@@ -83,6 +111,7 @@ export const syncToBring = async () => {
83111 }
84112 } ) ;
85113
114+ // ONE-WAY SYNC: add missing, remove no-longer-missing known items
86115 const bringCurrentNames = new Set ( bringItems . purchase . map ( ( i : any ) => i . name . toLowerCase ( ) ) ) ;
87116 for ( const missingName of missingNames ) {
88117 if ( ! bringCurrentNames . has ( missingName ) ) {
0 commit comments