feat: Implement admin pantry item management with create and update functionality, including category selection and validation
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-11 11:18:13 +02:00
parent 573c12cdc3
commit f132983b75
11 changed files with 683 additions and 38 deletions
@@ -0,0 +1,9 @@
import { IsInt, IsOptional, Min } from 'class-validator';
import { CreatePantryItemDto } from './create-pantry-item.dto';
export class CreateAdminPantryItemDto extends CreatePantryItemDto {
@IsOptional()
@IsInt()
@Min(1)
userId?: number;
}
@@ -0,0 +1,13 @@
import { IsInt, IsOptional, IsPositive, IsString, MaxLength } from 'class-validator';
export class UpdatePantryItemDto {
@IsOptional()
@IsInt()
@IsPositive()
productId?: number;
@IsOptional()
@IsString()
@MaxLength(50)
location?: string;
}
+21 -1
View File
@@ -1,9 +1,11 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query } from '@nestjs/common';
import { PantryService } from './pantry.service';
import { CreatePantryItemDto } from './dto/create-pantry-item.dto';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import { CreateInventoryDto } from '../inventory/dto/create-inventory.dto';
import { CreateAdminPantryItemDto } from './dto/create-admin-pantry-item.dto';
import { UpdatePantryItemDto } from './dto/update-pantry-item.dto';
@Controller('pantry')
export class PantryController {
@@ -31,6 +33,24 @@ export class PantryController {
});
}
@Roles('admin')
@Post('admin')
createAdmin(
@CurrentUser() user: { userId: number },
@Body() body: CreateAdminPantryItemDto,
) {
return this.pantryService.createAdmin(user.userId, body, body.userId);
}
@Roles('admin')
@Patch('admin/:id')
updateAdmin(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdatePantryItemDto,
) {
return this.pantryService.updateAdmin(id, body);
}
@Delete(':id')
remove(
@CurrentUser() user: { userId: number },
+117 -14
View File
@@ -3,6 +3,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { CreatePantryItemDto } from './dto/create-pantry-item.dto';
import { CreateInventoryDto } from '../inventory/dto/create-inventory.dto';
import { Prisma } from '@prisma/client';
import { UpdatePantryItemDto } from './dto/update-pantry-item.dto';
type PantryQuery = {
userId?: number;
@@ -12,11 +13,43 @@ type PantryQuery = {
export class PantryService {
constructor(private readonly prisma: PrismaService) {}
private readonly productWithCategoryInclude = {
include: {
categoryRef: {
include: {
parent: {
include: {
parent: true,
},
},
},
},
},
};
private async ensureProductExistsAny(productId: number) {
const product = await this.prisma.product.findUnique({ where: { id: productId } });
if (!product) {
throw new NotFoundException('Product not found');
}
return product;
}
private async ensureUserExists(userId: number) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {
throw new NotFoundException(`User with id ${userId} not found`);
}
}
findAll(userId: number) {
return this.prisma.pantryItem.findMany({
where: { userId },
include: {
product: true,
product: this.productWithCategoryInclude,
},
orderBy: {
product: { name: 'asc' },
@@ -36,17 +69,7 @@ export class PantryService {
},
},
product: {
include: {
categoryRef: {
include: {
parent: {
include: {
parent: true,
},
},
},
},
},
...this.productWithCategoryInclude,
},
},
orderBy: [
@@ -76,7 +99,87 @@ export class PantryService {
productId: data.productId,
location: data.location?.trim() || null,
},
include: { product: true },
include: { product: this.productWithCategoryInclude },
});
}
async createAdmin(adminUserId: number, data: CreatePantryItemDto, targetUserId?: number) {
const effectiveUserId = typeof targetUserId === 'number' ? targetUserId : adminUserId;
await this.ensureUserExists(effectiveUserId);
await this.ensureProductExistsAny(data.productId);
const existing = await this.prisma.pantryItem.findUnique({
where: {
userId_productId: {
userId: effectiveUserId,
productId: data.productId,
},
},
});
if (existing) {
throw new ConflictException('Produkten finns redan i baslagret');
}
return this.prisma.pantryItem.create({
data: {
userId: effectiveUserId,
productId: data.productId,
location: data.location?.trim() || null,
},
include: {
user: {
select: {
id: true,
username: true,
email: true,
},
},
product: this.productWithCategoryInclude,
},
});
}
async updateAdmin(id: number, data: UpdatePantryItemDto) {
const existing = await this.prisma.pantryItem.findUnique({ where: { id } });
if (!existing) {
throw new NotFoundException(`PantryItem med id ${id} hittades inte`);
}
if (typeof data.productId === 'number') {
await this.ensureProductExistsAny(data.productId);
const duplicate = await this.prisma.pantryItem.findUnique({
where: {
userId_productId: {
userId: existing.userId,
productId: data.productId,
},
},
});
if (duplicate && duplicate.id !== id) {
throw new ConflictException('Produkten finns redan i baslagret');
}
}
return this.prisma.pantryItem.update({
where: { id },
data: {
...(typeof data.productId === 'number'
? { product: { connect: { id: data.productId } } }
: {}),
...(typeof data.location === 'string'
? { location: data.location.trim() || null }
: {}),
},
include: {
user: {
select: {
id: true,
username: true,
email: true,
},
},
product: this.productWithCategoryInclude,
},
});
}
@@ -123,7 +226,7 @@ export class PantryService {
suitableFor: data.suitableFor?.trim() || undefined,
comment: data.comment?.trim() || undefined,
},
include: { product: true },
include: { product: this.productWithCategoryInclude },
});
await tx.pantryItem.delete({ where: { id: item.id } });