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:
@@ -2,7 +2,7 @@ class AuthApiPaths {
|
||||
static const login = '/auth/login';
|
||||
}
|
||||
|
||||
class ProductApiPaths {
|
||||
class ProductApiPaths {
|
||||
static const list = '/products';
|
||||
static const mine = '/products/mine';
|
||||
static const createPrivate = '/products/private';
|
||||
@@ -13,7 +13,8 @@ class ProductApiPaths {
|
||||
static const deleted = '/products/deleted';
|
||||
static const merge = '/products/merge';
|
||||
static const mergePrivate = '/products/private/merge';
|
||||
static String updateMineCategory(int id) => '/products/mine/$id/category';
|
||||
static String updateMineCategory(int id) => '/products/mine/$id/category';
|
||||
static const backfillMineCategories = '/products/mine/backfill-categories';
|
||||
static String mergePreview(int sourceProductId, int targetProductId) =>
|
||||
'/products/merge-preview?sourceProductId=$sourceProductId&targetProductId=$targetProductId';
|
||||
static String setStatus(int id) => '/products/$id/status';
|
||||
@@ -42,13 +43,22 @@ class FlyerImportApiPaths {
|
||||
static const parse = '/flyer-import/parse';
|
||||
static const latestSession = '/flyer-import/sessions/latest';
|
||||
static String bySession(int sessionId) => '/flyer-import/sessions/$sessionId';
|
||||
static String sourceBySession(int sessionId) => '/flyer-import/sessions/$sessionId/source';
|
||||
static String patchItem(int sessionId, int itemId) => '/flyer-import/sessions/$sessionId/items/$itemId';
|
||||
}
|
||||
|
||||
class FlyerSelectionApiPaths {
|
||||
static String bySession(int sessionId) => '/flyer-sessions/$sessionId/selections';
|
||||
static String bulkBySession(int sessionId) => '/flyer-sessions/$sessionId/selections/bulk';
|
||||
static String planToShoppingListBySession(int sessionId) =>
|
||||
'/flyer-sessions/$sessionId/selections/plan-to-shopping-list';
|
||||
static const open = '/flyer-selections/open';
|
||||
}
|
||||
|
||||
class ShoppingListApiPaths {
|
||||
static const items = '/shopping-list/items';
|
||||
static String updateStatus(int itemId) => '/shopping-list/items/$itemId/status';
|
||||
}
|
||||
|
||||
class HelpTextApiPaths {
|
||||
static String byKey(String key) => '/help-texts/${Uri.encodeComponent(key)}';
|
||||
|
||||
@@ -21,17 +21,19 @@ import '../../features/inventory/presentation/consume_inventory_screen.dart';
|
||||
import '../../features/inventory/presentation/consumption_history_screen.dart';
|
||||
import '../../features/meal_plan/presentation/meal_plan_screen.dart';
|
||||
import '../../features/pantry/presentation/pantry_screen.dart';
|
||||
import '../../features/import/presentation/import_screen.dart';
|
||||
import '../../features/admin/presentation/admin_screen.dart';
|
||||
import '../../features/import/presentation/import_screen.dart';
|
||||
import '../../features/shopping_list/presentation/shopping_list_screen.dart';
|
||||
import '../../features/admin/presentation/admin_screen.dart';
|
||||
|
||||
int? _shellBranchIndexForPath(String path) {
|
||||
if (path.startsWith('/recipes')) return 0;
|
||||
if (path.startsWith('/inventory')) return 1;
|
||||
if (path.startsWith('/matsedel')) return 2;
|
||||
if (path.startsWith('/baslager')) return 3;
|
||||
if (path.startsWith('/import')) return 4;
|
||||
if (path.startsWith('/profile')) return 5;
|
||||
if (path.startsWith('/admin')) return 6;
|
||||
if (path.startsWith('/import')) return 4;
|
||||
if (path.startsWith('/inkopslista')) return 5;
|
||||
if (path.startsWith('/profile')) return 6;
|
||||
if (path.startsWith('/admin')) return 7;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -242,18 +244,26 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/import',
|
||||
builder: (context, state) => const ImportScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/import',
|
||||
builder: (context, state) => const ImportScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/inkopslista',
|
||||
builder: (context, state) => const ShoppingListScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -49,13 +49,19 @@ class AppShell extends ConsumerWidget {
|
||||
icon: Icons.storefront_outlined,
|
||||
label: 'Baslager',
|
||||
),
|
||||
_AppDestination(
|
||||
path: '/import',
|
||||
title: 'Importera',
|
||||
icon: Icons.upload_file_outlined,
|
||||
label: 'Importera',
|
||||
),
|
||||
];
|
||||
_AppDestination(
|
||||
path: '/import',
|
||||
title: 'Importera',
|
||||
icon: Icons.upload_file_outlined,
|
||||
label: 'Importera',
|
||||
),
|
||||
_AppDestination(
|
||||
path: '/inkopslista',
|
||||
title: 'Inköpslista',
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
label: 'Inköpslista',
|
||||
),
|
||||
];
|
||||
|
||||
List<_AppDestination> _destinations() => _baseDestinations;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user