Implement tender workflows and dashboard integration
continuous-integration/drone/push Build encountered an error

This commit is contained in:
AmirReza Jamali
2026-07-14 11:12:09 +03:30
parent ce170f9bc8
commit 72881df210
29 changed files with 1820 additions and 159 deletions
+33 -7
View File
@@ -4,6 +4,8 @@ import 'package:tm_app/data/services/model/recommended_tenders_response/recommen
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/data/services/model/feedback_stats_response/feedback_stat_response.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
import '../data/repositories/home_repository.dart';
import '../data/services/model/home/home_response/home_response_model.dart';
@@ -44,6 +46,7 @@ class HomeViewModel with ChangeNotifier {
RecommendedTendersResponse? _recommendedTendersResponse;
TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
TenderSubmissionStats? tenderSubmissionStats;
int userLikedTendersCount = 0;
final List<TenderData> _tenders = [];
final int _pageSize = 10;
@@ -71,11 +74,15 @@ class HomeViewModel with ChangeNotifier {
_isRecommendedMode = false;
data = null;
tenderApprovalsStateResponse = null;
tenderSubmissionStats = null;
notifyListeners();
await getApprovedStates();
await getYourTenders(reset: true);
await loadUserLikedTendersCount();
await Future.wait([
getApprovedStates(),
getYourTenders(reset: true),
loadUserLikedTendersCount(),
loadSubmissionStats(),
]);
if (_errorMessage != null) {
_isLoading = false;
@@ -199,19 +206,38 @@ class HomeViewModel with ChangeNotifier {
}
Future<void> loadUserLikedTendersCount() async {
final result = await _homeRepository.getUserLikedTendersCount();
final result = await _homeRepository.getCustomerFeedbackStats();
switch (result) {
case Ok<int>():
userLikedTendersCount = result.value;
case Ok<FeedbackStatResponse>():
userLikedTendersCount = result.value.data?.totalLikes ?? 0;
break;
case Error<int>():
case Error<FeedbackStatResponse>():
userLikedTendersCount = 0;
break;
}
notifyListeners();
}
Future<void> loadSubmissionStats() async {
final result = await _homeRepository.getSubmissionStats();
switch (result) {
case Ok<TenderSubmissionStatsResponse>():
tenderSubmissionStats = result.value.data;
break;
case Error<TenderSubmissionStatsResponse>():
tenderSubmissionStats = null;
break;
}
notifyListeners();
}
int get approvedTendersCount =>
tenderApprovalsStateResponse?.data?.submittedTenders ?? 0;
int get submittedTendersCount =>
tenderSubmissionStats?.byStage['submitted'] ?? 0;
double get partnershipCount {
return tenderApprovalsStateResponse?.data?.partnershipCount?.toDouble() ??
0.0;
+60 -15
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tm_app/core/utils/device_utils.dart';
import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/core/network/app_exceptions.dart';
import 'package:tm_app/data/repositories/tenders_repository.dart';
import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart';
import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart';
@@ -36,6 +37,12 @@ class TendersViewModel with ChangeNotifier {
String? get searchQuery => _searchQuery;
List<FeedbackData> feedbacks = [];
final Set<String> _hydratedFeedbackTenderIds = <String>{};
final Set<String> _feedbackHydrationsInFlight = <String>{};
final Set<String> _feedbackRequestsInFlight = <String>{};
bool isFeedbackLoading(String tenderId) =>
_feedbackRequestsInFlight.contains(tenderId);
static const int _pageSize = 10;
@@ -207,8 +214,7 @@ class TendersViewModel with ChangeNotifier {
_currentOffset = meta!.offset!;
}
final tenders = _tendersResponse?.data?.tenders ?? [];
_hasMoreData =
meta?.hasMore ?? (tenders.length >= _pageSize);
_hasMoreData = meta?.hasMore ?? (tenders.length >= _pageSize);
if (tenders.isNotEmpty) {
if (fetchAllFeedbacks) {
for (final tender in tenders) {
@@ -263,8 +269,13 @@ class TendersViewModel with ChangeNotifier {
data: result.value.data?.copyWith(tenders: combined),
);
final newMeta = result.value.meta;
_hasMoreData =
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
_hasMoreData = newMeta?.hasMore ?? (newTenders.length >= _pageSize);
for (final tender in newTenders) {
final id = tender.id;
if (id != null && !_hydratedFeedbackTenderIds.contains(id)) {
getTenderFeedback(id);
}
}
} else {
_hasMoreData = false;
}
@@ -283,14 +294,18 @@ class TendersViewModel with ChangeNotifier {
}
Future<void> getTenderFeedback(String tenderId) async {
_errorMessage = null;
notifyListeners();
if (_hydratedFeedbackTenderIds.contains(tenderId) ||
_feedbackHydrationsInFlight.contains(tenderId)) {
return;
}
_feedbackHydrationsInFlight.add(tenderId);
final result = await _tendersRepository.getTenderFeedback(
tenderId: tenderId,
);
switch (result) {
case Ok<FeedbackResponse>():
_hydratedFeedbackTenderIds.add(tenderId);
_feedbackData = result.value.data;
if (_feedbackData != null) {
final index = feedbacks.indexWhere(
@@ -311,19 +326,33 @@ class TendersViewModel with ChangeNotifier {
}
break;
case Error<FeedbackResponse>():
_errorMessage = result.error.toString();
if (result.error is NotFoundException) {
// A 404 is the expected representation of neutral feedback state.
feedbacks.removeWhere((feedback) => feedback.tenderId == tenderId);
_hydratedFeedbackTenderIds.add(tenderId);
} else {
_feedbackErrorMessage = result.error.toString();
}
break;
}
_feedbackHydrationsInFlight.remove(tenderId);
notifyListeners();
_errorMessage = null;
notifyListeners();
if (_feedbackErrorMessage != null) {
_feedbackErrorMessage = null;
notifyListeners();
}
}
Future<void> toogleFeedback({
Future<void> toggleFeedback({
required String tenderId,
required String feedbackType,
}) async {
if (_feedbackRequestsInFlight.contains(tenderId)) {
return;
}
_feedbackErrorMessage = null;
_feedbackRequestsInFlight.add(tenderId);
final existingFeedback = getFeedbackForTender(tenderId);
final existingFeedbackIndex = feedbacks.indexWhere(
@@ -335,13 +364,13 @@ class TendersViewModel with ChangeNotifier {
if (existingFeedback == null) {
optimisticFeedback = FeedbackData(
companyId: null,
createdAt: DateTime.now().millisecondsSinceEpoch,
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
customerId: null,
feedbackType: feedbackType,
id: null,
tenderId: tenderId,
tender: null,
updatedAt: DateTime.now().millisecondsSinceEpoch,
updatedAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
feedbacks.add(optimisticFeedback);
} else {
@@ -353,7 +382,7 @@ class TendersViewModel with ChangeNotifier {
} else {
optimisticFeedback = existingFeedback.copyWith(
feedbackType: feedbackType,
updatedAt: DateTime.now().millisecondsSinceEpoch,
updatedAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
if (existingFeedbackIndex != -1) {
feedbacks[existingFeedbackIndex] = optimisticFeedback;
@@ -371,9 +400,13 @@ class TendersViewModel with ChangeNotifier {
switch (result) {
case Ok<FeedbackResponse>():
_feedbackData = result.value.data;
if (_feedbackData != null) {
final responseType = _feedbackData?.feedbackType;
if (responseType == 'unlike' || responseType == 'undislike') {
feedbacks.removeWhere((feedback) => feedback.tenderId == tenderId);
_feedbackData = null;
} else if (_feedbackData != null) {
final index = feedbacks.indexWhere(
(f) => f.tender == _feedbackData!.tender,
(feedback) => feedback.tenderId == tenderId,
);
if (index != -1) {
feedbacks[index] = _feedbackData!;
@@ -383,6 +416,7 @@ class TendersViewModel with ChangeNotifier {
} else {
feedbacks.removeWhere((f) => f.tenderId == tenderId);
}
_hydratedFeedbackTenderIds.add(tenderId);
break;
case Error<FeedbackResponse>():
if (existingFeedback == null) {
@@ -401,13 +435,24 @@ class TendersViewModel with ChangeNotifier {
break;
}
_feedbackRequestsInFlight.remove(tenderId);
notifyListeners();
_feedbackErrorMessage = null;
notifyListeners();
}
/// Kept temporarily for callers compiled against the old misspelled API.
@Deprecated('Use toggleFeedback')
Future<void> toogleFeedback({
required String tenderId,
required String feedbackType,
}) => toggleFeedback(tenderId: tenderId, feedbackType: feedbackType);
void clearFeedbacks() {
feedbacks.clear();
_hydratedFeedbackTenderIds.clear();
_feedbackHydrationsInFlight.clear();
_feedbackRequestsInFlight.clear();
_feedbackData = null;
_feedbackErrorMessage = null;
notifyListeners();
+119 -8
View File
@@ -3,9 +3,11 @@ 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';
@@ -22,6 +24,21 @@ class YourTendersViewModel with ChangeNotifier {
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;
@@ -247,6 +264,8 @@ class YourTendersViewModel with ChangeNotifier {
// Clear main data
data = null;
likedTendersData = null;
submissionsData = null;
submissionStats = null;
// Clear loading states
isLoading = false;
@@ -269,6 +288,7 @@ class YourTendersViewModel with ChangeNotifier {
Future<void> callWithFilter() async {
likedTendersData = null;
data = null;
submissionsData = null;
isLoading = true;
notifyListeners();
@@ -282,14 +302,8 @@ class YourTendersViewModel with ChangeNotifier {
);
await getTenders(requestModel: requestModel, reset: true);
} else if (selectedStatus == TenderApprovalStatus.submitted.value) {
final requestModel = GetTendersRequestModel(
status: TenderApprovalStatus.submitted.value,
createdFrom: startDateUnix,
createdTo: endDateUnix,
limit: _tendersLimit,
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel, reset: true);
selectedSubmissionStage = 'submitted';
await loadSubmissions();
} else if (selectedStatus == TenderFeedback.liked.value) {
await getTendersFeedback(
feedbackType: TenderFeedback.liked.value,
@@ -318,6 +332,11 @@ class YourTendersViewModel with ChangeNotifier {
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!;
}
@@ -329,6 +348,10 @@ class YourTendersViewModel with ChangeNotifier {
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;
@@ -340,6 +363,94 @@ class YourTendersViewModel with ChangeNotifier {
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) {