189 lines
5.2 KiB
TypeScript
189 lines
5.2 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
} |