From fb90c72f54aab80b15d621bb6afef21258e3a0d0 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 3 Nov 2025 17:12:40 +0700 Subject: [PATCH] =?UTF-8?q?nh=C3=A0=20m=E1=BA=ABu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/router/app_router.dart | 12 + .../home/presentation/pages/home_page.dart | 9 +- .../presentation/pages/model_houses_page.dart | 521 ++++++++++++++++++ 3 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 lib/features/showrooms/presentation/pages/model_houses_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1d76f88..0f122df 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -30,6 +30,7 @@ import 'package:worker/features/products/presentation/pages/product_detail_page. import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; +import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart'; /// App Router /// @@ -263,6 +264,14 @@ class AppRouter { 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()), + ), + // TODO: Add more routes as features are implemented ], @@ -387,6 +396,9 @@ class RouteNames { // Chat Route static const String chat = '/chat'; + // Model Houses Route + static const String modelHouses = '/model-houses'; + // Authentication Routes (TODO: implement when auth feature is ready) static const String login = '/login'; static const String otpVerification = '/otp-verification'; diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index da5174b..d4eb10a 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -203,7 +203,7 @@ class HomePage extends ConsumerWidget { QuickAction( icon: Icons.home_work, label: 'Nhà mẫu', - onTap: () => _showComingSoon(context, 'Nhà mẫu', l10n), + onTap: () => context.push(RouteNames.modelHouses), ), QuickAction( icon: Icons.business, @@ -211,16 +211,11 @@ class HomePage extends ConsumerWidget { onTap: () => _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) - const SizedBox(height: 100), + const SizedBox(height: 40), ], ), ), diff --git a/lib/features/showrooms/presentation/pages/model_houses_page.dart b/lib/features/showrooms/presentation/pages/model_houses_page.dart new file mode 100644 index 0000000..31c4bcd --- /dev/null +++ b/lib/features/showrooms/presentation/pages/model_houses_page.dart @@ -0,0 +1,521 @@ +/// 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:worker/core/constants/ui_constants.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 createState() => _ModelHousesPageState(); +} + +class _ModelHousesPageState extends ConsumerState + 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( + 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() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Chức năng tạo yêu cầu thiết kế sẽ được triển khai trong phiên bản tiếp theo'), + ), + ); + } + + @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: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Chức năng xem chi tiết yêu cầu sẽ được triển khai trong phiên bản tiếp theo'), + ), + ); + }, + 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, + ), + ), + ], + ), + ), + ), + ); + } +}