feat: Add cancellation reason handling and display in shift details

This commit is contained in:
Achintha Isuru
2026-03-19 16:36:35 -04:00
parent 56253893ed
commit bd2d5610b3
5 changed files with 91 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@@ -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<ShiftDetailsPage> {
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(

View File

@@ -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: <Widget>[
const Icon(
UiIcons.error,
color: UiColors.error,
size: UiConstants.iconMd,
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
titleLabel,
style: UiTypography.body2b.copyWith(color: UiColors.error),
),
const SizedBox(height: UiConstants.space1),
Text(
reason,
style: UiTypography.body3r.textPrimary,
),
],
),
),
],
),
),
);
}
}