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.
This commit is contained in:
@@ -1,9 +1,32 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
String timeConvertor(int timestamp) {
|
/// Parses API date fields that may arrive as int, double, or string.
|
||||||
// Convert seconds → milliseconds
|
int? unixTimestampFromJson(dynamic value) {
|
||||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
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);
|
return DateFormat('yyyy-MM-dd').format(date);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ignore_for_file: invalid_annotation_target
|
// ignore_for_file: invalid_annotation_target
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
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';
|
import 'package:tm_app/data/services/model/organization/organization.dart';
|
||||||
|
|
||||||
part 'tender_data.freezed.dart';
|
part 'tender_data.freezed.dart';
|
||||||
@@ -15,7 +16,8 @@ abstract class TenderData with _$TenderData {
|
|||||||
@JsonKey(name: 'tender_id') required String? tenderId,
|
@JsonKey(name: 'tender_id') required String? tenderId,
|
||||||
@JsonKey(name: 'notice_publication_id')
|
@JsonKey(name: 'notice_publication_id')
|
||||||
required String? noticePublicationId,
|
required String? noticePublicationId,
|
||||||
@JsonKey(name: 'publication_date') required int? publicationDate,
|
@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson)
|
||||||
|
required int? publicationDate,
|
||||||
required String? title,
|
required String? title,
|
||||||
required String? description,
|
required String? description,
|
||||||
@JsonKey(name: 'procurement_type_code')
|
@JsonKey(name: 'procurement_type_code')
|
||||||
@@ -24,9 +26,12 @@ abstract class TenderData with _$TenderData {
|
|||||||
@JsonKey(name: 'estimated_value') required int? estimatedValue,
|
@JsonKey(name: 'estimated_value') required int? estimatedValue,
|
||||||
required String? currency,
|
required String? currency,
|
||||||
required String? duration,
|
required String? duration,
|
||||||
@JsonKey(name: 'tender_deadline') required int? tenderDeadline,
|
@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson)
|
||||||
@JsonKey(name: 'submission_deadline') required int? submissionDeadline,
|
required int? tenderDeadline,
|
||||||
@JsonKey(name: 'application_deadline') required int? applicationDeadline,
|
@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: 'submission_url') required String? submissionUrl,
|
||||||
@JsonKey(name: 'country_code') required String? countryCode,
|
@JsonKey(name: 'country_code') required String? countryCode,
|
||||||
@JsonKey(name: 'duration_unit') required String? durationUnit,
|
@JsonKey(name: 'duration_unit') required String? durationUnit,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
|
|||||||
id: json['id'] as String?,
|
id: json['id'] as String?,
|
||||||
tenderId: json['tender_id'] as String?,
|
tenderId: json['tender_id'] as String?,
|
||||||
noticePublicationId: json['notice_publication_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?,
|
title: json['title'] as String?,
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
procurementTypeCode: json['procurement_type_code'] as String?,
|
procurementTypeCode: json['procurement_type_code'] as String?,
|
||||||
@@ -20,9 +20,9 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
|
|||||||
estimatedValue: (json['estimated_value'] as num?)?.toInt(),
|
estimatedValue: (json['estimated_value'] as num?)?.toInt(),
|
||||||
currency: json['currency'] as String?,
|
currency: json['currency'] as String?,
|
||||||
duration: json['duration'] as String?,
|
duration: json['duration'] as String?,
|
||||||
tenderDeadline: (json['tender_deadline'] as num?)?.toInt(),
|
tenderDeadline: unixTimestampFromJson(json['tender_deadline']),
|
||||||
submissionDeadline: (json['submission_deadline'] as num?)?.toInt(),
|
submissionDeadline: unixTimestampFromJson(json['submission_deadline']),
|
||||||
applicationDeadline: (json['application_deadline'] as num?)?.toInt(),
|
applicationDeadline: unixTimestampFromJson(json['application_deadline']),
|
||||||
submissionUrl: json['submission_url'] as String?,
|
submissionUrl: json['submission_url'] as String?,
|
||||||
countryCode: json['country_code'] as String?,
|
countryCode: json['country_code'] as String?,
|
||||||
durationUnit: json['duration_unit'] as String?,
|
durationUnit: json['duration_unit'] as String?,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class DeadlineItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4.0.h()),
|
SizedBox(height: 4.0.h()),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(detail.submissionDeadline!),
|
timeConvertor(detail.submissionDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey80,
|
color: AppColors.grey80,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -80,7 +80,7 @@ class DeadlineItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4.0.h()),
|
SizedBox(height: 4.0.h()),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(detail.applicationDeadline!),
|
timeConvertor(detail.applicationDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey80,
|
color: AppColors.grey80,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -124,7 +124,7 @@ class DeadlineItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(detail.submissionDeadline!),
|
timeConvertor(detail.submissionDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey70,
|
color: AppColors.grey70,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -146,7 +146,7 @@ class DeadlineItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(detail.applicationDeadline!),
|
timeConvertor(detail.applicationDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey70,
|
color: AppColors.grey70,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TendersListItem extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Text(
|
child: Text(
|
||||||
timeConvertor(tender.publicationDate ?? 0),
|
timeConvertor(tender.publicationDate),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12.0.sp(),
|
fontSize: 12.0.sp(),
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -64,7 +64,7 @@ class TendersListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(tender.tenderDeadline ?? 0),
|
timeConvertor(tender.tenderDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0.sp(),
|
fontSize: 14.0.sp(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class LikedListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(tender.tenderDeadline!),
|
timeConvertor(tender.tenderDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0.sp(),
|
fontSize: 14.0.sp(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -117,7 +117,7 @@ class LikedListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(tender.tenderDeadline!),
|
timeConvertor(tender.tenderDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0.sp(),
|
fontSize: 14.0.sp(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class TenderCard extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _dateText() {
|
Widget _dateText() {
|
||||||
return Text(
|
return Text(
|
||||||
timeConvertor(tender.publicationDate ?? 0),
|
timeConvertor(tender.publicationDate),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey60,
|
color: AppColors.grey60,
|
||||||
fontSize: 12.0.sp(),
|
fontSize: 12.0.sp(),
|
||||||
@@ -150,7 +150,7 @@ class TenderCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(width: 4.0.w()),
|
SizedBox(width: 4.0.w()),
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(tender.submissionDeadline ?? 0),
|
timeConvertor(tender.submissionDeadline),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey80,
|
color: AppColors.grey80,
|
||||||
fontSize: 14.0.sp(),
|
fontSize: 14.0.sp(),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class TenderCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Date
|
// Date
|
||||||
Text(
|
Text(
|
||||||
timeConvertor(int.parse(tender.publicationDate!.toString())),
|
timeConvertor(tender.publicationDate),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey60,
|
color: AppColors.grey60,
|
||||||
fontSize: 12.0.sp(),
|
fontSize: 12.0.sp(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user