From bd2d5610b3ab73939ce646348c5af0e6c35619a2 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 19 Mar 2026 16:36:35 -0400 Subject: [PATCH] feat: Add cancellation reason handling and display in shift details --- .../lib/src/l10n/en.i18n.json | 3 +- .../lib/src/l10n/es.i18n.json | 3 +- .../lib/src/entities/shifts/shift_detail.dart | 7 ++ .../pages/shift_details_page.dart | 10 +++ .../cancellation_reason_banner.dart | 70 +++++++++++++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/cancellation_reason_banner.dart diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 58dc4742..54a98264 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -1365,7 +1365,8 @@ "shift_accepted": "Shift accepted successfully!", "shift_declined_success": "Shift declined", "complete_account_title": "Complete Your Account", - "complete_account_description": "Complete your account to book this shift and start earning" + "complete_account_description": "Complete your account to book this shift and start earning", + "shift_cancelled": "Shift Cancelled" }, "my_shift_card": { "submit_for_approval": "Submit for Approval", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 96fa838f..199f4baf 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -1360,7 +1360,8 @@ "shift_accepted": "¡Turno aceptado con éxito!", "shift_declined_success": "Turno rechazado", "complete_account_title": "Completa Tu Cuenta", - "complete_account_description": "Completa tu cuenta para reservar este turno y comenzar a ganar" + "complete_account_description": "Completa tu cuenta para reservar este turno y comenzar a ganar", + "shift_cancelled": "Turno Cancelado" }, "my_shift_card": { "submit_for_approval": "Enviar para Aprobación", diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart index 38e2dc23..e8aac075 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart @@ -41,6 +41,7 @@ class ShiftDetail extends Equatable { required this.allowClockInOverride, this.geofenceRadiusMeters, this.nfcTagId, + this.cancellationReason, }); /// Deserialises from the V2 API JSON response. @@ -76,6 +77,7 @@ class ShiftDetail extends Equatable { allowClockInOverride: json['allowClockInOverride'] as bool? ?? false, geofenceRadiusMeters: json['geofenceRadiusMeters'] as int?, nfcTagId: json['nfcTagId'] as String?, + cancellationReason: json['cancellationReason'] as String?, ); } @@ -157,6 +159,9 @@ class ShiftDetail extends Equatable { /// NFC tag identifier for NFC-based clock-in. final String? nfcTagId; + /// Reason the shift was cancelled, if applicable. + final String? cancellationReason; + /// Duration of the shift in hours. double get durationHours { return endTime.difference(startTime).inMinutes / 60; @@ -194,6 +199,7 @@ class ShiftDetail extends Equatable { 'allowClockInOverride': allowClockInOverride, 'geofenceRadiusMeters': geofenceRadiusMeters, 'nfcTagId': nfcTagId, + 'cancellationReason': cancellationReason, }; } @@ -225,5 +231,6 @@ class ShiftDetail extends Equatable { allowClockInOverride, geofenceRadiusMeters, nfcTagId, + cancellationReason, ]; } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 26f72047..4d3d8d85 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -12,6 +12,7 @@ import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_ import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_state.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_date_time_section.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_description_section.dart'; +import 'package:staff_shifts/src/presentation/widgets/shift_details/cancellation_reason_banner.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_details_header.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_details_page_skeleton.dart'; @@ -117,6 +118,15 @@ class _ShiftDetailsPageState extends State { icon: UiIcons.sparkles, ), ), + if (detail.assignmentStatus == + AssignmentStatus.cancelled && + detail.cancellationReason != null && + detail.cancellationReason!.isNotEmpty) + CancellationReasonBanner( + reason: detail.cancellationReason!, + titleLabel: context.t.staff_shifts.shift_details + .shift_cancelled, + ), ShiftDetailsHeader(detail: detail), const Divider(height: 1, thickness: 0.5), ShiftStatsRow( diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/cancellation_reason_banner.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/cancellation_reason_banner.dart new file mode 100644 index 00000000..7550b07e --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/cancellation_reason_banner.dart @@ -0,0 +1,70 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A banner displaying the cancellation reason for a cancelled shift. +/// +/// Uses error styling to draw attention to the cancellation without being +/// overly alarming. Shown at the top of the shift details page when the +/// shift has been cancelled with a reason. +class CancellationReasonBanner extends StatelessWidget { + /// Creates a [CancellationReasonBanner]. + const CancellationReasonBanner({ + super.key, + required this.reason, + required this.titleLabel, + }); + + /// The cancellation reason text. + final String reason; + + /// Localized title label (e.g., "Shift Cancelled"). + final String titleLabel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space4, + ), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.tagError, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all( + color: UiColors.error.withValues(alpha: 0.3), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + UiIcons.error, + color: UiColors.error, + size: UiConstants.iconMd, + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titleLabel, + style: UiTypography.body2b.copyWith(color: UiColors.error), + ), + const SizedBox(height: UiConstants.space1), + Text( + reason, + style: UiTypography.body3r.textPrimary, + ), + ], + ), + ), + ], + ), + ), + ); + } +}