feat: implement recipe analysis service and data models
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
- Added RecipeAnalysisService to handle recipe ingredient analysis, including methods for checking ingredient availability and calculating quantities. - Introduced new TypeScript definitions for recipe analysis results, including ingredient status and summary. - Created corresponding Dart models for recipe analysis, including RecipeIngredientAnalysis, RecipeAnalysisSummary, and RecipeShoppingCandidate. - Updated Flutter UI to reflect changes in ingredient availability status. - Fixed color opacity issue in recipe image card.
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
type AnalysisStatus = 'exact_match' | 'covered_by_pantry' | 'substitutable' | 'missing';
|
||||||
|
export declare class RecipeAnalysisService {
|
||||||
|
private readonly prisma;
|
||||||
|
constructor(prisma: PrismaService);
|
||||||
|
private getAccessibleRecipe;
|
||||||
|
private calculateAvailableQuantity;
|
||||||
|
analyzeRecipeIngredients(id: number, userId: number): Promise<{
|
||||||
|
recipeId: number;
|
||||||
|
ingredients: ({
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
note: any;
|
||||||
|
status: AnalysisStatus;
|
||||||
|
matchedProductId: any;
|
||||||
|
matchedProductName: any;
|
||||||
|
source: string;
|
||||||
|
availableQuantity: number;
|
||||||
|
missingQuantity: number;
|
||||||
|
} | {
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
note: any;
|
||||||
|
status: AnalysisStatus;
|
||||||
|
matchedProductId: any;
|
||||||
|
matchedProductName: any;
|
||||||
|
source: null;
|
||||||
|
availableQuantity: number;
|
||||||
|
missingQuantity: number;
|
||||||
|
})[];
|
||||||
|
summary: {
|
||||||
|
exactCount: number;
|
||||||
|
pantryCount: number;
|
||||||
|
substituteCount: number;
|
||||||
|
missingCount: number;
|
||||||
|
};
|
||||||
|
shoppingListCandidates: {
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
missingQuantity: number;
|
||||||
|
}[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.RecipeAnalysisService = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const prisma_service_1 = require("../prisma/prisma.service");
|
||||||
|
const units_1 = require("../common/utils/units");
|
||||||
|
let RecipeAnalysisService = class RecipeAnalysisService {
|
||||||
|
constructor(prisma) {
|
||||||
|
this.prisma = prisma;
|
||||||
|
}
|
||||||
|
async getAccessibleRecipe(id, userId) {
|
||||||
|
const recipe = await this.prisma.recipe.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
OR: [
|
||||||
|
{ isPublic: true },
|
||||||
|
{ ownerId: userId },
|
||||||
|
{ shares: { some: { userId } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
ingredients: {
|
||||||
|
include: {
|
||||||
|
product: true,
|
||||||
|
},
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!recipe) {
|
||||||
|
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
|
||||||
|
}
|
||||||
|
return recipe;
|
||||||
|
}
|
||||||
|
calculateAvailableQuantity(inventoryItems, requiredUnit) {
|
||||||
|
if (!requiredUnit) {
|
||||||
|
return inventoryItems.reduce((sum, item) => sum + Number(item.quantity ?? 0), 0);
|
||||||
|
}
|
||||||
|
const normalizedRequiredUnit = requiredUnit.trim().toLowerCase();
|
||||||
|
const sameUnit = inventoryItems
|
||||||
|
.filter((item) => item.unit.trim().toLowerCase() === normalizedRequiredUnit)
|
||||||
|
.reduce((sum, item) => sum + Number(item.quantity ?? 0), 0);
|
||||||
|
const converted = inventoryItems
|
||||||
|
.filter((item) => item.unit.trim().toLowerCase() !== normalizedRequiredUnit)
|
||||||
|
.reduce((sum, item) => {
|
||||||
|
if (!(0, units_1.canConvert)(item.unit, requiredUnit))
|
||||||
|
return sum;
|
||||||
|
try {
|
||||||
|
return sum + (0, units_1.convertUnit)(Number(item.quantity ?? 0), item.unit, requiredUnit);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return sameUnit + converted;
|
||||||
|
}
|
||||||
|
async analyzeRecipeIngredients(id, userId) {
|
||||||
|
const recipe = await this.getAccessibleRecipe(id, userId);
|
||||||
|
const pantryItems = await this.prisma.pantryItem.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { productId: true },
|
||||||
|
});
|
||||||
|
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
|
||||||
|
const ingredients = await Promise.all(recipe.ingredients.map(async (ingredient) => {
|
||||||
|
const requiredQuantity = Number(ingredient.quantity ?? 0);
|
||||||
|
const requiredUnit = (ingredient.unit ?? '').trim();
|
||||||
|
const rawName = (ingredient.rawName ?? '').trim() || 'Okänd ingrediens';
|
||||||
|
if (!ingredient.productId || !ingredient.product) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'missing',
|
||||||
|
matchedProductId: null,
|
||||||
|
matchedProductName: null,
|
||||||
|
source: null,
|
||||||
|
availableQuantity: 0,
|
||||||
|
missingQuantity: requiredQuantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (pantryProductIds.has(ingredient.productId)) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'covered_by_pantry',
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: 'pantry',
|
||||||
|
availableQuantity: requiredQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const inventoryItems = await this.prisma.inventoryItem.findMany({
|
||||||
|
where: { productId: ingredient.productId },
|
||||||
|
select: { quantity: true, unit: true },
|
||||||
|
});
|
||||||
|
const availableQuantity = this.calculateAvailableQuantity(inventoryItems, requiredUnit);
|
||||||
|
if (availableQuantity >= requiredQuantity) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'exact_match',
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: 'inventory',
|
||||||
|
availableQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const alternativeProductIds = Array.isArray(ingredient.alternativeProductIds)
|
||||||
|
? ingredient.alternativeProductIds.filter((id) => typeof id === 'number')
|
||||||
|
: [];
|
||||||
|
for (const altProductId of alternativeProductIds) {
|
||||||
|
if (pantryProductIds.has(altProductId)) {
|
||||||
|
const altProduct = await this.prisma.product.findUnique({
|
||||||
|
where: { id: altProductId },
|
||||||
|
select: { id: true, name: true, canonicalName: true },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'substitutable',
|
||||||
|
matchedProductId: altProduct?.id ?? altProductId,
|
||||||
|
matchedProductName: altProduct?.canonicalName || altProduct?.name || null,
|
||||||
|
source: 'pantry_substitute',
|
||||||
|
availableQuantity: requiredQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const altInventoryItems = await this.prisma.inventoryItem.findMany({
|
||||||
|
where: { productId: altProductId },
|
||||||
|
select: { quantity: true, unit: true },
|
||||||
|
});
|
||||||
|
const altAvailable = this.calculateAvailableQuantity(altInventoryItems, requiredUnit);
|
||||||
|
if (altAvailable > 0) {
|
||||||
|
const altProduct = await this.prisma.product.findUnique({
|
||||||
|
where: { id: altProductId },
|
||||||
|
select: { id: true, name: true, canonicalName: true },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'substitutable',
|
||||||
|
matchedProductId: altProduct?.id ?? altProductId,
|
||||||
|
matchedProductName: altProduct?.canonicalName || altProduct?.name || null,
|
||||||
|
source: 'inventory_substitute',
|
||||||
|
availableQuantity: altAvailable,
|
||||||
|
missingQuantity: Math.max(0, requiredQuantity - altAvailable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'missing',
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: null,
|
||||||
|
availableQuantity,
|
||||||
|
missingQuantity: Math.max(0, requiredQuantity - availableQuantity),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
const summary = {
|
||||||
|
exactCount: ingredients.filter((i) => i.status === 'exact_match').length,
|
||||||
|
pantryCount: ingredients.filter((i) => i.status === 'covered_by_pantry').length,
|
||||||
|
substituteCount: ingredients.filter((i) => i.status === 'substitutable').length,
|
||||||
|
missingCount: ingredients.filter((i) => i.status === 'missing').length,
|
||||||
|
};
|
||||||
|
const shoppingListCandidates = ingredients
|
||||||
|
.filter((i) => i.status === 'missing')
|
||||||
|
.map((i) => ({
|
||||||
|
ingredientId: i.ingredientId,
|
||||||
|
rawName: i.rawName,
|
||||||
|
quantity: i.quantity,
|
||||||
|
unit: i.unit,
|
||||||
|
missingQuantity: i.missingQuantity,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
recipeId: recipe.id,
|
||||||
|
ingredients,
|
||||||
|
summary,
|
||||||
|
shoppingListCandidates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.RecipeAnalysisService = RecipeAnalysisService;
|
||||||
|
exports.RecipeAnalysisService = RecipeAnalysisService = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
|
], RecipeAnalysisService);
|
||||||
|
//# sourceMappingURL=recipe-analysis.service.js.map
|
||||||
File diff suppressed because one or more lines are too long
+221
-176
@@ -4,12 +4,14 @@ import { CreateIngredientDto } from './dto/create-ingredient.dto';
|
|||||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||||
import { ShareRecipeDto } from './dto/share-recipe.dto';
|
import { ShareRecipeDto } from './dto/share-recipe.dto';
|
||||||
import { SetRecipeVisibilityDto } from './dto/set-recipe-visibility.dto';
|
import { SetRecipeVisibilityDto } from './dto/set-recipe-visibility.dto';
|
||||||
|
import { RecipeAnalysisService } from './recipe-analysis.service';
|
||||||
declare class UpdateImageDto {
|
declare class UpdateImageDto {
|
||||||
sourceUrl: string;
|
sourceUrl: string;
|
||||||
}
|
}
|
||||||
export declare class RecipesController {
|
export declare class RecipesController {
|
||||||
private readonly recipesService;
|
private readonly recipesService;
|
||||||
constructor(recipesService: RecipesService);
|
private readonly recipeAnalysisService;
|
||||||
|
constructor(recipesService: RecipesService, recipeAnalysisService: RecipeAnalysisService);
|
||||||
parseMarkdown(dto: ParseMarkdownDto): Promise<{
|
parseMarkdown(dto: ParseMarkdownDto): Promise<{
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -43,6 +45,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -50,54 +54,52 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
})[]>;
|
})[]>;
|
||||||
getInventoryPreview(id: number, user: {
|
getInventoryPreview(id: number, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -145,6 +147,49 @@ export declare class RecipesController {
|
|||||||
pantryCount: number;
|
pantryCount: number;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
getRecipeAnalysis(id: number, user: {
|
||||||
|
userId: number;
|
||||||
|
}): Promise<{
|
||||||
|
recipeId: number;
|
||||||
|
ingredients: ({
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
note: any;
|
||||||
|
status: "missing" | "exact_match" | "covered_by_pantry" | "substitutable";
|
||||||
|
matchedProductId: any;
|
||||||
|
matchedProductName: any;
|
||||||
|
source: string;
|
||||||
|
availableQuantity: number;
|
||||||
|
missingQuantity: number;
|
||||||
|
} | {
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
note: any;
|
||||||
|
status: "missing" | "exact_match" | "covered_by_pantry" | "substitutable";
|
||||||
|
matchedProductId: any;
|
||||||
|
matchedProductName: any;
|
||||||
|
source: null;
|
||||||
|
availableQuantity: number;
|
||||||
|
missingQuantity: number;
|
||||||
|
})[];
|
||||||
|
summary: {
|
||||||
|
exactCount: number;
|
||||||
|
pantryCount: number;
|
||||||
|
substituteCount: number;
|
||||||
|
missingCount: number;
|
||||||
|
};
|
||||||
|
shoppingListCandidates: {
|
||||||
|
ingredientId: any;
|
||||||
|
rawName: any;
|
||||||
|
quantity: number;
|
||||||
|
unit: any;
|
||||||
|
missingQuantity: number;
|
||||||
|
}[];
|
||||||
|
}>;
|
||||||
findOne(id: number, user: {
|
findOne(id: number, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
@@ -155,6 +200,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -162,54 +209,52 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
create(createRecipeDto: CreateRecipeDto, user: {
|
create(createRecipeDto: CreateRecipeDto, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -217,6 +262,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -224,51 +271,49 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
update(id: number, createRecipeDto: CreateRecipeDto, user: {
|
update(id: number, createRecipeDto: CreateRecipeDto, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -276,6 +321,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -283,51 +330,49 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
remove(id: number, user: {
|
remove(id: number, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -342,6 +387,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -349,60 +396,60 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
addIngredient(id: number, ingredient: CreateIngredientDto, user: {
|
addIngredient(id: number, ingredient: CreateIngredientDto, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -410,38 +457,36 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
}>;
|
}>;
|
||||||
setVisibility(id: number, dto: SetRecipeVisibilityDto, user: {
|
setVisibility(id: number, dto: SetRecipeVisibilityDto, user: {
|
||||||
@@ -454,6 +499,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -461,54 +508,52 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
shareRecipe(id: number, dto: ShareRecipeDto, user: {
|
shareRecipe(id: number, dto: ShareRecipeDto, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -520,6 +565,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -527,54 +574,52 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
unshareRecipe(id: number, username: string, user: {
|
unshareRecipe(id: number, username: string, user: {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -586,6 +631,8 @@ export declare class RecipesController {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -593,54 +640,52 @@ export declare class RecipesController {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: import("@prisma/client/runtime/library").Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+16
-2
@@ -22,6 +22,7 @@ const parse_markdown_dto_1 = require("./dto/parse-markdown.dto");
|
|||||||
const current_user_decorator_1 = require("../auth/decorators/current-user.decorator");
|
const current_user_decorator_1 = require("../auth/decorators/current-user.decorator");
|
||||||
const share_recipe_dto_1 = require("./dto/share-recipe.dto");
|
const share_recipe_dto_1 = require("./dto/share-recipe.dto");
|
||||||
const set_recipe_visibility_dto_1 = require("./dto/set-recipe-visibility.dto");
|
const set_recipe_visibility_dto_1 = require("./dto/set-recipe-visibility.dto");
|
||||||
|
const recipe_analysis_service_1 = require("./recipe-analysis.service");
|
||||||
class UpdateImageDto {
|
class UpdateImageDto {
|
||||||
}
|
}
|
||||||
__decorate([
|
__decorate([
|
||||||
@@ -29,8 +30,9 @@ __decorate([
|
|||||||
__metadata("design:type", String)
|
__metadata("design:type", String)
|
||||||
], UpdateImageDto.prototype, "sourceUrl", void 0);
|
], UpdateImageDto.prototype, "sourceUrl", void 0);
|
||||||
let RecipesController = class RecipesController {
|
let RecipesController = class RecipesController {
|
||||||
constructor(recipesService) {
|
constructor(recipesService, recipeAnalysisService) {
|
||||||
this.recipesService = recipesService;
|
this.recipesService = recipesService;
|
||||||
|
this.recipeAnalysisService = recipeAnalysisService;
|
||||||
}
|
}
|
||||||
parseMarkdown(dto) {
|
parseMarkdown(dto) {
|
||||||
return this.recipesService.parseMarkdown(dto);
|
return this.recipesService.parseMarkdown(dto);
|
||||||
@@ -44,6 +46,9 @@ let RecipesController = class RecipesController {
|
|||||||
getInventoryPreview(id, user) {
|
getInventoryPreview(id, user) {
|
||||||
return this.recipesService.getInventoryPreview(id, user.userId);
|
return this.recipesService.getInventoryPreview(id, user.userId);
|
||||||
}
|
}
|
||||||
|
getRecipeAnalysis(id, user) {
|
||||||
|
return this.recipeAnalysisService.analyzeRecipeIngredients(id, user.userId);
|
||||||
|
}
|
||||||
findOne(id, user) {
|
findOne(id, user) {
|
||||||
return this.recipesService.findOne(id, user.userId);
|
return this.recipesService.findOne(id, user.userId);
|
||||||
}
|
}
|
||||||
@@ -102,6 +107,14 @@ __decorate([
|
|||||||
__metadata("design:paramtypes", [Number, Object]),
|
__metadata("design:paramtypes", [Number, Object]),
|
||||||
__metadata("design:returntype", void 0)
|
__metadata("design:returntype", void 0)
|
||||||
], RecipesController.prototype, "getInventoryPreview", null);
|
], RecipesController.prototype, "getInventoryPreview", null);
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Get)(':id/analysis'),
|
||||||
|
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
||||||
|
__param(1, (0, current_user_decorator_1.CurrentUser)()),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [Number, Object]),
|
||||||
|
__metadata("design:returntype", void 0)
|
||||||
|
], RecipesController.prototype, "getRecipeAnalysis", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Get)(':id'),
|
(0, common_1.Get)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
||||||
@@ -183,6 +196,7 @@ __decorate([
|
|||||||
], RecipesController.prototype, "unshareRecipe", null);
|
], RecipesController.prototype, "unshareRecipe", null);
|
||||||
exports.RecipesController = RecipesController = __decorate([
|
exports.RecipesController = RecipesController = __decorate([
|
||||||
(0, common_1.Controller)('recipes'),
|
(0, common_1.Controller)('recipes'),
|
||||||
__metadata("design:paramtypes", [recipes_service_1.RecipesService])
|
__metadata("design:paramtypes", [recipes_service_1.RecipesService,
|
||||||
|
recipe_analysis_service_1.RecipeAnalysisService])
|
||||||
], RecipesController);
|
], RecipesController);
|
||||||
//# sourceMappingURL=recipes.controller.js.map
|
//# sourceMappingURL=recipes.controller.js.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"recipes.controller.js","sourceRoot":"","sources":["../../src/recipes/recipes.controller.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"recipes.controller.js","sourceRoot":"","sources":["../../src/recipes/recipes.controller.ts"],"names":[],"mappings":"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"}
|
||||||
+2
-1
@@ -12,6 +12,7 @@ const prisma_module_1 = require("../prisma/prisma.module");
|
|||||||
const ai_module_1 = require("../ai/ai.module");
|
const ai_module_1 = require("../ai/ai.module");
|
||||||
const recipes_controller_1 = require("./recipes.controller");
|
const recipes_controller_1 = require("./recipes.controller");
|
||||||
const recipes_service_1 = require("./recipes.service");
|
const recipes_service_1 = require("./recipes.service");
|
||||||
|
const recipe_analysis_service_1 = require("./recipe-analysis.service");
|
||||||
let RecipesModule = class RecipesModule {
|
let RecipesModule = class RecipesModule {
|
||||||
};
|
};
|
||||||
exports.RecipesModule = RecipesModule;
|
exports.RecipesModule = RecipesModule;
|
||||||
@@ -19,7 +20,7 @@ exports.RecipesModule = RecipesModule = __decorate([
|
|||||||
(0, common_1.Module)({
|
(0, common_1.Module)({
|
||||||
imports: [prisma_module_1.PrismaModule, ai_module_1.AiModule],
|
imports: [prisma_module_1.PrismaModule, ai_module_1.AiModule],
|
||||||
controllers: [recipes_controller_1.RecipesController],
|
controllers: [recipes_controller_1.RecipesController],
|
||||||
providers: [recipes_service_1.RecipesService],
|
providers: [recipes_service_1.RecipesService, recipe_analysis_service_1.RecipeAnalysisService],
|
||||||
})
|
})
|
||||||
], RecipesModule);
|
], RecipesModule);
|
||||||
//# sourceMappingURL=recipes.module.js.map
|
//# sourceMappingURL=recipes.module.js.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"recipes.module.js","sourceRoot":"","sources":["../../src/recipes/recipes.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,+CAA2C;AAC3C,6DAAyD;AACzD,uDAAmD;AAO5C,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,oBAAQ,CAAC;QACjC,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,CAAC;KAC5B,CAAC;GACW,aAAa,CAAG"}
|
{"version":3,"file":"recipes.module.js","sourceRoot":"","sources":["../../src/recipes/recipes.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,+CAA2C;AAC3C,6DAAyD;AACzD,uDAAmD;AACnD,uEAAkE;AAO3D,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,oBAAQ,CAAC;QACjC,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,EAAE,+CAAqB,CAAC;KACnD,CAAC;GACW,aAAa,CAAG"}
|
||||||
+176
-175
@@ -17,6 +17,7 @@ export declare class RecipesService {
|
|||||||
private readonly logger;
|
private readonly logger;
|
||||||
constructor(prisma: PrismaService, aiService: AiService);
|
constructor(prisma: PrismaService, aiService: AiService);
|
||||||
private throwRecipeNotFound;
|
private throwRecipeNotFound;
|
||||||
|
private normalizeIngredientName;
|
||||||
private assertProductsActive;
|
private assertProductsActive;
|
||||||
private findRecipeByIdOrThrow;
|
private findRecipeByIdOrThrow;
|
||||||
private assertAndClaimRecipeOwner;
|
private assertAndClaimRecipeOwner;
|
||||||
@@ -73,6 +74,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -80,54 +83,52 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
})[]>;
|
})[]>;
|
||||||
findOne(id: number, userId: number): Promise<{
|
findOne(id: number, userId: number): Promise<{
|
||||||
owner: {
|
owner: {
|
||||||
@@ -137,6 +138,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -144,59 +147,59 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
|
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
|
||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -204,51 +207,49 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
remove(id: number, userId: number): Promise<void>;
|
remove(id: number, userId: number): Promise<void>;
|
||||||
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
|
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
|
||||||
@@ -259,6 +260,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -266,54 +269,52 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
|
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
|
||||||
owner: {
|
owner: {
|
||||||
@@ -323,6 +324,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -330,54 +333,52 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
shareWithUser(id: number, ownerId: number, username: string): Promise<{
|
shareWithUser(id: number, ownerId: number, username: string): Promise<{
|
||||||
owner: {
|
owner: {
|
||||||
@@ -387,6 +388,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -394,54 +397,52 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
|
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
|
||||||
owner: {
|
owner: {
|
||||||
@@ -451,6 +452,8 @@ export declare class RecipesService {
|
|||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -458,59 +461,59 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
shares: {
|
shares: {
|
||||||
userId: number;
|
userId: number;
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
create(createRecipeDto: CreateRecipeDto, userId: number): Promise<{
|
create(createRecipeDto: CreateRecipeDto, userId: number): Promise<{
|
||||||
ingredients: ({
|
ingredients: ({
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -518,55 +521,55 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
})[];
|
})[];
|
||||||
} & {
|
} & {
|
||||||
isPublic: boolean;
|
|
||||||
name: string;
|
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
name: string;
|
||||||
updatedAt: Date;
|
|
||||||
ownerId: number | null;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
instructions: string | null;
|
instructions: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
servings: number | null;
|
servings: number | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: number | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
|
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
|
||||||
product: ({
|
product: ({
|
||||||
nutrition: {
|
nutrition: {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
calories: number | null;
|
calories: number | null;
|
||||||
protein: number | null;
|
protein: number | null;
|
||||||
fat: number | null;
|
fat: number | null;
|
||||||
@@ -574,38 +577,36 @@ export declare class RecipesService {
|
|||||||
salt: number | null;
|
salt: number | null;
|
||||||
sugar: number | null;
|
sugar: number | null;
|
||||||
fiber: number | null;
|
fiber: number | null;
|
||||||
id: number;
|
|
||||||
productId: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
category: string | null;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
categoryId: number | null;
|
|
||||||
canonicalName: string | null;
|
|
||||||
id: number;
|
id: number;
|
||||||
normalizedName: string;
|
name: string;
|
||||||
isActive: boolean;
|
ownerId: number;
|
||||||
deletedAt: Date | null;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
ownerId: number;
|
status: string;
|
||||||
|
normalizedName: string;
|
||||||
|
category: string | null;
|
||||||
|
canonicalName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
categoryId: number | null;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}) | null;
|
}) | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
recipeId: number;
|
||||||
productId: number | null;
|
productId: number | null;
|
||||||
quantity: Prisma.Decimal | null;
|
|
||||||
unit: string | null;
|
|
||||||
rawName: string;
|
rawName: string;
|
||||||
rawLine: string | null;
|
rawLine: string | null;
|
||||||
|
quantity: Prisma.Decimal | null;
|
||||||
|
unit: string | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
alternativeProductIds: Prisma.JsonValue | null;
|
||||||
matchConfidence: number | null;
|
matchConfidence: number | null;
|
||||||
matchSource: string | null;
|
matchSource: string | null;
|
||||||
alternativeProductIds: Prisma.JsonValue | null;
|
|
||||||
recipeId: number;
|
|
||||||
analysisStatus: string | null;
|
analysisStatus: string | null;
|
||||||
}>;
|
}>;
|
||||||
suggestRecipesFromInventory(userId: number): Promise<{
|
suggestRecipesFromInventory(userId: number): Promise<{
|
||||||
|
|||||||
+8
-2
@@ -29,6 +29,12 @@ let RecipesService = RecipesService_1 = class RecipesService {
|
|||||||
throwRecipeNotFound(id) {
|
throwRecipeNotFound(id) {
|
||||||
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
|
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
|
||||||
}
|
}
|
||||||
|
normalizeIngredientName(value) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed)
|
||||||
|
return trimmed;
|
||||||
|
return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`;
|
||||||
|
}
|
||||||
async assertProductsActive(productIds) {
|
async assertProductsActive(productIds) {
|
||||||
if (productIds.length === 0)
|
if (productIds.length === 0)
|
||||||
return;
|
return;
|
||||||
@@ -299,7 +305,7 @@ let RecipesService = RecipesService_1 = class RecipesService {
|
|||||||
ingredients: {
|
ingredients: {
|
||||||
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId ?? null,
|
productId: ingredient.productId ?? null,
|
||||||
rawName: ingredient.rawName,
|
rawName: this.normalizeIngredientName(ingredient.rawName),
|
||||||
rawLine: ingredient.rawLine ?? null,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
quantity: ingredient.quantity ?? null,
|
quantity: ingredient.quantity ?? null,
|
||||||
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
@@ -443,7 +449,7 @@ let RecipesService = RecipesService_1 = class RecipesService {
|
|||||||
ingredients: {
|
ingredients: {
|
||||||
create: createRecipeDto.ingredients.map((ingredient) => ({
|
create: createRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId ?? null,
|
productId: ingredient.productId ?? null,
|
||||||
rawName: ingredient.rawName,
|
rawName: this.normalizeIngredientName(ingredient.rawName),
|
||||||
rawLine: ingredient.rawLine ?? null,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
quantity: ingredient.quantity ?? null,
|
quantity: ingredient.quantity ?? null,
|
||||||
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,228 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { canConvert, convertUnit } from '../common/utils/units';
|
||||||
|
|
||||||
|
type AnalysisStatus = 'exact_match' | 'covered_by_pantry' | 'substitutable' | 'missing';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RecipeAnalysisService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private async getAccessibleRecipe(id: number, userId: number) {
|
||||||
|
const recipe = await this.prisma.recipe.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
OR: [
|
||||||
|
{ isPublic: true },
|
||||||
|
{ ownerId: userId },
|
||||||
|
{ shares: { some: { userId } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
ingredients: {
|
||||||
|
include: {
|
||||||
|
product: true,
|
||||||
|
},
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
throw new NotFoundException(`Recipe with id ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateAvailableQuantity(
|
||||||
|
inventoryItems: Array<{ quantity: any; unit: string }>,
|
||||||
|
requiredUnit: string,
|
||||||
|
): number {
|
||||||
|
if (!requiredUnit) {
|
||||||
|
return inventoryItems.reduce((sum, item) => sum + Number(item.quantity ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRequiredUnit = requiredUnit.trim().toLowerCase();
|
||||||
|
|
||||||
|
const sameUnit = inventoryItems
|
||||||
|
.filter((item) => item.unit.trim().toLowerCase() === normalizedRequiredUnit)
|
||||||
|
.reduce((sum, item) => sum + Number(item.quantity ?? 0), 0);
|
||||||
|
|
||||||
|
const converted = inventoryItems
|
||||||
|
.filter((item) => item.unit.trim().toLowerCase() !== normalizedRequiredUnit)
|
||||||
|
.reduce((sum, item) => {
|
||||||
|
if (!canConvert(item.unit, requiredUnit)) return sum;
|
||||||
|
try {
|
||||||
|
return sum + convertUnit(Number(item.quantity ?? 0), item.unit, requiredUnit);
|
||||||
|
} catch {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return sameUnit + converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzeRecipeIngredients(id: number, userId: number) {
|
||||||
|
const recipe = await this.getAccessibleRecipe(id, userId);
|
||||||
|
|
||||||
|
const pantryItems = await this.prisma.pantryItem.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { productId: true },
|
||||||
|
});
|
||||||
|
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
|
||||||
|
|
||||||
|
const ingredients = await Promise.all(
|
||||||
|
recipe.ingredients.map(async (ingredient: any) => {
|
||||||
|
const requiredQuantity = Number(ingredient.quantity ?? 0);
|
||||||
|
const requiredUnit = (ingredient.unit ?? '').trim();
|
||||||
|
const rawName = (ingredient.rawName ?? '').trim() || 'Okänd ingrediens';
|
||||||
|
|
||||||
|
if (!ingredient.productId || !ingredient.product) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'missing' as AnalysisStatus,
|
||||||
|
matchedProductId: null,
|
||||||
|
matchedProductName: null,
|
||||||
|
source: null,
|
||||||
|
availableQuantity: 0,
|
||||||
|
missingQuantity: requiredQuantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pantryProductIds.has(ingredient.productId)) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'covered_by_pantry' as AnalysisStatus,
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: 'pantry',
|
||||||
|
availableQuantity: requiredQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inventoryItems = await this.prisma.inventoryItem.findMany({
|
||||||
|
where: { productId: ingredient.productId },
|
||||||
|
select: { quantity: true, unit: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableQuantity = this.calculateAvailableQuantity(inventoryItems, requiredUnit);
|
||||||
|
if (availableQuantity >= requiredQuantity) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'exact_match' as AnalysisStatus,
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: 'inventory',
|
||||||
|
availableQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternativeProductIds = Array.isArray(ingredient.alternativeProductIds)
|
||||||
|
? ingredient.alternativeProductIds.filter((id: any) => typeof id === 'number')
|
||||||
|
: [];
|
||||||
|
|
||||||
|
for (const altProductId of alternativeProductIds) {
|
||||||
|
if (pantryProductIds.has(altProductId)) {
|
||||||
|
const altProduct = await this.prisma.product.findUnique({
|
||||||
|
where: { id: altProductId },
|
||||||
|
select: { id: true, name: true, canonicalName: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'substitutable' as AnalysisStatus,
|
||||||
|
matchedProductId: altProduct?.id ?? altProductId,
|
||||||
|
matchedProductName: altProduct?.canonicalName || altProduct?.name || null,
|
||||||
|
source: 'pantry_substitute',
|
||||||
|
availableQuantity: requiredQuantity,
|
||||||
|
missingQuantity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const altInventoryItems = await this.prisma.inventoryItem.findMany({
|
||||||
|
where: { productId: altProductId },
|
||||||
|
select: { quantity: true, unit: true },
|
||||||
|
});
|
||||||
|
const altAvailable = this.calculateAvailableQuantity(altInventoryItems, requiredUnit);
|
||||||
|
if (altAvailable > 0) {
|
||||||
|
const altProduct = await this.prisma.product.findUnique({
|
||||||
|
where: { id: altProductId },
|
||||||
|
select: { id: true, name: true, canonicalName: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'substitutable' as AnalysisStatus,
|
||||||
|
matchedProductId: altProduct?.id ?? altProductId,
|
||||||
|
matchedProductName: altProduct?.canonicalName || altProduct?.name || null,
|
||||||
|
source: 'inventory_substitute',
|
||||||
|
availableQuantity: altAvailable,
|
||||||
|
missingQuantity: Math.max(0, requiredQuantity - altAvailable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
rawName,
|
||||||
|
quantity: requiredQuantity,
|
||||||
|
unit: requiredUnit,
|
||||||
|
note: ingredient.note ?? null,
|
||||||
|
status: 'missing' as AnalysisStatus,
|
||||||
|
matchedProductId: ingredient.productId,
|
||||||
|
matchedProductName: ingredient.product.canonicalName || ingredient.product.name,
|
||||||
|
source: null,
|
||||||
|
availableQuantity,
|
||||||
|
missingQuantity: Math.max(0, requiredQuantity - availableQuantity),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
exactCount: ingredients.filter((i) => i.status === 'exact_match').length,
|
||||||
|
pantryCount: ingredients.filter((i) => i.status === 'covered_by_pantry').length,
|
||||||
|
substituteCount: ingredients.filter((i) => i.status === 'substitutable').length,
|
||||||
|
missingCount: ingredients.filter((i) => i.status === 'missing').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const shoppingListCandidates = ingredients
|
||||||
|
.filter((i) => i.status === 'missing')
|
||||||
|
.map((i) => ({
|
||||||
|
ingredientId: i.ingredientId,
|
||||||
|
rawName: i.rawName,
|
||||||
|
quantity: i.quantity,
|
||||||
|
unit: i.unit,
|
||||||
|
missingQuantity: i.missingQuantity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
recipeId: recipe.id,
|
||||||
|
ingredients,
|
||||||
|
summary,
|
||||||
|
shoppingListCandidates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
|||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import { ShareRecipeDto } from './dto/share-recipe.dto';
|
import { ShareRecipeDto } from './dto/share-recipe.dto';
|
||||||
import { SetRecipeVisibilityDto } from './dto/set-recipe-visibility.dto';
|
import { SetRecipeVisibilityDto } from './dto/set-recipe-visibility.dto';
|
||||||
|
import { RecipeAnalysisService } from './recipe-analysis.service';
|
||||||
|
|
||||||
class UpdateImageDto {
|
class UpdateImageDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -15,7 +16,10 @@ class UpdateImageDto {
|
|||||||
|
|
||||||
@Controller('recipes')
|
@Controller('recipes')
|
||||||
export class RecipesController {
|
export class RecipesController {
|
||||||
constructor(private readonly recipesService: RecipesService) {}
|
constructor(
|
||||||
|
private readonly recipesService: RecipesService,
|
||||||
|
private readonly recipeAnalysisService: RecipeAnalysisService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('parse-markdown')
|
@Post('parse-markdown')
|
||||||
parseMarkdown(@Body() dto: ParseMarkdownDto) {
|
parseMarkdown(@Body() dto: ParseMarkdownDto) {
|
||||||
@@ -40,6 +44,14 @@ export class RecipesController {
|
|||||||
return this.recipesService.getInventoryPreview(id, user.userId);
|
return this.recipesService.getInventoryPreview(id, user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/analysis')
|
||||||
|
getRecipeAnalysis(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@CurrentUser() user: { userId: number },
|
||||||
|
) {
|
||||||
|
return this.recipeAnalysisService.analyzeRecipeIngredients(id, user.userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(
|
findOne(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { PrismaModule } from '../prisma/prisma.module';
|
|||||||
import { AiModule } from '../ai/ai.module';
|
import { AiModule } from '../ai/ai.module';
|
||||||
import { RecipesController } from './recipes.controller';
|
import { RecipesController } from './recipes.controller';
|
||||||
import { RecipesService } from './recipes.service';
|
import { RecipesService } from './recipes.service';
|
||||||
|
import { RecipeAnalysisService } from './recipe-analysis.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AiModule],
|
imports: [PrismaModule, AiModule],
|
||||||
controllers: [RecipesController],
|
controllers: [RecipesController],
|
||||||
providers: [RecipesService],
|
providers: [RecipesService, RecipeAnalysisService],
|
||||||
})
|
})
|
||||||
export class RecipesModule {}
|
export class RecipesModule {}
|
||||||
@@ -34,6 +34,12 @@ export class RecipesService {
|
|||||||
throw new NotFoundException(`Recipe with id ${id} not found`);
|
throw new NotFoundException(`Recipe with id ${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeIngredientName(value: string): string {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return trimmed;
|
||||||
|
return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
private async assertProductsActive(productIds: number[]): Promise<void> {
|
private async assertProductsActive(productIds: number[]): Promise<void> {
|
||||||
if (productIds.length === 0) return;
|
if (productIds.length === 0) return;
|
||||||
const activeProducts = await this.prisma.product.findMany({
|
const activeProducts = await this.prisma.product.findMany({
|
||||||
@@ -361,7 +367,7 @@ export class RecipesService {
|
|||||||
ingredients: {
|
ingredients: {
|
||||||
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId ?? null,
|
productId: ingredient.productId ?? null,
|
||||||
rawName: ingredient.rawName,
|
rawName: this.normalizeIngredientName(ingredient.rawName),
|
||||||
rawLine: ingredient.rawLine ?? null,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
quantity: ingredient.quantity ?? null,
|
quantity: ingredient.quantity ?? null,
|
||||||
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
@@ -537,7 +543,7 @@ export class RecipesService {
|
|||||||
ingredients: {
|
ingredients: {
|
||||||
create: createRecipeDto.ingredients.map((ingredient) => ({
|
create: createRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId ?? null,
|
productId: ingredient.productId ?? null,
|
||||||
rawName: ingredient.rawName,
|
rawName: this.normalizeIngredientName(ingredient.rawName),
|
||||||
rawLine: ingredient.rawLine ?? null,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
quantity: ingredient.quantity ?? null,
|
quantity: ingredient.quantity ?? null,
|
||||||
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@ class RecipeApiPaths {
|
|||||||
static String share(int id) => '/recipes/$id/share';
|
static String share(int id) => '/recipes/$id/share';
|
||||||
static String unshare(int id, String username) => '/recipes/$id/share/${Uri.encodeComponent(username)}';
|
static String unshare(int id, String username) => '/recipes/$id/share/${Uri.encodeComponent(username)}';
|
||||||
static String inventoryPreview(int id) => '/recipes/$id/inventory-preview';
|
static String inventoryPreview(int id) => '/recipes/$id/inventory-preview';
|
||||||
|
static String analysis(int id) => '/recipes/$id/analysis';
|
||||||
static const parseMarkdown = '/recipes/parse-markdown';
|
static const parseMarkdown = '/recipes/parse-markdown';
|
||||||
static const aiSuggestions = '/recipes/ai-suggestions';
|
static const aiSuggestions = '/recipes/ai-suggestions';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../../../core/api/guarded_api_call.dart';
|
|||||||
import '../../../features/auth/data/auth_providers.dart';
|
import '../../../features/auth/data/auth_providers.dart';
|
||||||
import '../domain/recipe.dart';
|
import '../domain/recipe.dart';
|
||||||
import '../domain/inventory_preview.dart';
|
import '../domain/inventory_preview.dart';
|
||||||
|
import '../domain/recipe_analysis.dart';
|
||||||
import 'recipe_repository.dart';
|
import 'recipe_repository.dart';
|
||||||
|
|
||||||
final recipeRepositoryProvider = Provider<RecipeRepository>((ref) {
|
final recipeRepositoryProvider = Provider<RecipeRepository>((ref) {
|
||||||
@@ -38,3 +39,14 @@ final inventoryPreviewProvider =
|
|||||||
.fetchInventoryPreview(id, token: token),
|
.fetchInventoryPreview(id, token: token),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final recipeAnalysisProvider =
|
||||||
|
FutureProvider.family<RecipeAnalysis, int>((ref, id) async {
|
||||||
|
final token = await ref.watch(authStateProvider.future);
|
||||||
|
return guardedApiCall(
|
||||||
|
ref,
|
||||||
|
() => ref
|
||||||
|
.read(recipeRepositoryProvider)
|
||||||
|
.fetchRecipeAnalysis(id, token: token),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '../../../core/api/api_paths.dart';
|
|||||||
import '../domain/parsed_recipe.dart';
|
import '../domain/parsed_recipe.dart';
|
||||||
import '../domain/recipe.dart';
|
import '../domain/recipe.dart';
|
||||||
import '../domain/inventory_preview.dart';
|
import '../domain/inventory_preview.dart';
|
||||||
|
import '../domain/recipe_analysis.dart';
|
||||||
|
|
||||||
class RecipeRepository {
|
class RecipeRepository {
|
||||||
final ApiClient _api;
|
final ApiClient _api;
|
||||||
@@ -174,6 +175,27 @@ class RecipeRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RecipeAnalysis> fetchRecipeAnalysis(int id,
|
||||||
|
{String? token}) async {
|
||||||
|
try {
|
||||||
|
final data = await _api.getJson(
|
||||||
|
RecipeApiPaths.analysis(id),
|
||||||
|
token: token,
|
||||||
|
);
|
||||||
|
if (data is! Map<String, dynamic>) {
|
||||||
|
throw const ApiException(
|
||||||
|
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
|
||||||
|
}
|
||||||
|
return RecipeAnalysis.fromJson(data);
|
||||||
|
} on ApiException {
|
||||||
|
rethrow;
|
||||||
|
} catch (_) {
|
||||||
|
throw const ApiException(
|
||||||
|
type: ApiErrorType.network,
|
||||||
|
message: 'Kunde inte hämta receptanalys.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ParsedRecipe> parseMarkdown(String markdown,
|
Future<ParsedRecipe> parseMarkdown(String markdown,
|
||||||
{String? token}) async {
|
{String? token}) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
enum RecipeIngredientAvailabilityStatus {
|
||||||
|
exactMatch,
|
||||||
|
coveredByPantry,
|
||||||
|
substitutable,
|
||||||
|
missing,
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecipeIngredientAnalysis {
|
||||||
|
final int ingredientId;
|
||||||
|
final String rawName;
|
||||||
|
final double quantity;
|
||||||
|
final String unit;
|
||||||
|
final String? note;
|
||||||
|
final RecipeIngredientAvailabilityStatus status;
|
||||||
|
final int? matchedProductId;
|
||||||
|
final String? matchedProductName;
|
||||||
|
final String? source;
|
||||||
|
final double availableQuantity;
|
||||||
|
final double missingQuantity;
|
||||||
|
|
||||||
|
const RecipeIngredientAnalysis({
|
||||||
|
required this.ingredientId,
|
||||||
|
required this.rawName,
|
||||||
|
required this.quantity,
|
||||||
|
required this.unit,
|
||||||
|
this.note,
|
||||||
|
required this.status,
|
||||||
|
this.matchedProductId,
|
||||||
|
this.matchedProductName,
|
||||||
|
this.source,
|
||||||
|
required this.availableQuantity,
|
||||||
|
required this.missingQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeIngredientAnalysis.fromJson(Map<String, dynamic> json) {
|
||||||
|
final rawStatus = json['status'] as String? ?? 'missing';
|
||||||
|
final status = switch (rawStatus) {
|
||||||
|
'exact_match' => RecipeIngredientAvailabilityStatus.exactMatch,
|
||||||
|
'covered_by_pantry' => RecipeIngredientAvailabilityStatus.coveredByPantry,
|
||||||
|
'substitutable' => RecipeIngredientAvailabilityStatus.substitutable,
|
||||||
|
_ => RecipeIngredientAvailabilityStatus.missing,
|
||||||
|
};
|
||||||
|
|
||||||
|
return RecipeIngredientAnalysis(
|
||||||
|
ingredientId: (json['ingredientId'] as num?)?.toInt() ?? 0,
|
||||||
|
rawName: (json['rawName'] as String? ?? '').trim(),
|
||||||
|
quantity: (json['quantity'] as num? ?? 0).toDouble(),
|
||||||
|
unit: json['unit'] as String? ?? '',
|
||||||
|
note: json['note'] as String?,
|
||||||
|
status: status,
|
||||||
|
matchedProductId: (json['matchedProductId'] as num?)?.toInt(),
|
||||||
|
matchedProductName: json['matchedProductName'] as String?,
|
||||||
|
source: json['source'] as String?,
|
||||||
|
availableQuantity: (json['availableQuantity'] as num? ?? 0).toDouble(),
|
||||||
|
missingQuantity: (json['missingQuantity'] as num? ?? 0).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecipeAnalysisSummary {
|
||||||
|
final int exactCount;
|
||||||
|
final int pantryCount;
|
||||||
|
final int substituteCount;
|
||||||
|
final int missingCount;
|
||||||
|
|
||||||
|
const RecipeAnalysisSummary({
|
||||||
|
required this.exactCount,
|
||||||
|
required this.pantryCount,
|
||||||
|
required this.substituteCount,
|
||||||
|
required this.missingCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeAnalysisSummary.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RecipeAnalysisSummary(
|
||||||
|
exactCount: (json['exactCount'] as num?)?.toInt() ?? 0,
|
||||||
|
pantryCount: (json['pantryCount'] as num?)?.toInt() ?? 0,
|
||||||
|
substituteCount: (json['substituteCount'] as num?)?.toInt() ?? 0,
|
||||||
|
missingCount: (json['missingCount'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecipeShoppingCandidate {
|
||||||
|
final int ingredientId;
|
||||||
|
final String rawName;
|
||||||
|
final double quantity;
|
||||||
|
final String unit;
|
||||||
|
final double missingQuantity;
|
||||||
|
|
||||||
|
const RecipeShoppingCandidate({
|
||||||
|
required this.ingredientId,
|
||||||
|
required this.rawName,
|
||||||
|
required this.quantity,
|
||||||
|
required this.unit,
|
||||||
|
required this.missingQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeShoppingCandidate.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RecipeShoppingCandidate(
|
||||||
|
ingredientId: (json['ingredientId'] as num?)?.toInt() ?? 0,
|
||||||
|
rawName: (json['rawName'] as String? ?? '').trim(),
|
||||||
|
quantity: (json['quantity'] as num? ?? 0).toDouble(),
|
||||||
|
unit: json['unit'] as String? ?? '',
|
||||||
|
missingQuantity: (json['missingQuantity'] as num? ?? 0).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecipeAnalysis {
|
||||||
|
final int recipeId;
|
||||||
|
final List<RecipeIngredientAnalysis> ingredients;
|
||||||
|
final RecipeAnalysisSummary summary;
|
||||||
|
final List<RecipeShoppingCandidate> shoppingListCandidates;
|
||||||
|
|
||||||
|
const RecipeAnalysis({
|
||||||
|
required this.recipeId,
|
||||||
|
required this.ingredients,
|
||||||
|
required this.summary,
|
||||||
|
required this.shoppingListCandidates,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeAnalysis.fromJson(Map<String, dynamic> json) {
|
||||||
|
final rawIngredients = json['ingredients'] as List<dynamic>? ?? const [];
|
||||||
|
final rawShopping = json['shoppingListCandidates'] as List<dynamic>? ?? const [];
|
||||||
|
|
||||||
|
return RecipeAnalysis(
|
||||||
|
recipeId: (json['recipeId'] as num?)?.toInt() ?? 0,
|
||||||
|
ingredients: rawIngredients
|
||||||
|
.map((e) => RecipeIngredientAnalysis.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
summary: RecipeAnalysisSummary.fromJson(
|
||||||
|
json['summary'] as Map<String, dynamic>? ?? const {},
|
||||||
|
),
|
||||||
|
shoppingListCandidates: rawShopping
|
||||||
|
.map((e) => RecipeShoppingCandidate.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
import '../../../core/api/api_paths.dart';
|
|
||||||
import '../../../core/api/api_providers.dart';
|
|
||||||
import '../../../core/utils/formatters.dart';
|
import '../../../core/utils/formatters.dart';
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
@@ -14,19 +12,6 @@ import '../domain/parsed_recipe.dart';
|
|||||||
|
|
||||||
enum _Step { input, review }
|
enum _Step { input, review }
|
||||||
|
|
||||||
class _ManualIngredient {
|
|
||||||
int? productId;
|
|
||||||
final TextEditingController qtyCtrl = TextEditingController();
|
|
||||||
final TextEditingController unitCtrl = TextEditingController(text: 'g');
|
|
||||||
final TextEditingController noteCtrl = TextEditingController();
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
qtyCtrl.dispose();
|
|
||||||
unitCtrl.dispose();
|
|
||||||
noteCtrl.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CreateRecipeScreen extends ConsumerStatefulWidget {
|
class CreateRecipeScreen extends ConsumerStatefulWidget {
|
||||||
/// Optional markdown to pre-fill the input field, e.g. from import.
|
/// Optional markdown to pre-fill the input field, e.g. from import.
|
||||||
final String? initialMarkdown;
|
final String? initialMarkdown;
|
||||||
@@ -64,20 +49,12 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
late TextEditingController _nameCtrl;
|
late TextEditingController _nameCtrl;
|
||||||
late TextEditingController _servingsCtrl;
|
late TextEditingController _servingsCtrl;
|
||||||
late List<bool> _included;
|
late List<bool> _included;
|
||||||
late Map<int, int?> _selectedProductIds;
|
|
||||||
late Map<int, String?> _selectedProductNames;
|
|
||||||
|
|
||||||
|
late Map<int, TextEditingController> _rawNameControllers;
|
||||||
late Map<int, TextEditingController> _qtyControllers;
|
late Map<int, TextEditingController> _qtyControllers;
|
||||||
late Map<int, TextEditingController> _unitControllers;
|
late Map<int, TextEditingController> _unitControllers;
|
||||||
late Map<int, TextEditingController> _noteControllers;
|
late Map<int, TextEditingController> _noteControllers;
|
||||||
|
|
||||||
// Produktlista för manuellt tillagda ingredienser
|
|
||||||
List<Map<String, dynamic>> _allProducts = [];
|
|
||||||
bool _isLoadingProducts = false;
|
|
||||||
|
|
||||||
// Manuellt tillagda ingredienser
|
|
||||||
final List<_ManualIngredient> _manualIngredients = [];
|
|
||||||
|
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
String? _saveError;
|
String? _saveError;
|
||||||
|
|
||||||
@@ -87,64 +64,37 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
if (_step == _Step.review) {
|
if (_step == _Step.review) {
|
||||||
_nameCtrl.dispose();
|
_nameCtrl.dispose();
|
||||||
_servingsCtrl.dispose();
|
_servingsCtrl.dispose();
|
||||||
|
for (final c in _rawNameControllers.values) c.dispose();
|
||||||
for (final c in _qtyControllers.values) c.dispose();
|
for (final c in _qtyControllers.values) c.dispose();
|
||||||
for (final c in _unitControllers.values) c.dispose();
|
for (final c in _unitControllers.values) c.dispose();
|
||||||
for (final c in _noteControllers.values) c.dispose();
|
for (final c in _noteControllers.values) c.dispose();
|
||||||
for (final m in _manualIngredients) m.dispose();
|
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadProducts() async {
|
|
||||||
setState(() => _isLoadingProducts = true);
|
|
||||||
try {
|
|
||||||
final token = await ref.read(authStateProvider.future);
|
|
||||||
final api = ref.read(apiClientProvider);
|
|
||||||
final data = await api.getJson(ProductApiPaths.list, token: token);
|
|
||||||
if (!mounted) return;
|
|
||||||
final products = (data as List<dynamic>)
|
|
||||||
.map((e) => e as Map<String, dynamic>)
|
|
||||||
.toList()
|
|
||||||
..sort((a, b) {
|
|
||||||
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString();
|
|
||||||
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString();
|
|
||||||
return aName.toLowerCase().compareTo(bName.toLowerCase());
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
_allProducts = products;
|
|
||||||
_isLoadingProducts = false;
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() => _isLoadingProducts = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initReviewState(ParsedRecipe parsed) {
|
void _initReviewState(ParsedRecipe parsed) {
|
||||||
_nameCtrl = TextEditingController(text: parsed.name);
|
_nameCtrl = TextEditingController(text: parsed.name);
|
||||||
_servingsCtrl = TextEditingController();
|
_servingsCtrl = TextEditingController();
|
||||||
_included = List.generate(parsed.ingredients.length, (_) => true);
|
_included = List.generate(parsed.ingredients.length, (_) => true);
|
||||||
_selectedProductIds = {};
|
_rawNameControllers = {};
|
||||||
_selectedProductNames = {};
|
|
||||||
_qtyControllers = {};
|
_qtyControllers = {};
|
||||||
_unitControllers = {};
|
_unitControllers = {};
|
||||||
_noteControllers = {};
|
_noteControllers = {};
|
||||||
for (var i = 0; i < parsed.ingredients.length; i++) {
|
for (var i = 0; i < parsed.ingredients.length; i++) {
|
||||||
final ing = parsed.ingredients[i];
|
final ing = parsed.ingredients[i];
|
||||||
|
_rawNameControllers[i] = TextEditingController(text: ing.rawName);
|
||||||
_qtyControllers[i] = TextEditingController(
|
_qtyControllers[i] = TextEditingController(
|
||||||
text: ing.quantity > 0 ? formatQuantity(ing.quantity) : '',
|
text: ing.quantity > 0 ? formatQuantity(ing.quantity) : '',
|
||||||
);
|
);
|
||||||
_unitControllers[i] = TextEditingController(text: ing.unit);
|
_unitControllers[i] = TextEditingController(text: ing.unit);
|
||||||
_noteControllers[i] = TextEditingController(text: ing.note ?? '');
|
_noteControllers[i] = TextEditingController(text: ing.note ?? '');
|
||||||
if (ing.suggestions.isNotEmpty) {
|
|
||||||
_selectedProductIds[i] = ing.suggestions.first.productId;
|
|
||||||
_selectedProductNames[i] = ing.suggestions.first.productName;
|
|
||||||
} else {
|
|
||||||
_selectedProductIds[i] = null;
|
|
||||||
_selectedProductNames[i] = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_loadProducts();
|
}
|
||||||
|
|
||||||
|
String _formatIngredientName(String value) {
|
||||||
|
final trimmed = value.trim();
|
||||||
|
if (trimmed.isEmpty) return trimmed;
|
||||||
|
return '${trimmed[0].toUpperCase()}${trimmed.substring(1)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _parseMarkdown() async {
|
Future<void> _parseMarkdown() async {
|
||||||
@@ -190,7 +140,10 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
final ingredients = <Map<String, dynamic>>[];
|
final ingredients = <Map<String, dynamic>>[];
|
||||||
for (var i = 0; i < _parsed!.ingredients.length; i++) {
|
for (var i = 0; i < _parsed!.ingredients.length; i++) {
|
||||||
if (!_included[i]) continue;
|
if (!_included[i]) continue;
|
||||||
final productId = _selectedProductIds[i];
|
final rawName = _formatIngredientName(_rawNameControllers[i]!.text);
|
||||||
|
if (rawName.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
final qty = double.tryParse(
|
final qty = double.tryParse(
|
||||||
_qtyControllers[i]!.text.trim().replaceAll(',', '.'),
|
_qtyControllers[i]!.text.trim().replaceAll(',', '.'),
|
||||||
) ??
|
) ??
|
||||||
@@ -198,40 +151,18 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
final unit = _unitControllers[i]!.text.trim();
|
final unit = _unitControllers[i]!.text.trim();
|
||||||
final note = _noteControllers[i]!.text.trim();
|
final note = _noteControllers[i]!.text.trim();
|
||||||
final ing = _parsed!.ingredients[i];
|
final ing = _parsed!.ingredients[i];
|
||||||
// Alternativa produkter: alla suggestions vars productId matchar ett alternativ
|
|
||||||
final alternativeProductIds = ing.alternatives.length > 1
|
|
||||||
? ing.suggestions
|
|
||||||
.where((s) => s.productId != productId)
|
|
||||||
.map((s) => s.productId)
|
|
||||||
.toList()
|
|
||||||
: <int>[];
|
|
||||||
ingredients.add({
|
ingredients.add({
|
||||||
'rawName': ing.rawName,
|
'rawName': rawName,
|
||||||
if ((ing.rawLine ?? '').trim().isNotEmpty) 'rawLine': ing.rawLine,
|
if ((ing.rawLine ?? '').trim().isNotEmpty) 'rawLine': ing.rawLine,
|
||||||
if (productId != null) 'productId': productId,
|
|
||||||
if (qty > 0) 'quantity': qty,
|
if (qty > 0) 'quantity': qty,
|
||||||
if (unit.isNotEmpty) 'unit': unit,
|
if (unit.isNotEmpty) 'unit': unit,
|
||||||
if (note.isNotEmpty) 'note': note,
|
if (note.isNotEmpty) 'note': note,
|
||||||
if (alternativeProductIds.isNotEmpty)
|
|
||||||
'alternativeProductIds': alternativeProductIds,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Inkludera manuellt tillagda ingredienser
|
|
||||||
for (final manual in _manualIngredients) {
|
if (ingredients.isEmpty) {
|
||||||
if (manual.productId == null) continue;
|
setState(() => _saveError = 'Lägg till minst en ingrediensrad.');
|
||||||
final qty = double.tryParse(
|
return;
|
||||||
manual.qtyCtrl.text.trim().replaceAll(',', '.'),
|
|
||||||
);
|
|
||||||
if (qty == null) continue;
|
|
||||||
final unit = manual.unitCtrl.text.trim();
|
|
||||||
if (unit.isEmpty) continue;
|
|
||||||
final note = manual.noteCtrl.text.trim();
|
|
||||||
ingredients.add({
|
|
||||||
'productId': manual.productId,
|
|
||||||
'quantity': qty,
|
|
||||||
'unit': unit,
|
|
||||||
if (note.isNotEmpty) 'note': note,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -370,27 +301,6 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
parsed.ingredients.length,
|
parsed.ingredients.length,
|
||||||
(i) => _buildIngredientRow(i, parsed.ingredients[i])),
|
(i) => _buildIngredientRow(i, parsed.ingredients[i])),
|
||||||
],
|
],
|
||||||
// Manuellt tillagda ingredienser
|
|
||||||
if (_manualIngredients.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...List.generate(
|
|
||||||
_manualIngredients.length,
|
|
||||||
(i) => _buildManualIngredientCard(i),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
// Knapp för att lägga till ingrediens
|
|
||||||
if (_isLoadingProducts)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: _addManualIngredient,
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Lägg till ingrediens'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -423,124 +333,11 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addManualIngredient() {
|
|
||||||
setState(() {
|
|
||||||
_manualIngredients.add(_ManualIngredient());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeManualIngredient(int index) {
|
|
||||||
setState(() {
|
|
||||||
_manualIngredients[index].dispose();
|
|
||||||
_manualIngredients.removeAt(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildManualIngredientCard(int index) {
|
|
||||||
final manual = _manualIngredients[index];
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Tillagd ingrediens',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
onPressed: () => _removeManualIngredient(index),
|
|
||||||
tooltip: 'Ta bort',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
DropdownButtonFormField<int>(
|
|
||||||
value: manual.productId,
|
|
||||||
isExpanded: true,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Produkt *',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
hint: const Text('Välj produkt'),
|
|
||||||
items: _allProducts
|
|
||||||
.map((p) => DropdownMenuItem<int>(
|
|
||||||
value: p['id'] as int,
|
|
||||||
child: Text(
|
|
||||||
((p['canonicalName'] ?? p['name']) as Object).toString(),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() => manual.productId = value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 72,
|
|
||||||
child: TextField(
|
|
||||||
controller: manual.qtyCtrl,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Mängd',
|
|
||||||
isDense: true,
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
contentPadding:
|
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
),
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: 72,
|
|
||||||
child: TextField(
|
|
||||||
controller: manual.unitCtrl,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Enhet',
|
|
||||||
isDense: true,
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
contentPadding:
|
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: manual.noteCtrl,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Not',
|
|
||||||
isDense: true,
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
contentPadding:
|
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIngredientRow(int index, ParsedIngredient ing) {
|
Widget _buildIngredientRow(int index, ParsedIngredient ing) {
|
||||||
final isIncluded = _included[index];
|
final isIncluded = _included[index];
|
||||||
final noProductFound = ing.suggestions.isEmpty;
|
final suggestionText = ing.suggestions.isEmpty
|
||||||
// Problem #2: tydlig varning om rad är inkluderad men saknar produkt
|
? null
|
||||||
final showMissingProductWarning = isIncluded && noProductFound;
|
: 'Förslag: ${ing.suggestions.take(3).map((s) => s.productName).join(', ')}';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
@@ -562,86 +359,78 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
: Text(ing.rawName),
|
: Text(_formatIngredientName(ing.rawName)),
|
||||||
subtitle: noProductFound
|
subtitle: suggestionText == null
|
||||||
? Text(
|
? null
|
||||||
context.l10n.recipeCreateNoProductFound,
|
: Text(
|
||||||
|
suggestionText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: showMissingProductWarning
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
? Theme.of(context).colorScheme.error
|
|
||||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: DropdownButton<int>(
|
|
||||||
value: _selectedProductIds[index],
|
|
||||||
isExpanded: true,
|
|
||||||
onChanged: isIncluded
|
|
||||||
? (id) {
|
|
||||||
if (id == null) return;
|
|
||||||
setState(() {
|
|
||||||
_selectedProductIds[index] = id;
|
|
||||||
_selectedProductNames[index] = ing.suggestions
|
|
||||||
.firstWhere((s) => s.productId == id)
|
|
||||||
.productName;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
items: ing.suggestions
|
|
||||||
.map((s) => DropdownMenuItem(
|
|
||||||
value: s.productId,
|
|
||||||
child: Text(s.productName),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Problem #1: editerbara qty/unit/note-fält per ingrediens
|
|
||||||
if (isIncluded)
|
if (isIncluded)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
child: Row(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
TextField(
|
||||||
width: 72,
|
controller: _rawNameControllers[index],
|
||||||
child: TextField(
|
decoration: const InputDecoration(
|
||||||
controller: _qtyControllers[index],
|
labelText: 'Ingrediens',
|
||||||
decoration: const InputDecoration(
|
isDense: true,
|
||||||
labelText: 'Mängd',
|
border: OutlineInputBorder(),
|
||||||
isDense: true,
|
contentPadding:
|
||||||
border: OutlineInputBorder(),
|
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
contentPadding:
|
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
Row(
|
||||||
width: 72,
|
children: [
|
||||||
child: TextField(
|
SizedBox(
|
||||||
controller: _unitControllers[index],
|
width: 72,
|
||||||
decoration: const InputDecoration(
|
child: TextField(
|
||||||
labelText: 'Enhet',
|
controller: _qtyControllers[index],
|
||||||
isDense: true,
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
labelText: 'Mängd',
|
||||||
contentPadding:
|
isDense: true,
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
),
|
SizedBox(
|
||||||
const SizedBox(width: 8),
|
width: 72,
|
||||||
Expanded(
|
child: TextField(
|
||||||
child: TextField(
|
controller: _unitControllers[index],
|
||||||
controller: _noteControllers[index],
|
decoration: const InputDecoration(
|
||||||
decoration: const InputDecoration(
|
labelText: 'Enhet',
|
||||||
labelText: 'Not',
|
isDense: true,
|
||||||
isDense: true,
|
border: OutlineInputBorder(),
|
||||||
border: OutlineInputBorder(),
|
contentPadding:
|
||||||
contentPadding:
|
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _noteControllers[index],
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Not',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import '../../../core/ui/async_state_views.dart';
|
|||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/recipe_providers.dart';
|
import '../data/recipe_providers.dart';
|
||||||
import '../domain/recipe.dart';
|
import '../domain/recipe.dart';
|
||||||
import '../domain/inventory_preview.dart';
|
import '../domain/recipe_analysis.dart';
|
||||||
|
|
||||||
String _fmtQty(double v) => formatQuantity(v);
|
String _fmtQty(double v) => formatQuantity(v);
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.45),
|
color: Colors.black.withValues(alpha: 0.45),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -296,7 +296,7 @@ class _ImagePlaceholder extends StatelessWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.restaurant,
|
Icons.restaurant,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
|
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -543,7 +543,7 @@ class _InventoryPreviewSectionState
|
|||||||
tooltip: 'Uppdatera',
|
tooltip: 'Uppdatera',
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.invalidate(inventoryPreviewProvider(widget.recipeId));
|
ref.invalidate(recipeAnalysisProvider(widget.recipeId));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -561,7 +561,7 @@ class _InventoryPreviewResults extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final previewAsync = ref.watch(inventoryPreviewProvider(recipeId));
|
final previewAsync = ref.watch(recipeAnalysisProvider(recipeId));
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return previewAsync.when(
|
return previewAsync.when(
|
||||||
@@ -587,6 +587,33 @@ class _InventoryPreviewResults extends ConsumerWidget {
|
|||||||
...preview.ingredients.map(
|
...preview.ingredients.map(
|
||||||
(ing) => _IngredientPreviewRow(ingredient: ing),
|
(ing) => _IngredientPreviewRow(ingredient: ing),
|
||||||
),
|
),
|
||||||
|
if (preview.shoppingListCandidates.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Shoppinglista', style: theme.textTheme.titleSmall),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...preview.shoppingListCandidates.map((item) {
|
||||||
|
final qty = _fmtQty(item.missingQuantity > 0 ? item.missingQuantity : item.quantity);
|
||||||
|
final measure = '$qty ${item.unit}'.trim();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shopping_cart_outlined, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
measure.isEmpty
|
||||||
|
? item.rawName
|
||||||
|
: '$measure ${item.rawName}',
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -595,7 +622,7 @@ class _InventoryPreviewResults extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SummaryChips extends StatelessWidget {
|
class _SummaryChips extends StatelessWidget {
|
||||||
final PreviewSummary summary;
|
final RecipeAnalysisSummary summary;
|
||||||
|
|
||||||
const _SummaryChips({required this.summary});
|
const _SummaryChips({required this.summary});
|
||||||
|
|
||||||
@@ -607,7 +634,7 @@ class _SummaryChips extends StatelessWidget {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: [
|
children: [
|
||||||
if (summary.canCookExactly)
|
if (summary.missingCount == 0)
|
||||||
Chip(
|
Chip(
|
||||||
avatar: Icon(Icons.check_circle,
|
avatar: Icon(Icons.check_circle,
|
||||||
color: cs.onPrimary, size: 16),
|
color: cs.onPrimary, size: 16),
|
||||||
@@ -624,21 +651,29 @@ class _SummaryChips extends StatelessWidget {
|
|||||||
backgroundColor: cs.errorContainer,
|
backgroundColor: cs.errorContainer,
|
||||||
labelStyle: TextStyle(color: cs.onErrorContainer),
|
labelStyle: TextStyle(color: cs.onErrorContainer),
|
||||||
),
|
),
|
||||||
if (summary.unitMismatchCount > 0)
|
if (summary.substituteCount > 0)
|
||||||
Chip(
|
Chip(
|
||||||
avatar: Icon(Icons.swap_horiz,
|
avatar: Icon(Icons.swap_horiz,
|
||||||
color: cs.onTertiaryContainer, size: 16),
|
color: cs.onTertiaryContainer, size: 16),
|
||||||
label: Text('${summary.unitMismatchCount} enhetsmismatch'),
|
label: Text('${summary.substituteCount} ersättningsbar'),
|
||||||
backgroundColor: cs.tertiaryContainer,
|
backgroundColor: cs.tertiaryContainer,
|
||||||
labelStyle: TextStyle(color: cs.onTertiaryContainer),
|
labelStyle: TextStyle(color: cs.onTertiaryContainer),
|
||||||
),
|
),
|
||||||
|
if (summary.pantryCount > 0)
|
||||||
|
Chip(
|
||||||
|
avatar: Icon(Icons.kitchen_outlined,
|
||||||
|
color: cs.onSecondaryContainer, size: 16),
|
||||||
|
label: Text('${summary.pantryCount} i skafferiet'),
|
||||||
|
backgroundColor: cs.secondaryContainer,
|
||||||
|
labelStyle: TextStyle(color: cs.onSecondaryContainer),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IngredientPreviewRow extends StatelessWidget {
|
class _IngredientPreviewRow extends StatelessWidget {
|
||||||
final IngredientPreview ingredient;
|
final RecipeIngredientAnalysis ingredient;
|
||||||
|
|
||||||
const _IngredientPreviewRow({required this.ingredient});
|
const _IngredientPreviewRow({required this.ingredient});
|
||||||
|
|
||||||
@@ -646,37 +681,35 @@ class _IngredientPreviewRow extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final cs = theme.colorScheme;
|
final cs = theme.colorScheme;
|
||||||
final label = ingredient.productName.trim().isEmpty
|
final matchedName = ingredient.matchedProductName?.trim() ?? '';
|
||||||
? 'Okänd ingrediens'
|
final label = matchedName.isEmpty
|
||||||
: ingredient.productName;
|
? (ingredient.rawName.trim().isEmpty ? 'Okänd ingrediens' : ingredient.rawName)
|
||||||
|
: matchedName;
|
||||||
|
|
||||||
final (icon, color) = ingredient.fromPantry
|
final (icon, color) = switch (ingredient.status) {
|
||||||
? (Icons.kitchen_outlined, cs.secondary)
|
RecipeIngredientAvailabilityStatus.coveredByPantry => (Icons.kitchen_outlined, cs.secondary),
|
||||||
: switch (ingredient.status) {
|
RecipeIngredientAvailabilityStatus.exactMatch => (Icons.check_circle_outline, cs.primary),
|
||||||
IngredientStatus.enough => (Icons.check_circle_outline, cs.primary),
|
RecipeIngredientAvailabilityStatus.substitutable => (Icons.swap_horiz, cs.tertiary),
|
||||||
IngredientStatus.unitMismatch => (
|
RecipeIngredientAvailabilityStatus.missing => (Icons.cancel_outlined, cs.error),
|
||||||
Icons.swap_horiz,
|
};
|
||||||
cs.tertiary,
|
|
||||||
),
|
|
||||||
IngredientStatus.missing => (Icons.cancel_outlined, cs.error),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
final effectiveUnit = ingredient.unit;
|
||||||
final requiredStr =
|
final requiredStr =
|
||||||
'${_fmtQty(ingredient.requiredQuantity)} ${ingredient.requiredUnit}'.trim();
|
'${_fmtQty(ingredient.quantity)} $effectiveUnit'.trim();
|
||||||
final availableStr =
|
final availableStr =
|
||||||
'${_fmtQty(ingredient.availableQuantity)} ${ingredient.requiredUnit}'.trim();
|
'${_fmtQty(ingredient.availableQuantity)} $effectiveUnit'.trim();
|
||||||
|
|
||||||
final subtitle = ingredient.fromPantry
|
final subtitle = switch (ingredient.status) {
|
||||||
? 'Finns i skafferiet'
|
RecipeIngredientAvailabilityStatus.coveredByPantry => 'Finns i skafferiet',
|
||||||
: switch (ingredient.status) {
|
RecipeIngredientAvailabilityStatus.exactMatch => 'Tillgängligt: $availableStr',
|
||||||
IngredientStatus.enough => 'Tillgängligt: $availableStr',
|
RecipeIngredientAvailabilityStatus.substitutable =>
|
||||||
IngredientStatus.missing => ingredient.availableQuantity > 0
|
ingredient.matchedProductName == null || ingredient.matchedProductName!.trim().isEmpty
|
||||||
? 'Saknar ${_fmtQty(ingredient.missingQuantity)} ${ingredient.requiredUnit} '
|
? 'Kan ersättas med annan vara'
|
||||||
'(har $availableStr)'
|
: 'Kan ersättas med ${ingredient.matchedProductName}',
|
||||||
: 'Saknas helt',
|
RecipeIngredientAvailabilityStatus.missing => ingredient.availableQuantity > 0
|
||||||
IngredientStatus.unitMismatch =>
|
? 'Saknar ${_fmtQty(ingredient.missingQuantity)} $effectiveUnit (har $availableStr)'
|
||||||
'Annan enhet i lager – kontrollera manuellt',
|
: 'Saknas helt',
|
||||||
};
|
};
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class _RecipeImageCard extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.45),
|
color: Colors.black.withValues(alpha: 0.45),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
Reference in New Issue
Block a user