update favorite, chat
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:hive_ce_flutter/hive_flutter.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import 'package:worker/core/constants/storage_constants.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
|
// TODO: Re-enable when build_runner generates this file successfully
|
||||||
// import 'package:worker/hive_registrar.g.dart';
|
// 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.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter');
|
||||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter');
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter');
|
||||||
|
|
||||||
// TODO: Register actual model type adapters when models are created
|
// Register model type adapters manually
|
||||||
// These will be added to the auto-generated registrar when models are created
|
// 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:
|
// Example:
|
||||||
// - UserModel (typeId: 0)
|
// - UserModel (typeId: 0)
|
||||||
// - ProductModel (typeId: 1)
|
// - ProductModel (typeId: 1)
|
||||||
|
|||||||
@@ -6,28 +6,30 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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/cart_page.dart';
|
||||||
import 'package:worker/features/cart/presentation/pages/checkout_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/favorites/presentation/pages/favorites_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/loyalty_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/points_history_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/rewards_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/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/order_detail_page.dart';
|
||||||
import 'package:worker/features/orders/presentation/pages/orders_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_detail_page.dart';
|
||||||
import 'package:worker/features/orders/presentation/pages/payment_qr_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/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/product_detail_page.dart';
|
||||||
import 'package:worker/features/products/presentation/pages/products_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/promotions/presentation/pages/promotion_detail_page.dart';
|
||||||
import 'package:worker/features/quotes/presentation/pages/quotes_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
|
/// App Router
|
||||||
///
|
///
|
||||||
@@ -253,6 +255,14 @@ class AppRouter {
|
|||||||
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
|
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
|
// TODO: Add more routes as features are implemented
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
462
lib/features/chat/presentation/pages/chat_list_page.dart
Normal file
@@ -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<ChatListPage> createState() => _ChatListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatListPageState extends ConsumerState<ChatListPage> {
|
||||||
|
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<Color>? 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,6 +227,7 @@ class HomePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/core/theme/colors.dart';
|
||||||
import 'package:worker/features/account/presentation/pages/account_page.dart';
|
import 'package:worker/features/account/presentation/pages/account_page.dart';
|
||||||
import 'package:worker/features/home/presentation/pages/home_page.dart';
|
import 'package:worker/features/home/presentation/pages/home_page.dart';
|
||||||
@@ -40,25 +42,15 @@ class MainScaffold extends ConsumerWidget {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: IndexedStack(
|
body: IndexedStack(index: currentIndex, children: pages),
|
||||||
index: currentIndex,
|
floatingActionButton: currentIndex < 4
|
||||||
children: pages,
|
|
||||||
),
|
|
||||||
floatingActionButton: currentIndex == 0
|
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () => context.push(RouteNames.chat),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
backgroundColor: const Color(0xFF35C6F4), // Accent cyan color
|
||||||
const SnackBar(
|
elevation: 4,
|
||||||
content: Text('Chat - Đang phát triển'),
|
child: const Icon(Icons.chat_bubble, color: AppColors.white, size: 28),
|
||||||
duration: Duration(seconds: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
backgroundColor: AppColors.accentCyan,
|
|
||||||
elevation: 8,
|
|
||||||
child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -66,11 +58,7 @@ class MainScaffold extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2)),
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, -2),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
@@ -87,18 +75,9 @@ class MainScaffold extends ConsumerWidget {
|
|||||||
currentIndex: currentIndex,
|
currentIndex: currentIndex,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
items: [
|
items: [
|
||||||
const BottomNavigationBarItem(
|
const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Trang chủ'),
|
||||||
icon: Icon(Icons.home),
|
const BottomNavigationBarItem(icon: Icon(Icons.loyalty), label: 'Hội viên'),
|
||||||
label: 'Trang chủ',
|
const BottomNavigationBarItem(icon: Icon(Icons.local_offer), label: 'Tin tức'),
|
||||||
),
|
|
||||||
const BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.loyalty),
|
|
||||||
label: 'Hội viên',
|
|
||||||
),
|
|
||||||
const BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.local_offer),
|
|
||||||
label: 'Tin tức',
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Stack(
|
icon: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
@@ -108,25 +87,12 @@ class MainScaffold extends ConsumerWidget {
|
|||||||
top: -4,
|
top: -4,
|
||||||
right: -4,
|
right: -4,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
horizontal: 6,
|
decoration: BoxDecoration(color: AppColors.danger, borderRadius: BorderRadius.circular(12)),
|
||||||
vertical: 2,
|
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.danger,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 20,
|
|
||||||
minHeight: 20,
|
|
||||||
),
|
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'5',
|
'5',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -135,10 +101,7 @@ class MainScaffold extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
label: 'Thông báo',
|
label: 'Thông báo',
|
||||||
),
|
),
|
||||||
const BottomNavigationBarItem(
|
const BottomNavigationBarItem(icon: Icon(Icons.account_circle), label: 'Cài đặt'),
|
||||||
icon: Icon(Icons.account_circle),
|
|
||||||
label: 'Cài đặt',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
ref.read(currentPageIndexProvider.notifier).setIndex(index);
|
ref.read(currentPageIndexProvider.notifier).setIndex(index);
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ import 'package:worker/generated/l10n/app_localizations.dart';
|
|||||||
/// Displays product information in a card format.
|
/// Displays product information in a card format.
|
||||||
/// Includes image, name, price, stock status, favorite toggle, and add to cart button.
|
/// Includes image, name, price, stock status, favorite toggle, and add to cart button.
|
||||||
class ProductCard extends ConsumerWidget {
|
class ProductCard extends ConsumerWidget {
|
||||||
final Product product;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
final VoidCallback? onAddToCart;
|
|
||||||
|
|
||||||
const ProductCard({
|
const ProductCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.product,
|
required this.product,
|
||||||
@@ -30,6 +26,10 @@ class ProductCard extends ConsumerWidget {
|
|||||||
this.onAddToCart,
|
this.onAddToCart,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Product product;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onAddToCart;
|
||||||
|
|
||||||
String _formatPrice(double price) {
|
String _formatPrice(double price) {
|
||||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||||
return '${formatter.format(price)}đ';
|
return '${formatter.format(price)}đ';
|
||||||
@@ -37,7 +37,7 @@ class ProductCard extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final isFavorited = ref.watch(isFavoriteProvider(product.productId));
|
final isFavorited = ref.watch(isFavoriteProvider(product.productId));
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
@@ -142,16 +142,21 @@ class ProductCard extends ConsumerWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
ref
|
// Capture current state before toggle
|
||||||
|
final wasfavorited = isFavorited;
|
||||||
|
|
||||||
|
// Toggle favorite
|
||||||
|
await ref
|
||||||
.read(favoritesProvider.notifier)
|
.read(favoritesProvider.notifier)
|
||||||
.toggleFavorite(product.productId);
|
.toggleFavorite(product.productId);
|
||||||
|
|
||||||
// Show feedback
|
// Show feedback with correct message
|
||||||
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
isFavorited
|
wasfavorited
|
||||||
? 'Đã xóa khỏi yêu thích'
|
? 'Đã xóa khỏi yêu thích'
|
||||||
: 'Đã thêm vào yêu thích',
|
: 'Đã thêm vào yêu thích',
|
||||||
),
|
),
|
||||||
@@ -159,6 +164,7 @@ class ProductCard extends ConsumerWidget {
|
|||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -169,7 +175,7 @@ class ProductCard extends ConsumerWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user