diff --git a/lib/core/database/hive_service.dart b/lib/core/database/hive_service.dart index 3ee90e5..826908f 100644 --- a/lib/core/database/hive_service.dart +++ b/lib/core/database/hive_service.dart @@ -5,6 +5,7 @@ import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/features/favorites/data/models/favorite_model.dart'; // TODO: Re-enable when build_runner generates this file successfully // import 'package:worker/hive_registrar.g.dart'; @@ -109,8 +110,14 @@ class HiveService { debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter'); debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter'); - // TODO: Register actual model type adapters when models are created - // These will be added to the auto-generated registrar when models are created + // Register model type adapters manually + // FavoriteModel adapter (typeId: 28) + if (!Hive.isAdapterRegistered(HiveTypeIds.favoriteModel)) { + Hive.registerAdapter(FavoriteModelAdapter()); + debugPrint('HiveService: ✓ FavoriteModel adapter registered'); + } + + // TODO: Register other model type adapters when created // Example: // - UserModel (typeId: 0) // - ProductModel (typeId: 1) diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 2fe0276..1d76f88 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -6,28 +6,30 @@ library; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; + +import 'package:worker/features/account/presentation/pages/addresses_page.dart'; +import 'package:worker/features/account/presentation/pages/change_password_page.dart'; +import 'package:worker/features/account/presentation/pages/profile_edit_page.dart'; import 'package:worker/features/cart/presentation/pages/cart_page.dart'; import 'package:worker/features/cart/presentation/pages/checkout_page.dart'; +import 'package:worker/features/chat/presentation/pages/chat_list_page.dart'; import 'package:worker/features/favorites/presentation/pages/favorites_page.dart'; import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart'; import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart'; import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart'; import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; +import 'package:worker/features/news/presentation/pages/news_detail_page.dart'; +import 'package:worker/features/news/presentation/pages/news_list_page.dart'; import 'package:worker/features/orders/presentation/pages/order_detail_page.dart'; import 'package:worker/features/orders/presentation/pages/orders_page.dart'; import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart'; import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart'; import 'package:worker/features/orders/presentation/pages/payments_page.dart'; +import 'package:worker/features/price_policy/price_policy.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; -import 'package:worker/features/price_policy/price_policy.dart'; -import 'package:worker/features/news/presentation/pages/news_list_page.dart'; -import 'package:worker/features/news/presentation/pages/news_detail_page.dart'; -import 'package:worker/features/account/presentation/pages/profile_edit_page.dart'; -import 'package:worker/features/account/presentation/pages/addresses_page.dart'; -import 'package:worker/features/account/presentation/pages/change_password_page.dart'; /// App Router /// @@ -253,6 +255,14 @@ class AppRouter { MaterialPage(key: state.pageKey, child: const ChangePasswordPage()), ), + // Chat List Route + GoRoute( + path: RouteNames.chat, + name: RouteNames.chat, + pageBuilder: (context, state) => + MaterialPage(key: state.pageKey, child: const ChatListPage()), + ), + // TODO: Add more routes as features are implemented ], diff --git a/lib/features/chat/presentation/pages/chat_list_page.dart b/lib/features/chat/presentation/pages/chat_list_page.dart new file mode 100644 index 0000000..9c863a5 --- /dev/null +++ b/lib/features/chat/presentation/pages/chat_list_page.dart @@ -0,0 +1,462 @@ +/// Page: Chat List Page +/// +/// List of chat conversations following html/chat-list.html design. +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'; + +/// Chat List Page +/// +/// Shows list of chat conversations with: +/// - Search functionality +/// - Conversation items with avatars +/// - Unread indicators +/// - Online status +/// - Load more button +class ChatListPage extends ConsumerStatefulWidget { + const ChatListPage({super.key}); + + @override + ConsumerState createState() => _ChatListPageState(); +} + +class _ChatListPageState extends ConsumerState { + bool _isSearchVisible = false; + final TextEditingController _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _toggleSearch() { + setState(() { + _isSearchVisible = !_isSearchVisible; + if (!_isSearchVisible) { + _searchController.clear(); + } + }); + } + + void _newConversation() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Chức năng tạo cuộc trò chuyện mới sẽ được triển khai trong phiên bản tiếp theo.', + ), + ), + ); + } + + void _openChat(String conversationId) { + // TODO: Navigate to chat detail page + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Mở cuộc trò chuyện: $conversationId')), + ); + } + + void _loadMore() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Đang tải thêm cuộc trò chuyện...')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.grey50, + appBar: AppBar( + backgroundColor: AppColors.white, + elevation: AppBarSpecs.elevation, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.of(context).pop(), + ), + centerTitle: false, + title: const Text( + 'Tin nhắn', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + actions: [ + IconButton( + icon: Icon( + _isSearchVisible ? Icons.search : Icons.search, + color: Colors.black, + ), + onPressed: _toggleSearch, + ), + IconButton( + icon: const Icon(Icons.add, color: Colors.black), + onPressed: _newConversation, + ), + const SizedBox(width: AppSpacing.sm), + ], + ), + body: Column( + children: [ + // Search Bar (conditional) + if (_isSearchVisible) + Container( + padding: const EdgeInsets.all(12), + color: AppColors.white, + child: TextField( + controller: _searchController, + autofocus: true, + decoration: InputDecoration( + hintText: 'Tìm kiếm cuộc trò chuyện...', + prefixIcon: const Icon(Icons.search, color: AppColors.grey500), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, color: AppColors.grey500), + onPressed: () { + setState(() { + _searchController.clear(); + }); + }, + ) + : null, + filled: true, + fillColor: AppColors.grey50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + setState(() {}); + }, + ), + ), + + // Conversations List + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + // Conversation 1 - Order Reference + _ConversationItem( + avatarIcon: Icons.inventory_2, + avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue], + contactName: 'Đơn hàng #SO001234', + messageTime: '14:30', + lastMessage: 'Đơn hàng đang được giao - Dự kiến đến 16:00', + lastMessageIcon: Icons.local_shipping, + contactType: 'Về: Đơn hàng #SO001234', + lastSeen: 'Cập nhật mới', + isUnread: true, + unreadCount: 2, + isOnline: true, + onTap: () => _openChat('order001'), + ), + + // Conversation 2 - Product Reference + _ConversationItem( + avatarIcon: Icons.inventory_outlined, + avatarGradient: null, + avatarBackgroundColor: AppColors.grey50, + iconColor: AppColors.primaryBlue, + contactName: 'Sản phẩm PR0123', + messageTime: '12:20', + lastMessage: 'Thông tin bổ sung về gạch Granite 60x60', + lastMessageIcon: Icons.info_outline, + contactType: 'Đơn hàng #DH001233', + lastSeen: '2 giờ trước', + isUnread: true, + unreadCount: 1, + isOnline: false, + isAway: true, + onTap: () => _openChat('product001'), + ), + + // Conversation 3 - Support Team + _ConversationItem( + avatarIcon: Icons.headset_mic, + avatarGradient: const [AppColors.primaryBlue, AppColors.lightBlue], + contactName: 'Tổng đài hỗ trợ', + messageTime: '13:45', + lastMessage: 'Thông tin về quy trình đổi trả sản phẩm', + contactType: 'Bộ phận hỗ trợ', + lastSeen: 'Đang hoạt động', + isUnread: false, + isOnline: true, + onTap: () => _openChat('support001'), + ), + + // Load More Button + Container( + padding: const EdgeInsets.all(20), + color: AppColors.white, + child: Center( + child: OutlinedButton.icon( + onPressed: _loadMore, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.grey900, + side: const BorderSide(color: AppColors.grey100), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.expand_more, size: 20), + label: const Text( + 'Tải thêm cuộc trò chuyện', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Conversation Item Widget +class _ConversationItem extends StatelessWidget { + const _ConversationItem({ + required this.avatarIcon, + this.avatarGradient, + this.avatarBackgroundColor, + this.iconColor, + required this.contactName, + required this.messageTime, + required this.lastMessage, + this.lastMessageIcon, + required this.contactType, + required this.lastSeen, + this.isUnread = false, + this.unreadCount, + this.isOnline = false, + this.isAway = false, + required this.onTap, + }); + + final IconData avatarIcon; + final List? avatarGradient; + final Color? avatarBackgroundColor; + final Color? iconColor; + final String contactName; + final String messageTime; + final String lastMessage; + final IconData? lastMessageIcon; + final String contactType; + final String lastSeen; + final bool isUnread; + final int? unreadCount; + final bool isOnline; + final bool isAway; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isUnread + ? const Color(0xFFF0F9FF) // Light blue background for unread + : AppColors.white, + border: const Border( + bottom: BorderSide(color: AppColors.grey100, width: 1), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar with online indicator + Stack( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + gradient: avatarGradient != null + ? LinearGradient( + colors: avatarGradient!, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: avatarBackgroundColor, + shape: BoxShape.circle, + ), + child: Icon( + avatarIcon, + color: iconColor ?? AppColors.white, + size: 20, + ), + ), + // Online indicator + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: isOnline + ? AppColors.success + : isAway + ? AppColors.warning + : AppColors.grey500, + shape: BoxShape.circle, + border: Border.all(color: AppColors.white, width: 2), + ), + ), + ), + ], + ), + + const SizedBox(width: 12), + + // Conversation content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: Contact name and time + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + contactName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Text( + messageTime, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Preview: Last message and unread count + Row( + children: [ + Expanded( + child: Row( + children: [ + if (lastMessageIcon != null) ...[ + Icon( + lastMessageIcon, + size: 14, + color: AppColors.grey500, + ), + const SizedBox(width: 6), + ], + Expanded( + child: Text( + lastMessage, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (unreadCount != null && unreadCount! > 0) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColors.danger, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints(minWidth: 18), + child: Text( + '$unreadCount', + style: const TextStyle( + color: AppColors.white, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ), + ], + ], + ), + + const SizedBox(height: 4), + + // Meta: Contact type and last seen + Row( + children: [ + Text( + contactType, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + fontWeight: FontWeight.w500, + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Text( + '•', + style: TextStyle( + color: AppColors.grey100, + fontSize: 12, + ), + ), + ), + Text( + lastSeen, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 19aad78..da5174b 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -227,6 +227,7 @@ class HomePage extends ConsumerWidget { ), ], ), + ); } diff --git a/lib/features/main/presentation/pages/main_scaffold.dart b/lib/features/main/presentation/pages/main_scaffold.dart index 87118eb..68a3ece 100644 --- a/lib/features/main/presentation/pages/main_scaffold.dart +++ b/lib/features/main/presentation/pages/main_scaffold.dart @@ -6,6 +6,8 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/account/presentation/pages/account_page.dart'; import 'package:worker/features/home/presentation/pages/home_page.dart'; @@ -40,25 +42,15 @@ class MainScaffold extends ConsumerWidget { ]; return Scaffold( - body: IndexedStack( - index: currentIndex, - children: pages, - ), - floatingActionButton: currentIndex == 0 + body: IndexedStack(index: currentIndex, children: pages), + floatingActionButton: currentIndex < 4 ? Padding( padding: const EdgeInsets.only(bottom: 20), child: FloatingActionButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Chat - Đang phát triển'), - duration: Duration(seconds: 1), - ), - ); - }, - backgroundColor: AppColors.accentCyan, - elevation: 8, - child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white), + onPressed: () => context.push(RouteNames.chat), + backgroundColor: const Color(0xFF35C6F4), // Accent cyan color + elevation: 4, + child: const Icon(Icons.chat_bubble, color: AppColors.white, size: 28), ), ) : null, @@ -66,11 +58,7 @@ class MainScaffold extends ConsumerWidget { decoration: BoxDecoration( color: Colors.white, boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, -2), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2)), ], ), child: SafeArea( @@ -87,18 +75,9 @@ class MainScaffold extends ConsumerWidget { currentIndex: currentIndex, elevation: 0, items: [ - const BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Trang chủ', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.loyalty), - label: 'Hội viên', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.local_offer), - label: 'Tin tức', - ), + const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Trang chủ'), + const BottomNavigationBarItem(icon: Icon(Icons.loyalty), label: 'Hội viên'), + const BottomNavigationBarItem(icon: Icon(Icons.local_offer), label: 'Tin tức'), BottomNavigationBarItem( icon: Stack( clipBehavior: Clip.none, @@ -108,25 +87,12 @@ class MainScaffold extends ConsumerWidget { top: -4, right: -4, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppColors.danger, - borderRadius: BorderRadius.circular(12), - ), - constraints: const BoxConstraints( - minWidth: 20, - minHeight: 20, - ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: AppColors.danger, borderRadius: BorderRadius.circular(12)), + constraints: const BoxConstraints(minWidth: 20, minHeight: 20), child: const Text( '5', - style: TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w700, - ), + style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700), textAlign: TextAlign.center, ), ), @@ -135,10 +101,7 @@ class MainScaffold extends ConsumerWidget { ), label: 'Thông báo', ), - const BottomNavigationBarItem( - icon: Icon(Icons.account_circle), - label: 'Cài đặt', - ), + const BottomNavigationBarItem(icon: Icon(Icons.account_circle), label: 'Cài đặt'), ], onTap: (index) { ref.read(currentPageIndexProvider.notifier).setIndex(index); diff --git a/lib/features/products/presentation/widgets/product_card.dart b/lib/features/products/presentation/widgets/product_card.dart index fd5b1f3..54f3f1b 100644 --- a/lib/features/products/presentation/widgets/product_card.dart +++ b/lib/features/products/presentation/widgets/product_card.dart @@ -19,10 +19,6 @@ import 'package:worker/generated/l10n/app_localizations.dart'; /// Displays product information in a card format. /// Includes image, name, price, stock status, favorite toggle, and add to cart button. class ProductCard extends ConsumerWidget { - final Product product; - final VoidCallback? onTap; - final VoidCallback? onAddToCart; - const ProductCard({ super.key, required this.product, @@ -30,6 +26,10 @@ class ProductCard extends ConsumerWidget { this.onAddToCart, }); + final Product product; + final VoidCallback? onTap; + final VoidCallback? onAddToCart; + String _formatPrice(double price) { final formatter = NumberFormat('#,###', 'vi_VN'); return '${formatter.format(price)}đ'; @@ -37,7 +37,7 @@ class ProductCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; + final l10n = AppLocalizations.of(context); final isFavorited = ref.watch(isFavoriteProvider(product.productId)); return Card( @@ -142,23 +142,29 @@ class ProductCard extends ConsumerWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: () { - ref + onTap: () async { + // Capture current state before toggle + final wasfavorited = isFavorited; + + // Toggle favorite + await ref .read(favoritesProvider.notifier) .toggleFavorite(product.productId); - // Show feedback - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - isFavorited - ? 'Đã xóa khỏi yêu thích' - : 'Đã thêm vào yêu thích', + // Show feedback with correct message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + wasfavorited + ? 'Đã xóa khỏi yêu thích' + : 'Đã thêm vào yêu thích', + ), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, ), - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - ), - ); + ); + } }, borderRadius: BorderRadius.circular(20), child: Container( @@ -169,7 +175,7 @@ class ProductCard extends ConsumerWidget { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.15), + color: Colors.black.withValues(alpha: 0.15), blurRadius: 6, offset: const Offset(0, 2), ),