From d32449d096eb0906580d240d3edd6275f2a26bc9 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Sat, 21 Feb 2026 08:32:07 +0800 Subject: [PATCH] Common Date and Time fomatters --- lib/screens/tasks/tasks_list_screen.dart | 15 +------ lib/screens/tickets/ticket_detail_screen.dart | 29 +----------- lib/screens/tickets/tickets_list_screen.dart | 15 +------ lib/utils/app_time.dart | 44 +++++++++++++++++++ test/app_time_test.dart | 40 +++++++++++++++++ 5 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 test/app_time_test.dart diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 479d164f..555c5c81 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tasq/utils/app_time.dart'; import 'package:go_router/go_router.dart'; import '../../models/notification_item.dart'; @@ -13,7 +14,6 @@ import '../../providers/profile_provider.dart'; import '../../providers/tasks_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../providers/typing_provider.dart'; -import '../../utils/app_time.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; @@ -203,7 +203,7 @@ class _TasksListScreenState extends ConsumerState { label: Text( _selectedDateRange == null ? 'Date range' - : _formatDateRange(_selectedDateRange!), + : AppTime.formatDateRange(_selectedDateRange!), ), ), if (_hasTaskFilters) @@ -636,17 +636,6 @@ Map _taskStatusCounts(List tasks) { return counts; } -String _formatDateRange(DateTimeRange range) { - return '${_formatDate(range.start)} - ${_formatDate(range.end)}'; -} - -String _formatDate(DateTime value) { - final year = value.year.toString().padLeft(4, '0'); - final month = value.month.toString().padLeft(2, '0'); - final day = value.day.toString().padLeft(2, '0'); - return '$year-$month-$day'; -} - class _StatusSummaryRow extends StatelessWidget { const _StatusSummaryRow({required this.counts}); diff --git a/lib/screens/tickets/ticket_detail_screen.dart b/lib/screens/tickets/ticket_detail_screen.dart index 9c0c8c80..2a6702f8 100644 --- a/lib/screens/tickets/ticket_detail_screen.dart +++ b/lib/screens/tickets/ticket_detail_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tasq/utils/app_time.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -809,32 +810,6 @@ class _TicketDetailScreenState extends ConsumerState { return office?.name ?? ticket.officeId; } - String _formatDate(DateTime value) { - final local = value.toLocal(); - final monthNames = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - final month = monthNames[local.month - 1]; - final day = local.day.toString().padLeft(2, '0'); - final year = local.year.toString(); - final hour24 = local.hour; - final hour12 = hour24 % 12 == 0 ? 12 : hour24 % 12; - final minute = local.minute.toString().padLeft(2, '0'); - final ampm = hour24 >= 12 ? 'PM' : 'AM'; - return '$month $day, $year $hour12:$minute $ampm'; - } - Future _showTimelineDialog(BuildContext context, Ticket ticket) async { await showDialog( context: context, @@ -866,7 +841,7 @@ class _TicketDetailScreenState extends ConsumerState { Widget _timelineRow(String label, DateTime? value) { return Padding( padding: const EdgeInsets.only(bottom: 8), - child: Text('$label: ${value == null ? '—' : _formatDate(value)}'), + child: Text('$label: ${value == null ? '—' : AppTime.formatDate(value)}'), ); } diff --git a/lib/screens/tickets/tickets_list_screen.dart b/lib/screens/tickets/tickets_list_screen.dart index 7f9e788b..47431c77 100644 --- a/lib/screens/tickets/tickets_list_screen.dart +++ b/lib/screens/tickets/tickets_list_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tasq/utils/app_time.dart'; import 'package:go_router/go_router.dart'; import '../../models/office.dart'; @@ -10,7 +11,6 @@ import '../../providers/notifications_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../providers/typing_provider.dart'; -import '../../utils/app_time.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; @@ -148,7 +148,7 @@ class _TicketsListScreenState extends ConsumerState { label: Text( _selectedDateRange == null ? 'Date range' - : _formatDateRange(_selectedDateRange!), + : AppTime.formatDateRange(_selectedDateRange!), ), ), if (_hasTicketFilters) @@ -500,17 +500,6 @@ Map _statusCounts(List tickets) { return counts; } -String _formatDateRange(DateTimeRange range) { - return '${_formatDate(range.start)} - ${_formatDate(range.end)}'; -} - -String _formatDate(DateTime value) { - final year = value.year.toString().padLeft(4, '0'); - final month = value.month.toString().padLeft(2, '0'); - final day = value.day.toString().padLeft(2, '0'); - return '$year-$month-$day'; -} - class _StatusSummaryRow extends StatelessWidget { const _StatusSummaryRow({required this.counts}); diff --git a/lib/utils/app_time.dart b/lib/utils/app_time.dart index 820eca3a..c84ca2fb 100644 --- a/lib/utils/app_time.dart +++ b/lib/utils/app_time.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; @@ -27,4 +28,47 @@ class AppTime { static DateTime parse(String value) { return toAppTime(DateTime.parse(value)); } + + /// Converts a [DateTime] into a human-readable short date string. + /// + /// Example: **Jan 05, 2025**. This matches the format previously used by + /// `_formatDate` helpers across multiple screens. + static String formatDate(DateTime value) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + final month = months[value.month - 1]; + final day = value.day.toString().padLeft(2, '0'); + return '$month $day, ${value.year}'; + } + + /// Formats a [DateTimeRange] as ``start - end`` using [formatDate]. + static String formatDateRange(DateTimeRange range) { + return '${formatDate(range.start)} - ${formatDate(range.end)}'; + } + + /// Renders a [DateTime] in 12‑hour clock notation with AM/PM suffix. + /// + /// Example: **08:30 PM**. Used primarily in workforce-related screens. + static String formatTime(DateTime value) { + final rawHour = value.hour; + final hour = (rawHour % 12 == 0 ? 12 : rawHour % 12).toString().padLeft( + 2, + '0', + ); + final minute = value.minute.toString().padLeft(2, '0'); + final suffix = rawHour >= 12 ? 'PM' : 'AM'; + return '$hour:$minute $suffix'; + } } diff --git a/test/app_time_test.dart b/test/app_time_test.dart new file mode 100644 index 00000000..f5a9b4bf --- /dev/null +++ b/test/app_time_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tasq/utils/app_time.dart'; + +void main() { + setUp(() { + // ensure timezone is initialized (no-op if already done) + AppTime.initialize(); + }); + + test('formatDate produces correct string', () { + final date = DateTime(2025, 1, 5); + expect(AppTime.formatDate(date), 'Jan 05, 2025'); + + final date2 = DateTime(2021, 12, 31); + expect(AppTime.formatDate(date2), 'Dec 31, 2021'); + }); + + test('formatDateRange composes two dates', () { + final start = DateTime(2023, 3, 1); + final end = DateTime(2023, 3, 15); + expect( + AppTime.formatDateRange(DateTimeRange(start: start, end: end)), + 'Mar 01, 2023 - Mar 15, 2023', + ); + + // identical start/end + expect( + AppTime.formatDateRange(DateTimeRange(start: start, end: start)), + 'Mar 01, 2023 - Mar 01, 2023', + ); + }); + + test('formatTime outputs 12-hour clock with suffix', () { + expect(AppTime.formatTime(DateTime(2020, 1, 1, 0, 0)), '12:00 AM'); + expect(AppTime.formatTime(DateTime(2020, 1, 1, 9, 5)), '09:05 AM'); + expect(AppTime.formatTime(DateTime(2020, 1, 1, 12, 0)), '12:00 PM'); + expect(AppTime.formatTime(DateTime(2020, 1, 1, 23, 59)), '11:59 PM'); + }); +}