feat: add rematch functionality for recipe ingredients and enhance inventory management
Test Suite / test (24.15.0) (push) Has been cancelled
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:
Vendored
+20
@@ -7,8 +7,28 @@ export type CategorySuggestion = {
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
usedFallback: boolean;
|
||||
};
|
||||
export type AiIngredientMatchSuggestion = {
|
||||
productId: number;
|
||||
reason?: string;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
};
|
||||
export type AiSubstitutionSuggestion = {
|
||||
productId: number;
|
||||
reason?: string;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
};
|
||||
export declare class AiService {
|
||||
private readonly logger;
|
||||
suggestIngredientMatches(rawIngredient: string, candidates: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
canonicalName?: string | null;
|
||||
}>): Promise<AiIngredientMatchSuggestion[]>;
|
||||
suggestSubstitutions(rawIngredient: string, availableProducts: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
canonicalName?: string | null;
|
||||
}>): Promise<AiSubstitutionSuggestion[]>;
|
||||
suggestCategory(productName: string, categories: FlatCategory[]): Promise<CategorySuggestion>;
|
||||
private fallbackToOvrigt;
|
||||
}
|
||||
|
||||
Vendored
+90
@@ -16,6 +16,96 @@ let AiService = AiService_1 = class AiService {
|
||||
constructor() {
|
||||
this.logger = new common_1.Logger(AiService_1.name);
|
||||
}
|
||||
async suggestIngredientMatches(rawIngredient, candidates) {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey || candidates.length === 0)
|
||||
return [];
|
||||
const candidateList = candidates
|
||||
.map((c) => `[${c.id}] ${c.canonicalName || c.name}`)
|
||||
.join('\n');
|
||||
const systemPrompt = `Du matchar en ingrediensrad mot produktkandidater.
|
||||
Svara ENDAST med JSON: {"matches":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
||||
Regler:
|
||||
1. Välj max 3 kandidater.
|
||||
2. Om inget passar, returnera tom lista.`;
|
||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nKandidater:\n${candidateList}`;
|
||||
try {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
max_tokens: 300,
|
||||
temperature: 0.1,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`suggestIngredientMatches API-fel: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
const data = (await response.json());
|
||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed.matches) ? parsed.matches.slice(0, 3) : [];
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.warn(`suggestIngredientMatches misslyckades: ${String(err)}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async suggestSubstitutions(rawIngredient, availableProducts) {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey || availableProducts.length === 0)
|
||||
return [];
|
||||
const productList = availableProducts
|
||||
.map((p) => `[${p.id}] ${p.canonicalName || p.name}`)
|
||||
.join('\n');
|
||||
const systemPrompt = `Du föreslår ersättningsvaror för en ingrediens.
|
||||
Svara ENDAST med JSON: {"substitutions":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
||||
Regler:
|
||||
1. Välj max 3 ersättningar.
|
||||
2. Om inget passar, returnera tom lista.`;
|
||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nTillgängliga produkter:\n${productList}`;
|
||||
try {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
max_tokens: 300,
|
||||
temperature: 0.2,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`suggestSubstitutions API-fel: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
const data = (await response.json());
|
||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed.substitutions) ? parsed.substitutions.slice(0, 3) : [];
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.warn(`suggestSubstitutions misslyckades: ${String(err)}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async suggestCategory(productName, categories) {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+22
-22
@@ -7,13 +7,13 @@ export declare class InventoryController {
|
||||
constructor(inventoryService: InventoryService);
|
||||
consume(id: number, body: ConsumeInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -22,6 +22,7 @@ export declare class InventoryController {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -29,7 +30,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -45,18 +45,18 @@ export declare class InventoryController {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
comment: string | null;
|
||||
amountUsed: import("@prisma/client/runtime/library").Decimal;
|
||||
inventoryItemId: number;
|
||||
amountUsed: import("@prisma/client/runtime/library").Decimal;
|
||||
}[]>;
|
||||
findAll(location?: string, sort?: string): Promise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -65,6 +65,7 @@ export declare class InventoryController {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -72,7 +73,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -83,13 +83,13 @@ export declare class InventoryController {
|
||||
})[]>;
|
||||
findExpiring(): Promise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -98,6 +98,7 @@ export declare class InventoryController {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -105,7 +106,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -116,13 +116,13 @@ export declare class InventoryController {
|
||||
})[]>;
|
||||
create(body: CreateInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -131,6 +131,7 @@ export declare class InventoryController {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -138,7 +139,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -149,13 +149,13 @@ export declare class InventoryController {
|
||||
}>;
|
||||
update(id: number, body: UpdateInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -164,6 +164,7 @@ export declare class InventoryController {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -171,7 +172,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -181,6 +181,7 @@ export declare class InventoryController {
|
||||
comment: string | null;
|
||||
}>;
|
||||
remove(id: number): Promise<{
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -188,7 +189,6 @@ export declare class InventoryController {
|
||||
quantity: import("@prisma/client/runtime/library").Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
|
||||
+22
-22
@@ -15,13 +15,13 @@ export declare class InventoryService {
|
||||
private ensureProductExists;
|
||||
findAll(query?: InventoryQuery): Promise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -30,6 +30,7 @@ export declare class InventoryService {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -37,7 +38,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -48,13 +48,13 @@ export declare class InventoryService {
|
||||
})[]>;
|
||||
consume(id: number, data: ConsumeInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -63,6 +63,7 @@ export declare class InventoryService {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -70,7 +71,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -86,18 +86,18 @@ export declare class InventoryService {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
comment: string | null;
|
||||
amountUsed: Prisma.Decimal;
|
||||
inventoryItemId: number;
|
||||
amountUsed: Prisma.Decimal;
|
||||
}[]>;
|
||||
findExpiring(): Promise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -106,6 +106,7 @@ export declare class InventoryService {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -113,7 +114,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -124,13 +124,13 @@ export declare class InventoryService {
|
||||
})[]>;
|
||||
create(data: CreateInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -139,6 +139,7 @@ export declare class InventoryService {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -146,7 +147,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -157,13 +157,13 @@ export declare class InventoryService {
|
||||
}>;
|
||||
update(id: number, data: UpdateInventoryDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -172,6 +172,7 @@ export declare class InventoryService {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
} & {
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -179,7 +180,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
@@ -189,6 +189,7 @@ export declare class InventoryService {
|
||||
comment: string | null;
|
||||
}>;
|
||||
remove(id: number): Promise<{
|
||||
origin: string | null;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -196,7 +197,6 @@ export declare class InventoryService {
|
||||
quantity: Prisma.Decimal;
|
||||
unit: string;
|
||||
brand: string | null;
|
||||
origin: string | null;
|
||||
receiptName: string | null;
|
||||
location: string | null;
|
||||
purchaseDate: Date | null;
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ export declare class MealPlanController {
|
||||
ingredients: {
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
} | null;
|
||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||
unit: string | null;
|
||||
@@ -61,8 +61,8 @@ export declare class MealPlanController {
|
||||
ingredients: {
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
} | null;
|
||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||
unit: string | null;
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ export declare class MealPlanService {
|
||||
ingredients: {
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
} | null;
|
||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||
unit: string | null;
|
||||
@@ -38,8 +38,8 @@ export declare class MealPlanService {
|
||||
ingredients: {
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
} | null;
|
||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||
unit: string | null;
|
||||
|
||||
+6
-6
@@ -7,13 +7,13 @@ export declare class PantryController {
|
||||
userId: number;
|
||||
}): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -32,13 +32,13 @@ export declare class PantryController {
|
||||
userId: number;
|
||||
}, body: CreatePantryItemDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
|
||||
+6
-6
@@ -5,13 +5,13 @@ export declare class PantryService {
|
||||
constructor(prisma: PrismaService);
|
||||
findAll(userId: number): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -28,13 +28,13 @@ export declare class PantryService {
|
||||
})[]>;
|
||||
create(userId: number, data: CreatePantryItemDto): Promise<{
|
||||
product: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
|
||||
+66
-66
@@ -17,6 +17,8 @@ export declare class ProductsController {
|
||||
constructor(productsService: ProductsService, aiService: AiService, categoriesService: CategoriesService);
|
||||
findAll(tag?: string): Promise<({
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -24,8 +26,6 @@ export declare class ProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -54,13 +54,13 @@ export declare class ProductsController {
|
||||
parentId: number | null;
|
||||
}) | null;
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -76,13 +76,13 @@ export declare class ProductsController {
|
||||
normalizedName: string;
|
||||
count: number;
|
||||
products: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -94,13 +94,13 @@ export declare class ProductsController {
|
||||
previewMerge(sourceProductId: number, targetProductId: number): Promise<{
|
||||
source: {
|
||||
inventoryCount: number;
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -110,13 +110,13 @@ export declare class ProductsController {
|
||||
};
|
||||
target: {
|
||||
inventoryCount: number;
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -134,13 +134,13 @@ export declare class ProductsController {
|
||||
message: string;
|
||||
updatedCount: number;
|
||||
products: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -166,13 +166,13 @@ export declare class ProductsController {
|
||||
parentId: number | null;
|
||||
}) | null;
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -186,13 +186,13 @@ export declare class ProductsController {
|
||||
suggestion: object;
|
||||
}[]>;
|
||||
findDeleted(): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -206,18 +206,18 @@ export declare class ProductsController {
|
||||
};
|
||||
}): Promise<{
|
||||
name: string;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
}[]>;
|
||||
findOne(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -230,13 +230,13 @@ export declare class ProductsController {
|
||||
id: number;
|
||||
};
|
||||
}): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -250,13 +250,13 @@ export declare class ProductsController {
|
||||
id: number;
|
||||
};
|
||||
}): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -269,13 +269,13 @@ export declare class ProductsController {
|
||||
id: number;
|
||||
};
|
||||
}): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -289,13 +289,13 @@ export declare class ProductsController {
|
||||
targetProductId: number;
|
||||
movedInventoryCount: number;
|
||||
softDeletedSource: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -305,13 +305,13 @@ export declare class ProductsController {
|
||||
};
|
||||
}>;
|
||||
updateCanonicalName(id: number, body: UpdateCanonicalNameDto): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -321,6 +321,8 @@ export declare class ProductsController {
|
||||
}>;
|
||||
setTags(id: number, body: SetTagsDto): Promise<({
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -328,8 +330,6 @@ export declare class ProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -341,13 +341,13 @@ export declare class ProductsController {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -356,6 +356,8 @@ export declare class ProductsController {
|
||||
isPrivate: boolean;
|
||||
}) | null>;
|
||||
upsertNutrition(id: number, body: UpsertNutritionDto): Promise<{
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -363,11 +365,11 @@ export declare class ProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
}>;
|
||||
update(id: number, body: UpdateProductDto): Promise<{
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -375,8 +377,6 @@ export declare class ProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -388,13 +388,13 @@ export declare class ProductsController {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -403,13 +403,13 @@ export declare class ProductsController {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
permanentDelete(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -418,13 +418,13 @@ export declare class ProductsController {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
remove(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -433,13 +433,13 @@ export declare class ProductsController {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
setStatus(id: number, body: SetProductStatusDto): import(".prisma/client").Prisma.Prisma__ProductClient<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -448,13 +448,13 @@ export declare class ProductsController {
|
||||
isPrivate: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
restore(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
|
||||
+66
-66
@@ -13,6 +13,8 @@ export declare class ProductsService {
|
||||
tag?: string;
|
||||
}): Promise<({
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -20,8 +22,6 @@ export declare class ProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -50,13 +50,13 @@ export declare class ProductsService {
|
||||
parentId: number | null;
|
||||
}) | null;
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -66,18 +66,18 @@ export declare class ProductsService {
|
||||
})[]>;
|
||||
findByOwner(userId: number): Promise<{
|
||||
name: string;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
}[]>;
|
||||
createPrivate(data: CreateProductDto, userId: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -89,13 +89,13 @@ export declare class ProductsService {
|
||||
normalizedName: string;
|
||||
count: number;
|
||||
products: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -105,13 +105,13 @@ export declare class ProductsService {
|
||||
}[];
|
||||
}[]>;
|
||||
findOne(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -120,13 +120,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
create(data: CreateProductDto, ownerId?: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -136,6 +136,8 @@ export declare class ProductsService {
|
||||
}>;
|
||||
update(id: number, data: UpdateProductDto): Promise<{
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -143,8 +145,6 @@ export declare class ProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -156,13 +156,13 @@ export declare class ProductsService {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -171,13 +171,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
updateCanonicalName(id: number, canonicalName: string): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -186,13 +186,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
findDeleted(): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -201,13 +201,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}[]>;
|
||||
remove(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -216,13 +216,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
permanentDelete(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -231,13 +231,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
restore(id: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -252,13 +252,13 @@ export declare class ProductsService {
|
||||
targetProductId: number;
|
||||
movedInventoryCount: number;
|
||||
softDeletedSource: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -270,13 +270,13 @@ export declare class ProductsService {
|
||||
previewMerge(sourceProductId: number, targetProductId: number): Promise<{
|
||||
source: {
|
||||
inventoryCount: number;
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -286,13 +286,13 @@ export declare class ProductsService {
|
||||
};
|
||||
target: {
|
||||
inventoryCount: number;
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -310,13 +310,13 @@ export declare class ProductsService {
|
||||
message: string;
|
||||
updatedCount: number;
|
||||
products: {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -327,6 +327,8 @@ export declare class ProductsService {
|
||||
}>;
|
||||
setTags(productId: number, tagNames: string[]): Promise<({
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -334,8 +336,6 @@ export declare class ProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -347,13 +347,13 @@ export declare class ProductsService {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -362,6 +362,8 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}) | null>;
|
||||
upsertNutrition(productId: number, data: UpsertNutritionDto): Promise<{
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -369,8 +371,6 @@ export declare class ProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
}>;
|
||||
findAllTags(): Promise<{
|
||||
name: string;
|
||||
@@ -411,13 +411,13 @@ export declare class ProductsService {
|
||||
parentId: number | null;
|
||||
}) | null;
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -426,13 +426,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
})[]>;
|
||||
createPending(data: CreateProductDto, userId: number): Promise<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -441,13 +441,13 @@ export declare class ProductsService {
|
||||
isPrivate: boolean;
|
||||
}>;
|
||||
setStatus(id: number, status: string): import(".prisma/client").Prisma.Prisma__ProductClient<{
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
|
||||
@@ -9,8 +9,8 @@ export declare class ReceiptAliasController {
|
||||
}): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
};
|
||||
} & {
|
||||
id: number;
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@ export declare class ReceiptAliasService {
|
||||
findAllForUser(userId: number, role: string): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
canonicalName: string | null;
|
||||
};
|
||||
} & {
|
||||
id: number;
|
||||
|
||||
+16
-5
@@ -141,7 +141,7 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
|
||||
async parseReceipt(file, _isPremium = false, userId) {
|
||||
const rawItems = await this.parseReceiptViaImporter(file);
|
||||
const matched = await this.matchProducts(rawItems, userId);
|
||||
return this.enrichWithAiCategories(matched);
|
||||
return this.enrichWithAiCategories(matched, userId);
|
||||
}
|
||||
async parseReceiptViaImporter(file) {
|
||||
const form = new FormData();
|
||||
@@ -283,7 +283,7 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
|
||||
}
|
||||
return best?.product;
|
||||
}
|
||||
async enrichWithAiCategories(items) {
|
||||
async enrichWithAiCategories(items, userId) {
|
||||
let categories;
|
||||
try {
|
||||
categories = await this.categoriesService.findFlattened();
|
||||
@@ -291,6 +291,12 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
|
||||
catch {
|
||||
return items;
|
||||
}
|
||||
const user = userId
|
||||
? await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { aiEngineEnabled: true },
|
||||
})
|
||||
: null;
|
||||
const enriched = [];
|
||||
for (const item of items) {
|
||||
if (!item.rawName) {
|
||||
@@ -347,9 +353,14 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
|
||||
pushTrace(`rule fallback applied -> "${byRule.path}"`);
|
||||
}
|
||||
if (!nextSuggestion) {
|
||||
pushTrace('ai invoked');
|
||||
nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
|
||||
if (user?.aiEngineEnabled) {
|
||||
pushTrace('ai invoked');
|
||||
nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
|
||||
}
|
||||
else {
|
||||
pushTrace('ai skipped, feature disabled');
|
||||
}
|
||||
}
|
||||
else {
|
||||
pushTrace(`ai skipped, current -> "${nextSuggestion.path}"`);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+44
-1
@@ -1,8 +1,10 @@
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
type AnalysisStatus = 'exact_match' | 'covered_by_pantry' | 'substitutable' | 'missing';
|
||||
export declare class RecipeAnalysisService {
|
||||
private readonly prisma;
|
||||
constructor(prisma: PrismaService);
|
||||
private readonly aiService;
|
||||
constructor(prisma: PrismaService, aiService: AiService);
|
||||
private getAccessibleRecipe;
|
||||
private calculateAvailableQuantity;
|
||||
analyzeRecipeIngredients(id: number, userId: number): Promise<{
|
||||
@@ -46,5 +48,46 @@ export declare class RecipeAnalysisService {
|
||||
missingQuantity: number;
|
||||
}[];
|
||||
}>;
|
||||
rematchRecipeIngredients(id: number, userId: number): Promise<{
|
||||
recipeId: number;
|
||||
ingredients: ({
|
||||
ingredientId: any;
|
||||
rawName: any;
|
||||
quantity: number;
|
||||
unit: any;
|
||||
note: any;
|
||||
status: AnalysisStatus;
|
||||
matchedProductId: any;
|
||||
matchedProductName: any;
|
||||
source: string;
|
||||
availableQuantity: number;
|
||||
missingQuantity: number;
|
||||
} | {
|
||||
ingredientId: any;
|
||||
rawName: any;
|
||||
quantity: number;
|
||||
unit: any;
|
||||
note: any;
|
||||
status: AnalysisStatus;
|
||||
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;
|
||||
}[];
|
||||
}>;
|
||||
}
|
||||
export {};
|
||||
|
||||
+61
-2
@@ -13,9 +13,11 @@ exports.RecipeAnalysisService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const prisma_service_1 = require("../prisma/prisma.service");
|
||||
const units_1 = require("../common/utils/units");
|
||||
const ai_service_1 = require("../ai/ai.service");
|
||||
let RecipeAnalysisService = class RecipeAnalysisService {
|
||||
constructor(prisma) {
|
||||
constructor(prisma, aiService) {
|
||||
this.prisma = prisma;
|
||||
this.aiService = aiService;
|
||||
}
|
||||
async getAccessibleRecipe(id, userId) {
|
||||
const recipe = await this.prisma.recipe.findFirst({
|
||||
@@ -65,16 +67,51 @@ let RecipeAnalysisService = class RecipeAnalysisService {
|
||||
}
|
||||
async analyzeRecipeIngredients(id, userId) {
|
||||
const recipe = await this.getAccessibleRecipe(id, userId);
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { aiEngineEnabled: true },
|
||||
});
|
||||
const pantryItems = await this.prisma.pantryItem.findMany({
|
||||
where: { userId },
|
||||
select: { productId: true },
|
||||
});
|
||||
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
|
||||
const userInventory = await this.prisma.inventoryItem.findMany({
|
||||
select: { productId: true },
|
||||
});
|
||||
const availableProductIds = new Set([
|
||||
...pantryItems.map((p) => p.productId),
|
||||
...userInventory.map((i) => i.productId),
|
||||
]);
|
||||
const availableProducts = availableProductIds.size > 0
|
||||
? await this.prisma.product.findMany({
|
||||
where: { id: { in: Array.from(availableProductIds) }, isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true },
|
||||
})
|
||||
: [];
|
||||
const ingredients = await Promise.all(recipe.ingredients.map(async (ingredient) => {
|
||||
const requiredQuantity = Number(ingredient.quantity ?? 0);
|
||||
const requiredUnit = (ingredient.unit ?? '').trim();
|
||||
const rawName = (ingredient.rawName ?? '').trim() || 'Okänd ingrediens';
|
||||
if (!ingredient.productId || !ingredient.product) {
|
||||
const aiMatches = user?.aiEngineEnabled ? await this.aiService.suggestIngredientMatches(rawName, availableProducts) : [];
|
||||
const aiBest = aiMatches[0];
|
||||
if (aiBest) {
|
||||
const matched = availableProducts.find((p) => p.id === aiBest.productId);
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
quantity: requiredQuantity,
|
||||
unit: requiredUnit,
|
||||
note: ingredient.note ?? null,
|
||||
status: 'substitutable',
|
||||
matchedProductId: aiBest.productId,
|
||||
matchedProductName: matched?.canonicalName || matched?.name || null,
|
||||
source: 'ai_match',
|
||||
availableQuantity: 0,
|
||||
missingQuantity: requiredQuantity,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
@@ -172,6 +209,24 @@ let RecipeAnalysisService = class RecipeAnalysisService {
|
||||
};
|
||||
}
|
||||
}
|
||||
const aiSubs = user?.aiEngineEnabled ? await this.aiService.suggestSubstitutions(rawName, availableProducts) : [];
|
||||
const aiBestSub = aiSubs[0];
|
||||
if (aiBestSub) {
|
||||
const aiProduct = availableProducts.find((p) => p.id === aiBestSub.productId);
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
quantity: requiredQuantity,
|
||||
unit: requiredUnit,
|
||||
note: ingredient.note ?? null,
|
||||
status: 'substitutable',
|
||||
matchedProductId: aiBestSub.productId,
|
||||
matchedProductName: aiProduct?.canonicalName || aiProduct?.name || null,
|
||||
source: 'ai_substitute',
|
||||
availableQuantity,
|
||||
missingQuantity: Math.max(0, requiredQuantity - availableQuantity),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
@@ -208,10 +263,14 @@ let RecipeAnalysisService = class RecipeAnalysisService {
|
||||
shoppingListCandidates,
|
||||
};
|
||||
}
|
||||
async rematchRecipeIngredients(id, userId) {
|
||||
return this.analyzeRecipeIngredients(id, userId);
|
||||
}
|
||||
};
|
||||
exports.RecipeAnalysisService = RecipeAnalysisService;
|
||||
exports.RecipeAnalysisService = RecipeAnalysisService = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||
ai_service_1.AiService])
|
||||
], RecipeAnalysisService);
|
||||
//# sourceMappingURL=recipe-analysis.service.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
export type ProductMatchCandidate = {
|
||||
id: number;
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
normalizedName: string;
|
||||
};
|
||||
export type IngredientSuggestion = {
|
||||
productId: number;
|
||||
productName: string;
|
||||
score: number;
|
||||
};
|
||||
export declare class RecipeMatchingService {
|
||||
private normalize;
|
||||
private levenshtein;
|
||||
private scoreProducts;
|
||||
buildIngredientSuggestions(rawName: string, alternatives: string[] | undefined, products: ProductMatchCandidate[]): IngredientSuggestion[];
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.RecipeMatchingService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
let RecipeMatchingService = class RecipeMatchingService {
|
||||
normalize(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-zåäö0-9\s]/gi, '')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
levenshtein(a, b) {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
return dp[m][n];
|
||||
}
|
||||
scoreProducts(query, products) {
|
||||
const normalizedQuery = this.normalize(query);
|
||||
return products
|
||||
.map((product) => {
|
||||
const targetName = this.normalize(product.canonicalName || product.name);
|
||||
const targetNormalized = this.normalize(product.normalizedName);
|
||||
if (targetNormalized === normalizedQuery || targetName === normalizedQuery) {
|
||||
return { product, score: 100 };
|
||||
}
|
||||
if (targetName.includes(normalizedQuery) || normalizedQuery.includes(targetName)) {
|
||||
return { product, score: 70 };
|
||||
}
|
||||
const dist = this.levenshtein(normalizedQuery, targetName);
|
||||
const maxLen = Math.max(normalizedQuery.length, targetName.length);
|
||||
const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);
|
||||
return { product, score: similarity };
|
||||
})
|
||||
.filter((s) => s.score >= 40)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5);
|
||||
}
|
||||
buildIngredientSuggestions(rawName, alternatives, products) {
|
||||
const variants = alternatives && alternatives.length > 1 ? alternatives : [rawName];
|
||||
const seenIds = new Set();
|
||||
return variants
|
||||
.flatMap((variant) => this.scoreProducts(variant, products))
|
||||
.filter((s) => {
|
||||
if (seenIds.has(s.product.id))
|
||||
return false;
|
||||
seenIds.add(s.product.id);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((s) => ({
|
||||
productId: s.product.id,
|
||||
productName: s.product.canonicalName || s.product.name,
|
||||
score: s.score,
|
||||
}));
|
||||
}
|
||||
};
|
||||
exports.RecipeMatchingService = RecipeMatchingService;
|
||||
exports.RecipeMatchingService = RecipeMatchingService = __decorate([
|
||||
(0, common_1.Injectable)()
|
||||
], RecipeMatchingService);
|
||||
//# sourceMappingURL=recipe-matching.service.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"recipe-matching.service.js","sourceRoot":"","sources":["../../src/recipes/recipe-matching.service.ts"],"names":[],"mappings":"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"}
|
||||
+176
-137
@@ -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 {};
|
||||
|
||||
+11
@@ -49,6 +49,9 @@ let RecipesController = class RecipesController {
|
||||
getRecipeAnalysis(id, user) {
|
||||
return this.recipeAnalysisService.analyzeRecipeIngredients(id, user.userId);
|
||||
}
|
||||
rematchRecipeIngredients(id, user) {
|
||||
return this.recipeAnalysisService.rematchRecipeIngredients(id, user.userId);
|
||||
}
|
||||
findOne(id, user) {
|
||||
return this.recipesService.findOne(id, user.userId);
|
||||
}
|
||||
@@ -115,6 +118,14 @@ __decorate([
|
||||
__metadata("design:paramtypes", [Number, Object]),
|
||||
__metadata("design:returntype", void 0)
|
||||
], RecipesController.prototype, "getRecipeAnalysis", null);
|
||||
__decorate([
|
||||
(0, common_1.Post)(':id/rematch'),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
||||
__param(1, (0, current_user_decorator_1.CurrentUser)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number, Object]),
|
||||
__metadata("design:returntype", void 0)
|
||||
], RecipesController.prototype, "rematchRecipeIngredients", null);
|
||||
__decorate([
|
||||
(0, common_1.Get)(':id'),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"recipes.controller.js","sourceRoot":"","sources":["../../src/recipes/recipes.controller.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"recipes.controller.js","sourceRoot":"","sources":["../../src/recipes/recipes.controller.ts"],"names":[],"mappings":"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"}
|
||||
+2
-1
@@ -13,6 +13,7 @@ const ai_module_1 = require("../ai/ai.module");
|
||||
const recipes_controller_1 = require("./recipes.controller");
|
||||
const recipes_service_1 = require("./recipes.service");
|
||||
const recipe_analysis_service_1 = require("./recipe-analysis.service");
|
||||
const recipe_matching_service_1 = require("./recipe-matching.service");
|
||||
let RecipesModule = class RecipesModule {
|
||||
};
|
||||
exports.RecipesModule = RecipesModule;
|
||||
@@ -20,7 +21,7 @@ exports.RecipesModule = RecipesModule = __decorate([
|
||||
(0, common_1.Module)({
|
||||
imports: [prisma_module_1.PrismaModule, ai_module_1.AiModule],
|
||||
controllers: [recipes_controller_1.RecipesController],
|
||||
providers: [recipes_service_1.RecipesService, recipe_analysis_service_1.RecipeAnalysisService],
|
||||
providers: [recipes_service_1.RecipesService, recipe_analysis_service_1.RecipeAnalysisService, recipe_matching_service_1.RecipeMatchingService],
|
||||
})
|
||||
], RecipesModule);
|
||||
//# sourceMappingURL=recipes.module.js.map
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"recipes.module.js","sourceRoot":"","sources":["../../src/recipes/recipes.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,+CAA2C;AAC3C,6DAAyD;AACzD,uDAAmD;AACnD,uEAAkE;AAO3D,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,oBAAQ,CAAC;QACjC,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,EAAE,+CAAqB,CAAC;KACnD,CAAC;GACW,aAAa,CAAG"}
|
||||
{"version":3,"file":"recipes.module.js","sourceRoot":"","sources":["../../src/recipes/recipes.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,+CAA2C;AAC3C,6DAAyD;AACzD,uDAAmD;AACnD,uEAAkE;AAClE,uEAAkE;AAO3D,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,oBAAQ,CAAC;QACjC,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,EAAE,+CAAqB,EAAE,+CAAqB,CAAC;KAC1E,CAAC;GACW,aAAa,CAAG"}
|
||||
+134
-136
@@ -4,6 +4,7 @@ import { AiService } from '../ai/ai.service';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
import { CreateIngredientDto } from './dto/create-ingredient.dto';
|
||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||
import { RecipeMatchingService } from './recipe-matching.service';
|
||||
export interface AiRecipeSuggestion {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -14,8 +15,9 @@ export interface AiRecipeSuggestion {
|
||||
export declare class RecipesService {
|
||||
private readonly prisma;
|
||||
private readonly aiService;
|
||||
private readonly recipeMatchingService;
|
||||
private readonly logger;
|
||||
constructor(prisma: PrismaService, aiService: AiService);
|
||||
constructor(prisma: PrismaService, aiService: AiService, recipeMatchingService: RecipeMatchingService);
|
||||
private throwRecipeNotFound;
|
||||
private normalizeIngredientName;
|
||||
private assertProductsActive;
|
||||
@@ -85,30 +87,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -119,16 +121,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
})[]>;
|
||||
findOne(id: number, userId: number): Promise<{
|
||||
owner: {
|
||||
@@ -149,30 +151,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -183,16 +185,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
}>;
|
||||
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
|
||||
ingredients: ({
|
||||
@@ -209,30 +211,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -240,16 +242,16 @@ export declare class RecipesService {
|
||||
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, userId: number): Promise<void>;
|
||||
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
|
||||
@@ -271,30 +273,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -305,16 +307,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
}>;
|
||||
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
|
||||
owner: {
|
||||
@@ -335,30 +337,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -369,16 +371,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
}>;
|
||||
shareWithUser(id: number, ownerId: number, username: string): Promise<{
|
||||
owner: {
|
||||
@@ -399,30 +401,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -433,16 +435,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
}>;
|
||||
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
|
||||
owner: {
|
||||
@@ -463,30 +465,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -497,16 +499,16 @@ export declare class RecipesService {
|
||||
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, userId: number): Promise<{
|
||||
ingredients: ({
|
||||
@@ -523,30 +525,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -554,16 +556,16 @@ export declare class RecipesService {
|
||||
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;
|
||||
}>;
|
||||
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
|
||||
product: ({
|
||||
@@ -579,30 +581,30 @@ export declare class RecipesService {
|
||||
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: Prisma.Decimal | null;
|
||||
unit: string | null;
|
||||
recipeId: number;
|
||||
rawName: string;
|
||||
rawLine: string | null;
|
||||
note: string | null;
|
||||
alternativeProductIds: Prisma.JsonValue | null;
|
||||
matchConfidence: number | null;
|
||||
@@ -623,11 +625,7 @@ export declare class RecipesService {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
note: string | null;
|
||||
suggestions: {
|
||||
productId: number;
|
||||
productName: string;
|
||||
score: number;
|
||||
}[];
|
||||
suggestions: import("./recipe-matching.service").IngredientSuggestion[];
|
||||
}[];
|
||||
}>;
|
||||
}
|
||||
|
||||
+6
-54
@@ -19,11 +19,13 @@ const ai_service_1 = require("../ai/ai.service");
|
||||
const download_image_1 = require("../common/utils/download-image");
|
||||
const recipe_parser_1 = require("../common/utils/recipe-parser");
|
||||
const units_1 = require("../common/utils/units");
|
||||
const recipe_matching_service_1 = require("./recipe-matching.service");
|
||||
const IMAGE_DEST_DIR = process.env.IMAGE_DEST_DIR || '/app/recipe-images';
|
||||
let RecipesService = RecipesService_1 = class RecipesService {
|
||||
constructor(prisma, aiService) {
|
||||
constructor(prisma, aiService, recipeMatchingService) {
|
||||
this.prisma = prisma;
|
||||
this.aiService = aiService;
|
||||
this.recipeMatchingService = recipeMatchingService;
|
||||
this.logger = new common_1.Logger(RecipesService_1.name);
|
||||
}
|
||||
throwRecipeNotFound(id) {
|
||||
@@ -608,59 +610,8 @@ Regler:
|
||||
where: { isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true, normalizedName: true },
|
||||
});
|
||||
const normalize = (s) => s.toLowerCase().trim().replace(/[^a-zåäö0-9\s]/gi, '').replace(/\s+/g, ' ');
|
||||
const levenshtein = (a, b) => {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
return dp[m][n];
|
||||
};
|
||||
const ingredientsWithSuggestions = parsed.ingredients.map((ingredient) => {
|
||||
const alternatives = ingredient.alternatives?.length > 1
|
||||
? ingredient.alternatives
|
||||
: [ingredient.rawName];
|
||||
const scoreProduct = (query) => allProducts
|
||||
.map((product) => {
|
||||
const targetName = normalize(product.canonicalName || product.name);
|
||||
const targetNormalized = normalize(product.normalizedName);
|
||||
if (targetNormalized === query || targetName === query) {
|
||||
return { product, score: 100 };
|
||||
}
|
||||
if (targetName.includes(query) || query.includes(targetName)) {
|
||||
return { product, score: 70 };
|
||||
}
|
||||
const dist = levenshtein(query, targetName);
|
||||
const maxLen = Math.max(query.length, targetName.length);
|
||||
const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);
|
||||
return { product, score: similarity };
|
||||
})
|
||||
.filter((s) => s.score >= 40)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5);
|
||||
const seenIds = new Set();
|
||||
const scored = alternatives
|
||||
.flatMap((alt) => scoreProduct(normalize(alt)))
|
||||
.filter((s) => {
|
||||
if (seenIds.has(s.product.id))
|
||||
return false;
|
||||
seenIds.add(s.product.id);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((s) => ({
|
||||
productId: s.product.id,
|
||||
productName: s.product.canonicalName || s.product.name,
|
||||
score: s.score,
|
||||
}));
|
||||
const scored = this.recipeMatchingService.buildIngredientSuggestions(ingredient.rawName, ingredient.alternatives, allProducts);
|
||||
return {
|
||||
rawName: ingredient.rawName,
|
||||
rawLine: ingredient.rawName,
|
||||
@@ -683,6 +634,7 @@ exports.RecipesService = RecipesService;
|
||||
exports.RecipesService = RecipesService = RecipesService_1 = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||
ai_service_1.AiService])
|
||||
ai_service_1.AiService,
|
||||
recipe_matching_service_1.RecipeMatchingService])
|
||||
], RecipesService);
|
||||
//# sourceMappingURL=recipes.service.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+13
-13
@@ -8,6 +8,8 @@ export declare class UserProductsController {
|
||||
}): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -15,8 +17,6 @@ export declare class UserProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -28,13 +28,13 @@ export declare class UserProductsController {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -48,8 +48,8 @@ export declare class UserProductsController {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
})[]>;
|
||||
@@ -58,6 +58,8 @@ export declare class UserProductsController {
|
||||
}, dto: UpsertUserProductDto): import(".prisma/client").Prisma.Prisma__UserProductClient<{
|
||||
product: {
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -65,8 +67,6 @@ export declare class UserProductsController {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -78,13 +78,13 @@ export declare class UserProductsController {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -98,8 +98,8 @@ export declare class UserProductsController {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
@@ -111,8 +111,8 @@ export declare class UserProductsController {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
|
||||
+19
-19
@@ -6,6 +6,8 @@ export declare class UserProductsService {
|
||||
findAll(userId: number): import(".prisma/client").Prisma.PrismaPromise<({
|
||||
product: {
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -13,8 +15,6 @@ export declare class UserProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -26,13 +26,13 @@ export declare class UserProductsService {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -46,14 +46,16 @@ export declare class UserProductsService {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
})[]>;
|
||||
findOne(userId: number, productId: number): import(".prisma/client").Prisma.Prisma__UserProductClient<({
|
||||
product: {
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -61,8 +63,6 @@ export declare class UserProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -74,13 +74,13 @@ export declare class UserProductsService {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -94,14 +94,16 @@ export declare class UserProductsService {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
}) | null, null, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
upsert(userId: number, dto: UpsertUserProductDto): import(".prisma/client").Prisma.Prisma__UserProductClient<{
|
||||
product: {
|
||||
nutrition: {
|
||||
id: number;
|
||||
productId: number;
|
||||
calories: number | null;
|
||||
protein: number | null;
|
||||
fat: number | null;
|
||||
@@ -109,8 +111,6 @@ export declare class UserProductsService {
|
||||
salt: number | null;
|
||||
sugar: number | null;
|
||||
fiber: number | null;
|
||||
id: number;
|
||||
productId: number;
|
||||
} | null;
|
||||
tags: ({
|
||||
tag: {
|
||||
@@ -122,13 +122,13 @@ export declare class UserProductsService {
|
||||
tagId: number;
|
||||
})[];
|
||||
} & {
|
||||
name: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
name: string;
|
||||
categoryId: number | null;
|
||||
canonicalName: string | null;
|
||||
id: number;
|
||||
categoryId: number | null;
|
||||
normalizedName: string;
|
||||
canonicalName: string | null;
|
||||
isActive: boolean;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
@@ -142,8 +142,8 @@ export declare class UserProductsService {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
@@ -153,8 +153,8 @@ export declare class UserProductsService {
|
||||
updatedAt: Date;
|
||||
isPrivate: boolean;
|
||||
productId: number;
|
||||
note: string | null;
|
||||
userId: number;
|
||||
note: string | null;
|
||||
preferredBrand: string | null;
|
||||
preferredStore: string | null;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
|
||||
+9
@@ -8,6 +8,9 @@ declare class SetPremiumDto {
|
||||
declare class SetRecipeSharingDto {
|
||||
canShareRecipes: boolean;
|
||||
}
|
||||
declare class SetAiEngineEnabledDto {
|
||||
aiEngineEnabled: boolean;
|
||||
}
|
||||
declare class AdminCreateUserDto {
|
||||
username: string;
|
||||
email: string;
|
||||
@@ -56,6 +59,7 @@ export declare class UsersController {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}[]>;
|
||||
setRole(id: number, caller: {
|
||||
userId: number;
|
||||
@@ -76,6 +80,11 @@ export declare class UsersController {
|
||||
username: string;
|
||||
canShareRecipes: boolean;
|
||||
}>;
|
||||
setAiEngineEnabled(id: number, dto: SetAiEngineEnabledDto): Promise<{
|
||||
id: number;
|
||||
username: string;
|
||||
aiEngineEnabled: boolean;
|
||||
}>;
|
||||
adminCreateUser(dto: AdminCreateUserDto): Promise<{
|
||||
id: number;
|
||||
username: string;
|
||||
|
||||
+19
@@ -36,6 +36,12 @@ __decorate([
|
||||
(0, class_validator_1.IsBoolean)(),
|
||||
__metadata("design:type", Boolean)
|
||||
], SetRecipeSharingDto.prototype, "canShareRecipes", void 0);
|
||||
class SetAiEngineEnabledDto {
|
||||
}
|
||||
__decorate([
|
||||
(0, class_validator_1.IsBoolean)(),
|
||||
__metadata("design:type", Boolean)
|
||||
], SetAiEngineEnabledDto.prototype, "aiEngineEnabled", void 0);
|
||||
class AdminCreateUserDto {
|
||||
}
|
||||
__decorate([
|
||||
@@ -125,6 +131,10 @@ let UsersController = class UsersController {
|
||||
const updated = await this.usersService.setRecipeSharing(id, dto.canShareRecipes);
|
||||
return { id: updated.id, username: updated.username, canShareRecipes: updated.canShareRecipes };
|
||||
}
|
||||
async setAiEngineEnabled(id, dto) {
|
||||
const updated = await this.usersService.setAiEngineEnabled(id, dto.aiEngineEnabled);
|
||||
return { id: updated.id, username: updated.username, aiEngineEnabled: updated.aiEngineEnabled };
|
||||
}
|
||||
async adminCreateUser(dto) {
|
||||
const user = await this.usersService.adminCreate(dto);
|
||||
return { id: user.id, username: user.username, email: user.email, role: user.role, createdAt: user.createdAt };
|
||||
@@ -209,6 +219,15 @@ __decorate([
|
||||
__metadata("design:paramtypes", [Number, SetRecipeSharingDto]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UsersController.prototype, "setRecipeSharing", null);
|
||||
__decorate([
|
||||
(0, roles_decorator_1.Roles)('admin'),
|
||||
(0, common_1.Patch)(':id/ai-engine'),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
||||
__param(1, (0, common_1.Body)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number, SetAiEngineEnabledDto]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UsersController.prototype, "setAiEngineEnabled", null);
|
||||
__decorate([
|
||||
(0, roles_decorator_1.Roles)('admin'),
|
||||
(0, common_1.Post)(),
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+25
@@ -14,6 +14,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
} | null, null, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
findById(id: number): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
@@ -27,6 +28,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
} | null, null, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
create(data: {
|
||||
username: string;
|
||||
@@ -44,6 +46,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
updateProfile(id: number, data: {
|
||||
firstName?: string;
|
||||
@@ -61,6 +64,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
findAll(): import(".prisma/client").Prisma.PrismaPromise<{
|
||||
id: number;
|
||||
@@ -72,6 +76,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}[]>;
|
||||
setRole(id: number, role: string): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
@@ -85,6 +90,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
setPremium(id: number, isPremium: boolean): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
@@ -98,6 +104,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
setRecipeSharing(id: number, canShareRecipes: boolean): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
@@ -111,6 +118,21 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
setAiEngineEnabled(id: number, aiEngineEnabled: boolean): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
passwordHash: string;
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
adminCreate(data: {
|
||||
username: string;
|
||||
@@ -129,6 +151,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}>;
|
||||
deleteUser(id: number): import(".prisma/client").Prisma.Prisma__UserClient<{
|
||||
id: number;
|
||||
@@ -142,6 +165,7 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
resetPassword(id: number): Promise<{
|
||||
temporaryPassword: string;
|
||||
@@ -158,5 +182,6 @@ export declare class UsersService {
|
||||
role: string;
|
||||
isPremium: boolean;
|
||||
canShareRecipes: boolean;
|
||||
aiEngineEnabled: boolean;
|
||||
}, never, import("@prisma/client/runtime/library").DefaultArgs, import(".prisma/client").Prisma.PrismaClientOptions>;
|
||||
}
|
||||
|
||||
Vendored
+4
@@ -41,6 +41,7 @@ let UsersService = class UsersService {
|
||||
role: true,
|
||||
isPremium: true,
|
||||
canShareRecipes: true,
|
||||
aiEngineEnabled: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { username: 'asc' },
|
||||
@@ -55,6 +56,9 @@ let UsersService = class UsersService {
|
||||
setRecipeSharing(id, canShareRecipes) {
|
||||
return this.prisma.user.update({ where: { id }, data: { canShareRecipes } });
|
||||
}
|
||||
setAiEngineEnabled(id, aiEngineEnabled) {
|
||||
return this.prisma.user.update({ where: { id }, data: { aiEngineEnabled } });
|
||||
}
|
||||
async adminCreate(data) {
|
||||
const existing = await this.prisma.user.findFirst({
|
||||
where: { OR: [{ username: data.username }, { email: data.email }] },
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AddColumn aiEngineEnabled to User table
|
||||
ALTER TABLE "User" ADD COLUMN "aiEngineEnabled" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -17,6 +17,7 @@ model User {
|
||||
role String @default("user")
|
||||
isPremium Boolean @default(false)
|
||||
canShareRecipes Boolean @default(true)
|
||||
aiEngineEnabled Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -13,10 +13,128 @@ export type CategorySuggestion = {
|
||||
usedFallback: boolean;
|
||||
};
|
||||
|
||||
export type AiIngredientMatchSuggestion = {
|
||||
productId: number;
|
||||
reason?: string;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
};
|
||||
|
||||
export type AiSubstitutionSuggestion = {
|
||||
productId: number;
|
||||
reason?: string;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private readonly logger = new Logger(AiService.name);
|
||||
|
||||
async suggestIngredientMatches(
|
||||
rawIngredient: string,
|
||||
candidates: Array<{ id: number; name: string; canonicalName?: string | null }>,
|
||||
): Promise<AiIngredientMatchSuggestion[]> {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey || candidates.length === 0) return [];
|
||||
|
||||
const candidateList = candidates
|
||||
.map((c) => `[${c.id}] ${c.canonicalName || c.name}`)
|
||||
.join('\n');
|
||||
|
||||
const systemPrompt = `Du matchar en ingrediensrad mot produktkandidater.
|
||||
Svara ENDAST med JSON: {"matches":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
||||
Regler:
|
||||
1. Välj max 3 kandidater.
|
||||
2. Om inget passar, returnera tom lista.`;
|
||||
|
||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nKandidater:\n${candidateList}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
max_tokens: 300,
|
||||
temperature: 0.1,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`suggestIngredientMatches API-fel: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { choices: { message: { content: string } }[] };
|
||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
||||
const parsed = JSON.parse(raw) as { matches?: AiIngredientMatchSuggestion[] };
|
||||
return Array.isArray(parsed.matches) ? parsed.matches.slice(0, 3) : [];
|
||||
} catch (err) {
|
||||
this.logger.warn(`suggestIngredientMatches misslyckades: ${String(err)}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async suggestSubstitutions(
|
||||
rawIngredient: string,
|
||||
availableProducts: Array<{ id: number; name: string; canonicalName?: string | null }>,
|
||||
): Promise<AiSubstitutionSuggestion[]> {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey || availableProducts.length === 0) return [];
|
||||
|
||||
const productList = availableProducts
|
||||
.map((p) => `[${p.id}] ${p.canonicalName || p.name}`)
|
||||
.join('\n');
|
||||
|
||||
const systemPrompt = `Du föreslår ersättningsvaror för en ingrediens.
|
||||
Svara ENDAST med JSON: {"substitutions":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
||||
Regler:
|
||||
1. Välj max 3 ersättningar.
|
||||
2. Om inget passar, returnera tom lista.`;
|
||||
|
||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nTillgängliga produkter:\n${productList}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
max_tokens: 300,
|
||||
temperature: 0.2,
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.warn(`suggestSubstitutions API-fel: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { choices: { message: { content: string } }[] };
|
||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
||||
const parsed = JSON.parse(raw) as { substitutions?: AiSubstitutionSuggestion[] };
|
||||
return Array.isArray(parsed.substitutions) ? parsed.substitutions.slice(0, 3) : [];
|
||||
} catch (err) {
|
||||
this.logger.warn(`suggestSubstitutions misslyckades: ${String(err)}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async suggestCategory(
|
||||
productName: string,
|
||||
categories: FlatCategory[],
|
||||
|
||||
@@ -159,7 +159,7 @@ export class ReceiptImportService {
|
||||
const matched = await this.matchProducts(rawItems, userId);
|
||||
|
||||
// Steg 3: Regel + AI-kategorisering för alla användare
|
||||
return this.enrichWithAiCategories(matched);
|
||||
return this.enrichWithAiCategories(matched, userId);
|
||||
}
|
||||
|
||||
private async parseReceiptViaImporter(file: Express.Multer.File): Promise<ParsedReceiptItem[]> {
|
||||
@@ -341,7 +341,7 @@ export class ReceiptImportService {
|
||||
return best?.product;
|
||||
}
|
||||
|
||||
private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
|
||||
private async enrichWithAiCategories(items: ParsedReceiptItem[], userId?: number): Promise<ParsedReceiptItem[]> {
|
||||
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
||||
try {
|
||||
categories = await this.categoriesService.findFlattened();
|
||||
@@ -349,6 +349,13 @@ export class ReceiptImportService {
|
||||
return items; // Om kategoritjänsten är otillgänglig, returnera utan AI-förslag
|
||||
}
|
||||
|
||||
const user = userId
|
||||
? await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { aiEngineEnabled: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
const enriched: ParsedReceiptItem[] = [];
|
||||
for (const item of items) {
|
||||
if (!item.rawName) {
|
||||
@@ -424,9 +431,13 @@ export class ReceiptImportService {
|
||||
|
||||
// AI används som fallback när varken matchning eller regler satte kategori
|
||||
if (!nextSuggestion) {
|
||||
pushTrace('ai invoked');
|
||||
nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
|
||||
if (user?.aiEngineEnabled) {
|
||||
pushTrace('ai invoked');
|
||||
nextSuggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
pushTrace(`ai result -> "${nextSuggestion.path}" (${nextSuggestion.confidence})`);
|
||||
} else {
|
||||
pushTrace('ai skipped, feature disabled');
|
||||
}
|
||||
} else {
|
||||
pushTrace(`ai skipped, current -> "${nextSuggestion.path}"`);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { canConvert, convertUnit } from '../common/utils/units';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
|
||||
type AnalysisStatus = 'exact_match' | 'covered_by_pantry' | 'substitutable' | 'missing';
|
||||
|
||||
@Injectable()
|
||||
export class RecipeAnalysisService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
) {}
|
||||
|
||||
private async getAccessibleRecipe(id: number, userId: number) {
|
||||
const recipe = await this.prisma.recipe.findFirst({
|
||||
@@ -66,12 +70,31 @@ export class RecipeAnalysisService {
|
||||
async analyzeRecipeIngredients(id: number, userId: number) {
|
||||
const recipe = await this.getAccessibleRecipe(id, userId);
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { aiEngineEnabled: true },
|
||||
});
|
||||
|
||||
const pantryItems = await this.prisma.pantryItem.findMany({
|
||||
where: { userId },
|
||||
select: { productId: true },
|
||||
});
|
||||
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
|
||||
|
||||
const userInventory = await this.prisma.inventoryItem.findMany({
|
||||
select: { productId: true },
|
||||
});
|
||||
const availableProductIds = new Set<number>([
|
||||
...pantryItems.map((p) => p.productId),
|
||||
...userInventory.map((i) => i.productId),
|
||||
]);
|
||||
const availableProducts = availableProductIds.size > 0
|
||||
? await this.prisma.product.findMany({
|
||||
where: { id: { in: Array.from(availableProductIds) }, isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const ingredients = await Promise.all(
|
||||
recipe.ingredients.map(async (ingredient: any) => {
|
||||
const requiredQuantity = Number(ingredient.quantity ?? 0);
|
||||
@@ -79,6 +102,25 @@ export class RecipeAnalysisService {
|
||||
const rawName = (ingredient.rawName ?? '').trim() || 'Okänd ingrediens';
|
||||
|
||||
if (!ingredient.productId || !ingredient.product) {
|
||||
const aiMatches = user?.aiEngineEnabled ? await this.aiService.suggestIngredientMatches(rawName, availableProducts) : [];
|
||||
const aiBest = aiMatches[0];
|
||||
if (aiBest) {
|
||||
const matched = availableProducts.find((p) => p.id === aiBest.productId);
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
quantity: requiredQuantity,
|
||||
unit: requiredUnit,
|
||||
note: ingredient.note ?? null,
|
||||
status: 'substitutable' as AnalysisStatus,
|
||||
matchedProductId: aiBest.productId,
|
||||
matchedProductName: matched?.canonicalName || matched?.name || null,
|
||||
source: 'ai_match',
|
||||
availableQuantity: 0,
|
||||
missingQuantity: requiredQuantity,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
@@ -185,6 +227,25 @@ export class RecipeAnalysisService {
|
||||
}
|
||||
}
|
||||
|
||||
const aiSubs = user?.aiEngineEnabled ? await this.aiService.suggestSubstitutions(rawName, availableProducts) : [];
|
||||
const aiBestSub = aiSubs[0];
|
||||
if (aiBestSub) {
|
||||
const aiProduct = availableProducts.find((p) => p.id === aiBestSub.productId);
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
quantity: requiredQuantity,
|
||||
unit: requiredUnit,
|
||||
note: ingredient.note ?? null,
|
||||
status: 'substitutable' as AnalysisStatus,
|
||||
matchedProductId: aiBestSub.productId,
|
||||
matchedProductName: aiProduct?.canonicalName || aiProduct?.name || null,
|
||||
source: 'ai_substitute',
|
||||
availableQuantity,
|
||||
missingQuantity: Math.max(0, requiredQuantity - availableQuantity),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
rawName,
|
||||
@@ -225,4 +286,8 @@ export class RecipeAnalysisService {
|
||||
shoppingListCandidates,
|
||||
};
|
||||
}
|
||||
|
||||
async rematchRecipeIngredients(id: number, userId: number) {
|
||||
return this.analyzeRecipeIngredients(id, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export type ProductMatchCandidate = {
|
||||
id: number;
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
normalizedName: string;
|
||||
};
|
||||
|
||||
export type IngredientSuggestion = {
|
||||
productId: number;
|
||||
productName: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RecipeMatchingService {
|
||||
private normalize(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-zåäö0-9\s]/gi, '')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
private levenshtein(a: string, b: string): number {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
||||
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
||||
);
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m][n];
|
||||
}
|
||||
|
||||
private scoreProducts(query: string, products: ProductMatchCandidate[]) {
|
||||
const normalizedQuery = this.normalize(query);
|
||||
|
||||
return products
|
||||
.map((product) => {
|
||||
const targetName = this.normalize(product.canonicalName || product.name);
|
||||
const targetNormalized = this.normalize(product.normalizedName);
|
||||
|
||||
if (targetNormalized === normalizedQuery || targetName === normalizedQuery) {
|
||||
return { product, score: 100 };
|
||||
}
|
||||
|
||||
if (targetName.includes(normalizedQuery) || normalizedQuery.includes(targetName)) {
|
||||
return { product, score: 70 };
|
||||
}
|
||||
|
||||
const dist = this.levenshtein(normalizedQuery, targetName);
|
||||
const maxLen = Math.max(normalizedQuery.length, targetName.length);
|
||||
const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);
|
||||
|
||||
return { product, score: similarity };
|
||||
})
|
||||
.filter((s) => s.score >= 40)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
buildIngredientSuggestions(
|
||||
rawName: string,
|
||||
alternatives: string[] | undefined,
|
||||
products: ProductMatchCandidate[],
|
||||
): IngredientSuggestion[] {
|
||||
const variants = alternatives && alternatives.length > 1 ? alternatives : [rawName];
|
||||
|
||||
const seenIds = new Set<number>();
|
||||
|
||||
return variants
|
||||
.flatMap((variant) => this.scoreProducts(variant, products))
|
||||
.filter((s) => {
|
||||
if (seenIds.has(s.product.id)) return false;
|
||||
seenIds.add(s.product.id);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((s) => ({
|
||||
productId: s.product.id,
|
||||
productName: s.product.canonicalName || s.product.name,
|
||||
score: s.score,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,14 @@ export class RecipesController {
|
||||
return this.recipeAnalysisService.analyzeRecipeIngredients(id, user.userId);
|
||||
}
|
||||
|
||||
@Post(':id/rematch')
|
||||
rematchRecipeIngredients(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() user: { userId: number },
|
||||
) {
|
||||
return this.recipeAnalysisService.rematchRecipeIngredients(id, user.userId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
|
||||
@@ -4,10 +4,11 @@ import { AiModule } from '../ai/ai.module';
|
||||
import { RecipesController } from './recipes.controller';
|
||||
import { RecipesService } from './recipes.service';
|
||||
import { RecipeAnalysisService } from './recipe-analysis.service';
|
||||
import { RecipeMatchingService } from './recipe-matching.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AiModule],
|
||||
controllers: [RecipesController],
|
||||
providers: [RecipesService, RecipeAnalysisService],
|
||||
providers: [RecipesService, RecipeAnalysisService, RecipeMatchingService],
|
||||
})
|
||||
export class RecipesModule {}
|
||||
@@ -9,7 +9,8 @@ import { CreateIngredientDto } from './dto/create-ingredient.dto';
|
||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||
import { downloadAndOptimizeImage } from '../common/utils/download-image';
|
||||
import { parseRecipeMarkdown, ParsedRecipe, ParsedIngredient } from '../common/utils/recipe-parser';
|
||||
import { normalizeUnit, getUnitType, convertUnit, canConvert } from '../common/utils/units';
|
||||
import { convertUnit, canConvert } from '../common/utils/units';
|
||||
import { RecipeMatchingService } from './recipe-matching.service';
|
||||
|
||||
const IMAGE_DEST_DIR = process.env.IMAGE_DEST_DIR || '/app/recipe-images';
|
||||
|
||||
@@ -28,6 +29,7 @@ export class RecipesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly recipeMatchingService: RecipeMatchingService,
|
||||
) {}
|
||||
|
||||
private throwRecipeNotFound(id: number): never {
|
||||
@@ -721,76 +723,12 @@ Regler:
|
||||
select: { id: true, name: true, canonicalName: true, normalizedName: true },
|
||||
});
|
||||
|
||||
// Normalisera en sträng för jämförelse (lowercase, trim, ta bort skiljetecken)
|
||||
const normalize = (s: string) =>
|
||||
s.toLowerCase().trim().replace(/[^a-zåäö0-9\s]/gi, '').replace(/\s+/g, ' ');
|
||||
|
||||
// Enkel Levenshtein-distans
|
||||
const levenshtein = (a: string, b: string): number => {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
||||
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
||||
);
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
return dp[m][n];
|
||||
};
|
||||
|
||||
const ingredientsWithSuggestions = parsed.ingredients.map((ingredient: ParsedIngredient) => {
|
||||
// Kör matchning mot alla alternativ och slå ihop suggestions
|
||||
const alternatives = ingredient.alternatives?.length > 1
|
||||
? ingredient.alternatives
|
||||
: [ingredient.rawName];
|
||||
|
||||
const scoreProduct = (query: string) => allProducts
|
||||
.map((product) => {
|
||||
const targetName = normalize(product.canonicalName || product.name);
|
||||
const targetNormalized = normalize(product.normalizedName);
|
||||
|
||||
// Exakt träff på normalizedName prioriteras
|
||||
if (targetNormalized === query || targetName === query) {
|
||||
return { product, score: 100 };
|
||||
}
|
||||
|
||||
// Delsträng-match
|
||||
if (targetName.includes(query) || query.includes(targetName)) {
|
||||
return { product, score: 70 };
|
||||
}
|
||||
|
||||
// Levenshtein-baserad likhet
|
||||
const dist = levenshtein(query, targetName);
|
||||
const maxLen = Math.max(query.length, targetName.length);
|
||||
const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);
|
||||
|
||||
return { product, score: similarity };
|
||||
})
|
||||
.filter((s) => s.score >= 40)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5);
|
||||
|
||||
// Slå ihop suggestions från alla alternativ, deduplicera på productId, ta topp 5
|
||||
const seenIds = new Set<number>();
|
||||
const scored = alternatives
|
||||
.flatMap((alt) => scoreProduct(normalize(alt)))
|
||||
.filter((s) => {
|
||||
if (seenIds.has(s.product.id)) return false;
|
||||
seenIds.add(s.product.id);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((s) => ({
|
||||
productId: s.product.id,
|
||||
productName: s.product.canonicalName || s.product.name,
|
||||
score: s.score,
|
||||
}));
|
||||
const scored = this.recipeMatchingService.buildIngredientSuggestions(
|
||||
ingredient.rawName,
|
||||
ingredient.alternatives,
|
||||
allProducts,
|
||||
);
|
||||
|
||||
return {
|
||||
rawName: ingredient.rawName,
|
||||
|
||||
@@ -19,6 +19,11 @@ class SetRecipeSharingDto {
|
||||
canShareRecipes: boolean;
|
||||
}
|
||||
|
||||
class SetAiEngineEnabledDto {
|
||||
@IsBoolean()
|
||||
aiEngineEnabled: boolean;
|
||||
}
|
||||
|
||||
class AdminCreateUserDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@@ -128,6 +133,16 @@ export class UsersController {
|
||||
return { id: updated.id, username: updated.username, canShareRecipes: updated.canShareRecipes };
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Patch(':id/ai-engine')
|
||||
async setAiEngineEnabled(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: SetAiEngineEnabledDto,
|
||||
) {
|
||||
const updated = await this.usersService.setAiEngineEnabled(id, dto.aiEngineEnabled);
|
||||
return { id: updated.id, username: updated.username, aiEngineEnabled: updated.aiEngineEnabled };
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Post()
|
||||
async adminCreateUser(
|
||||
|
||||
@@ -34,6 +34,7 @@ export class UsersService {
|
||||
role: true,
|
||||
isPremium: true,
|
||||
canShareRecipes: true,
|
||||
aiEngineEnabled: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { username: 'asc' },
|
||||
@@ -52,6 +53,10 @@ export class UsersService {
|
||||
return this.prisma.user.update({ where: { id }, data: { canShareRecipes } });
|
||||
}
|
||||
|
||||
setAiEngineEnabled(id: number, aiEngineEnabled: boolean) {
|
||||
return this.prisma.user.update({ where: { id }, data: { aiEngineEnabled } });
|
||||
}
|
||||
|
||||
async adminCreate(data: { username: string; email: string; password: string; role?: string }) {
|
||||
const existing = await this.prisma.user.findFirst({
|
||||
where: { OR: [{ username: data.username }, { email: data.email }] },
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -46,6 +46,7 @@ class RecipeApiPaths {
|
||||
static String unshare(int id, String username) => '/recipes/$id/share/${Uri.encodeComponent(username)}';
|
||||
static String inventoryPreview(int id) => '/recipes/$id/inventory-preview';
|
||||
static String analysis(int id) => '/recipes/$id/analysis';
|
||||
static String rematch(int id) => '/recipes/$id/rematch';
|
||||
static const parseMarkdown = '/recipes/parse-markdown';
|
||||
static const aiSuggestions = '/recipes/ai-suggestions';
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/api/api_error_mapper.dart';
|
||||
import '../../../core/api/api_paths.dart';
|
||||
import '../../../core/api/api_providers.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/ui/async_state_views.dart';
|
||||
import '../../auth/data/auth_providers.dart';
|
||||
import '../data/inventory_providers.dart';
|
||||
import 'swipeable_inventory_tile.dart';
|
||||
|
||||
@@ -19,6 +22,65 @@ class InventoryScreen extends ConsumerWidget {
|
||||
(value: 'bestBeforeDesc', label: context.l10n.inventorySortBestBeforeDesc),
|
||||
];
|
||||
|
||||
Future<void> _createManualProduct(BuildContext context, WidgetRef ref) async {
|
||||
final nameCtrl = TextEditingController();
|
||||
final created = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Ny produkt'),
|
||||
content: TextField(
|
||||
controller: nameCtrl,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Produktnamn',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final value = nameCtrl.text.trim();
|
||||
if (value.isEmpty) return;
|
||||
Navigator.pop(dialogContext, value);
|
||||
},
|
||||
child: const Text('Skapa'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
nameCtrl.dispose();
|
||||
|
||||
if (created == null || created.trim().isEmpty || !context.mounted) return;
|
||||
|
||||
try {
|
||||
final token = ref.read(authStateProvider).maybeWhen(
|
||||
data: (t) => t,
|
||||
orElse: () => null,
|
||||
) ??
|
||||
await ref.read(authStateProvider.future);
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.postJson(
|
||||
ProductApiPaths.createPrivate,
|
||||
body: {'name': created.trim()},
|
||||
token: token,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Produkt skapad. Lägg nu till den i inventariet.')),
|
||||
);
|
||||
context.push('/inventory/create');
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final location = ref.watch(inventoryLocationFilterProvider);
|
||||
@@ -96,10 +158,21 @@ class InventoryScreen extends ConsumerWidget {
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () => context.push('/inventory/create'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(context.l10n.addAction),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => context.push('/inventory/create'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(context.l10n.addAction),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => _createManualProduct(context, ref),
|
||||
icon: const Icon(Icons.add_box_outlined),
|
||||
label: const Text('Ny produkt'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -129,6 +202,12 @@ class InventoryScreen extends ConsumerWidget {
|
||||
label: Text(context.l10n.addAction),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => _createManualProduct(context, ref),
|
||||
icon: const Icon(Icons.add_box_outlined),
|
||||
label: const Text('Ny produkt'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () => context.go('/recipes'),
|
||||
icon: const Icon(Icons.restaurant_menu),
|
||||
|
||||
@@ -196,6 +196,28 @@ class RecipeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<RecipeAnalysis> rematchRecipeIngredients(int id,
|
||||
{String? token}) async {
|
||||
try {
|
||||
final data = await _api.postJson(
|
||||
RecipeApiPaths.rematch(id),
|
||||
body: const {},
|
||||
token: token,
|
||||
);
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
|
||||
}
|
||||
return RecipeAnalysis.fromJson(data);
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte köra om matchning.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ParsedRecipe> parseMarkdown(String markdown,
|
||||
{String? token}) async {
|
||||
try {
|
||||
|
||||
@@ -521,6 +521,29 @@ class _InventoryPreviewSectionState
|
||||
extends ConsumerState<_InventoryPreviewSection> {
|
||||
bool _loaded = false;
|
||||
|
||||
Future<void> _runRematch() async {
|
||||
try {
|
||||
final token = ref.read(authStateProvider).maybeWhen(
|
||||
data: (t) => t,
|
||||
orElse: () => null,
|
||||
) ??
|
||||
await ref.read(authStateProvider.future);
|
||||
await ref
|
||||
.read(recipeRepositoryProvider)
|
||||
.rematchRecipeIngredients(widget.recipeId, token: token);
|
||||
ref.invalidate(recipeAnalysisProvider(widget.recipeId));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Matchning uppdaterad')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -538,6 +561,12 @@ class _InventoryPreviewSectionState
|
||||
icon: const Icon(Icons.search, size: 16),
|
||||
label: const Text('Kontrollera inventarie'),
|
||||
),
|
||||
if (_loaded)
|
||||
IconButton(
|
||||
tooltip: 'Kör om matchning',
|
||||
icon: const Icon(Icons.auto_fix_high),
|
||||
onPressed: _runRematch,
|
||||
),
|
||||
if (_loaded)
|
||||
IconButton(
|
||||
tooltip: 'Uppdatera',
|
||||
|
||||
Reference in New Issue
Block a user