feat: implement meal planning feature with CRUD operations and UI integration
This commit is contained in:
@@ -6,6 +6,7 @@ import { InventoryModule } from './inventory/inventory.module';
|
||||
import { RecipesModule } from './recipes/recipes.module';
|
||||
import { QuickImportModule } from './quick-import/quick-import.module';
|
||||
import { PantryModule } from './pantry/pantry.module';
|
||||
import { MealPlanModule } from './meal-plan/meal-plan.module';
|
||||
|
||||
|
||||
@Module({
|
||||
@@ -17,6 +18,7 @@ import { PantryModule } from './pantry/pantry.module';
|
||||
RecipesModule,
|
||||
QuickImportModule,
|
||||
PantryModule,
|
||||
MealPlanModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IsDateString, IsInt, IsPositive } from 'class-validator';
|
||||
|
||||
export class CreateMealPlanEntryDto {
|
||||
@IsDateString()
|
||||
date: string; // YYYY-MM-DD
|
||||
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
recipeId: number;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { MealPlanService } from './meal-plan.service';
|
||||
import { CreateMealPlanEntryDto } from './dto/create-meal-plan-entry.dto';
|
||||
|
||||
@Controller('meal-plan')
|
||||
export class MealPlanController {
|
||||
constructor(private readonly mealPlanService: MealPlanService) {}
|
||||
|
||||
@Get()
|
||||
findByRange(@Query('from') from: string, @Query('to') to: string) {
|
||||
return this.mealPlanService.findByRange(from, to);
|
||||
}
|
||||
|
||||
@Get('shopping-list')
|
||||
shoppingList(@Query('from') from: string, @Query('to') to: string) {
|
||||
return this.mealPlanService.shoppingList(from, to);
|
||||
}
|
||||
|
||||
@Post()
|
||||
upsert(@Body() dto: CreateMealPlanEntryDto) {
|
||||
return this.mealPlanService.upsert(dto);
|
||||
}
|
||||
|
||||
@Delete(':date')
|
||||
removeByDate(@Param('date') date: string) {
|
||||
return this.mealPlanService.removeByDate(date);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MealPlanController } from './meal-plan.controller';
|
||||
import { MealPlanService } from './meal-plan.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
controllers: [MealPlanController],
|
||||
providers: [MealPlanService],
|
||||
imports: [PrismaModule],
|
||||
})
|
||||
export class MealPlanModule {}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateMealPlanEntryDto } from './dto/create-meal-plan-entry.dto';
|
||||
|
||||
const recipeSelect = {
|
||||
id: true,
|
||||
name: true,
|
||||
imageUrl: true,
|
||||
ingredients: {
|
||||
select: {
|
||||
quantity: true,
|
||||
unit: true,
|
||||
note: true,
|
||||
product: { select: { id: true, name: true, canonicalName: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MealPlanService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/** Hämta matplan för ett datumintervall (default: nuvarande vecka) */
|
||||
async findByRange(from: string, to: string) {
|
||||
return this.prisma.mealPlanEntry.findMany({
|
||||
where: {
|
||||
date: { gte: new Date(from), lte: new Date(to) },
|
||||
},
|
||||
include: { recipe: { select: recipeSelect } },
|
||||
orderBy: { date: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/** Sätt recept för ett datum (upsert — ett recept per dag) */
|
||||
async upsert(dto: CreateMealPlanEntryDto) {
|
||||
const date = new Date(dto.date);
|
||||
return this.prisma.mealPlanEntry.upsert({
|
||||
where: { date },
|
||||
create: { date, recipeId: dto.recipeId },
|
||||
update: { recipeId: dto.recipeId },
|
||||
include: { recipe: { select: recipeSelect } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Ta bort matplanspost för ett datum */
|
||||
async removeByDate(date: string) {
|
||||
const entry = await this.prisma.mealPlanEntry.findUnique({
|
||||
where: { date: new Date(date) },
|
||||
});
|
||||
if (!entry) throw new NotFoundException('Ingen matplanspost för detta datum');
|
||||
return this.prisma.mealPlanEntry.delete({ where: { id: entry.id } });
|
||||
}
|
||||
|
||||
/** Samlad ingredienslista för ett datumintervall */
|
||||
async shoppingList(from: string, to: string) {
|
||||
const entries = await this.findByRange(from, to);
|
||||
|
||||
// Summera ingredienser per produkt+enhet
|
||||
const map = new Map<string, { productId: number; name: string; quantity: number; unit: string }>();
|
||||
for (const entry of entries) {
|
||||
for (const ing of entry.recipe.ingredients) {
|
||||
const key = `${ing.product.id}-${ing.unit}`;
|
||||
const existing = map.get(key);
|
||||
const qty = Number(ing.quantity);
|
||||
if (existing) {
|
||||
existing.quantity += qty;
|
||||
} else {
|
||||
map.set(key, {
|
||||
productId: ing.product.id,
|
||||
name: ing.product.canonicalName || ing.product.name,
|
||||
quantity: qty,
|
||||
unit: ing.unit,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user