From 3741239d83f1dfe789788fe56057cf566f7ba0e7 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Wed, 26 Nov 2025 11:21:35 +0700 Subject: [PATCH] dang ki du an --- lib/core/router/app_router.dart | 10 + .../pages/payment_detail_page.dart | 46 +- .../presentation/pages/payments_page.dart | 350 ++------ .../pages/submission_create_page.dart | 791 ++++++++++++++++++ .../presentation/pages/submissions_page.dart | 29 +- 5 files changed, 913 insertions(+), 313 deletions(-) create mode 100644 lib/features/projects/presentation/pages/submission_create_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index f1f0f81..4a11dd8 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -41,6 +41,7 @@ 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/products_page.dart'; import 'package:worker/features/products/presentation/pages/write_review_page.dart'; +import 'package:worker/features/projects/presentation/pages/submission_create_page.dart'; import 'package:worker/features/projects/presentation/pages/submissions_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; @@ -363,6 +364,14 @@ final routerProvider = Provider((ref) { MaterialPage(key: state.pageKey, child: const SubmissionsPage()), ), + // Submission Create Route + GoRoute( + path: RouteNames.submissionCreate, + name: RouteNames.submissionCreate, + pageBuilder: (context, state) => + MaterialPage(key: state.pageKey, child: const SubmissionCreatePage()), + ), + // Quotes Route GoRoute( path: RouteNames.quotes, @@ -564,6 +573,7 @@ class RouteNames { static const String projectDetail = '/projects/:id'; static const String projectCreate = '/projects/create'; static const String submissions = '/submissions'; + static const String submissionCreate = '/submissions/create'; static const String quotes = '/quotes'; static const String quoteDetail = '/quotes/:id'; static const String quoteCreate = '/quotes/create'; diff --git a/lib/features/orders/presentation/pages/payment_detail_page.dart b/lib/features/orders/presentation/pages/payment_detail_page.dart index c03730b..9097d7e 100644 --- a/lib/features/orders/presentation/pages/payment_detail_page.dart +++ b/lib/features/orders/presentation/pages/payment_detail_page.dart @@ -11,6 +11,7 @@ import 'package:intl/intl.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/database/models/enums.dart'; import 'package:worker/core/theme/colors.dart'; +import 'package:worker/core/utils/extensions.dart'; import 'package:worker/features/orders/presentation/providers/invoices_provider.dart'; /// Payment Detail Page @@ -65,16 +66,12 @@ class PaymentDetailPage extends ConsumerWidget { orElse: () => invoices.first, ); - final currencyFormatter = NumberFormat.currency( - locale: 'vi_VN', - symbol: 'đ', - decimalDigits: 0, - ); return SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, children: [ // Invoice Header Card _buildInvoiceHeader( @@ -82,13 +79,11 @@ class PaymentDetailPage extends ConsumerWidget { invoice.orderId, invoice.issueDate, invoice.status, - currencyFormatter, invoice.totalAmount, invoice.amountPaid, invoice.amountRemaining, ), - const SizedBox(height: 16), // Dates and Customer Info Card _buildCustomerInfo( @@ -97,26 +92,21 @@ class PaymentDetailPage extends ConsumerWidget { invoice.isOverdue, ), - const SizedBox(height: 16), // Product List Card _buildProductList(), - const SizedBox(height: 16), // Payment History Card _buildPaymentHistory( invoice.amountPaid, - invoice.issueDate, - currencyFormatter, + invoice.issueDate ), - const SizedBox(height: 16), // Download Section Card _buildDownloadSection(invoice.invoiceNumber), - const SizedBox(height: 16), // Support Button Container( @@ -148,7 +138,6 @@ class PaymentDetailPage extends ConsumerWidget { ), ), - const SizedBox(height: 12), // Payment Button Container( @@ -195,7 +184,6 @@ class PaymentDetailPage extends ConsumerWidget { ), ), - const SizedBox(height: 16), ], ), ); @@ -236,7 +224,6 @@ class PaymentDetailPage extends ConsumerWidget { String? orderId, DateTime issueDate, InvoiceStatus status, - NumberFormat currencyFormatter, double totalAmount, double amountPaid, double amountRemaining, @@ -289,12 +276,12 @@ class PaymentDetailPage extends ConsumerWidget { children: [ _buildSummaryRow( 'Tổng tiền hóa đơn:', - currencyFormatter.format(totalAmount), + totalAmount.toVNCurrency, ), const SizedBox(height: 12), _buildSummaryRow( 'Đã thanh toán:', - currencyFormatter.format(amountPaid), + amountPaid.toVNCurrency, ), const Padding( padding: EdgeInsets.symmetric(vertical: 12), @@ -302,7 +289,7 @@ class PaymentDetailPage extends ConsumerWidget { ), _buildSummaryRow( 'Còn lại:', - currencyFormatter.format(amountRemaining), + amountRemaining.toVNCurrency, isHighlighted: true, valueColor: amountRemaining > 0 ? AppColors.danger @@ -560,7 +547,6 @@ class PaymentDetailPage extends ConsumerWidget { Widget _buildPaymentHistory( double amountPaid, DateTime paymentDate, - NumberFormat currencyFormatter, ) { final hasHistory = amountPaid > 0; @@ -572,11 +558,11 @@ class PaymentDetailPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ FaIcon(FontAwesomeIcons.clockRotateLeft, color: AppColors.primaryBlue, size: 18), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Lịch sử thanh toán', style: TextStyle( fontSize: 18, @@ -604,10 +590,12 @@ class PaymentDetailPage extends ConsumerWidget { color: AppColors.success.withValues(alpha: 0.1), shape: BoxShape.circle, ), - child: const FaIcon( - FontAwesomeIcons.check, - color: AppColors.success, - size: 18, + child: const Center( + child: FaIcon( + FontAwesomeIcons.check, + color: AppColors.success, + size: 18, + ), ), ), const SizedBox(width: 16), @@ -643,7 +631,7 @@ class PaymentDetailPage extends ConsumerWidget { ), ), Text( - currencyFormatter.format(amountPaid), + amountPaid.toVNCurrency, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, diff --git a/lib/features/orders/presentation/pages/payments_page.dart b/lib/features/orders/presentation/pages/payments_page.dart index f4ad4ad..2509902 100644 --- a/lib/features/orders/presentation/pages/payments_page.dart +++ b/lib/features/orders/presentation/pages/payments_page.dart @@ -1,6 +1,6 @@ /// Page: Payments Page /// -/// Displays list of invoices/payments with tab filters. +/// Displays list of invoices/payments. library; import 'package:flutter/material.dart'; @@ -8,7 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:worker/core/constants/ui_constants.dart'; -import 'package:worker/core/database/models/enums.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/orders/data/models/invoice_model.dart'; import 'package:worker/features/orders/presentation/providers/invoices_provider.dart'; @@ -17,131 +16,31 @@ import 'package:worker/features/orders/presentation/widgets/invoice_card.dart'; /// Payments Page /// /// Features: -/// - Tab bar for invoice status filtering /// - List of invoice cards /// - Pull-to-refresh -/// - Empty states for each tab -class PaymentsPage extends ConsumerStatefulWidget { +/// - Empty state +class PaymentsPage extends ConsumerWidget { const PaymentsPage({super.key}); @override - ConsumerState createState() => _PaymentsPageState(); -} - -class _PaymentsPageState extends ConsumerState - with SingleTickerProviderStateMixin { - late TabController _tabController; - - final List> _tabs = [ - {'key': 'all', 'label': 'Tất cả'}, - {'key': 'unpaid', 'label': 'Chưa thanh toán'}, - {'key': 'overdue', 'label': 'Quá hạn'}, - {'key': 'paid', 'label': 'Đã thanh toán'}, - ]; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: _tabs.length, vsync: this); - _tabController.addListener(_onTabChanged); - } - - @override - void dispose() { - _tabController - ..removeListener(_onTabChanged) - ..dispose(); - super.dispose(); - } - - void _onTabChanged() { - setState(() {}); // Rebuild to show filtered list - } - - /// Filter invoices based on tab key - List _filterInvoices( - List invoices, - String tabKey, - ) { - var filtered = List.from(invoices); - - switch (tabKey) { - case 'unpaid': - // Unpaid tab: issued status only - filtered = filtered - .where( - (invoice) => - invoice.status == InvoiceStatus.issued && !invoice.isPaid, - ) - .toList(); - break; - case 'overdue': - // Overdue tab: overdue status - filtered = filtered - .where( - (invoice) => - invoice.status == InvoiceStatus.overdue || invoice.isOverdue, - ) - .toList(); - break; - case 'paid': - // Paid tab: paid status - filtered = filtered - .where( - (invoice) => - invoice.status == InvoiceStatus.paid || invoice.isPaid, - ) - .toList(); - break; - case 'all': - default: - // All tab: no filtering - break; - } - - // Sort by issue date (newest first) - filtered.sort((a, b) => b.issueDate.compareTo(a.issueDate)); - - return filtered; - } - - /// Get counts for each tab - Map _getCounts(List invoices) { - return { - 'all': invoices.length, - 'unpaid': invoices - .where( - (invoice) => - invoice.status == InvoiceStatus.issued && !invoice.isPaid, - ) - .length, - 'overdue': invoices - .where( - (invoice) => - invoice.status == InvoiceStatus.overdue || invoice.isOverdue, - ) - .length, - 'paid': invoices - .where( - (invoice) => invoice.status == InvoiceStatus.paid || invoice.isPaid, - ) - .length, - }; - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final invoicesAsync = ref.watch(invoicesProvider); return invoicesAsync.when( - data: (allInvoices) { - final counts = _getCounts(allInvoices); + data: (invoices) { + // Sort by issue date (newest first) + final sortedInvoices = List.from(invoices) + ..sort((a, b) => b.issueDate.compareTo(a.issueDate)); return Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), onPressed: () => context.pop(), ), title: const Text( @@ -150,123 +49,53 @@ class _PaymentsPageState extends ConsumerState ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, - foregroundColor: AppColors.grey900, centerTitle: false, - bottom: TabBar( - controller: _tabController, - isScrollable: true, - tabAlignment: TabAlignment.start, - labelColor: AppColors.primaryBlue, - unselectedLabelColor: AppColors.grey500, - labelStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - indicatorColor: AppColors.primaryBlue, - indicatorWeight: 3, - tabs: _tabs.map((tab) { - final count = counts[tab['key']] ?? 0; - return Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(tab['label']!), - const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, + ), + body: sortedInvoices.isEmpty + ? _buildEmptyState(ref) + : RefreshIndicator( + onRefresh: () async { + await ref.read(invoicesProvider.notifier).refresh(); + }, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: sortedInvoices.length, + itemBuilder: (context, index) { + final invoice = sortedInvoices[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: InvoiceCard( + invoice: invoice, + onTap: () { + context.push('/payments/${invoice.invoiceId}'); + }, + onPaymentTap: () { + context.push('/payments/${invoice.invoiceId}'); + }, ), - decoration: BoxDecoration( - color: _tabController.index == _tabs.indexOf(tab) - ? AppColors.primaryBlue.withValues(alpha: 0.1) - : AppColors.grey100, - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '$count', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: _tabController.index == _tabs.indexOf(tab) - ? AppColors.primaryBlue - : AppColors.grey500, - ), - ), - ), - ], + ); + }, ), - ); - }).toList(), - ), - ), - body: RefreshIndicator( - onRefresh: () async { - print('refresh'); - await ref.read(invoicesProvider.notifier).refresh(); - }, - child: TabBarView( - controller: _tabController, - children: _tabs.map((tab) { - final filteredInvoices = _filterInvoices( - allInvoices, - tab['key']!, - ); - - return CustomScrollView( - slivers: [ - // Invoices List - SliverPadding( - padding: const EdgeInsets.all(16), - sliver: filteredInvoices.isEmpty - ? _buildEmptyState(tab['label']!) - : SliverList( - delegate: SliverChildBuilderDelegate(( - context, - index, - ) { - final invoice = filteredInvoices[index]; - return InvoiceCard( - invoice: invoice, - onTap: () { - context.push( - '/payments/${invoice.invoiceId}', - ); - }, - onPaymentTap: () { - context.push( - '/payments/${invoice.invoiceId}', - ); - }, - ); - }, childCount: filteredInvoices.length), - ), - ), - ], - ); - }).toList(), - ), - ), + ), ); }, loading: () => Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), onPressed: () => context.pop(), ), title: const Text( - 'Danh sách hoá đơn', + 'Thanh toán', style: TextStyle(color: Colors.black), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, - foregroundColor: AppColors.grey900, centerTitle: false, ), body: const Center(child: CircularProgressIndicator()), @@ -275,16 +104,19 @@ class _PaymentsPageState extends ConsumerState backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), onPressed: () => context.pop(), ), title: const Text( - 'Danh sách hoá đơn', + 'Thanh toán', style: TextStyle(color: Colors.black), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, - foregroundColor: AppColors.grey900, centerTitle: false, ), body: Center( @@ -319,54 +151,44 @@ class _PaymentsPageState extends ConsumerState } /// Build empty state - Widget _buildEmptyState(String tabLabel) { - String message; - IconData icon; - - switch (tabLabel) { - case 'Chưa thanh toán': - message = 'Không có hóa đơn chưa thanh toán'; - icon = FontAwesomeIcons.receipt; - break; - case 'Quá hạn': - message = 'Không có hóa đơn quá hạn'; - icon = FontAwesomeIcons.triangleExclamation; - break; - case 'Đã thanh toán': - message = 'Không có hóa đơn đã thanh toán'; - icon = FontAwesomeIcons.circleCheck; - break; - default: - message = 'Không có hóa đơn nào'; - icon = FontAwesomeIcons.receipt; - } - - return SliverFillRemaining( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FaIcon( - icon, - size: 80, - color: AppColors.grey500.withValues(alpha: 0.5), - ), - const SizedBox(height: 16), - Text( - message, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.grey500, + Widget _buildEmptyState(WidgetRef ref) { + return RefreshIndicator( + onRefresh: () async { + await ref.read(invoicesProvider.notifier).refresh(); + }, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SizedBox( + height: 500, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.receipt, + size: 80, + color: AppColors.grey500.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + const Text( + 'Không có hóa đơn nào', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 8), + const Text( + 'Kéo xuống để làm mới', + style: TextStyle(fontSize: 14, color: AppColors.grey500), + ), + ], ), ), - const SizedBox(height: 8), - const Text( - 'Kéo xuống để làm mới', - style: TextStyle(fontSize: 14, color: AppColors.grey500), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/projects/presentation/pages/submission_create_page.dart b/lib/features/projects/presentation/pages/submission_create_page.dart new file mode 100644 index 0000000..54b3829 --- /dev/null +++ b/lib/features/projects/presentation/pages/submission_create_page.dart @@ -0,0 +1,791 @@ +/// Submission Create Page +/// +/// Form for creating new project submissions. +library; + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:worker/core/constants/ui_constants.dart'; +import 'package:worker/core/theme/colors.dart'; + +/// Project Submission Create Page +class SubmissionCreatePage extends ConsumerStatefulWidget { + const SubmissionCreatePage({super.key}); + + @override + ConsumerState createState() => + _SubmissionCreatePageState(); +} + +class _SubmissionCreatePageState extends ConsumerState { + final _formKey = GlobalKey(); + + // Form controllers + final _projectNameController = TextEditingController(); + final _addressController = TextEditingController(); + final _ownerController = TextEditingController(); + final _designUnitController = TextEditingController(); + final _constructionUnitController = TextEditingController(); + final _areaController = TextEditingController(); + final _productsController = TextEditingController(); + final _descriptionController = TextEditingController(); + + // Form state + String? _selectedProgress; + DateTime? _expectedStartDate; + final List _uploadedFiles = []; + bool _showStartDateField = false; + + @override + void dispose() { + _projectNameController.dispose(); + _addressController.dispose(); + _ownerController.dispose(); + _designUnitController.dispose(); + _constructionUnitController.dispose(); + _areaController.dispose(); + _productsController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + leading: IconButton( + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text( + 'Đăng ký Công trình', + style: TextStyle(color: Colors.black), + ), + actions: [ + IconButton( + icon: const FaIcon( + FontAwesomeIcons.circleInfo, + color: Colors.black, + size: 20, + ), + onPressed: _showInfoDialog, + ), + const SizedBox(width: AppSpacing.sm), + ], + elevation: AppBarSpecs.elevation, + backgroundColor: AppColors.white, + centerTitle: false, + ), + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(4), + children: [ + // Basic Information + _buildBasicInfoCard(), + const SizedBox(height: 16), + + // Project Details + _buildProjectDetailsCard(), + const SizedBox(height: 16), + + // Additional Information + _buildAdditionalInfoCard(), + const SizedBox(height: 16), + + // File Upload + _buildFileUploadCard(), + const SizedBox(height: 24), + + // Submit Button + _buildSubmitButton(), + const SizedBox(height: 40), + ], + ), + ), + ); + } + + Widget _buildBasicInfoCard() { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Thông tin cơ bản', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _projectNameController, + label: 'Tên công trình', + required: true, + hint: 'Nhập tên công trình', + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _addressController, + label: 'Địa chỉ', + required: true, + hint: 'Nhập địa chỉ đầy đủ', + maxLines: 2, + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _ownerController, + label: 'Chủ đầu tư', + required: true, + hint: 'Tên chủ đầu tư', + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _designUnitController, + label: 'Đơn vị thiết kế', + hint: 'Tên đơn vị thiết kế', + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _constructionUnitController, + label: 'Đơn vị thi công', + hint: 'Tên đơn vị thi công', + ), + ], + ), + ), + ); + } + + Widget _buildProjectDetailsCard() { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Chi tiết dự án', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _areaController, + label: 'Tổng diện tích', + required: true, + hint: 'Nhập diện tích m²', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _productsController, + label: 'Sản phẩm đưa vào thiết kế', + required: true, + hint: 'Liệt kê các sản phẩm gạch đã sử dụng trong công trình (tên sản phẩm, mã SP, số lượng...)', + maxLines: 4, + helperText: 'Ví dụ: Gạch granite 60x60 - GP-001 - 100m², Gạch mosaic - MS-002 - 50m²', + ), + const SizedBox(height: 16), + + _buildProgressDropdown(), + + if (_showStartDateField) ...[ + const SizedBox(height: 16), + _buildDateField(), + ], + ], + ), + ), + ); + } + + Widget _buildAdditionalInfoCard() { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Thông tin bổ sung', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _descriptionController, + label: 'Mô tả công trình', + hint: 'Mô tả ngắn gọn về công trình, diện tích, đặc điểm nổi bật...', + maxLines: 3, + ), + ], + ), + ), + ); + } + + Widget _buildFileUploadCard() { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ảnh minh chứng', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 16), + + // Upload Area + InkWell( + onTap: _pickFiles, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.grey100, + width: 2, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Column( + children: [ + FaIcon( + FontAwesomeIcons.cloudArrowUp, + size: 48, + color: AppColors.grey500, + ), + SizedBox(height: 12), + Text( + 'Kéo thả ảnh vào đây', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.grey900, + ), + ), + SizedBox(height: 4), + Text( + 'hoặc nhấn để chọn file', + style: TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + ], + ), + ), + ), + + if (_uploadedFiles.isNotEmpty) ...[ + const SizedBox(height: 16), + ..._uploadedFiles.asMap().entries.map((entry) { + final index = entry.key; + final file = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildFileItem(file, index), + ); + }), + ], + + 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, + ), + ), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + String? hint, + bool required = false, + int maxLines = 1, + TextInputType? keyboardType, + String? helperText, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + if (required) + const Text( + ' *', + style: TextStyle( + fontSize: 14, + color: AppColors.danger, + ), + ), + ], + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: AppColors.grey500), + filled: true, + fillColor: AppColors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + 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), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + validator: required + ? (value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập $label'; + } + return null; + } + : null, + ), + if (helperText != null) ...[ + const SizedBox(height: 4), + Text( + helperText, + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ], + ); + } + + Widget _buildProgressDropdown() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Text( + 'Tiến độ công trình', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + Text( + ' *', + style: TextStyle( + fontSize: 14, + color: AppColors.danger, + ), + ), + ], + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: _selectedProgress, + decoration: InputDecoration( + filled: true, + fillColor: AppColors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + hint: const Text('Chọn tiến độ'), + items: const [ + DropdownMenuItem( + value: 'not-started', + child: Text('Chưa khởi công'), + ), + DropdownMenuItem( + value: 'foundation', + child: Text('Khởi công móng'), + ), + DropdownMenuItem( + value: 'rough-construction', + child: Text('Đang phần thô'), + ), + DropdownMenuItem( + value: 'finishing', + child: Text('Đang hoàn thiện'), + ), + DropdownMenuItem( + value: 'topped-out', + child: Text('Cất nóc'), + ), + ], + onChanged: (value) { + setState(() { + _selectedProgress = value; + _showStartDateField = value == 'not-started'; + if (!_showStartDateField) { + _expectedStartDate = null; + } + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Vui lòng chọn tiến độ công trình'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildDateField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ngày dự kiến khởi công', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + InkWell( + onTap: _pickDate, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.white, + border: Border.all(color: AppColors.grey100), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _expectedStartDate != null + ? '${_expectedStartDate!.day}/${_expectedStartDate!.month}/${_expectedStartDate!.year}' + : 'Chọn ngày', + style: TextStyle( + color: _expectedStartDate != null + ? AppColors.grey900 + : AppColors.grey500, + ), + ), + const FaIcon( + FontAwesomeIcons.calendar, + size: 16, + color: AppColors.grey500, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildFileItem(File file, int index) { + final fileName = file.path.split('/').last; + final fileSizeInBytes = file.lengthSync(); + final fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toStringAsFixed(2); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FA), + border: Border.all(color: AppColors.grey100), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.file( + file, + width: 48, + height: 48, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 48, + height: 48, + color: AppColors.grey100, + child: const FaIcon( + FontAwesomeIcons.image, + size: 24, + color: AppColors.grey500, + ), + ); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fileName, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.grey900, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + '${fileSizeInMB}MB', + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ), + ), + IconButton( + icon: const FaIcon( + FontAwesomeIcons.xmark, + size: 16, + color: AppColors.danger, + ), + onPressed: () { + setState(() { + _uploadedFiles.removeAt(index); + }); + }, + ), + ], + ), + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: AppColors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon(FontAwesomeIcons.paperPlane, size: 16), + SizedBox(width: 8), + Text( + 'Gửi đăng ký', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + Future _pickDate() async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + + if (date != null) { + setState(() { + _expectedStartDate = date; + }); + } + } + + Future _pickFiles() async { + try { + final ImagePicker picker = ImagePicker(); + final List images = await picker.pickMultiImage( + maxWidth: 1920, + maxHeight: 1920, + imageQuality: 85, + ); + + if (images.isNotEmpty) { + setState(() { + for (final image in images) { + _uploadedFiles.add(File(image.path)); + } + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Đã thêm ${images.length} ảnh'), + duration: const Duration(seconds: 2), + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Lỗi khi chọn ảnh: $e')), + ); + } + } + } + + Future _handleSubmit() async { + if (_formKey.currentState!.validate()) { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Xác nhận'), + content: const Text('Xác nhận gửi đăng ký công trình?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Hủy'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Xác nhận'), + ), + ], + ), + ); + + if (confirmed == true && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.', + ), + ), + ); + Navigator.pop(context); + } + } + } + + void _showInfoDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Hướng dẫn đăng ký'), + 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 Đăng ký Công trình:', + ), + SizedBox(height: 12), + Text('• Điền đầy đủ thông tin công trình theo yêu cầu'), + Text('• Upload ảnh minh chứng chất lượng cao'), + Text('• Mô tả chi tiết sản phẩm đã sử dụng'), + Text('• Chọn đúng tiến độ hiện tại của công trình'), + Text('• Kiểm tra kỹ thông tin trước khi gửi'), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Đóng'), + ), + ], + ), + ); + } +} diff --git a/lib/features/projects/presentation/pages/submissions_page.dart b/lib/features/projects/presentation/pages/submissions_page.dart index f38865d..acd4b36 100644 --- a/lib/features/projects/presentation/pages/submissions_page.dart +++ b/lib/features/projects/presentation/pages/submissions_page.dart @@ -6,8 +6,10 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.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/projects/domain/entities/project_submission.dart'; import 'package:worker/features/projects/presentation/providers/submissions_provider.dart'; @@ -36,12 +38,7 @@ class SubmissionsPage extends ConsumerWidget { actions: [ IconButton( icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20), - onPressed: () { - // TODO: Navigate to create submission page - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Tạo dự án mới - Đang phát triển')), - ); - }, + onPressed: () => context.push(RouteNames.submissionCreate), ), const SizedBox(width: AppSpacing.sm), ], @@ -119,17 +116,17 @@ class SubmissionsPage extends ConsumerWidget { children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.5, - child: Center( + child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const FaIcon( + FaIcon( FontAwesomeIcons.folderOpen, size: 64, color: AppColors.grey500, ), - const SizedBox(height: 16), - const Text( + SizedBox(height: 16), + Text( 'Không có dự án nào', style: TextStyle( fontSize: 16, @@ -137,8 +134,8 @@ class SubmissionsPage extends ConsumerWidget { color: AppColors.grey900, ), ), - const SizedBox(height: 8), - const Text( + SizedBox(height: 8), + Text( 'Không tìm thấy dự án phù hợp', style: TextStyle(color: AppColors.grey500), ), @@ -264,14 +261,6 @@ class SubmissionsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: _getStatusColor(submission.status), - width: 4, - ), - ), - ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [