feat: Implement geofence override approval with justification notes and update related state management
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user