/// Page: Invoice Detail Page /// /// Displays invoice detail following html/invoice-detail.html design. 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:worker/core/widgets/loading_indicator.dart'; import 'package:share_plus/share_plus.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/utils/extensions.dart'; import 'package:worker/features/invoices/domain/entities/invoice.dart'; import 'package:worker/features/invoices/presentation/providers/invoices_provider.dart'; /// Invoice Detail Page /// /// Features: /// - Invoice header with status /// - Seller and buyer information (2-column grid) /// - Product list table with unit price /// - Invoice summary (total, discount, grand total) /// - Share and contact support actions class InvoiceDetailPage extends ConsumerWidget { const InvoiceDetailPage({ super.key, required this.invoiceId, }); final String invoiceId; @override Widget build(BuildContext context, WidgetRef ref) { final invoiceAsync = ref.watch(invoiceDetailProvider(invoiceId)); final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.surfaceContainerLowest, appBar: AppBar( leading: IconButton( icon: Icon(Icons.arrow_back, color: colorScheme.onSurface), onPressed: () => context.pop(), ), title: Text( 'Chi tiết Hóa đơn', style: TextStyle( color: colorScheme.onSurface, fontSize: 20, fontWeight: FontWeight.w600, ), ), elevation: AppBarSpecs.elevation, backgroundColor: colorScheme.surface, centerTitle: false, actions: [ IconButton( icon: FaIcon( FontAwesomeIcons.shareNodes, size: 20, color: colorScheme.onSurface, ), onPressed: () => _shareInvoice(context), ), const SizedBox(width: AppSpacing.sm), ], ), body: invoiceAsync.when( data: (invoice) => _buildContent(context, invoice), loading: () => const CustomLoadingIndicator(), error: (error, stack) => _buildErrorState(context, ref, error), ), ); } Widget _buildContent(BuildContext context, Invoice invoice) { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( children: [ // Invoice Header Card _buildHeaderCard(context, invoice), const SizedBox(height: 16), // Products Section _buildProductsCard(context, invoice), const SizedBox(height: 16), // Action Button _buildActionButton(context), const SizedBox(height: 40), ], ), ); } /// Build invoice header card Widget _buildHeaderCard(BuildContext context, Invoice invoice) { final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: colorScheme.outline.withValues(alpha: 0.1), width: 1.5, ), boxShadow: [ BoxShadow( color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // Invoice Header Section (centered) Container( padding: const EdgeInsets.only(bottom: 24), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: colorScheme.outlineVariant, width: 2, ), ), ), child: Column( children: [ // Invoice Icon Container( width: 80, height: 80, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ colorScheme.primary, colorScheme.primary.withValues(alpha: 0.8), ], ), borderRadius: BorderRadius.circular(12), ), child: const Center( child: FaIcon( FontAwesomeIcons.fileInvoiceDollar, size: 32, color: Colors.white, ), ), ), const SizedBox(height: 16), // Title Text( 'HÓA ĐƠN GTGT', style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), // Invoice Number Text( '#${invoice.name}', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: colorScheme.primary, ), ), const SizedBox(height: 12), // Status Badge _StatusBadge( status: invoice.status, statusColor: invoice.statusColor, ), const SizedBox(height: 16), // Invoice Meta Info Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _MetaItem(label: 'Ngày xuất:', value: invoice.formattedDate), if (invoice.orderId != null) ...[ const SizedBox(width: 32), _MetaItem(label: 'Đơn hàng:', value: '#${invoice.orderId}'), ], ], ), ], ), ), // Company Information Section (2-column grid) if (invoice.sellerInfo != null || invoice.buyerInfo != null) ...[ const SizedBox(height: 24), _buildCompanyInfoSection(context, invoice), ], ], ), ), ); } /// Build company info section (seller and buyer) - 2 column grid Widget _buildCompanyInfoSection(BuildContext context, Invoice invoice) { final colorScheme = Theme.of(context).colorScheme; final screenWidth = MediaQuery.of(context).size.width; final isWideScreen = screenWidth > 600; if (isWideScreen) { // 2-column grid for wide screens return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Seller Info if (invoice.sellerInfo != null) Expanded( child: _CompanyInfoBlock( icon: FontAwesomeIcons.building, iconColor: colorScheme.primary, title: 'Đơn vị bán hàng', lines: _buildSellerInfoLines(invoice), ), ), if (invoice.sellerInfo != null && invoice.buyerInfo != null) const SizedBox(width: 24), // Buyer Info if (invoice.buyerInfo != null) Expanded( child: _CompanyInfoBlock( icon: FontAwesomeIcons.userTie, iconColor: Colors.green.shade600, title: 'Đơn vị mua hàng', lines: _buildBuyerInfoLines(invoice), ), ), ], ); } else { // Single column for narrow screens return Column( children: [ // Seller Info if (invoice.sellerInfo != null) _CompanyInfoBlock( icon: FontAwesomeIcons.building, iconColor: colorScheme.primary, title: 'Đơn vị bán hàng', lines: _buildSellerInfoLines(invoice), ), if (invoice.sellerInfo != null && invoice.buyerInfo != null) const SizedBox(height: 24), // Buyer Info if (invoice.buyerInfo != null) _CompanyInfoBlock( icon: FontAwesomeIcons.userTie, iconColor: Colors.green.shade600, title: 'Đơn vị mua hàng', lines: _buildBuyerInfoLines(invoice), ), ], ); } } List<_InfoLine> _buildSellerInfoLines(Invoice invoice) { return [ if (invoice.sellerInfo!.companyName != null) _InfoLine(label: 'Công ty', value: invoice.sellerInfo!.companyName!), if (invoice.sellerInfo!.taxCode != null) _InfoLine(label: 'Mã số thuế', value: invoice.sellerInfo!.taxCode!), if (invoice.sellerInfo!.fullAddress.isNotEmpty) _InfoLine(label: 'Địa chỉ', value: invoice.sellerInfo!.fullAddress), if (invoice.sellerInfo!.phone != null) _InfoLine(label: 'Điện thoại', value: invoice.sellerInfo!.phone!), if (invoice.sellerInfo!.email != null) _InfoLine(label: 'Email', value: invoice.sellerInfo!.email!), ]; } List<_InfoLine> _buildBuyerInfoLines(Invoice invoice) { return [ if (invoice.buyerInfo!.name != null) _InfoLine(label: 'Người mua hàng', value: invoice.buyerInfo!.name!), if (invoice.customerName != null) _InfoLine(label: 'Tên đơn vị', value: invoice.customerName!), if (invoice.buyerInfo!.taxCode != null) _InfoLine(label: 'Mã số thuế', value: invoice.buyerInfo!.taxCode!), if (invoice.buyerInfo!.fullAddress.isNotEmpty) _InfoLine(label: 'Địa chỉ', value: invoice.buyerInfo!.fullAddress), if (invoice.buyerInfo!.phone != null) _InfoLine(label: 'Điện thoại', value: invoice.buyerInfo!.phone!), if (invoice.buyerInfo!.email != null) _InfoLine(label: 'Email', value: invoice.buyerInfo!.email!), ]; } /// Build products card Widget _buildProductsCard(BuildContext context, Invoice invoice) { final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: colorScheme.outline.withValues(alpha: 0.1), width: 1.5, ), boxShadow: [ BoxShadow( color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section Title Row( children: [ FaIcon( FontAwesomeIcons.boxOpen, size: 18, color: colorScheme.onSurface, ), const SizedBox(width: 8), Text( 'Chi tiết hàng hóa', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), ], ), const SizedBox(height: 16), // Products Table if (invoice.items != null && invoice.items!.isNotEmpty) _buildProductsTable(context, invoice) else Center( child: Padding( padding: const EdgeInsets.all(20), child: Text( 'Không có thông tin sản phẩm', style: TextStyle( fontSize: 14, color: colorScheme.onSurface, ), ), ), ), const SizedBox(height: 20), // Invoice Summary _buildInvoiceSummary(context, invoice), ], ), ), ); } /// Build products table - with Đơn giá column Widget _buildProductsTable(BuildContext context, Invoice invoice) { final colorScheme = Theme.of(context).colorScheme; return Column( children: [ // Table Header Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), border: Border( bottom: BorderSide( color: colorScheme.outlineVariant, width: 2, ), ), ), child: Row( children: [ // # column SizedBox( width: 32, child: Text( '#', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ), // Tên hàng hóa column Expanded( flex: 3, child: Text( 'Tên hàng hóa', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ), // Số lượng column SizedBox( width: 55, child: Text( 'SL', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ), // Đơn giá column SizedBox( width: 80, child: Text( 'Đơn giá', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.right, ), ), // Thành tiền column SizedBox( width: 90, child: Text( 'Thành tiền', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.right, ), ), ], ), ), // Table Body ...invoice.items!.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.5)), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // # column SizedBox( width: 32, child: Text( '${index + 1}', style: TextStyle( fontSize: 14, color: colorScheme.onSurface, ), ), ), // Tên hàng hóa column Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.itemName, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: 2), Text( 'SKU: ${item.itemCode}', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), ), ], ), ), // Số lượng column SizedBox( width: 55, child: Text( item.qty.toStringAsFixed(item.qty.truncateToDouble() == item.qty ? 0 : 2), style: TextStyle( fontSize: 14, color: colorScheme.onSurface, ), textAlign: TextAlign.center, ), ), // Đơn giá column SizedBox( width: 80, child: Text( item.rate.toVNCurrency, style: TextStyle( fontSize: 13, color: colorScheme.onSurface, ), textAlign: TextAlign.right, ), ), // Thành tiền column SizedBox( width: 90, child: Text( item.amount.toVNCurrency, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), textAlign: TextAlign.right, ), ), ], ), ); }), ], ); } /// Build invoice summary Widget _buildInvoiceSummary(BuildContext context, Invoice invoice) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ // Subtotal if (invoice.total != null) _SummaryRow( label: 'Tổng tiền hàng:', value: invoice.total!.toVNCurrency, ), // Discount if (invoice.discountAmount != null && invoice.discountAmount! > 0) _SummaryRow( label: 'Chiết khấu:', value: '-${invoice.discountAmount!.toVNCurrency}', valueColor: const Color(0xFF059669), ), // Grand Total Container( padding: const EdgeInsets.only(top: 16), margin: const EdgeInsets.only(top: 8), decoration: BoxDecoration( border: Border( top: BorderSide(color: colorScheme.outlineVariant, width: 2), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'TỔNG THANH TOÁN:', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), Text( invoice.grandTotal.toVNCurrency, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: Color(0xFFDC2626), ), ), ], ), ), ], ), ); } /// Build action button Widget _buildActionButton(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _contactSupport(context), icon: const FaIcon(FontAwesomeIcons.comments, size: 18), label: const Text('Liên hệ hỗ trợ'), style: OutlinedButton.styleFrom( foregroundColor: colorScheme.onSurface, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), side: BorderSide(color: colorScheme.outlineVariant, width: 2), ), ), ); } /// Build error state Widget _buildErrorState(BuildContext context, WidgetRef ref, Object error) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon( FontAwesomeIcons.circleExclamation, size: 64, color: colorScheme.error.withValues(alpha: 0.7), ), const SizedBox(height: 16), Text( 'Có lỗi xảy ra', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( error.toString(), style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.invalidate(invoiceDetailProvider(invoiceId)), child: const Text('Thử lại'), ), ], ), ); } /// Share invoice void _shareInvoice(BuildContext context) { SharePlus.instance.share( ShareParams( text: 'Chi tiết hóa đơn #$invoiceId - EuroTile Worker', subject: 'Hóa đơn #$invoiceId', ), ); } /// Contact support void _contactSupport(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Hotline hỗ trợ: 1900 1234'), duration: Duration(seconds: 3), ), ); } } /// Status Badge Widget - with uppercase text class _StatusBadge extends StatelessWidget { const _StatusBadge({ required this.status, required this.statusColor, }); final String status; final String statusColor; @override Widget build(BuildContext context) { Color backgroundColor; Color textColor; switch (statusColor.toLowerCase()) { case 'success': backgroundColor = const Color(0xFFD1FAE5); textColor = const Color(0xFF065F46); case 'danger': backgroundColor = const Color(0xFFFEF3C7); textColor = const Color(0xFFD97706); case 'warning': backgroundColor = const Color(0xFFFEF3C7); textColor = const Color(0xFFD97706); case 'info': backgroundColor = const Color(0xFFE0E7FF); textColor = const Color(0xFF3730A3); default: backgroundColor = const Color(0xFFF3F4F6); textColor = const Color(0xFF6B7280); } return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(20), ), child: Text( status.toUpperCase(), style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: textColor, ), ), ); } } /// Meta Item Widget class _MetaItem extends StatelessWidget { const _MetaItem({ required this.label, required this.value, }); final String label; final String value; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Column( children: [ Text( label, style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ); } } /// Company Info Block Widget class _CompanyInfoBlock extends StatelessWidget { const _CompanyInfoBlock({ required this.icon, required this.iconColor, required this.title, required this.lines, }); final IconData icon; final Color iconColor; final String title; final List<_InfoLine> lines; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ FaIcon(icon, size: 16, color: iconColor), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), ], ), const SizedBox(height: 12), ...lines, ], ); } } /// Info Line Widget class _InfoLine extends StatelessWidget { const _InfoLine({ required this.label, required this.value, }); final String label; final String value; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 6), child: RichText( text: TextSpan( style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, height: 1.6, ), children: [ TextSpan( text: '$label: ', style: const TextStyle(fontWeight: FontWeight.w400), ), TextSpan( text: value, style: TextStyle( fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ], ), ), ); } } /// Summary Row Widget class _SummaryRow extends StatelessWidget { const _SummaryRow({ required this.label, required this.value, this.valueColor, }); final String label; final String value; final Color? valueColor; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: 15, color: colorScheme.onSurfaceVariant, ), ), Text( value, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: valueColor ?? colorScheme.onSurface, ), ), ], ), ); } }