Compare commits

...

4 Commits

Author SHA1 Message Date
Phuoc Nguyen
24a8508fce add model/design 2025-11-03 17:31:12 +07:00
Phuoc Nguyen
fb90c72f54 nhà mẫu 2025-11-03 17:12:40 +07:00
Phuoc Nguyen
b3d7637760 update favorite, chat 2025-11-03 17:05:47 +07:00
Phuoc Nguyen
988216b151 payment page 2025-11-03 16:16:15 +07:00
13 changed files with 3484 additions and 97 deletions

View File

@@ -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)

View File

@@ -6,27 +6,33 @@ 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/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/showrooms/presentation/pages/design_request_create_page.dart';
import 'package:worker/features/news/presentation/pages/news_list_page.dart'; import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
import 'package:worker/features/news/presentation/pages/news_detail_page.dart'; import 'package:worker/features/showrooms/presentation/pages/model_houses_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
/// ///
@@ -176,6 +182,21 @@ class AppRouter {
}, },
), ),
// Payment QR Route
GoRoute(
path: RouteNames.paymentQr,
name: RouteNames.paymentQr,
pageBuilder: (context, state) {
final orderId = state.uri.queryParameters['orderId'] ?? '';
final amountStr = state.uri.queryParameters['amount'] ?? '0';
final amount = double.tryParse(amountStr) ?? 0.0;
return MaterialPage(
key: state.pageKey,
child: PaymentQrPage(orderId: orderId, amount: amount),
);
},
),
// Quotes Route // Quotes Route
GoRoute( GoRoute(
path: RouteNames.quotes, path: RouteNames.quotes,
@@ -237,6 +258,43 @@ 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()),
),
// Model Houses Route
GoRoute(
path: RouteNames.modelHouses,
name: RouteNames.modelHouses,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
),
// Design Request Create Route
GoRoute(
path: RouteNames.designRequestCreate,
name: RouteNames.designRequestCreate,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const DesignRequestCreatePage()),
),
// Design Request Detail Route
GoRoute(
path: RouteNames.designRequestDetail,
name: RouteNames.designRequestDetail,
pageBuilder: (context, state) {
final requestId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
child: DesignRequestDetailPage(requestId: requestId ?? 'YC001'),
);
},
),
// TODO: Add more routes as features are implemented // TODO: Add more routes as features are implemented
], ],
@@ -328,6 +386,7 @@ class RouteNames {
static const String orderDetail = '/orders/:id'; static const String orderDetail = '/orders/:id';
static const String payments = '/payments'; static const String payments = '/payments';
static const String paymentDetail = '/payments/:id'; static const String paymentDetail = '/payments/:id';
static const String paymentQr = '/payment-qr';
// Projects & Quotes Routes // Projects & Quotes Routes
static const String projects = '/projects'; static const String projects = '/projects';
@@ -360,6 +419,11 @@ class RouteNames {
// Chat Route // Chat Route
static const String chat = '/chat'; static const String chat = '/chat';
// Model Houses & Design Requests Routes
static const String modelHouses = '/model-houses';
static const String designRequestCreate = '/model-houses/design-request/create';
static const String designRequestDetail = '/model-houses/design-request/:id';
// Authentication Routes (TODO: implement when auth feature is ready) // Authentication Routes (TODO: implement when auth feature is ready)
static const String login = '/login'; static const String login = '/login';
static const String otpVerification = '/otp-verification'; static const String otpVerification = '/otp-verification';

View File

@@ -5,7 +5,9 @@ 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/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
/// Checkout Submit Button /// Checkout Submit Button
@@ -103,16 +105,6 @@ class CheckoutSubmitButton extends StatelessWidget {
duration: Duration(seconds: 2), duration: Duration(seconds: 2),
), ),
); );
} else {
// Show order success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đặt hàng thành công!'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
// Navigate back after a short delay // Navigate back after a short delay
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
@@ -120,5 +112,31 @@ class CheckoutSubmitButton extends StatelessWidget {
context.pop(); context.pop();
} }
}); });
} else {
// Generate order ID (mock - replace with actual from backend)
final orderId = 'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
// Show order success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đặt hàng thành công! Chuyển đến thanh toán...'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 1),
),
);
// Navigate to payment QR page after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pushNamed(
RouteNames.paymentQr,
queryParameters: {
'orderId': orderId,
'amount': total.toString(),
},
);
}
});
}
} }
} }

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

View File

@@ -203,7 +203,7 @@ class HomePage extends ConsumerWidget {
QuickAction( QuickAction(
icon: Icons.home_work, icon: Icons.home_work,
label: 'Nhà mẫu', label: 'Nhà mẫu',
onTap: () => _showComingSoon(context, 'Nhà mẫu', l10n), onTap: () => context.push(RouteNames.modelHouses),
), ),
QuickAction( QuickAction(
icon: Icons.business, icon: Icons.business,
@@ -211,22 +211,18 @@ class HomePage extends ConsumerWidget {
onTap: () => onTap: () =>
_showComingSoon(context, 'Đăng ký dự án', l10n), _showComingSoon(context, 'Đăng ký dự án', l10n),
), ),
QuickAction(
icon: Icons.article,
label: 'Tin tức',
onTap: () => context.push(RouteNames.news),
),
], ],
), ),
// Bottom Padding (for bottom nav clearance) // Bottom Padding (for bottom nav clearance)
const SizedBox(height: 100), const SizedBox(height: 40),
], ],
), ),
), ),
), ),
], ],
), ),
); );
} }

View File

@@ -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);

View File

@@ -0,0 +1,605 @@
/// Payment QR Page
///
/// QR code payment screen with bank transfer information.
/// Features:
/// - Payment amount display with minimum payment warning
/// - QR code for quick payment
/// - Bank transfer information with copy buttons
/// - Payment confirmation and proof upload buttons
/// - Countdown timer for payment
/// - Payment instructions modal
library;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Payment QR Page
///
/// Displays QR code and bank transfer information for payment.
class PaymentQrPage extends HookConsumerWidget {
final String orderId;
final double amount;
const PaymentQrPage({
super.key,
required this.orderId,
required this.amount,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Countdown timer (15 minutes = 900 seconds)
final remainingSeconds = useState<int>(900);
final timer = useRef<Timer?>(null);
// Start countdown timer
useEffect(() {
timer.value = Timer.periodic(const Duration(seconds: 1), (t) {
if (remainingSeconds.value > 0) {
remainingSeconds.value--;
} else {
t.cancel();
}
});
return () {
timer.value?.cancel();
};
}, []);
// Format timer display
final minutes = remainingSeconds.value ~/ 60;
final seconds = remainingSeconds.value % 60;
final timerDisplay =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text(
'Thanh toán',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.black),
onPressed: () => _showInfoDialog(context),
),
const SizedBox(width: AppSpacing.sm),
],
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: AppSpacing.md),
// Payment Amount Card
_buildAmountCard(amount),
const SizedBox(height: AppSpacing.md),
// QR Code Card
_buildQrCodeCard(amount, orderId),
const SizedBox(height: AppSpacing.md),
// Bank Transfer Info Card
_buildBankInfoCard(context, orderId),
const SizedBox(height: AppSpacing.md),
// Action Buttons
_buildActionButtons(context),
const SizedBox(height: AppSpacing.md),
// Timer
_buildTimer(timerDisplay, remainingSeconds.value),
const SizedBox(height: AppSpacing.lg),
],
),
),
);
}
/// Build payment amount card
Widget _buildAmountCard(double amount) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Text(
_formatCurrency(amount),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue,
),
),
const SizedBox(height: 8),
const Text(
'Số tiền cần thanh toán',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFFFFD54F)),
),
child: Row(
children: [
const Icon(Icons.info, color: AppColors.warning, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'Thanh toán không dưới 20%',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange[900],
),
),
),
],
),
),
],
),
);
}
/// Build QR code card
Widget _buildQrCodeCard(double amount, String orderId) {
// Generate QR code data URL
final qrData = Uri.encodeComponent(
'https://eurotile.com/payment/$orderId?amount=$amount');
final qrUrl =
'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=$qrData';
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
const Text(
'Quét mã QR để thanh toán',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
),
const SizedBox(height: AppSpacing.md),
Container(
width: 220,
height: 220,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Image.network(
qrUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.qr_code, size: 80, color: AppColors.grey500),
SizedBox(height: 8),
Text(
'Không thể tải mã QR',
style: TextStyle(fontSize: 12, color: AppColors.grey500),
),
],
);
},
),
),
const SizedBox(height: AppSpacing.md),
const Text(
'Quét mã QR bằng ứng dụng ngân hàng để thanh toán nhanh chóng',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
],
),
);
}
/// Build bank transfer info card
Widget _buildBankInfoCard(BuildContext context, String orderId) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Thông tin chuyển khoản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
),
const SizedBox(height: AppSpacing.md),
// Bank Name
_buildInfoRow(
context: context,
label: 'Ngân hàng:',
value: 'BIDV',
),
const Divider(height: 24),
// Account Number
_buildInfoRow(
context: context,
label: 'Số tài khoản:',
value: '19036810704016',
),
const Divider(height: 24),
// Account Holder
_buildInfoRow(
context: context,
label: 'Chủ tài khoản:',
value: 'CÔNG TY EUROTILE',
),
const Divider(height: 24),
// Transfer Content
_buildInfoRow(
context: context,
label: 'Nội dung:',
value: '$orderId La Nguyen Quynh',
),
const SizedBox(height: AppSpacing.md),
// Note
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFF90CAF9)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.lightbulb_outline,
color: AppColors.primaryBlue, size: 20),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 13,
color: Color(0xFF1565C0),
height: 1.4,
),
children: [
const TextSpan(
text: 'Lưu ý: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(
text:
'Vui lòng ghi đúng nội dung chuyển khoản để đơn hàng được xử lý nhanh chóng.',
),
],
),
),
),
],
),
),
],
),
);
}
/// Build info row with copy button
Widget _buildInfoRow({
required BuildContext context,
required String label,
required String value,
}) {
return Row(
children: [
Expanded(
flex: 2,
child: Text(
label,
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
),
IconButton(
icon: const Icon(Icons.copy, size: 20, color: AppColors.primaryBlue),
onPressed: () => _copyToClipboard(context, value),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
);
}
/// Build action buttons
Widget _buildActionButtons(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
// Confirmed Payment Button
Expanded(
child: OutlinedButton.icon(
onPressed: () => _confirmPayment(context),
icon: const Icon(Icons.check, size: 20),
label: const Text(
'Đã thanh toán',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
const SizedBox(width: AppSpacing.sm),
// Upload Proof Button
Expanded(
child: ElevatedButton.icon(
onPressed: () => _uploadProof(context),
icon: const Icon(Icons.camera_alt, size: 20),
label: const Text(
'Upload bill',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
],
),
);
}
/// Build countdown timer
Widget _buildTimer(String timerDisplay, int remainingSeconds) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.schedule, size: 18, color: AppColors.grey500),
const SizedBox(width: 8),
const Text(
'Thời gian thanh toán: ',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
Text(
timerDisplay,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: remainingSeconds < 300
? AppColors.danger
: AppColors.grey900,
),
),
],
),
);
}
/// Copy text to clipboard
void _copyToClipboard(BuildContext context, String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Đã sao chép: $text'),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
),
);
}
/// Show payment info dialog
void _showInfoDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Hướng dẫn thanh toán'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Đây là nội dung hướng dẫn sử dụng cho tính năng Thanh toán:',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 12),
_buildInfoItem('Quét mã QR bằng app ngân hàng hoặc ví điện tử'),
_buildInfoItem('Chuyển khoản theo thông tin được cung cấp'),
_buildInfoItem('Ghi đúng nội dung chuyển khoản'),
_buildInfoItem('Upload hóa đơn sau khi chuyển khoản'),
_buildInfoItem('Thanh toán tối thiểu 20% giá trị đơn hàng'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Đóng'),
),
],
),
);
}
/// Build info item for dialog
Widget _buildInfoItem(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('', style: TextStyle(fontSize: 14)),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 14)),
),
],
),
);
}
/// Confirm payment
void _confirmPayment(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Xác nhận thanh toán'),
content: const Text(
'Bạn đã hoàn tất thanh toán cho đơn hàng này?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Chưa'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xác nhận thanh toán!'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
// Navigate back after delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pop();
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
),
child: const Text('Đã thanh toán'),
),
],
),
);
}
/// Upload payment proof
void _uploadProof(BuildContext context) {
// TODO: Implement image picker and upload
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng upload bill đang được phát triển'),
duration: Duration(seconds: 2),
),
);
}
/// Format currency
String _formatCurrency(double amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
)}';
}
}

View File

@@ -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),
), ),

View File

@@ -0,0 +1,814 @@
/// Page: Design Request Create Page
///
/// Form to create a new design request following html/design-request-create.html.
library;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Design Request Create Page
///
/// Form with:
/// - Progress steps indicator
/// - Basic information (name, area, location, style, budget)
/// - Detailed requirements (notes)
/// - File upload with preview
class DesignRequestCreatePage extends HookConsumerWidget {
const DesignRequestCreatePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final projectNameController = useTextEditingController();
final areaController = useTextEditingController();
final locationController = useTextEditingController();
final notesController = useTextEditingController();
final selectedStyle = useState<String>('');
final selectedBudget = useState<String>('');
final selectedFiles = useState<List<PlatformFile>>([]);
final isSubmitting = useState(false);
Future<void> pickFiles() async {
try {
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'],
);
if (result != null) {
// Validate file sizes
final validFiles = <PlatformFile>[];
for (final file in result.files) {
if (file.size <= 10 * 1024 * 1024) { // 10MB max
validFiles.add(file);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${file.name} quá lớn (tối đa 10MB)'),
backgroundColor: AppColors.danger,
),
);
}
}
}
selectedFiles.value = [...selectedFiles.value, ...validFiles];
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi khi chọn file: $e'),
backgroundColor: AppColors.danger,
),
);
}
}
}
void removeFile(int index) {
final files = List<PlatformFile>.from(selectedFiles.value);
files.removeAt(index);
selectedFiles.value = files;
}
Future<void> submitForm() async {
if (formKey.currentState?.validate() ?? false) {
isSubmitting.value = true;
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
isSubmitting.value = false;
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Yêu cầu thiết kế đã được gửi thành công!'),
backgroundColor: AppColors.success,
),
);
// Navigate back
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pop();
}
});
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Vui lòng kiểm tra lại thông tin'),
backgroundColor: AppColors.danger,
),
);
}
}
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(
'Tạo yêu cầu thiết kế mới',
style: TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
actions: const [
SizedBox(width: AppSpacing.sm),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: formKey,
child: Column(
children: [
// Progress Steps
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_ProgressStep(number: 1, isActive: true),
Container(
width: 16,
height: 2,
color: AppColors.grey100,
margin: const EdgeInsets.symmetric(horizontal: 8),
),
_ProgressStep(number: 2),
Container(
width: 16,
height: 2,
color: AppColors.grey100,
margin: const EdgeInsets.symmetric(horizontal: 8),
),
_ProgressStep(number: 3),
],
),
const SizedBox(height: 24),
// Basic Information Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: AppColors.primaryBlue,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Thông tin cơ bản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 20),
// Project Name
_FormField(
label: 'Tên dự án/Khách hàng',
required: true,
controller: projectNameController,
hint: 'VD: Thiết kế nhà anh Minh - Quận 7',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng nhập tên dự án';
}
return null;
},
),
const SizedBox(height: 20),
// Area
_FormField(
label: 'Diện tích (m²)',
required: true,
controller: areaController,
hint: 'VD: 120',
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng nhập diện tích';
}
final area = double.tryParse(value);
if (area == null || area <= 0) {
return 'Diện tích phải là số dương';
}
return null;
},
),
const SizedBox(height: 20),
// Location
_FormField(
label: 'Khu vực (Tỉnh/ Thành phố)',
required: true,
controller: locationController,
hint: 'VD: Hà Nội',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng nhập khu vực';
}
return null;
},
),
const SizedBox(height: 20),
// Style
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: const TextSpan(
text: 'Phong cách mong muốn',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
children: [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
),
],
),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedStyle.value.isEmpty ? null : selectedStyle.value,
decoration: InputDecoration(
hintText: '-- Chọn phong cách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
items: const [
DropdownMenuItem(value: 'hien-dai', child: Text('Hiện đại')),
DropdownMenuItem(value: 'toi-gian', child: Text('Tối giản')),
DropdownMenuItem(value: 'co-dien', child: Text('Cổ điển')),
DropdownMenuItem(value: 'scandinavian', child: Text('Scandinavian')),
DropdownMenuItem(value: 'industrial', child: Text('Industrial')),
DropdownMenuItem(value: 'tropical', child: Text('Tropical')),
DropdownMenuItem(value: 'luxury', child: Text('Luxury')),
DropdownMenuItem(value: 'khac', child: Text('Khác (ghi rõ trong ghi chú)')),
],
onChanged: (value) {
selectedStyle.value = value ?? '';
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng chọn phong cách';
}
return null;
},
),
],
),
const SizedBox(height: 20),
// Budget
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ngân sách dự kiến',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedBudget.value.isEmpty ? null : selectedBudget.value,
decoration: InputDecoration(
hintText: '-- Chọn ngân sách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
items: const [
DropdownMenuItem(value: 'duoi-100tr', child: Text('Dưới 100 triệu')),
DropdownMenuItem(value: '100-300tr', child: Text('100 - 300 triệu')),
DropdownMenuItem(value: '300-500tr', child: Text('300 - 500 triệu')),
DropdownMenuItem(value: '500tr-1ty', child: Text('500 triệu - 1 tỷ')),
DropdownMenuItem(value: 'tren-1ty', child: Text('Trên 1 tỷ')),
DropdownMenuItem(value: 'trao-doi', child: Text('Trao đổi trực tiếp')),
],
onChanged: (value) {
selectedBudget.value = value ?? '';
},
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Detailed Requirements Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.edit_outlined,
color: AppColors.primaryBlue,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Yêu cầu chi tiết',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 20),
// Notes
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: const TextSpan(
text: 'Ghi chú chi tiết',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
children: [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
),
],
),
),
const SizedBox(height: 8),
TextFormField(
controller: notesController,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Mô tả chi tiết về yêu cầu thiết kế, số phòng, công năng sử dụng, sở thích cá nhân...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
),
contentPadding: const EdgeInsets.all(16),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng mô tả yêu cầu chi tiết';
}
return null;
},
),
],
),
],
),
),
),
const SizedBox(height: 20),
// File Upload Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.cloud_upload_outlined,
color: AppColors.primaryBlue,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Đính kèm tài liệu',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 20),
// Upload Area
InkWell(
onTap: pickFiles,
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.grey100,
width: 2,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(8),
color: AppColors.grey50,
),
child: Column(
children: [
Icon(
Icons.cloud_upload_outlined,
size: 32,
color: AppColors.grey500,
),
const SizedBox(height: 12),
const Text(
'Nhấn để chọn file hoặc kéo thả vào đây',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
const Text(
'Hỗ trợ: JPG, PNG, PDF (Tối đa 10MB mỗi file)',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
),
// File Preview
if (selectedFiles.value.isNotEmpty) ...[
const SizedBox(height: 16),
...selectedFiles.value.asMap().entries.map((entry) {
final index = entry.key;
final file = entry.value;
return _FilePreviewItem(
file: file,
onRemove: () => removeFile(index),
);
}),
],
],
),
),
),
const SizedBox(height: 32),
// Submit Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isSubmitting.value ? null : submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
disabledBackgroundColor: AppColors.grey100,
disabledForegroundColor: AppColors.grey500,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: isSubmitting.value
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.white,
),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.send, size: 20),
SizedBox(width: 8),
Text(
'Gửi yêu cầu',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 40),
],
),
),
),
);
}
}
/// Progress Step Widget
class _ProgressStep extends StatelessWidget {
final int number;
final bool isActive;
final bool isCompleted;
const _ProgressStep({
required this.number,
this.isActive = false,
this.isCompleted = false,
});
@override
Widget build(BuildContext context) {
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted
? AppColors.success
: isActive
? AppColors.primaryBlue
: AppColors.grey100,
),
child: Center(
child: Text(
'$number',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isActive || isCompleted ? AppColors.white : AppColors.grey500,
),
),
),
);
}
}
/// Form Field Widget
class _FormField extends StatelessWidget {
final String label;
final bool required;
final TextEditingController controller;
final String hint;
final TextInputType? keyboardType;
final String? Function(String?)? validator;
const _FormField({
required this.label,
this.required = false,
required this.controller,
required this.hint,
this.keyboardType,
this.validator,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
children: required
? const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
),
]
: null,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
validator: validator,
),
],
);
}
}
/// File Preview Item Widget
class _FilePreviewItem extends StatelessWidget {
final PlatformFile file;
final VoidCallback onRemove;
const _FilePreviewItem({
required this.file,
required this.onRemove,
});
IconData _getFileIcon() {
final extension = file.extension?.toLowerCase();
if (extension == 'pdf') return Icons.picture_as_pdf;
if (extension == 'jpg' || extension == 'jpeg' || extension == 'png') {
return Icons.image;
}
return Icons.insert_drive_file;
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
_getFileIcon(),
color: AppColors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatFileSize(file.size),
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
color: AppColors.danger,
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,895 @@
/// Page: Design Request Detail Page
///
/// Displays design request details following html/design-request-detail.html.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
/// Design Request Detail Page
///
/// Shows complete details of a design request including:
/// - Request header with ID, date, and status
/// - Project information grid (area, style, budget, status)
/// - Completion highlight (if completed) with 3D design link
/// - Project details (name, description, contact, files)
/// - Status timeline
/// - Action buttons (edit, contact)
class DesignRequestDetailPage extends ConsumerWidget {
const DesignRequestDetailPage({
required this.requestId,
super.key,
});
final String requestId;
// Mock data - in real app, this would come from a provider
Map<String, dynamic> _getRequestData() {
final mockData = {
'YC001': {
'id': 'YC001',
'name': 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
'area': '120m²',
'style': 'Hiện đại',
'budget': '300-500 triệu',
'status': DesignRequestStatus.completed,
'statusText': 'Đã hoàn thành',
'description':
'Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. '
'Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. '
'Tầng 1: garage, phòng khách, bếp. '
'Tầng 2: 2 phòng ngủ, 2 phòng tắm. '
'Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.',
'contact': 'SĐT: 0901234567 | Email: minh.nguyen@email.com',
'createdDate': '20/10/2024',
'files': ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
'designLink': 'https://example.com/3d-design/YC001',
'timeline': [
{
'title': 'Thiết kế hoàn thành',
'description': 'File thiết kế 3D đã được gửi đến khách hàng',
'date': '25/10/2024 - 14:30',
'status': DesignRequestStatus.completed,
},
{
'title': 'Bắt đầu thiết kế',
'description': 'KTS Nguyễn Văn An đã nhận và bắt đầu thiết kế',
'date': '22/10/2024 - 09:00',
'status': DesignRequestStatus.designing,
},
{
'title': 'Tiếp nhận yêu cầu',
'description': 'Yêu cầu thiết kế đã được tiếp nhận và xem xét',
'date': '20/10/2024 - 16:45',
'status': DesignRequestStatus.pending,
},
{
'title': 'Gửi yêu cầu',
'description': 'Yêu cầu thiết kế đã được gửi thành công',
'date': '20/10/2024 - 16:30',
'status': DesignRequestStatus.pending,
},
],
},
'YC002': {
'id': 'YC002',
'name': 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
'area': '85m²',
'style': 'Scandinavian',
'budget': '100-300 triệu',
'status': DesignRequestStatus.designing,
'statusText': 'Đang thiết kế',
'description':
'Cải tạo căn hộ chung cư 3PN theo phong cách Scandinavian. '
'Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
'contact': 'SĐT: 0987654321',
'createdDate': '25/10/2024',
'files': ['hinh-anh-hien-trang.jpg'],
'designLink': null,
'timeline': [
{
'title': 'Bắt đầu thiết kế',
'description': 'KTS đã nhận và đang tiến hành thiết kế',
'date': '26/10/2024 - 10:00',
'status': DesignRequestStatus.designing,
},
{
'title': 'Tiếp nhận yêu cầu',
'description': 'Yêu cầu thiết kế đã được tiếp nhận',
'date': '25/10/2024 - 14:30',
'status': DesignRequestStatus.pending,
},
{
'title': 'Gửi yêu cầu',
'description': 'Yêu cầu thiết kế đã được gửi thành công',
'date': '25/10/2024 - 14:15',
'status': DesignRequestStatus.pending,
},
],
},
'YC003': {
'id': 'YC003',
'name': 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
'area': '200m²',
'style': 'Luxury',
'budget': 'Trên 1 tỷ',
'status': DesignRequestStatus.pending,
'statusText': 'Chờ tiếp nhận',
'description':
'Thiết kế biệt thự 2 tầng phong cách luxury với hồ bơi và sân vườn. '
'5 phòng ngủ, 4 phòng tắm, phòng giải trí và garage 2 xe.',
'contact': 'SĐT: 0923456789 | Email: duc.le@gmail.com',
'createdDate': '28/10/2024',
'files': ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
'designLink': null,
'timeline': [
{
'title': 'Gửi yêu cầu',
'description': 'Yêu cầu thiết kế đã được gửi thành công',
'date': '28/10/2024 - 11:20',
'status': DesignRequestStatus.pending,
},
],
},
};
return mockData[requestId] ?? mockData['YC001']!;
}
Color _getStatusColor(DesignRequestStatus status) {
switch (status) {
case DesignRequestStatus.pending:
return const Color(0xFFffc107);
case DesignRequestStatus.designing:
return const Color(0xFF3730a3);
case DesignRequestStatus.completed:
return const Color(0xFF065f46);
}
}
Color _getStatusBackgroundColor(DesignRequestStatus status) {
switch (status) {
case DesignRequestStatus.pending:
return const Color(0xFFfef3c7);
case DesignRequestStatus.designing:
return const Color(0xFFe0e7ff);
case DesignRequestStatus.completed:
return const Color(0xFFd1fae5);
}
}
IconData _getTimelineIcon(DesignRequestStatus status, int index) {
if (status == DesignRequestStatus.completed) {
return Icons.check;
} else if (status == DesignRequestStatus.designing) {
return Icons.architecture;
} else {
return index == 0 ? Icons.send : Icons.access_time;
}
}
IconData _getFileIcon(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif'].contains(extension)) {
return Icons.image;
} else if (extension == 'pdf') {
return Icons.picture_as_pdf;
} else if (extension == 'dwg') {
return Icons.architecture;
} else if (['doc', 'docx'].contains(extension)) {
return Icons.description;
}
return Icons.insert_drive_file;
}
Future<void> _viewDesign3D(BuildContext context, String? designLink) async {
if (designLink != null) {
final uri = Uri.parse(designLink);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Không thể mở link thiết kế 3D'),
backgroundColor: AppColors.danger,
),
);
}
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Link thiết kế 3D chưa có sẵn'),
backgroundColor: AppColors.warning,
),
);
}
}
}
void _editRequest(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Chức năng chỉnh sửa yêu cầu sẽ được triển khai trong phiên bản tiếp theo',
),
),
);
}
void _contactSupport(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Liên hệ hỗ trợ'),
content: const Text(
'Bạn có muốn liên hệ hỗ trợ về yêu cầu thiết kế này?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Hủy'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
context.push(RouteNames.chat);
},
child: const Text('Liên hệ'),
),
],
),
);
}
Future<void> _shareRequest(BuildContext context, String requestId, String name) async {
try {
await Share.share(
'Yêu cầu thiết kế #$requestId\n$name',
subject: 'Chia sẻ yêu cầu thiết kế',
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi khi chia sẻ: $e'),
backgroundColor: AppColors.danger,
),
);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final request = _getRequestData();
final status = request['status'] as DesignRequestStatus;
final timeline = request['timeline'] as List<Map<String, dynamic>>;
final files = request['files'] as List<String>;
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(
'Chi tiết Yêu cầu',
style: TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: const Icon(Icons.share, color: Colors.black),
onPressed: () => _shareRequest(
context,
request['id'] as String,
request['name'] as String,
),
),
const SizedBox(width: AppSpacing.sm),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Request Header Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Request ID and Date
Text(
'#${request['id']}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
Text(
'Ngày gửi: ${request['createdDate']}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 16),
// Status Badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(status),
borderRadius: BorderRadius.circular(20),
),
child: Text(
request['statusText'] as String,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _getStatusColor(status),
),
),
),
const SizedBox(height: 24),
// Info Grid
_InfoGrid(
area: request['area'] as String,
style: request['style'] as String,
budget: request['budget'] as String,
statusText: request['statusText'] as String,
),
],
),
),
),
const SizedBox(height: 20),
// Completion Highlight (only if completed)
if (status == DesignRequestStatus.completed)
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFd1fae5), Color(0xFFa7f3d0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(color: const Color(0xFF10b981), width: 2),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(20),
child: Column(
children: [
const Text(
'🎉 Thiết kế đã hoàn thành!',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Color(0xFF065f46),
),
),
const SizedBox(height: 12),
const Text(
'Thiết kế 3D của bạn đã sẵn sàng để xem',
style: TextStyle(
color: Color(0xFF065f46),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _viewDesign3D(
context,
request['designLink'] as String?,
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10b981),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.view_in_ar, color: Colors.white),
label: const Text(
'Xem Link Thiết kế 3D',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
),
if (status == DesignRequestStatus.completed)
const SizedBox(height: 20),
// Project Details Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Project Name
_SectionHeader(
icon: Icons.info,
title: 'Thông tin dự án',
),
const SizedBox(height: 12),
RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.6,
),
children: [
const TextSpan(
text: 'Tên dự án: ',
style: TextStyle(fontWeight: FontWeight.w600),
),
TextSpan(text: request['name'] as String),
],
),
),
const SizedBox(height: 24),
// Description
_SectionHeader(
icon: Icons.edit,
title: 'Mô tả yêu cầu',
),
const SizedBox(height: 12),
Text(
request['description'] as String,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.6,
),
),
const SizedBox(height: 24),
// Contact Info
_SectionHeader(
icon: Icons.phone,
title: 'Thông tin liên hệ',
),
const SizedBox(height: 12),
Text(
request['contact'] as String,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.6,
),
),
const SizedBox(height: 24),
// Files
_SectionHeader(
icon: Icons.attach_file,
title: 'Tài liệu đính kèm',
),
const SizedBox(height: 16),
if (files.isEmpty)
const Text(
'Không có tài liệu đính kèm',
style: TextStyle(
color: AppColors.grey500,
fontStyle: FontStyle.italic,
),
)
else
...files.map(
(file) => _FileItem(
fileName: file,
icon: _getFileIcon(file),
),
),
],
),
),
),
const SizedBox(height: 20),
// Timeline Card
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionHeader(
icon: Icons.history,
title: 'Lịch sử trạng thái',
),
const SizedBox(height: 16),
...List.generate(
timeline.length,
(index) {
final item = timeline[index];
return _TimelineItem(
title: item['title'] as String,
description: item['description'] as String,
date: item['date'] as String,
status: item['status'] as DesignRequestStatus,
icon: _getTimelineIcon(
item['status'] as DesignRequestStatus,
timeline.length - index - 1,
),
isLast: index == timeline.length - 1,
getStatusColor: _getStatusColor,
getStatusBackgroundColor: _getStatusBackgroundColor,
);
},
),
],
),
),
),
const SizedBox(height: 20),
// Action Buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _editRequest(context),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.grey900,
side: const BorderSide(color: AppColors.grey100, width: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.edit),
label: const Text(
'Chỉnh sửa',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _contactSupport(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.chat_bubble),
label: const Text(
'Liên hệ',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
),
const SizedBox(height: 20),
],
),
),
);
}
}
/// Info Grid Widget
class _InfoGrid extends StatelessWidget {
const _InfoGrid({
required this.area,
required this.style,
required this.budget,
required this.statusText,
});
final String area;
final String style;
final String budget;
final String statusText;
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Expanded(
child: _InfoItem(label: 'Diện tích', value: area),
),
const SizedBox(width: 16),
Expanded(
child: _InfoItem(label: 'Phong cách', value: style),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _InfoItem(label: 'Ngân sách', value: budget),
),
const SizedBox(width: 16),
Expanded(
child: _InfoItem(label: 'Trạng thái', value: statusText),
),
],
),
],
);
}
}
/// Info Item Widget
class _InfoItem extends StatelessWidget {
const _InfoItem({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: AppColors.grey50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Section Header Widget
class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.icon,
required this.title,
});
final IconData icon;
final String title;
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, color: AppColors.primaryBlue, size: 20),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
);
}
}
/// File Item Widget
class _FileItem extends StatelessWidget {
const _FileItem({
required this.fileName,
required this.icon,
});
final String fileName;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.grey50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(icon, color: Colors.white, size: 16),
),
const SizedBox(width: 12),
Expanded(
child: Text(
fileName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
),
],
),
);
}
}
/// Timeline Item Widget
class _TimelineItem extends StatelessWidget {
const _TimelineItem({
required this.title,
required this.description,
required this.date,
required this.status,
required this.icon,
required this.isLast,
required this.getStatusColor,
required this.getStatusBackgroundColor,
});
final String title;
final String description;
final String date;
final DesignRequestStatus status;
final IconData icon;
final bool isLast;
final Color Function(DesignRequestStatus) getStatusColor;
final Color Function(DesignRequestStatus) getStatusBackgroundColor;
@override
Widget build(BuildContext context) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon and line
Column(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: getStatusBackgroundColor(status),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: getStatusColor(status),
size: 20,
),
),
if (!isLast)
Expanded(
child: Container(
width: 2,
margin: const EdgeInsets.symmetric(vertical: 4),
color: AppColors.grey100,
),
),
],
),
const SizedBox(width: 16),
// Content
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
date,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,515 @@
/// Page: Model Houses Page
///
/// Displays model house library and design requests following html/nha-mau.html.
library;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
/// Model Houses Page
///
/// Two tabs:
/// 1. Thư viện mẫu - Model house library with 360° views
/// 2. Yêu cầu thiết kế - Design requests with status tracking
class ModelHousesPage extends ConsumerStatefulWidget {
const ModelHousesPage({super.key});
@override
ConsumerState<ModelHousesPage> createState() => _ModelHousesPageState();
}
class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _showInfoDialog() {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text(
'Hướng dẫn sử dụng',
style: TextStyle(fontWeight: FontWeight.bold),
),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Đây là nội dung hướng dẫn sử dụng cho tính năng Nhà mẫu:'),
SizedBox(height: 12),
Text('• Tab "Thư viện Mẫu 360": Là nơi công ty cung cấp các mẫu thiết kế 360° có sẵn để bạn tham khảo.'),
SizedBox(height: 8),
Text('• Tab "Yêu cầu Thiết kế": Là nơi bạn gửi yêu cầu (ticket) để đội ngũ thiết kế của chúng tôi hỗ trợ bạn.'),
SizedBox(height: 8),
Text('• Bấm nút "+" trong tab "Yêu cầu Thiết kế" để tạo một Yêu cầu Thiết kế mới.'),
SizedBox(height: 8),
Text('• Khi yêu cầu hoàn thành, bạn có thể xem link thiết kế 3D trong trang chi tiết yêu cầu.'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Đóng'),
),
],
),
);
}
void _createNewRequest() {
context.push(RouteNames.designRequestCreate);
}
@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(
'Nhà mẫu',
style: TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.black),
onPressed: _showInfoDialog,
),
const SizedBox(width: AppSpacing.sm),
],
bottom: TabBar(
controller: _tabController,
indicatorColor: AppColors.primaryBlue,
indicatorWeight: 3,
labelColor: AppColors.primaryBlue,
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
unselectedLabelColor: AppColors.grey500,
unselectedLabelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
tabs: const [
Tab(text: 'Thư viện mẫu'),
Tab(text: 'Yêu cầu thiết kế'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
_LibraryTab(),
_DesignRequestsTab(),
],
),
floatingActionButton: AnimatedBuilder(
animation: _tabController,
builder: (context, child) {
// Show FAB only on Design Requests tab
return _tabController.index == 1
? FloatingActionButton(
onPressed: _createNewRequest,
backgroundColor: AppColors.primaryBlue,
elevation: 4,
child: const Icon(Icons.add, color: AppColors.white, size: 28),
)
: const SizedBox.shrink();
},
),
);
}
}
/// Library Tab - Model house 360° library
class _LibraryTab extends StatelessWidget {
const _LibraryTab();
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(20),
children: const [
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
title: 'Căn hộ Studio',
date: '15/11/2024',
description: 'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.',
has360View: true,
),
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
title: 'Biệt thự Hiện đại',
date: '12/11/2024',
description: 'Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.',
has360View: true,
),
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
title: 'Nhà phố Tối giản',
date: '08/11/2024',
description: 'Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.',
has360View: true,
),
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
title: 'Chung cư Cao cấp',
date: '05/11/2024',
description: 'Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.',
has360View: true,
),
],
);
}
}
/// Library Card Widget
class _LibraryCard extends StatelessWidget {
const _LibraryCard({
required this.imageUrl,
required this.title,
required this.date,
required this.description,
this.has360View = false,
});
final String imageUrl;
final String title;
final String date;
final String description;
final bool has360View;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Chức năng xem chi tiết sẽ được triển khai trong phiên bản tiếp theo'),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image with 360 badge
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: CachedNetworkImage(
imageUrl: imageUrl,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height: 200,
color: AppColors.grey100,
child: const Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
height: 200,
color: AppColors.grey100,
child: const Icon(
Icons.image_not_supported,
size: 48,
color: AppColors.grey500,
),
),
),
),
if (has360View)
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.primaryBlue.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'Xem 360°',
style: TextStyle(
color: AppColors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
// Content
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
// Date
Row(
children: [
const Icon(
Icons.calendar_today,
size: 14,
color: AppColors.grey500,
),
const SizedBox(width: 6),
Text(
'Ngày đăng: $date',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
const SizedBox(height: 12),
// Description
Text(
description,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.5,
),
),
],
),
),
],
),
),
);
}
}
/// Design Requests Tab
class _DesignRequestsTab extends StatelessWidget {
const _DesignRequestsTab();
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(20),
children: const [
_RequestCard(
code: '#YC001',
status: DesignRequestStatus.completed,
date: '20/10/2024',
description: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
),
_RequestCard(
code: '#YC002',
status: DesignRequestStatus.designing,
date: '25/10/2024',
description: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
),
_RequestCard(
code: '#YC003',
status: DesignRequestStatus.pending,
date: '28/10/2024',
description: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
),
_RequestCard(
code: '#YC004',
status: DesignRequestStatus.pending,
date: '01/11/2024',
description: 'Thiết kế cửa hàng kinh doanh - Chị Mai (Quận 1)',
),
],
);
}
}
/// Design Request Status
enum DesignRequestStatus {
pending,
designing,
completed,
}
/// Request Card Widget
class _RequestCard extends StatelessWidget {
const _RequestCard({
required this.code,
required this.status,
required this.date,
required this.description,
});
final String code;
final DesignRequestStatus status;
final String date;
final String description;
Color _getStatusColor() {
switch (status) {
case DesignRequestStatus.pending:
return const Color(0xFFffc107); // Warning yellow
case DesignRequestStatus.designing:
return const Color(0xFF3730a3); // Indigo
case DesignRequestStatus.completed:
return const Color(0xFF065f46); // Success green
}
}
Color _getStatusBackgroundColor() {
switch (status) {
case DesignRequestStatus.pending:
return const Color(0xFFfef3c7); // Light yellow
case DesignRequestStatus.designing:
return const Color(0xFFe0e7ff); // Light indigo
case DesignRequestStatus.completed:
return const Color(0xFFd1fae5); // Light green
}
}
String _getStatusText() {
switch (status) {
case DesignRequestStatus.pending:
return 'CHỜ TIẾP NHẬN';
case DesignRequestStatus.designing:
return 'ĐANG THIẾT KẾ';
case DesignRequestStatus.completed:
return 'HOÀN THÀNH';
}
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
context.push('/model-houses/design-request/${code.replaceAll('#', '')}');
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Code and Status
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Mã yêu cầu: $code',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_getStatusText(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getStatusColor(),
),
),
),
],
),
const SizedBox(height: 8),
// Date
Text(
'Ngày gửi: $date',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
// Description
Text(
description,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey900,
),
),
],
),
),
),
);
}
}

View File

@@ -377,6 +377,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
url: "https://pub.dev"
source: hosted
version: "8.0.7"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1353,6 +1361,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
url: "https://pub.dev"
source: hosted
version: "6.3.24"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
url: "https://pub.dev"
source: hosted
version: "6.3.5"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1361,6 +1393,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
url: "https://pub.dev"
source: hosted
version: "3.2.4"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:

View File

@@ -65,6 +65,8 @@ dependencies:
intl: ^0.20.0 intl: ^0.20.0
share_plus: ^9.0.0 share_plus: ^9.0.0
image_picker: ^1.1.2 image_picker: ^1.1.2
file_picker: ^8.0.0
url_launcher: ^6.3.0
path_provider: ^2.1.3 path_provider: ^2.1.3
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3