feat: Implement geofence override approval with justification notes and update related state management

This commit is contained in:
Achintha Isuru
2026-03-13 17:11:14 -04:00
parent ab1cd8c355
commit a85cd36975
5 changed files with 55 additions and 23 deletions

View File

@@ -33,6 +33,7 @@ class GeofenceBloc extends Bloc<GeofenceEvent, GeofenceState>
on<GeofenceRetryRequested>(_onRetry); on<GeofenceRetryRequested>(_onRetry);
on<BackgroundTrackingStarted>(_onBackgroundTrackingStarted); on<BackgroundTrackingStarted>(_onBackgroundTrackingStarted);
on<BackgroundTrackingStopped>(_onBackgroundTrackingStopped); on<BackgroundTrackingStopped>(_onBackgroundTrackingStopped);
on<GeofenceOverrideApproved>(_onOverrideApproved);
on<GeofenceStopped>(_onStopped); on<GeofenceStopped>(_onStopped);
} }
/// The geofence service for foreground proximity checks. /// The geofence service for foreground proximity checks.
@@ -240,6 +241,18 @@ class GeofenceBloc extends Bloc<GeofenceEvent, GeofenceState>
); );
} }
/// Handles the [GeofenceOverrideApproved] event by storing the override
/// flag and justification notes, enabling the swipe slider.
void _onOverrideApproved(
GeofenceOverrideApproved event,
Emitter<GeofenceState> emit,
) {
emit(state.copyWith(
isGeofenceOverridden: true,
overrideNotes: event.notes,
));
}
/// Handles the [GeofenceStopped] event by cancelling all subscriptions /// Handles the [GeofenceStopped] event by cancelling all subscriptions
/// and resetting the state. /// and resetting the state.
Future<void> _onStopped( Future<void> _onStopped(

View File

@@ -99,6 +99,18 @@ class BackgroundTrackingStopped extends GeofenceEvent {
const BackgroundTrackingStopped(); 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<Object?> get props => <Object?>[notes];
}
/// Stops all geofence monitoring (foreground and background). /// Stops all geofence monitoring (foreground and background).
class GeofenceStopped extends GeofenceEvent { class GeofenceStopped extends GeofenceEvent {
/// Creates a [GeofenceStopped] event. /// Creates a [GeofenceStopped] event.

View File

@@ -14,6 +14,8 @@ class GeofenceState extends Equatable {
this.isLocationTimedOut = false, this.isLocationTimedOut = false,
this.isVerifying = false, this.isVerifying = false,
this.isBackgroundTrackingActive = false, this.isBackgroundTrackingActive = false,
this.isGeofenceOverridden = false,
this.overrideNotes,
this.targetLat, this.targetLat,
this.targetLng, this.targetLng,
}); });
@@ -41,6 +43,12 @@ class GeofenceState extends Equatable {
/// Whether background tracking is active. /// Whether background tracking is active.
final bool isBackgroundTrackingActive; 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. /// Target latitude being monitored.
final double? targetLat; final double? targetLat;
@@ -60,6 +68,8 @@ class GeofenceState extends Equatable {
bool? isLocationTimedOut, bool? isLocationTimedOut,
bool? isVerifying, bool? isVerifying,
bool? isBackgroundTrackingActive, bool? isBackgroundTrackingActive,
bool? isGeofenceOverridden,
String? overrideNotes,
double? targetLat, double? targetLat,
double? targetLng, double? targetLng,
}) { }) {
@@ -74,6 +84,8 @@ class GeofenceState extends Equatable {
isVerifying: isVerifying ?? this.isVerifying, isVerifying: isVerifying ?? this.isVerifying,
isBackgroundTrackingActive: isBackgroundTrackingActive:
isBackgroundTrackingActive ?? this.isBackgroundTrackingActive, isBackgroundTrackingActive ?? this.isBackgroundTrackingActive,
isGeofenceOverridden: isGeofenceOverridden ?? this.isGeofenceOverridden,
overrideNotes: overrideNotes ?? this.overrideNotes,
targetLat: targetLat ?? this.targetLat, targetLat: targetLat ?? this.targetLat,
targetLng: targetLng ?? this.targetLng, targetLng: targetLng ?? this.targetLng,
); );
@@ -89,6 +101,8 @@ class GeofenceState extends Equatable {
isLocationTimedOut, isLocationTimedOut,
isVerifying, isVerifying,
isBackgroundTrackingActive, isBackgroundTrackingActive,
isGeofenceOverridden,
overrideNotes,
targetLat, targetLat,
targetLng, targetLng,
]; ];

View File

@@ -120,10 +120,11 @@ class ClockInActionSection extends StatelessWidget {
selectedShift?.longitude != null; selectedShift?.longitude != null;
// Disable swipe when the shift has coordinates and the user is // 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 && final bool isGeofenceBlocking = hasCoordinates &&
!geofenceState.isLocationVerified && !geofenceState.isLocationVerified &&
!geofenceState.isLocationTimedOut; !geofenceState.isLocationTimedOut &&
!geofenceState.isGeofenceOverridden;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -155,8 +156,10 @@ class ClockInActionSection extends StatelessWidget {
ReadContext(context).read<ClockInBloc>().add( ReadContext(context).read<ClockInBloc>().add(
CheckInRequested( CheckInRequested(
shiftId: selectedShift!.id, shiftId: selectedShift!.id,
notes: geofenceState.overrideNotes,
isLocationVerified: geofenceState.isLocationVerified, isLocationVerified: geofenceState.isLocationVerified,
isLocationTimedOut: geofenceState.isLocationTimedOut, isLocationTimedOut: geofenceState.isLocationTimedOut,
isGeofenceOverridden: geofenceState.isGeofenceOverridden,
), ),
); );
} }
@@ -164,8 +167,10 @@ class ClockInActionSection extends StatelessWidget {
ReadContext(context).read<ClockInBloc>().add( ReadContext(context).read<ClockInBloc>().add(
CheckInRequested( CheckInRequested(
shiftId: selectedShift!.id, shiftId: selectedShift!.id,
notes: geofenceState.overrideNotes,
isLocationVerified: geofenceState.isLocationVerified, isLocationVerified: geofenceState.isLocationVerified,
isLocationTimedOut: geofenceState.isLocationTimedOut, isLocationTimedOut: geofenceState.isLocationTimedOut,
isGeofenceOverridden: geofenceState.isGeofenceOverridden,
), ),
); );
} }

View File

@@ -5,9 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import '../../bloc/clock_in_bloc.dart'; import '../../bloc/geofence_bloc.dart';
import '../../bloc/clock_in_event.dart'; import '../../bloc/geofence_event.dart';
import '../../bloc/clock_in_state.dart';
/// Modal bottom sheet that collects a justification note before allowing /// Modal bottom sheet that collects a justification note before allowing
/// a geofence-overridden clock-in. /// a geofence-overridden clock-in.
@@ -31,8 +30,8 @@ class GeofenceOverrideModal extends StatefulWidget {
top: Radius.circular(UiConstants.space4), top: Radius.circular(UiConstants.space4),
), ),
), ),
builder: (_) => BlocProvider<ClockInBloc>.value( builder: (_) => BlocProvider<GeofenceBloc>.value(
value: ReadContext(context).read<ClockInBloc>(), value: ReadContext(context).read<GeofenceBloc>(),
child: const GeofenceOverrideModal(), child: const GeofenceOverrideModal(),
), ),
); );
@@ -74,9 +73,7 @@ class _GeofenceOverrideModalState extends State<GeofenceOverrideModal> {
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Text( Text(
i18n.override_desc, i18n.override_desc,
style: UiTypography.body2r.copyWith( style: UiTypography.body2r.textSecondary,
color: UiColors.textSecondary,
),
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
UiTextField( UiTextField(
@@ -104,23 +101,14 @@ class _GeofenceOverrideModalState extends State<GeofenceOverrideModal> {
); );
} }
/// Dispatches the clock-in event with the override flag and justification, /// Stores the override justification in GeofenceBloc state (enabling the
/// then closes the modal. /// swipe slider), then closes the modal.
void _submit(BuildContext context) { void _submit(BuildContext context) {
final String justification = _controller.text.trim(); final String justification = _controller.text.trim();
if (justification.isEmpty) return; if (justification.isEmpty) return;
final ClockInState clockInState = ReadContext(context).read<GeofenceBloc>().add(
ReadContext(context).read<ClockInBloc>().state; GeofenceOverrideApproved(notes: justification),
final String? shiftId = clockInState.selectedShift?.id;
if (shiftId == null) return;
ReadContext(context).read<ClockInBloc>().add(
CheckInRequested(
shiftId: shiftId,
notes: justification,
isGeofenceOverridden: true,
),
); );
Modular.to.popSafe(); Modular.to.popSafe();