/// Page: Quotes Page /// /// Displays list of quote requests with search and filter functionality. library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/database/models/enums.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/quotes/data/models/quote_model.dart'; import 'package:worker/features/quotes/presentation/providers/quotes_provider.dart'; /// Quotes Page /// /// Features: /// - Search bar for quote numbers and project names /// - Filter section /// - List of quote request cards with status indicators /// - Pull-to-refresh class QuotesPage extends ConsumerStatefulWidget { const QuotesPage({super.key}); @override ConsumerState createState() => _QuotesPageState(); } class _QuotesPageState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _searchController.addListener(_onSearchChanged); } @override void dispose() { _searchController ..removeListener(_onSearchChanged) ..dispose(); super.dispose(); } void _onSearchChanged() { ref .read(quoteSearchQueryProvider.notifier) .updateQuery(_searchController.text); } @override Widget build(BuildContext context) { final filteredQuotesAsync = ref.watch(filteredQuotesProvider); return Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => context.pop(), ), title: const Text( 'Yêu cầu báo giá', style: TextStyle(color: Colors.black), ), actions: [ IconButton( icon: const Icon(Icons.add, color: Colors.black), onPressed: () { // TODO: Navigate to quote create page ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tạo yêu cầu báo giá mới')), ); }, ), ], elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, foregroundColor: AppColors.grey900, centerTitle: false, ), body: RefreshIndicator( onRefresh: () async { await ref.read(quotesProvider.notifier).refresh(); }, child: CustomScrollView( slivers: [ // Search Bar and Filter Button SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: _buildSearchAndFilterRow(), ), ), // Quotes List SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), sliver: filteredQuotesAsync.when( data: (quotes) { if (quotes.isEmpty) { return _buildEmptyState(); } return SliverList( delegate: SliverChildBuilderDelegate((context, index) { final quote = quotes[index]; return _buildQuoteCard(quote); }, childCount: quotes.length), ); }, loading: () => _buildLoadingState(), error: (error, stack) => _buildErrorState(error), ), ), ], ), ), ); } /// Build search bar and filter button row Widget _buildSearchAndFilterRow() { return Row( children: [ // Search Bar Expanded( child: Container( decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Tìm theo mã yêu cầu hoặc tên dự án', hintStyle: const TextStyle( color: AppColors.grey500, fontSize: 14, ), prefixIcon: const Icon(Icons.search, color: AppColors.grey500), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, color: AppColors.grey500), onPressed: () { _searchController.clear(); }, ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), ), ), ), const SizedBox(width: 12), // Filter Button Container( decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: IconButton( onPressed: () { _showFilterDialog(); }, icon: Icon(Icons.filter_list, color: AppColors.primaryBlue), iconSize: 24, padding: const EdgeInsets.all(12), constraints: const BoxConstraints(minWidth: 48, minHeight: 48), ), ), ], ); } /// Build quote card Widget _buildQuoteCard(QuoteModel quote) { final dateFormatter = DateFormat('dd/MM/yyyy'); return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { // TODO: Navigate to quote detail ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Chi tiết báo giá ${quote.quoteNumber}')), ); }, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Quote ID and Date Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '#${quote.quoteNumber}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.grey900, ), ), Text( dateFormatter.format(quote.createdAt), style: const TextStyle( fontSize: 12, color: AppColors.grey500, ), ), ], ), const SizedBox(height: 8), // Project Name if (quote.projectName != null) Text( 'Dự án: ${quote.projectName}', style: const TextStyle( fontSize: 14, color: AppColors.grey900, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), // Additional Info (placeholder for now) const Text( '5 sản phẩm - Diện tích: 200m²', style: TextStyle(fontSize: 13, color: AppColors.grey500), ), const SizedBox(height: 8), // Status Badge _buildStatusBadge(quote.status), const SizedBox(height: 8), // Notes if (quote.notes != null && quote.notes!.isNotEmpty) Text( quote.notes!, style: const TextStyle( fontSize: 13, color: AppColors.grey500, fontStyle: FontStyle.italic, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ), ); } /// Build status badge Widget _buildStatusBadge(QuoteStatus status) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: _getStatusColor(status).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: _getStatusColor(status).withValues(alpha: 0.3), width: 1, ), ), child: Text( _getStatusText(status), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _getStatusColor(status), ), ), ); } /// Get status color Color _getStatusColor(QuoteStatus status) { switch (status) { case QuoteStatus.draft: return AppColors.warning; case QuoteStatus.sent: return AppColors.info; case QuoteStatus.viewed: return const Color(0xFFF59E0B); // yellow/orange case QuoteStatus.accepted: return AppColors.primaryBlue; case QuoteStatus.rejected: return AppColors.danger; case QuoteStatus.expired: return AppColors.grey500; case QuoteStatus.converted: return AppColors.success; case QuoteStatus.cancelled: return AppColors.grey500; } } /// Get status text String _getStatusText(QuoteStatus status) { switch (status) { case QuoteStatus.draft: return 'Chờ duyệt'; case QuoteStatus.sent: return 'Đã gửi'; case QuoteStatus.viewed: return 'Đang đàm phán'; case QuoteStatus.accepted: return 'Đã chốt'; case QuoteStatus.rejected: return 'Từ chối'; case QuoteStatus.expired: return 'Hết hạn'; case QuoteStatus.converted: return 'Đã thành đơn hàng'; case QuoteStatus.cancelled: return 'Đã hủy'; } } /// Show filter dialog void _showFilterDialog() { showDialog( context: context, builder: (context) { final selectedStatus = ref.read(selectedQuoteStatusProvider); return AlertDialog( title: const Text('Lọc theo trạng thái'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildFilterOption( context, 'Tất cả', null, selectedStatus == null, ), ...QuoteStatus.values.map((status) { return _buildFilterOption( context, _getStatusText(status), status, selectedStatus == status, ); }), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Đóng'), ), ], ); }, ); } /// Build filter option Widget _buildFilterOption( BuildContext context, String label, QuoteStatus? status, bool isSelected, ) { return RadioListTile( title: Text(label), value: status, groupValue: ref.watch(selectedQuoteStatusProvider), onChanged: (value) { ref.read(selectedQuoteStatusProvider.notifier).selectStatus(value); Navigator.of(context).pop(); }, activeColor: AppColors.primaryBlue, ); } /// Build empty state Widget _buildEmptyState() { return SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.receipt_long_outlined, size: 80, color: AppColors.grey500.withValues(alpha: 0.5), ), const SizedBox(height: 16), const Text( 'Không có yêu cầu báo giá 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), ), ], ), ), ); } /// Build loading state Widget _buildLoadingState() { return const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), ); } /// Build error state Widget _buildErrorState(Object error) { return SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 80, color: AppColors.danger.withValues(alpha: 0.7), ), const SizedBox(height: 16), const Text( 'Có lỗi xảy ra', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.grey900, ), ), const SizedBox(height: 8), Text( error.toString(), style: const TextStyle(fontSize: 14, color: AppColors.grey500), textAlign: TextAlign.center, ), ], ), ), ); } }