feat: Add cancellation reason handling and display in shift details
This commit is contained in:
@@ -1365,7 +1365,8 @@
|
|||||||
"shift_accepted": "Shift accepted successfully!",
|
"shift_accepted": "Shift accepted successfully!",
|
||||||
"shift_declined_success": "Shift declined",
|
"shift_declined_success": "Shift declined",
|
||||||
"complete_account_title": "Complete Your Account",
|
"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": {
|
"my_shift_card": {
|
||||||
"submit_for_approval": "Submit for Approval",
|
"submit_for_approval": "Submit for Approval",
|
||||||
|
|||||||
@@ -1360,7 +1360,8 @@
|
|||||||
"shift_accepted": "¡Turno aceptado con éxito!",
|
"shift_accepted": "¡Turno aceptado con éxito!",
|
||||||
"shift_declined_success": "Turno rechazado",
|
"shift_declined_success": "Turno rechazado",
|
||||||
"complete_account_title": "Completa Tu Cuenta",
|
"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": {
|
"my_shift_card": {
|
||||||
"submit_for_approval": "Enviar para Aprobación",
|
"submit_for_approval": "Enviar para Aprobación",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class ShiftDetail extends Equatable {
|
|||||||
required this.allowClockInOverride,
|
required this.allowClockInOverride,
|
||||||
this.geofenceRadiusMeters,
|
this.geofenceRadiusMeters,
|
||||||
this.nfcTagId,
|
this.nfcTagId,
|
||||||
|
this.cancellationReason,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Deserialises from the V2 API JSON response.
|
/// Deserialises from the V2 API JSON response.
|
||||||
@@ -76,6 +77,7 @@ class ShiftDetail extends Equatable {
|
|||||||
allowClockInOverride: json['allowClockInOverride'] as bool? ?? false,
|
allowClockInOverride: json['allowClockInOverride'] as bool? ?? false,
|
||||||
geofenceRadiusMeters: json['geofenceRadiusMeters'] as int?,
|
geofenceRadiusMeters: json['geofenceRadiusMeters'] as int?,
|
||||||
nfcTagId: json['nfcTagId'] as String?,
|
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.
|
/// NFC tag identifier for NFC-based clock-in.
|
||||||
final String? nfcTagId;
|
final String? nfcTagId;
|
||||||
|
|
||||||
|
/// Reason the shift was cancelled, if applicable.
|
||||||
|
final String? cancellationReason;
|
||||||
|
|
||||||
/// Duration of the shift in hours.
|
/// Duration of the shift in hours.
|
||||||
double get durationHours {
|
double get durationHours {
|
||||||
return endTime.difference(startTime).inMinutes / 60;
|
return endTime.difference(startTime).inMinutes / 60;
|
||||||
@@ -194,6 +199,7 @@ class ShiftDetail extends Equatable {
|
|||||||
'allowClockInOverride': allowClockInOverride,
|
'allowClockInOverride': allowClockInOverride,
|
||||||
'geofenceRadiusMeters': geofenceRadiusMeters,
|
'geofenceRadiusMeters': geofenceRadiusMeters,
|
||||||
'nfcTagId': nfcTagId,
|
'nfcTagId': nfcTagId,
|
||||||
|
'cancellationReason': cancellationReason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,5 +231,6 @@ class ShiftDetail extends Equatable {
|
|||||||
allowClockInOverride,
|
allowClockInOverride,
|
||||||
geofenceRadiusMeters,
|
geofenceRadiusMeters,
|
||||||
nfcTagId,
|
nfcTagId,
|
||||||
|
cancellationReason,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/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_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/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_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/shift_details_header.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/shift_details_page_skeleton.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,
|
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),
|
ShiftDetailsHeader(detail: detail),
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
ShiftStatsRow(
|
ShiftStatsRow(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user