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.
This commit is contained in:
@@ -1692,9 +1692,44 @@
|
|||||||
"todays_cost": "Today's Cost",
|
"todays_cost": "Today's Cost",
|
||||||
"no_shifts_day": "No shifts scheduled for this day",
|
"no_shifts_day": "No shifts scheduled for this day",
|
||||||
"no_workers_assigned": "No workers assigned yet",
|
"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": {
|
"worker_row": {
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"verified_message": "Worker attire verified for $name"
|
"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": {
|
"client_reports_common": {
|
||||||
|
|||||||
@@ -1692,9 +1692,44 @@
|
|||||||
"todays_cost": "Costo de Hoy",
|
"todays_cost": "Costo de Hoy",
|
||||||
"no_shifts_day": "No hay turnos programados para este día",
|
"no_shifts_day": "No hay turnos programados para este día",
|
||||||
"no_workers_assigned": "Aún no hay trabajadores asignados",
|
"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": {
|
"worker_row": {
|
||||||
"verify": "Verificar",
|
"verify": "Verificar",
|
||||||
"verified_message": "Vestimenta del trabajador verificada para $name"
|
"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": {
|
"client_reports_common": {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../blocs/coverage_state.dart';
|
|||||||
import '../widgets/coverage_calendar_selector.dart';
|
import '../widgets/coverage_calendar_selector.dart';
|
||||||
import '../widgets/coverage_quick_stats.dart';
|
import '../widgets/coverage_quick_stats.dart';
|
||||||
import '../widgets/coverage_shift_list.dart';
|
import '../widgets/coverage_shift_list.dart';
|
||||||
|
import '../widgets/coverage_stats_header.dart';
|
||||||
import '../widgets/late_workers_alert.dart';
|
import '../widgets/late_workers_alert.dart';
|
||||||
|
|
||||||
/// Page for displaying daily coverage information.
|
/// Page for displaying daily coverage information.
|
||||||
@@ -83,7 +84,7 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
_isScrolled
|
_isScrolled
|
||||||
? DateFormat('MMMM d').format(selectedDate)
|
? DateFormat('MMMM d').format(selectedDate)
|
||||||
: 'Daily Coverage',
|
: context.t.client_coverage.page.daily_coverage,
|
||||||
key: ValueKey<bool>(_isScrolled),
|
key: ValueKey<bool>(_isScrolled),
|
||||||
style: UiTypography.title2m.copyWith(
|
style: UiTypography.title2m.copyWith(
|
||||||
color: UiColors.primaryForeground,
|
color: UiColors.primaryForeground,
|
||||||
@@ -100,7 +101,7 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
icon: Container(
|
icon: Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space2),
|
padding: const EdgeInsets.all(UiConstants.space2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.primaryForeground.withOpacity(0.2),
|
color: UiColors.primaryForeground.withValues(alpha: 0.2),
|
||||||
borderRadius: UiConstants.radiusMd,
|
borderRadius: UiConstants.radiusMd,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -143,57 +144,13 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
// Coverage Stats Container
|
CoverageStatsHeader(
|
||||||
Container(
|
coveragePercent:
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
(state.stats?.coveragePercent ?? 0)
|
||||||
decoration: BoxDecoration(
|
.toDouble(),
|
||||||
color:
|
totalConfirmed:
|
||||||
UiColors.primaryForeground.withOpacity(0.1),
|
state.stats?.totalConfirmed ?? 0,
|
||||||
borderRadius: UiConstants.radiusLg,
|
totalNeeded: state.stats?.totalNeeded ?? 0,
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -244,13 +201,13 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
Text(
|
Text(
|
||||||
state.errorMessage != null
|
state.errorMessage != null
|
||||||
? translateErrorKey(state.errorMessage!)
|
? translateErrorKey(state.errorMessage!)
|
||||||
: 'An error occurred',
|
: context.t.client_coverage.page.error_occurred,
|
||||||
style: UiTypography.body1m.textError,
|
style: UiTypography.body1m.textError,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
UiButton.secondary(
|
UiButton.secondary(
|
||||||
text: 'Retry',
|
text: context.t.client_coverage.page.retry,
|
||||||
onPressed: () => BlocProvider.of<CoverageBloc>(context).add(
|
onPressed: () => BlocProvider.of<CoverageBloc>(context).add(
|
||||||
const CoverageRefreshRequested(),
|
const CoverageRefreshRequested(),
|
||||||
),
|
),
|
||||||
@@ -280,7 +237,7 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Shifts (${state.shifts.length})',
|
'${context.t.client_coverage.page.shifts} (${state.shifts.length})',
|
||||||
style: UiTypography.title2b.copyWith(
|
style: UiTypography.title2b.copyWith(
|
||||||
color: UiColors.textPrimary,
|
color: UiColors.textPrimary,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'calendar_nav_button.dart';
|
||||||
|
|
||||||
/// Calendar selector widget for choosing dates.
|
/// Calendar selector widget for choosing dates.
|
||||||
///
|
///
|
||||||
/// Displays a week view with navigation buttons and date selection.
|
/// Displays a week view with navigation buttons and date selection.
|
||||||
@@ -74,16 +77,16 @@ class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_NavButton(
|
CalendarNavButton(
|
||||||
text: '← Prev Week',
|
text: context.t.client_coverage.calendar.prev_week,
|
||||||
onTap: _navigatePrevWeek,
|
onTap: _navigatePrevWeek,
|
||||||
),
|
),
|
||||||
_NavButton(
|
CalendarNavButton(
|
||||||
text: 'Today',
|
text: context.t.client_coverage.calendar.today,
|
||||||
onTap: _navigateToday,
|
onTap: _navigateToday,
|
||||||
),
|
),
|
||||||
_NavButton(
|
CalendarNavButton(
|
||||||
text: 'Next Week →',
|
text: context.t.client_coverage.calendar.next_week,
|
||||||
onTap: _navigateNextWeek,
|
onTap: _navigateNextWeek,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -145,41 +148,3 @@ class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<DateTime> 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: <Color>[
|
|
||||||
UiColors.primary,
|
|
||||||
UiColors.primary,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Text(
|
|
||||||
'Workers',
|
|
||||||
style: UiTypography.body2r.copyWith(
|
|
||||||
color: UiColors.primaryForeground.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$totalConfirmed/$totalNeeded',
|
|
||||||
style: UiTypography.title2m.copyWith(
|
|
||||||
color: UiColors.primaryForeground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'coverage_stat_card.dart';
|
||||||
|
|
||||||
/// Quick statistics cards showing coverage metrics.
|
/// 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 {
|
class CoverageQuickStats extends StatelessWidget {
|
||||||
/// Creates a [CoverageQuickStats].
|
/// Creates a [CoverageQuickStats].
|
||||||
const CoverageQuickStats({
|
const CoverageQuickStats({
|
||||||
@@ -21,17 +24,17 @@ class CoverageQuickStats extends StatelessWidget {
|
|||||||
spacing: UiConstants.space2,
|
spacing: UiConstants.space2,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _StatCard(
|
child: CoverageStatCard(
|
||||||
icon: UiIcons.success,
|
icon: UiIcons.success,
|
||||||
label: 'Checked In',
|
label: context.t.client_coverage.stats.checked_in,
|
||||||
value: stats.checkedIn.toString(),
|
value: stats.checkedIn.toString(),
|
||||||
color: UiColors.iconSuccess,
|
color: UiColors.iconSuccess,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _StatCard(
|
child: CoverageStatCard(
|
||||||
icon: UiIcons.clock,
|
icon: UiIcons.clock,
|
||||||
label: 'En Route',
|
label: context.t.client_coverage.stats.en_route,
|
||||||
value: stats.enRoute.toString(),
|
value: stats.enRoute.toString(),
|
||||||
color: UiColors.textWarning,
|
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: <Widget>[
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
color: color,
|
|
||||||
size: UiConstants.space6,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: UiTypography.title1b.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: UiTypography.body3r.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'shift_header.dart';
|
||||||
|
import 'worker_row.dart';
|
||||||
|
|
||||||
/// List of shifts with their workers.
|
/// List of shifts with their workers.
|
||||||
///
|
///
|
||||||
/// Displays all shifts for the selected date, or an empty state if none exist.
|
/// Displays all shifts for the selected date, or an empty state if none exist.
|
||||||
@@ -33,6 +36,8 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCoverageEn l10n = context.t.client_coverage;
|
||||||
|
|
||||||
if (shifts.isEmpty) {
|
if (shifts.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space8),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
@@ -51,7 +56,7 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
color: UiColors.textSecondary,
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'No shifts scheduled for this day',
|
l10n.no_shifts_day,
|
||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -71,7 +76,7 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_ShiftHeader(
|
ShiftHeader(
|
||||||
title: shift.title,
|
title: shift.title,
|
||||||
location: shift.location,
|
location: shift.location,
|
||||||
startTime: _formatTime(shift.startTime),
|
startTime: _formatTime(shift.startTime),
|
||||||
@@ -91,7 +96,7 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: isLast ? 0 : UiConstants.space2,
|
bottom: isLast ? 0 : UiConstants.space2,
|
||||||
),
|
),
|
||||||
child: _WorkerRow(
|
child: WorkerRow(
|
||||||
worker: worker,
|
worker: worker,
|
||||||
shiftStartTime: _formatTime(shift.startTime),
|
shiftStartTime: _formatTime(shift.startTime),
|
||||||
formatTime: _formatTime,
|
formatTime: _formatTime,
|
||||||
@@ -104,7 +109,7 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'No workers assigned yet',
|
l10n.no_workers_assigned,
|
||||||
style: UiTypography.body3r.copyWith(
|
style: UiTypography.body3r.copyWith(
|
||||||
color: UiColors.mutedForeground,
|
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: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Row(
|
|
||||||
spacing: UiConstants.space1,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Text(
|
|
||||||
worker.name,
|
|
||||||
style: UiTypography.body2b.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
statusText,
|
|
||||||
style: UiTypography.body3m.copyWith(
|
|
||||||
color: textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
|
||||||
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,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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: <Widget>[
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: color,
|
||||||
|
size: UiConstants.space6,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.title1b.copyWith(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <Widget>[
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Alert widget for displaying late workers warning.
|
/// 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 {
|
class LateWorkersAlert extends StatelessWidget {
|
||||||
/// Creates a [LateWorkersAlert].
|
/// Creates a [LateWorkersAlert].
|
||||||
const LateWorkersAlert({
|
const LateWorkersAlert({
|
||||||
@@ -38,11 +39,12 @@ class LateWorkersAlert extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
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,
|
style: UiTypography.body1b.textError,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Auto-backup system system is searching for replacements.',
|
context.t.client_coverage.alert.auto_backup_searching,
|
||||||
style: UiTypography.body3r.copyWith(
|
style: UiTypography.body3r.copyWith(
|
||||||
color: UiColors.textError.withValues(alpha: 0.7),
|
color: UiColors.textError.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
Row(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: UiConstants.space3,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
startTime,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CoverageBadge(
|
||||||
|
current: current,
|
||||||
|
total: total,
|
||||||
|
coveragePercent: coveragePercent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <Widget>[
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
Text(
|
||||||
|
worker.name,
|
||||||
|
style: UiTypography.body2b.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
style: UiTypography.body3m.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
children: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user