From 24a8508fce572325f4dae27ab287b9d2a64b8c82 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 3 Nov 2025 17:31:12 +0700 Subject: [PATCH] add model/design --- lib/core/router/app_router.dart | 27 +- .../pages/design_request_create_page.dart | 814 ++++++++++++++++ .../pages/design_request_detail_page.dart | 895 ++++++++++++++++++ .../presentation/pages/model_houses_page.dart | 14 +- pubspec.lock | 40 + pubspec.yaml | 2 + 6 files changed, 1781 insertions(+), 11 deletions(-) create mode 100644 lib/features/showrooms/presentation/pages/design_request_create_page.dart create mode 100644 lib/features/showrooms/presentation/pages/design_request_detail_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 0f122df..8aca77e 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -30,6 +30,8 @@ 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/design_request_create_page.dart'; +import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart'; import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart'; /// App Router @@ -272,6 +274,27 @@ class AppRouter { 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 ], @@ -396,8 +419,10 @@ class RouteNames { // Chat Route static const String chat = '/chat'; - // Model Houses Route + // 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) static const String login = '/login'; diff --git a/lib/features/showrooms/presentation/pages/design_request_create_page.dart b/lib/features/showrooms/presentation/pages/design_request_create_page.dart new file mode 100644 index 0000000..881f756 --- /dev/null +++ b/lib/features/showrooms/presentation/pages/design_request_create_page.dart @@ -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()); + final projectNameController = useTextEditingController(); + final areaController = useTextEditingController(); + final locationController = useTextEditingController(); + final notesController = useTextEditingController(); + + final selectedStyle = useState(''); + final selectedBudget = useState(''); + final selectedFiles = useState>([]); + final isSubmitting = useState(false); + + Future 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 = []; + 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.from(selectedFiles.value); + files.removeAt(index); + selectedFiles.value = files; + } + + Future 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( + 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( + 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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/showrooms/presentation/pages/design_request_detail_page.dart b/lib/features/showrooms/presentation/pages/design_request_detail_page.dart new file mode 100644 index 0000000..6658b76 --- /dev/null +++ b/lib/features/showrooms/presentation/pages/design_request_detail_page.dart @@ -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 _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 _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( + 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 _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>; + final files = request['files'] as List; + + 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, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/showrooms/presentation/pages/model_houses_page.dart b/lib/features/showrooms/presentation/pages/model_houses_page.dart index 31c4bcd..381627a 100644 --- a/lib/features/showrooms/presentation/pages/model_houses_page.dart +++ b/lib/features/showrooms/presentation/pages/model_houses_page.dart @@ -6,7 +6,9 @@ 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 @@ -73,11 +75,7 @@ class _ModelHousesPageState extends ConsumerState } 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'), - ), - ); + context.push(RouteNames.designRequestCreate); } @override @@ -446,11 +444,7 @@ class _RequestCard extends StatelessWidget { 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'), - ), - ); + context.push('/model-houses/design-request/${code.replaceAll('#', '')}'); }, borderRadius: BorderRadius.circular(12), child: Padding( diff --git a/pubspec.lock b/pubspec.lock index f9eb0e5..0a33c27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -377,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -1353,6 +1361,30 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -1361,6 +1393,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 373f4d0..62d991d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,8 @@ dependencies: intl: ^0.20.0 share_plus: ^9.0.0 image_picker: ^1.1.2 + file_picker: ^8.0.0 + url_launcher: ^6.3.0 path_provider: ^2.1.3 shared_preferences: ^2.2.3