import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/error_utils.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/network/app_exceptions.dart'; import 'package:tm_app/data/repositories/your_tenders_repository.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart'; import 'package:tm_app/views/your_tenders/strings/your_tenders_strings.dart'; import '../core/constants/assets.dart'; import '../core/constants/tender_approval_status.dart'; import '../core/constants/tender_feedback.dart'; import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; import '../data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart'; class YourTendersViewModel with ChangeNotifier { final YourTendersRepository yourTendersRepository; YourTendersViewModel({required this.yourTendersRepository}); bool isLoading = false; String? errorMessage; TenderApprovalsResponse? data; LikedTendersResponse? likedTendersData; TenderSubmissionsResponse? submissionsData; TenderSubmissionStats? submissionStats; String selectedSubmissionStage = 'opportunity'; String? updatingSubmissionId; String? submissionActionError; bool get isSubmissionWorkflow => selectedStatus == TenderApprovalStatus.submitted.value; static const submissionStages = [ 'opportunity', 'in_progress', 'submitted', 'not_applied', ]; // Pagination state for feedback lists bool isLoadingMore = false; bool hasMore = true; int _offset = 0; final int _limit = 10; String? _currentFeedbackType; // Pagination state for tenders bool isLoadingMoreTenders = false; bool hasMoreTenders = true; int _tendersOffset = 0; final int _tendersLimit = 10; // Filter States DateTimeRange? selectedDateRange; String selectedStatus = 'All'; final _fmt = DateFormat('yyyy/MM/dd'); String get startDateFormatted => selectedDateRange?.start != null ? _fmt.format(selectedDateRange!.start) : 'YYYY/MM/DD'; String get endDateFormatted => selectedDateRange?.end != null ? _fmt.format(selectedDateRange!.end) : 'YYYY/MM/DD'; String get startDateUnix => selectedDateRange?.start != null ? (selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000).toString() : ''; String get endDateUnix => selectedDateRange?.end != null ? (selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000).toString() : ''; Future getTenders({ required GetTendersRequestModel requestModel, bool reset = false, }) async { if (!reset && (isLoadingMoreTenders || !hasMoreTenders)) { return; } if (reset) { _tendersOffset = 0; hasMoreTenders = true; data = null; } if (_tendersOffset == 0) { isLoading = true; } else { isLoadingMoreTenders = true; } notifyListeners(); final result = await yourTendersRepository.getTenders( requestModel: requestModel, ); switch (result) { case Ok(): final incoming = result.value; final incomingList = incoming.data?.tenders ?? []; if (reset || data == null) { data = incoming; } else { final existingList = data!.data?.tenders ?? []; final combined = List.of(existingList)..addAll(incomingList); final newMetadata = incoming.data?.metadata ?? data!.data?.metadata; data = data!.copyWith( data: data!.data!.copyWith( tenders: combined, metadata: newMetadata, ), ); } hasMoreTenders = incomingList.length == _tendersLimit; _tendersOffset += _tendersLimit; errorMessage = null; break; case Error(): errorMessage = ErrorUtils.getErrorMessage(result.error); break; } isLoading = false; isLoadingMoreTenders = false; notifyListeners(); } Future getTendersFeedback({ required String feedbackType, bool reset = false, }) async { if (reset) { _offset = (currentPage - 1) * _limit; hasMore = true; _currentFeedbackType = feedbackType; } if (!reset && (isLoadingMore || !hasMore)) { return; } if (_offset == 0) { isLoading = true; } else { isLoadingMore = true; } notifyListeners(); final requestModel = TenderFeedbackRequestModel( offset: _offset, limit: _limit, feedbackType: _currentFeedbackType ?? feedbackType, dateFrom: startDateUnix, dateTo: endDateUnix, ); final result = await yourTendersRepository.getTendersFeedback( requestModel: requestModel, ); switch (result) { case Ok(): final incoming = result.value; final incomingList = incoming.data?.feedback ?? []; if (reset || likedTendersData == null) { likedTendersData = incoming; } else { final existingList = likedTendersData!.data?.feedback ?? []; final combined = List.of(existingList)..addAll(incomingList); final newMeta = incoming.data?.meta ?? likedTendersData!.data?.meta; likedTendersData = likedTendersData!.copyWith( data: likedTendersData!.data!.copyWith( feedback: combined, meta: newMeta, ), ); } hasMore = incomingList.length == _limit; _offset += _limit; errorMessage = null; break; case Error(): errorMessage = ErrorUtils.getErrorMessage(result.error); break; } isLoading = false; isLoadingMore = false; notifyListeners(); } Future loadMoreFeedback() async { if (_currentFeedbackType == null) { return; } await getTendersFeedback(feedbackType: _currentFeedbackType!, reset: false); } Future loadMoreTenders() async { if (isLoadingMoreTenders || !hasMoreTenders) { return; } if (selectedStatus == YourTendersStrings.all) { final requestModel = GetTendersRequestModel( status: '', createdFrom: startDateUnix, createdTo: endDateUnix, limit: _tendersLimit, offset: _tendersOffset, ); await getTenders(requestModel: requestModel); } else if (selectedStatus == YourTendersStrings.tendersSubmitted || selectedStatus == TenderApprovalStatus.submitted.value || selectedStatus == TenderApprovalStatus.approved.value) { final requestModel = GetTendersRequestModel( status: TenderApprovalStatus.submitted.value, createdFrom: startDateUnix, createdTo: endDateUnix, limit: _tendersLimit, offset: _tendersOffset, ); await getTenders(requestModel: requestModel); } } // --- Filter Actions --- void setDateRange(DateTimeRange? range) { selectedDateRange = range; notifyListeners(); } void setStatus(String status) { // Clear data when status changes _clearAllData(); selectedStatus = status; currentPage = 1; notifyListeners(); } void clearFilter() { // Clear data when clearing filter _clearAllData(); selectedDateRange = null; selectedStatus = YourTendersStrings.all; notifyListeners(); } // Clear all data and reset states void _clearAllData() { // Clear main data data = null; likedTendersData = null; submissionsData = null; submissionStats = null; // Clear loading states isLoading = false; isLoadingMore = false; isLoadingMoreTenders = false; // Clear error messages errorMessage = null; // Reset pagination states _offset = 0; _tendersOffset = 0; hasMore = true; hasMoreTenders = true; _currentFeedbackType = null; notifyListeners(); } Future callWithFilter() async { likedTendersData = null; data = null; submissionsData = null; isLoading = true; notifyListeners(); if (selectedStatus == YourTendersStrings.all) { final requestModel = GetTendersRequestModel( status: '', createdFrom: startDateUnix, createdTo: endDateUnix, limit: _tendersLimit, offset: _tendersOffset, ); await getTenders(requestModel: requestModel, reset: true); } else if (selectedStatus == TenderApprovalStatus.submitted.value) { selectedSubmissionStage = 'submitted'; await loadSubmissions(); } else if (selectedStatus == TenderFeedback.liked.value) { await getTendersFeedback( feedbackType: TenderFeedback.liked.value, reset: true, ); } else if (selectedStatus == TenderFeedback.disliked.value) { await getTendersFeedback( feedbackType: TenderFeedback.disliked.value, reset: true, ); } else if (selectedStatus == TenderApprovalStatus.approved.value) { // "Approved tenders" maps to submitted tender-approvals. The API has no // `approved` value; approved tenders are the ones a company has submitted // for pursuit (GET /api/v1/tender-approvals?status=submitted). final requestModel = GetTendersRequestModel( status: TenderApprovalStatus.submitted.value, createdFrom: startDateUnix, createdTo: endDateUnix, limit: _tendersLimit, offset: _tendersOffset, ); await getTenders(requestModel: requestModel, reset: true); } } int currentPage = 1; int get totalPages { if (isSubmissionWorkflow && submissionsData?.meta != null) { return submissionsData!.meta!.pages == 0 ? 1 : submissionsData!.meta!.pages; } if (likedTendersData?.data?.meta?.pages != null) { return likedTendersData!.data!.meta!.pages!; } if (data?.data?.metadata?.pages != null) { return data!.data!.metadata!.pages!; } return 1; } Future jumpToPage(int page) async { currentPage = page; if (isSubmissionWorkflow) { await loadSubmissions(offset: (page - 1) * 20); return; } _offset = (page - 1) * _limit; _tendersOffset = (page - 1) * _tendersLimit; likedTendersData = null; data = null; hasMore = true; hasMoreTenders = true; await callWithFilter(); } Future selectSubmissionStage(String stage) async { if (!submissionStages.contains(stage) || stage == selectedSubmissionStage) { return; } selectedSubmissionStage = stage; submissionActionError = null; currentPage = 1; notifyListeners(); await loadSubmissions(); } Future loadSubmissions({int offset = 0}) async { isLoading = true; errorMessage = null; notifyListeners(); final result = await yourTendersRepository.getSubmissions( stage: selectedSubmissionStage, limit: 20, offset: offset, ); switch (result) { case Ok(): submissionsData = result.value; currentPage = result.value.meta?.page ?? 1; case Error(): errorMessage = ErrorUtils.getErrorMessage(result.error); } final statsResult = await yourTendersRepository.getSubmissionStats(); switch (statsResult) { case Ok(): submissionStats = statsResult.value.data; case Error(): // The list remains useful when stats are temporarily unavailable. break; } isLoading = false; notifyListeners(); } Future updateSubmissionStatus({ required TenderSubmission submission, required String status, String? reason, String? description, }) async { if (submission.id.isEmpty || submission.isTerminal) { return false; } updatingSubmissionId = submission.id; submissionActionError = null; notifyListeners(); final result = await yourTendersRepository.updateSubmissionStatus( id: submission.id, status: status, reason: reason, description: description, ); switch (result) { case Ok(): updatingSubmissionId = null; await loadSubmissions(); return true; case Error(): updatingSubmissionId = null; if (result.error is AppException && (result.error as AppException).statusCode == 409) { await loadSubmissions(); submissionActionError = 'This submission changed elsewhere. The latest state has been loaded; please try again.'; notifyListeners(); return false; } submissionActionError = ErrorUtils.getErrorMessage(result.error); notifyListeners(); return false; } } void dismissSubmissionActionError() { submissionActionError = null; notifyListeners(); } Color getBackGroundColorByStatus(String status) { if (status == TenderFeedback.liked.value || status == TenderApprovalStatus.submitted.value) { return AppColors.green0; } else if (status == TenderFeedback.disliked.value || status == TenderApprovalStatus.rejected.value) { return AppColors.red0; } return AppColors.grey0; } Color getBorderColorByStatus(String status) { if (status == TenderFeedback.liked.value || status == TenderApprovalStatus.submitted.value) { return AppColors.green10; } else if (status == TenderFeedback.disliked.value || status == TenderApprovalStatus.rejected.value) { return AppColors.red10; } return AppColors.grey30; } Color getStatusCardColorByStatus(String status) { if (status == TenderFeedback.liked.value || status == TenderApprovalStatus.submitted.value) { return AppColors.green20; } else if (status == TenderFeedback.disliked.value || status == TenderApprovalStatus.rejected.value) { return AppColors.red10; } return AppColors.grey0; } Color getStatusTextColorByStatus(String status) { if (status == TenderFeedback.liked.value || status == TenderApprovalStatus.submitted.value) { return AppColors.green30; } else if (status == TenderFeedback.disliked.value || status == TenderApprovalStatus.rejected.value) { return AppColors.red20; } return AppColors.grey60; } String? getStatusIconByStatus(String status) { if (status == TenderFeedback.liked.value || status == TenderApprovalStatus.submitted.value) { return AssetsManager.tickCircle; } else if (status == TenderFeedback.disliked.value || status == TenderApprovalStatus.rejected.value) { return AssetsManager.closeCircle; } return null; } }