diff --git a/lib/features/categories/presentation/pages/category_detail_page.dart b/lib/features/categories/presentation/pages/category_detail_page.dart index 55f99a5..78efc03 100644 --- a/lib/features/categories/presentation/pages/category_detail_page.dart +++ b/lib/features/categories/presentation/pages/category_detail_page.dart @@ -159,9 +159,6 @@ class _CategoryDetailPageState extends ConsumerState { itemBuilder: (context, index) { return ProductListItem( product: products[index], - onTap: () { - // TODO: Navigate to product detail or add to cart - }, ); }, ); diff --git a/lib/features/products/data/datasources/product_local_datasource.dart b/lib/features/products/data/datasources/product_local_datasource.dart index 33006db..22aaa68 100644 --- a/lib/features/products/data/datasources/product_local_datasource.dart +++ b/lib/features/products/data/datasources/product_local_datasource.dart @@ -6,6 +6,7 @@ abstract class ProductLocalDataSource { Future> getAllProducts(); Future getProductById(String id); Future cacheProducts(List products); + Future updateProduct(ProductModel product); Future clearProducts(); } @@ -30,6 +31,11 @@ class ProductLocalDataSourceImpl implements ProductLocalDataSource { await box.putAll(productMap); } + @override + Future updateProduct(ProductModel product) async { + await box.put(product.id, product); + } + @override Future clearProducts() async { await box.clear(); diff --git a/lib/features/products/presentation/pages/batch_update_page.dart b/lib/features/products/presentation/pages/batch_update_page.dart new file mode 100644 index 0000000..cda4009 --- /dev/null +++ b/lib/features/products/presentation/pages/batch_update_page.dart @@ -0,0 +1,372 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/product.dart'; +import '../../data/models/product_model.dart'; +import '../providers/products_provider.dart'; +import '../../data/providers/product_providers.dart'; + +/// Batch update page for updating multiple products +class BatchUpdatePage extends ConsumerStatefulWidget { + final List selectedProducts; + + const BatchUpdatePage({ + super.key, + required this.selectedProducts, + }); + + @override + ConsumerState createState() => _BatchUpdatePageState(); +} + +class _BatchUpdatePageState extends ConsumerState { + final _formKey = GlobalKey(); + late List _productsData; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + // Initialize update data for each product + _productsData = widget.selectedProducts.map((product) { + return ProductUpdateData( + product: product, + priceController: TextEditingController(text: product.price.toStringAsFixed(2)), + stock: product.stockQuantity, + ); + }).toList(); + } + + @override + void dispose() { + for (var data in _productsData) { + data.priceController.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Edit ${widget.selectedProducts.length} Products'), + actions: [ + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ), + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + children: [ + Expanded( + child: Text( + 'Product', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + SizedBox( + width: 70, + child: Text( + 'Price', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 80, + child: Text( + 'Stock', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ], + ), + ), + + // Products list + Expanded( + child: ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: _productsData.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemBuilder: (context, index) { + return _buildProductItem(_productsData[index]); + }, + ), + ), + + // Action buttons + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _isLoading ? null : () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + onPressed: _isLoading ? null : _handleSave, + child: const Text('Save Changes'), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + /// Build product item + Widget _buildProductItem(ProductUpdateData data) { + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Product info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.product.name, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '\$${data.product.price.toStringAsFixed(2)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 4), + + // Price field + SizedBox( + width: 70, + child: TextFormField( + controller: data.priceController, + decoration: const InputDecoration( + prefixText: '\$', + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 6), + border: OutlineInputBorder(), + ), + style: Theme.of(context).textTheme.bodySmall, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), + ], + validator: (value) { + if (value == null || value.isEmpty) return ''; + final number = double.tryParse(value); + if (number == null || number < 0) return ''; + return null; + }, + ), + ), + const SizedBox(width: 4), + + // Stock controls + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Decrease button + InkWell( + onTap: data.stock > 0 + ? () { + setState(() { + data.stock--; + }); + } + : null, + child: Container( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.remove, + size: 18, + color: data.stock > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor, + ), + ), + ), + + // Stock count + Container( + constraints: const BoxConstraints(minWidth: 35), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${data.stock}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Increase button + InkWell( + onTap: () { + setState(() { + data.stock++; + }); + }, + child: Container( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.add, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + /// Handle save + Future _handleSave() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final localDataSource = ref.read(productLocalDataSourceProvider); + + // Update each product + for (var data in _productsData) { + final newPrice = double.parse(data.priceController.text); + final newStock = data.stock; + + // Create updated product model + final updatedProduct = ProductModel( + id: data.product.id, + name: data.product.name, + description: data.product.description, + price: newPrice, + imageUrl: data.product.imageUrl, + categoryId: data.product.categoryId, + stockQuantity: newStock, + isAvailable: data.product.isAvailable, + createdAt: data.product.createdAt, + updatedAt: DateTime.now(), + ); + + // Update in local storage + await localDataSource.updateProduct(updatedProduct); + } + + // Refresh products provider + ref.invalidate(productsProvider); + + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${_productsData.length} product${_productsData.length == 1 ? '' : 's'} updated successfully', + ), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating products: $e'), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} + +/// Product update data class +class ProductUpdateData { + final Product product; + final TextEditingController priceController; + int stock; + + ProductUpdateData({ + required this.product, + required this.priceController, + required this.stock, + }); +} diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart new file mode 100644 index 0000000..33c7a7c --- /dev/null +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -0,0 +1,419 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:intl/intl.dart'; +import '../../domain/entities/product.dart'; +import '../../../categories/presentation/providers/categories_provider.dart'; +import '../../../../shared/widgets/price_display.dart'; + +/// Product detail page showing full product information +class ProductDetailPage extends ConsumerWidget { + final Product product; + + const ProductDetailPage({ + super.key, + required this.product, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final categoriesAsync = ref.watch(categoriesProvider); + + // Find category name + final categoryName = categoriesAsync.whenOrNull( + data: (categories) { + final category = categories.firstWhere( + (cat) => cat.id == product.categoryId, + orElse: () => categories.first, + ); + return category.name; + }, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Product Details'), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // TODO: Navigate to product edit page + }, + tooltip: 'Edit product', + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Product Image + _buildProductImage(context), + + // Product Info Section + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Product Name + Text( + product.name, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + // Category Badge + if (categoryName != null) + Chip( + avatar: const Icon(Icons.category, size: 16), + label: Text(categoryName), + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + ), + const SizedBox(height: 16), + + // Price + Row( + children: [ + Text( + 'Price:', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 8), + PriceDisplay( + price: product.price, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Stock Information + _buildStockSection(context), + const SizedBox(height: 24), + + // Description Section + _buildDescriptionSection(context), + const SizedBox(height: 24), + + // Additional Information + _buildAdditionalInfo(context), + const SizedBox(height: 24), + + // Action Buttons + _buildActionButtons(context), + ], + ), + ), + ], + ), + ), + ); + } + + /// Build product image section + Widget _buildProductImage(BuildContext context) { + return Hero( + tag: 'product-${product.id}', + child: Container( + width: double.infinity, + height: 300, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + child: product.imageUrl != null + ? CachedNetworkImage( + imageUrl: product.imageUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Center( + child: Icon( + Icons.image_not_supported, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + : Center( + child: Icon( + Icons.inventory_2_outlined, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ); + } + + /// Build stock information section + Widget _buildStockSection(BuildContext context) { + final stockColor = _getStockColor(context); + final stockStatus = _getStockStatus(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.inventory, + color: stockColor, + ), + const SizedBox(width: 8), + Text( + 'Stock Information', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Quantity', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + '${product.stockQuantity} units', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: stockColor, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + stockStatus, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + product.isAvailable ? Icons.check_circle : Icons.cancel, + size: 16, + color: product.isAvailable ? Colors.green : Colors.red, + ), + const SizedBox(width: 8), + Text( + product.isAvailable ? 'Available for sale' : 'Not available', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: product.isAvailable ? Colors.green : Colors.red, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } + + /// Build description section + Widget _buildDescriptionSection(BuildContext context) { + if (product.description.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + product.description, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ); + } + + /// Build additional information section + Widget _buildAdditionalInfo(BuildContext context) { + final dateFormat = DateFormat('MMM dd, yyyy'); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Additional Information', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + _buildInfoRow( + context, + icon: Icons.fingerprint, + label: 'Product ID', + value: product.id, + ), + const Divider(height: 24), + _buildInfoRow( + context, + icon: Icons.calendar_today, + label: 'Created', + value: dateFormat.format(product.createdAt), + ), + const Divider(height: 24), + _buildInfoRow( + context, + icon: Icons.update, + label: 'Last Updated', + value: dateFormat.format(product.updatedAt), + ), + ], + ), + ), + ); + } + + /// Build info row + Widget _buildInfoRow( + BuildContext context, { + required IconData icon, + required String label, + required String value, + }) { + return Row( + children: [ + Icon( + icon, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ); + } + + /// Build action buttons + Widget _buildActionButtons(BuildContext context) { + return Column( + children: [ + // Add to Cart Button + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: product.isAvailable && product.stockQuantity > 0 + ? () { + // TODO: Add to cart functionality + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${product.name} added to cart'), + behavior: SnackBarBehavior.floating, + ), + ); + } + : null, + icon: const Icon(Icons.shopping_cart), + label: const Text('Add to Cart'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + const SizedBox(height: 12), + // Stock Adjustment Button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + // TODO: Navigate to stock adjustment + }, + icon: const Icon(Icons.inventory_2), + label: const Text('Adjust Stock'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ], + ); + } + + /// Get stock color based on quantity + Color _getStockColor(BuildContext context) { + if (product.stockQuantity == 0) { + return Colors.red; + } else if (product.stockQuantity < 5) { + return Colors.orange; + } else { + return Colors.green; + } + } + + /// Get stock status text + String _getStockStatus() { + if (product.stockQuantity == 0) { + return 'Out of Stock'; + } else if (product.stockQuantity < 5) { + return 'Low Stock'; + } else { + return 'In Stock'; + } + } +} diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 7363e32..3daaf98 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -3,11 +3,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../widgets/product_grid.dart'; import '../widgets/product_search_bar.dart'; import '../widgets/product_list_item.dart'; +import '../widgets/product_card.dart'; import '../providers/products_provider.dart'; import '../providers/selected_category_provider.dart' as product_providers; import '../providers/filtered_products_provider.dart'; import '../../domain/entities/product.dart'; import '../../../categories/presentation/providers/categories_provider.dart'; +import 'batch_update_page.dart'; /// View mode for products display enum ViewMode { grid, list } @@ -24,6 +26,10 @@ class _ProductsPageState extends ConsumerState { ProductSortOption _sortOption = ProductSortOption.nameAsc; ViewMode _viewMode = ViewMode.grid; + // Multi-select mode + bool _isSelectionMode = false; + final Set _selectedProductIds = {}; + @override Widget build(BuildContext context) { final categoriesAsync = ref.watch(categoriesProvider); @@ -46,27 +52,101 @@ class _ProductsPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Products'), + leading: _isSelectionMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + _isSelectionMode = false; + _selectedProductIds.clear(); + }); + }, + ) + : null, + title: _isSelectionMode + ? Text('${_selectedProductIds.length} selected') + : const Text('Products'), actions: [ - // View mode toggle - IconButton( - icon: Icon( - _viewMode == ViewMode.grid - ? Icons.view_list_rounded - : Icons.grid_view_rounded, + if (_isSelectionMode) ...[ + // Select All / Deselect All + IconButton( + icon: Icon( + _selectedProductIds.length == filteredProducts.length + ? Icons.deselect + : Icons.select_all, + ), + onPressed: () { + setState(() { + if (_selectedProductIds.length == filteredProducts.length) { + _selectedProductIds.clear(); + } else { + _selectedProductIds.addAll( + filteredProducts.map((p) => p.id), + ); + } + }); + }, + tooltip: _selectedProductIds.length == filteredProducts.length + ? 'Deselect all' + : 'Select all', ), - onPressed: () { - setState(() { - _viewMode = - _viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid; - }); - }, - tooltip: _viewMode == ViewMode.grid - ? 'Switch to list view' - : 'Switch to grid view', - ), - // Sort button - PopupMenuButton( + // Batch Update button + IconButton( + icon: const Icon(Icons.edit), + onPressed: _selectedProductIds.isEmpty + ? null + : () { + final selectedProducts = filteredProducts + .where((p) => _selectedProductIds.contains(p.id)) + .toList(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BatchUpdatePage( + selectedProducts: selectedProducts, + ), + ), + ).then((_) { + setState(() { + _isSelectionMode = false; + _selectedProductIds.clear(); + }); + }); + }, + tooltip: 'Batch update', + ), + ] else ...[ + // Multi-select mode button + IconButton( + icon: const Icon(Icons.checklist), + onPressed: filteredProducts.isEmpty + ? null + : () { + setState(() { + _isSelectionMode = true; + }); + }, + tooltip: 'Select products', + ), + // View mode toggle + IconButton( + icon: Icon( + _viewMode == ViewMode.grid + ? Icons.view_list_rounded + : Icons.grid_view_rounded, + ), + onPressed: () { + setState(() { + _viewMode = + _viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid; + }); + }, + tooltip: _viewMode == ViewMode.grid + ? 'Switch to list view' + : 'Switch to grid view', + ), + // Sort button + PopupMenuButton( icon: const Icon(Icons.sort), tooltip: 'Sort products', onSelected: (option) { @@ -136,7 +216,8 @@ class _ProductsPageState extends ConsumerState { ), ), ], - ), + ), + ], ], bottom: PreferredSize( preferredSize: const Size.fromHeight(120), @@ -220,9 +301,7 @@ class _ProductsPageState extends ConsumerState { // Product grid or list Expanded( child: _viewMode == ViewMode.grid - ? ProductGrid( - sortOption: _sortOption, - ) + ? _buildGridView() : _buildListView(), ), ], @@ -248,58 +327,154 @@ class _ProductsPageState extends ConsumerState { ); } + /// Build grid view for products + Widget _buildGridView() { + if (_isSelectionMode) { + final filteredProducts = ref.watch(filteredProductsProvider); + final sortedProducts = _sortProducts(filteredProducts, _sortOption); + + if (sortedProducts.isEmpty) { + return _buildEmptyState(); + } + + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: sortedProducts.length, + itemBuilder: (context, index) { + final product = sortedProducts[index]; + final isSelected = _selectedProductIds.contains(product.id); + + return GestureDetector( + onTap: () { + setState(() { + if (isSelected) { + _selectedProductIds.remove(product.id); + } else { + _selectedProductIds.add(product.id); + } + }); + }, + child: Stack( + children: [ + ProductCard(product: product), + Positioned( + top: 8, + right: 8, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey, + width: 2, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Icon( + isSelected ? Icons.check : null, + size: 16, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + return ProductGrid(sortOption: _sortOption); + } + /// Build list view for products Widget _buildListView() { final filteredProducts = ref.watch(filteredProductsProvider); - - // Apply sorting final sortedProducts = _sortProducts(filteredProducts, _sortOption); if (sortedProducts.isEmpty) { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 64, - color: Colors.grey, - ), - SizedBox(height: 16), - Text( - 'No products found', - style: TextStyle( - fontSize: 16, - color: Colors.grey, - ), - ), - SizedBox(height: 8), - Text( - 'Try adjusting your filters', - style: TextStyle( - fontSize: 14, - color: Colors.grey, - ), - ), - ], - ), - ); + return _buildEmptyState(); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: sortedProducts.length, itemBuilder: (context, index) { - return ProductListItem( - product: sortedProducts[index], - onTap: () { - // TODO: Navigate to product detail or add to cart - }, - ); + final product = sortedProducts[index]; + + if (_isSelectionMode) { + final isSelected = _selectedProductIds.contains(product.id); + + return CheckboxListTile( + value: isSelected, + onChanged: (value) { + setState(() { + if (value == true) { + _selectedProductIds.add(product.id); + } else { + _selectedProductIds.remove(product.id); + } + }); + }, + secondary: SizedBox( + width: 60, + height: 60, + child: ProductCard(product: product), + ), + title: Text(product.name), + subtitle: Text('\$${product.price.toStringAsFixed(2)} • Stock: ${product.stockQuantity}'), + ); + } + + return ProductListItem(product: product); }, ); } + /// Build empty state + Widget _buildEmptyState() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No products found', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + SizedBox(height: 8), + Text( + 'Try adjusting your filters', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ); + } + /// Sort products based on selected option List _sortProducts(List products, ProductSortOption option) { final sorted = List.from(products); diff --git a/lib/features/products/presentation/widgets/product_card.dart b/lib/features/products/presentation/widgets/product_card.dart index e8210ff..5e5ac02 100644 --- a/lib/features/products/presentation/widgets/product_card.dart +++ b/lib/features/products/presentation/widgets/product_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../domain/entities/product.dart'; +import '../pages/product_detail_page.dart'; import '../../../../shared/widgets/price_display.dart'; /// Product card widget @@ -18,7 +19,13 @@ class ProductCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { - // TODO: Navigate to product details or add to cart + // Navigate to product detail page + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProductDetailPage(product: product), + ), + ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/products/presentation/widgets/product_list_item.dart b/lib/features/products/presentation/widgets/product_list_item.dart index 9f86958..a8580eb 100644 --- a/lib/features/products/presentation/widgets/product_list_item.dart +++ b/lib/features/products/presentation/widgets/product_list_item.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../domain/entities/product.dart'; +import '../pages/product_detail_page.dart'; import '../../../../shared/widgets/price_display.dart'; /// Product list item widget for list view @@ -19,7 +20,16 @@ class ProductListItem extends StatelessWidget { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: InkWell( - onTap: onTap, + onTap: onTap ?? + () { + // Navigate to product detail page + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProductDetailPage(product: product), + ), + ); + }, child: Padding( padding: const EdgeInsets.all(12.0), child: Row(