diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/app.dart b/lib/app.dart index 849d129..ba476a4 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -36,8 +36,8 @@ class _RetailAppState extends ConsumerState { return MaterialApp( title: 'Retail POS', debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme(), - darkTheme: AppTheme.darkTheme(), + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, themeMode: themeMode, // Wrap the home with AuthWrapper to require login home: const AuthWrapper( diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index 50d1526..3c67952 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -23,4 +23,17 @@ class AppConstants { static const int minStockThreshold = 5; static const int maxCartItemQuantity = 999; static const double minTransactionAmount = 0.01; + + // Spacing and Sizes + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double borderRadius = 12.0; + static const double buttonHeight = 48.0; + static const double textFieldHeight = 56.0; + + // Animation Durations + static const Duration shortAnimationDuration = Duration(milliseconds: 200); + static const Duration mediumAnimationDuration = Duration(milliseconds: 400); + static const Duration longAnimationDuration = Duration(milliseconds: 600); } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 8a6e4b3..1d0d94e 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,125 +1,298 @@ import 'package:flutter/material.dart'; -import 'colors.dart'; +import 'package:flutter/services.dart'; +import '../constants/app_constants.dart'; -/// Material 3 theme configuration for the app +/// Application theme configuration using Material Design 3 class AppTheme { AppTheme._(); - /// Light theme - static ThemeData lightTheme() { + // Color scheme for light theme + static const ColorScheme _lightColorScheme = ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF1976D2), // Blue + onPrimary: Color(0xFFFFFFFF), + primaryContainer: Color(0xFFE3F2FD), + onPrimaryContainer: Color(0xFF0D47A1), + secondary: Color(0xFF757575), // Grey + onSecondary: Color(0xFFFFFFFF), + secondaryContainer: Color(0xFFE0E0E0), + onSecondaryContainer: Color(0xFF424242), + tertiary: Color(0xFF4CAF50), // Green + onTertiary: Color(0xFFFFFFFF), + tertiaryContainer: Color(0xFFE8F5E8), + onTertiaryContainer: Color(0xFF2E7D32), + error: Color(0xFFD32F2F), + onError: Color(0xFFFFFFFF), + errorContainer: Color(0xFFFFEBEE), + onErrorContainer: Color(0xFFB71C1C), + surface: Color(0xFFFFFFFF), + onSurface: Color(0xFF212121), + surfaceContainerHighest: Color(0xFFF5F5F5), + onSurfaceVariant: Color(0xFF616161), + outline: Color(0xFFBDBDBD), + outlineVariant: Color(0xFFE0E0E0), + shadow: Color(0xFF000000), + scrim: Color(0xFF000000), + inverseSurface: Color(0xFF303030), + onInverseSurface: Color(0xFFF5F5F5), + inversePrimary: Color(0xFF90CAF9), + surfaceTint: Color(0xFF1976D2), + ); + + // Color scheme for dark theme + static const ColorScheme _darkColorScheme = ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFF90CAF9), // Light Blue + onPrimary: Color(0xFF0D47A1), + primaryContainer: Color(0xFF1565C0), + onPrimaryContainer: Color(0xFFE3F2FD), + secondary: Color(0xFFBDBDBD), // Light Grey + onSecondary: Color(0xFF424242), + secondaryContainer: Color(0xFF616161), + onSecondaryContainer: Color(0xFFE0E0E0), + tertiary: Color(0xFF81C784), // Light Green + onTertiary: Color(0xFF2E7D32), + tertiaryContainer: Color(0xFF388E3C), + onTertiaryContainer: Color(0xFFE8F5E8), + error: Color(0xFFEF5350), + onError: Color(0xFFB71C1C), + errorContainer: Color(0xFFD32F2F), + onErrorContainer: Color(0xFFFFEBEE), + surface: Color(0xFF121212), + onSurface: Color(0xFFE0E0E0), + surfaceContainerHighest: Color(0xFF2C2C2C), + onSurfaceVariant: Color(0xFFBDBDBD), + outline: Color(0xFF757575), + outlineVariant: Color(0xFF424242), + shadow: Color(0xFF000000), + scrim: Color(0xFF000000), + inverseSurface: Color(0xFFE0E0E0), + onInverseSurface: Color(0xFF303030), + inversePrimary: Color(0xFF1976D2), + surfaceTint: Color(0xFF90CAF9), + ); + + /// Light theme configuration + static ThemeData get lightTheme { return ThemeData( useMaterial3: true, - brightness: Brightness.light, - colorScheme: ColorScheme.light( - primary: AppColors.primaryLight, - secondary: AppColors.secondaryLight, - tertiary: AppColors.tertiaryLight, - error: AppColors.errorLight, - surface: AppColors.surfaceLight, - onPrimary: AppColors.white, - onSecondary: AppColors.white, - onSurface: AppColors.black, - onError: AppColors.white, - primaryContainer: AppColors.primaryContainer, - secondaryContainer: AppColors.secondaryContainer, - ), - scaffoldBackgroundColor: AppColors.backgroundLight, - appBarTheme: const AppBarTheme( - centerTitle: true, + colorScheme: _lightColorScheme, + scaffoldBackgroundColor: _lightColorScheme.surface, + + // App Bar Theme + appBarTheme: AppBarTheme( elevation: 0, - backgroundColor: AppColors.primaryLight, - foregroundColor: AppColors.white, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + scrolledUnderElevation: 1, + backgroundColor: _lightColorScheme.surface, + foregroundColor: _lightColorScheme.onSurface, + titleTextStyle: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: _lightColorScheme.onSurface, ), + systemOverlayStyle: SystemUiOverlayStyle.dark, ), + + // Elevated Button Theme elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + minimumSize: Size(double.infinity, AppConstants.buttonHeight), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, ), ), ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + minimumSize: Size(0, AppConstants.buttonHeight), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Input Decoration Theme inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: AppColors.grey100, + fillColor: _lightColorScheme.surfaceContainerHighest, + contentPadding: EdgeInsets.all(AppConstants.defaultPadding), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.outline), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.outline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.primaryLight, width: 2), + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.primary, width: 2), ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.error, width: 2), + ), + labelStyle: TextStyle(color: _lightColorScheme.onSurfaceVariant), + hintStyle: TextStyle(color: _lightColorScheme.onSurfaceVariant), + ), + + + // List Tile Theme + listTileTheme: ListTileThemeData( + contentPadding: EdgeInsets.symmetric( + horizontal: AppConstants.defaultPadding, + vertical: AppConstants.smallPadding, + ), + ), + + // Divider Theme + dividerTheme: DividerThemeData( + color: _lightColorScheme.outline, + thickness: 0.5, + ), + + // Progress Indicator Theme + progressIndicatorTheme: ProgressIndicatorThemeData( + color: _lightColorScheme.primary, + ), + + // Snack Bar Theme + snackBarTheme: SnackBarThemeData( + backgroundColor: _lightColorScheme.inverseSurface, + contentTextStyle: TextStyle(color: _lightColorScheme.onInverseSurface), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + behavior: SnackBarBehavior.floating, ), ); } - /// Dark theme - static ThemeData darkTheme() { + /// Dark theme configuration + static ThemeData get darkTheme { return ThemeData( useMaterial3: true, - brightness: Brightness.dark, - colorScheme: ColorScheme.dark( - primary: AppColors.primaryDark, - secondary: AppColors.secondaryDark, - tertiary: AppColors.tertiaryDark, - error: AppColors.errorDark, - surface: AppColors.surfaceDark, - onPrimary: AppColors.black, - onSecondary: AppColors.black, - onSurface: AppColors.white, - onError: AppColors.black, - primaryContainer: AppColors.primaryContainer, - secondaryContainer: AppColors.secondaryContainer, - ), - scaffoldBackgroundColor: AppColors.backgroundDark, - appBarTheme: const AppBarTheme( - centerTitle: true, + colorScheme: _darkColorScheme, + scaffoldBackgroundColor: _darkColorScheme.surface, + + // App Bar Theme + appBarTheme: AppBarTheme( elevation: 0, - backgroundColor: AppColors.backgroundDark, - foregroundColor: AppColors.white, - ), - cardTheme: CardThemeData( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + scrolledUnderElevation: 1, + backgroundColor: _darkColorScheme.surface, + foregroundColor: _darkColorScheme.onSurface, + titleTextStyle: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: _darkColorScheme.onSurface, ), + systemOverlayStyle: SystemUiOverlayStyle.light, ), + + // Elevated Button Theme elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 0, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + minimumSize: Size(double.infinity, AppConstants.buttonHeight), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, ), ), ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + minimumSize: Size(0, AppConstants.buttonHeight), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Input Decoration Theme inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: AppColors.grey800, + fillColor: _darkColorScheme.surfaceContainerHighest, + contentPadding: EdgeInsets.all(AppConstants.defaultPadding), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.outline), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.outline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.primaryDark, width: 2), + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.primary, width: 2), ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.error, width: 2), + ), + labelStyle: TextStyle(color: _darkColorScheme.onSurfaceVariant), + hintStyle: TextStyle(color: _darkColorScheme.onSurfaceVariant), + ), + + + // List Tile Theme + listTileTheme: ListTileThemeData( + contentPadding: EdgeInsets.symmetric( + horizontal: AppConstants.defaultPadding, + vertical: AppConstants.smallPadding, + ), + ), + + // Divider Theme + dividerTheme: DividerThemeData( + color: _darkColorScheme.outline, + thickness: 0.5, + ), + + // Progress Indicator Theme + progressIndicatorTheme: ProgressIndicatorThemeData( + color: _darkColorScheme.primary, + ), + + // Snack Bar Theme + snackBarTheme: SnackBarThemeData( + backgroundColor: _darkColorScheme.inverseSurface, + contentTextStyle: TextStyle(color: _darkColorScheme.onInverseSurface), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + behavior: SnackBarBehavior.floating, ), ); } -} +} \ No newline at end of file diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 3581c3c..d793095 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -164,6 +164,10 @@ class _LoginPageState extends ConsumerState { // Forgot password link TextButton( onPressed: isLoading ? null : _handleForgotPassword, + style: TextButton.styleFrom( + minimumSize: const Size(0, 0), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), child: Text( 'Forgot Password?', style: theme.textTheme.bodyMedium?.copyWith( diff --git a/lib/features/categories/presentation/providers/categories_provider.g.dart b/lib/features/categories/presentation/providers/categories_provider.g.dart index adb91e4..f87207a 100644 --- a/lib/features/categories/presentation/providers/categories_provider.g.dart +++ b/lib/features/categories/presentation/providers/categories_provider.g.dart @@ -8,15 +8,15 @@ part of 'categories_provider.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// Provider for categories list with API-first approach +/// Provider for categories list with online-first approach @ProviderFor(Categories) const categoriesProvider = CategoriesProvider._(); -/// Provider for categories list with API-first approach +/// Provider for categories list with online-first approach final class CategoriesProvider extends $AsyncNotifierProvider> { - /// Provider for categories list with API-first approach + /// Provider for categories list with online-first approach const CategoriesProvider._() : super( from: null, @@ -38,7 +38,7 @@ final class CategoriesProvider String _$categoriesHash() => r'33c33b08f8926e5bbbd112285591c74a3ff0f61c'; -/// Provider for categories list with API-first approach +/// Provider for categories list with online-first approach abstract class _$Categories extends $AsyncNotifier> { FutureOr> build(); diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 114c40d..cb120f9 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -1,169 +1,404 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../widgets/product_selector.dart'; -import '../widgets/cart_summary.dart'; +import '../../../products/presentation/providers/products_provider.dart'; +import '../../../products/presentation/providers/selected_category_provider.dart'; +import '../../../categories/presentation/providers/categories_provider.dart'; import '../providers/cart_provider.dart'; +import '../providers/cart_total_provider.dart'; import '../../domain/entities/cart_item.dart'; +import '../../../../core/widgets/loading_indicator.dart'; +import '../../../../core/widgets/error_widget.dart'; +import '../../../../core/widgets/empty_state.dart'; +import '../../../../core/widgets/optimized_cached_image.dart'; +import '../../../../shared/widgets/price_display.dart'; -/// Home page - POS interface with product selector and cart -class HomePage extends ConsumerWidget { +/// Home page - Quick sale POS interface +class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _HomePageState(); +} + +class _HomePageState extends ConsumerState { + String _searchQuery = ''; + + @override + Widget build(BuildContext context) { + final productsAsync = ref.watch(productsProvider); + final categoriesAsync = ref.watch(categoriesProvider); + final selectedCategory = ref.watch(selectedCategoryProvider); final cartAsync = ref.watch(cartProvider); - final isWideScreen = MediaQuery.of(context).size.width > 600; + final totalData = ref.watch(cartTotalProvider); + final theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - title: const Text('Point of Sale'), - actions: [ - // Cart item count badge - cartAsync.whenOrNull( - data: (items) => items.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Center( - child: Badge( - label: Text('${items.length}'), - child: const Icon(Icons.shopping_cart), - ), - ), - ) - : null, - ) ?? const SizedBox.shrink(), - ], - ), - body: isWideScreen - ? Row( - children: [ - // Product selector on left - Expanded( - flex: 3, - child: ProductSelector( - onProductTap: (product) { - _showAddToCartDialog(context, ref, product); - }, - ), - ), - // Divider - const VerticalDivider(width: 1), - // Cart on right - const Expanded( - flex: 2, - child: CartSummary(), - ), - ], - ) - : Column( - children: [ - // Product selector on top - Expanded( - flex: 2, - child: ProductSelector( - onProductTap: (product) { - _showAddToCartDialog(context, ref, product); - }, - ), - ), - // Divider - const Divider(height: 1), - // Cart on bottom - const Expanded( - flex: 3, - child: CartSummary(), - ), - ], - ), - ); - } + final cartItems = cartAsync.value ?? []; + final itemCount = cartItems.length; - void _showAddToCartDialog( - BuildContext context, - WidgetRef ref, - dynamic product, - ) { - int quantity = 1; + return SafeArea( + bottom: false, + child: Scaffold( + backgroundColor: theme.colorScheme.surfaceContainerLowest, - showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: const Text('Add to Cart'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - product.name, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.remove_circle_outline), - onPressed: quantity > 1 - ? () => setState(() => quantity--) - : null, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - '$quantity', - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: quantity < product.stockQuantity - ? () => setState(() => quantity++) - : null, - ), - ], - ), - if (product.stockQuantity < 5) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'Only ${product.stockQuantity} in stock', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.error, + body: Column( + children: [ + // Search bar + Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + color: theme.colorScheme.surface, + child: TextField( + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: InputDecoration( + hintText: 'Search Menu', + prefixIcon: const Icon(Icons.search, size: 20), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + setState(() { + _searchQuery = ''; + }); + }, + ) + : IconButton( + icon: const Icon(Icons.tune, size: 20), + onPressed: () { + // TODO: Show filters + }, ), + filled: true, + fillColor: theme.colorScheme.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric(vertical: 8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.3), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.3), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 1.5, + ), ), ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + ), ), - FilledButton.icon( - onPressed: () { - // Create cart item from product - final cartItem = CartItem( - productId: product.id, - productName: product.name, - price: product.price, - quantity: quantity, - imageUrl: product.imageUrl, - addedAt: DateTime.now(), - ); - // Add to cart - ref.read(cartProvider.notifier).addItem(cartItem); + // Category filter buttons + categoriesAsync.when( + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + data: (categories) { + if (categories.isEmpty) return const SizedBox.shrink(); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Added ${product.name} to cart'), - duration: const Duration(seconds: 2), + return Container( + height: 75, + padding: const EdgeInsets.symmetric(vertical: 8), + color: theme.colorScheme.surface, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + // All/Favorite category + _CategoryButton( + icon: Icons.star, + label: 'Favorite', + isSelected: selectedCategory == null, + onTap: () { + ref + .read(selectedCategoryProvider.notifier) + .clearSelection(); + }, + ), + const SizedBox(width: 12), + // Category buttons + ...categories.map( + (category) => Padding( + padding: const EdgeInsets.only(right: 12.0), + child: _CategoryButton( + icon: _getCategoryIcon(category.name), + label: category.name, + isSelected: selectedCategory == category.id, + onTap: () { + ref + .read(selectedCategoryProvider.notifier) + .selectCategory(category.id); + }, + ), + ), + ), + ], ), ); }, - icon: const Icon(Icons.add_shopping_cart), - label: const Text('Add'), + ), + + // Products list + Expanded( + child: productsAsync.when( + loading: () => const LoadingIndicator( + message: 'Loading products...', + ), + error: (error, stack) => ErrorDisplay( + message: error.toString(), + onRetry: () => ref.refresh(productsProvider), + ), + data: (products) { + // Filter available products + var availableProducts = + products.where((p) => p.isAvailable).toList(); + + // Apply category filter + if (selectedCategory != null) { + availableProducts = availableProducts + .where((p) => p.categoryId == selectedCategory) + .toList(); + } + + // Apply search filter + if (_searchQuery.isNotEmpty) { + availableProducts = availableProducts.where((p) { + final query = _searchQuery.toLowerCase(); + return p.name.toLowerCase().contains(query) || + (p.description?.toLowerCase().contains(query) ?? false); + }).toList(); + } + + if (availableProducts.isEmpty) { + return EmptyState( + message: _searchQuery.isNotEmpty + ? 'No products found' + : 'No products available', + subMessage: _searchQuery.isNotEmpty + ? 'Try a different search term' + : 'Add products to start selling', + icon: Icons.inventory_2_outlined, + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: availableProducts.length, + itemBuilder: (context, index) { + final product = availableProducts[index]; + + // Find if product is in cart + final cartItem = cartItems.firstWhere( + (item) => item.productId == product.id, + orElse: () => CartItem( + productId: '', + productName: '', + price: 0, + quantity: 0, + addedAt: DateTime.now(), + ), + ); + final isInCart = cartItem.productId.isNotEmpty; + final quantity = isInCart ? cartItem.quantity : 0; + + return _ProductListItem( + product: product, + quantity: quantity, + onAdd: () => _addToCart(product), + onIncrement: isInCart + ? () => ref + .read(cartProvider.notifier) + .updateQuantity(product.id, quantity + 1) + : null, + onDecrement: isInCart + ? () { + if (quantity > 1) { + ref + .read(cartProvider.notifier) + .updateQuantity(product.id, quantity - 1); + } else { + ref + .read(cartProvider.notifier) + .removeItem(product.id); + } + } + : null, + ); + }, + ); + }, + ), + ), + ], + ), + // Bottom bar + bottomNavigationBar: itemCount > 0 + ? Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: FilledButton( + onPressed: () => _proceedToCheckout(), + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const SizedBox(width: 8), + Text( + 'Proceed New Order', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Row( + children: [ + Text( + '$itemCount items', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + const SizedBox(width: 8), + PriceDisplay( + price: totalData.total, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, color: Colors.white), + ], + ), + ], + ), + ), + ), + ) + : null, + ), + ); + } + + void _addToCart(dynamic product) { + final cartItem = CartItem( + productId: product.id, + productName: product.name, + price: product.price, + quantity: 1, + imageUrl: product.imageUrl, + addedAt: DateTime.now(), + ); + + ref.read(cartProvider.notifier).addItem(cartItem); + } + + void _proceedToCheckout() { + // TODO: Navigate to checkout/order detail screen + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Proceeding to checkout...'), + duration: Duration(seconds: 2), + ), + ); + } + + IconData _getCategoryIcon(String categoryName) { + final name = categoryName.toLowerCase(); + if (name.contains('drink') || name.contains('beverage')) { + return Icons.local_cafe; + } else if (name.contains('food') || name.contains('meal')) { + return Icons.restaurant; + } else if (name.contains('dessert') || name.contains('sweet')) { + return Icons.cake; + } else { + return Icons.category; + } + } +} + +/// Category filter button +class _CategoryButton extends StatelessWidget { + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _CategoryButton({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? theme.colorScheme.primary + : Colors.transparent, + width: 1.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + size: 22, + ), + const SizedBox(height: 2), + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), ), ], ), @@ -171,3 +406,134 @@ class HomePage extends ConsumerWidget { ); } } + +/// Product list item +class _ProductListItem extends StatelessWidget { + final dynamic product; + final int quantity; + final VoidCallback onAdd; + final VoidCallback? onIncrement; + final VoidCallback? onDecrement; + + const _ProductListItem({ + required this.product, + required this.quantity, + required this.onAdd, + this.onIncrement, + this.onDecrement, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isInCart = quantity > 0; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isInCart + ? theme.colorScheme.primary.withOpacity(0.3) + : theme.colorScheme.outlineVariant, + width: isInCart ? 2 : 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // Product image + RepaintBoundary( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: product.imageUrl != null + ? OptimizedCachedImage( + key: ValueKey('product_img_${product.id}'), + imageUrl: product.imageUrl!, + width: 60, + height: 60, + fit: BoxFit.cover, + ) + : Container( + width: 60, + height: 60, + color: theme.colorScheme.surfaceContainerHighest, + child: Icon( + Icons.inventory_2, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + const SizedBox(width: 12), + // Product info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + PriceDisplay( + price: product.price, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + // Add/quantity controls + if (!isInCart) + IconButton( + onPressed: onAdd, + icon: const Icon(Icons.add_circle), + iconSize: 32, + color: theme.colorScheme.primary, + ) + else + Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + IconButton( + onPressed: onDecrement, + icon: const Icon(Icons.remove), + iconSize: 20, + color: theme.colorScheme.primary, + ), + Text( + '$quantity', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + IconButton( + onPressed: onIncrement, + icon: const Icon(Icons.add), + iconSize: 20, + color: theme.colorScheme.primary, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/providers/cart_total_provider.dart b/lib/features/home/presentation/providers/cart_total_provider.dart index 4c52dbb..3e325fb 100644 --- a/lib/features/home/presentation/providers/cart_total_provider.dart +++ b/lib/features/home/presentation/providers/cart_total_provider.dart @@ -27,7 +27,7 @@ class CartTotal extends _$CartTotal { // Calculate subtotal final subtotal = items.fold( 0.0, - (sum, item) => sum + item.lineTotal, + (sum, item) => sum + item.total, ); // Calculate tax diff --git a/lib/features/home/presentation/providers/cart_total_provider.g.dart b/lib/features/home/presentation/providers/cart_total_provider.g.dart index 8c6f641..95700b1 100644 --- a/lib/features/home/presentation/providers/cart_total_provider.g.dart +++ b/lib/features/home/presentation/providers/cart_total_provider.g.dart @@ -44,7 +44,7 @@ final class CartTotalProvider } } -String _$cartTotalHash() => r'044f6d4749eec49f9ef4173fc42d149a3841df21'; +String _$cartTotalHash() => r'3e4ed08789743e7149a77047651b5d99e380a696'; /// Cart totals calculation provider diff --git a/lib/features/home/presentation/widgets/cart_bottom_bar.dart b/lib/features/home/presentation/widgets/cart_bottom_bar.dart new file mode 100644 index 0000000..a166869 --- /dev/null +++ b/lib/features/home/presentation/widgets/cart_bottom_bar.dart @@ -0,0 +1,348 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/cart_provider.dart'; +import '../providers/cart_total_provider.dart'; +import '../../../../shared/widgets/price_display.dart'; + +/// Bottom bar showing cart total and checkout button +class CartBottomBar extends ConsumerWidget { + const CartBottomBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final cartAsync = ref.watch(cartProvider); + final totalData = ref.watch(cartTotalProvider); + final theme = Theme.of(context); + + final itemCount = cartAsync.value?.length ?? 0; + final hasItems = itemCount > 0; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: hasItems ? 80 : 0, + child: hasItems + ? Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // Cart icon with badge + Stack( + clipBehavior: Clip.none, + children: [ + Icon( + Icons.shopping_cart, + size: 32, + color: theme.colorScheme.onPrimaryContainer, + ), + Positioned( + right: -8, + top: -8, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: theme.colorScheme.error, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: Center( + child: Text( + '$itemCount', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onError, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(width: 16), + // Total info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$itemCount item${itemCount == 1 ? '' : 's'}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 2), + PriceDisplay( + price: totalData.total, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ], + ), + ), + // View Cart button + OutlinedButton( + onPressed: () { + _showCartBottomSheet(context, ref); + }, + style: OutlinedButton.styleFrom( + foregroundColor: theme.colorScheme.onPrimaryContainer, + side: BorderSide( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + child: const Text('View Cart'), + ), + const SizedBox(width: 8), + // Checkout button + FilledButton.icon( + onPressed: () { + // TODO: Navigate to checkout + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Checkout coming soon!'), + ), + ); + }, + icon: const Icon(Icons.payment), + label: const Text('Checkout'), + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ); + } + + void _showCartBottomSheet(BuildContext context, WidgetRef ref) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) { + return CartBottomSheet(scrollController: scrollController); + }, + ), + ); + } +} + +/// Cart bottom sheet content +class CartBottomSheet extends ConsumerWidget { + final ScrollController scrollController; + + const CartBottomSheet({ + super.key, + required this.scrollController, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final cartAsync = ref.watch(cartProvider); + final totalData = ref.watch(cartTotalProvider); + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Handle bar + Container( + margin: const EdgeInsets.symmetric(vertical: 12), + width: 40, + height: 4, + decoration: BoxDecoration( + color: theme.colorScheme.onSurfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(2), + ), + ), + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Shopping Cart', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (cartAsync.value?.isNotEmpty ?? false) + TextButton.icon( + onPressed: () { + ref.read(cartProvider.notifier).clearCart(); + }, + icon: const Icon(Icons.delete_sweep), + label: const Text('Clear'), + ), + ], + ), + ), + const Divider(), + // Cart items + Expanded( + child: cartAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + data: (items) { + if (items.isEmpty) { + return const Center( + child: Text('Cart is empty'), + ); + } + + return ListView.separated( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: items.length, + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + final item = items[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + title: Text(item.productName), + subtitle: PriceDisplay( + price: item.price, + style: theme.textTheme.bodyMedium, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Quantity controls + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: item.quantity > 1 + ? () => ref + .read(cartProvider.notifier) + .updateQuantity( + item.productId, + item.quantity - 1, + ) + : null, + iconSize: 20, + ), + Text( + '${item.quantity}', + style: theme.textTheme.titleMedium, + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => ref + .read(cartProvider.notifier) + .updateQuantity( + item.productId, + item.quantity + 1, + ), + iconSize: 20, + ), + // Remove button + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => ref + .read(cartProvider.notifier) + .removeItem(item.productId), + color: theme.colorScheme.error, + iconSize: 20, + ), + ], + ), + ); + }, + ); + }, + ), + ), + // Summary + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + border: Border( + top: BorderSide(color: theme.dividerColor), + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Subtotal:', style: theme.textTheme.bodyLarge), + PriceDisplay( + price: totalData.subtotal, + style: theme.textTheme.bodyLarge, + ), + ], + ), + if (totalData.tax > 0) ...[ + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Tax (${(totalData.taxRate * 100).toStringAsFixed(0)}%):', + style: theme.textTheme.bodyLarge, + ), + PriceDisplay( + price: totalData.tax, + style: theme.textTheme.bodyLarge, + ), + ], + ), + ], + const Divider(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total:', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + PriceDisplay( + price: totalData.total, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/pos_product_card.dart b/lib/features/home/presentation/widgets/pos_product_card.dart new file mode 100644 index 0000000..647afcd --- /dev/null +++ b/lib/features/home/presentation/widgets/pos_product_card.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import '../../../products/domain/entities/product.dart'; +import '../../../../shared/widgets/price_display.dart'; +import '../../../../core/widgets/optimized_cached_image.dart'; + +/// POS-specific product card with Add to Cart button +class PosProductCard extends StatelessWidget { + final Product product; + final VoidCallback onAddToCart; + + const PosProductCard({ + super.key, + required this.product, + required this.onAddToCart, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isLowStock = product.stockQuantity < 5; + final isOutOfStock = product.stockQuantity == 0; + + return Card( + clipBehavior: Clip.antiAlias, + elevation: 2, + child: InkWell( + onTap: isOutOfStock ? null : onAddToCart, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Product Image + Expanded( + flex: 3, + child: Stack( + fit: StackFit.expand, + children: [ + product.imageUrl != null + ? OptimizedCachedImage( + imageUrl: product.imageUrl!, + fit: BoxFit.cover, + ) + : Container( + color: theme.colorScheme.surfaceContainerHighest, + child: Icon( + Icons.inventory_2, + size: 48, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + // Stock badge + if (isOutOfStock) + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.error, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'OUT OF STOCK', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onError, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + else if (isLowStock) + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${product.stockQuantity} left', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onErrorContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + // Product Info + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Product Name + Text( + product.name, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + // Price and Add Button Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: PriceDisplay( + price: product.price, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + ), + // Add to Cart Button + FilledButton.icon( + onPressed: isOutOfStock ? null : onAddToCart, + icon: const Icon(Icons.add_shopping_cart, size: 18), + label: const Text('Add'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/product_selector.dart b/lib/features/home/presentation/widgets/product_selector.dart index 3661d51..7957c07 100644 --- a/lib/features/home/presentation/widgets/product_selector.dart +++ b/lib/features/home/presentation/widgets/product_selector.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../products/presentation/providers/products_provider.dart'; -import '../../../products/presentation/widgets/product_card.dart'; import '../../../products/domain/entities/product.dart'; import '../../../../core/widgets/loading_indicator.dart'; import '../../../../core/widgets/error_widget.dart'; import '../../../../core/widgets/empty_state.dart'; +import 'pos_product_card.dart'; /// Product selector widget for POS -class ProductSelector extends ConsumerWidget { +class ProductSelector extends ConsumerStatefulWidget { final void Function(Product)? onProductTap; const ProductSelector({ @@ -17,7 +17,14 @@ class ProductSelector extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _ProductSelectorState(); +} + +class _ProductSelectorState extends ConsumerState { + String _searchQuery = ''; + + @override + Widget build(BuildContext context) { final productsAsync = ref.watch(productsProvider); return Container( @@ -30,6 +37,33 @@ class ProductSelector extends ConsumerWidget { style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), + // Search Bar + TextField( + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: InputDecoration( + hintText: 'Search products...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _searchQuery = ''; + }); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + filled: true, + ), + ), + const SizedBox(height: 16), Expanded( child: productsAsync.when( loading: () => const LoadingIndicator( @@ -50,13 +84,26 @@ class ProductSelector extends ConsumerWidget { } // Filter only available products for POS - final availableProducts = + var availableProducts = products.where((p) => p.isAvailable).toList(); + // Apply search filter + if (_searchQuery.isNotEmpty) { + availableProducts = availableProducts.where((p) { + final query = _searchQuery.toLowerCase(); + return p.name.toLowerCase().contains(query) || + (p.description?.toLowerCase().contains(query) ?? false); + }).toList(); + } + if (availableProducts.isEmpty) { - return const EmptyState( - message: 'No products available', - subMessage: 'All products are currently unavailable', + return EmptyState( + message: _searchQuery.isNotEmpty + ? 'No products found' + : 'No products available', + subMessage: _searchQuery.isNotEmpty + ? 'Try a different search term' + : 'All products are currently unavailable', icon: Icons.inventory_2_outlined, ); } @@ -81,9 +128,9 @@ class ProductSelector extends ConsumerWidget { itemCount: availableProducts.length, itemBuilder: (context, index) { final product = availableProducts[index]; - return GestureDetector( - onTap: () => onProductTap?.call(product), - child: ProductCard(product: product), + return PosProductCard( + product: product, + onAddToCart: () => widget.onProductTap?.call(product), ); }, ); diff --git a/lib/features/products/presentation/providers/products_provider.g.dart b/lib/features/products/presentation/providers/products_provider.g.dart index 1f390f9..bddeaad 100644 --- a/lib/features/products/presentation/providers/products_provider.g.dart +++ b/lib/features/products/presentation/providers/products_provider.g.dart @@ -8,15 +8,15 @@ part of 'products_provider.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// Provider for products list with API-first approach +/// Provider for products list with online-first approach @ProviderFor(Products) const productsProvider = ProductsProvider._(); -/// Provider for products list with API-first approach +/// Provider for products list with online-first approach final class ProductsProvider extends $AsyncNotifierProvider> { - /// Provider for products list with API-first approach + /// Provider for products list with online-first approach const ProductsProvider._() : super( from: null, @@ -38,7 +38,7 @@ final class ProductsProvider String _$productsHash() => r'0ff8c2de46bb4b1e29678cc811ec121c9fb4c8eb'; -/// Provider for products list with API-first approach +/// Provider for products list with online-first approach abstract class _$Products extends $AsyncNotifier> { FutureOr> build();