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
This commit is contained in:
+51
@@ -0,0 +1,51 @@
|
||||
ALTER TABLE `FlyerSession`
|
||||
ADD COLUMN `sourceFileName` VARCHAR(191) NULL,
|
||||
ADD COLUMN `sourceMimeType` VARCHAR(191) NULL,
|
||||
ADD COLUMN `sourceFileSize` INTEGER NULL,
|
||||
ADD COLUMN `sourceStorageKey` VARCHAR(191) NULL,
|
||||
ADD COLUMN `sourceData` LONGBLOB NULL;
|
||||
|
||||
ALTER TABLE `FlyerItem`
|
||||
ADD COLUMN `categoryId` INTEGER NULL;
|
||||
|
||||
ALTER TABLE `FlyerItem`
|
||||
ADD CONSTRAINT `FlyerItem_categoryId_fkey`
|
||||
FOREIGN KEY (`categoryId`) REFERENCES `Category`(`id`)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX `FlyerItem_categoryId_idx` ON `FlyerItem`(`categoryId`);
|
||||
|
||||
CREATE TABLE `ShoppingListItem` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`productId` INTEGER NULL,
|
||||
`categoryId` INTEGER NULL,
|
||||
`quantity` DECIMAL(10, 2) NULL,
|
||||
`unit` VARCHAR(191) NULL,
|
||||
`source` VARCHAR(191) NOT NULL DEFAULT 'manual',
|
||||
`status` VARCHAR(191) NOT NULL DEFAULT 'open',
|
||||
`checkedAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `ShoppingListItem_userId_status_idx`(`userId`, `status`),
|
||||
INDEX `ShoppingListItem_productId_unit_status_idx`(`productId`, `unit`, `status`),
|
||||
INDEX `ShoppingListItem_categoryId_idx`(`categoryId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE `ShoppingListItem`
|
||||
ADD CONSTRAINT `ShoppingListItem_userId_fkey`
|
||||
FOREIGN KEY (`userId`) REFERENCES `User`(`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `ShoppingListItem`
|
||||
ADD CONSTRAINT `ShoppingListItem_productId_fkey`
|
||||
FOREIGN KEY (`productId`) REFERENCES `Product`(`id`)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `ShoppingListItem`
|
||||
ADD CONSTRAINT `ShoppingListItem_categoryId_fkey`
|
||||
FOREIGN KEY (`categoryId`) REFERENCES `Category`(`id`)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -32,6 +32,7 @@ model User {
|
||||
unitMappings UnitMapping[]
|
||||
flyerSessions FlyerSession[]
|
||||
flyerSelections FlyerSelection[]
|
||||
shoppingListItems ShoppingListItem[]
|
||||
}
|
||||
|
||||
model Product {
|
||||
@@ -57,16 +58,19 @@ model Product {
|
||||
categoryId Int?
|
||||
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
isPrivate Boolean @default(false)
|
||||
unitMappings UnitMapping[]
|
||||
}
|
||||
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[]
|
||||
children Category[] @relation("CategoryTree")
|
||||
products Product[]
|
||||
flyerItems FlyerItem[]
|
||||
shoppingListItems ShoppingListItem[]
|
||||
|
||||
@@unique([name, parentId])
|
||||
@@index([parentId])
|
||||
@@ -289,6 +293,11 @@ model FlyerSession {
|
||||
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[]
|
||||
@@ -305,6 +314,7 @@ model FlyerItem {
|
||||
rawName String
|
||||
normalizedName String
|
||||
categoryHint String?
|
||||
categoryId Int?
|
||||
price Decimal? @db.Decimal(10, 2)
|
||||
priceUnit String?
|
||||
comparisonPrice Decimal? @db.Decimal(10, 2)
|
||||
@@ -321,10 +331,12 @@ model FlyerItem {
|
||||
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 {
|
||||
@@ -348,3 +360,26 @@ model FlyerSelection {
|
||||
@@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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user