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<BackgroundTrackingStarted>(_onBackgroundTrackingStarted);
on<BackgroundTrackingStopped>(_onBackgroundTrackingStopped);
on<GeofenceOverrideApproved>(_onOverrideApproved);
on<GeofenceStopped>(_onStopped);
}
/// 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
/// and resetting the state.
Future<void> _onStopped(

View File

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

View File

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

View File

@@ -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<ClockInBloc>().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<ClockInBloc>().add(
CheckInRequested(
shiftId: selectedShift!.id,
notes: geofenceState.overrideNotes,
isLocationVerified: geofenceState.isLocationVerified,
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: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<ClockInBloc>.value(
value: ReadContext(context).read<ClockInBloc>(),
builder: (_) => BlocProvider<GeofenceBloc>.value(
value: ReadContext(context).read<GeofenceBloc>(),
child: const GeofenceOverrideModal(),
),
);
@@ -74,9 +73,7 @@ class _GeofenceOverrideModalState extends State<GeofenceOverrideModal> {
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<GeofenceOverrideModal> {
);
}
/// 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<ClockInBloc>().state;
final String? shiftId = clockInState.selectedShift?.id;
if (shiftId == null) return;
ReadContext(context).read<ClockInBloc>().add(
CheckInRequested(
shiftId: shiftId,
notes: justification,
isGeofenceOverridden: true,
),
ReadContext(context).read<GeofenceBloc>().add(
GeofenceOverrideApproved(notes: justification),
);
Modular.to.popSafe();