feat: add servings field to Recipe model and implement inventory comparison functionality

This commit is contained in:
Nils-Johan Gynther
2026-04-17 18:48:08 +02:00
parent 8a86b0aebd
commit 8e0aed032c
10 changed files with 260 additions and 20 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Recipe` ADD COLUMN `servings` INTEGER NULL;
+1
View File
@@ -72,6 +72,7 @@ model Recipe {
description String? @db.Text
instructions String? @db.Text
imageUrl String?
servings Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -16,6 +16,11 @@ export class MealPlanController {
return this.mealPlanService.shoppingList(from, to);
}
@Get('inventory-compare')
inventoryCompare(@Query('from') from: string, @Query('to') to: string) {
return this.mealPlanService.inventoryCompare(from, to);
}
@Post()
upsert(@Body() dto: CreateMealPlanEntryDto) {
return this.mealPlanService.upsert(dto);
@@ -77,4 +77,55 @@ export class MealPlanService {
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
}
/** Jämför veckans ingrediensbehov mot inventariet */
async inventoryCompare(from: string, to: string) {
const entries = await this.findByRange(from, to);
// Aggregera ingredienser per produkt+enhet
const map = new Map<string, { productId: number; name: string; required: number; unit: string }>();
for (const entry of entries) {
for (const ing of entry.recipe.ingredients) {
const key = `${ing.product.id}-${ing.unit}`;
const qty = Number(ing.quantity);
const existing = map.get(key);
if (existing) {
existing.required += qty;
} else {
map.set(key, {
productId: ing.product.id,
name: ing.product.canonicalName || ing.product.name,
required: qty,
unit: ing.unit,
});
}
}
}
// Kontrollera inventariet för varje ingrediens
const result = await Promise.all(
Array.from(map.values()).map(async (item) => {
const inventoryItems = await this.prisma.inventoryItem.findMany({
where: { productId: item.productId },
});
const available = inventoryItems
.filter((i: any) => i.unit.trim().toLowerCase() === item.unit.trim().toLowerCase())
.reduce((sum: number, i: any) => sum + Number(i.quantity), 0);
return {
productId: item.productId,
name: item.name,
required: item.required,
unit: item.unit,
available,
missing: Math.max(0, item.required - available),
status: (available >= item.required ? 'enough' : 'missing') as 'enough' | 'missing',
};
}),
);
return result.sort((a, b) => {
if (a.status !== b.status) return a.status === 'missing' ? -1 : 1;
return a.name.localeCompare(b.name, 'sv');
});
}
}
+9 -4
View File
@@ -1,12 +1,12 @@
import {
IsArray,
IsOptional,
IsString,
ValidateNested,
ArrayMinSize,
IsInt,
IsNumber,
IsOptional,
IsString,
Min,
ValidateNested,
ArrayMinSize,
} from 'class-validator';
import { Type } from 'class-transformer';
@@ -42,6 +42,11 @@ export class CreateRecipeDto {
@IsString()
imageUrl?: string;
@IsOptional()
@IsInt()
@Min(1)
servings?: number;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
+7 -5
View File
@@ -295,7 +295,7 @@ export class RecipesService {
include: {
ingredients: {
include: {
product: true,
product: { include: { nutrition: true } },
},
},
},
@@ -308,7 +308,7 @@ export class RecipesService {
include: {
ingredients: {
include: {
product: true,
product: { include: { nutrition: true } },
},
},
},
@@ -343,6 +343,7 @@ export class RecipesService {
name: updateRecipeDto.name,
description: updateRecipeDto.description || null,
instructions: updateRecipeDto.instructions || null,
servings: updateRecipeDto.servings ?? null,
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
ingredients: {
create: updateRecipeDto.ingredients.map((ingredient) => ({
@@ -356,7 +357,7 @@ export class RecipesService {
include: {
ingredients: {
include: {
product: true,
product: { include: { nutrition: true } },
},
},
},
@@ -389,7 +390,7 @@ export class RecipesService {
return this.prisma.recipe.update({
where: { id },
data: { imageUrl },
include: { ingredients: { include: { product: true } } },
include: { ingredients: { include: { product: { include: { nutrition: true } } } } },
});
}
@@ -411,6 +412,7 @@ export class RecipesService {
description: createRecipeDto.description || null,
instructions: createRecipeDto.instructions || null,
imageUrl,
servings: createRecipeDto.servings ?? null,
ingredients: {
create: createRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId,
@@ -423,7 +425,7 @@ export class RecipesService {
include: {
ingredients: {
include: {
product: true,
product: { include: { nutrition: true } },
},
},
},