From 825cffbc33667915c2a96edd46ffa5e13b983bb7 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 12:07:36 -0400 Subject: [PATCH 1/3] feat: Update typography styles and improve layout in coverage components --- .../design_system/lib/src/ui_typography.dart | 2 +- .../src/presentation/pages/coverage_page.dart | 28 +++++++----- .../widgets/coverage_quick_stats.dart | 44 ++++++++----------- .../widgets/late_workers_alert.dart | 19 ++++---- 4 files changed, 44 insertions(+), 49 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/ui_typography.dart b/apps/mobile/packages/design_system/lib/src/ui_typography.dart index 42567ce4..2293ecd8 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -322,7 +322,7 @@ class UiTypography { /// Body 1 Medium - Font: Instrument Sans, Size: 16, Height: 1.5 (#121826) static final TextStyle body1m = _primaryBase.copyWith( - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, fontSize: 16, height: 1.5, letterSpacing: -0.025, diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index 7d3bf602..592a8c40 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -59,7 +59,8 @@ class _CoveragePageState extends State { child: Scaffold( body: BlocConsumer( listener: (BuildContext context, CoverageState state) { - if (state.status == CoverageStatus.failure && state.errorMessage != null) { + if (state.status == CoverageStatus.failure && + state.errorMessage != null) { UiSnackbar.show( context, message: translateErrorKey(state.errorMessage!), @@ -251,8 +252,8 @@ class _CoveragePageState extends State { UiButton.secondary( text: 'Retry', onPressed: () => BlocProvider.of(context).add( - const CoverageRefreshRequested(), - ), + const CoverageRefreshRequested(), + ), ), ], ), @@ -265,22 +266,25 @@ class _CoveragePageState extends State { padding: const EdgeInsets.all(UiConstants.space5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space6, children: [ - if (state.stats != null) ...[ - CoverageQuickStats(stats: state.stats!), - const SizedBox(height: UiConstants.space5), - ], - if (state.stats != null && state.stats!.late > 0) ...[ - LateWorkersAlert(lateCount: state.stats!.late), - const SizedBox(height: UiConstants.space5), - ], + Column( + spacing: UiConstants.space2, + children: [ + if (state.stats != null && state.stats!.late > 0) ...[ + LateWorkersAlert(lateCount: state.stats!.late), + ], + if (state.stats != null) ...[ + CoverageQuickStats(stats: state.stats!), + ], + ], + ), Text( 'Shifts (${state.shifts.length})', style: UiTypography.title2b.copyWith( color: UiColors.textPrimary, ), ), - const SizedBox(height: UiConstants.space3), CoverageShiftList(shifts: state.shifts), const SizedBox( height: UiConstants.space24, diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart index e2b90af2..25f98b0f 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart @@ -18,6 +18,7 @@ class CoverageQuickStats extends StatelessWidget { @override Widget build(BuildContext context) { return Row( + spacing: UiConstants.space2, children: [ Expanded( child: _StatCard( @@ -27,7 +28,6 @@ class CoverageQuickStats extends StatelessWidget { color: UiColors.iconSuccess, ), ), - const SizedBox(width: UiConstants.space3), Expanded( child: _StatCard( icon: UiIcons.clock, @@ -36,15 +36,6 @@ class CoverageQuickStats extends StatelessWidget { color: UiColors.textWarning, ), ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: _StatCard( - icon: UiIcons.warning, - label: 'Late', - value: stats.late.toString(), - color: UiColors.destructive, - ), - ), ], ); } @@ -84,27 +75,30 @@ class _StatCard extends StatelessWidget { width: 0.75, ), ), - child: Column( + child: Row( + spacing: UiConstants.space2, children: [ Icon( icon, color: color, size: UiConstants.space6, ), - const SizedBox(height: UiConstants.space2), - Text( - value, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - const SizedBox(height: UiConstants.space1), - Text( - label, - style: UiTypography.body3r.copyWith( - color: UiColors.mutedForeground, - ), - textAlign: TextAlign.center, + Row( + spacing: UiConstants.space1, + children: [ + Text( + value, + style: UiTypography.title1b.copyWith( + color: color, + ), + ), + Text( + label, + style: UiTypography.body3r.copyWith( + color: color, + ), + ), + ], ), ], ), diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart index c501796a..8090e0a0 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart @@ -22,32 +22,29 @@ class LateWorkersAlert extends StatelessWidget { color: UiColors.destructive.withValues(alpha: 0.1), borderRadius: UiConstants.radiusLg, border: Border.all( - color: UiColors.destructive.withValues(alpha: 0.3), + color: UiColors.destructive, + width: 0.5, ), ), child: Row( + spacing: UiConstants.space4, children: [ const Icon( UiIcons.warning, color: UiColors.destructive, - size: UiConstants.space5, ), - const SizedBox(width: UiConstants.space3), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Late Workers Alert', - style: UiTypography.body1b.copyWith( - color: UiColors.destructive, - ), - ), - const SizedBox(height: UiConstants.space1), Text( '$lateCount ${lateCount == 1 ? 'worker is' : 'workers are'} running late', + style: UiTypography.body1b.textError, + ), + Text( + 'Auto-backup system system is searching for replacements.', style: UiTypography.body3r.copyWith( - color: UiColors.destructiveForeground, + color: UiColors.textError.withValues(alpha: 0.7), ), ), ], From a22a092b56882c75c08b40fbaa0c5fd5b7d4eafc Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 12:08:51 -0400 Subject: [PATCH 2/3] fix: Adjust border width and improve layout of stat card in coverage quick stats --- .../src/presentation/pages/coverage_page.dart | 2 +- .../widgets/coverage_quick_stats.dart | 30 ++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index 592a8c40..6e31e0dc 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -270,7 +270,7 @@ class _CoveragePageState extends State { children: [ Column( spacing: UiConstants.space2, - children: [ + children: [ if (state.stats != null && state.stats!.late > 0) ...[ LateWorkersAlert(lateCount: state.stats!.late), ], diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart index 25f98b0f..0d0e948c 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart @@ -72,33 +72,29 @@ class _StatCard extends StatelessWidget { borderRadius: UiConstants.radiusLg, border: Border.all( color: color, - width: 0.75, + width: 0.5, ), ), child: Row( spacing: UiConstants.space2, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( icon, color: color, size: UiConstants.space6, ), - Row( - spacing: UiConstants.space1, - children: [ - Text( - value, - style: UiTypography.title1b.copyWith( - color: color, - ), - ), - Text( - label, - style: UiTypography.body3r.copyWith( - color: color, - ), - ), - ], + Text( + value, + style: UiTypography.title1b.copyWith( + color: color, + ), + ), + Text( + label, + style: UiTypography.body3r.copyWith( + color: color, + ), ), ], ), From 80b83a16f3319bf33b00879e8e82d157fc95015d Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 12:27:27 -0400 Subject: [PATCH 3/3] Refactor coverage widgets and improve localization - Replaced custom navigation buttons with a new CalendarNavButton widget in coverage_calendar_selector.dart. - Removed the CoverageHeader widget as it is no longer needed. - Updated CoverageQuickStats to use CoverageStatCard for displaying statistics. - Refactored CoverageShiftList to utilize ShiftHeader and WorkerRow for better structure. - Added LateWorkersAlert with improved localization for late worker notifications. - Introduced CoverageBadge and CoverageStatCard for better encapsulation of UI components. - Created CoverageStatsHeader for displaying coverage metrics in a consistent format. - Implemented ShiftHeader to manage shift-related information display. - Developed WorkerRow to represent individual worker statuses with proper localization. --- .../lib/src/l10n/en.i18n.json | 35 ++ .../lib/src/l10n/es.i18n.json | 35 ++ .../src/presentation/pages/coverage_page.dart | 69 +-- .../widgets/calendar_nav_button.dart | 41 ++ .../presentation/widgets/coverage_badge.dart | 59 +++ .../widgets/coverage_calendar_selector.dart | 53 +-- .../presentation/widgets/coverage_header.dart | 177 -------- .../widgets/coverage_quick_stats.dart | 74 +-- .../widgets/coverage_shift_list.dart | 424 +----------------- .../widgets/coverage_stat_card.dart | 64 +++ .../widgets/coverage_stats_header.dart | 73 +++ .../widgets/late_workers_alert.dart | 8 +- .../presentation/widgets/shift_header.dart | 125 ++++++ .../src/presentation/widgets/worker_row.dart | 231 ++++++++++ 14 files changed, 707 insertions(+), 761 deletions(-) create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/calendar_nav_button.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_badge.dart delete mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stat_card.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stats_header.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/shift_header.dart create mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/worker_row.dart diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index bfbf59ef..9be43245 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -1692,9 +1692,44 @@ "todays_cost": "Today's Cost", "no_shifts_day": "No shifts scheduled for this day", "no_workers_assigned": "No workers assigned yet", + "status_checked_in_at": "Checked In at $time", + "status_on_site": "On Site", + "status_en_route": "En Route", + "status_en_route_expected": "En Route - Expected $time", + "status_confirmed": "Confirmed", + "status_running_late": "Running Late", + "status_late": "Late", + "status_checked_out": "Checked Out", + "status_done": "Done", + "status_no_show": "No Show", + "status_completed": "Completed", "worker_row": { "verify": "Verify", "verified_message": "Worker attire verified for $name" + }, + "page": { + "daily_coverage": "Daily Coverage", + "coverage_status": "Coverage Status", + "workers": "Workers", + "error_occurred": "An error occurred", + "retry": "Retry", + "shifts": "Shifts" + }, + "calendar": { + "prev_week": "\u2190 Prev Week", + "today": "Today", + "next_week": "Next Week \u2192" + }, + "stats": { + "checked_in": "Checked In", + "en_route": "En Route" + }, + "alert": { + "workers_running_late(count)": { + "one": "$count worker is running late", + "other": "$count workers are running late" + }, + "auto_backup_searching": "Auto-backup system is searching for replacements." } }, "client_reports_common": { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 1d8e5bc7..9f99b499 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -1692,9 +1692,44 @@ "todays_cost": "Costo de Hoy", "no_shifts_day": "No hay turnos programados para este día", "no_workers_assigned": "Aún no hay trabajadores asignados", + "status_checked_in_at": "Registrado a las $time", + "status_on_site": "En Sitio", + "status_en_route": "En Camino", + "status_en_route_expected": "En Camino - Esperado $time", + "status_confirmed": "Confirmado", + "status_running_late": "Llegando Tarde", + "status_late": "Tarde", + "status_checked_out": "Salida Registrada", + "status_done": "Hecho", + "status_no_show": "No Se Presentó", + "status_completed": "Completado", "worker_row": { "verify": "Verificar", "verified_message": "Vestimenta del trabajador verificada para $name" + }, + "page": { + "daily_coverage": "Cobertura Diaria", + "coverage_status": "Estado de Cobertura", + "workers": "Trabajadores", + "error_occurred": "Ocurri\u00f3 un error", + "retry": "Reintentar", + "shifts": "Turnos" + }, + "calendar": { + "prev_week": "\u2190 Semana Anterior", + "today": "Hoy", + "next_week": "Semana Siguiente \u2192" + }, + "stats": { + "checked_in": "Registrado", + "en_route": "En Camino" + }, + "alert": { + "workers_running_late(count)": { + "one": "$count trabajador est\u00e1 llegando tarde", + "other": "$count trabajadores est\u00e1n llegando tarde" + }, + "auto_backup_searching": "El sistema de respaldo autom\u00e1tico est\u00e1 buscando reemplazos." } }, "client_reports_common": { diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index 6e31e0dc..509a4e6d 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -11,6 +11,7 @@ import '../blocs/coverage_state.dart'; import '../widgets/coverage_calendar_selector.dart'; import '../widgets/coverage_quick_stats.dart'; import '../widgets/coverage_shift_list.dart'; +import '../widgets/coverage_stats_header.dart'; import '../widgets/late_workers_alert.dart'; /// Page for displaying daily coverage information. @@ -83,7 +84,7 @@ class _CoveragePageState extends State { child: Text( _isScrolled ? DateFormat('MMMM d').format(selectedDate) - : 'Daily Coverage', + : context.t.client_coverage.page.daily_coverage, key: ValueKey(_isScrolled), style: UiTypography.title2m.copyWith( color: UiColors.primaryForeground, @@ -100,7 +101,7 @@ class _CoveragePageState extends State { icon: Container( padding: const EdgeInsets.all(UiConstants.space2), decoration: BoxDecoration( - color: UiColors.primaryForeground.withOpacity(0.2), + color: UiColors.primaryForeground.withValues(alpha: 0.2), borderRadius: UiConstants.radiusMd, ), child: const Icon( @@ -143,57 +144,13 @@ class _CoveragePageState extends State { }, ), const SizedBox(height: UiConstants.space4), - // Coverage Stats Container - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: - UiColors.primaryForeground.withOpacity(0.1), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Coverage Status', - style: UiTypography.body2r.copyWith( - color: UiColors.primaryForeground - .withOpacity(0.7), - ), - ), - Text( - '${state.stats?.coveragePercent ?? 0}%', - style: UiTypography.display1b.copyWith( - color: UiColors.primaryForeground, - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - 'Workers', - style: UiTypography.body2r.copyWith( - color: UiColors.primaryForeground - .withOpacity(0.7), - ), - ), - Text( - '${state.stats?.totalConfirmed ?? 0}/${state.stats?.totalNeeded ?? 0}', - style: UiTypography.title2m.copyWith( - color: UiColors.primaryForeground, - ), - ), - ], - ), - ], - ), + CoverageStatsHeader( + coveragePercent: + (state.stats?.coveragePercent ?? 0) + .toDouble(), + totalConfirmed: + state.stats?.totalConfirmed ?? 0, + totalNeeded: state.stats?.totalNeeded ?? 0, ), ], ), @@ -244,13 +201,13 @@ class _CoveragePageState extends State { Text( state.errorMessage != null ? translateErrorKey(state.errorMessage!) - : 'An error occurred', + : context.t.client_coverage.page.error_occurred, style: UiTypography.body1m.textError, textAlign: TextAlign.center, ), const SizedBox(height: UiConstants.space4), UiButton.secondary( - text: 'Retry', + text: context.t.client_coverage.page.retry, onPressed: () => BlocProvider.of(context).add( const CoverageRefreshRequested(), ), @@ -280,7 +237,7 @@ class _CoveragePageState extends State { ], ), Text( - 'Shifts (${state.shifts.length})', + '${context.t.client_coverage.page.shifts} (${state.shifts.length})', style: UiTypography.title2b.copyWith( color: UiColors.textPrimary, ), diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/calendar_nav_button.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/calendar_nav_button.dart new file mode 100644 index 00000000..c2fa4a94 --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/calendar_nav_button.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Navigation button used in the calendar selector for week navigation. +class CalendarNavButton extends StatelessWidget { + /// Creates a [CalendarNavButton]. + const CalendarNavButton({ + required this.text, + required this.onTap, + super.key, + }); + + /// The button label text. + final String text; + + /// Callback when the button is tapped. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.primaryForeground.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusMd, + ), + child: Text( + text, + style: UiTypography.body3r.copyWith( + color: UiColors.primaryForeground, + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_badge.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_badge.dart new file mode 100644 index 00000000..12dbcdcd --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_badge.dart @@ -0,0 +1,59 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Badge showing worker count ratio with color-coded coverage status. +/// +/// Green for 100%+, yellow for 80%+, red below 80%. +class CoverageBadge extends StatelessWidget { + /// Creates a [CoverageBadge]. + const CoverageBadge({ + required this.current, + required this.total, + required this.coveragePercent, + super.key, + }); + + /// Current number of assigned workers. + final int current; + + /// Total workers needed. + final int total; + + /// Coverage percentage used to determine badge color. + final int coveragePercent; + + @override + Widget build(BuildContext context) { + Color bg; + Color text; + + if (coveragePercent >= 100) { + bg = UiColors.textSuccess.withAlpha(40); + text = UiColors.textSuccess; + } else if (coveragePercent >= 80) { + bg = UiColors.textWarning.withAlpha(40); + text = UiColors.textWarning; + } else { + bg = UiColors.destructive.withAlpha(40); + text = UiColors.destructive; + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2 + UiConstants.space1, + vertical: UiConstants.space1 / 2, + ), + decoration: BoxDecoration( + color: bg, + border: Border.all(color: text, width: 0.75), + borderRadius: UiConstants.radiusMd, + ), + child: Text( + '$current/$total', + style: UiTypography.body3b.copyWith( + color: text, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_calendar_selector.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_calendar_selector.dart index a5e7787e..f0518e1e 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_calendar_selector.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_calendar_selector.dart @@ -1,7 +1,10 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'calendar_nav_button.dart'; + /// Calendar selector widget for choosing dates. /// /// Displays a week view with navigation buttons and date selection. @@ -74,16 +77,16 @@ class _CoverageCalendarSelectorState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _NavButton( - text: '← Prev Week', + CalendarNavButton( + text: context.t.client_coverage.calendar.prev_week, onTap: _navigatePrevWeek, ), - _NavButton( - text: 'Today', + CalendarNavButton( + text: context.t.client_coverage.calendar.today, onTap: _navigateToday, ), - _NavButton( - text: 'Next Week →', + CalendarNavButton( + text: context.t.client_coverage.calendar.next_week, onTap: _navigateNextWeek, ), ], @@ -145,41 +148,3 @@ class _CoverageCalendarSelectorState extends State { ); } } - -/// Navigation button for calendar navigation. -class _NavButton extends StatelessWidget { - /// Creates a [_NavButton]. - const _NavButton({ - required this.text, - required this.onTap, - }); - - /// The button text. - final String text; - - /// Callback when tapped. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.primaryForeground.withOpacity(0.2), - borderRadius: UiConstants.radiusMd, - ), - child: Text( - text, - style: UiTypography.body3r.copyWith( - color: UiColors.primaryForeground, - ), - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart deleted file mode 100644 index 7b23f2a9..00000000 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; -import 'coverage_calendar_selector.dart'; - -/// Header widget for the coverage page. -/// -/// Displays: -/// - Back button and title -/// - Refresh button -/// - Calendar date selector -/// - Coverage summary statistics -class CoverageHeader extends StatelessWidget { - /// Creates a [CoverageHeader]. - const CoverageHeader({ - required this.selectedDate, - required this.coveragePercent, - required this.totalConfirmed, - required this.totalNeeded, - required this.onDateSelected, - required this.onRefresh, - super.key, - }); - - /// The currently selected date. - final DateTime selectedDate; - - /// The coverage percentage. - final int coveragePercent; - - /// The total number of confirmed workers. - final int totalConfirmed; - - /// The total number of workers needed. - final int totalNeeded; - - /// Callback when a date is selected. - final ValueChanged onDateSelected; - - /// Callback when refresh is requested. - final VoidCallback onRefresh; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.only( - top: UiConstants.space14, - left: UiConstants.space5, - right: UiConstants.space5, - bottom: UiConstants.space6, - ), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.primary, - UiColors.primary, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - GestureDetector( - onTap: () => Modular.to.toClientHome(), - child: Container( - width: UiConstants.space10, - height: UiConstants.space10, - decoration: BoxDecoration( - color: UiColors.primaryForeground.withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: const Icon( - UiIcons.arrowLeft, - color: UiColors.primaryForeground, - size: UiConstants.space5, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Text( - 'Daily Coverage', - style: UiTypography.title1m.copyWith( - color: UiColors.primaryForeground, - ), - ), - ], - ), - Container( - width: UiConstants.space8, - height: UiConstants.space8, - decoration: BoxDecoration( - color: UiColors.transparent, - borderRadius: UiConstants.radiusMd, - ), - child: IconButton( - onPressed: onRefresh, - icon: const Icon( - UiIcons.rotateCcw, - color: UiColors.primaryForeground, - size: UiConstants.space4, - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - style: IconButton.styleFrom( - hoverColor: UiColors.primaryForeground.withValues(alpha: 0.2), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusMd, - ), - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - CoverageCalendarSelector( - selectedDate: selectedDate, - onDateSelected: onDateSelected, - ), - const SizedBox(height: UiConstants.space4), - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.primaryForeground.withValues(alpha: 0.1), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Coverage Status', - style: UiTypography.body2r.copyWith( - color: UiColors.primaryForeground.withValues(alpha: 0.7), - ), - ), - Text( - '$coveragePercent%', - style: UiTypography.display1b.copyWith( - color: UiColors.primaryForeground, - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - 'Workers', - style: UiTypography.body2r.copyWith( - color: UiColors.primaryForeground.withValues(alpha: 0.7), - ), - ), - Text( - '$totalConfirmed/$totalNeeded', - style: UiTypography.title2m.copyWith( - color: UiColors.primaryForeground, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart index 0d0e948c..7ae538b9 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart @@ -1,10 +1,13 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'coverage_stat_card.dart'; + /// Quick statistics cards showing coverage metrics. /// -/// Displays checked-in, en-route, and late worker counts. +/// Displays checked-in and en-route worker counts. class CoverageQuickStats extends StatelessWidget { /// Creates a [CoverageQuickStats]. const CoverageQuickStats({ @@ -21,17 +24,17 @@ class CoverageQuickStats extends StatelessWidget { spacing: UiConstants.space2, children: [ Expanded( - child: _StatCard( + child: CoverageStatCard( icon: UiIcons.success, - label: 'Checked In', + label: context.t.client_coverage.stats.checked_in, value: stats.checkedIn.toString(), color: UiColors.iconSuccess, ), ), Expanded( - child: _StatCard( + child: CoverageStatCard( icon: UiIcons.clock, - label: 'En Route', + label: context.t.client_coverage.stats.en_route, value: stats.enRoute.toString(), color: UiColors.textWarning, ), @@ -40,64 +43,3 @@ class CoverageQuickStats extends StatelessWidget { ); } } - -/// Individual stat card widget. -class _StatCard extends StatelessWidget { - /// Creates a [_StatCard]. - const _StatCard({ - required this.icon, - required this.label, - required this.value, - required this.color, - }); - - /// The icon to display. - final IconData icon; - - /// The label text. - final String label; - - /// The value to display. - final String value; - - /// The accent color for the card. - final Color color; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: color.withAlpha(10), - borderRadius: UiConstants.radiusLg, - border: Border.all( - color: color, - width: 0.5, - ), - ), - child: Row( - spacing: UiConstants.space2, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - icon, - color: color, - size: UiConstants.space6, - ), - Text( - value, - style: UiTypography.title1b.copyWith( - color: color, - ), - ), - Text( - label, - style: UiTypography.body3r.copyWith( - color: color, - ), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart index c1bedeed..e70aa5b2 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'shift_header.dart'; +import 'worker_row.dart'; + /// List of shifts with their workers. /// /// Displays all shifts for the selected date, or an empty state if none exist. @@ -33,6 +36,8 @@ class CoverageShiftList extends StatelessWidget { @override Widget build(BuildContext context) { + final TranslationsClientCoverageEn l10n = context.t.client_coverage; + if (shifts.isEmpty) { return Container( padding: const EdgeInsets.all(UiConstants.space8), @@ -51,7 +56,7 @@ class CoverageShiftList extends StatelessWidget { color: UiColors.textSecondary, ), Text( - 'No shifts scheduled for this day', + l10n.no_shifts_day, style: UiTypography.body2r.textSecondary, ), ], @@ -71,7 +76,7 @@ class CoverageShiftList extends StatelessWidget { clipBehavior: Clip.antiAlias, child: Column( children: [ - _ShiftHeader( + ShiftHeader( title: shift.title, location: shift.location, startTime: _formatTime(shift.startTime), @@ -91,7 +96,7 @@ class CoverageShiftList extends StatelessWidget { padding: EdgeInsets.only( bottom: isLast ? 0 : UiConstants.space2, ), - child: _WorkerRow( + child: WorkerRow( worker: worker, shiftStartTime: _formatTime(shift.startTime), formatTime: _formatTime, @@ -104,7 +109,7 @@ class CoverageShiftList extends StatelessWidget { Padding( padding: const EdgeInsets.all(UiConstants.space4), child: Text( - 'No workers assigned yet', + l10n.no_workers_assigned, style: UiTypography.body3r.copyWith( color: UiColors.mutedForeground, ), @@ -117,414 +122,3 @@ class CoverageShiftList extends StatelessWidget { ); } } - -/// Header for a shift card. -class _ShiftHeader extends StatelessWidget { - /// Creates a [_ShiftHeader]. - const _ShiftHeader({ - required this.title, - required this.location, - required this.startTime, - required this.current, - required this.total, - required this.coveragePercent, - required this.shiftId, - }); - - /// The shift title. - final String title; - - /// The shift location. - final String location; - - /// The shift start time. - final String startTime; - - /// Current number of workers. - final int current; - - /// Total workers needed. - final int total; - - /// Coverage percentage. - final int coveragePercent; - - /// The shift ID. - final String shiftId; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: const BoxDecoration( - color: UiColors.muted, - border: Border( - bottom: BorderSide( - color: UiColors.border, - ), - ), - ), - child: Row( - spacing: UiConstants.space4, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: UiConstants.space2, - children: [ - Row( - spacing: UiConstants.space2, - children: [ - Container( - width: UiConstants.space2, - height: UiConstants.space2, - decoration: const BoxDecoration( - color: UiColors.primary, - shape: BoxShape.circle, - ), - ), - Text( - title, - style: UiTypography.body1b.textPrimary, - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: UiConstants.space1, - children: [ - const Icon( - UiIcons.mapPin, - size: UiConstants.space3, - color: UiColors.iconSecondary, - ), - Expanded( - child: Text( - location, - style: UiTypography.body3r.textSecondary, - overflow: TextOverflow.ellipsis, - )), - ], - ), - Row( - spacing: UiConstants.space1, - children: [ - const Icon( - UiIcons.clock, - size: UiConstants.space3, - color: UiColors.iconSecondary, - ), - Text( - startTime, - style: UiTypography.body3r.textSecondary, - ), - ], - ), - ], - ), - ], - ), - ), - _CoverageBadge( - current: current, - total: total, - coveragePercent: coveragePercent, - ), - ], - ), - ); - } -} - -/// Coverage badge showing worker count and status. -class _CoverageBadge extends StatelessWidget { - /// Creates a [_CoverageBadge]. - const _CoverageBadge({ - required this.current, - required this.total, - required this.coveragePercent, - }); - - /// Current number of workers. - final int current; - - /// Total workers needed. - final int total; - - /// Coverage percentage. - final int coveragePercent; - - @override - Widget build(BuildContext context) { - Color bg; - Color text; - - if (coveragePercent >= 100) { - bg = UiColors.textSuccess.withAlpha(40); - text = UiColors.textSuccess; - } else if (coveragePercent >= 80) { - bg = UiColors.textWarning.withAlpha(40); - text = UiColors.textWarning; - } else { - bg = UiColors.destructive.withAlpha(40); - text = UiColors.destructive; - } - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2 + UiConstants.space1, - vertical: UiConstants.space1 / 2, - ), - decoration: BoxDecoration( - color: bg, - border: Border.all(color: text, width: 0.75), - borderRadius: UiConstants.radiusMd, - ), - child: Text( - '$current/$total', - style: UiTypography.body3b.copyWith( - color: text, - ), - ), - ); - } -} - -/// Row displaying a single worker's status. -class _WorkerRow extends StatelessWidget { - /// Creates a [_WorkerRow]. - const _WorkerRow({ - required this.worker, - required this.shiftStartTime, - required this.formatTime, - }); - - /// The worker to display. - final CoverageWorker worker; - - /// The shift start time. - final String shiftStartTime; - - /// Function to format time strings. - final String Function(String?) formatTime; - - @override - Widget build(BuildContext context) { - Color bg; - Color border; - Color textBg; - Color textColor; - IconData icon; - String statusText; - Color badgeBg; - Color badgeText; - Color badgeBorder; - String badgeLabel; - - switch (worker.status) { - case CoverageWorkerStatus.checkedIn: - bg = UiColors.textSuccess.withAlpha(26); - border = UiColors.textSuccess; - textBg = UiColors.textSuccess.withAlpha(51); - textColor = UiColors.textSuccess; - icon = UiIcons.success; - statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}'; - badgeBg = UiColors.textSuccess.withAlpha(40); - badgeText = UiColors.textSuccess; - badgeBorder = badgeText; - badgeLabel = 'On Site'; - case CoverageWorkerStatus.confirmed: - if (worker.checkInTime == null) { - bg = UiColors.textWarning.withAlpha(26); - border = UiColors.textWarning; - textBg = UiColors.textWarning.withAlpha(51); - textColor = UiColors.textWarning; - icon = UiIcons.clock; - statusText = 'En Route - Expected $shiftStartTime'; - badgeBg = UiColors.textWarning.withAlpha(40); - badgeText = UiColors.textWarning; - badgeBorder = badgeText; - badgeLabel = 'En Route'; - } else { - bg = UiColors.muted.withAlpha(26); - border = UiColors.border; - textBg = UiColors.muted.withAlpha(51); - textColor = UiColors.textSecondary; - icon = UiIcons.success; - statusText = 'Confirmed'; - badgeBg = UiColors.textSecondary.withAlpha(40); - badgeText = UiColors.textSecondary; - badgeBorder = badgeText; - badgeLabel = 'Confirmed'; - } - case CoverageWorkerStatus.late: - bg = UiColors.destructive.withAlpha(26); - border = UiColors.destructive; - textBg = UiColors.destructive.withAlpha(51); - textColor = UiColors.destructive; - icon = UiIcons.warning; - statusText = '⚠ Running Late'; - badgeBg = UiColors.destructive.withAlpha(40); - badgeText = UiColors.destructive; - badgeBorder = badgeText; - badgeLabel = 'Late'; - case CoverageWorkerStatus.checkedOut: - bg = UiColors.muted.withAlpha(26); - border = UiColors.border; - textBg = UiColors.muted.withAlpha(51); - textColor = UiColors.textSecondary; - icon = UiIcons.success; - statusText = 'Checked Out'; - badgeBg = UiColors.textSecondary.withAlpha(40); - badgeText = UiColors.textSecondary; - badgeBorder = badgeText; - badgeLabel = 'Done'; - case CoverageWorkerStatus.noShow: - bg = UiColors.destructive.withAlpha(26); - border = UiColors.destructive; - textBg = UiColors.destructive.withAlpha(51); - textColor = UiColors.destructive; - icon = UiIcons.warning; - statusText = 'No Show'; - badgeBg = UiColors.destructive.withAlpha(40); - badgeText = UiColors.destructive; - badgeBorder = badgeText; - badgeLabel = 'No Show'; - case CoverageWorkerStatus.completed: - bg = UiColors.iconSuccess.withAlpha(26); - border = UiColors.iconSuccess; - textBg = UiColors.iconSuccess.withAlpha(51); - textColor = UiColors.textSuccess; - icon = UiIcons.success; - statusText = 'Completed'; - badgeBg = UiColors.textSuccess.withAlpha(40); - badgeText = UiColors.textSuccess; - badgeBorder = badgeText; - badgeLabel = 'Completed'; - case CoverageWorkerStatus.pending: - case CoverageWorkerStatus.accepted: - case CoverageWorkerStatus.rejected: - bg = UiColors.muted.withAlpha(26); - border = UiColors.border; - textBg = UiColors.muted.withAlpha(51); - textColor = UiColors.textSecondary; - icon = UiIcons.clock; - statusText = worker.status.name.toUpperCase(); - badgeBg = UiColors.textSecondary.withAlpha(40); - badgeText = UiColors.textSecondary; - badgeBorder = badgeText; - badgeLabel = worker.status.name[0].toUpperCase() + - worker.status.name.substring(1); - } - - return Container( - padding: const EdgeInsets.all(UiConstants.space2), - decoration: BoxDecoration( - color: bg, - borderRadius: UiConstants.radiusMd, - ), - child: Row( - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - Container( - width: UiConstants.space10, - height: UiConstants.space10, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: border, width: 2), - ), - child: CircleAvatar( - backgroundColor: textBg, - child: Text( - worker.name.isNotEmpty ? worker.name[0] : 'W', - style: UiTypography.body1b.copyWith( - color: textColor, - ), - ), - ), - ), - Positioned( - bottom: -2, - right: -2, - child: Container( - width: UiConstants.space4, - height: UiConstants.space4, - decoration: BoxDecoration( - color: border, - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: UiConstants.space2 + UiConstants.space1, - color: UiColors.primaryForeground, - ), - ), - ), - ], - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - worker.name, - style: UiTypography.body2b.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - statusText, - style: UiTypography.body3m.copyWith( - color: textColor, - ), - ), - ], - ), - ), - Column( - spacing: UiConstants.space2, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1 / 2, - ), - decoration: BoxDecoration( - color: badgeBg, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: badgeBorder, width: 0.5), - ), - child: Text( - badgeLabel, - style: UiTypography.footnote2b.copyWith( - color: badgeText, - ), - ), - ), - // if (worker.status == CoverageWorkerStatus.checkedIn) - // UiButton.primary( - // text: context.t.client_coverage.worker_row.verify, - // size: UiButtonSize.small, - // onPressed: () { - // UiSnackbar.show( - // context, - // message: - // context.t.client_coverage.worker_row.verified_message( - // name: worker.name, - // ), - // type: UiSnackbarType.success, - // ); - // }, - // ), - ], - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stat_card.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stat_card.dart new file mode 100644 index 00000000..b82585ce --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stat_card.dart @@ -0,0 +1,64 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Stat card displaying an icon, value, and label with an accent color. +class CoverageStatCard extends StatelessWidget { + /// Creates a [CoverageStatCard]. + const CoverageStatCard({ + required this.icon, + required this.label, + required this.value, + required this.color, + super.key, + }); + + /// The icon to display. + final IconData icon; + + /// The label text describing the stat. + final String label; + + /// The numeric value to display. + final String value; + + /// The accent color for the card border, icon, and text. + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: color.withAlpha(10), + borderRadius: UiConstants.radiusLg, + border: Border.all( + color: color, + width: 0.5, + ), + ), + child: Row( + spacing: UiConstants.space2, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + icon, + color: color, + size: UiConstants.space6, + ), + Text( + value, + style: UiTypography.title1b.copyWith( + color: color, + ), + ), + Text( + label, + style: UiTypography.body3r.copyWith( + color: color, + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stats_header.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stats_header.dart new file mode 100644 index 00000000..15b4b448 --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_stats_header.dart @@ -0,0 +1,73 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Displays coverage percentage and worker ratio in the app bar header. +class CoverageStatsHeader extends StatelessWidget { + /// Creates a [CoverageStatsHeader]. + const CoverageStatsHeader({ + required this.coveragePercent, + required this.totalConfirmed, + required this.totalNeeded, + super.key, + }); + + /// The current coverage percentage. + final double coveragePercent; + + /// The number of confirmed workers. + final int totalConfirmed; + + /// The total number of workers needed. + final int totalNeeded; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.primaryForeground.withOpacity(0.1), + borderRadius: UiConstants.radiusLg, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.t.client_coverage.page.coverage_status, + style: UiTypography.body2r.copyWith( + color: UiColors.primaryForeground.withOpacity(0.7), + ), + ), + Text( + '${coveragePercent.toStringAsFixed(0)}%', + style: UiTypography.display1b.copyWith( + color: UiColors.primaryForeground, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + context.t.client_coverage.page.workers, + style: UiTypography.body2r.copyWith( + color: UiColors.primaryForeground.withOpacity(0.7), + ), + ), + Text( + '$totalConfirmed/$totalNeeded', + style: UiTypography.title2m.copyWith( + color: UiColors.primaryForeground, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart index 8090e0a0..716512cc 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/late_workers_alert.dart @@ -1,9 +1,10 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; /// Alert widget for displaying late workers warning. /// -/// Shows a warning banner when there are late workers. +/// Shows a warning banner when workers are running late. class LateWorkersAlert extends StatelessWidget { /// Creates a [LateWorkersAlert]. const LateWorkersAlert({ @@ -38,11 +39,12 @@ class LateWorkersAlert extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$lateCount ${lateCount == 1 ? 'worker is' : 'workers are'} running late', + context.t.client_coverage.alert + .workers_running_late(n: lateCount, count: lateCount), style: UiTypography.body1b.textError, ), Text( - 'Auto-backup system system is searching for replacements.', + context.t.client_coverage.alert.auto_backup_searching, style: UiTypography.body3r.copyWith( color: UiColors.textError.withValues(alpha: 0.7), ), diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/shift_header.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/shift_header.dart new file mode 100644 index 00000000..d35c49ca --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/shift_header.dart @@ -0,0 +1,125 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import 'coverage_badge.dart'; + +/// Header section for a shift card showing title, location, time, and coverage. +class ShiftHeader extends StatelessWidget { + /// Creates a [ShiftHeader]. + const ShiftHeader({ + required this.title, + required this.location, + required this.startTime, + required this.current, + required this.total, + required this.coveragePercent, + required this.shiftId, + super.key, + }); + + /// The shift title. + final String title; + + /// The shift location. + final String location; + + /// The formatted shift start time. + final String startTime; + + /// Current number of assigned workers. + final int current; + + /// Total workers needed for the shift. + final int total; + + /// Coverage percentage (0-100+). + final int coveragePercent; + + /// The shift identifier. + final String shiftId; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: const BoxDecoration( + color: UiColors.muted, + border: Border( + bottom: BorderSide( + color: UiColors.border, + ), + ), + ), + child: Row( + spacing: UiConstants.space4, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space2, + children: [ + Row( + spacing: UiConstants.space2, + children: [ + Container( + width: UiConstants.space2, + height: UiConstants.space2, + decoration: const BoxDecoration( + color: UiColors.primary, + shape: BoxShape.circle, + ), + ), + Text( + title, + style: UiTypography.body1b.textPrimary, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: UiConstants.space1, + children: [ + const Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.iconSecondary, + ), + Expanded( + child: Text( + location, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + )), + ], + ), + Row( + spacing: UiConstants.space1, + children: [ + const Icon( + UiIcons.clock, + size: UiConstants.space3, + color: UiColors.iconSecondary, + ), + Text( + startTime, + style: UiTypography.body3r.textSecondary, + ), + ], + ), + ], + ), + ], + ), + ), + CoverageBadge( + current: current, + total: total, + coveragePercent: coveragePercent, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/worker_row.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/worker_row.dart new file mode 100644 index 00000000..25171bc8 --- /dev/null +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/worker_row.dart @@ -0,0 +1,231 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Row displaying a single worker's avatar, name, status, and badge. +class WorkerRow extends StatelessWidget { + /// Creates a [WorkerRow]. + const WorkerRow({ + required this.worker, + required this.shiftStartTime, + required this.formatTime, + super.key, + }); + + /// The worker data to display. + final CoverageWorker worker; + + /// The formatted shift start time. + final String shiftStartTime; + + /// Callback to format a raw time string into a readable format. + final String Function(String?) formatTime; + + @override + Widget build(BuildContext context) { + final TranslationsClientCoverageEn l10n = context.t.client_coverage; + + Color bg; + Color border; + Color textBg; + Color textColor; + IconData icon; + String statusText; + Color badgeBg; + Color badgeText; + Color badgeBorder; + String badgeLabel; + + switch (worker.status) { + case CoverageWorkerStatus.checkedIn: + bg = UiColors.textSuccess.withAlpha(26); + border = UiColors.textSuccess; + textBg = UiColors.textSuccess.withAlpha(51); + textColor = UiColors.textSuccess; + icon = UiIcons.success; + statusText = l10n.status_checked_in_at( + time: formatTime(worker.checkInTime), + ); + badgeBg = UiColors.textSuccess.withAlpha(40); + badgeText = UiColors.textSuccess; + badgeBorder = badgeText; + badgeLabel = l10n.status_on_site; + case CoverageWorkerStatus.confirmed: + if (worker.checkInTime == null) { + bg = UiColors.textWarning.withAlpha(26); + border = UiColors.textWarning; + textBg = UiColors.textWarning.withAlpha(51); + textColor = UiColors.textWarning; + icon = UiIcons.clock; + statusText = l10n.status_en_route_expected(time: shiftStartTime); + badgeBg = UiColors.textWarning.withAlpha(40); + badgeText = UiColors.textWarning; + badgeBorder = badgeText; + badgeLabel = l10n.status_en_route; + } else { + bg = UiColors.muted.withAlpha(26); + border = UiColors.border; + textBg = UiColors.muted.withAlpha(51); + textColor = UiColors.textSecondary; + icon = UiIcons.success; + statusText = l10n.status_confirmed; + badgeBg = UiColors.textSecondary.withAlpha(40); + badgeText = UiColors.textSecondary; + badgeBorder = badgeText; + badgeLabel = l10n.status_confirmed; + } + case CoverageWorkerStatus.late: + bg = UiColors.destructive.withAlpha(26); + border = UiColors.destructive; + textBg = UiColors.destructive.withAlpha(51); + textColor = UiColors.destructive; + icon = UiIcons.warning; + statusText = l10n.status_running_late; + badgeBg = UiColors.destructive.withAlpha(40); + badgeText = UiColors.destructive; + badgeBorder = badgeText; + badgeLabel = l10n.status_late; + case CoverageWorkerStatus.checkedOut: + bg = UiColors.muted.withAlpha(26); + border = UiColors.border; + textBg = UiColors.muted.withAlpha(51); + textColor = UiColors.textSecondary; + icon = UiIcons.success; + statusText = l10n.status_checked_out; + badgeBg = UiColors.textSecondary.withAlpha(40); + badgeText = UiColors.textSecondary; + badgeBorder = badgeText; + badgeLabel = l10n.status_done; + case CoverageWorkerStatus.noShow: + bg = UiColors.destructive.withAlpha(26); + border = UiColors.destructive; + textBg = UiColors.destructive.withAlpha(51); + textColor = UiColors.destructive; + icon = UiIcons.warning; + statusText = l10n.status_no_show; + badgeBg = UiColors.destructive.withAlpha(40); + badgeText = UiColors.destructive; + badgeBorder = badgeText; + badgeLabel = l10n.status_no_show; + case CoverageWorkerStatus.completed: + bg = UiColors.iconSuccess.withAlpha(26); + border = UiColors.iconSuccess; + textBg = UiColors.iconSuccess.withAlpha(51); + textColor = UiColors.textSuccess; + icon = UiIcons.success; + statusText = l10n.status_completed; + badgeBg = UiColors.textSuccess.withAlpha(40); + badgeText = UiColors.textSuccess; + badgeBorder = badgeText; + badgeLabel = l10n.status_completed; + case CoverageWorkerStatus.pending: + case CoverageWorkerStatus.accepted: + case CoverageWorkerStatus.rejected: + bg = UiColors.muted.withAlpha(26); + border = UiColors.border; + textBg = UiColors.muted.withAlpha(51); + textColor = UiColors.textSecondary; + icon = UiIcons.clock; + statusText = worker.status.name.toUpperCase(); + badgeBg = UiColors.textSecondary.withAlpha(40); + badgeText = UiColors.textSecondary; + badgeBorder = badgeText; + badgeLabel = worker.status.name[0].toUpperCase() + + worker.status.name.substring(1); + } + + return Container( + padding: const EdgeInsets.all(UiConstants.space2), + decoration: BoxDecoration( + color: bg, + borderRadius: UiConstants.radiusMd, + ), + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: UiConstants.space10, + height: UiConstants.space10, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: border, width: 2), + ), + child: CircleAvatar( + backgroundColor: textBg, + child: Text( + worker.name.isNotEmpty ? worker.name[0] : 'W', + style: UiTypography.body1b.copyWith( + color: textColor, + ), + ), + ), + ), + Positioned( + bottom: -2, + right: -2, + child: Container( + width: UiConstants.space4, + height: UiConstants.space4, + decoration: BoxDecoration( + color: border, + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: UiConstants.space2 + UiConstants.space1, + color: UiColors.primaryForeground, + ), + ), + ), + ], + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + worker.name, + style: UiTypography.body2b.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + statusText, + style: UiTypography.body3m.copyWith( + color: textColor, + ), + ), + ], + ), + ), + Column( + spacing: UiConstants.space2, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1 / 2, + ), + decoration: BoxDecoration( + color: badgeBg, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: badgeBorder, width: 0.5), + ), + child: Text( + badgeLabel, + style: UiTypography.footnote2b.copyWith( + color: badgeText, + ), + ), + ), + ], + ), + ], + ), + ); + } +}