Recipe-app main
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { IsInt, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||
|
||||
export class CreateRecipeIngredientDto {
|
||||
@IsInt()
|
||||
productId!: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
quantity!: number;
|
||||
|
||||
@IsString()
|
||||
unit!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
ArrayMinSize,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { CreateRecipeIngredientDto } from './create-recipe-ingredient.dto';
|
||||
|
||||
export class CreateRecipeDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
instructions?: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateRecipeIngredientDto)
|
||||
ingredients!: CreateRecipeIngredientDto[];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
import { RecipesService } from './recipes.service';
|
||||
|
||||
@Controller('recipes')
|
||||
export class RecipesController {
|
||||
constructor(private readonly recipesService: RecipesService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.recipesService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id/inventory-preview')
|
||||
getInventoryPreview(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.recipesService.getInventoryPreview(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.recipesService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateRecipeDto) {
|
||||
return this.recipesService.create(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { RecipesController } from './recipes.controller';
|
||||
import { RecipesService } from './recipes.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [RecipesController],
|
||||
providers: [RecipesService],
|
||||
})
|
||||
export class RecipesModule {}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RecipesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.recipe.findMany({
|
||||
include: {
|
||||
ingredients: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const recipe = await this.prisma.recipe.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
ingredients: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipe) {
|
||||
throw new NotFoundException(`Recipe with id ${id} not found`);
|
||||
}
|
||||
|
||||
return recipe;
|
||||
}
|
||||
|
||||
async create(data: CreateRecipeDto) {
|
||||
for (const ingredient of data.ingredients) {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id: ingredient.productId },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException(
|
||||
`Product with id ${ingredient.productId} not found`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.recipe.create({
|
||||
data: {
|
||||
name: data.name.trim(),
|
||||
description: data.description?.trim() || null,
|
||||
instructions: data.instructions?.trim() || null,
|
||||
ingredients: {
|
||||
create: data.ingredients.map((ingredient) => ({
|
||||
productId: ingredient.productId,
|
||||
quantity: new Prisma.Decimal(ingredient.quantity),
|
||||
unit: ingredient.unit.trim(),
|
||||
note: ingredient.note?.trim() || null,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
ingredients: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getInventoryPreview(id: number) {
|
||||
const recipe = await this.prisma.recipe.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
ingredients: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipe) {
|
||||
throw new NotFoundException(`Recipe with id ${id} not found`);
|
||||
}
|
||||
|
||||
const ingredientPreviews = await Promise.all(
|
||||
recipe.ingredients.map(async (ingredient: typeof recipe.ingredients[0]) => {
|
||||
const inventoryItems = await this.prisma.inventoryItem.findMany({
|
||||
where: {
|
||||
productId: ingredient.productId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
const sameUnitItems = inventoryItems.filter(
|
||||
(item) => item.unit.trim().toLowerCase() === ingredient.unit.trim().toLowerCase(),
|
||||
);
|
||||
|
||||
const availableQuantity = sameUnitItems.reduce(
|
||||
(sum, item) => sum + Number(item.quantity),
|
||||
0,
|
||||
);
|
||||
|
||||
const requiredQuantity = Number(ingredient.quantity);
|
||||
|
||||
let status: 'enough' | 'missing' | 'unit_mismatch';
|
||||
|
||||
if (sameUnitItems.length > 0) {
|
||||
status = availableQuantity >= requiredQuantity ? 'enough' : 'missing';
|
||||
} else if (inventoryItems.length > 0) {
|
||||
status = 'unit_mismatch';
|
||||
} else {
|
||||
status = 'missing';
|
||||
}
|
||||
|
||||
return {
|
||||
ingredientId: ingredient.id,
|
||||
productId: ingredient.productId,
|
||||
productName: ingredient.product.canonicalName || ingredient.product.name,
|
||||
requiredQuantity,
|
||||
requiredUnit: ingredient.unit,
|
||||
note: ingredient.note,
|
||||
availableQuantity,
|
||||
availableUnit: sameUnitItems.length > 0 ? ingredient.unit : null,
|
||||
matchingInventoryItems: sameUnitItems.map((item) => ({
|
||||
id: item.id,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
location: item.location,
|
||||
})),
|
||||
otherInventoryItems: inventoryItems
|
||||
.filter((item) => item.unit.trim().toLowerCase() !== ingredient.unit.trim().toLowerCase())
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
location: item.location,
|
||||
})),
|
||||
status,
|
||||
missingQuantity:
|
||||
status === 'missing'
|
||||
? Math.max(0, requiredQuantity - availableQuantity)
|
||||
: 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const summary = {
|
||||
totalIngredients: ingredientPreviews.length,
|
||||
enoughCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'enough').length,
|
||||
missingCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'missing').length,
|
||||
unitMismatchCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'unit_mismatch').length,
|
||||
canCookExactly:
|
||||
ingredientPreviews.every((i: typeof ingredientPreviews[0]) => i.status === 'enough'),
|
||||
};
|
||||
|
||||
return {
|
||||
recipe: {
|
||||
id: recipe.id,
|
||||
name: recipe.name,
|
||||
description: recipe.description,
|
||||
},
|
||||
ingredients: ingredientPreviews,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user