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:
Achintha Isuru
2026-03-19 00:12:35 -04:00
parent b317c53e7e
commit 8bb336d1b5
14 changed files with 617 additions and 540 deletions

View File

@@ -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)": {

View File

@@ -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)": {

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),
], ],
), ),
); );

View File

@@ -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),
),
),
], ],
), ),
), ),

View File

@@ -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

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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(),
); );
} }
} }

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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,
),
), ),
), ),
], ],
), ),
],
), ),
); );
} }

View File

@@ -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,
),
], ],
), ),
),
); );
} }
} }

View File

@@ -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,
),
),
],
),
),
); );
} }
} }

View File

@@ -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,
),
),
],
),
), ),
), ),
], ],