feat: make pantry items and meal plan entries user-scoped; update related services and controllers
This commit is contained in:
@@ -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;
|
||||||
@@ -23,6 +23,8 @@ model User {
|
|||||||
ownedRecipes Recipe[] @relation("RecipeOwner")
|
ownedRecipes Recipe[] @relation("RecipeOwner")
|
||||||
sharedRecipes RecipeShare[]
|
sharedRecipes RecipeShare[]
|
||||||
ownedProducts Product[]
|
ownedProducts Product[]
|
||||||
|
pantryItems PantryItem[]
|
||||||
|
mealPlanEntries MealPlanEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Product {
|
model Product {
|
||||||
@@ -160,10 +162,15 @@ model RecipeIngredient {
|
|||||||
|
|
||||||
model PantryItem {
|
model PantryItem {
|
||||||
id Int @id @default(autoincrement())
|
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)
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([userId, productId])
|
||||||
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ReceiptAlias {
|
model ReceiptAlias {
|
||||||
@@ -176,14 +183,17 @@ model ReceiptAlias {
|
|||||||
|
|
||||||
model MealPlanEntry {
|
model MealPlanEntry {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
||||||
recipeId Int
|
recipeId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
servings Int?
|
servings Int?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@unique([date])
|
@@unique([userId, date]) # Bara ett recept per dag och användare
|
||||||
|
@@index([userId])
|
||||||
@@index([date])
|
@@index([date])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,52 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
||||||
import { MealPlanService } from './meal-plan.service';
|
import { MealPlanService } from './meal-plan.service';
|
||||||
import { CreateMealPlanEntryDto } from './dto/create-meal-plan-entry.dto';
|
import { CreateMealPlanEntryDto } from './dto/create-meal-plan-entry.dto';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
|
||||||
@Controller('meal-plan')
|
@Controller('meal-plan')
|
||||||
export class MealPlanController {
|
export class MealPlanController {
|
||||||
constructor(private readonly mealPlanService: MealPlanService) {}
|
constructor(private readonly mealPlanService: MealPlanService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findByRange(@Query('from') from: string, @Query('to') to: string) {
|
findByRange(
|
||||||
return this.mealPlanService.findByRange(from, to);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Query('from') from: string,
|
||||||
|
@Query('to') to: string,
|
||||||
|
) {
|
||||||
|
return this.mealPlanService.findByRange(user.userId, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('shopping-list')
|
@Get('shopping-list')
|
||||||
shoppingList(@Query('from') from: string, @Query('to') to: string) {
|
shoppingList(
|
||||||
return this.mealPlanService.shoppingList(from, to);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Query('from') from: string,
|
||||||
|
@Query('to') to: string,
|
||||||
|
) {
|
||||||
|
return this.mealPlanService.shoppingList(user.userId, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('inventory-compare')
|
@Get('inventory-compare')
|
||||||
inventoryCompare(@Query('from') from: string, @Query('to') to: string) {
|
inventoryCompare(
|
||||||
return this.mealPlanService.inventoryCompare(from, to);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Query('from') from: string,
|
||||||
|
@Query('to') to: string,
|
||||||
|
) {
|
||||||
|
return this.mealPlanService.inventoryCompare(user.userId, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
upsert(@Body() dto: CreateMealPlanEntryDto) {
|
upsert(
|
||||||
return this.mealPlanService.upsert(dto);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Body() dto: CreateMealPlanEntryDto,
|
||||||
|
) {
|
||||||
|
return this.mealPlanService.upsert(user.userId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':date')
|
@Delete(':date')
|
||||||
removeByDate(@Param('date') date: string) {
|
removeByDate(
|
||||||
return this.mealPlanService.removeByDate(date);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Param('date') date: string,
|
||||||
|
) {
|
||||||
|
return this.mealPlanService.removeByDate(user.userId, date);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ export class MealPlanService {
|
|||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
/** Hämta matplan för ett datumintervall (default: nuvarande vecka) */
|
/** 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({
|
return this.prisma.mealPlanEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
userId,
|
||||||
date: { gte: new Date(from), lte: new Date(to) },
|
date: { gte: new Date(from), lte: new Date(to) },
|
||||||
},
|
},
|
||||||
include: { recipe: { select: recipeSelect } },
|
include: { recipe: { select: recipeSelect } },
|
||||||
@@ -33,28 +34,43 @@ export class MealPlanService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Sätt recept för ett datum (upsert — ett recept per dag) */
|
/** 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);
|
const date = new Date(dto.date);
|
||||||
return this.prisma.mealPlanEntry.upsert({
|
return this.prisma.mealPlanEntry.upsert({
|
||||||
where: { date },
|
where: {
|
||||||
create: { date, recipeId: dto.recipeId, servings: dto.servings ?? null },
|
userId_date: {
|
||||||
|
userId,
|
||||||
|
date,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
date,
|
||||||
|
recipeId: dto.recipeId,
|
||||||
|
servings: dto.servings ?? null,
|
||||||
|
},
|
||||||
update: { recipeId: dto.recipeId, servings: dto.servings ?? null },
|
update: { recipeId: dto.recipeId, servings: dto.servings ?? null },
|
||||||
include: { recipe: { select: recipeSelect } },
|
include: { recipe: { select: recipeSelect } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ta bort matplanspost för ett datum */
|
/** Ta bort matplanspost för ett datum */
|
||||||
async removeByDate(date: string) {
|
async removeByDate(userId: number, date: string) {
|
||||||
const entry = await this.prisma.mealPlanEntry.findUnique({
|
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');
|
if (!entry) throw new NotFoundException('Ingen matplanspost för detta datum');
|
||||||
return this.prisma.mealPlanEntry.delete({ where: { id: entry.id } });
|
return this.prisma.mealPlanEntry.delete({ where: { id: entry.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Samlad ingredienslista för ett datumintervall */
|
/** Samlad ingredienslista för ett datumintervall */
|
||||||
async shoppingList(from: string, to: string) {
|
async shoppingList(userId: number, from: string, to: string) {
|
||||||
const entries = await this.findByRange(from, to);
|
const entries = await this.findByRange(userId, from, to);
|
||||||
|
|
||||||
// Summera ingredienser per produkt+enhet (skalat per portionsantal)
|
// Summera ingredienser per produkt+enhet (skalat per portionsantal)
|
||||||
const map = new Map<string, { productId: number; name: string; quantity: number; unit: string }>();
|
const map = new Map<string, { productId: number; name: string; quantity: number; unit: string }>();
|
||||||
@@ -83,11 +99,14 @@ export class MealPlanService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Jämför veckans ingrediensbehov mot inventariet */
|
/** Jämför veckans ingrediensbehov mot inventariet */
|
||||||
async inventoryCompare(from: string, to: string) {
|
async inventoryCompare(userId: number, from: string, to: string) {
|
||||||
const entries = await this.findByRange(from, to);
|
const entries = await this.findByRange(userId, from, to);
|
||||||
|
|
||||||
// Hämta pantry-produkter — dessa anses alltid tillgängliga
|
// 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));
|
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
|
||||||
|
|
||||||
// Aggregera ingredienser per produkt+enhet (skalat per portionsantal)
|
// Aggregera ingredienser per produkt+enhet (skalat per portionsantal)
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
||||||
import { PantryService } from './pantry.service';
|
import { PantryService } from './pantry.service';
|
||||||
import { CreatePantryItemDto } from './dto/create-pantry-item.dto';
|
import { CreatePantryItemDto } from './dto/create-pantry-item.dto';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
|
||||||
@Controller('pantry')
|
@Controller('pantry')
|
||||||
export class PantryController {
|
export class PantryController {
|
||||||
constructor(private readonly pantryService: PantryService) {}
|
constructor(private readonly pantryService: PantryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll(@CurrentUser() user: { userId: number }) {
|
||||||
return this.pantryService.findAll();
|
return this.pantryService.findAll(user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() body: CreatePantryItemDto) {
|
create(
|
||||||
return this.pantryService.create(body);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Body() body: CreatePantryItemDto,
|
||||||
|
) {
|
||||||
|
return this.pantryService.create(user.userId, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
remove(
|
||||||
return this.pantryService.remove(id);
|
@CurrentUser() user: { userId: number },
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
) {
|
||||||
|
return this.pantryService.remove(user.userId, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { CreatePantryItemDto } from './dto/create-pantry-item.dto';
|
|||||||
export class PantryService {
|
export class PantryService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
findAll() {
|
findAll(userId: number) {
|
||||||
return this.prisma.pantryItem.findMany({
|
return this.prisma.pantryItem.findMany({
|
||||||
|
where: { userId },
|
||||||
include: {
|
include: {
|
||||||
product: true,
|
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({
|
const existing = await this.prisma.pantryItem.findUnique({
|
||||||
where: { productId: data.productId },
|
where: {
|
||||||
|
userId_productId: {
|
||||||
|
userId,
|
||||||
|
productId: data.productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -27,13 +33,15 @@ export class PantryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.pantryItem.create({
|
return this.prisma.pantryItem.create({
|
||||||
data: { productId: data.productId },
|
data: { userId, productId: data.productId },
|
||||||
include: { product: true },
|
include: { product: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: number) {
|
async remove(userId: number, id: number) {
|
||||||
const item = await this.prisma.pantryItem.findUnique({ where: { id } });
|
const item = await this.prisma.pantryItem.findFirst({
|
||||||
|
where: { id, userId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new NotFoundException(`PantryItem med id ${id} hittades inte`);
|
throw new NotFoundException(`PantryItem med id ${id} hittades inte`);
|
||||||
|
|||||||
Reference in New Issue
Block a user