Add liked tenders functionality to HomeRepository and ViewModel
- Integrated LikedTendersService into HomeRepository to fetch the count of user-liked tenders. - Updated HomeViewModel to load and manage the userLikedTendersCount. - Adjusted UI components across home pages to display the liked tenders count. - Refactored date handling in date_utils.dart to improve timestamp parsing and validation. - Enhanced unit tests for date utilities to cover new edge cases.
This commit is contained in:
@@ -121,6 +121,7 @@ List<SingleChildWidget> get repositories {
|
|||||||
(context) => HomeRepository(
|
(context) => HomeRepository(
|
||||||
homeService: context.read(),
|
homeService: context.read(),
|
||||||
yourTendersService: context.read(),
|
yourTendersService: context.read(),
|
||||||
|
likedTendersService: context.read(),
|
||||||
),
|
),
|
||||||
lazy: true,
|
lazy: true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
/// Parses API date fields that may arrive as int, double, or string.
|
/// Parses an API timestamp field that may arrive as int, double, or string.
|
||||||
|
/// Returns null for missing, empty, unparseable, or non-positive values
|
||||||
|
/// (a non-positive epoch is treated as "no value" by the API).
|
||||||
int? unixTimestampFromJson(dynamic value) {
|
int? unixTimestampFromJson(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is int) return value;
|
int? parsed;
|
||||||
if (value is num) return value.toInt();
|
if (value is int) {
|
||||||
if (value is String) {
|
parsed = value;
|
||||||
|
} else if (value is num) {
|
||||||
|
parsed = value.toInt();
|
||||||
|
} else if (value is String) {
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
if (trimmed.isEmpty) return null;
|
if (trimmed.isEmpty) return null;
|
||||||
return int.tryParse(trimmed);
|
parsed = int.tryParse(trimmed);
|
||||||
}
|
}
|
||||||
return null;
|
if (parsed == null || parsed <= 0) return null;
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a Unix timestamp (seconds) from the API to a display string.
|
/// Converts a Unix timestamp from the API to a display string.
|
||||||
/// Mirrors Next.js `unixToDate` using `moment.unix(unix)`.
|
///
|
||||||
|
/// The API contract is seconds since epoch, but some legacy fields still
|
||||||
|
/// emit milliseconds. Anything past the year 2286 in seconds (>1e10) is
|
||||||
|
/// assumed to be milliseconds and converted down. Remove this branch once
|
||||||
|
/// every endpoint has been migrated to seconds.
|
||||||
String timeConvertor(int? unix, {bool hasTime = false}) {
|
String timeConvertor(int? unix, {bool hasTime = false}) {
|
||||||
if (unix == null || unix == 0) return '-';
|
if (unix == null || unix <= 0) return '-';
|
||||||
|
|
||||||
// Values above ~year 2286 in seconds are treated as milliseconds.
|
|
||||||
final seconds = unix > 9999999999 ? unix ~/ 1000 : unix;
|
final seconds = unix > 9999999999 ? unix ~/ 1000 : unix;
|
||||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||||
seconds * 1000,
|
seconds * 1000,
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import 'package:tm_app/core/utils/result.dart';
|
import 'package:tm_app/core/utils/result.dart';
|
||||||
import 'package:tm_app/data/services/home_service.dart';
|
import 'package:tm_app/data/services/home_service.dart';
|
||||||
|
import 'package:tm_app/data/services/liked_tenders_service.dart';
|
||||||
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
|
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_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_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_approvals_stats_response/tender_approvals_stats_response.dart';
|
||||||
|
|
||||||
import '../services/model/feedback_stats_response/feedback_stat_response.dart';
|
|
||||||
import '../services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
import '../services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
||||||
|
import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart';
|
||||||
import '../services/your_tenders_service.dart';
|
import '../services/your_tenders_service.dart';
|
||||||
|
|
||||||
class HomeRepository {
|
class HomeRepository {
|
||||||
HomeRepository({
|
HomeRepository({
|
||||||
required HomeService homeService,
|
required HomeService homeService,
|
||||||
required YourTendersService yourTendersService,
|
required YourTendersService yourTendersService,
|
||||||
|
required LikedTendersService likedTendersService,
|
||||||
}) : _homeService = homeService,
|
}) : _homeService = homeService,
|
||||||
_yourTendersService = yourTendersService;
|
_yourTendersService = yourTendersService,
|
||||||
|
_likedTendersService = likedTendersService;
|
||||||
final HomeService _homeService;
|
final HomeService _homeService;
|
||||||
final YourTendersService _yourTendersService;
|
final YourTendersService _yourTendersService;
|
||||||
|
final LikedTendersService _likedTendersService;
|
||||||
|
|
||||||
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
|
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
|
||||||
required int limit,
|
required int limit,
|
||||||
@@ -34,11 +38,25 @@ class HomeRepository {
|
|||||||
return _homeService.getTendersApprovalsStats();
|
return _homeService.getTendersApprovalsStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<FeedbackStatResponse>> getFeedbackStats() {
|
/// Returns how many tenders the current user has liked (not company-wide).
|
||||||
return _homeService.getFeedbackStats();
|
Future<Result<int>> getUserLikedTendersCount() async {
|
||||||
|
final result = await _likedTendersService.getLikedTenders(
|
||||||
|
requestModel: TenderFeedbackRequestModel(
|
||||||
|
feedbackType: 'like',
|
||||||
|
limit: 1,
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case Ok(value: final response):
|
||||||
|
return Result.ok(response.data?.meta?.total ?? 0);
|
||||||
|
case Error(error: final error):
|
||||||
|
return Result.error(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async {
|
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async {
|
||||||
return _homeService.checkUnreadNotifications();
|
return _homeService.checkUnreadNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:tm_app/core/utils/result.dart';
|
import 'package:tm_app/core/utils/result.dart';
|
||||||
import 'package:tm_app/data/services/model/feedback_stats/feedback_stats.dart';
|
|
||||||
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
|
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_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_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_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/tender_data/tender_data.dart';
|
||||||
|
|
||||||
import '../data/repositories/home_repository.dart';
|
import '../data/repositories/home_repository.dart';
|
||||||
import '../data/services/model/feedback_stats_response/feedback_stat_response.dart';
|
|
||||||
import '../data/services/model/home/home_response/home_response_model.dart';
|
import '../data/services/model/home/home_response/home_response_model.dart';
|
||||||
import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
||||||
import '../data/services/notification_state_service.dart';
|
import '../data/services/notification_state_service.dart';
|
||||||
@@ -46,7 +44,7 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
|
|
||||||
RecommendedTendersResponse? _recommendedTendersResponse;
|
RecommendedTendersResponse? _recommendedTendersResponse;
|
||||||
TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
|
TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
|
||||||
FeedbackStats? feedbackStats;
|
int userLikedTendersCount = 0;
|
||||||
final List<TenderData> _tenders = [];
|
final List<TenderData> _tenders = [];
|
||||||
final int _pageSize = 10;
|
final int _pageSize = 10;
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
@@ -65,11 +63,19 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
userLikedTendersCount = 0;
|
||||||
|
_tenders.clear();
|
||||||
|
_currentPage = 0;
|
||||||
|
_hasMore = true;
|
||||||
|
_isRecommendedMode = false;
|
||||||
|
data = null;
|
||||||
|
tenderApprovalsStateResponse = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
await getApprovedStates();
|
await getApprovedStates();
|
||||||
await getYourTenders(reset: true);
|
await getYourTenders(reset: true);
|
||||||
await getFeedbackStats();
|
await loadUserLikedTendersCount();
|
||||||
|
|
||||||
if (_errorMessage != null) {
|
if (_errorMessage != null) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -192,15 +198,15 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getFeedbackStats() async {
|
Future<void> loadUserLikedTendersCount() async {
|
||||||
final result = await _homeRepository.getFeedbackStats();
|
final result = await _homeRepository.getUserLikedTendersCount();
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case Ok<FeedbackStatResponse>():
|
case Ok<int>():
|
||||||
feedbackStats = result.value.data;
|
userLikedTendersCount = result.value;
|
||||||
break;
|
break;
|
||||||
case Error<FeedbackStatResponse>():
|
case Error<int>():
|
||||||
_errorMessage = result.error.toString();
|
userLikedTendersCount = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class DeadlineItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (detail.submissionDeadline == null && detail.applicationDeadline == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return isScreenBig
|
return isScreenBig
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0),
|
padding: const EdgeInsets.symmetric(vertical: 0),
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ class DesktopHomePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||||
homeViewModel.data != null &&
|
homeViewModel.data != null) {
|
||||||
homeViewModel.feedbackStats != null) {
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -181,9 +180,9 @@ class DesktopHomePage extends StatelessWidget {
|
|||||||
backgroundColor: AppColors.grey10,
|
backgroundColor: AppColors.grey10,
|
||||||
iconPath: AssetsManager.thumbLike,
|
iconPath: AssetsManager.thumbLike,
|
||||||
title: HomeStrings.likedTenders,
|
title: HomeStrings.likedTenders,
|
||||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||||
textColor: AppColors.grey50,
|
textColor: AppColors.grey50,
|
||||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
enableTap: homeViewModel.userLikedTendersCount != 0,
|
||||||
width: 178,
|
width: 178,
|
||||||
height: 148,
|
height: 148,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ class MobileHomePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||||
homeViewModel.data != null &&
|
homeViewModel.data != null) {
|
||||||
homeViewModel.feedbackStats != null) {
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -156,9 +155,9 @@ class MobileHomePage extends StatelessWidget {
|
|||||||
backgroundColor: AppColors.grey10,
|
backgroundColor: AppColors.grey10,
|
||||||
iconPath: AssetsManager.thumbLike,
|
iconPath: AssetsManager.thumbLike,
|
||||||
title: HomeStrings.likedTenders,
|
title: HomeStrings.likedTenders,
|
||||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||||
textColor: AppColors.grey50,
|
textColor: AppColors.grey50,
|
||||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
enableTap: homeViewModel.userLikedTendersCount != 0,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
const LikedTendersRouteData().push(context).then((value) {
|
const LikedTendersRouteData().push(context).then((value) {
|
||||||
homeViewModel.init();
|
homeViewModel.init();
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ class TabletHomePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||||
homeViewModel.data != null &&
|
homeViewModel.data != null) {
|
||||||
homeViewModel.feedbackStats != null) {
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsetsDirectional.fromSTEB(
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
@@ -186,9 +185,9 @@ class TabletHomePage extends StatelessWidget {
|
|||||||
backgroundColor: AppColors.grey10,
|
backgroundColor: AppColors.grey10,
|
||||||
iconPath: AssetsManager.thumbLike,
|
iconPath: AssetsManager.thumbLike,
|
||||||
title: HomeStrings.likedTenders,
|
title: HomeStrings.likedTenders,
|
||||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||||
textColor: AppColors.grey50,
|
textColor: AppColors.grey50,
|
||||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
enableTap: homeViewModel.userLikedTendersCount != 0,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 148,
|
height: 148,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -3,18 +3,54 @@ import 'package:tm_app/core/utils/date_utils.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('timeConvertor', () {
|
group('timeConvertor', () {
|
||||||
test('returns dash for null and zero', () {
|
test('returns dash for null, zero, and negative', () {
|
||||||
expect(timeConvertor(null), '-');
|
expect(timeConvertor(null), '-');
|
||||||
expect(timeConvertor(0), '-');
|
expect(timeConvertor(0), '-');
|
||||||
|
expect(timeConvertor(-1), '-');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats unix seconds like moment.unix', () {
|
test('formats valid seconds as yyyy-MM-dd', () {
|
||||||
expect(timeConvertor(1700000000), '2023-11-14');
|
final formatted = timeConvertor(1700000000);
|
||||||
|
expect(formatted, matches(RegExp(r'^\d{4}-\d{2}-\d{2}$')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parses string timestamps from json', () {
|
test('formats valid seconds with time when hasTime is true', () {
|
||||||
|
final formatted = timeConvertor(1700000000, hasTime: true);
|
||||||
|
expect(
|
||||||
|
formatted,
|
||||||
|
matches(RegExp(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('treats values above 9999999999 as milliseconds', () {
|
||||||
|
// 1700000000000 ms and 1700000000 s should format to the same day.
|
||||||
|
expect(timeConvertor(1700000000000), timeConvertor(1700000000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('unixTimestampFromJson', () {
|
||||||
|
test('parses int and num', () {
|
||||||
|
expect(unixTimestampFromJson(1700000000), 1700000000);
|
||||||
|
expect(unixTimestampFromJson(1700000000.7), 1700000000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses numeric strings', () {
|
||||||
expect(unixTimestampFromJson('1700000000'), 1700000000);
|
expect(unixTimestampFromJson('1700000000'), 1700000000);
|
||||||
|
expect(unixTimestampFromJson(' 1700000000 '), 1700000000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for empty, null, and non-numeric input', () {
|
||||||
|
expect(unixTimestampFromJson(null), isNull);
|
||||||
expect(unixTimestampFromJson(''), isNull);
|
expect(unixTimestampFromJson(''), isNull);
|
||||||
|
expect(unixTimestampFromJson('abc'), isNull);
|
||||||
|
expect(unixTimestampFromJson(true), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-positive values', () {
|
||||||
|
expect(unixTimestampFromJson(0), isNull);
|
||||||
|
expect(unixTimestampFromJson(-1), isNull);
|
||||||
|
expect(unixTimestampFromJson('-1'), isNull);
|
||||||
|
expect(unixTimestampFromJson('0'), isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user