feat(clock_in): add error handling support to check-in interactions

This commit is contained in:
Achintha Isuru
2026-03-14 20:21:10 -04:00
parent e93f7f7004
commit 10bd61b250
6 changed files with 36 additions and 21 deletions

View File

@@ -16,6 +16,7 @@ abstract class CheckInInteraction {
required bool isCheckedIn, required bool isCheckedIn,
required bool isDisabled, required bool isDisabled,
required bool isLoading, required bool isLoading,
required bool hasError,
required VoidCallback onCheckIn, required VoidCallback onCheckIn,
required VoidCallback onCheckOut, required VoidCallback onCheckOut,
}); });

View File

@@ -21,6 +21,7 @@ class NfcCheckInInteraction implements CheckInInteraction {
required bool isCheckedIn, required bool isCheckedIn,
required bool isDisabled, required bool isDisabled,
required bool isLoading, required bool isLoading,
required bool hasError,
required VoidCallback onCheckIn, required VoidCallback onCheckIn,
required VoidCallback onCheckOut, required VoidCallback onCheckOut,
}) { }) {

View File

@@ -16,6 +16,7 @@ class SwipeCheckInInteraction implements CheckInInteraction {
required bool isCheckedIn, required bool isCheckedIn,
required bool isDisabled, required bool isDisabled,
required bool isLoading, required bool isLoading,
required bool hasError,
required VoidCallback onCheckIn, required VoidCallback onCheckIn,
required VoidCallback onCheckOut, required VoidCallback onCheckOut,
}) { }) {
@@ -23,6 +24,7 @@ class SwipeCheckInInteraction implements CheckInInteraction {
isCheckedIn: isCheckedIn, isCheckedIn: isCheckedIn,
isDisabled: isDisabled, isDisabled: isDisabled,
isLoading: isLoading, isLoading: isLoading,
hasError: hasError,
onCheckIn: onCheckIn, onCheckIn: onCheckIn,
onCheckOut: onCheckOut, onCheckOut: onCheckOut,
); );

View File

@@ -6,16 +6,15 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/validators/clock_in_validation_context.dart';
import '../../domain/validators/validators/time_window_validator.dart';
import '../bloc/clock_in/clock_in_bloc.dart'; import '../bloc/clock_in/clock_in_bloc.dart';
import '../bloc/clock_in/clock_in_event.dart'; import '../bloc/clock_in/clock_in_event.dart';
import '../bloc/geofence/geofence_bloc.dart'; import '../bloc/geofence/geofence_bloc.dart';
import '../bloc/geofence/geofence_state.dart'; import '../bloc/geofence/geofence_state.dart';
import '../../domain/validators/clock_in_validation_context.dart';
import '../../domain/validators/validators/time_window_validator.dart';
import '../strategies/check_in_interaction.dart'; import '../strategies/check_in_interaction.dart';
import '../strategies/nfc_check_in_interaction.dart'; import '../strategies/nfc_check_in_interaction.dart';
import '../strategies/swipe_check_in_interaction.dart'; import '../strategies/swipe_check_in_interaction.dart';
import 'early_check_in_banner.dart';
import 'geofence_status_banner/geofence_status_banner.dart'; import 'geofence_status_banner/geofence_status_banner.dart';
import 'lunch_break_modal.dart'; import 'lunch_break_modal.dart';
import 'no_shifts_banner.dart'; import 'no_shifts_banner.dart';
@@ -35,6 +34,7 @@ class ClockInActionSection extends StatelessWidget {
required this.checkOutTime, required this.checkOutTime,
required this.checkInMode, required this.checkInMode,
required this.isActionInProgress, required this.isActionInProgress,
this.hasError = false,
super.key, super.key,
}); });
@@ -60,6 +60,9 @@ class ClockInActionSection extends StatelessWidget {
/// Whether a check-in or check-out action is currently in progress. /// Whether a check-in or check-out action is currently in progress.
final bool isActionInProgress; final bool isActionInProgress;
/// Whether the last action attempt resulted in an error.
final bool hasError;
/// Resolves the [CheckInInteraction] for the current mode. /// Resolves the [CheckInInteraction] for the current mode.
/// ///
/// Falls back to [SwipeCheckInInteraction] if the mode is unrecognized. /// Falls back to [SwipeCheckInInteraction] if the mode is unrecognized.
@@ -81,21 +84,21 @@ class ClockInActionSection extends StatelessWidget {
/// Builds the action widget for an active (not completed) shift. /// Builds the action widget for an active (not completed) shift.
Widget _buildActiveShiftAction(BuildContext context) { Widget _buildActiveShiftAction(BuildContext context) {
if (!isCheckedIn && !_isCheckInAllowed(selectedShift!)) { // if (!isCheckedIn && !_isCheckInAllowed(selectedShift!)) {
return Column( // return Column(
mainAxisSize: MainAxisSize.min, // mainAxisSize: MainAxisSize.min,
children: <Widget>[ // children: <Widget>[
const GeofenceStatusBanner(), // const GeofenceStatusBanner(),
const SizedBox(height: UiConstants.space3), // const SizedBox(height: UiConstants.space3),
EarlyCheckInBanner( // EarlyCheckInBanner(
availabilityTime: _getAvailabilityTimeText( // availabilityTime: _getAvailabilityTimeText(
selectedShift!, // selectedShift!,
context, // context,
), // ),
), // ),
], // ],
); // );
} // }
return BlocBuilder<GeofenceBloc, GeofenceState>( return BlocBuilder<GeofenceBloc, GeofenceState>(
builder: (BuildContext context, GeofenceState geofenceState) { builder: (BuildContext context, GeofenceState geofenceState) {
@@ -119,6 +122,7 @@ class ClockInActionSection extends StatelessWidget {
isCheckedIn: isCheckedIn, isCheckedIn: isCheckedIn,
isDisabled: isGeofenceBlocking, isDisabled: isGeofenceBlocking,
isLoading: isActionInProgress, isLoading: isActionInProgress,
hasError: hasError,
onCheckIn: () => _handleCheckIn(context), onCheckIn: () => _handleCheckIn(context),
onCheckOut: () => _handleCheckOut(context), onCheckOut: () => _handleCheckOut(context),
), ),

View File

@@ -111,6 +111,7 @@ class _ClockInBodyState extends State<ClockInBody> {
checkInMode: state.checkInMode, checkInMode: state.checkInMode,
isActionInProgress: isActionInProgress:
state.status == ClockInStatus.actionInProgress, state.status == ClockInStatus.actionInProgress,
hasError: state.status == ClockInStatus.failure,
), ),
// checked-in banner (only when checked in to the selected shift) // checked-in banner (only when checked in to the selected shift)

View File

@@ -17,6 +17,7 @@ class SwipeToCheckIn extends StatefulWidget {
this.isLoading = false, this.isLoading = false,
this.isCheckedIn = false, this.isCheckedIn = false,
this.isDisabled = false, this.isDisabled = false,
this.hasError = false,
}); });
/// Called when the user completes the swipe to check in. /// Called when the user completes the swipe to check in.
@@ -34,6 +35,9 @@ class SwipeToCheckIn extends StatefulWidget {
/// Whether the slider is disabled (e.g. geofence blocking). /// Whether the slider is disabled (e.g. geofence blocking).
final bool isDisabled; final bool isDisabled;
/// Whether an error occurred during the last action attempt.
final bool hasError;
@override @override
State<SwipeToCheckIn> createState() => _SwipeToCheckInState(); State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
} }
@@ -54,9 +58,11 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
_dragValue = 0.0; _dragValue = 0.0;
}); });
} }
// Reset on error: loading finished but check-in state didn't change. // Reset on error: loading finished without state change, or validation error.
if (oldWidget.isLoading && !widget.isLoading && if (_isComplete &&
widget.isCheckedIn == oldWidget.isCheckedIn && _isComplete) { widget.isCheckedIn == oldWidget.isCheckedIn &&
((oldWidget.isLoading && !widget.isLoading) ||
(!oldWidget.hasError && widget.hasError))) {
setState(() { setState(() {
_isComplete = false; _isComplete = false;
_dragValue = 0.0; _dragValue = 0.0;