feat: add servings field to Recipe model and implement inventory comparison functionality
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Recipe` ADD COLUMN `servings` INTEGER NULL;
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user