update favorite, chat

This commit is contained in:
Phuoc Nguyen
2025-11-03 17:05:47 +07:00
parent 988216b151
commit b3d7637760
6 changed files with 530 additions and 81 deletions

View 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,
),
),
],
),
],
),
),
],
),
),
);
}
}