feat(meal-plan): add servings field to MealPlanEntry and update related functionality

feat(products): implement bulk update for product categories

feat(recipes): add servings input to WriteRecipePage and update MealPlanClient for servings management

refactor(types): enhance Product and Category types with additional properties
This commit is contained in:
Nils-Johan Gynther
2026-04-17 22:50:41 +02:00
parent a81bd6b460
commit 21dc06829a
12 changed files with 323 additions and 52 deletions
@@ -1,4 +1,4 @@
import { IsDateString, IsInt, IsPositive } from 'class-validator';
import { IsDateString, IsInt, IsOptional, IsPositive, Min } from 'class-validator';
export class CreateMealPlanEntryDto {
@IsDateString()
@@ -7,4 +7,9 @@ export class CreateMealPlanEntryDto {
@IsInt()
@IsPositive()
recipeId: number;
@IsOptional()
@IsInt()
@Min(1)
servings?: number;
}
+13 -6
View File
@@ -6,6 +6,7 @@ const recipeSelect = {
id: true,
name: true,
imageUrl: true,
servings: true,
ingredients: {
select: {
quantity: true,
@@ -36,8 +37,8 @@ export class MealPlanService {
const date = new Date(dto.date);
return this.prisma.mealPlanEntry.upsert({
where: { date },
create: { date, recipeId: dto.recipeId },
update: { recipeId: dto.recipeId },
create: { date, recipeId: dto.recipeId, servings: dto.servings ?? null },
update: { recipeId: dto.recipeId, servings: dto.servings ?? null },
include: { recipe: { select: recipeSelect } },
});
}
@@ -55,13 +56,16 @@ export class MealPlanService {
async shoppingList(from: string, to: string) {
const entries = await this.findByRange(from, to);
// Summera ingredienser per produkt+enhet
// Summera ingredienser per produkt+enhet (skalat per portionsantal)
const map = new Map<string, { productId: number; name: string; quantity: number; unit: string }>();
for (const entry of entries) {
const recipeServings = (entry.recipe as any).servings as number | null;
const entryServings = (entry as any).servings as number | null;
const scale = recipeServings && entryServings ? entryServings / recipeServings : 1;
for (const ing of entry.recipe.ingredients) {
const key = `${ing.product.id}-${ing.unit}`;
const existing = map.get(key);
const qty = Number(ing.quantity);
const qty = Number(ing.quantity) * scale;
if (existing) {
existing.quantity += qty;
} else {
@@ -82,12 +86,15 @@ export class MealPlanService {
async inventoryCompare(from: string, to: string) {
const entries = await this.findByRange(from, to);
// Aggregera ingredienser per produkt+enhet
// Aggregera ingredienser per produkt+enhet (skalat per portionsantal)
const map = new Map<string, { productId: number; name: string; required: number; unit: string }>();
for (const entry of entries) {
const recipeServings = (entry.recipe as any).servings as number | null;
const entryServings = (entry as any).servings as number | null;
const scale = recipeServings && entryServings ? entryServings / recipeServings : 1;
for (const ing of entry.recipe.ingredients) {
const key = `${ing.product.id}-${ing.unit}`;
const qty = Number(ing.quantity);
const qty = Number(ing.quantity) * scale;
const existing = map.get(key);
if (existing) {
existing.required += qty;
@@ -0,0 +1,12 @@
import { IsArray, IsInt, IsNumber, IsOptional, ArrayMinSize } from 'class-validator';
export class BulkUpdateProductsDto {
@IsArray()
@ArrayMinSize(1)
@IsInt({ each: true })
ids: number[];
@IsOptional()
@IsNumber()
categoryId?: number | null;
}
+7 -1
View File
@@ -17,7 +17,7 @@ import { ProductsService } from './products.service';
import { MergeProductsDto } from './dto/merge-products.dto';
import { UpdateCanonicalNameDto } from './dto/update-canonical-name.dto';
import { SetTagsDto } from './dto/set-tags.dto';
import { UpsertNutritionDto } from './dto/upsert-nutrition.dto';
import { BulkUpdateProductsDto } from './dto/bulk-update-products.dto';
@Controller('products')
export class ProductsController {
@@ -116,4 +116,10 @@ export class ProductsController {
resetAll() {
return this.productsService.resetAll();
}
@Post('bulk-update')
@HttpCode(200)
bulkUpdate(@Body() body: BulkUpdateProductsDto) {
return this.productsService.bulkUpdate(body.ids, { categoryId: body.categoryId });
}
}
+10
View File
@@ -397,4 +397,14 @@ export class ProductsService {
]);
return { ok: true };
}
async bulkUpdate(ids: number[], data: { categoryId?: number | null }) {
const updateData: Record<string, any> = {};
if ('categoryId' in data) {
updateData.categoryId = data.categoryId;
}
if (Object.keys(updateData).length === 0) return { updated: 0 };
await this.prisma.product.updateMany({ where: { id: { in: ids } }, data: updateData });
return { updated: ids.length };
}
}