From 2515190576197735620d595d2ec0b012a62e67fb Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 24 May 2026 15:28:47 +0330 Subject: [PATCH] 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); + }); + }); +}