Files
tm_app/lib/view_models/your_tenders_view_model.dart
T
AmirReza Jamali 72881df210
continuous-integration/drone/push Build encountered an error
Implement tender workflows and dashboard integration
2026-07-14 11:12:09 +03:30

509 lines
15 KiB
Dart

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 = <String>[
'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<void> 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<TenderApprovalsResponse>():
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<TenderApprovalsResponse>():
errorMessage = ErrorUtils.getErrorMessage(result.error);
break;
}
isLoading = false;
isLoadingMoreTenders = false;
notifyListeners();
}
Future<void> 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<LikedTendersResponse>():
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<LikedTendersResponse>():
errorMessage = ErrorUtils.getErrorMessage(result.error);
break;
}
isLoading = false;
isLoadingMore = false;
notifyListeners();
}
Future<void> loadMoreFeedback() async {
if (_currentFeedbackType == null) {
return;
}
await getTendersFeedback(feedbackType: _currentFeedbackType!, reset: false);
}
Future<void> 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<void> 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<void> 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<void> selectSubmissionStage(String stage) async {
if (!submissionStages.contains(stage) || stage == selectedSubmissionStage) {
return;
}
selectedSubmissionStage = stage;
submissionActionError = null;
currentPage = 1;
notifyListeners();
await loadSubmissions();
}
Future<void> 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<TenderSubmissionsResponse>():
submissionsData = result.value;
currentPage = result.value.meta?.page ?? 1;
case Error<TenderSubmissionsResponse>():
errorMessage = ErrorUtils.getErrorMessage(result.error);
}
final statsResult = await yourTendersRepository.getSubmissionStats();
switch (statsResult) {
case Ok<TenderSubmissionStatsResponse>():
submissionStats = statsResult.value.data;
case Error<TenderSubmissionStatsResponse>():
// The list remains useful when stats are temporarily unavailable.
break;
}
isLoading = false;
notifyListeners();
}
Future<bool> 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<TenderSubmissionResponse>():
updatingSubmissionId = null;
await loadSubmissions();
return true;
case Error<TenderSubmissionResponse>():
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;
}
}