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<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(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user