From a85cd369756d874b604e0670887f4b31b24e321a Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 13 Mar 2026 17:11:14 -0400 Subject: [PATCH] feat: Implement geofence override approval with justification notes and update related state management --- .../src/presentation/bloc/geofence_bloc.dart | 13 ++++++++ .../src/presentation/bloc/geofence_event.dart | 12 ++++++++ .../src/presentation/bloc/geofence_state.dart | 14 +++++++++ .../widgets/clock_in_action_section.dart | 9 ++++-- .../geofence_override_modal.dart | 30 ++++++------------- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_bloc.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_bloc.dart index f9f171ab..ad5154a3 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_bloc.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_bloc.dart @@ -33,6 +33,7 @@ class GeofenceBloc extends Bloc on(_onRetry); on(_onBackgroundTrackingStarted); on(_onBackgroundTrackingStopped); + on(_onOverrideApproved); on(_onStopped); } /// The geofence service for foreground proximity checks. @@ -240,6 +241,18 @@ class GeofenceBloc extends Bloc ); } + /// Handles the [GeofenceOverrideApproved] event by storing the override + /// flag and justification notes, enabling the swipe slider. + void _onOverrideApproved( + GeofenceOverrideApproved event, + Emitter emit, + ) { + emit(state.copyWith( + isGeofenceOverridden: true, + overrideNotes: event.notes, + )); + } + /// Handles the [GeofenceStopped] event by cancelling all subscriptions /// and resetting the state. Future _onStopped( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_event.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_event.dart index f4c68d50..e88b8463 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_event.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_event.dart @@ -99,6 +99,18 @@ class BackgroundTrackingStopped extends GeofenceEvent { const BackgroundTrackingStopped(); } +/// Worker approved geofence override by providing justification notes. +class GeofenceOverrideApproved extends GeofenceEvent { + /// The justification notes provided by the worker. + final String notes; + + /// Creates a [GeofenceOverrideApproved] event. + const GeofenceOverrideApproved({required this.notes}); + + @override + List get props => [notes]; +} + /// Stops all geofence monitoring (foreground and background). class GeofenceStopped extends GeofenceEvent { /// Creates a [GeofenceStopped] event. diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_state.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_state.dart index ff343569..080e5a75 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_state.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/geofence_state.dart @@ -14,6 +14,8 @@ class GeofenceState extends Equatable { this.isLocationTimedOut = false, this.isVerifying = false, this.isBackgroundTrackingActive = false, + this.isGeofenceOverridden = false, + this.overrideNotes, this.targetLat, this.targetLng, }); @@ -41,6 +43,12 @@ class GeofenceState extends Equatable { /// Whether background tracking is active. final bool isBackgroundTrackingActive; + /// Whether the worker has overridden the geofence check via justification. + final bool isGeofenceOverridden; + + /// Justification notes provided when overriding the geofence. + final String? overrideNotes; + /// Target latitude being monitored. final double? targetLat; @@ -60,6 +68,8 @@ class GeofenceState extends Equatable { bool? isLocationTimedOut, bool? isVerifying, bool? isBackgroundTrackingActive, + bool? isGeofenceOverridden, + String? overrideNotes, double? targetLat, double? targetLng, }) { @@ -74,6 +84,8 @@ class GeofenceState extends Equatable { isVerifying: isVerifying ?? this.isVerifying, isBackgroundTrackingActive: isBackgroundTrackingActive ?? this.isBackgroundTrackingActive, + isGeofenceOverridden: isGeofenceOverridden ?? this.isGeofenceOverridden, + overrideNotes: overrideNotes ?? this.overrideNotes, targetLat: targetLat ?? this.targetLat, targetLng: targetLng ?? this.targetLng, ); @@ -89,6 +101,8 @@ class GeofenceState extends Equatable { isLocationTimedOut, isVerifying, isBackgroundTrackingActive, + isGeofenceOverridden, + overrideNotes, targetLat, targetLng, ]; diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart index d2e4436f..758dbdea 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart @@ -120,10 +120,11 @@ class ClockInActionSection extends StatelessWidget { selectedShift?.longitude != null; // Disable swipe when the shift has coordinates and the user is - // not verified and the timeout has not been reached. + // not verified, not timed out, and has not overridden the geofence. final bool isGeofenceBlocking = hasCoordinates && !geofenceState.isLocationVerified && - !geofenceState.isLocationTimedOut; + !geofenceState.isLocationTimedOut && + !geofenceState.isGeofenceOverridden; return Column( mainAxisSize: MainAxisSize.min, @@ -155,8 +156,10 @@ class ClockInActionSection extends StatelessWidget { ReadContext(context).read().add( CheckInRequested( shiftId: selectedShift!.id, + notes: geofenceState.overrideNotes, isLocationVerified: geofenceState.isLocationVerified, isLocationTimedOut: geofenceState.isLocationTimedOut, + isGeofenceOverridden: geofenceState.isGeofenceOverridden, ), ); } @@ -164,8 +167,10 @@ class ClockInActionSection extends StatelessWidget { ReadContext(context).read().add( CheckInRequested( shiftId: selectedShift!.id, + notes: geofenceState.overrideNotes, isLocationVerified: geofenceState.isLocationVerified, isLocationTimedOut: geofenceState.isLocationTimedOut, + isGeofenceOverridden: geofenceState.isGeofenceOverridden, ), ); } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_override_modal.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_override_modal.dart index 1164261b..43778d2d 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_override_modal.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_override_modal.dart @@ -5,9 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; -import '../../bloc/clock_in_bloc.dart'; -import '../../bloc/clock_in_event.dart'; -import '../../bloc/clock_in_state.dart'; +import '../../bloc/geofence_bloc.dart'; +import '../../bloc/geofence_event.dart'; /// Modal bottom sheet that collects a justification note before allowing /// a geofence-overridden clock-in. @@ -31,8 +30,8 @@ class GeofenceOverrideModal extends StatefulWidget { top: Radius.circular(UiConstants.space4), ), ), - builder: (_) => BlocProvider.value( - value: ReadContext(context).read(), + builder: (_) => BlocProvider.value( + value: ReadContext(context).read(), child: const GeofenceOverrideModal(), ), ); @@ -74,9 +73,7 @@ class _GeofenceOverrideModalState extends State { const SizedBox(height: UiConstants.space2), Text( i18n.override_desc, - style: UiTypography.body2r.copyWith( - color: UiColors.textSecondary, - ), + style: UiTypography.body2r.textSecondary, ), const SizedBox(height: UiConstants.space4), UiTextField( @@ -104,23 +101,14 @@ class _GeofenceOverrideModalState extends State { ); } - /// Dispatches the clock-in event with the override flag and justification, - /// then closes the modal. + /// Stores the override justification in GeofenceBloc state (enabling the + /// swipe slider), then closes the modal. void _submit(BuildContext context) { final String justification = _controller.text.trim(); if (justification.isEmpty) return; - final ClockInState clockInState = - ReadContext(context).read().state; - final String? shiftId = clockInState.selectedShift?.id; - if (shiftId == null) return; - - ReadContext(context).read().add( - CheckInRequested( - shiftId: shiftId, - notes: justification, - isGeofenceOverridden: true, - ), + ReadContext(context).read().add( + GeofenceOverrideApproved(notes: justification), ); Modular.to.popSafe();