feat: add rematch functionality for recipe ingredients and enhance inventory management
Test Suite / test (24.15.0) (push) Has been cancelled

- Added a new API path for rematching recipe ingredients in `api_paths.dart`.
- Implemented a manual product creation dialog in `inventory_screen.dart` to allow users to create new products directly.
- Integrated the rematch functionality in `recipe_repository.dart` to handle rematching of recipe ingredients.
- Updated the recipe detail screen to include a button for triggering the rematch process.
- Introduced a new `RecipeMatchingService` in the backend to handle ingredient matching logic.
- Added database migration to include `aiEngineEnabled` column in the User table.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-05-06 09:20:31 +02:00
parent 9fe85a719c
commit 04b1fc3024
53 changed files with 1420 additions and 652 deletions
+176 -137
View File
@@ -23,11 +23,7 @@ export declare class RecipesController {
quantity: number;
unit: string;
note: string | null;
suggestions: {
productId: number;
productName: string;
score: number;
}[];
suggestions: import("./recipe-matching.service").IngredientSuggestion[];
}[];
}>;
getAiSuggestions(user: {
@@ -56,30 +52,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -90,16 +86,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
})[]>;
getInventoryPreview(id: number, user: {
userId: number;
@@ -157,7 +153,7 @@ export declare class RecipesController {
quantity: number;
unit: any;
note: any;
status: "missing" | "exact_match" | "covered_by_pantry" | "substitutable";
status: "missing" | "substitutable" | "exact_match" | "covered_by_pantry";
matchedProductId: any;
matchedProductName: any;
source: string;
@@ -169,7 +165,50 @@ export declare class RecipesController {
quantity: number;
unit: any;
note: any;
status: "missing" | "exact_match" | "covered_by_pantry" | "substitutable";
status: "missing" | "substitutable" | "exact_match" | "covered_by_pantry";
matchedProductId: any;
matchedProductName: any;
source: null;
availableQuantity: number;
missingQuantity: number;
})[];
summary: {
exactCount: number;
pantryCount: number;
substituteCount: number;
missingCount: number;
};
shoppingListCandidates: {
ingredientId: any;
rawName: any;
quantity: number;
unit: any;
missingQuantity: number;
}[];
}>;
rematchRecipeIngredients(id: number, user: {
userId: number;
}): Promise<{
recipeId: number;
ingredients: ({
ingredientId: any;
rawName: any;
quantity: number;
unit: any;
note: any;
status: "missing" | "substitutable" | "exact_match" | "covered_by_pantry";
matchedProductId: any;
matchedProductName: any;
source: string;
availableQuantity: number;
missingQuantity: number;
} | {
ingredientId: any;
rawName: any;
quantity: number;
unit: any;
note: any;
status: "missing" | "substitutable" | "exact_match" | "covered_by_pantry";
matchedProductId: any;
matchedProductName: any;
source: null;
@@ -211,30 +250,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -245,16 +284,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
create(createRecipeDto: CreateRecipeDto, user: {
userId: number;
@@ -273,30 +312,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -304,16 +343,16 @@ export declare class RecipesController {
analysisStatus: string | null;
})[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
update(id: number, createRecipeDto: CreateRecipeDto, user: {
userId: number;
@@ -332,30 +371,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -363,16 +402,16 @@ export declare class RecipesController {
analysisStatus: string | null;
})[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
remove(id: number, user: {
userId: number;
@@ -398,30 +437,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -432,16 +471,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
addIngredient(id: number, ingredient: CreateIngredientDto, user: {
userId: number;
@@ -459,30 +498,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -510,30 +549,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -544,16 +583,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
shareRecipe(id: number, dto: ShareRecipeDto, user: {
userId: number;
@@ -576,30 +615,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -610,16 +649,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
unshareRecipe(id: number, username: string, user: {
userId: number;
@@ -642,30 +681,30 @@ export declare class RecipesController {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
matchConfidence: number | null;
@@ -676,16 +715,16 @@ export declare class RecipesController {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
}
export {};