Refactor coverage-related widgets and improve UI consistency
- Updated CancelLateWorkerSheet to enhance layout and information display. - Modified CoverageCalendarSelector to adjust color opacity for better visibility. - Simplified CoveragePageSkeleton by removing unused quick stats row. - Removed CoverageQuickStats widget as it was redundant. - Enhanced CoverageShiftList to manage expanded state and improve worker display. - Deleted CoverageStatCard as its functionality is no longer needed. - Revamped CoverageStatsHeader to provide a clearer overview of coverage statistics. - Improved LateWorkersAlert for better visual feedback on late workers. - Redesigned ShiftHeader for a more interactive and informative shift card. - Updated WorkerRow to streamline status display and action buttons.
This commit is contained in:
@@ -1793,7 +1793,9 @@
|
|||||||
"workers": "Workers",
|
"workers": "Workers",
|
||||||
"error_occurred": "An error occurred",
|
"error_occurred": "An error occurred",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"shifts": "Shifts"
|
"shifts": "Shifts",
|
||||||
|
"overall_coverage": "Overall Coverage",
|
||||||
|
"live_activity": "LIVE ACTIVITY"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"prev_week": "\u2190 Prev Week",
|
"prev_week": "\u2190 Prev Week",
|
||||||
@@ -1802,7 +1804,9 @@
|
|||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"checked_in": "Checked In",
|
"checked_in": "Checked In",
|
||||||
"en_route": "En Route"
|
"en_route": "En Route",
|
||||||
|
"on_site": "On Site",
|
||||||
|
"late": "Late"
|
||||||
},
|
},
|
||||||
"alert": {
|
"alert": {
|
||||||
"workers_running_late(count)": {
|
"workers_running_late(count)": {
|
||||||
|
|||||||
@@ -1793,7 +1793,9 @@
|
|||||||
"workers": "Trabajadores",
|
"workers": "Trabajadores",
|
||||||
"error_occurred": "Ocurri\u00f3 un error",
|
"error_occurred": "Ocurri\u00f3 un error",
|
||||||
"retry": "Reintentar",
|
"retry": "Reintentar",
|
||||||
"shifts": "Turnos"
|
"shifts": "Turnos",
|
||||||
|
"overall_coverage": "Cobertura General",
|
||||||
|
"live_activity": "ACTIVIDAD EN VIVO"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"prev_week": "\u2190 Semana Anterior",
|
"prev_week": "\u2190 Semana Anterior",
|
||||||
@@ -1802,7 +1804,9 @@
|
|||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"checked_in": "Registrado",
|
"checked_in": "Registrado",
|
||||||
"en_route": "En Camino"
|
"en_route": "En Camino",
|
||||||
|
"on_site": "En Sitio",
|
||||||
|
"late": "Tarde"
|
||||||
},
|
},
|
||||||
"alert": {
|
"alert": {
|
||||||
"workers_running_late(count)": {
|
"workers_running_late(count)": {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class UiChip extends StatelessWidget {
|
|||||||
|
|
||||||
final Row content = Row(
|
final Row content = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (leadingIcon != null) ...<Widget>[
|
if (leadingIcon != null) ...<Widget>[
|
||||||
Icon(leadingIcon, size: iconSize, color: contentColor),
|
Icon(leadingIcon, size: iconSize, color: contentColor),
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import 'package:client_coverage/src/presentation/blocs/coverage_event.dart';
|
|||||||
import 'package:client_coverage/src/presentation/blocs/coverage_state.dart';
|
import 'package:client_coverage/src/presentation/blocs/coverage_state.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/coverage_calendar_selector.dart';
|
import 'package:client_coverage/src/presentation/widgets/coverage_calendar_selector.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/coverage_page_skeleton.dart';
|
import 'package:client_coverage/src/presentation/widgets/coverage_page_skeleton.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/coverage_quick_stats.dart';
|
|
||||||
import 'package:client_coverage/src/presentation/widgets/coverage_shift_list.dart';
|
import 'package:client_coverage/src/presentation/widgets/coverage_shift_list.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/coverage_stats_header.dart';
|
import 'package:client_coverage/src/presentation/widgets/coverage_stats_header.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/late_workers_alert.dart';
|
import 'package:client_coverage/src/presentation/widgets/late_workers_alert.dart';
|
||||||
|
|
||||||
/// Page for displaying daily coverage information.
|
/// Page for displaying daily coverage information.
|
||||||
///
|
///
|
||||||
/// Shows shifts, worker statuses, and coverage statistics for a selected date.
|
/// Shows shifts, worker statuses, and coverage statistics for a selected date
|
||||||
|
/// using a collapsible SliverAppBar with gradient header and live activity feed.
|
||||||
class CoveragePage extends StatefulWidget {
|
class CoveragePage extends StatefulWidget {
|
||||||
/// Creates a [CoveragePage].
|
/// Creates a [CoveragePage].
|
||||||
const CoveragePage({super.key});
|
const CoveragePage({super.key});
|
||||||
@@ -27,14 +27,13 @@ class CoveragePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CoveragePageState extends State<CoveragePage> {
|
class _CoveragePageState extends State<CoveragePage> {
|
||||||
|
/// Controller for the [CustomScrollView].
|
||||||
late ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
bool _isScrolled = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController = ScrollController();
|
_scrollController = ScrollController();
|
||||||
_scrollController.addListener(_onScroll);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -43,16 +42,6 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
|
||||||
if (_scrollController.hasClients) {
|
|
||||||
if (_scrollController.offset > 180 && !_isScrolled) {
|
|
||||||
setState(() => _isScrolled = true);
|
|
||||||
} else if (_scrollController.offset <= 180 && _isScrolled) {
|
|
||||||
setState(() => _isScrolled = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<CoverageBloc>(
|
return BlocProvider<CoverageBloc>(
|
||||||
@@ -93,19 +82,26 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 300.0,
|
expandedHeight: 316.0,
|
||||||
backgroundColor: UiColors.primary,
|
backgroundColor: UiColors.primary,
|
||||||
title: AnimatedSwitcher(
|
title: Column(
|
||||||
duration: const Duration(milliseconds: 200),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
_isScrolled
|
children: <Widget>[
|
||||||
? DateFormat('MMMM d').format(selectedDate)
|
Text(
|
||||||
: context.t.client_coverage.page.daily_coverage,
|
context.t.client_coverage.page.daily_coverage,
|
||||||
key: ValueKey<bool>(_isScrolled),
|
|
||||||
style: UiTypography.title2m.copyWith(
|
style: UiTypography.title2m.copyWith(
|
||||||
color: UiColors.primaryForeground,
|
color: UiColors.primaryForeground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('EEEE, MMMM d').format(selectedDate),
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.primaryForeground
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -135,7 +131,7 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
UiColors.primary,
|
UiColors.primary,
|
||||||
UiColors.primary,
|
Color(0xFF0626A8),
|
||||||
],
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
@@ -169,6 +165,12 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
state.stats?.totalPositionsConfirmed ?? 0,
|
state.stats?.totalPositionsConfirmed ?? 0,
|
||||||
totalNeeded:
|
totalNeeded:
|
||||||
state.stats?.totalPositionsNeeded ?? 0,
|
state.stats?.totalPositionsNeeded ?? 0,
|
||||||
|
totalCheckedIn:
|
||||||
|
state.stats?.totalWorkersCheckedIn ?? 0,
|
||||||
|
totalEnRoute:
|
||||||
|
state.stats?.totalWorkersEnRoute ?? 0,
|
||||||
|
totalLate:
|
||||||
|
state.stats?.totalWorkersLate ?? 0,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -191,7 +193,10 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the main body content based on the current state.
|
/// Builds the main body content based on the current [CoverageState].
|
||||||
|
///
|
||||||
|
/// Displays a skeleton loader, error state, or the live activity feed
|
||||||
|
/// with late worker alerts and shift list.
|
||||||
Widget _buildBody({
|
Widget _buildBody({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required CoverageState state,
|
required CoverageState state,
|
||||||
@@ -241,9 +246,6 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: UiConstants.space6,
|
spacing: UiConstants.space6,
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (state.stats != null &&
|
if (state.stats != null &&
|
||||||
state.stats!.totalWorkersLate > 0) ...<Widget>[
|
state.stats!.totalWorkersLate > 0) ...<Widget>[
|
||||||
@@ -251,15 +253,13 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
lateCount: state.stats!.totalWorkersLate,
|
lateCount: state.stats!.totalWorkersLate,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (state.stats != null) ...<Widget>[
|
|
||||||
CoverageQuickStats(stats: state.stats!),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'${context.t.client_coverage.page.shifts} (${state.shifts.length})',
|
context.t.client_coverage.page.live_activity,
|
||||||
style: UiTypography.title2b.copyWith(
|
style: UiTypography.body4m.copyWith(
|
||||||
color: UiColors.textPrimary,
|
color: UiColors.textSecondary,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
CoverageShiftList(shifts: state.shifts),
|
CoverageShiftList(shifts: state.shifts),
|
||||||
|
|||||||
@@ -91,14 +91,29 @@ class _CancelLateWorkerSheetState extends State<CancelLateWorkerSheet> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
Row(
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
UiIcons.warning,
|
UiIcons.warning,
|
||||||
color: UiColors.destructive,
|
color: UiColors.destructive,
|
||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(l10n.title, style: UiTypography.title1b.textError),
|
||||||
|
Text(
|
||||||
|
l10n.subtitle,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Navigator.of(context).pop(),
|
onTap: () => Navigator.of(context).pop(),
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
UiIcons.close,
|
UiIcons.close,
|
||||||
color: UiColors.textSecondary,
|
color: UiColors.textSecondary,
|
||||||
size: 24,
|
size: 24,
|
||||||
@@ -106,21 +121,14 @@ class _CancelLateWorkerSheetState extends State<CancelLateWorkerSheet> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(l10n.title, style: UiTypography.title1b),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
l10n.subtitle,
|
|
||||||
style: UiTypography.body2r.copyWith(color: UiColors.destructive),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
Text(
|
Text(
|
||||||
l10n.confirm_message(name: widget.worker.fullName),
|
l10n.confirm_message(name: widget.worker.fullName),
|
||||||
style: UiTypography.body1m,
|
style: UiTypography.body1r,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
l10n.helper_text,
|
l10n.helper_text,
|
||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
@@ -146,28 +154,19 @@ class _CancelLateWorkerSheetState extends State<CancelLateWorkerSheet> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space3),
|
const SizedBox(width: UiConstants.space3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: UiButton.primary(
|
||||||
|
text: l10n.confirm,
|
||||||
onPressed: () => _onConfirm(context),
|
onPressed: () => _onConfirm(context),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: UiColors.destructive,
|
backgroundColor: UiColors.destructive,
|
||||||
foregroundColor: UiColors.primaryForeground,
|
foregroundColor: UiColors.primaryForeground,
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: UiConstants.space3,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: UiConstants.radiusMd,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
l10n.confirm,
|
|
||||||
style: UiTypography.body1b.copyWith(
|
|
||||||
color: UiColors.primaryForeground,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? UiColors.primaryForeground
|
? UiColors.primaryForeground
|
||||||
: UiColors.primaryForeground.withOpacity(0.1),
|
: UiColors.primaryForeground.withAlpha(25),
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: isToday && !isSelected
|
border: isToday && !isSelected
|
||||||
? Border.all(
|
? Border.all(
|
||||||
@@ -122,6 +122,14 @@ class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
DateFormat('E').format(date),
|
||||||
|
style: UiTypography.body4m.copyWith(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.primary
|
||||||
|
: UiColors.primaryForeground.withAlpha(179),
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
date.day.toString().padLeft(2, '0'),
|
date.day.toString().padLeft(2, '0'),
|
||||||
style: UiTypography.body1b.copyWith(
|
style: UiTypography.body1b.copyWith(
|
||||||
@@ -130,14 +138,6 @@ class _CoverageCalendarSelectorState extends State<CoverageCalendarSelector> {
|
|||||||
: UiColors.primaryForeground,
|
: UiColors.primaryForeground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
DateFormat('E').format(date),
|
|
||||||
style: UiTypography.body4m.copyWith(
|
|
||||||
color: isSelected
|
|
||||||
? UiColors.mutedForeground
|
|
||||||
: UiColors.primaryForeground.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import 'package:client_coverage/src/presentation/widgets/coverage_page_skeleton/
|
|||||||
|
|
||||||
/// Shimmer loading skeleton that mimics the coverage page loaded layout.
|
/// Shimmer loading skeleton that mimics the coverage page loaded layout.
|
||||||
///
|
///
|
||||||
/// Shows placeholder shapes for the quick stats row, shift section header,
|
/// Shows placeholder shapes for the live activity section label and a list
|
||||||
/// and a list of shift cards with worker rows.
|
/// of shift cards with worker rows.
|
||||||
class CoveragePageSkeleton extends StatelessWidget {
|
class CoveragePageSkeleton extends StatelessWidget {
|
||||||
/// Creates a [CoveragePageSkeleton].
|
/// Creates a [CoveragePageSkeleton].
|
||||||
const CoveragePageSkeleton({super.key});
|
const CoveragePageSkeleton({super.key});
|
||||||
@@ -19,18 +19,8 @@ class CoveragePageSkeleton extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Quick stats row (2 stat cards)
|
// "LIVE ACTIVITY" section label placeholder
|
||||||
Row(
|
UiShimmerLine(width: 100, height: 10),
|
||||||
children: <Widget>[
|
|
||||||
Expanded(child: UiShimmerStatsCard()),
|
|
||||||
SizedBox(width: UiConstants.space2),
|
|
||||||
Expanded(child: UiShimmerStatsCard()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
// Shifts section header
|
|
||||||
UiShimmerLine(width: 140, height: 18),
|
|
||||||
SizedBox(height: UiConstants.space6),
|
SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
// Shift cards with worker rows
|
// Shift cards with worker rows
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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 'package:client_coverage/src/presentation/widgets/coverage_stat_card.dart';
|
|
||||||
|
|
||||||
/// Quick statistics cards showing coverage metrics.
|
|
||||||
///
|
|
||||||
/// Displays checked-in and en-route worker counts.
|
|
||||||
class CoverageQuickStats extends StatelessWidget {
|
|
||||||
/// Creates a [CoverageQuickStats].
|
|
||||||
const CoverageQuickStats({
|
|
||||||
required this.stats,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The coverage statistics to display.
|
|
||||||
final CoverageStats stats;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: CoverageStatCard(
|
|
||||||
icon: UiIcons.success,
|
|
||||||
label: context.t.client_coverage.stats.checked_in,
|
|
||||||
value: stats.totalWorkersCheckedIn.toString(),
|
|
||||||
color: UiColors.iconSuccess,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: CoverageStatCard(
|
|
||||||
icon: UiIcons.clock,
|
|
||||||
label: context.t.client_coverage.stats.en_route,
|
|
||||||
value: stats.totalWorkersEnRoute.toString(),
|
|
||||||
color: UiColors.textWarning,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,10 +9,12 @@ import 'package:client_coverage/src/presentation/widgets/shift_header.dart';
|
|||||||
import 'package:client_coverage/src/presentation/widgets/worker_row.dart';
|
import 'package:client_coverage/src/presentation/widgets/worker_row.dart';
|
||||||
import 'package:client_coverage/src/presentation/widgets/worker_review_sheet.dart';
|
import 'package:client_coverage/src/presentation/widgets/worker_review_sheet.dart';
|
||||||
|
|
||||||
/// List of shifts with their workers.
|
/// Displays a list of shifts as collapsible cards with worker details.
|
||||||
///
|
///
|
||||||
/// Displays all shifts for the selected date, or an empty state if none exist.
|
/// Each shift is rendered as a card with a tappable [ShiftHeader] that toggles
|
||||||
class CoverageShiftList extends StatelessWidget {
|
/// visibility of the worker rows beneath it. All cards start expanded.
|
||||||
|
/// Shows an empty state when [shifts] is empty.
|
||||||
|
class CoverageShiftList extends StatefulWidget {
|
||||||
/// Creates a [CoverageShiftList].
|
/// Creates a [CoverageShiftList].
|
||||||
const CoverageShiftList({
|
const CoverageShiftList({
|
||||||
required this.shifts,
|
required this.shifts,
|
||||||
@@ -22,17 +24,73 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
/// The list of shifts to display.
|
/// The list of shifts to display.
|
||||||
final List<ShiftWithWorkers> shifts;
|
final List<ShiftWithWorkers> shifts;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CoverageShiftList> createState() => _CoverageShiftListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for [CoverageShiftList] managing which shift cards are expanded.
|
||||||
|
class _CoverageShiftListState extends State<CoverageShiftList> {
|
||||||
|
/// Set of shift IDs whose cards are currently expanded.
|
||||||
|
final Set<String> _expandedShiftIds = <String>{};
|
||||||
|
|
||||||
|
/// Whether the expanded set has been initialised from the first build.
|
||||||
|
bool _initialised = false;
|
||||||
|
|
||||||
/// Formats a [DateTime] to a readable time string (h:mm a).
|
/// Formats a [DateTime] to a readable time string (h:mm a).
|
||||||
String _formatTime(DateTime? time) {
|
String _formatTime(DateTime? time) {
|
||||||
if (time == null) return '';
|
if (time == null) return '';
|
||||||
return DateFormat('h:mm a').format(time);
|
return DateFormat('h:mm a').format(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggles the expanded / collapsed state for the shift with [shiftId].
|
||||||
|
void _toggleShift(String shiftId) {
|
||||||
|
setState(() {
|
||||||
|
if (_expandedShiftIds.contains(shiftId)) {
|
||||||
|
_expandedShiftIds.remove(shiftId);
|
||||||
|
} else {
|
||||||
|
_expandedShiftIds.add(shiftId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seeds [_expandedShiftIds] with all current shift IDs on first build,
|
||||||
|
/// and adds any new shift IDs when the widget is rebuilt with new data.
|
||||||
|
void _ensureInitialised() {
|
||||||
|
if (!_initialised) {
|
||||||
|
_expandedShiftIds.addAll(
|
||||||
|
widget.shifts.map((ShiftWithWorkers s) => s.shiftId),
|
||||||
|
);
|
||||||
|
_initialised = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Add any new shift IDs that arrived after initial build.
|
||||||
|
for (final ShiftWithWorkers shift in widget.shifts) {
|
||||||
|
if (!_expandedShiftIds.contains(shift.shiftId)) {
|
||||||
|
_expandedShiftIds.add(shift.shiftId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant CoverageShiftList oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// Add newly-appeared shift IDs so they start expanded.
|
||||||
|
for (final ShiftWithWorkers shift in widget.shifts) {
|
||||||
|
if (!oldWidget.shifts.any(
|
||||||
|
(ShiftWithWorkers old) => old.shiftId == shift.shiftId,
|
||||||
|
)) {
|
||||||
|
_expandedShiftIds.add(shift.shiftId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
_ensureInitialised();
|
||||||
|
|
||||||
final TranslationsClientCoverageEn l10n = context.t.client_coverage;
|
final TranslationsClientCoverageEn l10n = context.t.client_coverage;
|
||||||
|
|
||||||
if (shifts.isEmpty) {
|
if (widget.shifts.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space8),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -59,59 +117,122 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: shifts.map((ShiftWithWorkers shift) {
|
children: widget.shifts.map((ShiftWithWorkers shift) {
|
||||||
final int coveragePercent = shift.requiredWorkerCount > 0
|
final int coveragePercent = shift.requiredWorkerCount > 0
|
||||||
? ((shift.assignedWorkerCount / shift.requiredWorkerCount) * 100)
|
? ((shift.assignedWorkerCount / shift.requiredWorkerCount) * 100)
|
||||||
.round()
|
.round()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
// Per-shift worker status counts.
|
||||||
|
final int onSite = shift.assignedWorkers
|
||||||
|
.where(
|
||||||
|
(AssignedWorker w) => w.status == AssignmentStatus.checkedIn,
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
final int enRoute = shift.assignedWorkers
|
||||||
|
.where(
|
||||||
|
(AssignedWorker w) =>
|
||||||
|
w.status == AssignmentStatus.accepted && w.checkInAt == null,
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
final int lateCount = shift.assignedWorkers
|
||||||
|
.where(
|
||||||
|
(AssignedWorker w) => w.status == AssignmentStatus.noShow,
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
final bool isExpanded = _expandedShiftIds.contains(shift.shiftId);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radius2xl,
|
||||||
border: Border.all(color: UiColors.border),
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ShiftHeader(
|
ShiftHeader(
|
||||||
title: shift.roleName,
|
title: shift.roleName,
|
||||||
location: '', // V2 API does not return location on coverage
|
|
||||||
startTime: _formatTime(shift.timeRange.startsAt),
|
startTime: _formatTime(shift.timeRange.startsAt),
|
||||||
current: shift.assignedWorkerCount,
|
current: shift.assignedWorkerCount,
|
||||||
total: shift.requiredWorkerCount,
|
total: shift.requiredWorkerCount,
|
||||||
coveragePercent: coveragePercent,
|
coveragePercent: coveragePercent,
|
||||||
shiftId: shift.shiftId,
|
shiftId: shift.shiftId,
|
||||||
|
onSiteCount: onSite,
|
||||||
|
enRouteCount: enRoute,
|
||||||
|
lateCount: lateCount,
|
||||||
|
isExpanded: isExpanded,
|
||||||
|
onToggle: () => _toggleShift(shift.shiftId),
|
||||||
),
|
),
|
||||||
if (shift.assignedWorkers.isNotEmpty)
|
AnimatedCrossFade(
|
||||||
|
firstChild: const SizedBox.shrink(),
|
||||||
|
secondChild: _buildWorkerSection(shift, l10n),
|
||||||
|
crossFadeState: isExpanded
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the expanded worker section for a shift including divider.
|
||||||
|
Widget _buildWorkerSection(
|
||||||
|
ShiftWithWorkers shift,
|
||||||
|
TranslationsClientCoverageEn l10n,
|
||||||
|
) {
|
||||||
|
if (shift.assignedWorkers.isEmpty) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const Divider(height: 1, color: UiColors.border),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Text(
|
||||||
|
l10n.no_workers_assigned,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.mutedForeground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const Divider(height: 1, color: UiColors.border),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: shift.assignedWorkers
|
children:
|
||||||
.map<Widget>((AssignedWorker worker) {
|
shift.assignedWorkers.map<Widget>((AssignedWorker worker) {
|
||||||
final bool isLast =
|
final bool isLast = worker == shift.assignedWorkers.last;
|
||||||
worker == shift.assignedWorkers.last;
|
|
||||||
return Padding(
|
return Padding(
|
||||||
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:
|
shiftStartTime: _formatTime(shift.timeRange.startsAt),
|
||||||
_formatTime(shift.timeRange.startsAt),
|
|
||||||
showRateButton:
|
showRateButton:
|
||||||
worker.status == AssignmentStatus.checkedIn ||
|
worker.status == AssignmentStatus.checkedIn ||
|
||||||
worker.status ==
|
worker.status == AssignmentStatus.checkedOut ||
|
||||||
AssignmentStatus.checkedOut ||
|
worker.status == AssignmentStatus.completed,
|
||||||
worker.status ==
|
|
||||||
AssignmentStatus.completed,
|
|
||||||
showCancelButton:
|
showCancelButton:
|
||||||
worker.status == AssignmentStatus.noShow ||
|
worker.status == AssignmentStatus.noShow ||
|
||||||
worker.status ==
|
worker.status == AssignmentStatus.assigned ||
|
||||||
AssignmentStatus.assigned ||
|
worker.status == AssignmentStatus.accepted,
|
||||||
worker.status ==
|
|
||||||
AssignmentStatus.accepted,
|
|
||||||
onRate: () => WorkerReviewSheet.show(
|
onRate: () => WorkerReviewSheet.show(
|
||||||
context,
|
context,
|
||||||
worker: worker,
|
worker: worker,
|
||||||
@@ -124,21 +245,8 @@ class CoverageShiftList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
|
||||||
child: Text(
|
|
||||||
l10n.no_workers_assigned,
|
|
||||||
style: UiTypography.body3r.copyWith(
|
|
||||||
color: UiColors.mutedForeground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,43 +2,77 @@ 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';
|
||||||
|
|
||||||
/// Displays coverage percentage and worker ratio in the app bar header.
|
/// Displays overall coverage statistics in the SliverAppBar expanded header.
|
||||||
|
///
|
||||||
|
/// Shows the coverage percentage, a progress bar, and real-time worker
|
||||||
|
/// status counts (on site, en route, late) on a primary blue gradient
|
||||||
|
/// background with a semi-transparent white container.
|
||||||
class CoverageStatsHeader extends StatelessWidget {
|
class CoverageStatsHeader extends StatelessWidget {
|
||||||
/// Creates a [CoverageStatsHeader].
|
/// Creates a [CoverageStatsHeader] with coverage and worker status data.
|
||||||
const CoverageStatsHeader({
|
const CoverageStatsHeader({
|
||||||
required this.coveragePercent,
|
required this.coveragePercent,
|
||||||
required this.totalConfirmed,
|
required this.totalConfirmed,
|
||||||
required this.totalNeeded,
|
required this.totalNeeded,
|
||||||
|
required this.totalCheckedIn,
|
||||||
|
required this.totalEnRoute,
|
||||||
|
required this.totalLate,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The current coverage percentage.
|
/// The current overall coverage percentage (0-100).
|
||||||
final double coveragePercent;
|
final double coveragePercent;
|
||||||
|
|
||||||
/// The number of confirmed workers.
|
/// The number of confirmed workers.
|
||||||
final int totalConfirmed;
|
final int totalConfirmed;
|
||||||
|
|
||||||
/// The total number of workers needed.
|
/// The total number of workers needed for full coverage.
|
||||||
final int totalNeeded;
|
final int totalNeeded;
|
||||||
|
|
||||||
|
/// The number of workers currently checked in and on site.
|
||||||
|
final int totalCheckedIn;
|
||||||
|
|
||||||
|
/// The number of workers currently en route.
|
||||||
|
final int totalEnRoute;
|
||||||
|
|
||||||
|
/// The number of workers currently marked as late.
|
||||||
|
final int totalLate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.primaryForeground.withOpacity(0.1),
|
color: UiColors.primaryForeground.withValues(alpha: 0.12),
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusXl,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _buildCoverageColumn(context),
|
||||||
|
),
|
||||||
|
_buildStatusColumn(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
_buildProgressBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the left column with the "Overall Coverage" label and percentage.
|
||||||
|
Widget _buildCoverageColumn(BuildContext context) {
|
||||||
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
context.t.client_coverage.page.coverage_status,
|
context.t.client_coverage.page.overall_coverage,
|
||||||
style: UiTypography.body2r.copyWith(
|
style: UiTypography.body3r.copyWith(
|
||||||
color: UiColors.primaryForeground.withOpacity(0.7),
|
color: UiColors.primaryForeground.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -48,25 +82,95 @@ class CoverageStatsHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
Column(
|
}
|
||||||
|
|
||||||
|
/// Builds the right column with on-site, en-route, and late stat items.
|
||||||
|
Widget _buildStatusColumn(BuildContext context) {
|
||||||
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildStatRow(
|
||||||
|
context: context,
|
||||||
|
value: totalCheckedIn,
|
||||||
|
label: context.t.client_coverage.stats.on_site,
|
||||||
|
valueColor: UiColors.primaryForeground,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
_buildStatRow(
|
||||||
|
context: context,
|
||||||
|
value: totalEnRoute,
|
||||||
|
label: context.t.client_coverage.stats.en_route,
|
||||||
|
valueColor: UiColors.accent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
_buildStatRow(
|
||||||
|
context: context,
|
||||||
|
value: totalLate,
|
||||||
|
label: context.t.client_coverage.stats.late,
|
||||||
|
valueColor: UiColors.tagError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a single stat row with a colored number and a muted label.
|
||||||
|
Widget _buildStatRow({
|
||||||
|
required BuildContext context,
|
||||||
|
required int value,
|
||||||
|
required String label,
|
||||||
|
required Color valueColor,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
context.t.client_coverage.page.workers,
|
value.toString(),
|
||||||
style: UiTypography.body2r.copyWith(
|
style: UiTypography.title2b.copyWith(
|
||||||
color: UiColors.primaryForeground.withOpacity(0.7),
|
color: valueColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
'$totalConfirmed/$totalNeeded',
|
label,
|
||||||
style: UiTypography.title2m.copyWith(
|
style: UiTypography.body4m.copyWith(
|
||||||
|
color: UiColors.primaryForeground.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the horizontal progress bar indicating coverage fill.
|
||||||
|
Widget _buildProgressBar() {
|
||||||
|
final double clampedFraction =
|
||||||
|
(coveragePercent / 100).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 8,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primaryForeground.withValues(alpha: 0.2),
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FractionallySizedBox(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
widthFactor: clampedFraction,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
color: UiColors.primaryForeground,
|
color: UiColors.primaryForeground,
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,54 @@ 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 banner displayed when workers are running late.
|
||||||
///
|
///
|
||||||
/// Shows a warning banner when workers are running late.
|
/// Renders a solid red container with a warning icon, late worker count,
|
||||||
|
/// and auto-backup status message in white text.
|
||||||
class LateWorkersAlert extends StatelessWidget {
|
class LateWorkersAlert extends StatelessWidget {
|
||||||
/// Creates a [LateWorkersAlert].
|
/// Creates a [LateWorkersAlert] with the given [lateCount].
|
||||||
const LateWorkersAlert({
|
const LateWorkersAlert({
|
||||||
required this.lateCount,
|
required this.lateCount,
|
||||||
this.onTap,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The number of late workers.
|
/// The number of workers currently marked as late.
|
||||||
final int lateCount;
|
final int lateCount;
|
||||||
|
|
||||||
/// Optional callback invoked when the alert is tapped.
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Container(
|
||||||
onTap: onTap,
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Container(
|
horizontal: UiConstants.space4,
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
vertical: UiConstants.space3,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.destructive.withValues(alpha: 0.1),
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
border: Border.all(
|
|
||||||
color: UiColors.destructive,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.destructive.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: UiConstants.space4,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(
|
Container(
|
||||||
UiIcons.warning,
|
width: 32,
|
||||||
color: UiColors.destructive,
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
child: const Icon(
|
||||||
|
UiIcons.warning,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -47,26 +57,21 @@ class LateWorkersAlert extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
context.t.client_coverage.alert
|
context.t.client_coverage.alert
|
||||||
.workers_running_late(n: lateCount, count: lateCount),
|
.workers_running_late(n: lateCount, count: lateCount),
|
||||||
style: UiTypography.body1b.textError,
|
style: UiTypography.body1b.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
context.t.client_coverage.alert.auto_backup_searching,
|
context.t.client_coverage.alert.auto_backup_searching,
|
||||||
style: UiTypography.body3r.copyWith(
|
style: UiTypography.body3r.copyWith(
|
||||||
color: UiColors.textError.withValues(alpha: 0.7),
|
color: Colors.white.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onTap != null)
|
|
||||||
const Icon(
|
|
||||||
UiIcons.chevronRight,
|
|
||||||
size: UiConstants.space4,
|
|
||||||
color: UiColors.destructive,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,125 +1,193 @@
|
|||||||
|
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:client_coverage/src/presentation/widgets/coverage_badge.dart';
|
/// Tappable header for a collapsible shift card.
|
||||||
|
///
|
||||||
/// Header section for a shift card showing title, location, time, and coverage.
|
/// Displays a status dot colour-coded by coverage, the shift title and time,
|
||||||
|
/// a filled/total badge, a linear progress bar, and per-shift worker summary
|
||||||
|
/// counts (on site, en route, late). Tapping anywhere triggers [onToggle].
|
||||||
class ShiftHeader extends StatelessWidget {
|
class ShiftHeader extends StatelessWidget {
|
||||||
/// Creates a [ShiftHeader].
|
/// Creates a [ShiftHeader].
|
||||||
const ShiftHeader({
|
const ShiftHeader({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.location,
|
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
required this.current,
|
required this.current,
|
||||||
required this.total,
|
required this.total,
|
||||||
required this.coveragePercent,
|
required this.coveragePercent,
|
||||||
required this.shiftId,
|
required this.shiftId,
|
||||||
|
required this.onSiteCount,
|
||||||
|
required this.enRouteCount,
|
||||||
|
required this.lateCount,
|
||||||
|
required this.isExpanded,
|
||||||
|
required this.onToggle,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The shift title.
|
/// The shift role or title.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
/// The shift location.
|
/// Formatted shift start time (e.g. "8:00 AM").
|
||||||
final String location;
|
|
||||||
|
|
||||||
/// The formatted shift start time.
|
|
||||||
final String startTime;
|
final String startTime;
|
||||||
|
|
||||||
/// Current number of assigned workers.
|
/// Current number of assigned workers.
|
||||||
final int current;
|
final int current;
|
||||||
|
|
||||||
/// Total workers needed for the shift.
|
/// Total workers required for the shift.
|
||||||
final int total;
|
final int total;
|
||||||
|
|
||||||
/// Coverage percentage (0-100+).
|
/// Coverage percentage (0-100+).
|
||||||
final int coveragePercent;
|
final int coveragePercent;
|
||||||
|
|
||||||
/// The shift identifier.
|
/// Unique shift identifier.
|
||||||
final String shiftId;
|
final String shiftId;
|
||||||
|
|
||||||
|
/// Number of workers currently on site (checked in).
|
||||||
|
final int onSiteCount;
|
||||||
|
|
||||||
|
/// Number of workers en route (accepted but not checked in).
|
||||||
|
final int enRouteCount;
|
||||||
|
|
||||||
|
/// Number of workers marked as late / no-show.
|
||||||
|
final int lateCount;
|
||||||
|
|
||||||
|
/// Whether the shift card is currently expanded to show workers.
|
||||||
|
final bool isExpanded;
|
||||||
|
|
||||||
|
/// Callback invoked when the header is tapped to expand or collapse.
|
||||||
|
final VoidCallback onToggle;
|
||||||
|
|
||||||
|
/// Returns the status colour based on [coveragePercent].
|
||||||
|
///
|
||||||
|
/// Green for >= 100 %, yellow for >= 80 %, red otherwise.
|
||||||
|
Color _statusColor() {
|
||||||
|
if (coveragePercent >= 100) {
|
||||||
|
return UiColors.textSuccess;
|
||||||
|
} else if (coveragePercent >= 80) {
|
||||||
|
return UiColors.textWarning;
|
||||||
|
}
|
||||||
|
return UiColors.destructive;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final Color statusColor = _statusColor();
|
||||||
|
final TranslationsClientCoverageStatsEn stats =
|
||||||
|
context.t.client_coverage.stats;
|
||||||
|
final double fillFraction =
|
||||||
|
total > 0 ? (current / total).clamp(0.0, 1.0) : 0.0;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onToggle,
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: UiConstants.space2,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
// Row 1: status dot, title + time, badge, chevron.
|
||||||
Row(
|
Row(
|
||||||
spacing: UiConstants.space2,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
// Status dot.
|
||||||
width: UiConstants.space2,
|
Padding(
|
||||||
height: UiConstants.space2,
|
padding: const EdgeInsets.only(top: UiConstants.space1),
|
||||||
decoration: const BoxDecoration(
|
child: Container(
|
||||||
color: UiColors.primary,
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
// Title and start time.
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.body1b.textPrimary,
|
style: UiTypography.body1b.textPrimary,
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: UiConstants.space1),
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
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>[
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.clock,
|
UiIcons.clock,
|
||||||
size: UiConstants.space3,
|
size: 10,
|
||||||
color: UiColors.iconSecondary,
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
startTime,
|
startTime,
|
||||||
style: UiTypography.body3r.textSecondary,
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
// Coverage badge.
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space2,
|
||||||
|
vertical: UiConstants.space1,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withAlpha(26),
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$current/$total',
|
||||||
|
style: UiTypography.body3b.copyWith(color: statusColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
CoverageBadge(
|
const SizedBox(width: UiConstants.space2),
|
||||||
current: current,
|
// Expand / collapse chevron.
|
||||||
total: total,
|
Icon(
|
||||||
coveragePercent: coveragePercent,
|
isExpanded ? UiIcons.chevronUp : UiIcons.chevronDown,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
// Progress bar.
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
child: Container(
|
||||||
|
height: 6,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.muted,
|
||||||
|
),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
widthFactor: fillFraction,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
// Summary text: on site / en route / late.
|
||||||
|
Text(
|
||||||
|
'$onSiteCount ${stats.on_site} · '
|
||||||
|
'$enRouteCount ${stats.en_route} · '
|
||||||
|
'$lateCount ${stats.late}',
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
Color textColor;
|
Color textColor;
|
||||||
IconData icon;
|
IconData icon;
|
||||||
String statusText;
|
String statusText;
|
||||||
Color badgeBg;
|
|
||||||
Color badgeText;
|
|
||||||
Color badgeBorder;
|
|
||||||
String badgeLabel;
|
|
||||||
|
|
||||||
switch (worker.status) {
|
switch (worker.status) {
|
||||||
case AssignmentStatus.checkedIn:
|
case AssignmentStatus.checkedIn:
|
||||||
@@ -66,10 +62,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
statusText = l10n.status_checked_in_at(
|
statusText = l10n.status_checked_in_at(
|
||||||
time: _formatCheckInTime(worker.checkInAt),
|
time: _formatCheckInTime(worker.checkInAt),
|
||||||
);
|
);
|
||||||
badgeBg = UiColors.textSuccess.withAlpha(40);
|
|
||||||
badgeText = UiColors.textSuccess;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_on_site;
|
|
||||||
case AssignmentStatus.accepted:
|
case AssignmentStatus.accepted:
|
||||||
if (worker.checkInAt == null) {
|
if (worker.checkInAt == null) {
|
||||||
bg = UiColors.textWarning.withAlpha(26);
|
bg = UiColors.textWarning.withAlpha(26);
|
||||||
@@ -78,10 +70,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.textWarning;
|
textColor = UiColors.textWarning;
|
||||||
icon = UiIcons.clock;
|
icon = UiIcons.clock;
|
||||||
statusText = l10n.status_en_route_expected(time: shiftStartTime);
|
statusText = l10n.status_en_route_expected(time: shiftStartTime);
|
||||||
badgeBg = UiColors.textWarning.withAlpha(40);
|
|
||||||
badgeText = UiColors.textWarning;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_en_route;
|
|
||||||
} else {
|
} else {
|
||||||
bg = UiColors.muted.withAlpha(26);
|
bg = UiColors.muted.withAlpha(26);
|
||||||
border = UiColors.border;
|
border = UiColors.border;
|
||||||
@@ -89,10 +77,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.textSecondary;
|
textColor = UiColors.textSecondary;
|
||||||
icon = UiIcons.success;
|
icon = UiIcons.success;
|
||||||
statusText = l10n.status_confirmed;
|
statusText = l10n.status_confirmed;
|
||||||
badgeBg = UiColors.textSecondary.withAlpha(40);
|
|
||||||
badgeText = UiColors.textSecondary;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_confirmed;
|
|
||||||
}
|
}
|
||||||
case AssignmentStatus.noShow:
|
case AssignmentStatus.noShow:
|
||||||
bg = UiColors.destructive.withAlpha(26);
|
bg = UiColors.destructive.withAlpha(26);
|
||||||
@@ -101,10 +85,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.destructive;
|
textColor = UiColors.destructive;
|
||||||
icon = UiIcons.warning;
|
icon = UiIcons.warning;
|
||||||
statusText = l10n.status_no_show;
|
statusText = l10n.status_no_show;
|
||||||
badgeBg = UiColors.destructive.withAlpha(40);
|
|
||||||
badgeText = UiColors.destructive;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_no_show;
|
|
||||||
case AssignmentStatus.checkedOut:
|
case AssignmentStatus.checkedOut:
|
||||||
bg = UiColors.muted.withAlpha(26);
|
bg = UiColors.muted.withAlpha(26);
|
||||||
border = UiColors.border;
|
border = UiColors.border;
|
||||||
@@ -112,10 +92,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.textSecondary;
|
textColor = UiColors.textSecondary;
|
||||||
icon = UiIcons.success;
|
icon = UiIcons.success;
|
||||||
statusText = l10n.status_checked_out;
|
statusText = l10n.status_checked_out;
|
||||||
badgeBg = UiColors.textSecondary.withAlpha(40);
|
|
||||||
badgeText = UiColors.textSecondary;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_done;
|
|
||||||
case AssignmentStatus.completed:
|
case AssignmentStatus.completed:
|
||||||
bg = UiColors.iconSuccess.withAlpha(26);
|
bg = UiColors.iconSuccess.withAlpha(26);
|
||||||
border = UiColors.iconSuccess;
|
border = UiColors.iconSuccess;
|
||||||
@@ -123,10 +99,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.textSuccess;
|
textColor = UiColors.textSuccess;
|
||||||
icon = UiIcons.success;
|
icon = UiIcons.success;
|
||||||
statusText = l10n.status_completed;
|
statusText = l10n.status_completed;
|
||||||
badgeBg = UiColors.textSuccess.withAlpha(40);
|
|
||||||
badgeText = UiColors.textSuccess;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = l10n.status_completed;
|
|
||||||
case AssignmentStatus.assigned:
|
case AssignmentStatus.assigned:
|
||||||
case AssignmentStatus.swapRequested:
|
case AssignmentStatus.swapRequested:
|
||||||
case AssignmentStatus.cancelled:
|
case AssignmentStatus.cancelled:
|
||||||
@@ -137,10 +109,6 @@ class WorkerRow extends StatelessWidget {
|
|||||||
textColor = UiColors.textSecondary;
|
textColor = UiColors.textSecondary;
|
||||||
icon = UiIcons.clock;
|
icon = UiIcons.clock;
|
||||||
statusText = worker.status.value;
|
statusText = worker.status.value;
|
||||||
badgeBg = UiColors.textSecondary.withAlpha(40);
|
|
||||||
badgeText = UiColors.textSecondary;
|
|
||||||
badgeBorder = badgeText;
|
|
||||||
badgeLabel = worker.status.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -213,88 +181,23 @@ class WorkerRow extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
spacing: UiConstants.space2,
|
spacing: UiConstants.space2,
|
||||||
children: <Widget>[
|
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 (showRateButton && onRate != null)
|
if (showRateButton && onRate != null)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onRate,
|
onTap: onRate,
|
||||||
child: Container(
|
child: UiChip(
|
||||||
padding: const EdgeInsets.symmetric(
|
label: l10n.actions.rate,
|
||||||
horizontal: UiConstants.space2,
|
size: UiChipSize.small,
|
||||||
vertical: UiConstants.space1 / 2,
|
leadingIcon: UiIcons.star,
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.primary.withAlpha(26),
|
|
||||||
borderRadius: UiConstants.radiusMd,
|
|
||||||
border: Border.all(color: UiColors.primary, width: 0.5),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: UiConstants.space1,
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(
|
|
||||||
UiIcons.star,
|
|
||||||
size: 12,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.actions.rate,
|
|
||||||
style: UiTypography.footnote2b.copyWith(
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (!showCancelButton && onCancel != null)
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showCancelButton && onCancel != null)
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onCancel,
|
onTap: onCancel,
|
||||||
child: Container(
|
child: UiChip(
|
||||||
padding: const EdgeInsets.symmetric(
|
label: l10n.actions.cancel,
|
||||||
horizontal: UiConstants.space2,
|
size: UiChipSize.small,
|
||||||
vertical: UiConstants.space1 / 2,
|
leadingIcon: UiIcons.close,
|
||||||
),
|
variant: UiChipVariant.destructive,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.destructive.withAlpha(26),
|
|
||||||
borderRadius: UiConstants.radiusMd,
|
|
||||||
border: Border.all(
|
|
||||||
color: UiColors.destructive,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: UiConstants.space1,
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(
|
|
||||||
UiIcons.close,
|
|
||||||
size: 12,
|
|
||||||
color: UiColors.destructive,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.actions.cancel,
|
|
||||||
style: UiTypography.footnote2b.copyWith(
|
|
||||||
color: UiColors.destructive,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user