Implement tender workflows and dashboard integration
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/push Build encountered an error
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user