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

This commit is contained in:
Nils-Johan Gynther
2026-05-06 07:25:42 +02:00
parent 612fcddb47
commit e4f201ea36
9 changed files with 349 additions and 267 deletions
@@ -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
View File
@@ -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)
}
+25 -6
View File
@@ -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 })
+43 -8
View File
@@ -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,
), ),
), ),