feat: implement meal planning feature with CRUD operations and UI integration

This commit is contained in:
Nils-Johan Gynther
2026-04-16 19:37:09 +02:00
parent 8b12df4aa4
commit 1b82b02021
13 changed files with 468 additions and 1 deletions
@@ -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'));
}
}