Recipe-app main

This commit is contained in:
2026-04-09 09:14:39 +02:00
commit 962f4e4be5
10015 changed files with 2445177 additions and 0 deletions
@@ -0,0 +1,11 @@
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
export class ConsumeInventoryDto {
@IsNumber()
@Min(0.01)
amountUsed!: number;
@IsOptional()
@IsString()
comment?: string;
}
@@ -0,0 +1,70 @@
import {
IsBoolean,
IsInt,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class CreateInventoryDto {
@IsInt()
productId!: number;
@IsNumber()
@Min(0)
quantity!: number;
@IsString()
unit!: string;
@IsOptional()
@IsString()
location?: string;
@IsOptional()
@IsInt()
priority?: number;
@IsOptional()
purchaseDate?: string;
@IsOptional()
bestBeforeDate?: string;
@IsOptional()
@IsString()
brand?: string;
@IsOptional()
@IsBoolean()
opened?: boolean;
@IsOptional()
@IsString()
shelfNote?: string;
@IsOptional()
@IsString()
suitableFor?: string;
@IsOptional()
@IsBoolean()
isOnSale?: boolean;
@IsOptional()
@IsInt()
priceLevel?: number;
@IsOptional()
@IsString()
proteinType?: string;
@IsOptional()
@IsBoolean()
isLeftover?: boolean;
@IsOptional()
@IsString()
comment?: string;
}
@@ -0,0 +1,73 @@
import {
IsBoolean,
IsInt,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class UpdateInventoryDto {
@IsOptional()
@IsInt()
productId?: number;
@IsOptional()
@IsNumber()
@Min(0)
quantity?: number;
@IsOptional()
@IsString()
unit?: string;
@IsOptional()
@IsString()
location?: string;
@IsOptional()
@IsInt()
priority?: number;
@IsOptional()
purchaseDate?: string;
@IsOptional()
bestBeforeDate?: string;
@IsOptional()
@IsString()
brand?: string;
@IsOptional()
@IsBoolean()
opened?: boolean;
@IsOptional()
@IsString()
shelfNote?: string;
@IsOptional()
@IsString()
suitableFor?: string;
@IsOptional()
@IsBoolean()
isOnSale?: boolean;
@IsOptional()
@IsInt()
priceLevel?: number;
@IsOptional()
@IsString()
proteinType?: string;
@IsOptional()
@IsBoolean()
isLeftover?: boolean;
@IsOptional()
@IsString()
comment?: string;
}
@@ -0,0 +1,58 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { CreateInventoryDto } from './dto/create-inventory.dto';
import { UpdateInventoryDto } from './dto/update-inventory.dto';
import { InventoryService } from './inventory.service';
import { ConsumeInventoryDto } from './dto/consume-inventory.dto';
@Controller('inventory')
export class InventoryController {
constructor(private readonly inventoryService: InventoryService) {}
@Post(':id/consume')
consume(
@Param('id', ParseIntPipe) id: number,
@Body() body: ConsumeInventoryDto,
) {
return this.inventoryService.consume(id, body);
}
@Get(':id/consumption-history')
findConsumptionHistory(@Param('id', ParseIntPipe) id: number) {
return this.inventoryService.findConsumptionHistory(id);
}
@Get()
findAll(
@Query('location') location?: string,
@Query('sort') sort?: string,
) {
return this.inventoryService.findAll({ location, sort });
}
@Get('expiring')
findExpiring() {
return this.inventoryService.findExpiring();
}
@Post()
create(@Body() body: CreateInventoryDto) {
return this.inventoryService.create(body);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateInventoryDto,
) {
return this.inventoryService.update(id, body);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { InventoryController } from './inventory.controller';
import { InventoryService } from './inventory.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
controllers: [InventoryController],
providers: [InventoryService],
imports: [PrismaModule],
})
export class InventoryModule {}
+248
View File
@@ -0,0 +1,248 @@
import { ConsumeInventoryDto } from './dto/consume-inventory.dto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateInventoryDto } from './dto/create-inventory.dto';
import { UpdateInventoryDto } from './dto/update-inventory.dto';
type InventoryQuery = {
location?: string;
sort?: string;
};
@Injectable()
export class InventoryService {
constructor(private prisma: PrismaService) {}
async findAll(query?: InventoryQuery) {
const where: Prisma.InventoryItemWhereInput = {};
const orderBy: Prisma.InventoryItemOrderByWithRelationInput[] = [];
if (query?.location) {
where.location = query.location;
}
if (query?.sort === 'bestBeforeAsc') {
orderBy.push({ bestBeforeDate: 'asc' });
} else if (query?.sort === 'bestBeforeDesc') {
orderBy.push({ bestBeforeDate: 'desc' });
} else if (query?.sort === 'purchaseDateAsc') {
orderBy.push({ purchaseDate: 'asc' });
} else if (query?.sort === 'purchaseDateDesc') {
orderBy.push({ purchaseDate: 'desc' });
} else {
orderBy.push({ createdAt: 'desc' });
}
return this.prisma.inventoryItem.findMany({
where,
include: {
product: true,
},
orderBy,
});
}
async consume(id: number, data: ConsumeInventoryDto) {
const existing = await this.prisma.inventoryItem.findUnique({
where: { id },
include: {
product: true,
},
});
if (!existing) {
throw new NotFoundException(`Inventory item with id ${id} not found`);
}
const currentQuantity = Number(existing.quantity);
const newQuantity = Math.max(0, currentQuantity - data.amountUsed);
return this.prisma.$transaction(async (tx) => {
const updatedItem = await tx.inventoryItem.update({
where: { id },
data: {
quantity: new Prisma.Decimal(newQuantity),
},
include: {
product: true,
},
});
await tx.inventoryConsumption.create({
data: {
inventoryItemId: id,
amountUsed: new Prisma.Decimal(data.amountUsed),
comment: data.comment?.trim() || null,
},
});
return updatedItem;
});
}
async findConsumptionHistory(id: number) {
const existing = await this.prisma.inventoryItem.findUnique({
where: { id },
});
if (!existing) {
throw new NotFoundException(`Inventory item with id ${id} not found`);
}
return this.prisma.inventoryConsumption.findMany({
where: {
inventoryItemId: id,
},
orderBy: {
createdAt: 'desc',
},
});
}
async findExpiring() {
const now = new Date();
return this.prisma.inventoryItem.findMany({
where: {
bestBeforeDate: {
not: null,
gte: now,
},
},
include: {
product: true,
},
orderBy: [{ bestBeforeDate: 'asc' }, { createdAt: 'desc' }],
});
}
async create(data: CreateInventoryDto) {
const product = await this.prisma.product.findUnique({
where: { id: data.productId },
});
if (!product) {
throw new NotFoundException('Product not found');
}
return this.prisma.inventoryItem.create({
data: {
...data,
quantity: new Prisma.Decimal(data.quantity),
location: data.location?.trim() || undefined,
brand: data.brand?.trim() || undefined,
suitableFor: data.suitableFor?.trim() || undefined,
comment: data.comment?.trim() || undefined,
purchaseDate: data.purchaseDate
? new Date(data.purchaseDate)
: undefined,
bestBeforeDate: data.bestBeforeDate
? new Date(data.bestBeforeDate)
: undefined,
},
include: {
product: true,
},
});
}
async update(id: number, data: UpdateInventoryDto) {
const existing = await this.prisma.inventoryItem.findUnique({
where: { id },
});
if (!existing) {
throw new NotFoundException(`Inventory item with id ${id} not found`);
}
if (typeof data.productId === 'number') {
const product = await this.prisma.product.findUnique({
where: { id: data.productId },
});
if (!product) {
throw new NotFoundException('Product not found');
}
}
const updateData: Prisma.InventoryItemUpdateInput = {};
if (typeof data.productId === 'number') {
updateData.product = {
connect: { id: data.productId },
};
}
if (typeof data.quantity === 'number') {
updateData.quantity = new Prisma.Decimal(data.quantity);
}
if (typeof data.unit === 'string') {
updateData.unit = data.unit.trim();
}
if (typeof data.location === 'string') {
updateData.location = data.location.trim();
}
if (typeof data.brand === 'string') {
updateData.brand = data.brand.trim();
}
if (typeof data.priority === 'number') {
updateData.priority = data.priority;
}
if (typeof data.purchaseDate === 'string') {
updateData.purchaseDate = data.purchaseDate
? new Date(data.purchaseDate)
: null;
}
if (typeof data.bestBeforeDate === 'string') {
updateData.bestBeforeDate = data.bestBeforeDate
? new Date(data.bestBeforeDate)
: null;
}
if (typeof data.opened === 'boolean') {
updateData.opened = data.opened;
}
if (typeof data.shelfNote === 'string') {
updateData.shelfNote = data.shelfNote.trim();
}
if (typeof data.suitableFor === 'string') {
updateData.suitableFor = data.suitableFor.trim();
}
if (typeof data.isOnSale === 'boolean') {
updateData.isOnSale = data.isOnSale;
}
if (typeof data.priceLevel === 'number') {
updateData.priceLevel = data.priceLevel;
}
if (typeof data.proteinType === 'string') {
updateData.proteinType = data.proteinType.trim();
}
if (typeof data.isLeftover === 'boolean') {
updateData.isLeftover = data.isLeftover;
}
if (typeof data.comment === 'string') {
updateData.comment = data.comment.trim();
}
return this.prisma.inventoryItem.update({
where: { id },
data: updateData,
include: {
product: true,
},
});
}
}