Files
recipe-app/backend/prisma/schema.prisma
T
Nils-Johan Gynther a1a2c33427
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 5m8s
Test Suite / flutter-quality (push) Failing after 1m41s
feat(shopping-list): add shopping list feature with flyer integration
This commit introduces a comprehensive shopping list feature with the following key changes:

Backend:
- Added ShoppingListItem model with relations to User, Product, and Category
- Added new fields to FlyerSession for source file metadata
- Added categoryId field to FlyerItem model
- Implemented session source file retrieval endpoint
- Added endpoint for updating flyer session items with category assignment
- Added endpoint for planning flyer selections to shopping list
- Implemented backfillCategoriesMine for AI-assisted category assignment
- Added ShoppingListModule and integrated with FlyerSelectionModule

Frontend:
- Added ShoppingListScreen and navigation route
- Implemented API paths and client methods for shopping list operations
- Added category tree loading for shopping list item creation
- Integrated shopping list functionality in flyer import tab
- Added category backfill trigger in inventory screen
- Updated FlyerImportItem model with categoryId support
- Added methods for updating flyer session items and retrieving source files

Database:
- Added new Prisma migration for flyer source metadata and shopping list items
- Updated schema with new relations and indexes

The shopping list feature allows users to:
1. Plan flyer selections directly to their shopping list
2. View and manage their shopping list items
3. Update flyer session items with proper categorization
4. Retrieve original flyer source files
5. Automatically backfill categories for uncategorized products
2026-05-20 09:07:30 +02:00

386 lines
12 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
firstName String?
lastName String?
passwordHash String
role String @default("user")
isPremium Boolean @default(false)
canShareRecipes Boolean @default(true)
aiEngineEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userProducts UserProduct[]
ownedRecipes Recipe[] @relation("RecipeOwner")
sharedRecipes RecipeShare[]
ownedProducts Product[]
inventoryItems InventoryItem[]
pantryItems PantryItem[]
mealPlanEntries MealPlanEntry[]
receiptAliases ReceiptAlias[]
unitMappings UnitMapping[]
flyerSessions FlyerSession[]
flyerSelections FlyerSelection[]
shoppingListItems ShoppingListItem[]
}
model Product {
id Int @id @default(autoincrement())
name String
normalizedName String @unique
canonicalName String?
isActive Boolean @default(true)
status String @default("active")
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inventoryItems InventoryItem[]
recipeIngredients RecipeIngredient[]
pantryItems PantryItem[]
receiptAliases ReceiptAlias[]
tags ProductTag[]
nutrition Nutrition?
ownerId Int
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
userProducts UserProduct[]
categoryId Int?
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
isPrivate Boolean @default(false)
unitMappings UnitMapping[]
shoppingListItems ShoppingListItem[]
}
model Category {
id Int @id @default(autoincrement())
name String
parentId Int?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
children Category[] @relation("CategoryTree")
products Product[]
flyerItems FlyerItem[]
shoppingListItems ShoppingListItem[]
@@unique([name, parentId])
@@index([parentId])
}
model UserProduct {
id Int @id @default(autoincrement())
userId Int
productId Int
note String? @db.Text
preferredBrand String?
preferredStore String?
isPrivate Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@unique([userId, productId])
@@index([userId])
@@index([productId])
}
model InventoryItem {
id Int @id @default(autoincrement())
userId Int
productId Int
quantity Decimal @db.Decimal(10, 2)
unit String
brand String?
origin String?
receiptName String?
location String?
purchaseDate DateTime?
opened Boolean?
suitableFor String?
bestBeforeDate DateTime?
comment String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
consumptions InventoryConsumption[]
@@index([userId])
@@index([productId])
}
model InventoryConsumption {
id Int @id @default(autoincrement())
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
inventoryItemId Int
amountUsed Decimal @db.Decimal(10, 2)
comment String?
createdAt DateTime @default(now())
}
model Recipe {
id Int @id @default(autoincrement())
name String
description String? @db.Text
instructions String? @db.Text
imageUrl String?
servings Int?
isPublic Boolean @default(false)
ownerId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User? @relation("RecipeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
ingredients RecipeIngredient[]
mealPlanEntries MealPlanEntry[]
shares RecipeShare[]
}
model RecipeShare {
recipeId Int
userId Int
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([recipeId, userId])
@@index([userId])
}
model RecipeIngredient {
id Int @id @default(autoincrement())
recipe Recipe @relation(fields: [recipeId], references: [id])
recipeId Int
product Product? @relation(fields: [productId], references: [id])
productId Int?
rawName String @default("")
rawLine String? @db.Text
quantity Decimal? @db.Decimal(10, 2)
unit String?
note String?
alternativeProductIds Json? // [id, id, ...] — alternativa produkter (t.ex. "ris eller couscous")
matchConfidence Float?
matchSource String?
analysisStatus String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PantryItem {
id Int @id @default(autoincrement())
userId Int
productId Int
location String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, productId])
@@index([userId])
}
model ReceiptAlias {
id Int @id @default(autoincrement())
receiptName String // normaliserat kvittonamn (lowercase, trim)
ownerId Int?
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
isGlobal Boolean @default(false)
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([receiptName, ownerId, isGlobal])
@@index([ownerId])
@@index([isGlobal])
@@index([receiptName])
}
model MealPlanEntry {
id Int @id @default(autoincrement())
userId Int
date DateTime @db.Date
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
recipeId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
servings Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Bara ett recept per dag och anvandare.
@@unique([userId, date])
@@index([userId])
@@index([date])
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
products ProductTag[]
}
model ProductTag {
productId Int
tagId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([productId, tagId])
@@index([tagId])
}
model Nutrition {
id Int @id @default(autoincrement())
productId Int @unique
calories Float?
protein Float?
fat Float?
carbohydrates Float?
salt Float?
sugar Float?
fiber Float?
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
}
model UnitMapping {
id Int @id @default(autoincrement())
productId Int
originalUnit String
preferredUnit String
userId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([productId, originalUnit, userId])
@@index([productId])
@@index([userId])
}
model HelpText {
id Int @id @default(autoincrement())
key String
scope String @default("default")
title String
content String @db.Text
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([key, scope])
@@index([key, isActive])
}
model FlyerSession {
id Int @id @default(autoincrement())
userId Int
retailer String
weekKey String
status String @default("draft")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
sourceFileName String?
sourceMimeType String?
sourceFileSize Int?
sourceStorageKey String?
sourceData Bytes?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
items FlyerItem[]
selections FlyerSelection[]
@@index([userId])
@@index([weekKey])
@@index([status])
}
model FlyerItem {
id Int @id @default(autoincrement())
sessionId Int
rawName String
normalizedName String
categoryHint String?
categoryId Int?
price Decimal? @db.Decimal(10, 2)
priceUnit String?
comparisonPrice Decimal? @db.Decimal(10, 2)
comparisonUnit String?
offerText String?
parseConfidence Float
parseReasons Json?
matchedProductId Int?
matchedProductName String?
matchedVia String?
matchConfidence Float?
matchReasons Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
session FlyerSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
selections FlyerSelection[]
@@index([sessionId])
@@index([normalizedName])
@@index([categoryId])
}
model FlyerSelection {
id Int @id @default(autoincrement())
sessionId Int
itemId Int
userId Int
plannedQuantity Decimal? @db.Decimal(10, 2)
plannedUnit String?
priority String @default("normal")
note String?
status String @default("planned")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
session FlyerSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
item FlyerItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([sessionId, itemId])
@@index([sessionId])
@@index([userId, status])
}
model ShoppingListItem {
id Int @id @default(autoincrement())
userId Int
name String
productId Int?
categoryId Int?
quantity Decimal? @db.Decimal(10, 2)
unit String?
source String @default("manual")
status String @default("open")
checkedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
@@index([userId, status])
@@index([productId, unit, status])
@@index([categoryId])
}