diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index bad8406..ef21d05 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -194,7 +194,10 @@ class _LoginPageState extends ConsumerState { TextButton(onPressed: () { context.pushNamed(RouteNames.otpVerification); - }, child: Text('otp')) + }, child: Text('otp')), + TextButton(onPressed: () { + context.pushReplacementNamed(RouteNames.home); + }, child: Text('home')) ], ), ), diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 0687218..a16210d 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -13,6 +13,7 @@ import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/products/presentation/providers/categories_provider.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart'; import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart'; +import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; import 'package:worker/features/products/presentation/widgets/product_grid.dart'; import 'package:worker/features/products/presentation/widgets/product_search_bar.dart'; import 'package:worker/generated/l10n/app_localizations.dart'; @@ -37,6 +38,7 @@ class ProductsPage extends ConsumerWidget { return Scaffold( backgroundColor: const Color(0xFFF4F6F8), // Match HTML background + endDrawer: const ProductFilterDrawer(), appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), @@ -65,12 +67,55 @@ class ProductsPage extends ConsumerWidget { const SizedBox(width: AppSpacing.sm), ], ), - body: Column( - children: [ - // Search Bar - const SizedBox(height: AppSpacing.sm), - const ProductSearchBar(), - const SizedBox(height: AppSpacing.sm), + body: Builder( + builder: (BuildContext scaffoldContext) { + return Column( + children: [ + // Search Bar with Filter Button + Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + children: [ + // Search Bar (Expanded) + const Expanded( + child: ProductSearchBar(), + ), + const SizedBox(width: 8), + // Filter Button + SizedBox( + height: InputFieldSpecs.height, + child: OutlinedButton.icon( + onPressed: () { + // Open filter drawer from right + Scaffold.of(scaffoldContext).openEndDrawer(); + }, + icon: const Icon(Icons.filter_list, size: 20), + label: const Text( + 'Lọc', + style: TextStyle(fontSize: 12), + ), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.grey900, + side: const BorderSide( + color: AppColors.white, + width: 0, + ), + backgroundColor: AppColors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + InputFieldSpecs.borderRadius, + ), + ), + ), + ), + ), + ], + ), + ), // Category Filter Chips categoriesAsync.when( @@ -82,45 +127,48 @@ class ProductsPage extends ConsumerWidget { error: (error, stack) => const SizedBox.shrink(), ), - const SizedBox(height: AppSpacing.sm), + const SizedBox(height: AppSpacing.sm), - // Products Grid - Expanded( - child: productsAsync.when( - data: (products) { - if (products.isEmpty) { - return _buildEmptyState(context, l10n); - } + // Products Grid + Expanded( + child: productsAsync.when( + data: (products) { + if (products.isEmpty) { + return _buildEmptyState(context, l10n); + } - return ProductGrid( - products: products, - onProductTap: (product) { - // Navigate to product detail page - context.push('/products/${product.productId}'); - }, - onAddToCart: (product) { - // Add to cart - ref.read(cartProvider.notifier).addToCart(product); + return ProductGrid( + products: products, + onProductTap: (product) { + // Navigate to product detail page + context.push('/products/${product.productId}'); + }, + onAddToCart: (product) { + // Add to cart + ref.read(cartProvider.notifier).addToCart(product); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${product.name} đã thêm vào giỏ hàng'), - duration: const Duration(seconds: 2), - action: SnackBarAction( - label: 'Xem', - onPressed: () => context.go(RouteNames.cart), - ), - ), + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('${product.name} đã thêm vào giỏ hàng'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + label: 'Xem', + onPressed: () => context.go(RouteNames.cart), + ), + ), + ); + }, ); }, - ); - }, - loading: () => _buildLoadingState(), - error: (error, stack) => - _buildErrorState(context, l10n, error, ref), - ), - ), - ], + loading: () => _buildLoadingState(), + error: (error, stack) => + _buildErrorState(context, l10n, error, ref), + ), + ), + ], + ); + }, ), ); } diff --git a/lib/features/products/presentation/providers/product_filters_provider.dart b/lib/features/products/presentation/providers/product_filters_provider.dart new file mode 100644 index 0000000..382d671 --- /dev/null +++ b/lib/features/products/presentation/providers/product_filters_provider.dart @@ -0,0 +1,132 @@ +/// Provider: Product Filters State +/// +/// Manages product filter selections. +library; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Product Filters State +class ProductFiltersState { + final Set productLines; + final Set spaces; + final Set sizes; + final Set surfaces; + final Set brands; + + const ProductFiltersState({ + this.productLines = const {}, + this.spaces = const {}, + this.sizes = const {}, + this.surfaces = const {}, + this.brands = const {}, + }); + + ProductFiltersState copyWith({ + Set? productLines, + Set? spaces, + Set? sizes, + Set? surfaces, + Set? brands, + }) { + return ProductFiltersState( + productLines: productLines ?? this.productLines, + spaces: spaces ?? this.spaces, + sizes: sizes ?? this.sizes, + surfaces: surfaces ?? this.surfaces, + brands: brands ?? this.brands, + ); + } + + /// Get total filter count + int get totalCount => + productLines.length + + spaces.length + + sizes.length + + surfaces.length + + brands.length; + + /// Check if any filters are active + bool get hasActiveFilters => totalCount > 0; + + /// Reset all filters + ProductFiltersState reset() { + return const ProductFiltersState(); + } +} + +/// Product Filters Notifier +class ProductFiltersNotifier extends Notifier { + @override + ProductFiltersState build() => const ProductFiltersState(); + + /// Toggle product line filter + void toggleProductLine(String value) { + final newSet = Set.from(state.productLines); + if (newSet.contains(value)) { + newSet.remove(value); + } else { + newSet.add(value); + } + state = state.copyWith(productLines: newSet); + } + + /// Toggle space filter + void toggleSpace(String value) { + final newSet = Set.from(state.spaces); + if (newSet.contains(value)) { + newSet.remove(value); + } else { + newSet.add(value); + } + state = state.copyWith(spaces: newSet); + } + + /// Toggle size filter + void toggleSize(String value) { + final newSet = Set.from(state.sizes); + if (newSet.contains(value)) { + newSet.remove(value); + } else { + newSet.add(value); + } + state = state.copyWith(sizes: newSet); + } + + /// Toggle surface filter + void toggleSurface(String value) { + final newSet = Set.from(state.surfaces); + if (newSet.contains(value)) { + newSet.remove(value); + } else { + newSet.add(value); + } + state = state.copyWith(surfaces: newSet); + } + + /// Toggle brand filter + void toggleBrand(String value) { + final newSet = Set.from(state.brands); + if (newSet.contains(value)) { + newSet.remove(value); + } else { + newSet.add(value); + } + state = state.copyWith(brands: newSet); + } + + /// Reset all filters + void reset() { + state = const ProductFiltersState(); + } + + /// Apply filters (placeholder for future implementation) + void apply() { + // TODO: Trigger products provider refresh with filters + } +} + +/// Product Filters Provider +final productFiltersProvider = + NotifierProvider( + ProductFiltersNotifier.new, +); diff --git a/lib/features/products/presentation/widgets/product_filter_drawer.dart b/lib/features/products/presentation/widgets/product_filter_drawer.dart new file mode 100644 index 0000000..4bb194a --- /dev/null +++ b/lib/features/products/presentation/widgets/product_filter_drawer.dart @@ -0,0 +1,311 @@ +/// Widget: Product Filter Drawer +/// +/// Right side drawer with product filtering options. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:worker/core/constants/ui_constants.dart'; +import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/products/presentation/providers/product_filters_provider.dart'; + +/// Product Filter Drawer Widget +/// +/// A drawer that slides from the right with filter options: +/// - Dòng sản phẩm (Product Line) +/// - Không gian (Space) +/// - Kích thước (Size) +/// - Bề mặt (Surface) +/// - Thương hiệu (Brand) +class ProductFilterDrawer extends ConsumerWidget { + const ProductFilterDrawer({super.key}); + + // Filter options (from HTML) + static const List _productLines = [ + FilterOption(value: 'tam-lon', label: 'Tấm lớn'), + FilterOption(value: 'third-firing', label: 'Third-Firing'), + FilterOption(value: 'outdoor', label: 'Outdoor'), + FilterOption(value: 'van-da', label: 'Vân đá'), + FilterOption(value: 'xi-mang', label: 'Xi măng'), + FilterOption(value: 'van-go', label: 'Vân gỗ'), + FilterOption(value: 'xuong-trang', label: 'Xương trắng'), + FilterOption(value: 'cam-thach', label: 'Cẩm thạch'), + ]; + + static const List _spaces = [ + FilterOption(value: 'phong-khach', label: 'Phòng khách'), + FilterOption(value: 'phong-ngu', label: 'Phòng ngủ'), + FilterOption(value: 'phong-tam', label: 'Phòng tắm'), + FilterOption(value: 'nha-bep', label: 'Nhà bếp'), + FilterOption(value: 'khong-gian-khac', label: 'Không gian khác'), + ]; + + static const List _sizes = [ + FilterOption(value: '200x1600', label: '200x1600'), + FilterOption(value: '1200x2400', label: '1200x2400'), + FilterOption(value: '7500x1500', label: '7500x1500'), + FilterOption(value: '1200x1200', label: '1200x1200'), + FilterOption(value: '600x1200', label: '600x1200'), + FilterOption(value: '450x900', label: '450x900'), + ]; + + static const List _surfaces = [ + FilterOption(value: 'satin', label: 'SATIN'), + FilterOption(value: 'honed', label: 'HONED'), + FilterOption(value: 'matt', label: 'MATT'), + FilterOption(value: 'polish', label: 'POLISH'), + FilterOption(value: 'babyskin', label: 'BABYSKIN'), + ]; + + static const List _brands = [ + FilterOption(value: 'eurotile', label: 'Eurotile'), + FilterOption(value: 'vasta-stone', label: 'Vasta Stone'), + ]; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final filtersState = ref.watch(productFiltersProvider); + + return Drawer( + child: SafeArea( + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColors.grey100, width: 1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Bộ lọc sản phẩm', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + color: AppColors.grey500, + ), + ], + ), + ), + + // Filter Options (Scrollable) + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Dòng sản phẩm + _buildFilterGroup( + title: 'Dòng sản phẩm', + options: _productLines, + selectedValues: filtersState.productLines, + onToggle: (value) => ref + .read(productFiltersProvider.notifier) + .toggleProductLine(value), + ), + + const SizedBox(height: AppSpacing.lg), + + // Không gian + _buildFilterGroup( + title: 'Không gian', + options: _spaces, + selectedValues: filtersState.spaces, + onToggle: (value) => ref + .read(productFiltersProvider.notifier) + .toggleSpace(value), + ), + + const SizedBox(height: AppSpacing.lg), + + // Kích thước + _buildFilterGroup( + title: 'Kích thước', + options: _sizes, + selectedValues: filtersState.sizes, + onToggle: (value) => ref + .read(productFiltersProvider.notifier) + .toggleSize(value), + ), + + const SizedBox(height: AppSpacing.lg), + + // Bề mặt + _buildFilterGroup( + title: 'Bề mặt', + options: _surfaces, + selectedValues: filtersState.surfaces, + onToggle: (value) => ref + .read(productFiltersProvider.notifier) + .toggleSurface(value), + ), + + const SizedBox(height: AppSpacing.lg), + + // Thương hiệu + _buildFilterGroup( + title: 'Thương hiệu', + options: _brands, + selectedValues: filtersState.brands, + onToggle: (value) => ref + .read(productFiltersProvider.notifier) + .toggleBrand(value), + ), + + const SizedBox(height: 100), // Space for footer buttons + ], + ), + ), + ), + + // Footer Buttons + Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: AppColors.grey100, width: 1), + ), + ), + child: Row( + children: [ + // Reset Button + Expanded( + child: OutlinedButton( + onPressed: () { + ref.read(productFiltersProvider.notifier).reset(); + }, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.grey900, + side: const BorderSide( + color: AppColors.grey100, + width: 1, + ), + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.md, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Xóa bộ lọc', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: AppSpacing.md), + // Apply Button + Expanded( + child: ElevatedButton( + onPressed: () { + ref.read(productFiltersProvider.notifier).apply(); + Navigator.of(context).pop(); + + if (filtersState.hasActiveFilters) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Đã áp dụng ${filtersState.totalCount} bộ lọc', + ), + duration: const Duration(seconds: 2), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: AppColors.white, + elevation: 0, + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.md, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Áp dụng', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildFilterGroup({ + required String title, + required List options, + required Set selectedValues, + required Function(String) onToggle, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 12), + Column( + children: options.map((option) { + return CheckboxListTile( + title: Text( + option.label, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey900, + ), + ), + value: selectedValues.contains(option.value), + onChanged: (bool? checked) { + onToggle(option.value); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + activeColor: AppColors.primaryBlue, + ); + }).toList(), + ), + ], + ); + } +} + +/// Filter Option Model +class FilterOption { + final String value; + final String label; + + const FilterOption({ + required this.value, + required this.label, + }); +} diff --git a/lib/features/products/presentation/widgets/product_search_bar.dart b/lib/features/products/presentation/widgets/product_search_bar.dart index a17592a..dd50abb 100644 --- a/lib/features/products/presentation/widgets/product_search_bar.dart +++ b/lib/features/products/presentation/widgets/product_search_bar.dart @@ -54,9 +54,8 @@ class _ProductSearchBarState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return Container( + return SizedBox( height: InputFieldSpecs.height, - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), child: TextField( controller: _controller, focusNode: _focusNode,