feat: enhance recipe ingredient model; add raw fields and optional properties for better ingredient handling
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE `RecipeIngredient`
|
||||||
|
MODIFY `productId` INTEGER NULL,
|
||||||
|
MODIFY `quantity` DECIMAL(10, 2) NULL,
|
||||||
|
MODIFY `unit` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `rawName` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN `rawLine` TEXT NULL,
|
||||||
|
ADD COLUMN `matchConfidence` DOUBLE NULL,
|
||||||
|
ADD COLUMN `matchSource` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `analysisStatus` VARCHAR(191) NULL;
|
||||||
+242
-237
@@ -1,237 +1,242 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
firstName String?
|
firstName String?
|
||||||
lastName String?
|
lastName String?
|
||||||
passwordHash String
|
passwordHash String
|
||||||
role String @default("user")
|
role String @default("user")
|
||||||
isPremium Boolean @default(false)
|
isPremium Boolean @default(false)
|
||||||
canShareRecipes Boolean @default(true)
|
canShareRecipes Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
userProducts UserProduct[]
|
userProducts UserProduct[]
|
||||||
ownedRecipes Recipe[] @relation("RecipeOwner")
|
ownedRecipes Recipe[] @relation("RecipeOwner")
|
||||||
sharedRecipes RecipeShare[]
|
sharedRecipes RecipeShare[]
|
||||||
ownedProducts Product[]
|
ownedProducts Product[]
|
||||||
pantryItems PantryItem[]
|
pantryItems PantryItem[]
|
||||||
mealPlanEntries MealPlanEntry[]
|
mealPlanEntries MealPlanEntry[]
|
||||||
receiptAliases ReceiptAlias[]
|
receiptAliases ReceiptAlias[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Product {
|
model Product {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
normalizedName String @unique
|
normalizedName String @unique
|
||||||
category String?
|
category String?
|
||||||
canonicalName String?
|
canonicalName String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
status String @default("active")
|
status String @default("active")
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
inventoryItems InventoryItem[]
|
inventoryItems InventoryItem[]
|
||||||
recipeIngredients RecipeIngredient[]
|
recipeIngredients RecipeIngredient[]
|
||||||
pantryItems PantryItem[]
|
pantryItems PantryItem[]
|
||||||
receiptAliases ReceiptAlias[]
|
receiptAliases ReceiptAlias[]
|
||||||
tags ProductTag[]
|
tags ProductTag[]
|
||||||
nutrition Nutrition?
|
nutrition Nutrition?
|
||||||
ownerId Int
|
ownerId Int
|
||||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
userProducts UserProduct[]
|
userProducts UserProduct[]
|
||||||
categoryId Int?
|
categoryId Int?
|
||||||
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||||
isPrivate Boolean @default(false)
|
isPrivate Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Category {
|
model Category {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
parentId Int?
|
parentId Int?
|
||||||
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
|
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
|
||||||
children Category[] @relation("CategoryTree")
|
children Category[] @relation("CategoryTree")
|
||||||
products Product[]
|
products Product[]
|
||||||
|
|
||||||
@@unique([name, parentId])
|
@@unique([name, parentId])
|
||||||
@@index([parentId])
|
@@index([parentId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserProduct {
|
model UserProduct {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int
|
userId Int
|
||||||
productId Int
|
productId Int
|
||||||
note String? @db.Text
|
note String? @db.Text
|
||||||
preferredBrand String?
|
preferredBrand String?
|
||||||
preferredStore String?
|
preferredStore String?
|
||||||
isPrivate Boolean @default(false)
|
isPrivate Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([userId, productId])
|
@@unique([userId, productId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([productId])
|
@@index([productId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model InventoryItem {
|
model InventoryItem {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
productId Int
|
productId Int
|
||||||
quantity Decimal @db.Decimal(10, 2)
|
quantity Decimal @db.Decimal(10, 2)
|
||||||
unit String
|
unit String
|
||||||
brand String?
|
brand String?
|
||||||
origin String?
|
origin String?
|
||||||
receiptName String?
|
receiptName String?
|
||||||
location String?
|
location String?
|
||||||
purchaseDate DateTime?
|
purchaseDate DateTime?
|
||||||
opened Boolean?
|
opened Boolean?
|
||||||
suitableFor String?
|
suitableFor String?
|
||||||
bestBeforeDate DateTime?
|
bestBeforeDate DateTime?
|
||||||
comment String?
|
comment String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
consumptions InventoryConsumption[]
|
consumptions InventoryConsumption[]
|
||||||
|
|
||||||
@@index([productId])
|
@@index([productId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model InventoryConsumption {
|
model InventoryConsumption {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
|
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
|
||||||
inventoryItemId Int
|
inventoryItemId Int
|
||||||
amountUsed Decimal @db.Decimal(10, 2)
|
amountUsed Decimal @db.Decimal(10, 2)
|
||||||
comment String?
|
comment String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Recipe {
|
model Recipe {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
instructions String? @db.Text
|
instructions String? @db.Text
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
servings Int?
|
servings Int?
|
||||||
isPublic Boolean @default(false)
|
isPublic Boolean @default(false)
|
||||||
ownerId Int?
|
ownerId Int?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
owner User? @relation("RecipeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
|
owner User? @relation("RecipeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
|
||||||
ingredients RecipeIngredient[]
|
ingredients RecipeIngredient[]
|
||||||
mealPlanEntries MealPlanEntry[]
|
mealPlanEntries MealPlanEntry[]
|
||||||
shares RecipeShare[]
|
shares RecipeShare[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model RecipeShare {
|
model RecipeShare {
|
||||||
recipeId Int
|
recipeId Int
|
||||||
userId Int
|
userId Int
|
||||||
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@id([recipeId, userId])
|
@@id([recipeId, userId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model RecipeIngredient {
|
model RecipeIngredient {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
recipe Recipe @relation(fields: [recipeId], references: [id])
|
recipe Recipe @relation(fields: [recipeId], references: [id])
|
||||||
recipeId Int
|
recipeId Int
|
||||||
product Product @relation(fields: [productId], references: [id])
|
product Product? @relation(fields: [productId], references: [id])
|
||||||
productId Int
|
productId Int?
|
||||||
quantity Decimal @db.Decimal(10, 2)
|
rawName String @default("")
|
||||||
unit String
|
rawLine String? @db.Text
|
||||||
note String?
|
quantity Decimal? @db.Decimal(10, 2)
|
||||||
alternativeProductIds Json? // [id, id, ...] — alternativa produkter (t.ex. "ris eller couscous")
|
unit String?
|
||||||
|
note String?
|
||||||
createdAt DateTime @default(now())
|
alternativeProductIds Json? // [id, id, ...] — alternativa produkter (t.ex. "ris eller couscous")
|
||||||
updatedAt DateTime @updatedAt
|
matchConfidence Float?
|
||||||
}
|
matchSource String?
|
||||||
|
analysisStatus String?
|
||||||
model PantryItem {
|
|
||||||
id Int @id @default(autoincrement())
|
createdAt DateTime @default(now())
|
||||||
userId Int
|
updatedAt DateTime @updatedAt
|
||||||
productId Int
|
}
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
model PantryItem {
|
||||||
createdAt DateTime @default(now())
|
id Int @id @default(autoincrement())
|
||||||
updatedAt DateTime @updatedAt
|
userId Int
|
||||||
|
productId Int
|
||||||
@@unique([userId, productId])
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
@@index([userId])
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
}
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
model ReceiptAlias {
|
|
||||||
id Int @id @default(autoincrement())
|
@@unique([userId, productId])
|
||||||
receiptName String // normaliserat kvittonamn (lowercase, trim)
|
@@index([userId])
|
||||||
ownerId Int?
|
}
|
||||||
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
||||||
isGlobal Boolean @default(false)
|
model ReceiptAlias {
|
||||||
productId Int
|
id Int @id @default(autoincrement())
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
receiptName String // normaliserat kvittonamn (lowercase, trim)
|
||||||
createdAt DateTime @default(now())
|
ownerId Int?
|
||||||
|
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
@@unique([receiptName, ownerId, isGlobal])
|
isGlobal Boolean @default(false)
|
||||||
@@index([ownerId])
|
productId Int
|
||||||
@@index([isGlobal])
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
}
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
model MealPlanEntry {
|
@@unique([receiptName, ownerId, isGlobal])
|
||||||
id Int @id @default(autoincrement())
|
@@index([ownerId])
|
||||||
userId Int
|
@@index([isGlobal])
|
||||||
date DateTime @db.Date
|
}
|
||||||
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
||||||
recipeId Int
|
model MealPlanEntry {
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
id Int @id @default(autoincrement())
|
||||||
servings Int?
|
userId Int
|
||||||
createdAt DateTime @default(now())
|
date DateTime @db.Date
|
||||||
updatedAt DateTime @updatedAt
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
||||||
|
recipeId Int
|
||||||
// Bara ett recept per dag och anvandare.
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
@@unique([userId, date])
|
servings Int?
|
||||||
@@index([userId])
|
createdAt DateTime @default(now())
|
||||||
@@index([date])
|
updatedAt DateTime @updatedAt
|
||||||
}
|
|
||||||
|
// Bara ett recept per dag och anvandare.
|
||||||
model Tag {
|
@@unique([userId, date])
|
||||||
id Int @id @default(autoincrement())
|
@@index([userId])
|
||||||
name String @unique
|
@@index([date])
|
||||||
products ProductTag[]
|
}
|
||||||
}
|
|
||||||
|
model Tag {
|
||||||
model ProductTag {
|
id Int @id @default(autoincrement())
|
||||||
productId Int
|
name String @unique
|
||||||
tagId Int
|
products ProductTag[]
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
}
|
||||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
||||||
|
model ProductTag {
|
||||||
@@id([productId, tagId])
|
productId Int
|
||||||
@@index([tagId])
|
tagId Int
|
||||||
}
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
model Nutrition {
|
|
||||||
id Int @id @default(autoincrement())
|
@@id([productId, tagId])
|
||||||
productId Int @unique
|
@@index([tagId])
|
||||||
calories Float?
|
}
|
||||||
protein Float?
|
|
||||||
fat Float?
|
model Nutrition {
|
||||||
carbohydrates Float?
|
id Int @id @default(autoincrement())
|
||||||
salt Float?
|
productId Int @unique
|
||||||
sugar Float?
|
calories Float?
|
||||||
fiber Float?
|
protein Float?
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
fat Float?
|
||||||
}
|
carbohydrates Float?
|
||||||
|
salt Float?
|
||||||
|
sugar Float?
|
||||||
|
fiber Float?
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,20 +12,39 @@ import {
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
class CreateRecipeIngredientDto {
|
class CreateRecipeIngredientDto {
|
||||||
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
productId!: number;
|
productId?: number;
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
quantity!: number;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
unit!: string;
|
rawName!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
rawLine?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
quantity?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
unit?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
matchConfidence?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
matchSource?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsInt({ each: true })
|
@IsInt({ each: true })
|
||||||
|
|||||||
@@ -113,6 +113,24 @@ export class RecipesService {
|
|||||||
|
|
||||||
const ingredientPreviews = await Promise.all(
|
const ingredientPreviews = await Promise.all(
|
||||||
recipe.ingredients.map(async (ingredient: any) => {
|
recipe.ingredients.map(async (ingredient: any) => {
|
||||||
|
if (!ingredient.productId || !ingredient.product) {
|
||||||
|
return {
|
||||||
|
ingredientId: ingredient.id,
|
||||||
|
productId: null,
|
||||||
|
productName: ingredient.rawName || 'Okänd ingrediens',
|
||||||
|
requiredQuantity: Number(ingredient.quantity ?? 0),
|
||||||
|
requiredUnit: ingredient.unit || '',
|
||||||
|
note: ingredient.note,
|
||||||
|
availableQuantity: 0,
|
||||||
|
availableUnit: ingredient.unit || '',
|
||||||
|
matchingInventoryItems: [],
|
||||||
|
otherInventoryItems: [],
|
||||||
|
status: 'missing' as const,
|
||||||
|
fromPantry: false,
|
||||||
|
missingQuantity: Number(ingredient.quantity ?? 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Täcks ingrediensen av pantry (inkl. alternativ)?
|
// Täcks ingrediensen av pantry (inkl. alternativ)?
|
||||||
const coveredByPantry =
|
const coveredByPantry =
|
||||||
pantryProductIds.has(ingredient.productId) ||
|
pantryProductIds.has(ingredient.productId) ||
|
||||||
@@ -313,7 +331,11 @@ export class RecipesService {
|
|||||||
await this.assertAndClaimRecipeOwner(existingRecipe, userId);
|
await this.assertAndClaimRecipeOwner(existingRecipe, userId);
|
||||||
|
|
||||||
// Validera att alla produkter är aktiva
|
// Validera att alla produkter är aktiva
|
||||||
await this.assertProductsActive(updateRecipeDto.ingredients.map((i) => i.productId));
|
await this.assertProductsActive(
|
||||||
|
updateRecipeDto.ingredients
|
||||||
|
.map((i) => i.productId)
|
||||||
|
.filter((id): id is number => typeof id === 'number'),
|
||||||
|
);
|
||||||
|
|
||||||
// Transaktionsblock: ta bort gamla + skapa nya ingredienser atomärt
|
// Transaktionsblock: ta bort gamla + skapa nya ingredienser atomärt
|
||||||
const recipe = await this.prisma.$transaction(async (tx) => {
|
const recipe = await this.prisma.$transaction(async (tx) => {
|
||||||
@@ -329,11 +351,15 @@ export class RecipesService {
|
|||||||
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
|
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
|
||||||
ingredients: {
|
ingredients: {
|
||||||
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
create: updateRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId,
|
productId: ingredient.productId ?? null,
|
||||||
quantity: ingredient.quantity,
|
rawName: ingredient.rawName,
|
||||||
unit: ingredient.unit,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
|
quantity: ingredient.quantity ?? null,
|
||||||
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
note: ingredient.note || null,
|
note: ingredient.note || null,
|
||||||
alternativeProductIds: ingredient.alternativeProductIds ?? [],
|
alternativeProductIds: ingredient.alternativeProductIds ?? [],
|
||||||
|
matchConfidence: ingredient.matchConfidence ?? null,
|
||||||
|
matchSource: ingredient.matchSource ?? null,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -462,7 +488,11 @@ export class RecipesService {
|
|||||||
|
|
||||||
async create(createRecipeDto: CreateRecipeDto, userId: number) {
|
async create(createRecipeDto: CreateRecipeDto, userId: number) {
|
||||||
// Validera att alla produkter är aktiva
|
// Validera att alla produkter är aktiva
|
||||||
await this.assertProductsActive(createRecipeDto.ingredients.map((i) => i.productId));
|
await this.assertProductsActive(
|
||||||
|
createRecipeDto.ingredients
|
||||||
|
.map((i) => i.productId)
|
||||||
|
.filter((id): id is number => typeof id === 'number'),
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,
|
`[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,
|
||||||
@@ -497,11 +527,15 @@ export class RecipesService {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
ingredients: {
|
ingredients: {
|
||||||
create: createRecipeDto.ingredients.map((ingredient) => ({
|
create: createRecipeDto.ingredients.map((ingredient) => ({
|
||||||
productId: ingredient.productId,
|
productId: ingredient.productId ?? null,
|
||||||
quantity: ingredient.quantity,
|
rawName: ingredient.rawName,
|
||||||
unit: ingredient.unit,
|
rawLine: ingredient.rawLine ?? null,
|
||||||
|
quantity: ingredient.quantity ?? null,
|
||||||
|
unit: ingredient.unit?.trim() ? ingredient.unit : null,
|
||||||
note: ingredient.note || null,
|
note: ingredient.note || null,
|
||||||
alternativeProductIds: ingredient.alternativeProductIds ?? [],
|
alternativeProductIds: ingredient.alternativeProductIds ?? [],
|
||||||
|
matchConfidence: ingredient.matchConfidence ?? null,
|
||||||
|
matchSource: ingredient.matchSource ?? null,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -745,6 +779,7 @@ Regler:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
rawName: ingredient.rawName,
|
rawName: ingredient.rawName,
|
||||||
|
rawLine: ingredient.rawName,
|
||||||
alternatives: ingredient.alternatives ?? [],
|
alternatives: ingredient.alternatives ?? [],
|
||||||
quantity: ingredient.quantity,
|
quantity: ingredient.quantity,
|
||||||
unit: ingredient.unit,
|
unit: ingredient.unit,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ enum IngredientStatus { enough, missing, unitMismatch }
|
|||||||
|
|
||||||
class IngredientPreview {
|
class IngredientPreview {
|
||||||
final int ingredientId;
|
final int ingredientId;
|
||||||
final int productId;
|
final int? productId;
|
||||||
final String productName;
|
final String productName;
|
||||||
final double requiredQuantity;
|
final double requiredQuantity;
|
||||||
final String requiredUnit;
|
final String requiredUnit;
|
||||||
@@ -14,7 +14,7 @@ class IngredientPreview {
|
|||||||
|
|
||||||
const IngredientPreview({
|
const IngredientPreview({
|
||||||
required this.ingredientId,
|
required this.ingredientId,
|
||||||
required this.productId,
|
this.productId,
|
||||||
required this.productName,
|
required this.productName,
|
||||||
required this.requiredQuantity,
|
required this.requiredQuantity,
|
||||||
required this.requiredUnit,
|
required this.requiredUnit,
|
||||||
@@ -34,8 +34,8 @@ class IngredientPreview {
|
|||||||
};
|
};
|
||||||
return IngredientPreview(
|
return IngredientPreview(
|
||||||
ingredientId: json['ingredientId'] as int,
|
ingredientId: json['ingredientId'] as int,
|
||||||
productId: json['productId'] as int,
|
productId: (json['productId'] as num?)?.toInt(),
|
||||||
productName: json['productName'] as String,
|
productName: (json['productName'] as String?) ?? (json['rawName'] as String? ?? ''),
|
||||||
requiredQuantity: (json['requiredQuantity'] as num).toDouble(),
|
requiredQuantity: (json['requiredQuantity'] as num).toDouble(),
|
||||||
requiredUnit: json['requiredUnit'] as String? ?? '',
|
requiredUnit: json['requiredUnit'] as String? ?? '',
|
||||||
note: json['note'] as String?,
|
note: json['note'] as String?,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class IngredientSuggestion {
|
|||||||
|
|
||||||
class ParsedIngredient {
|
class ParsedIngredient {
|
||||||
final String rawName;
|
final String rawName;
|
||||||
|
final String? rawLine;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final String unit;
|
final String unit;
|
||||||
final String? note;
|
final String? note;
|
||||||
@@ -27,6 +28,7 @@ class ParsedIngredient {
|
|||||||
|
|
||||||
const ParsedIngredient({
|
const ParsedIngredient({
|
||||||
required this.rawName,
|
required this.rawName,
|
||||||
|
this.rawLine,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unit,
|
required this.unit,
|
||||||
this.note,
|
this.note,
|
||||||
@@ -38,6 +40,7 @@ class ParsedIngredient {
|
|||||||
final rawSuggestions = json['suggestions'] as List<dynamic>? ?? [];
|
final rawSuggestions = json['suggestions'] as List<dynamic>? ?? [];
|
||||||
return ParsedIngredient(
|
return ParsedIngredient(
|
||||||
rawName: json['rawName'] as String? ?? '',
|
rawName: json['rawName'] as String? ?? '',
|
||||||
|
rawLine: json['rawLine'] as String?,
|
||||||
quantity: (json['quantity'] as num? ?? 0).toDouble(),
|
quantity: (json['quantity'] as num? ?? 0).toDouble(),
|
||||||
unit: json['unit'] as String? ?? '',
|
unit: json['unit'] as String? ?? '',
|
||||||
note: json['note'] as String?,
|
note: json['note'] as String?,
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
class RecipeIngredient {
|
class RecipeIngredient {
|
||||||
final int id;
|
final int id;
|
||||||
final int productId;
|
final int? productId;
|
||||||
final String productName;
|
final String? productName;
|
||||||
|
final String rawName;
|
||||||
|
final String? rawLine;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final String unit;
|
final String unit;
|
||||||
final String? note;
|
final String? note;
|
||||||
|
|
||||||
const RecipeIngredient({
|
const RecipeIngredient({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.productId,
|
this.productId,
|
||||||
required this.productName,
|
this.productName,
|
||||||
|
required this.rawName,
|
||||||
|
this.rawLine,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unit,
|
required this.unit,
|
||||||
this.note,
|
this.note,
|
||||||
@@ -20,8 +24,10 @@ class RecipeIngredient {
|
|||||||
final rawQty = json['quantity'];
|
final rawQty = json['quantity'];
|
||||||
return RecipeIngredient(
|
return RecipeIngredient(
|
||||||
id: (json['id'] as num).toInt(),
|
id: (json['id'] as num).toInt(),
|
||||||
productId: (json['productId'] as num).toInt(),
|
productId: (json['productId'] as num?)?.toInt(),
|
||||||
productName: product?['name'] as String? ?? '',
|
productName: product?['canonicalName'] as String? ?? product?['name'] as String?,
|
||||||
|
rawName: json['rawName'] as String? ?? '',
|
||||||
|
rawLine: json['rawLine'] as String?,
|
||||||
quantity: rawQty is num
|
quantity: rawQty is num
|
||||||
? rawQty.toDouble()
|
? rawQty.toDouble()
|
||||||
: double.tryParse(rawQty?.toString() ?? '') ?? 0,
|
: double.tryParse(rawQty?.toString() ?? '') ?? 0,
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
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 productId = _selectedProductIds[i];
|
||||||
if (productId == null) continue;
|
|
||||||
final qty = double.tryParse(
|
final qty = double.tryParse(
|
||||||
_qtyControllers[i]!.text.trim().replaceAll(',', '.'),
|
_qtyControllers[i]!.text.trim().replaceAll(',', '.'),
|
||||||
) ??
|
) ??
|
||||||
@@ -207,9 +206,11 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
.toList()
|
.toList()
|
||||||
: <int>[];
|
: <int>[];
|
||||||
ingredients.add({
|
ingredients.add({
|
||||||
'productId': productId,
|
'rawName': ing.rawName,
|
||||||
'quantity': qty,
|
if ((ing.rawLine ?? '').trim().isNotEmpty) 'rawLine': ing.rawLine,
|
||||||
'unit': unit,
|
if (productId != null) 'productId': productId,
|
||||||
|
if (qty > 0) 'quantity': qty,
|
||||||
|
if (unit.isNotEmpty) 'unit': unit,
|
||||||
if (note.isNotEmpty) 'note': note,
|
if (note.isNotEmpty) 'note': note,
|
||||||
if (alternativeProductIds.isNotEmpty)
|
if (alternativeProductIds.isNotEmpty)
|
||||||
'alternativeProductIds': alternativeProductIds,
|
'alternativeProductIds': alternativeProductIds,
|
||||||
|
|||||||
@@ -396,6 +396,10 @@ class _RecipeBody extends StatelessWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
...recipe.ingredients.map((ing) {
|
...recipe.ingredients.map((ing) {
|
||||||
final qtyStr = ing.quantity == 0 ? '' : _fmtQty(ing.quantity);
|
final qtyStr = ing.quantity == 0 ? '' : _fmtQty(ing.quantity);
|
||||||
|
final ingredientLabel = (ing.rawName.trim().isNotEmpty
|
||||||
|
? ing.rawName
|
||||||
|
: (ing.productName ?? '').trim())
|
||||||
|
.trim();
|
||||||
final measureParts = [
|
final measureParts = [
|
||||||
if (qtyStr.isNotEmpty) qtyStr,
|
if (qtyStr.isNotEmpty) qtyStr,
|
||||||
if (ing.unit.isNotEmpty) ing.unit,
|
if (ing.unit.isNotEmpty) ing.unit,
|
||||||
@@ -430,8 +434,8 @@ class _RecipeBody extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
ing.note != null
|
ing.note != null
|
||||||
? '${ing.productName} (${ing.note})'
|
? '$ingredientLabel (${ing.note})'
|
||||||
: ing.productName,
|
: ingredientLabel,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user