import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/date_utils.dart'; import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart'; import 'package:tm_app/view_models/your_tenders_view_model.dart'; class SubmissionWorkflow extends StatelessWidget { const SubmissionWorkflow({super.key}); static const _stageLabels = { 'opportunity': 'Opportunity', 'in_progress': 'In Progress', 'submitted': 'Submitted', 'not_applied': 'Not Applied', }; @override Widget build(BuildContext context) { return Consumer( builder: (context, viewModel, _) { final submissions = viewModel.submissionsData?.data ?? const []; return Column( children: [ SizedBox( height: 54, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 6, ), children: YourTendersViewModel.submissionStages.map((stage) { final selected = viewModel.selectedSubmissionStage == stage; final count = viewModel.submissionStats?.byStage[stage] ?? 0; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( selected: selected, onSelected: (_) => viewModel.selectSubmissionStage(stage), label: Text('${_stageLabels[stage]} ($count)'), selectedColor: AppColors.secondary20, side: BorderSide( color: selected ? AppColors.secondary50 : AppColors.grey30, ), ), ); }).toList(), ), ), if (viewModel.submissionActionError != null) Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), padding: const EdgeInsets.fromLTRB(12, 8, 4, 8), decoration: BoxDecoration( color: AppColors.red0, border: Border.all(color: AppColors.red10), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.error_outline, color: AppColors.error, size: 18), const SizedBox(width: 8), Expanded( child: Text( viewModel.submissionActionError!, style: TextStyle(color: AppColors.error, fontSize: 12), ), ), IconButton( visualDensity: VisualDensity.compact, onPressed: viewModel.dismissSubmissionActionError, icon: const Icon(Icons.close, size: 18), ), ], ), ), if (viewModel.isLoading && submissions.isEmpty) const Expanded( child: Center( child: CircularProgressIndicator( color: AppColors.secondary50, ), ), ) else if (submissions.isEmpty) Expanded( child: Center( child: Text( 'No ${_stageLabels[viewModel.selectedSubmissionStage]?.toLowerCase()} submissions.', style: TextStyle(color: AppColors.grey60), ), ), ) else Expanded( child: RefreshIndicator( onRefresh: viewModel.loadSubmissions, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), itemCount: submissions.length, itemBuilder: (context, index) => _SubmissionCard(submission: submissions[index]), ), ), ), if ((viewModel.submissionsData?.meta?.pages ?? 1) > 1) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( tooltip: 'Previous page', onPressed: viewModel.currentPage > 1 && !viewModel.isLoading ? () => viewModel.jumpToPage( viewModel.currentPage - 1, ) : null, icon: const Icon(Icons.chevron_left), ), Text( 'Page ${viewModel.currentPage} of ${viewModel.totalPages}', style: TextStyle(color: AppColors.grey60), ), IconButton( tooltip: 'Next page', onPressed: viewModel.currentPage < viewModel.totalPages && !viewModel.isLoading ? () => viewModel.jumpToPage( viewModel.currentPage + 1, ) : null, icon: const Icon(Icons.chevron_right), ), ], ), ), ], ); }, ); } } class _SubmissionCard extends StatelessWidget { const _SubmissionCard({required this.submission}); final TenderSubmission submission; static const _statusLabels = { 'opportunity': 'Opportunity', 'validating': 'Validating', 'partner_pending': 'Partner Pending', 'applying': 'Applying', 'sent_waiting': 'Sent / Waiting', 'rejected': 'Lost', 'contract': 'Contract Awarded', 'not_applied': 'Not Applied', }; @override Widget build(BuildContext context) { final viewModel = context.watch(); final tender = submission.tender; final busy = viewModel.updatingSubmissionId == submission.id; final deadline = tender?.submissionDeadline ?? tender?.tenderDeadline; return Card( color: AppColors.cardBackground, elevation: 0, margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: AppColors.borderColor), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( tender?.title?.trim().isNotEmpty == true ? tender!.title! : 'Tender ${submission.tenderId}', style: TextStyle( color: AppColors.grey80, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 12), _StatusBadge( label: _statusLabels[submission.status] ?? submission.status, status: submission.status, ), ], ), if (tender?.description?.trim().isNotEmpty == true) ...[ const SizedBox(height: 8), Text( tender!.description!, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(color: AppColors.grey70, height: 1.4), ), ], const SizedBox(height: 12), Wrap( spacing: 16, runSpacing: 6, children: [ if (tender?.countryCode?.isNotEmpty == true) _Meta( icon: Icons.location_on_outlined, text: tender!.countryCode!, ), if (deadline != null) _Meta( icon: Icons.event_outlined, text: 'Deadline ${timeConvertor(deadline)}', ), if (tender?.buyerOrganization?.name?.isNotEmpty == true) _Meta( icon: Icons.business_outlined, text: tender!.buyerOrganization!.name!, ), ], ), if (submission.statusHistory.isNotEmpty) ...[ const SizedBox(height: 4), ExpansionTile( tilePadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero, dense: true, title: Text( 'Activity (${submission.statusHistory.length})', style: TextStyle(color: AppColors.grey60, fontSize: 13), ), children: submission.statusHistory.reversed .map( (item) => ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: const Icon(Icons.circle, size: 8), title: Text( _statusLabels[item.status] ?? item.status, style: TextStyle(color: AppColors.grey80), ), subtitle: Text( [ timeConvertor(item.changedAt, hasTime: true), if (item.description?.isNotEmpty == true) item.description!, if (item.reason?.isNotEmpty == true) item.reason!, ].join(' ยท '), ), ), ) .toList(), ), ], if (!submission.isTerminal) ...[ const SizedBox(height: 12), if (busy) const LinearProgressIndicator(color: AppColors.secondary50) else _Actions(submission: submission), ], ], ), ), ); } } class _Actions extends StatelessWidget { const _Actions({required this.submission}); final TenderSubmission submission; @override Widget build(BuildContext context) { final primary = <({String label, String status, String description})>[]; switch (submission.status) { case 'opportunity': primary.add(( label: 'Start validation', status: 'validating', description: 'User accepted tender', )); case 'validating': if (submission.submissionMode == 'partnership') { primary.add(( label: 'Wait for partner', status: 'partner_pending', description: 'Validation completed; waiting for partner documents', )); } else if (submission.submissionMode == 'self-apply') { primary.add(( label: 'Start applying', status: 'applying', description: 'Validation completed', )); } else { primary.addAll([ ( label: 'Start applying', status: 'applying', description: 'Validation completed', ), ( label: 'Wait for partner', status: 'partner_pending', description: 'Validation completed; waiting for partner documents', ), ]); } case 'partner_pending': primary.add(( label: 'Partner docs ready', status: 'applying', description: 'Partner documents received', )); case 'applying': primary.add(( label: 'Submitted to buyer', status: 'sent_waiting', description: 'Application filed with procurement authority', )); case 'sent_waiting': primary.addAll([ ( label: 'Mark won', status: 'contract', description: 'Contract awarded', ), ( label: 'Mark lost', status: 'rejected', description: 'Buyer rejected bid', ), ]); } return Wrap( spacing: 8, runSpacing: 8, children: [ for (final action in primary) FilledButton( onPressed: () => _confirmUpdate( context, status: action.status, label: action.label, description: action.description, ), style: FilledButton.styleFrom( backgroundColor: AppColors.secondary50, ), child: Text(action.label), ), if (submission.status != 'sent_waiting') OutlinedButton( onPressed: () => _reject(context), style: OutlinedButton.styleFrom(foregroundColor: AppColors.error), child: const Text('Do not apply'), ), ], ); } Future _confirmUpdate( BuildContext context, { required String status, required String label, required String description, }) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(label), content: const Text('Update this tender submission?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: const Text('Confirm'), ), ], ), ); if (confirmed == true && context.mounted) { await context.read().updateSubmissionStatus( submission: submission, status: status, description: description, ); } } Future _reject(BuildContext context) async { final controller = TextEditingController(); final reason = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Do not apply'), content: TextField( controller: controller, maxLength: 500, maxLines: 3, decoration: const InputDecoration( labelText: 'Reason (optional)', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, controller.text), child: const Text('Confirm'), ), ], ), ); controller.dispose(); if (reason != null && context.mounted) { await context.read().updateSubmissionStatus( submission: submission, status: 'not_applied', reason: reason, ); } } } class _StatusBadge extends StatelessWidget { const _StatusBadge({required this.label, required this.status}); final String label; final String status; @override Widget build(BuildContext context) { final terminal = status == 'rejected' || status == 'not_applied'; final won = status == 'contract'; final color = terminal ? AppColors.error : (won ? AppColors.green30 : AppColors.blue10); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withValues(alpha: .12), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w600, ), ), ); } } class _Meta extends StatelessWidget { const _Meta({required this.icon, required this.text}); final IconData icon; final String text; @override Widget build(BuildContext context) => Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: AppColors.grey60), const SizedBox(width: 4), Text(text, style: TextStyle(color: AppColors.grey60, fontSize: 12)), ], ); }