From 2515190576197735620d595d2ec0b012a62e67fb Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 24 May 2026 15:28:47 +0330 Subject: [PATCH 1/7] Refactor date handling in tender data model and UI components. Added `unixTimestampFromJson` for parsing timestamps and updated `timeConvertor` to handle null values. Updated relevant model fields and UI to use the new parsing logic. Added unit tests for date utilities. --- lib/core/utils/date_utils.dart | 31 ++++++++++++++++--- .../model/tender_data/tender_data.dart | 13 +++++--- .../model/tender_data/tender_data.g.dart | 8 ++--- lib/views/detail/widgets/deadline_item.dart | 8 ++--- lib/views/home/tenders_list_item.dart | 4 +-- .../widgets/liked_tenders_list_item.dart | 4 +-- lib/views/tenders/widgets/tender_card.dart | 4 +-- .../your_tenders/widgets/tender_card.dart | 2 +- test/core/date_utils_test.dart | 20 ++++++++++++ 9 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 test/core/date_utils_test.dart diff --git a/lib/core/utils/date_utils.dart b/lib/core/utils/date_utils.dart index 77ea623..f0d6821 100644 --- a/lib/core/utils/date_utils.dart +++ b/lib/core/utils/date_utils.dart @@ -1,9 +1,32 @@ import 'package:intl/intl.dart'; -String timeConvertor(int timestamp) { - // Convert seconds → milliseconds - final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); +/// Parses API date fields that may arrive as int, double, or string. +int? unixTimestampFromJson(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + final trimmed = value.trim(); + if (trimmed.isEmpty) return null; + return int.tryParse(trimmed); + } + return null; +} - // Format to yyyy-MM-dd +/// Converts a Unix timestamp (seconds) from the API to a display string. +/// Mirrors Next.js `unixToDate` using `moment.unix(unix)`. +String timeConvertor(int? unix, {bool hasTime = false}) { + if (unix == null || unix == 0) return '-'; + + // Values above ~year 2286 in seconds are treated as milliseconds. + final seconds = unix > 9999999999 ? unix ~/ 1000 : unix; + final date = DateTime.fromMillisecondsSinceEpoch( + seconds * 1000, + isUtc: true, + ).toLocal(); + + if (hasTime) { + return DateFormat('yyyy-MM-dd HH:mm').format(date); + } return DateFormat('yyyy-MM-dd').format(date); } diff --git a/lib/data/services/model/tender_data/tender_data.dart b/lib/data/services/model/tender_data/tender_data.dart index 6556da9..9754b48 100644 --- a/lib/data/services/model/tender_data/tender_data.dart +++ b/lib/data/services/model/tender_data/tender_data.dart @@ -1,6 +1,7 @@ // ignore_for_file: invalid_annotation_target import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:tm_app/core/utils/date_utils.dart'; import 'package:tm_app/data/services/model/organization/organization.dart'; part 'tender_data.freezed.dart'; @@ -15,7 +16,8 @@ abstract class TenderData with _$TenderData { @JsonKey(name: 'tender_id') required String? tenderId, @JsonKey(name: 'notice_publication_id') required String? noticePublicationId, - @JsonKey(name: 'publication_date') required int? publicationDate, + @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) + required int? publicationDate, required String? title, required String? description, @JsonKey(name: 'procurement_type_code') @@ -24,9 +26,12 @@ abstract class TenderData with _$TenderData { @JsonKey(name: 'estimated_value') required int? estimatedValue, required String? currency, required String? duration, - @JsonKey(name: 'tender_deadline') required int? tenderDeadline, - @JsonKey(name: 'submission_deadline') required int? submissionDeadline, - @JsonKey(name: 'application_deadline') required int? applicationDeadline, + @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) + required int? tenderDeadline, + @JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) + required int? submissionDeadline, + @JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) + required int? applicationDeadline, @JsonKey(name: 'submission_url') required String? submissionUrl, @JsonKey(name: 'country_code') required String? countryCode, @JsonKey(name: 'duration_unit') required String? durationUnit, diff --git a/lib/data/services/model/tender_data/tender_data.g.dart b/lib/data/services/model/tender_data/tender_data.g.dart index 68b9b62..1c934b5 100644 --- a/lib/data/services/model/tender_data/tender_data.g.dart +++ b/lib/data/services/model/tender_data/tender_data.g.dart @@ -12,7 +12,7 @@ _TenderData _$TenderDataFromJson(Map json) => _TenderData( id: json['id'] as String?, tenderId: json['tender_id'] as String?, noticePublicationId: json['notice_publication_id'] as String?, - publicationDate: (json['publication_date'] as num?)?.toInt(), + publicationDate: unixTimestampFromJson(json['publication_date']), title: json['title'] as String?, description: json['description'] as String?, procurementTypeCode: json['procurement_type_code'] as String?, @@ -20,9 +20,9 @@ _TenderData _$TenderDataFromJson(Map json) => _TenderData( estimatedValue: (json['estimated_value'] as num?)?.toInt(), currency: json['currency'] as String?, duration: json['duration'] as String?, - tenderDeadline: (json['tender_deadline'] as num?)?.toInt(), - submissionDeadline: (json['submission_deadline'] as num?)?.toInt(), - applicationDeadline: (json['application_deadline'] as num?)?.toInt(), + tenderDeadline: unixTimestampFromJson(json['tender_deadline']), + submissionDeadline: unixTimestampFromJson(json['submission_deadline']), + applicationDeadline: unixTimestampFromJson(json['application_deadline']), submissionUrl: json['submission_url'] as String?, countryCode: json['country_code'] as String?, durationUnit: json['duration_unit'] as String?, diff --git a/lib/views/detail/widgets/deadline_item.dart b/lib/views/detail/widgets/deadline_item.dart index e0d1f73..1bd01fc 100644 --- a/lib/views/detail/widgets/deadline_item.dart +++ b/lib/views/detail/widgets/deadline_item.dart @@ -51,7 +51,7 @@ class DeadlineItem extends StatelessWidget { ), SizedBox(height: 4.0.h()), Text( - timeConvertor(detail.submissionDeadline!), + timeConvertor(detail.submissionDeadline), style: TextStyle( color: AppColors.grey80, fontWeight: FontWeight.w400, @@ -80,7 +80,7 @@ class DeadlineItem extends StatelessWidget { ), SizedBox(height: 4.0.h()), Text( - timeConvertor(detail.applicationDeadline!), + timeConvertor(detail.applicationDeadline), style: TextStyle( color: AppColors.grey80, fontWeight: FontWeight.w400, @@ -124,7 +124,7 @@ class DeadlineItem extends StatelessWidget { ), ), Text( - timeConvertor(detail.submissionDeadline!), + timeConvertor(detail.submissionDeadline), style: TextStyle( color: AppColors.grey70, fontWeight: FontWeight.w400, @@ -146,7 +146,7 @@ class DeadlineItem extends StatelessWidget { ), ), Text( - timeConvertor(detail.applicationDeadline!), + timeConvertor(detail.applicationDeadline), style: TextStyle( color: AppColors.grey70, fontWeight: FontWeight.w400, diff --git a/lib/views/home/tenders_list_item.dart b/lib/views/home/tenders_list_item.dart index fd708dd..6e1fc50 100644 --- a/lib/views/home/tenders_list_item.dart +++ b/lib/views/home/tenders_list_item.dart @@ -34,7 +34,7 @@ class TendersListItem extends StatelessWidget { SizedBox( width: double.infinity, child: Text( - timeConvertor(tender.publicationDate ?? 0), + timeConvertor(tender.publicationDate), style: TextStyle( fontSize: 12.0.sp(), fontWeight: FontWeight.w400, @@ -64,7 +64,7 @@ class TendersListItem extends StatelessWidget { ), ), Text( - timeConvertor(tender.tenderDeadline ?? 0), + timeConvertor(tender.tenderDeadline), style: TextStyle( fontSize: 14.0.sp(), fontWeight: FontWeight.w500, diff --git a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart index 26c0d07..04334ab 100644 --- a/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart +++ b/lib/views/liked_tenders/widgets/liked_tenders_list_item.dart @@ -70,7 +70,7 @@ class LikedListItem extends StatelessWidget { ), ), Text( - timeConvertor(tender.tenderDeadline!), + timeConvertor(tender.tenderDeadline), style: TextStyle( fontSize: 14.0.sp(), fontWeight: FontWeight.w500, @@ -117,7 +117,7 @@ class LikedListItem extends StatelessWidget { ), ), Text( - timeConvertor(tender.tenderDeadline!), + timeConvertor(tender.tenderDeadline), style: TextStyle( fontSize: 14.0.sp(), fontWeight: FontWeight.w500, diff --git a/lib/views/tenders/widgets/tender_card.dart b/lib/views/tenders/widgets/tender_card.dart index 62ca7be..15d9295 100644 --- a/lib/views/tenders/widgets/tender_card.dart +++ b/lib/views/tenders/widgets/tender_card.dart @@ -115,7 +115,7 @@ class TenderCard extends StatelessWidget { Widget _dateText() { return Text( - timeConvertor(tender.publicationDate ?? 0), + timeConvertor(tender.publicationDate), style: TextStyle( color: AppColors.grey60, fontSize: 12.0.sp(), @@ -150,7 +150,7 @@ class TenderCard extends StatelessWidget { ), SizedBox(width: 4.0.w()), Text( - timeConvertor(tender.submissionDeadline ?? 0), + timeConvertor(tender.submissionDeadline), style: TextStyle( color: AppColors.grey80, fontSize: 14.0.sp(), diff --git a/lib/views/your_tenders/widgets/tender_card.dart b/lib/views/your_tenders/widgets/tender_card.dart index 7fa5d6c..222d2a0 100644 --- a/lib/views/your_tenders/widgets/tender_card.dart +++ b/lib/views/your_tenders/widgets/tender_card.dart @@ -53,7 +53,7 @@ class TenderCard extends StatelessWidget { children: [ // Date Text( - timeConvertor(int.parse(tender.publicationDate!.toString())), + timeConvertor(tender.publicationDate), style: TextStyle( color: AppColors.grey60, fontSize: 12.0.sp(), diff --git a/test/core/date_utils_test.dart b/test/core/date_utils_test.dart new file mode 100644 index 0000000..1eb151f --- /dev/null +++ b/test/core/date_utils_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tm_app/core/utils/date_utils.dart'; + +void main() { + group('timeConvertor', () { + test('returns dash for null and zero', () { + expect(timeConvertor(null), '-'); + expect(timeConvertor(0), '-'); + }); + + test('formats unix seconds like moment.unix', () { + expect(timeConvertor(1700000000), '2023-11-14'); + }); + + test('parses string timestamps from json', () { + expect(unixTimestampFromJson('1700000000'), 1700000000); + expect(unixTimestampFromJson(''), isNull); + }); + }); +} From 35af3e94ea2ea29e0ec7ab85df34be02f028493a Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 24 May 2026 18:23:37 +0330 Subject: [PATCH 2/7] 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. --- lib/core/config/dependencies.dart | 1 + lib/core/utils/date_utils.dart | 29 +++++++++----- lib/data/repositories/home_repository.dart | 34 ++++++++++++---- lib/view_models/home_view_model.dart | 26 +++++++----- lib/views/detail/widgets/deadline_item.dart | 3 ++ lib/views/home/pages/d_home_page.dart | 7 ++-- lib/views/home/pages/m_home_page.dart | 7 ++-- lib/views/home/pages/t_home_page.dart | 7 ++-- test/core/date_utils_test.dart | 44 +++++++++++++++++++-- 9 files changed, 114 insertions(+), 44 deletions(-) diff --git a/lib/core/config/dependencies.dart b/lib/core/config/dependencies.dart index 3461b4b..3ed519d 100644 --- a/lib/core/config/dependencies.dart +++ b/lib/core/config/dependencies.dart @@ -121,6 +121,7 @@ List get repositories { (context) => HomeRepository( homeService: context.read(), yourTendersService: context.read(), + likedTendersService: context.read(), ), lazy: true, ), diff --git a/lib/core/utils/date_utils.dart b/lib/core/utils/date_utils.dart index f0d6821..3cadf73 100644 --- a/lib/core/utils/date_utils.dart +++ b/lib/core/utils/date_utils.dart @@ -1,24 +1,33 @@ 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) { if (value == null) return null; - if (value is int) return value; - if (value is num) return value.toInt(); - if (value is String) { + int? parsed; + if (value is int) { + parsed = value; + } else if (value is num) { + parsed = value.toInt(); + } else if (value is String) { final trimmed = value.trim(); 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. -/// Mirrors Next.js `unixToDate` using `moment.unix(unix)`. +/// Converts a Unix timestamp from the API to a display string. +/// +/// 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}) { - 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 date = DateTime.fromMillisecondsSinceEpoch( seconds * 1000, diff --git a/lib/data/repositories/home_repository.dart b/lib/data/repositories/home_repository.dart index 1855bb1..50a9a7a 100644 --- a/lib/data/repositories/home_repository.dart +++ b/lib/data/repositories/home_repository.dart @@ -1,21 +1,25 @@ import 'package:tm_app/core/utils/result.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/tender_approvals_response/tender_approvals_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/tender_feedback_request_model/tender_feedback_request_model.dart'; import '../services/your_tenders_service.dart'; class HomeRepository { HomeRepository({ required HomeService homeService, required YourTendersService yourTendersService, + required LikedTendersService likedTendersService, }) : _homeService = homeService, - _yourTendersService = yourTendersService; + _yourTendersService = yourTendersService, + _likedTendersService = likedTendersService; final HomeService _homeService; final YourTendersService _yourTendersService; + final LikedTendersService _likedTendersService; Future> getHomeRecommendedTenders({ required int limit, @@ -34,11 +38,25 @@ class HomeRepository { return _homeService.getTendersApprovalsStats(); } - Future> getFeedbackStats() { - return _homeService.getFeedbackStats(); - } - Future>> checkUnreadNotifications() async { - return _homeService.checkUnreadNotifications(); -} + /// Returns how many tenders the current user has liked (not company-wide). + Future> 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>> checkUnreadNotifications() async { + return _homeService.checkUnreadNotifications(); + } } diff --git a/lib/view_models/home_view_model.dart b/lib/view_models/home_view_model.dart index a157e46..978d9a8 100644 --- a/lib/view_models/home_view_model.dart +++ b/lib/view_models/home_view_model.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.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/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 '../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/request_models/get_tenders_request_model/get_tenders_request_model.dart'; import '../data/services/notification_state_service.dart'; @@ -46,7 +44,7 @@ class HomeViewModel with ChangeNotifier { RecommendedTendersResponse? _recommendedTendersResponse; TenderApprovalsStatsResponse? tenderApprovalsStateResponse; - FeedbackStats? feedbackStats; + int userLikedTendersCount = 0; final List _tenders = []; final int _pageSize = 10; int _currentPage = 0; @@ -65,11 +63,19 @@ class HomeViewModel with ChangeNotifier { void init() async { _isLoading = true; + _errorMessage = null; + userLikedTendersCount = 0; + _tenders.clear(); + _currentPage = 0; + _hasMore = true; + _isRecommendedMode = false; + data = null; + tenderApprovalsStateResponse = null; notifyListeners(); await getApprovedStates(); await getYourTenders(reset: true); - await getFeedbackStats(); + await loadUserLikedTendersCount(); if (_errorMessage != null) { _isLoading = false; @@ -192,15 +198,15 @@ class HomeViewModel with ChangeNotifier { notifyListeners(); } - Future getFeedbackStats() async { - final result = await _homeRepository.getFeedbackStats(); + Future loadUserLikedTendersCount() async { + final result = await _homeRepository.getUserLikedTendersCount(); switch (result) { - case Ok(): - feedbackStats = result.value.data; + case Ok(): + userLikedTendersCount = result.value; break; - case Error(): - _errorMessage = result.error.toString(); + case Error(): + userLikedTendersCount = 0; break; } notifyListeners(); diff --git a/lib/views/detail/widgets/deadline_item.dart b/lib/views/detail/widgets/deadline_item.dart index 1bd01fc..b198b3e 100644 --- a/lib/views/detail/widgets/deadline_item.dart +++ b/lib/views/detail/widgets/deadline_item.dart @@ -19,6 +19,9 @@ class DeadlineItem extends StatelessWidget { @override Widget build(BuildContext context) { + if (detail.submissionDeadline == null && detail.applicationDeadline == null) { + return const SizedBox.shrink(); + } return isScreenBig ? Padding( padding: const EdgeInsets.symmetric(vertical: 0), diff --git a/lib/views/home/pages/d_home_page.dart b/lib/views/home/pages/d_home_page.dart index 12c5048..300c978 100644 --- a/lib/views/home/pages/d_home_page.dart +++ b/lib/views/home/pages/d_home_page.dart @@ -41,8 +41,7 @@ class DesktopHomePage extends StatelessWidget { } if (homeViewModel.tenderApprovalsStateResponse != null && - homeViewModel.data != null && - homeViewModel.feedbackStats != null) { + homeViewModel.data != null) { return Expanded( child: SingleChildScrollView( child: SizedBox( @@ -181,9 +180,9 @@ class DesktopHomePage extends StatelessWidget { backgroundColor: AppColors.grey10, iconPath: AssetsManager.thumbLike, title: HomeStrings.likedTenders, - amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', + amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, + enableTap: homeViewModel.userLikedTendersCount != 0, width: 178, height: 148, onTap: () { diff --git a/lib/views/home/pages/m_home_page.dart b/lib/views/home/pages/m_home_page.dart index c3ae3ad..6d7a17e 100644 --- a/lib/views/home/pages/m_home_page.dart +++ b/lib/views/home/pages/m_home_page.dart @@ -33,8 +33,7 @@ class MobileHomePage extends StatelessWidget { } if (homeViewModel.tenderApprovalsStateResponse != null && - homeViewModel.data != null && - homeViewModel.feedbackStats != null) { + homeViewModel.data != null) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -156,9 +155,9 @@ class MobileHomePage extends StatelessWidget { backgroundColor: AppColors.grey10, iconPath: AssetsManager.thumbLike, title: HomeStrings.likedTenders, - amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', + amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, + enableTap: homeViewModel.userLikedTendersCount != 0, onTap: () { const LikedTendersRouteData().push(context).then((value) { homeViewModel.init(); diff --git a/lib/views/home/pages/t_home_page.dart b/lib/views/home/pages/t_home_page.dart index 666ad91..daf7d1a 100644 --- a/lib/views/home/pages/t_home_page.dart +++ b/lib/views/home/pages/t_home_page.dart @@ -41,8 +41,7 @@ class TabletHomePage extends StatelessWidget { } if (homeViewModel.tenderApprovalsStateResponse != null && - homeViewModel.data != null && - homeViewModel.feedbackStats != null) { + homeViewModel.data != null) { return SingleChildScrollView( child: Padding( padding: EdgeInsetsDirectional.fromSTEB( @@ -186,9 +185,9 @@ class TabletHomePage extends StatelessWidget { backgroundColor: AppColors.grey10, iconPath: AssetsManager.thumbLike, title: HomeStrings.likedTenders, - amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', + amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, + enableTap: homeViewModel.userLikedTendersCount != 0, width: double.infinity, height: 148, onTap: () { diff --git a/test/core/date_utils_test.dart b/test/core/date_utils_test.dart index 1eb151f..16ff633 100644 --- a/test/core/date_utils_test.dart +++ b/test/core/date_utils_test.dart @@ -3,18 +3,54 @@ import 'package:tm_app/core/utils/date_utils.dart'; void main() { group('timeConvertor', () { - test('returns dash for null and zero', () { + test('returns dash for null, zero, and negative', () { expect(timeConvertor(null), '-'); expect(timeConvertor(0), '-'); + expect(timeConvertor(-1), '-'); }); - test('formats unix seconds like moment.unix', () { - expect(timeConvertor(1700000000), '2023-11-14'); + test('formats valid seconds as yyyy-MM-dd', () { + 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); + }); + + test('returns null for empty, null, and non-numeric input', () { + expect(unixTimestampFromJson(null), 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); }); }); } From 4b1260a43765f163c6104abfd04cc37d9c3e6fdd Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Mon, 25 May 2026 18:21:13 +0330 Subject: [PATCH 3/7] Update home page navigation to reinitialize ViewModel after navigating to liked tenders --- lib/views/home/pages/d_home_page.dart | 4 +++- lib/views/home/pages/t_home_page.dart | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/views/home/pages/d_home_page.dart b/lib/views/home/pages/d_home_page.dart index 300c978..e4a552c 100644 --- a/lib/views/home/pages/d_home_page.dart +++ b/lib/views/home/pages/d_home_page.dart @@ -186,7 +186,9 @@ class DesktopHomePage extends StatelessWidget { width: 178, height: 148, onTap: () { - const LikedTendersRouteData().push(context); + const LikedTendersRouteData().push(context).then((value) { + homeViewModel.init(); + }); }, ), ], diff --git a/lib/views/home/pages/t_home_page.dart b/lib/views/home/pages/t_home_page.dart index daf7d1a..0919a13 100644 --- a/lib/views/home/pages/t_home_page.dart +++ b/lib/views/home/pages/t_home_page.dart @@ -191,7 +191,9 @@ class TabletHomePage extends StatelessWidget { width: double.infinity, height: 148, onTap: () { - const LikedTendersRouteData().push(context); + const LikedTendersRouteData().push(context).then((value) { + homeViewModel.init(); + }); }, ), ), From 02057988dcc6c2c7a248d62a195b0cb68c0e4b2f Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 26 May 2026 10:41:34 +0330 Subject: [PATCH 4/7] Remove TaskStatus enum and related extensions, refactor BoardService and BoardResponse models for improved API integration, and update BoardViewModel to handle card movements between columns. Adjust UI components to reflect these changes and enhance error handling for card operations. --- lib/core/enums/task_status.dart | 29 - lib/core/extensions/board_data_extension.dart | 12 - lib/data/repositories/board_repositories.dart | 14 +- lib/data/services/board_service.dart | 104 ++- .../model/board_response/board_response.dart | 247 +++++++- .../board_response.freezed.dart | 596 ------------------ .../board_response/board_response.g.dart | 51 -- lib/view_models/board_view_model.dart | 114 ++-- lib/views/board/pages/board_screen.dart | 377 +++++------ lib/views/board/strings/board_strings.dart | 10 +- 10 files changed, 542 insertions(+), 1012 deletions(-) delete mode 100644 lib/core/enums/task_status.dart delete mode 100644 lib/core/extensions/board_data_extension.dart delete mode 100644 lib/data/services/model/board_response/board_response.freezed.dart delete mode 100644 lib/data/services/model/board_response/board_response.g.dart diff --git a/lib/core/enums/task_status.dart b/lib/core/enums/task_status.dart deleted file mode 100644 index 5298c23..0000000 --- a/lib/core/enums/task_status.dart +++ /dev/null @@ -1,29 +0,0 @@ -enum TaskStatus { toDo, inProgress, done } - -extension TaskStatusExtension on TaskStatus { - // تبدیل enum به string - String get value => name; - - // تبدیل string به enum - static TaskStatus fromString(String? status) { - if (status == null) { - return TaskStatus.toDo; - } - - return TaskStatus.values.firstWhere( - (e) => e.name == status, - orElse: () => TaskStatus.toDo, - ); - } - - String get displayNameEn { - switch (this) { - case TaskStatus.toDo: - return 'To Do'; - case TaskStatus.inProgress: - return 'In Progress'; - case TaskStatus.done: - return 'Done'; - } - } -} diff --git a/lib/core/extensions/board_data_extension.dart b/lib/core/extensions/board_data_extension.dart deleted file mode 100644 index 31039de..0000000 --- a/lib/core/extensions/board_data_extension.dart +++ /dev/null @@ -1,12 +0,0 @@ -import '../../data/services/model/board_response/board_response.dart'; -import '../enums/task_status.dart'; - -extension BoardDataExtension on BoardData { - // تبدیل status string به TaskStatus enum - TaskStatus get statusEnum => TaskStatusExtension.fromString(status); - - // کپی BoardData با status جدید - BoardData copyWithStatus(TaskStatus newStatus) { - return copyWith(status: newStatus.value); - } -} diff --git a/lib/data/repositories/board_repositories.dart b/lib/data/repositories/board_repositories.dart index 703ef5b..6f2cca1 100644 --- a/lib/data/repositories/board_repositories.dart +++ b/lib/data/repositories/board_repositories.dart @@ -7,7 +7,17 @@ class BoardRepository { BoardRepository({required BoardService boardService}) : _boardService = boardService; - Future> getBoard() async { - return _boardService.getBoard(); + Future> getBoard() => _boardService.getBoard(); + + Future> moveCard({ + required String cardId, + required String columnId, + required int newOrder, + }) { + return _boardService.moveCard( + cardId: cardId, + columnId: columnId, + newOrder: newOrder, + ); } } diff --git a/lib/data/services/board_service.dart b/lib/data/services/board_service.dart index 7d2f579..5343c55 100644 --- a/lib/data/services/board_service.dart +++ b/lib/data/services/board_service.dart @@ -1,5 +1,6 @@ -import '../../../core/network/network_manager.dart'; -import '../../core/config/app_config.dart'; +import 'dart:convert'; + +import '../../core/network/network_manager.dart'; import '../../core/utils/result.dart'; import 'model/board_response/board_response.dart'; @@ -9,72 +10,45 @@ class BoardService { final NetworkManager _networkManager; + static const _basePath = '/admin/v1/kanban/kanban'; + Future> getBoard() async { - final uri = '${AppConfig.apiBaseUrl}/api/v1/board'; - await Future.delayed(const Duration(seconds: 2)); + return _networkManager.makeRequest( + '$_basePath/boards/default', + (json) => BoardResponse.fromJson(json), + method: 'GET', + ); + } - // final result = await _networkManager.makeRequest( - // uri, - // (json) => BoardResponse.fromJson(json), - // method: 'GET', - // ); - // return result; - - return Result.ok( - BoardResponse( - success: true, - message: 'Board data fetched successfully', - data: tasks, - error: null, - ), + Future> moveCard({ + required String cardId, + required String columnId, + required int newOrder, + }) async { + final body = jsonEncode({ + 'card_id': cardId, + 'column_id': columnId, + 'new_order': newOrder, + }); + return _networkManager.makeRequest( + '$_basePath/cards/move', + (json) => MoveCardResponse.fromJson(json), + method: 'PUT', + data: body, ); } } -List tasks = [ - BoardData( - id: '1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'toDo', - ), - BoardData( - id: '2', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: true, - status: 'toDo', - ), - BoardData( - id: '3', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'inProgress', - ), - BoardData( - id: '4', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: true, - status: 'inProgress', - ), - BoardData( - id: '5', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'done', - ), -]; +class MoveCardResponse { + MoveCardResponse({required this.success, required this.message}); + + final bool? success; + final String? message; + + factory MoveCardResponse.fromJson(Map json) { + return MoveCardResponse( + success: json['success'] as bool?, + message: json['message'] as String?, + ); + } +} diff --git a/lib/data/services/model/board_response/board_response.dart b/lib/data/services/model/board_response/board_response.dart index 8bf6c6b..1b0f140 100644 --- a/lib/data/services/model/board_response/board_response.dart +++ b/lib/data/services/model/board_response/board_response.dart @@ -1,34 +1,229 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - +import '../../../../core/utils/date_utils.dart'; import '../error/error_model.dart'; -part 'board_response.freezed.dart'; -part 'board_response.g.dart'; +class BoardResponse { + BoardResponse({ + required this.success, + required this.message, + required this.data, + required this.error, + }); -@freezed -abstract class BoardResponse with _$BoardResponse { - const factory BoardResponse({ - required bool? success, - required String? message, - required List? data, - required ErrorModel? error, - }) = _BoardResponse; + final bool? success; + final String? message; + final BoardDetailData? data; + final ErrorModel? error; - factory BoardResponse.fromJson(Map json) => - _$BoardResponseFromJson(json); + factory BoardResponse.fromJson(Map json) { + final dataJson = json['data']; + final errorJson = json['error']; + return BoardResponse( + success: json['success'] as bool?, + message: json['message'] as String?, + data: + dataJson is Map + ? BoardDetailData.fromJson(dataJson) + : null, + error: + errorJson is Map + ? ErrorModel.fromJson(errorJson) + : null, + ); + } } -@freezed -abstract class BoardData with _$BoardData { - const factory BoardData({ - required String? id, - required String? description, - required DateTime? deadline, - required String? assignedTo, - required bool? isReady, - required String? status, - }) = _BoardData; +class BoardDetailData { + BoardDetailData({required this.board}); - factory BoardData.fromJson(Map json) => - _$BoardDataFromJson(json); + final Board? board; + + factory BoardDetailData.fromJson(Map json) { + final boardJson = json['board']; + return BoardDetailData( + board: + boardJson is Map ? Board.fromJson(boardJson) : null, + ); + } +} + +class Board { + Board({ + required this.id, + required this.name, + required this.description, + required this.columns, + required this.isDefault, + required this.userId, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? name; + final String? description; + final List columns; + final bool? isDefault; + final String? userId; + final int? createdAt; + final int? updatedAt; + + factory Board.fromJson(Map json) { + final columnsJson = json['columns']; + return Board( + id: json['id'] as String?, + name: json['name'] as String?, + description: json['description'] as String?, + columns: + columnsJson is List + ? columnsJson + .whereType>() + .map(BoardColumn.fromJson) + .toList() + : [], + isDefault: json['is_default'] as bool?, + userId: json['user_id'] as String?, + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + Board copyWith({List? columns}) { + return Board( + id: id, + name: name, + description: description, + columns: columns ?? this.columns, + isDefault: isDefault, + userId: userId, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } +} + +class BoardColumn { + BoardColumn({ + required this.id, + required this.boardId, + required this.name, + required this.order, + required this.color, + required this.limit, + required this.cards, + required this.cardCount, + required this.isOverLimit, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? boardId; + final String? name; + final int? order; + final String? color; + final int? limit; + final List cards; + final int? cardCount; + final bool? isOverLimit; + final int? createdAt; + final int? updatedAt; + + factory BoardColumn.fromJson(Map json) { + final cardsJson = json['cards']; + return BoardColumn( + id: json['id'] as String?, + boardId: json['board_id'] as String?, + name: json['name'] as String?, + order: (json['order'] as num?)?.toInt(), + color: json['color'] as String?, + limit: (json['limit'] as num?)?.toInt(), + cards: + cardsJson is List + ? cardsJson + .whereType>() + .map(BoardCard.fromJson) + .toList() + : [], + cardCount: (json['card_count'] as num?)?.toInt(), + isOverLimit: json['is_over_limit'] as bool?, + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + BoardColumn copyWith({List? cards, int? cardCount}) { + return BoardColumn( + id: id, + boardId: boardId, + name: name, + order: order, + color: color, + limit: limit, + cards: cards ?? this.cards, + cardCount: cardCount ?? this.cardCount, + isOverLimit: isOverLimit, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } +} + +class BoardCard { + BoardCard({ + required this.id, + required this.columnId, + required this.tenderId, + required this.title, + required this.order, + required this.priority, + required this.labels, + required this.dueDate, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? columnId; + final String? tenderId; + final String? title; + final int? order; + final String? priority; + final List labels; + final int? dueDate; + final int? createdAt; + final int? updatedAt; + + factory BoardCard.fromJson(Map json) { + final labelsJson = json['labels']; + return BoardCard( + id: json['id'] as String?, + columnId: json['column_id'] as String?, + tenderId: json['tender_id'] as String?, + title: json['title'] as String?, + order: (json['order'] as num?)?.toInt(), + priority: json['priority'] as String?, + labels: + labelsJson is List + ? labelsJson.whereType().toList() + : [], + dueDate: unixTimestampFromJson(json['due_date']), + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + BoardCard copyWith({String? columnId, int? order}) { + return BoardCard( + id: id, + columnId: columnId ?? this.columnId, + tenderId: tenderId, + title: title, + order: order ?? this.order, + priority: priority, + labels: labels, + dueDate: dueDate, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } } diff --git a/lib/data/services/model/board_response/board_response.freezed.dart b/lib/data/services/model/board_response/board_response.freezed.dart deleted file mode 100644 index bc12e33..0000000 --- a/lib/data/services/model/board_response/board_response.freezed.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'board_response.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$BoardResponse { - - bool? get success; String? get message; List? get data; ErrorModel? get error; -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BoardResponseCopyWith get copyWith => _$BoardResponseCopyWithImpl(this as BoardResponse, _$identity); - - /// Serializes this BoardResponse to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.error, error) || other.error == error)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(data),error); - -@override -String toString() { - return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class $BoardResponseCopyWith<$Res> { - factory $BoardResponseCopyWith(BoardResponse value, $Res Function(BoardResponse) _then) = _$BoardResponseCopyWithImpl; -@useResult -$Res call({ - bool? success, String? message, List? data, ErrorModel? error -}); - - -$ErrorModelCopyWith<$Res>? get error; - -} -/// @nodoc -class _$BoardResponseCopyWithImpl<$Res> - implements $BoardResponseCopyWith<$Res> { - _$BoardResponseCopyWithImpl(this._self, this._then); - - final BoardResponse _self; - final $Res Function(BoardResponse) _then; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? data = freezed,Object? error = freezed,}) { - return _then(_self.copyWith( -success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ErrorModel?, - )); -} -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ErrorModelCopyWith<$Res>? get error { - if (_self.error == null) { - return null; - } - - return $ErrorModelCopyWith<$Res>(_self.error!, (value) { - return _then(_self.copyWith(error: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [BoardResponse]. -extension BoardResponsePatterns on BoardResponse { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _BoardResponse value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _BoardResponse value) $default,){ -final _that = this; -switch (_that) { -case _BoardResponse(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BoardResponse value)? $default,){ -final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( bool? success, String? message, List? data, ErrorModel? error)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that.success,_that.message,_that.data,_that.error);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( bool? success, String? message, List? data, ErrorModel? error) $default,) {final _that = this; -switch (_that) { -case _BoardResponse(): -return $default(_that.success,_that.message,_that.data,_that.error);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool? success, String? message, List? data, ErrorModel? error)? $default,) {final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that.success,_that.message,_that.data,_that.error);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _BoardResponse implements BoardResponse { - const _BoardResponse({required this.success, required this.message, required final List? data, required this.error}): _data = data; - factory _BoardResponse.fromJson(Map json) => _$BoardResponseFromJson(json); - -@override final bool? success; -@override final String? message; - final List? _data; -@override List? get data { - final value = _data; - if (value == null) return null; - if (_data is EqualUnmodifiableListView) return _data; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); -} - -@override final ErrorModel? error; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$BoardResponseCopyWith<_BoardResponse> get copyWith => __$BoardResponseCopyWithImpl<_BoardResponse>(this, _$identity); - -@override -Map toJson() { - return _$BoardResponseToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.error, error) || other.error == error)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(_data),error); - -@override -String toString() { - return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class _$BoardResponseCopyWith<$Res> implements $BoardResponseCopyWith<$Res> { - factory _$BoardResponseCopyWith(_BoardResponse value, $Res Function(_BoardResponse) _then) = __$BoardResponseCopyWithImpl; -@override @useResult -$Res call({ - bool? success, String? message, List? data, ErrorModel? error -}); - - -@override $ErrorModelCopyWith<$Res>? get error; - -} -/// @nodoc -class __$BoardResponseCopyWithImpl<$Res> - implements _$BoardResponseCopyWith<$Res> { - __$BoardResponseCopyWithImpl(this._self, this._then); - - final _BoardResponse _self; - final $Res Function(_BoardResponse) _then; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? data = freezed,Object? error = freezed,}) { - return _then(_BoardResponse( -success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String?,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ErrorModel?, - )); -} - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ErrorModelCopyWith<$Res>? get error { - if (_self.error == null) { - return null; - } - - return $ErrorModelCopyWith<$Res>(_self.error!, (value) { - return _then(_self.copyWith(error: value)); - }); -} -} - - -/// @nodoc -mixin _$BoardData { - - String? get id; String? get description; DateTime? get deadline; String? get assignedTo; bool? get isReady; String? get status; -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BoardDataCopyWith get copyWith => _$BoardDataCopyWithImpl(this as BoardData, _$identity); - - /// Serializes this BoardData to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status); - -@override -String toString() { - return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)'; -} - - -} - -/// @nodoc -abstract mixin class $BoardDataCopyWith<$Res> { - factory $BoardDataCopyWith(BoardData value, $Res Function(BoardData) _then) = _$BoardDataCopyWithImpl; -@useResult -$Res call({ - String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status -}); - - - - -} -/// @nodoc -class _$BoardDataCopyWithImpl<$Res> - implements $BoardDataCopyWith<$Res> { - _$BoardDataCopyWithImpl(this._self, this._then); - - final BoardData _self; - final $Res Function(BoardData) _then; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) { - return _then(_self.copyWith( -id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable -as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable -as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable -as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [BoardData]. -extension BoardDataPatterns on BoardData { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _BoardData value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _BoardData value) $default,){ -final _that = this; -switch (_that) { -case _BoardData(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BoardData value)? $default,){ -final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status) $default,) {final _that = this; -switch (_that) { -case _BoardData(): -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,) {final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _BoardData implements BoardData { - const _BoardData({required this.id, required this.description, required this.deadline, required this.assignedTo, required this.isReady, required this.status}); - factory _BoardData.fromJson(Map json) => _$BoardDataFromJson(json); - -@override final String? id; -@override final String? description; -@override final DateTime? deadline; -@override final String? assignedTo; -@override final bool? isReady; -@override final String? status; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$BoardDataCopyWith<_BoardData> get copyWith => __$BoardDataCopyWithImpl<_BoardData>(this, _$identity); - -@override -Map toJson() { - return _$BoardDataToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status); - -@override -String toString() { - return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)'; -} - - -} - -/// @nodoc -abstract mixin class _$BoardDataCopyWith<$Res> implements $BoardDataCopyWith<$Res> { - factory _$BoardDataCopyWith(_BoardData value, $Res Function(_BoardData) _then) = __$BoardDataCopyWithImpl; -@override @useResult -$Res call({ - String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status -}); - - - - -} -/// @nodoc -class __$BoardDataCopyWithImpl<$Res> - implements _$BoardDataCopyWith<$Res> { - __$BoardDataCopyWithImpl(this._self, this._then); - - final _BoardData _self; - final $Res Function(_BoardData) _then; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) { - return _then(_BoardData( -id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable -as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable -as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable -as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - - -} - -// dart format on diff --git a/lib/data/services/model/board_response/board_response.g.dart b/lib/data/services/model/board_response/board_response.g.dart deleted file mode 100644 index 38983f4..0000000 --- a/lib/data/services/model/board_response/board_response.g.dart +++ /dev/null @@ -1,51 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'board_response.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_BoardResponse _$BoardResponseFromJson(Map json) => - _BoardResponse( - success: json['success'] as bool?, - message: json['message'] as String?, - data: - (json['data'] as List?) - ?.map((e) => BoardData.fromJson(e as Map)) - .toList(), - error: - json['error'] == null - ? null - : ErrorModel.fromJson(json['error'] as Map), - ); - -Map _$BoardResponseToJson(_BoardResponse instance) => - { - 'success': instance.success, - 'message': instance.message, - 'data': instance.data, - 'error': instance.error, - }; - -_BoardData _$BoardDataFromJson(Map json) => _BoardData( - id: json['id'] as String?, - description: json['description'] as String?, - deadline: - json['deadline'] == null - ? null - : DateTime.parse(json['deadline'] as String), - assignedTo: json['assignedTo'] as String?, - isReady: json['isReady'] as bool?, - status: json['status'] as String?, -); - -Map _$BoardDataToJson(_BoardData instance) => - { - 'id': instance.id, - 'description': instance.description, - 'deadline': instance.deadline?.toIso8601String(), - 'assignedTo': instance.assignedTo, - 'isReady': instance.isReady, - 'status': instance.status, - }; diff --git a/lib/view_models/board_view_model.dart b/lib/view_models/board_view_model.dart index 6b3f407..c568cf1 100644 --- a/lib/view_models/board_view_model.dart +++ b/lib/view_models/board_view_model.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import '../core/enums/task_status.dart'; import '../core/utils/result.dart'; import '../data/repositories/board_repositories.dart'; +import '../data/services/board_service.dart'; import '../data/services/model/board_response/board_response.dart'; class BoardViewModel with ChangeNotifier { @@ -12,23 +12,27 @@ class BoardViewModel with ChangeNotifier { bool _isLoading = false; String? _errorMessage; - List _boardData = []; + Board? _board; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; - List get boardData => _boardData; - bool get hasData => _boardData.isNotEmpty; + Board? get board => _board; + List get columns => _board?.columns ?? const []; + bool get hasData => _board != null && columns.isNotEmpty; Future getBoard() async { _isLoading = true; + _errorMessage = null; notifyListeners(); final result = await _boardRepository.getBoard(); switch (result) { case Ok(): - // تبدیل به لیست قابل تغییر - _boardData = List.from(result.value.data ?? []); + _board = result.value.data?.board; + if (_board == null) { + _errorMessage = result.value.error?.message ?? 'No board data'; + } break; case Error(): _errorMessage = result.error.toString(); @@ -38,40 +42,72 @@ class BoardViewModel with ChangeNotifier { notifyListeners(); } - // update task status - void updateTaskStatus(String taskId, String newStatus) { - final taskIndex = _boardData.indexWhere((task) => task.id == taskId); - if (taskIndex != -1) { - _boardData[taskIndex] = _boardData[taskIndex].copyWith(status: newStatus); + /// Look up the column a given card currently lives in. + BoardColumn? columnForCard(String cardId) { + for (final column in columns) { + if (column.cards.any((c) => c.id == cardId)) return column; + } + return null; + } + + /// Move a card to [targetColumnId] at the end of that column. + /// Optimistically updates local state, then calls the API; rolls back on + /// failure. + Future> moveCard({ + required BoardCard card, + required BoardColumn targetColumn, + }) async { + final cardId = card.id; + final targetColumnId = targetColumn.id; + if (cardId == null || targetColumnId == null || _board == null) { + return Result.error(Exception('Invalid card or column')); + } + + final sourceColumn = columnForCard(cardId); + if (sourceColumn == null) { + return Result.error(Exception('Card not found in any column')); + } + if (sourceColumn.id == targetColumnId) { + return Result.ok(MoveCardResponse(success: true, message: 'No change')); + } + + final previousColumns = _board!.columns; + final newOrder = targetColumn.cards.length; + + _board = _board!.copyWith( + columns: + previousColumns.map((column) { + if (column.id == sourceColumn.id) { + final remaining = + column.cards.where((c) => c.id != cardId).toList(); + return column.copyWith( + cards: remaining, + cardCount: remaining.length, + ); + } + if (column.id == targetColumnId) { + final updated = List.from(column.cards) + ..add(card.copyWith(columnId: targetColumnId, order: newOrder)); + return column.copyWith( + cards: updated, + cardCount: updated.length, + ); + } + return column; + }).toList(), + ); + notifyListeners(); + + final result = await _boardRepository.moveCard( + cardId: cardId, + columnId: targetColumnId, + newOrder: newOrder, + ); + + if (result is Error) { + _board = _board!.copyWith(columns: previousColumns); notifyListeners(); } - } - - bool isValidMove(TaskStatus currentStatus, TaskStatus targetStatus) { - if (currentStatus == TaskStatus.toDo) { - return targetStatus == TaskStatus.inProgress; - } else if (currentStatus == TaskStatus.inProgress) { - return targetStatus == TaskStatus.toDo || targetStatus == TaskStatus.done; - } else if (currentStatus == TaskStatus.done) { - return targetStatus == TaskStatus.inProgress; - } - return false; - } - - List getTasksByStatus(TaskStatus status) { - return boardData.where((task) => task.status == status.value).toList(); - } - - void _updateTaskStatus(BoardData task, TaskStatus newStatus) { - if (task.status == newStatus.value) { - return; - } - - final oldStatus = task.status; - - // update task status in ViewModel - updateTaskStatus(task.id!, newStatus.value); - - // _showStatusChangeMessage(context, task, oldStatus, newStatus); + return result; } } diff --git a/lib/views/board/pages/board_screen.dart b/lib/views/board/pages/board_screen.dart index 32cbdfe..07df1d4 100644 --- a/lib/views/board/pages/board_screen.dart +++ b/lib/views/board/pages/board_screen.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; -import 'package:tm_app/core/extensions/board_data_extension.dart'; import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/date_utils.dart'; +import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/data/services/board_service.dart'; import 'package:tm_app/data/services/model/board_response/board_response.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; import '../../../core/constants/assets.dart'; -import '../../../core/enums/task_status.dart'; import '../../../core/utils/app_toast.dart'; import '../../../view_models/board_view_model.dart'; import '../strings/board_strings.dart'; @@ -32,38 +33,27 @@ class _BoardScreenState extends State { }); } - void _updateTaskStatus(BoardData task, TaskStatus newStatus) { - if (task.statusEnum == newStatus) { - return; - } + Future _onCardDropped(BoardCard card, BoardColumn target) async { + final source = _boardViewModel.columnForCard(card.id ?? ''); + if (source == null || source.id == target.id) return; - final oldStatus = task.statusEnum; - - // update task status in ViewModel - _boardViewModel.updateTaskStatus(task.id!, newStatus.value); - - _showStatusChangeMessage(context, task, oldStatus, newStatus); - } - - void _showStatusChangeMessage( - BuildContext context, - BoardData task, - TaskStatus oldStatus, - TaskStatus newStatus, - ) { - final statusNames = { - TaskStatus.toDo: BoardStrings.toDo, - TaskStatus.inProgress: BoardStrings.inProgress, - TaskStatus.done: BoardStrings.done, - }; - - AppToast.success( - context, - BoardStrings.taskMovedMessage( - statusNames[oldStatus]!, - statusNames[newStatus]!, - ), + final result = await _boardViewModel.moveCard( + card: card, + targetColumn: target, ); + + if (!mounted) return; + if (result is Ok) { + AppToast.success( + context, + BoardStrings.cardMovedMessage( + source.name ?? '', + target.name ?? '', + ), + ); + } else if (result is Error) { + AppToast.error(context, BoardStrings.moveFailed); + } } @override @@ -73,9 +63,7 @@ class _BoardScreenState extends State { backgroundColor: AppColors.backgroundColor, body: Column( children: [ - const DesktopNavigationWidget( - currentIndex: 2, // Tenders index - ), + const DesktopNavigationWidget(currentIndex: 2), Consumer( builder: (context, viewModel, child) { if (viewModel.isLoading) { @@ -92,12 +80,16 @@ class _BoardScreenState extends State { child: Center(child: Text(viewModel.errorMessage!)), ); } - if (viewModel.hasData == false) { + if (!viewModel.hasData) { return const Expanded( child: Center(child: Text(BoardStrings.emptyListText)), ); } + final columns = [...viewModel.columns]..sort( + (a, b) => (a.order ?? 0).compareTo(b.order ?? 0), + ); + return Expanded( child: SingleChildScrollView( child: SizedBox( @@ -108,7 +100,7 @@ class _BoardScreenState extends State { children: [ const SizedBox(height: 32), Text( - BoardStrings.boardTitle, + viewModel.board?.name ?? BoardStrings.boardTitle, style: TextStyle( fontSize: 20.0.sp(), fontWeight: FontWeight.w600, @@ -126,50 +118,15 @@ class _BoardScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // To Do - Expanded( - child: _BoardColumn( - title: BoardStrings.toDo, - color: AppColors.primary20, - textColor: const Color(0xFF1976D2), - tasks: viewModel.getTasksByStatus( - TaskStatus.toDo, + for (int i = 0; i < columns.length; i++) ...[ + if (i > 0) const SizedBox(width: 12), + Expanded( + child: _BoardColumnView( + column: columns[i], + onCardDropped: _onCardDropped, ), - status: TaskStatus.toDo, - onTaskDropped: _updateTaskStatus, - isValidMove: viewModel.isValidMove, ), - ), - const SizedBox(width: 12), - // In Progress - Expanded( - child: _BoardColumn( - title: BoardStrings.inProgress, - color: AppColors.orange20, - textColor: AppColors.orange, - tasks: viewModel.getTasksByStatus( - TaskStatus.inProgress, - ), - status: TaskStatus.inProgress, - onTaskDropped: _updateTaskStatus, - isValidMove: viewModel.isValidMove, - ), - ), - const SizedBox(width: 12), - // Done - Expanded( - child: _BoardColumn( - title: BoardStrings.done, - color: AppColors.secondary20, - textColor: AppColors.secondary70, - tasks: viewModel.getTasksByStatus( - TaskStatus.done, - ), - status: TaskStatus.done, - onTaskDropped: _updateTaskStatus, - isValidMove: viewModel.isValidMove, - ), - ), + ], ], ), ), @@ -187,43 +144,29 @@ class _BoardScreenState extends State { } } -class _BoardColumn extends StatelessWidget { - final String title; - final Color color; - final Color textColor; - final List tasks; - final TaskStatus status; - final Function(BoardData, TaskStatus) onTaskDropped; - final bool Function(TaskStatus, TaskStatus) isValidMove; +class _BoardColumnView extends StatelessWidget { + final BoardColumn column; + final Future Function(BoardCard, BoardColumn) onCardDropped; - const _BoardColumn({ - required this.title, - required this.color, - required this.textColor, - required this.tasks, - required this.status, - required this.onTaskDropped, - required this.isValidMove, - }); + const _BoardColumnView({required this.column, required this.onCardDropped}); @override Widget build(BuildContext context) { - return DragTarget( - onWillAcceptWithDetails: (details) { - return isValidMove(details.data.statusEnum, status); - }, - onAcceptWithDetails: (details) { - onTaskDropped(details.data, status); - }, + final accent = _parseHexColor(column.color) ?? AppColors.textBlue; + return DragTarget( + onWillAcceptWithDetails: (details) => details.data.columnId != column.id, + onAcceptWithDetails: (details) => onCardDropped(details.data, column), builder: (context, candidateData, rejectedData) { final isHovering = candidateData.isNotEmpty; return Container( decoration: BoxDecoration( color: - isHovering ? color.withValues(alpha: 0.3) : AppColors.primary0, + isHovering + ? accent.withValues(alpha: 0.15) + : AppColors.primary0, borderRadius: BorderRadius.circular(12), border: Border.all( - color: isHovering ? textColor : AppColors.borderBlue, + color: isHovering ? accent : AppColors.borderBlue, width: isHovering ? 2 : 1, ), ), @@ -235,26 +178,25 @@ class _BoardColumn extends StatelessWidget { height: 34, margin: const EdgeInsets.all(8), decoration: BoxDecoration( - color: color, + color: accent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.center, child: Text( - title, + _columnHeader(column), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: textColor, + color: accent, ), ), ), - Expanded( child: ListView.builder( padding: const EdgeInsets.all(12), - itemCount: tasks.length, + itemCount: column.cards.length, itemBuilder: (context, index) { - return _TaskCard(task: tasks[index]); + return _TaskCard(card: column.cards[index], accent: accent); }, ), ), @@ -264,21 +206,25 @@ class _BoardColumn extends StatelessWidget { }, ); } + + String _columnHeader(BoardColumn column) { + final name = column.name ?? ''; + final limit = column.limit; + if (limit == null || limit <= 0) return name; + return '$name (${column.cards.length}/$limit)'; + } } class _TaskCard extends StatelessWidget { - final BoardData task; + final BoardCard card; + final Color accent; - const _TaskCard({required this.task}); - - String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } + const _TaskCard({required this.card, required this.accent}); @override Widget build(BuildContext context) { - return Draggable( - data: task, + return Draggable( + data: card, feedback: Material( elevation: 8, borderRadius: BorderRadius.circular(12), @@ -319,91 +265,72 @@ class _TaskCard extends StatelessWidget { } Widget _buildCardContent(BuildContext context) { + final due = card.dueDate; + final labels = card.labels; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - height: 24.0, - padding: EdgeInsets.symmetric(horizontal: 10.0.w()), - decoration: BoxDecoration( - color: - task.statusEnum == TaskStatus.toDo - ? AppColors.primary20 - : task.statusEnum == TaskStatus.inProgress - ? AppColors.orange10 - : AppColors.secondary10, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${BoardStrings.deadlineLabel} :', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w500, - color: AppColors.textBlue, + if (due != null) + Container( + height: 24.0, + padding: EdgeInsets.symmetric(horizontal: 10.0.w()), + decoration: BoxDecoration( + color: accent.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${BoardStrings.deadlineLabel} :', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: AppColors.textBlue, + ), ), - ), - Text( - _formatDate(task.deadline ?? DateTime.now()), - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w500, - color: AppColors.grey80, + Text( + timeConvertor(due), + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 12), + if (due != null) const SizedBox(height: 12), Text( - task.description!, - style: TextStyle(fontSize: 14, color: AppColors.grey70), + card.title ?? '', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.grey70, + ), maxLines: 3, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), - - Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - decoration: BoxDecoration( - color: task.isReady ?? false ? AppColors.primary20 : AppColors.red0, - borderRadius: BorderRadius.circular(8), + if (card.priority != null) _PriorityChip(priority: card.priority!), + if (labels.isNotEmpty) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 6, + children: [for (final label in labels) _LabelChip(label: label)], ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: task.isReady! ? AppColors.textBlue : AppColors.error, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - Text( - task.isReady! - ? BoardStrings.readyLabel - : BoardStrings.incompleteLabel, - style: TextStyle( - fontSize: 12, - color: task.isReady! ? AppColors.textBlue : AppColors.red20, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), + ], const SizedBox(height: 12), Row( children: [ SvgPicture.asset(AssetsManager.profile), const SizedBox(width: 4), Text( - task.assignedTo!, + card.tenderId ?? '', style: TextStyle(fontSize: 12, color: AppColors.grey60), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), @@ -411,3 +338,83 @@ class _TaskCard extends StatelessWidget { ); } } + +class _PriorityChip extends StatelessWidget { + final String priority; + const _PriorityChip({required this.priority}); + + @override + Widget build(BuildContext context) { + final (bg, fg, dot) = _styleFor(priority); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: dot, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + priority, + style: TextStyle( + fontSize: 12, + color: fg, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } + + (Color bg, Color fg, Color dot) _styleFor(String priority) { + switch (priority.toLowerCase()) { + case 'urgent': + case 'high': + return (AppColors.red0, AppColors.red20, AppColors.error); + case 'low': + return (AppColors.primary20, AppColors.textBlue, AppColors.textBlue); + case 'medium': + default: + return (AppColors.orange10, AppColors.orange, AppColors.orange); + } + } +} + +class _LabelChip extends StatelessWidget { + final String label; + const _LabelChip({required this.label}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + style: TextStyle(fontSize: 11, color: AppColors.textBlue), + ), + ); + } +} + +Color? _parseHexColor(String? hex) { + if (hex == null) return null; + var value = hex.trim(); + if (value.startsWith('#')) value = value.substring(1); + if (value.length == 6) value = 'FF$value'; + if (value.length != 8) return null; + final parsed = int.tryParse(value, radix: 16); + if (parsed == null) return null; + return Color(parsed); +} diff --git a/lib/views/board/strings/board_strings.dart b/lib/views/board/strings/board_strings.dart index f53a263..5a56774 100644 --- a/lib/views/board/strings/board_strings.dart +++ b/lib/views/board/strings/board_strings.dart @@ -2,13 +2,9 @@ class BoardStrings { BoardStrings._(); static const String boardTitle = 'My Board'; - static const String toDo = 'To Do'; - static const String inProgress = 'In Progress'; - static const String done = 'Done'; static const String emptyListText = 'No tasks found'; static const String deadlineLabel = 'Deadline'; - static const String readyLabel = 'Ready'; - static const String incompleteLabel = 'Incomplete'; - static String taskMovedMessage(String oldStatus, String newStatus) => - 'Task moved from $oldStatus to $newStatus'; + static const String moveFailed = 'Could not move card'; + static String cardMovedMessage(String fromColumn, String toColumn) => + 'Card moved from $fromColumn to $toColumn'; } From ae08b946f6569dc21fc082985167a13e7c14ee5c Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 26 May 2026 18:30:46 +0330 Subject: [PATCH 5/7] Refactor tenders pagination and enhance API integration - Updated TendersRepository and TendersService to support cursor-based pagination. - Modified TendersViewModel to manage pagination state and handle API responses more effectively. - Replaced existing pagination UI components with a new WindowedPagination widget across desktop, mobile, and tablet views. - Improved notification handling in the UI to reflect the presence of notifications dynamically. - Adjusted main tenders slider to ensure proper rendering based on the current page index. --- lib/data/repositories/tenders_repository.dart | 4 +- lib/data/services/api/tenders_api.dart | 10 +- lib/data/services/model/meta/meta.dart | 3 + .../services/model/meta/meta.freezed.dart | 49 +- lib/data/services/model/meta/meta.g.dart | 6 + lib/data/services/tenders_service.dart | 5 +- lib/main.dart | 9 +- lib/view_models/tenders_view_model.dart | 271 ++++---- lib/views/home/pages/d_home_page.dart | 2 +- lib/views/home/pages/m_home_page.dart | 2 +- lib/views/home/pages/t_home_page.dart | 2 +- .../pages/d_notification_page.dart | 53 +- .../pages/m_notification_page.dart | 51 +- .../pages/t_notification_page.dart | 53 +- lib/views/tenders/pages/d_tenders_page.dart | 103 +-- lib/views/tenders/pages/m_tenders_page.dart | 29 +- lib/views/tenders/pages/t_tenders_page.dart | 31 +- .../tenders/widgets/main_tenders_slider.dart | 32 - .../tenders/widgets/windowed_pagination.dart | 250 +++++++ pubspec.lock | 614 +++++++++--------- 20 files changed, 927 insertions(+), 652 deletions(-) create mode 100644 lib/views/tenders/widgets/windowed_pagination.dart diff --git a/lib/data/repositories/tenders_repository.dart b/lib/data/repositories/tenders_repository.dart index 567e010..d8a38d7 100644 --- a/lib/data/repositories/tenders_repository.dart +++ b/lib/data/repositories/tenders_repository.dart @@ -13,7 +13,8 @@ class TendersRepository { Future> getTenders({ required int limit, - required int offset, + int? offset, + String? cursor, String? query, String? sortBy, String? sortOrder, @@ -23,6 +24,7 @@ class TendersRepository { return _tendersService.getTenders( limit: limit, offset: offset, + cursor: cursor, query: query, sortBy: sortBy, sortOrder: sortOrder, diff --git a/lib/data/services/api/tenders_api.dart b/lib/data/services/api/tenders_api.dart index 1b582e4..96dc88a 100644 --- a/lib/data/services/api/tenders_api.dart +++ b/lib/data/services/api/tenders_api.dart @@ -4,14 +4,20 @@ class TendersApi { static const String tenders = '/api/v1/tenders'; static String getTenders({ required int limit, - required int offset, + int? offset, + String? cursor, String? query, String? sortBy, String? sortOrder, String? publicationDateFrom, String? publicationDateTo, }) { - String url = '$tenders?limit=$limit&offset=$offset'; + String url = '$tenders?limit=$limit'; + if (cursor != null && cursor.isNotEmpty) { + url += '&cursor=${Uri.encodeComponent(cursor)}'; + } else if (offset != null) { + url += '&offset=$offset'; + } if (query != null && query.isNotEmpty) { url += '&q=${Uri.encodeComponent(query)}'; } diff --git a/lib/data/services/model/meta/meta.dart b/lib/data/services/model/meta/meta.dart index 39ea5ca..650b869 100644 --- a/lib/data/services/model/meta/meta.dart +++ b/lib/data/services/model/meta/meta.dart @@ -13,6 +13,9 @@ abstract class Meta with _$Meta { required int? page, required int? pages, required int? total, + @JsonKey(name: 'has_more') bool? hasMore, + @JsonKey(name: 'next_cursor') String? nextCursor, + @JsonKey(name: 'prev_cursor') String? prevCursor, }) = _Meta; factory Meta.fromJson(Map json) => _$MetaFromJson(json); diff --git a/lib/data/services/model/meta/meta.freezed.dart b/lib/data/services/model/meta/meta.freezed.dart index 5926249..f408471 100644 --- a/lib/data/services/model/meta/meta.freezed.dart +++ b/lib/data/services/model/meta/meta.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$Meta { - int? get limit; int? get offset; int? get page; int? get pages; int? get total; + int? get limit; int? get offset; int? get page; int? get pages; int? get total;@JsonKey(name: 'has_more') bool? get hasMore;@JsonKey(name: 'next_cursor') String? get nextCursor;@JsonKey(name: 'prev_cursor') String? get prevCursor; /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $MetaCopyWith get copyWith => _$MetaCopyWithImpl(this as Meta, _$ide @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.prevCursor, prevCursor) || other.prevCursor == prevCursor)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total); +int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total,hasMore,nextCursor,prevCursor); @override String toString() { - return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total)'; + return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total, hasMore: $hasMore, nextCursor: $nextCursor, prevCursor: $prevCursor)'; } @@ -48,7 +48,7 @@ abstract mixin class $MetaCopyWith<$Res> { factory $MetaCopyWith(Meta value, $Res Function(Meta) _then) = _$MetaCopyWithImpl; @useResult $Res call({ - int? limit, int? offset, int? page, int? pages, int? total + int? limit, int? offset, int? page, int? pages, int? total,@JsonKey(name: 'has_more') bool? hasMore,@JsonKey(name: 'next_cursor') String? nextCursor,@JsonKey(name: 'prev_cursor') String? prevCursor }); @@ -65,14 +65,17 @@ class _$MetaCopyWithImpl<$Res> /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,Object? hasMore = freezed,Object? nextCursor = freezed,Object? prevCursor = freezed,}) { return _then(_self.copyWith( limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable as int?,total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable -as int?, +as int?,hasMore: freezed == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool?,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable +as String?,prevCursor: freezed == prevCursor ? _self.prevCursor : prevCursor // ignore: cast_nullable_to_non_nullable +as String?, )); } @@ -157,10 +160,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( int? limit, int? offset, int? page, int? pages, int? total)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Meta() when $default != null: -return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _: +return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _: return orElse(); } @@ -178,10 +181,10 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( int? limit, int? offset, int? page, int? pages, int? total) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor) $default,) {final _that = this; switch (_that) { case _Meta(): -return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _: +return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _: throw StateError('Unexpected subclass'); } @@ -198,10 +201,10 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? limit, int? offset, int? page, int? pages, int? total)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor)? $default,) {final _that = this; switch (_that) { case _Meta() when $default != null: -return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _: +return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _: return null; } @@ -213,7 +216,7 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas @JsonSerializable() class _Meta implements Meta { - const _Meta({required this.limit, required this.offset, required this.page, required this.pages, required this.total}); + const _Meta({required this.limit, required this.offset, required this.page, required this.pages, required this.total, @JsonKey(name: 'has_more') this.hasMore, @JsonKey(name: 'next_cursor') this.nextCursor, @JsonKey(name: 'prev_cursor') this.prevCursor}); factory _Meta.fromJson(Map json) => _$MetaFromJson(json); @override final int? limit; @@ -221,6 +224,9 @@ class _Meta implements Meta { @override final int? page; @override final int? pages; @override final int? total; +@override@JsonKey(name: 'has_more') final bool? hasMore; +@override@JsonKey(name: 'next_cursor') final String? nextCursor; +@override@JsonKey(name: 'prev_cursor') final String? prevCursor; /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. @@ -235,16 +241,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.prevCursor, prevCursor) || other.prevCursor == prevCursor)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total); +int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total,hasMore,nextCursor,prevCursor); @override String toString() { - return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total)'; + return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total, hasMore: $hasMore, nextCursor: $nextCursor, prevCursor: $prevCursor)'; } @@ -255,7 +261,7 @@ abstract mixin class _$MetaCopyWith<$Res> implements $MetaCopyWith<$Res> { factory _$MetaCopyWith(_Meta value, $Res Function(_Meta) _then) = __$MetaCopyWithImpl; @override @useResult $Res call({ - int? limit, int? offset, int? page, int? pages, int? total + int? limit, int? offset, int? page, int? pages, int? total,@JsonKey(name: 'has_more') bool? hasMore,@JsonKey(name: 'next_cursor') String? nextCursor,@JsonKey(name: 'prev_cursor') String? prevCursor }); @@ -272,14 +278,17 @@ class __$MetaCopyWithImpl<$Res> /// Create a copy of Meta /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,Object? hasMore = freezed,Object? nextCursor = freezed,Object? prevCursor = freezed,}) { return _then(_Meta( limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable as int?,total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable -as int?, +as int?,hasMore: freezed == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool?,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable +as String?,prevCursor: freezed == prevCursor ? _self.prevCursor : prevCursor // ignore: cast_nullable_to_non_nullable +as String?, )); } diff --git a/lib/data/services/model/meta/meta.g.dart b/lib/data/services/model/meta/meta.g.dart index 6e38bc5..2610829 100644 --- a/lib/data/services/model/meta/meta.g.dart +++ b/lib/data/services/model/meta/meta.g.dart @@ -12,6 +12,9 @@ _Meta _$MetaFromJson(Map json) => _Meta( page: (json['page'] as num?)?.toInt(), pages: (json['pages'] as num?)?.toInt(), total: (json['total'] as num?)?.toInt(), + hasMore: json['has_more'] as bool?, + nextCursor: json['next_cursor'] as String?, + prevCursor: json['prev_cursor'] as String?, ); Map _$MetaToJson(_Meta instance) => { @@ -20,4 +23,7 @@ Map _$MetaToJson(_Meta instance) => { 'page': instance.page, 'pages': instance.pages, 'total': instance.total, + 'has_more': instance.hasMore, + 'next_cursor': instance.nextCursor, + 'prev_cursor': instance.prevCursor, }; diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index 703813b..519a35b 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -16,7 +16,8 @@ class TendersService { Future> getTenders({ required int limit, - required int offset, + int? offset, + String? cursor, String? query, String? sortBy, String? sortOrder, @@ -27,6 +28,7 @@ class TendersService { TendersApi.getTenders( limit: limit, offset: offset, + cursor: cursor, query: query, sortBy: sortBy, sortOrder: sortOrder, @@ -37,7 +39,6 @@ class TendersService { method: 'GET', (json) => TendersResponse.fromJson(json), ); - print(result); return result; } diff --git a/lib/main.dart b/lib/main.dart index 551e1e1..29c9d4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -130,6 +131,9 @@ class _MyAppState extends State { Widget build(BuildContext context) { // Listen to theme changes final themeProvider = context.watch(); + final pageTransition = kIsWeb + ? const FadeUpwardsPageTransitionsBuilder() + : const CupertinoPageTransitionsBuilder(); return ToastificationWrapper( child: MaterialApp.router( @@ -147,10 +151,7 @@ class _MyAppState extends State { pageTransitionsTheme: PageTransitionsTheme( builders: { for (final platform in TargetPlatform.values) - platform: - kIsWeb - ? const FadeUpwardsPageTransitionsBuilder() - : const CupertinoPageTransitionsBuilder(), + platform: pageTransition, }, ), brightness: diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index baefb2c..47ff830 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -5,6 +5,7 @@ import 'package:tm_app/core/utils/logger.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'; +import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; @@ -36,15 +37,44 @@ class TendersViewModel with ChangeNotifier { List feedbacks = []; - int currentPage = 1; - int _currentOffset = 0; - int _totalPages = 0; - int get totalPages => _totalPages; static const int _pageSize = 10; - bool _hasMoreData = true; + // --- Offset pagination state --- + int _currentOffset = 0; + int _lastValidPages = 1; + int _lastValidLimit = _pageSize; + + bool _hasMoreData = true; bool get hasMoreData => _hasMoreData; + /// 0-based current page index. + int get currentPageIndex { + final meta = _tendersResponse?.meta; + final limit = meta?.limit ?? _lastValidLimit; + + if (limit > 0) { + return _currentOffset ~/ limit; + } + if (meta?.offset != null && limit > 0) { + return meta!.offset! ~/ limit; + } + if (meta?.page != null) { + return meta!.page! - 1; + } + return 0; + } + + /// 1-based current page (for display). + int get currentPage => currentPageIndex + 1; + + int get totalPages { + final pages = _tendersResponse?.meta?.pages; + if (pages != null && pages > 0) { + return pages; + } + return _lastValidPages; + } + // Filter States DateTimeRange? selectedDateRange; String selectedStatus = 'All'; @@ -80,15 +110,12 @@ class TendersViewModel with ChangeNotifier { } void setStatus(String status) { - // Clear data when status changes _clearAllData(); selectedStatus = status; - currentPage = 1; notifyListeners(); } void setSort(String sort) { - // Clear data when status changes _clearAllData(); selectedSort = sort; if (sort == TendersStrings.minimumTime) { @@ -98,16 +125,13 @@ class TendersViewModel with ChangeNotifier { sortBy = 'submission_deadline'; sortOrder = 'desc'; } - currentPage = 1; notifyListeners(); } void _clearAllData() { _tendersResponse = null; _errorMessage = null; - _currentOffset = 0; - _hasMoreData = true; - currentPage = 1; + _resetPagination(); sortBy = ''; sortOrder = ''; selectedSort = ''; @@ -116,6 +140,11 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } + void _resetPagination() { + _currentOffset = 0; + _hasMoreData = true; + } + void init(BuildContext context) async { _isLoading = true; notifyListeners(); @@ -151,10 +180,7 @@ class TendersViewModel with ChangeNotifier { } Future getTendersMobile({String? query}) async { - // Clear all data before fetching new tenders - - _currentOffset = 0; - _hasMoreData = true; + _resetPagination(); _searchQuery = query; final result = await _tendersRepository.getTenders( @@ -164,28 +190,7 @@ class TendersViewModel with ChangeNotifier { sortBy: '', sortOrder: '', ); - switch (result) { - case Ok(): - _tendersResponse = result.value; - // Check if we have more data - if (_tendersResponse?.data?.tenders != null) { - _hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize; - } - if (_tendersResponse?.data?.tenders?.isNotEmpty == true) { - // Fetch feedback and approvals for the first tender only - getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!); - } - if (_tendersResponse?.meta?.pages != null) { - _totalPages = _tendersResponse!.meta!.pages!; - } else { - _totalPages = 1; - } - notifyListeners(); - break; - case Error(): - _errorMessage = result.error.toString(); - break; - } + _handleListResult(result, fetchAllFeedbacks: false); _isLoading = false; notifyListeners(); _errorMessage = null; @@ -199,11 +204,9 @@ class TendersViewModel with ChangeNotifier { String? publicationDateFrom, String? publicationDateTo, }) async { - // Clear all data before fetching new tenders _isLoading = true; notifyListeners(); - _currentOffset = 0; - _hasMoreData = true; + _resetPagination(); _searchQuery = query; final result = await _tendersRepository.getTenders( @@ -215,35 +218,55 @@ class TendersViewModel with ChangeNotifier { publicationDateFrom: publicationDateFrom ?? '', publicationDateTo: publicationDateTo ?? '', ); - switch (result) { - case Ok(): - _tendersResponse = result.value; - // Check if we have more data - if (_tendersResponse?.data?.tenders != null) { - _hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize; - } - if (_tendersResponse?.data?.tenders?.isNotEmpty == true) { - for (var tender in _tendersResponse!.data!.tenders!) { - getTenderFeedback(tender.id!); - } - } - if (_tendersResponse?.meta?.pages != null) { - _totalPages = _tendersResponse!.meta!.pages!; - } else { - _totalPages = 1; - } - notifyListeners(); - break; - case Error(): - _errorMessage = result.error.toString(); - break; - } + _handleListResult(result, fetchAllFeedbacks: true); _isLoading = false; notifyListeners(); _errorMessage = null; notifyListeners(); } + void _handleListResult( + Result result, { + required bool fetchAllFeedbacks, + }) { + switch (result) { + case Ok(): + _tendersResponse = result.value; + final meta = _tendersResponse?.meta; + if (meta?.pages != null && meta!.pages! > 0) { + _lastValidPages = meta.pages!; + } + if (meta?.limit != null && meta!.limit! > 0) { + _lastValidLimit = meta.limit!; + } + if (meta?.offset != null) { + _currentOffset = meta!.offset!; + } + final tenders = _tendersResponse?.data?.tenders ?? []; + _hasMoreData = + meta?.hasMore ?? (tenders.length >= _pageSize); + if (tenders.isNotEmpty) { + if (fetchAllFeedbacks) { + for (final tender in tenders) { + if (tender.id != null) { + getTenderFeedback(tender.id!); + } + } + } else { + getTenderFeedback(tenders.first.id!); + } + } + break; + case Error(): + _handleListError(result.error); + break; + } + } + + void _handleListError(Exception error) { + _errorMessage = error.toString(); + } + Future loadMoreTenders() async { if (_isLoadingMore || !_hasMoreData) { return; @@ -253,7 +276,10 @@ class TendersViewModel with ChangeNotifier { _errorMessage = null; notifyListeners(); - _currentOffset += _pageSize; + final meta = _tendersResponse?.meta; + final limit = meta?.limit ?? _pageSize; + _currentOffset += limit; + final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, @@ -264,30 +290,31 @@ class TendersViewModel with ChangeNotifier { switch (result) { case Ok(): - if (result.value.data?.tenders != null && - result.value.data!.tenders!.isNotEmpty) { - // Create a new modifiable list by combining existing and new tenders - if (_tendersResponse?.data?.tenders != null) { - final existingTenders = _tendersResponse!.data!.tenders!; - final newTenders = result.value.data!.tenders!; - final combinedTenders = [...existingTenders, ...newTenders]; - - // Create a new TendersData with the combined list - _tendersResponse = _tendersResponse!.copyWith( - data: _tendersResponse!.data!.copyWith(tenders: combinedTenders), - ); - } else { - _tendersResponse = result.value; + final newTenders = result.value.data?.tenders ?? []; + if (newTenders.isNotEmpty) { + final existingTenders = + _tendersResponse?.data?.tenders ?? []; + final combined = [...existingTenders, ...newTenders]; + _tendersResponse = result.value.copyWith( + data: result.value.data?.copyWith(tenders: combined), + ); + final newMeta = result.value.meta; + if (newMeta?.pages != null && newMeta!.pages! > 0) { + _lastValidPages = newMeta.pages!; } - // Check if we have more data - _hasMoreData = result.value.data!.tenders!.length >= _pageSize; + if (newMeta?.limit != null && newMeta!.limit! > 0) { + _lastValidLimit = newMeta.limit!; + } + _hasMoreData = + newMeta?.hasMore ?? (newTenders.length >= _pageSize); } else { _hasMoreData = false; } break; case Error(): - _errorMessage = result.error.toString(); - _currentOffset -= _pageSize; // Revert offset on error + final limit = meta?.limit ?? _pageSize; + _currentOffset = (_currentOffset - limit).clamp(0, 1 << 30); + _handleListError(result.error); break; } @@ -298,7 +325,6 @@ class TendersViewModel with ChangeNotifier { } Future getTenderFeedback(String tenderId) async { - // _isLoading = true; _errorMessage = null; notifyListeners(); @@ -308,7 +334,6 @@ class TendersViewModel with ChangeNotifier { switch (result) { case Ok(): _feedbackData = result.value.data; - // Add or update _feedbackData in feedbacks list by tender id if (_feedbackData != null) { final index = feedbacks.indexWhere( (f) => f.tenderId == _feedbackData!.tenderId, @@ -331,7 +356,6 @@ class TendersViewModel with ChangeNotifier { _errorMessage = result.error.toString(); break; } - // _isLoading = false; notifyListeners(); _errorMessage = null; notifyListeners(); @@ -343,38 +367,32 @@ class TendersViewModel with ChangeNotifier { }) async { _feedbackErrorMessage = null; - // Get current feedback for this tender final existingFeedback = getFeedbackForTender(tenderId); final existingFeedbackIndex = feedbacks.indexWhere( (f) => f.tenderId == tenderId, ); - // Create optimistic feedback data FeedbackData? optimisticFeedback; if (existingFeedback == null) { - // No existing feedback - create new optimistic feedback optimisticFeedback = FeedbackData( - companyId: null, // Will be filled by server + companyId: null, createdAt: DateTime.now().millisecondsSinceEpoch, - customerId: null, // Will be filled by server + customerId: null, feedbackType: feedbackType, - id: null, // Will be filled by server + id: null, tenderId: tenderId, tender: null, updatedAt: DateTime.now().millisecondsSinceEpoch, ); feedbacks.add(optimisticFeedback); } else { - // Existing feedback - update optimistically if (existingFeedback.feedbackType == feedbackType) { - // Same feedback type - remove feedback (toggle off) if (existingFeedbackIndex != -1) { feedbacks.removeAt(existingFeedbackIndex); } optimisticFeedback = null; } else { - // Different feedback type - update to new type optimisticFeedback = existingFeedback.copyWith( feedbackType: feedbackType, updatedAt: DateTime.now().millisecondsSinceEpoch, @@ -385,10 +403,8 @@ class TendersViewModel with ChangeNotifier { } } - // Notify listeners immediately for optimistic update notifyListeners(); - // Call API final result = await _tendersRepository.toggleFeedback( tenderId: tenderId, feedbackType: feedbackType, @@ -398,7 +414,6 @@ class TendersViewModel with ChangeNotifier { case Ok(): _feedbackData = result.value.data; if (_feedbackData != null) { - // Update with complete data from server final index = feedbacks.indexWhere( (f) => f.tender == _feedbackData!.tender, ); @@ -408,17 +423,13 @@ class TendersViewModel with ChangeNotifier { feedbacks.add(_feedbackData!); } } else { - // Server returned null data - remove feedback feedbacks.removeWhere((f) => f.tenderId == tenderId); } break; case Error(): - // Revert optimistic update on error if (existingFeedback == null) { - // Remove the optimistic feedback we added feedbacks.removeWhere((f) => f.tenderId == tenderId); } else { - // Restore the original feedback - find current index and update, or add if not found final currentIndex = feedbacks.indexWhere( (f) => f.tenderId == tenderId, ); @@ -437,7 +448,6 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - /// Clear all feedbacks void clearFeedbacks() { feedbacks.clear(); _feedbackData = null; @@ -445,13 +455,10 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - /// Clear all tenders void clearTenders() { _tendersResponse = null; _errorMessage = null; - _currentOffset = 0; - _hasMoreData = true; - currentPage = 1; + _resetPagination(); notifyListeners(); } @@ -464,7 +471,6 @@ class TendersViewModel with ChangeNotifier { ); } - /// Search tenders with query Future searchTenders(String query) async { await getTendersDesktop( query: query.trim(), @@ -475,7 +481,6 @@ class TendersViewModel with ChangeNotifier { ); } - /// Clear all data (tenders, feedbacks, and approvals) void clearAll() { clearTenders(); clearFeedbacks(); @@ -488,17 +493,32 @@ class TendersViewModel with ChangeNotifier { notifyListeners(); } - void jumpToPage(int page) async { - final meta = tendersResponse?.meta; - if (meta?.pages == null) { + /// [selected] is the absolute 0-based target page index. + Future handlePaginationChange(int selected) async { + if (selected < 0) { return; } - if (page < 1 || page > meta!.pages!) { + final maxPage = totalPages - 1; + if (maxPage >= 0 && selected > maxPage) { return; } - currentPage = page; - _currentOffset = (page - 1) * _pageSize; + if (selected == currentPageIndex) { + return; + } + + await _goToOffsetPage(selected); + } + + Future _goToOffsetPage(int pageIndex) async { + final limit = _tendersResponse?.meta?.limit ?? _pageSize; + _currentOffset = pageIndex * limit; + await _fetchPage(); + } + + Future _fetchPage() async { + _isLoading = true; + notifyListeners(); final result = await _tendersRepository.getTenders( limit: _pageSize, @@ -506,17 +526,20 @@ class TendersViewModel with ChangeNotifier { query: _searchQuery, sortBy: sortBy, sortOrder: sortOrder, + publicationDateFrom: startDateUnix, + publicationDateTo: endDateUnix, ); - switch (result) { - case Ok(): - _tendersResponse = result.value; - break; - case Error(): - _errorMessage = result.error.toString(); - break; - } - + _handleListResult(result, fetchAllFeedbacks: true); + _isLoading = false; + notifyListeners(); + _errorMessage = null; notifyListeners(); } + + /// Kept for backwards compatibility with existing callers. + /// Accepts a 1-based page number. + void jumpToPage(int page) { + handlePaginationChange(page - 1); + } } diff --git a/lib/views/home/pages/d_home_page.dart b/lib/views/home/pages/d_home_page.dart index e4a552c..6d6a967 100644 --- a/lib/views/home/pages/d_home_page.dart +++ b/lib/views/home/pages/d_home_page.dart @@ -182,7 +182,7 @@ class DesktopHomePage extends StatelessWidget { title: HomeStrings.likedTenders, amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: homeViewModel.userLikedTendersCount != 0, + enableTap: true, width: 178, height: 148, onTap: () { diff --git a/lib/views/home/pages/m_home_page.dart b/lib/views/home/pages/m_home_page.dart index 6d7a17e..82563f8 100644 --- a/lib/views/home/pages/m_home_page.dart +++ b/lib/views/home/pages/m_home_page.dart @@ -157,7 +157,7 @@ class MobileHomePage extends StatelessWidget { title: HomeStrings.likedTenders, amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: homeViewModel.userLikedTendersCount != 0, + enableTap: true, onTap: () { const LikedTendersRouteData().push(context).then((value) { homeViewModel.init(); diff --git a/lib/views/home/pages/t_home_page.dart b/lib/views/home/pages/t_home_page.dart index 0919a13..feb700f 100644 --- a/lib/views/home/pages/t_home_page.dart +++ b/lib/views/home/pages/t_home_page.dart @@ -187,7 +187,7 @@ class TabletHomePage extends StatelessWidget { title: HomeStrings.likedTenders, amount: homeViewModel.userLikedTendersCount.toString(), textColor: AppColors.grey50, - enableTap: homeViewModel.userLikedTendersCount != 0, + enableTap: true, width: double.infinity, height: 148, onTap: () { diff --git a/lib/views/notification/pages/d_notification_page.dart b/lib/views/notification/pages/d_notification_page.dart index 17c180e..d3e3a96 100644 --- a/lib/views/notification/pages/d_notification_page.dart +++ b/lib/views/notification/pages/d_notification_page.dart @@ -87,29 +87,42 @@ class _DesktopNotificationPageState extends State padding: EdgeInsetsDirectional.only(end: 24.0.w()), child: Align( alignment: Alignment.centerRight, - child: InkWell( - onTap: () { - viewModel.markAllAsRead(context); - }, - borderRadius: BorderRadius.circular(99), - child: Container( - width: 132.0.w(), - height: 32.0.h(), - decoration: BoxDecoration( - color: AppColors.primary20, + child: Builder( + builder: (context) { + final hasNotifications = (viewModel + .allNotificationResponse + ?.data + ?.isNotEmpty ?? + false); + return InkWell( + onTap: hasNotifications + ? () => viewModel.markAllAsRead(context) + : null, borderRadius: BorderRadius.circular(99), - ), - child: Center( - child: Text( - NotificationStrings.markAllAsReadButton, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, + child: Container( + width: 132.0.w(), + height: 32.0.h(), + decoration: BoxDecoration( + color: hasNotifications + ? AppColors.primary20 + : AppColors.grey20, + borderRadius: BorderRadius.circular(99), + ), + child: Center( + child: Text( + NotificationStrings.markAllAsReadButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: hasNotifications + ? AppColors.mainBlue + : AppColors.grey50, + ), + ), ), ), - ), - ), + ); + }, ), ), ), diff --git a/lib/views/notification/pages/m_notification_page.dart b/lib/views/notification/pages/m_notification_page.dart index 7108af5..86c1f5e 100644 --- a/lib/views/notification/pages/m_notification_page.dart +++ b/lib/views/notification/pages/m_notification_page.dart @@ -101,29 +101,40 @@ class _MobileNotificationPageState extends State padding: EdgeInsetsDirectional.only(end: 24.0.w()), child: Align( alignment: Alignment.centerRight, - child: InkWell( - onTap: () { - viewModel.markAllAsRead(context); - }, - borderRadius: BorderRadius.circular(99), - child: Container( - width: 132.0.w(), - height: 32.0.h(), - decoration: BoxDecoration( - color: AppColors.primary20, + child: Builder( + builder: (context) { + final hasNotifications = + (viewModel.allNotificationResponse?.data?.isNotEmpty ?? + false); + return InkWell( + onTap: hasNotifications + ? () => viewModel.markAllAsRead(context) + : null, borderRadius: BorderRadius.circular(99), - ), - child: Center( - child: Text( - NotificationStrings.markAllAsReadButton, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, + child: Container( + width: 132.0.w(), + height: 32.0.h(), + decoration: BoxDecoration( + color: hasNotifications + ? AppColors.primary20 + : AppColors.grey20, + borderRadius: BorderRadius.circular(99), + ), + child: Center( + child: Text( + NotificationStrings.markAllAsReadButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: hasNotifications + ? AppColors.mainBlue + : AppColors.grey50, + ), + ), ), ), - ), - ), + ); + }, ), ), ), diff --git a/lib/views/notification/pages/t_notification_page.dart b/lib/views/notification/pages/t_notification_page.dart index b96ff97..ca4653b 100644 --- a/lib/views/notification/pages/t_notification_page.dart +++ b/lib/views/notification/pages/t_notification_page.dart @@ -90,29 +90,42 @@ class _TabletNotificationPageState extends State padding: EdgeInsetsDirectional.only(end: 24.0.w()), child: Align( alignment: Alignment.centerRight, - child: InkWell( - onTap: () { - viewModel.markAllAsRead(context); - }, - borderRadius: BorderRadius.circular(99), - child: Container( - width: 132.0.w(), - height: 32.0.h(), - decoration: BoxDecoration( - color: AppColors.primary20, + child: Builder( + builder: (context) { + final hasNotifications = (viewModel + .allNotificationResponse + ?.data + ?.isNotEmpty ?? + false); + return InkWell( + onTap: hasNotifications + ? () => viewModel.markAllAsRead(context) + : null, borderRadius: BorderRadius.circular(99), - ), - child: Center( - child: Text( - NotificationStrings.markAllAsReadButton, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.mainBlue, + child: Container( + width: 132.0.w(), + height: 32.0.h(), + decoration: BoxDecoration( + color: hasNotifications + ? AppColors.primary20 + : AppColors.grey20, + borderRadius: BorderRadius.circular(99), + ), + child: Center( + child: Text( + NotificationStrings.markAllAsReadButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: hasNotifications + ? AppColors.mainBlue + : AppColors.grey50, + ), + ), ), ), - ), - ), + ); + }, ), ), ), diff --git a/lib/views/tenders/pages/d_tenders_page.dart b/lib/views/tenders/pages/d_tenders_page.dart index 7a7bb85..cdfce5b 100644 --- a/lib/views/tenders/pages/d_tenders_page.dart +++ b/lib/views/tenders/pages/d_tenders_page.dart @@ -9,11 +9,11 @@ import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; -import 'package:tm_app/views/shared/page_selection_dialog.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; import 'package:tm_app/views/tenders/widgets/tenders_filter_dialog.dart'; import 'package:tm_app/views/tenders/widgets/tenders_sort_dialog.dart'; +import 'package:tm_app/views/tenders/widgets/windowed_pagination.dart'; import '../../../core/constants/common_strings.dart'; import '../../../core/utils/app_toast.dart'; @@ -95,10 +95,13 @@ class _DesktopTendersPageState extends State { ), ), ), - _DesktopPagination( - totalPages: vm.totalPages, - currentPage: vm.currentPage, - onPageSelected: vm.jumpToPage, + SizedBox( + width: 740, + child: WindowedPagination( + currentPage: vm.currentPageIndex, + totalPages: vm.totalPages, + onPageChanged: vm.handlePaginationChange, + ), ), ], ); @@ -264,93 +267,3 @@ class _ActionButtons extends StatelessWidget { } } -class _DesktopPagination extends StatelessWidget { - final int totalPages; - final int currentPage; - final ValueChanged onPageSelected; - - const _DesktopPagination({ - required this.totalPages, - required this.currentPage, - required this.onPageSelected, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 680, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 12.0.h()), - child: Row( - children: [ - const Spacer(), - Text( - TendersStrings.page, - style: TextStyle( - fontSize: 13.0.sp(), - fontWeight: FontWeight.w300, - color: AppColors.grey60, - ), - ), - SizedBox(width: 10.0.w()), - InkWell( - onTap: () async { - final selectedPage = await showDialog( - context: context, - builder: - (context) => PageSelectionDialog( - totalPages: totalPages, - currentPage: currentPage, - ), - ); - if (selectedPage != null) { - onPageSelected(selectedPage); - } - }, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: AppColors.grey30, width: 1), - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - SizedBox(width: 5.0.w()), - Text( - '$currentPage', - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w500, - color: AppColors.grey70, - ), - ), - SizedBox(width: 5.0.w()), - Icon(Icons.arrow_drop_down, color: AppColors.grey80), - SizedBox(width: 5.0.w()), - ], - ), - ), - ), - SizedBox(width: 5.0.w()), - Text( - TendersStrings.of, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w400, - color: AppColors.grey60, - ), - ), - SizedBox(width: 5.0.w()), - Text( - '$totalPages', - style: TextStyle( - fontSize: 13.0.sp(), - fontWeight: FontWeight.w400, - color: AppColors.grey60, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/views/tenders/pages/m_tenders_page.dart b/lib/views/tenders/pages/m_tenders_page.dart index 5325e16..f29d6a1 100644 --- a/lib/views/tenders/pages/m_tenders_page.dart +++ b/lib/views/tenders/pages/m_tenders_page.dart @@ -9,6 +9,7 @@ import '../../../core/utils/app_toast.dart'; import '../../shared/tender_app_bar.dart'; import '../strings/tenders_strings.dart'; import '../widgets/main_tenders_slider.dart'; +import '../widgets/windowed_pagination.dart'; class MobileTendersPage extends StatefulWidget { const MobileTendersPage({super.key}); @@ -61,13 +62,29 @@ class _MobileTendersPageState extends State { return const Center(child: Text(CommonStrings.noData)); } - return SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 18.0.h()), - child: MainTendersSlider( - tenders: viewModel.tendersResponse?.data?.tenders ?? [], + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 18.0.h()), + child: MainTendersSlider( + key: ValueKey( + 'tenders-slider-${viewModel.currentPageIndex}', + ), + tenders: + viewModel.tendersResponse?.data?.tenders ?? [], + ), + ), + ), ), - ), + WindowedPagination( + currentPage: viewModel.currentPageIndex, + totalPages: viewModel.totalPages, + onPageChanged: viewModel.handlePaginationChange, + compact: true, + ), + ], ); }, ), diff --git a/lib/views/tenders/pages/t_tenders_page.dart b/lib/views/tenders/pages/t_tenders_page.dart index e3b38de..4129f0a 100644 --- a/lib/views/tenders/pages/t_tenders_page.dart +++ b/lib/views/tenders/pages/t_tenders_page.dart @@ -5,6 +5,7 @@ import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; +import 'package:tm_app/views/tenders/widgets/windowed_pagination.dart'; import '../../../core/constants/common_strings.dart'; import '../../shared/tender_app_bar.dart'; @@ -53,16 +54,30 @@ class _TabletTendersPageState extends State { viewModel.isLoading == false) { return const Center(child: Text(CommonStrings.noData)); } - return SingleChildScrollView( - child: Padding( - padding: EdgeInsetsDirectional.only( - end: 24.0.w(), - top: 128.0.h(), + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsetsDirectional.only( + end: 24.0.w(), + top: 128.0.h(), + ), + child: MainTendersSlider( + key: ValueKey( + 'tenders-slider-${viewModel.currentPageIndex}', + ), + tenders: viewModel.tendersResponse?.data?.tenders ?? [], + ), + ), + ), ), - child: MainTendersSlider( - tenders: viewModel.tendersResponse?.data?.tenders ?? [], + WindowedPagination( + currentPage: viewModel.currentPageIndex, + totalPages: viewModel.totalPages, + onPageChanged: viewModel.handlePaginationChange, ), - ), + ], ); }, ), diff --git a/lib/views/tenders/widgets/main_tenders_slider.dart b/lib/views/tenders/widgets/main_tenders_slider.dart index b0c2b70..66cde15 100644 --- a/lib/views/tenders/widgets/main_tenders_slider.dart +++ b/lib/views/tenders/widgets/main_tenders_slider.dart @@ -27,31 +27,6 @@ class _MainTendersSliderState extends State { final PageController pageController = PageController(initialPage: 0); int currentPage = 1; - ScrollController? _scrollController; - - @override - void initState() { - super.initState(); - - if (widget.isDesktop) { - _scrollController = ScrollController(); - _scrollController!.addListener(_onScroll); - } - } - - void _onScroll() { - if (!widget.isDesktop) { - return; - } - final viewModel = context.read(); - if (_scrollController!.position.pixels >= - _scrollController!.position.maxScrollExtent - 200 && - viewModel.hasMoreData && - !viewModel.isLoadingMore) { - viewModel.loadMoreTenders(); - } - } - void _goToPreviousPage() { if (currentPage == 1) { return; @@ -107,7 +82,6 @@ class _MainTendersSliderState extends State { @override void dispose() { pageController.dispose(); - _scrollController?.dispose(); super.dispose(); } @@ -141,12 +115,6 @@ class _MainTendersSliderState extends State { if (!viewModel.hasFeedbackForTender(id)) { viewModel.getTenderFeedback(id); } - - if (index >= widget.tenders.length - 1 && - viewModel.hasMoreData && - !viewModel.isLoadingMore) { - viewModel.loadMoreTenders(); - } }, itemBuilder: (context, index) { final tender = widget.tenders[index]; diff --git a/lib/views/tenders/widgets/windowed_pagination.dart b/lib/views/tenders/widgets/windowed_pagination.dart new file mode 100644 index 0000000..3514267 --- /dev/null +++ b/lib/views/tenders/widgets/windowed_pagination.dart @@ -0,0 +1,250 @@ +import 'package:flutter/material.dart'; +import 'package:tm_app/core/theme/colors.dart'; + +/// A windowed numbered pagination widget that mirrors the Next.js +/// implementation: ±10 page-button window around the current page, +/// 1-based labels, prev/next chevrons. +class WindowedPagination extends StatelessWidget { + const WindowedPagination({ + super.key, + required this.currentPage, + required this.totalPages, + required this.onPageChanged, + this.maxPageJump = 10, + this.compact = false, + }); + + /// 0-based absolute current page index. + final int currentPage; + + /// Total number of pages (1-based count). + final int totalPages; + + /// Called with the 0-based target page index. + final ValueChanged onPageChanged; + + /// Half-width of the page-button window (web uses 10). + final int maxPageJump; + + /// If true, renders smaller buttons (for mobile). + final bool compact; + + @override + Widget build(BuildContext context) { + if (totalPages <= 1) { + return const SizedBox.shrink(); + } + + final clampedCurrent = currentPage.clamp(0, totalPages - 1); + final windowStart = (clampedCurrent - maxPageJump).clamp(0, totalPages - 1); + final windowEnd = (clampedCurrent + maxPageJump).clamp(0, totalPages - 1); + + final buttons = []; + + buttons.add( + _NavButton( + icon: Icons.chevron_left, + enabled: clampedCurrent > 0, + compact: compact, + onTap: () => onPageChanged(clampedCurrent - 1), + ), + ); + + if (windowStart > 0) { + buttons.add(_buildPageButton(0, clampedCurrent)); + if (windowStart > 1) { + buttons.add(const _Ellipsis()); + } + } + + for (var i = windowStart; i <= windowEnd; i++) { + buttons.add(_buildPageButton(i, clampedCurrent)); + } + + if (windowEnd < totalPages - 1) { + if (windowEnd < totalPages - 2) { + buttons.add(const _Ellipsis()); + } + buttons.add(_buildPageButton(totalPages - 1, clampedCurrent)); + } + + buttons.add( + _NavButton( + icon: Icons.chevron_right, + enabled: clampedCurrent < totalPages - 1, + compact: compact, + onTap: () => onPageChanged(clampedCurrent + 1), + ), + ); + + final gap = compact ? 4.0 : 6.0; + return Padding( + padding: EdgeInsets.symmetric(vertical: compact ? 8 : 12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _withSpacing(buttons, gap), + ), + ), + ); + }, + ), + ); + } + + Widget _buildPageButton(int pageIndex, int currentPageIndex) { + final isActive = pageIndex == currentPageIndex; + return _PageButton( + label: '${pageIndex + 1}', + isActive: isActive, + compact: compact, + onTap: isActive ? null : () => onPageChanged(pageIndex), + ); + } + + static List _withSpacing(List items, double gap) { + final result = []; + for (var i = 0; i < items.length; i++) { + result.add(items[i]); + if (i != items.length - 1) { + result.add(SizedBox(width: gap)); + } + } + return result; + } +} + +class _PageButton extends StatelessWidget { + const _PageButton({ + required this.label, + required this.isActive, + required this.compact, + this.onTap, + }); + + final String label; + final bool isActive; + final bool compact; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final size = compact ? 32.0 : 38.0; + return SizedBox( + width: size, + height: size, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: isActive + ? const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.mainBlue, AppColors.primaryColor], + ) + : null, + color: isActive ? null : AppColors.grey0, + border: Border.all( + color: isActive ? Colors.transparent : AppColors.grey30, + width: 1, + ), + boxShadow: isActive + ? [ + BoxShadow( + color: AppColors.mainBlue.withValues(alpha: 0.25), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Text( + label, + style: TextStyle( + fontSize: compact ? 12 : 13, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, + color: isActive ? Colors.white : AppColors.grey70, + ), + ), + ), + ), + ), + ); + } +} + +class _NavButton extends StatelessWidget { + const _NavButton({ + required this.icon, + required this.enabled, + required this.compact, + required this.onTap, + }); + + final IconData icon; + final bool enabled; + final bool compact; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final size = compact ? 32.0 : 38.0; + return SizedBox( + width: size, + height: size, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: enabled ? onTap : null, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: AppColors.grey0, + border: Border.all( + color: enabled ? AppColors.grey30 : AppColors.grey20, + width: 1, + ), + ), + child: Icon( + icon, + size: compact ? 18 : 20, + color: enabled ? AppColors.grey70 : AppColors.grey40, + ), + ), + ), + ), + ); + } +} + +class _Ellipsis extends StatelessWidget { + const _Ellipsis(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Text( + '…', + style: TextStyle( + fontSize: 14, + color: AppColors.grey60, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 386acf0..97cd259 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,143 +6,143 @@ packages: description: name: _fe_analyzer_shared sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "85.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "948f7d74f41dd6f2d563ea9f4c21d7ea764f8e047d2b24138974c19c24d37eb6" - url: "https://pub.dev" + sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.61" + version: "1.3.71" analyzer: dependency: transitive description: name: analyzer sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.7.1" archive: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" async: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 - url: "https://pub.dev" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.12.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" build: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" - url: "https://pub.dev" + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.4" + version: "3.1.0" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - url: "https://pub.dev" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" - url: "https://pub.dev" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.4" + version: "4.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 - url: "https://pub.dev" + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.4" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" - url: "https://pub.dev" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.4" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" - url: "https://pub.dev" + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" + url: "https://pub.flutter-io.cn" source: hosted - version: "9.1.2" + version: "9.3.1" built_collection: dependency: transitive description: name: built_collection sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d - url: "https://pub.dev" + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.flutter-io.cn" source: hosted - version: "8.12.0" + version: "8.12.6" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.3" + version: "2.0.4" cli_util: dependency: transitive description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2" clock: @@ -150,23 +150,31 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: dad6bf6b9f4f378b0a69edbf42584d336efd1a9ce15deb1ba591cbb1b5ff440f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - url: "https://pub.dev" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" color: @@ -174,7 +182,7 @@ packages: description: name: color sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: @@ -182,39 +190,39 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" cross_file: dependency: transitive description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.4+2" + version: "0.3.5+2" crypto: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.8" + version: "1.0.9" dart_style: dependency: transitive description: name: dart_style sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" dartx: @@ -222,135 +230,135 @@ packages: description: name: dartx sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" - url: "https://pub.dev" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.11" + version: "0.7.12" dio: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 - url: "https://pub.dev" + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.flutter-io.cn" source: hosted - version: "5.9.0" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.dev" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.1" + version: "2.1.2" dotted_border: dependency: "direct main" description: name: dotted_border sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" equatable: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" - url: "https://pub.dev" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.4" + version: "2.2.0" file: dependency: transitive description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_picker: dependency: "direct main" description: name: file_picker - sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f - url: "https://pub.dev" + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + url: "https://pub.flutter-io.cn" source: hosted - version: "10.3.3" + version: "10.3.10" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "967dae9a65f69377beb9f4ab292ea63ce5befa1ce24682cab1b69ca4b7a46927" - url: "https://pub.dev" + sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.0" + version: "4.9.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" - url: "https://pub.dev" + sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.0" + version: "7.0.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f7ee08febc1c4451588ce58ffcf28edaee857e9a196fee88b85deb889990094a - url: "https://pub.dev" + sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0" + version: "3.7.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: aad5dcdea5698499b70d74d5a53b1f6a9972f85f97225e4b7ac006dd8d4f9bac - url: "https://pub.dev" + sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7" + url: "https://pub.flutter-io.cn" source: hosted - version: "16.0.1" + version: "16.2.2" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "825bc11767bf50a43dccf49b3026f847ec31d0f176139bfc48d662cc128b5014" - url: "https://pub.dev" + sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.7.1" + version: "4.7.11" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: db8dbdd79921245c4de02407e33cae2d1868683be18a5ba948d2af5311e3ef5d - url: "https://pub.dev" + sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.1" + version: "4.1.7" fixnum: dependency: transitive description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" flutter: @@ -362,24 +370,24 @@ packages: dependency: transitive description: name: flutter_gen_core - sha256: eda54fdc5de08e7eeea663eb8442aafc8660b5a13fda4e0c9e572c64e50195fb - url: "https://pub.dev" + sha256: "454ec926fd0758667615942032e1e5308d240e309627bdf3f2bf55b17d54b23e" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.11.0" + version: "5.14.0" flutter_gen_runner: dependency: "direct dev" description: name: flutter_gen_runner - sha256: "669bf8b7a9b4acbdcb7fcc5e12bf638aca19acedf43341714cbca3bf3a219521" - url: "https://pub.dev" + sha256: "2c926ab58103ea483963df9a000031e11958c7f4d1065ab021adc6829d9f6ddc" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.11.0" + version: "5.14.0" flutter_launcher_icons: dependency: "direct main" description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.14.4" flutter_lints: @@ -387,25 +395,25 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 - url: "https://pub.dev" + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.30" + version: "2.0.34" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 - url: "https://pub.dev" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -420,16 +428,16 @@ packages: dependency: "direct main" description: name: freezed - sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22" - url: "https://pub.dev" + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: name: freezed_annotation sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" frontend_server_client: @@ -437,7 +445,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" glob: @@ -445,31 +453,31 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227 - url: "https://pub.dev" + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + url: "https://pub.flutter-io.cn" source: hosted - version: "16.2.1" + version: "16.3.0" go_router_builder: dependency: "direct dev" description: name: go_router_builder - sha256: e436f0c69e717bd215639fbe27381b7e6605a255852b45cb244e28f01ab2e93a - url: "https://pub.dev" + sha256: "75200eb331e3bdfcaaa7df059c7db151c6efa56437f6715698cb3ad3d94c36e4" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.0" + version: "3.3.1" graphs: dependency: transitive description: name: graphs sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" hashcodes: @@ -477,23 +485,31 @@ packages: description: name: hashcodes sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31 + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" http: dependency: "direct main" description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 - url: "https://pub.dev" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" http_parser: @@ -501,31 +517,23 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" - iconsax_flutter: - dependency: transitive - description: - name: iconsax_flutter - sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 - url: "https://pub.dev" - source: hosted - version: "1.0.1" image: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.dev" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.flutter-io.cn" source: hosted - version: "4.5.4" + version: "4.8.0" image_size_getter: dependency: transitive description: name: image_size_getter sha256: "7c26937e0ae341ca558b7556591fd0cc456fcc454583b7cb665d2f03e93e590f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" intl: @@ -533,7 +541,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" io: @@ -541,111 +549,119 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" - js: + jni: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.7" + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" json_annotation: dependency: "direct main" description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c - url: "https://pub.dev" + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 + url: "https://pub.flutter-io.cn" source: hosted - version: "6.9.5" + version: "6.11.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec - url: "https://pub.dev" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" logger: dependency: "direct main" description: name: logger - sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" - url: "https://pub.dev" + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "2.7.0" logging: dependency: transitive description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.16.0" + version: "1.18.0" mime: dependency: transitive description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" nested: @@ -653,15 +669,23 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.1" package_config: dependency: transitive description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" path: @@ -669,7 +693,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_parsing: @@ -677,7 +701,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" path_provider: @@ -685,31 +709,31 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" - url: "https://pub.dev" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.18" + version: "2.3.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" - url: "https://pub.dev" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.2" + version: "2.6.0" path_provider_linux: dependency: transitive description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -717,7 +741,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" path_provider_windows: @@ -725,7 +749,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" pausable_timer: @@ -733,23 +757,23 @@ packages: description: name: pausable_timer sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0+3" petitparser: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" - url: "https://pub.dev" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.0" + version: "7.0.2" pinput: dependency: "direct main" description: name: pinput sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.2" platform: @@ -757,7 +781,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -765,31 +789,31 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.3" + version: "6.5.0" provider: dependency: "direct main" description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" pub_semver: @@ -797,7 +821,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" pubspec_parse: @@ -805,55 +829,63 @@ packages: description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.3" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 - url: "https://pub.dev" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.12" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -861,7 +893,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shelf: @@ -869,7 +901,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" shelf_web_socket: @@ -877,7 +909,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -889,40 +921,32 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - url: "https://pub.dev" + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "3.1.0" source_helper: dependency: transitive description: name: source_helper - sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca - url: "https://pub.dev" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.7" + version: "1.3.8" source_span: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -930,7 +954,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" stream_transform: @@ -938,7 +962,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" string_scanner: @@ -946,7 +970,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" term_glyph: @@ -954,47 +978,47 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.4" + version: "0.7.11" time: dependency: transitive description: name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" - url: "https://pub.dev" + sha256: "46187cf30bffdab28c56be9a63861b36e4ab7347bf403297595d6a97e10c789f" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.5" + version: "2.1.6" timing: dependency: transitive description: name: timing sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" toastification: dependency: "direct main" description: name: toastification - sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" - url: "https://pub.dev" + sha256: "66c96678e3dece8ba24de3ea31634bd65a80aaecb8105f9bafe946e5f0d7590a" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.3" + version: "3.2.0" typed_data: dependency: transitive description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" universal_platform: @@ -1002,71 +1026,71 @@ packages: description: name: universal_platform sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.5.1" + version: "4.5.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.19" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc - url: "https://pub.dev" + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.19" + version: "1.2.3" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" - url: "https://pub.dev" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.flutter-io.cn" source: hosted - version: "14.3.1" + version: "15.2.0" watcher: dependency: transitive description: name: watcher - sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" - url: "https://pub.dev" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.3" + version: "1.2.1" web: dependency: transitive description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" web_socket: @@ -1074,7 +1098,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: @@ -1082,41 +1106,41 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" - url: "https://pub.dev" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.flutter-io.cn" source: hosted - version: "5.13.0" + version: "5.15.0" xdg_directories: dependency: transitive description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" sdks: - dart: ">=3.7.2 <4.0.0" - flutter: ">=3.29.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" From e6b4720dcdb65ad06da66786ad5873354e156d01 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 30 May 2026 16:48:05 +0330 Subject: [PATCH 6/7] Update iOS deployment target to 13.0, enhance Flutter integration, and improve tender submission flow - Changed the iOS deployment target from 12.0 to 13.0 in Podfile and project settings. - Updated AppDelegate to support implicit Flutter engine initialization. - Modified Info.plist to include scene configuration for multiple scenes. - Enhanced CompletionOfDocumentsRouteData to accept a tenderId parameter. - Implemented tender submission functionality in TenderDetailViewModel and updated UI components to reflect loading states and success/error messages. - Added new strings for submission success and error notifications. --- ios/Flutter/AppFrameworkInfo.plist | 2 - ios/Podfile | 2 +- ios/Runner.xcodeproj/project.pbxproj | 28 ++- .../xcshareddata/swiftpm/Package.resolved | 176 ++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 20 ++ ios/Runner/AppDelegate.swift | 7 +- ios/Runner/Info.plist | 29 ++- lib/core/routes/app_routes.dart | 7 +- lib/core/routes/app_routes.g.dart | 14 +- lib/view_models/tender_detail_view_model.dart | 30 +++ lib/view_models/your_tenders_view_model.dart | 17 +- .../complectopn_of_documents_screen.dart | 11 +- .../pages/d_completion_of_documents_page.dart | 46 ++++- .../pages/m_completion_of_documents_page.dart | 42 ++++- .../pages/t_completion_of_documents_page.dart | 46 ++++- .../completion_of_documents_string.dart | 2 + .../detail/widgets/tender_detail_action.dart | 11 +- .../profile/strings/profile_strings.dart | 1 + lib/views/profile/widgets/main_data_tab.dart | 9 + 19 files changed, 445 insertions(+), 55 deletions(-) create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..391a902 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee2..620e46e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d08bbb7..e13d49d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,6 +64,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -79,6 +82,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -142,6 +146,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -165,6 +172,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -346,7 +356,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -472,7 +482,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -523,7 +533,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -611,6 +621,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7e63959 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,176 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "dkcamera", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKCamera", + "state" : { + "branch" : "master", + "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" + } + }, + { + "identity" : "dkimagepickercontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKImagePickerController", + "state" : { + "branch" : "4.3.9", + "revision" : "0bdfeacefa308545adde07bef86e349186335915" + } + }, + { + "identity" : "dkphotogallery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKPhotoGallery", + "state" : { + "branch" : "master", + "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", + "version" : "12.14.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", + "version" : "3.6.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", + "version" : "12.14.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", + "version" : "5.3.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage", + "state" : { + "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", + "version" : "5.21.7" + } + }, + { + "identity" : "swiftygif", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kirualex/SwiftyGif.git", + "state" : { + "revision" : "4430cbc148baa3907651d40562d96325426f409a", + "version" : "5.4.5" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController", + "state" : { + "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e", + "version" : "2.8.0" + } + } + ], + "version" : 2 +} diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada4..c3fedb2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3abf06c..a328b97 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +66,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index f8aa9e6..4b039fe 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -250,11 +250,14 @@ class FinalCompletionOfDocumentsRouteData extends GoRouteData @TypedGoRoute(path: '/completion_of_documents') class CompletionOfDocumentsRouteData extends GoRouteData with _$CompletionOfDocumentsRouteData { - const CompletionOfDocumentsRouteData(); + final String? tenderId; + const CompletionOfDocumentsRouteData({this.tenderId}); @override Widget build(BuildContext context, GoRouterState state) { - return const CompletionOfDocumentsScreen(); + return tenderDetailProvider( + child: CompletionOfDocumentsScreen(tenderId: tenderId), + ); } } diff --git a/lib/core/routes/app_routes.g.dart b/lib/core/routes/app_routes.g.dart index 261a276..d4e96d6 100644 --- a/lib/core/routes/app_routes.g.dart +++ b/lib/core/routes/app_routes.g.dart @@ -308,10 +308,20 @@ RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route( mixin _$CompletionOfDocumentsRouteData on GoRouteData { static CompletionOfDocumentsRouteData _fromState(GoRouterState state) => - const CompletionOfDocumentsRouteData(); + CompletionOfDocumentsRouteData( + tenderId: state.uri.queryParameters['tender-id'], + ); + + CompletionOfDocumentsRouteData get _self => + this as CompletionOfDocumentsRouteData; @override - String get location => GoRouteData.$location('/completion_of_documents'); + String get location => GoRouteData.$location( + '/completion_of_documents', + queryParams: { + if (_self.tenderId != null) 'tender-id': _self.tenderId!, + }, + ); @override void go(BuildContext context) => context.go(location); diff --git a/lib/view_models/tender_detail_view_model.dart b/lib/view_models/tender_detail_view_model.dart index d5b0b0d..a4c5f99 100644 --- a/lib/view_models/tender_detail_view_model.dart +++ b/lib/view_models/tender_detail_view_model.dart @@ -6,6 +6,7 @@ import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart'; import '../core/constants/tender_approval_status.dart'; +import '../core/constants/tender_submision_mode.dart'; import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart'; import '../data/services/model/tender_approvals_data/tender_approvals_data.dart'; import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart'; @@ -143,4 +144,33 @@ class TenderDetailViewModel with ChangeNotifier { _errorMessage = null; notifyListeners(); } + + /// Submits the tender for completion via the self-apply flow. + /// POSTs to /api/v1/tender-approvals with status "submitted" and + /// submission_mode "self-apply". Returns true on success. + Future submitForCompletion({required String tenderId}) async { + _isSubmitApprovalLoading = true; + _errorMessage = null; + notifyListeners(); + + bool success = false; + final result = await _tendersRepository.toggleApprovals( + tenderId: tenderId, + status: TenderApprovalStatus.submitted.value, + submissionMode: TenderSubmissionMode.selfApply.value, + ); + switch (result) { + case Ok(): + success = true; + _status = result.value.data?.status ?? ''; + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + _isSubmitApprovalLoading = false; + notifyListeners(); + return success; + } } diff --git a/lib/view_models/your_tenders_view_model.dart b/lib/view_models/your_tenders_view_model.dart index a7aee20..493b373 100644 --- a/lib/view_models/your_tenders_view_model.dart +++ b/lib/view_models/your_tenders_view_model.dart @@ -206,7 +206,9 @@ class YourTendersViewModel with ChangeNotifier { offset: _tendersOffset, ); await getTenders(requestModel: requestModel); - } else if (selectedStatus == YourTendersStrings.tendersSubmitted) { + } else if (selectedStatus == YourTendersStrings.tendersSubmitted || + selectedStatus == TenderApprovalStatus.submitted.value || + selectedStatus == TenderApprovalStatus.approved.value) { final requestModel = GetTendersRequestModel( status: TenderApprovalStatus.submitted.value, createdFrom: startDateUnix, @@ -299,10 +301,17 @@ class YourTendersViewModel with ChangeNotifier { reset: true, ); } else if (selectedStatus == TenderApprovalStatus.approved.value) { - await getTendersFeedback( - feedbackType: TenderApprovalStatus.approved.value, - reset: true, + // "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); } } diff --git a/lib/views/completion_of_documents/pages/complectopn_of_documents_screen.dart b/lib/views/completion_of_documents/pages/complectopn_of_documents_screen.dart index 2b902ee..b65d93e 100644 --- a/lib/views/completion_of_documents/pages/complectopn_of_documents_screen.dart +++ b/lib/views/completion_of_documents/pages/complectopn_of_documents_screen.dart @@ -7,7 +7,8 @@ import '../../../core/utils/size_config.dart'; import '../../shared/responsive_builder.dart'; class CompletionOfDocumentsScreen extends StatefulWidget { - const CompletionOfDocumentsScreen({super.key}); + final String? tenderId; + const CompletionOfDocumentsScreen({this.tenderId, super.key}); @override State createState() => _CompletionOfDocumentsScreenState(); @@ -41,10 +42,10 @@ class _CompletionOfDocumentsScreenState extends State( + builder: (context, viewModel, _) => BaseButton( + isEnabled: !viewModel.isSubmitApprovalLoading, + isLoading: viewModel.isSubmitApprovalLoading, + onPressed: () => _submitForCompletion(context), + text: + CompletionOfDocumentsStrings + .submitForCompletion, + textColor: AppColors.textBlue, + backgroundColor: AppColors.primary30, + ), ), SizedBox(height: 24.0.h()), ], @@ -304,6 +312,26 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget { ); } + Future _submitForCompletion(BuildContext context) async { + final id = tenderId; + if (id == null || id.isEmpty) { + AppToast.error(context, CompletionOfDocumentsStrings.submitError); + return; + } + final viewModel = context.read(); + final success = await viewModel.submitForCompletion(tenderId: id); + if (!context.mounted) return; + if (success) { + AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess); + context.pop(); + } else { + AppToast.error( + context, + viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError, + ); + } + } + Widget _docCard({ required String title, required String subtitle, diff --git a/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart b/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart index 22bcbd2..0d28c32 100644 --- a/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart +++ b/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart @@ -3,13 +3,18 @@ import 'package:flutter_svg/svg.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tm_app/core/utils/app_toast.dart'; +import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart'; import 'package:tm_app/views/shared/base_button.dart'; import '../../shared/tender_app_bar.dart'; class CompletionOfDocumentsMobilePage extends StatelessWidget { - const CompletionOfDocumentsMobilePage({super.key}); + final String? tenderId; + const CompletionOfDocumentsMobilePage({this.tenderId, super.key}); @override Widget build(BuildContext context) { @@ -242,12 +247,15 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget { ), ), SizedBox(height: 24.0.h()), - BaseButton( - isEnabled: true, - onPressed: () {}, - text: CompletionOfDocumentsStrings.submitForCompletion, - textColor: AppColors.textBlue, - backgroundColor: AppColors.primary30, + Consumer( + builder: (context, viewModel, _) => BaseButton( + isEnabled: !viewModel.isSubmitApprovalLoading, + isLoading: viewModel.isSubmitApprovalLoading, + onPressed: () => _submitForCompletion(context), + text: CompletionOfDocumentsStrings.submitForCompletion, + textColor: AppColors.textBlue, + backgroundColor: AppColors.primary30, + ), ), ], ), @@ -256,6 +264,26 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget { ); } + Future _submitForCompletion(BuildContext context) async { + final id = tenderId; + if (id == null || id.isEmpty) { + AppToast.error(context, CompletionOfDocumentsStrings.submitError); + return; + } + final viewModel = context.read(); + final success = await viewModel.submitForCompletion(tenderId: id); + if (!context.mounted) return; + if (success) { + AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess); + context.pop(); + } else { + AppToast.error( + context, + viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError, + ); + } + } + Widget _docCard({ required String title, required String subtitle, diff --git a/lib/views/completion_of_documents/pages/t_completion_of_documents_page.dart b/lib/views/completion_of_documents/pages/t_completion_of_documents_page.dart index adbeeff..de5a21b 100644 --- a/lib/views/completion_of_documents/pages/t_completion_of_documents_page.dart +++ b/lib/views/completion_of_documents/pages/t_completion_of_documents_page.dart @@ -3,6 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tm_app/core/utils/app_toast.dart'; +import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/tablet_desktop_appbar.dart'; @@ -10,7 +14,8 @@ import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/shared/tender_app_bar.dart'; class CompletionOfDocumentsTabletPage extends StatelessWidget { - const CompletionOfDocumentsTabletPage({super.key}); + final String? tenderId; + const CompletionOfDocumentsTabletPage({this.tenderId, super.key}); @override Widget build(BuildContext context) { @@ -83,14 +88,17 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget { SizedBox(height: 24.0.h()), - BaseButton( - isEnabled: true, - onPressed: () {}, - text: - CompletionOfDocumentsStrings - .submitForCompletion, - textColor: AppColors.textBlue, - backgroundColor: AppColors.primary30, + Consumer( + builder: (context, viewModel, _) => BaseButton( + isEnabled: !viewModel.isSubmitApprovalLoading, + isLoading: viewModel.isSubmitApprovalLoading, + onPressed: () => _submitForCompletion(context), + text: + CompletionOfDocumentsStrings + .submitForCompletion, + textColor: AppColors.textBlue, + backgroundColor: AppColors.primary30, + ), ), ], ), @@ -309,6 +317,26 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget { ); } + Future _submitForCompletion(BuildContext context) async { + final id = tenderId; + if (id == null || id.isEmpty) { + AppToast.error(context, CompletionOfDocumentsStrings.submitError); + return; + } + final viewModel = context.read(); + final success = await viewModel.submitForCompletion(tenderId: id); + if (!context.mounted) return; + if (success) { + AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess); + context.pop(); + } else { + AppToast.error( + context, + viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError, + ); + } + } + // -------------------- Document Card -------------------- Widget _docCard({ required String title, diff --git a/lib/views/completion_of_documents/strings/completion_of_documents_string.dart b/lib/views/completion_of_documents/strings/completion_of_documents_string.dart index 14af095..c2baa04 100644 --- a/lib/views/completion_of_documents/strings/completion_of_documents_string.dart +++ b/lib/views/completion_of_documents/strings/completion_of_documents_string.dart @@ -7,4 +7,6 @@ class CompletionOfDocumentsStrings { 'Set Deadline for Completion of documents :'; static const String note = 'Note:'; static const String submitForCompletion = 'Submit for completion'; + static const String submitSuccess = 'Tender submitted for completion'; + static const String submitError = 'Failed to submit tender for completion'; } diff --git a/lib/views/detail/widgets/tender_detail_action.dart b/lib/views/detail/widgets/tender_detail_action.dart index e2a8e08..4dac05c 100644 --- a/lib/views/detail/widgets/tender_detail_action.dart +++ b/lib/views/detail/widgets/tender_detail_action.dart @@ -75,9 +75,9 @@ class _TenderDetailActionsState extends State { // submissionMode: value, // ); - const CompletionOfDocumentsRouteData().push( - context, - ); + CompletionOfDocumentsRouteData( + tenderId: widget.detail.id!, + ).push(context); } else if (value == TenderSubmissionMode.partnership.value) { Future.delayed(Duration.zero, () { @@ -213,8 +213,9 @@ class _TenderDetailActionsState extends State { const FinalCompletionOfDocumentsRouteData() .push(context); } else { - const CompletionOfDocumentsRouteData() - .push(context); + CompletionOfDocumentsRouteData( + tenderId: widget.detail.id!, + ).push(context); } } } else if (value == diff --git a/lib/views/profile/strings/profile_strings.dart b/lib/views/profile/strings/profile_strings.dart index ab08801..f19f769 100644 --- a/lib/views/profile/strings/profile_strings.dart +++ b/lib/views/profile/strings/profile_strings.dart @@ -23,4 +23,5 @@ class ProfileStrings { static const String mainDataTab = 'Main Data'; static const String membersTab = 'Members'; static const String subjectTab = 'Subject'; + static const String appVersion = 'Version 1.0.7'; } diff --git a/lib/views/profile/widgets/main_data_tab.dart b/lib/views/profile/widgets/main_data_tab.dart index 9171a59..c176ada 100644 --- a/lib/views/profile/widgets/main_data_tab.dart +++ b/lib/views/profile/widgets/main_data_tab.dart @@ -98,6 +98,15 @@ class MainDataTab extends StatelessWidget { ), ), ), + SizedBox(height: 24.0.h()), + Text( + ProfileStrings.appVersion, + style: TextStyle( + fontSize: 12.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey70, + ), + ), ], ), ); From 83b77c05efef6cfcd5e45fc1de38fc39217cb2f3 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 30 May 2026 18:46:13 +0330 Subject: [PATCH 7/7] Refactor card movement logic in BoardViewModel to improve error handling and success validation - Updated the handling of card movement results to differentiate between successful and unsuccessful API responses. - Implemented rollback of board state and error messaging for failed moves, ensuring a more robust user experience. - Enhanced the notification system to surface relevant error messages when card movements fail. --- lib/view_models/board_view_model.dart | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/view_models/board_view_model.dart b/lib/view_models/board_view_model.dart index c568cf1..ad6bcb0 100644 --- a/lib/view_models/board_view_model.dart +++ b/lib/view_models/board_view_model.dart @@ -104,10 +104,24 @@ class BoardViewModel with ChangeNotifier { newOrder: newOrder, ); - if (result is Error) { - _board = _board!.copyWith(columns: previousColumns); - notifyListeners(); + switch (result) { + case Ok(): + // HTTP 200 alone isn't success: the API can return + // {"success": false, "message": ...} (e.g. column over-limit, + // permission denied). Only keep the optimistic move when the body + // confirms success; otherwise roll back and surface the message. + if (result.value.success == true) { + return result; + } + _board = _board!.copyWith(columns: previousColumns); + notifyListeners(); + return Result.error( + Exception(result.value.message ?? 'Failed to move card'), + ); + case Error(): + _board = _board!.copyWith(columns: previousColumns); + notifyListeners(); + return result; } - return result; } }