diff --git a/backend/prisma/migrations/20260422130000_user_scope_pantry_meal_plan/migration.sql b/backend/prisma/migrations/20260422130000_user_scope_pantry_meal_plan/migration.sql new file mode 100644 index 00000000..349404a4 --- /dev/null +++ b/backend/prisma/migrations/20260422130000_user_scope_pantry_meal_plan/migration.sql @@ -0,0 +1,25 @@ +-- PantryItem: make user-scoped +ALTER TABLE `PantryItem` ADD COLUMN `userId` INTEGER NULL; + +UPDATE `PantryItem` +SET `userId` = (SELECT `id` FROM `User` ORDER BY `id` ASC LIMIT 1) +WHERE `userId` IS NULL; + +ALTER TABLE `PantryItem` DROP INDEX `PantryItem_productId_key`; +ALTER TABLE `PantryItem` MODIFY `userId` INTEGER NOT NULL; +ALTER TABLE `PantryItem` ADD INDEX `PantryItem_userId_idx`(`userId`); +ALTER TABLE `PantryItem` ADD UNIQUE INDEX `PantryItem_userId_productId_key`(`userId`, `productId`); +ALTER TABLE `PantryItem` ADD CONSTRAINT `PantryItem_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- MealPlanEntry: make user-scoped +ALTER TABLE `MealPlanEntry` ADD COLUMN `userId` INTEGER NULL; + +UPDATE `MealPlanEntry` +SET `userId` = (SELECT `id` FROM `User` ORDER BY `id` ASC LIMIT 1) +WHERE `userId` IS NULL; + +ALTER TABLE `MealPlanEntry` DROP INDEX `MealPlanEntry_date_key`; +ALTER TABLE `MealPlanEntry` MODIFY `userId` INTEGER NOT NULL; +ALTER TABLE `MealPlanEntry` ADD INDEX `MealPlanEntry_userId_idx`(`userId`); +ALTER TABLE `MealPlanEntry` ADD UNIQUE INDEX `MealPlanEntry_userId_date_key`(`userId`, `date`); +ALTER TABLE `MealPlanEntry` ADD CONSTRAINT `MealPlanEntry_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b13406bd..311eb38f 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -23,6 +23,8 @@ model User { ownedRecipes Recipe[] @relation("RecipeOwner") sharedRecipes RecipeShare[] ownedProducts Product[] + pantryItems PantryItem[] + mealPlanEntries MealPlanEntry[] } model Product { @@ -160,10 +162,15 @@ model RecipeIngredient { model PantryItem { id Int @id @default(autoincrement()) - productId Int @unique + userId Int + productId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) product Product @relation(fields: [productId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@unique([userId, productId]) + @@index([userId]) } model ReceiptAlias { @@ -176,14 +183,17 @@ model ReceiptAlias { model MealPlanEntry { id Int @id @default(autoincrement()) + userId Int date DateTime @db.Date recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade) recipeId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) servings Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@unique([date]) + @@unique([userId, date]) # Bara ett recept per dag och användare + @@index([userId]) @@index([date]) } diff --git a/backend/src/meal-plan/meal-plan.controller.ts b/backend/src/meal-plan/meal-plan.controller.ts index 70b36a88..3e5d13e9 100644 --- a/backend/src/meal-plan/meal-plan.controller.ts +++ b/backend/src/meal-plan/meal-plan.controller.ts @@ -1,33 +1,52 @@ 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'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; @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); + findByRange( + @CurrentUser() user: { userId: number }, + @Query('from') from: string, + @Query('to') to: string, + ) { + return this.mealPlanService.findByRange(user.userId, from, to); } @Get('shopping-list') - shoppingList(@Query('from') from: string, @Query('to') to: string) { - return this.mealPlanService.shoppingList(from, to); + shoppingList( + @CurrentUser() user: { userId: number }, + @Query('from') from: string, + @Query('to') to: string, + ) { + return this.mealPlanService.shoppingList(user.userId, from, to); } @Get('inventory-compare') - inventoryCompare(@Query('from') from: string, @Query('to') to: string) { - return this.mealPlanService.inventoryCompare(from, to); + inventoryCompare( + @CurrentUser() user: { userId: number }, + @Query('from') from: string, + @Query('to') to: string, + ) { + return this.mealPlanService.inventoryCompare(user.userId, from, to); } @Post() - upsert(@Body() dto: CreateMealPlanEntryDto) { - return this.mealPlanService.upsert(dto); + upsert( + @CurrentUser() user: { userId: number }, + @Body() dto: CreateMealPlanEntryDto, + ) { + return this.mealPlanService.upsert(user.userId, dto); } @Delete(':date') - removeByDate(@Param('date') date: string) { - return this.mealPlanService.removeByDate(date); + removeByDate( + @CurrentUser() user: { userId: number }, + @Param('date') date: string, + ) { + return this.mealPlanService.removeByDate(user.userId, date); } } diff --git a/backend/src/meal-plan/meal-plan.service.ts b/backend/src/meal-plan/meal-plan.service.ts index d4250243..44d78436 100644 --- a/backend/src/meal-plan/meal-plan.service.ts +++ b/backend/src/meal-plan/meal-plan.service.ts @@ -22,9 +22,10 @@ export class MealPlanService { constructor(private readonly prisma: PrismaService) {} /** Hämta matplan för ett datumintervall (default: nuvarande vecka) */ - async findByRange(from: string, to: string) { + async findByRange(userId: number, from: string, to: string) { return this.prisma.mealPlanEntry.findMany({ where: { + userId, date: { gte: new Date(from), lte: new Date(to) }, }, include: { recipe: { select: recipeSelect } }, @@ -33,28 +34,43 @@ export class MealPlanService { } /** Sätt recept för ett datum (upsert — ett recept per dag) */ - async upsert(dto: CreateMealPlanEntryDto) { + async upsert(userId: number, dto: CreateMealPlanEntryDto) { const date = new Date(dto.date); return this.prisma.mealPlanEntry.upsert({ - where: { date }, - create: { date, recipeId: dto.recipeId, servings: dto.servings ?? null }, + where: { + userId_date: { + userId, + date, + }, + }, + create: { + userId, + date, + recipeId: dto.recipeId, + servings: dto.servings ?? null, + }, update: { recipeId: dto.recipeId, servings: dto.servings ?? null }, include: { recipe: { select: recipeSelect } }, }); } /** Ta bort matplanspost för ett datum */ - async removeByDate(date: string) { + async removeByDate(userId: number, date: string) { const entry = await this.prisma.mealPlanEntry.findUnique({ - where: { date: new Date(date) }, + where: { + userId_date: { + userId, + 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); + async shoppingList(userId: number, from: string, to: string) { + const entries = await this.findByRange(userId, from, to); // Summera ingredienser per produkt+enhet (skalat per portionsantal) const map = new Map(); @@ -83,11 +99,14 @@ export class MealPlanService { } /** Jämför veckans ingrediensbehov mot inventariet */ - async inventoryCompare(from: string, to: string) { - const entries = await this.findByRange(from, to); + async inventoryCompare(userId: number, from: string, to: string) { + const entries = await this.findByRange(userId, from, to); // Hämta pantry-produkter — dessa anses alltid tillgängliga - const pantryItems = await this.prisma.pantryItem.findMany({ select: { productId: true } }); + const pantryItems = await this.prisma.pantryItem.findMany({ + where: { userId }, + select: { productId: true }, + }); const pantryProductIds = new Set(pantryItems.map((p) => p.productId)); // Aggregera ingredienser per produkt+enhet (skalat per portionsantal) diff --git a/backend/src/pantry/pantry.controller.ts b/backend/src/pantry/pantry.controller.ts index 944ae2e7..73506ebf 100644 --- a/backend/src/pantry/pantry.controller.ts +++ b/backend/src/pantry/pantry.controller.ts @@ -1,23 +1,30 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common'; import { PantryService } from './pantry.service'; import { CreatePantryItemDto } from './dto/create-pantry-item.dto'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; @Controller('pantry') export class PantryController { constructor(private readonly pantryService: PantryService) {} @Get() - findAll() { - return this.pantryService.findAll(); + findAll(@CurrentUser() user: { userId: number }) { + return this.pantryService.findAll(user.userId); } @Post() - create(@Body() body: CreatePantryItemDto) { - return this.pantryService.create(body); + create( + @CurrentUser() user: { userId: number }, + @Body() body: CreatePantryItemDto, + ) { + return this.pantryService.create(user.userId, body); } @Delete(':id') - remove(@Param('id', ParseIntPipe) id: number) { - return this.pantryService.remove(id); + remove( + @CurrentUser() user: { userId: number }, + @Param('id', ParseIntPipe) id: number, + ) { + return this.pantryService.remove(user.userId, id); } } diff --git a/backend/src/pantry/pantry.service.ts b/backend/src/pantry/pantry.service.ts index 9942f201..5f942e36 100644 --- a/backend/src/pantry/pantry.service.ts +++ b/backend/src/pantry/pantry.service.ts @@ -6,8 +6,9 @@ import { CreatePantryItemDto } from './dto/create-pantry-item.dto'; export class PantryService { constructor(private readonly prisma: PrismaService) {} - findAll() { + findAll(userId: number) { return this.prisma.pantryItem.findMany({ + where: { userId }, include: { product: true, }, @@ -17,9 +18,14 @@ export class PantryService { }); } - async create(data: CreatePantryItemDto) { + async create(userId: number, data: CreatePantryItemDto) { const existing = await this.prisma.pantryItem.findUnique({ - where: { productId: data.productId }, + where: { + userId_productId: { + userId, + productId: data.productId, + }, + }, }); if (existing) { @@ -27,13 +33,15 @@ export class PantryService { } return this.prisma.pantryItem.create({ - data: { productId: data.productId }, + data: { userId, productId: data.productId }, include: { product: true }, }); } - async remove(id: number) { - const item = await this.prisma.pantryItem.findUnique({ where: { id } }); + async remove(userId: number, id: number) { + const item = await this.prisma.pantryItem.findFirst({ + where: { id, userId }, + }); if (!item) { throw new NotFoundException(`PantryItem med id ${id} hittades inte`);