feat: implement meal planning feature with CRUD operations and UI integration
This commit is contained in:
@@ -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