feat(geofence): enhance geofence handling with improved messaging and justification for clock-out

This commit is contained in:
Achintha Isuru
2026-03-18 18:30:15 -04:00
parent f488577e6b
commit 2d452f65e6
6 changed files with 46 additions and 17 deletions

View File

@@ -978,12 +978,15 @@
"retry": "Retry", "retry": "Retry",
"clock_in_anyway": "Clock In Anyway", "clock_in_anyway": "Clock In Anyway",
"override_title": "Justification Required", "override_title": "Justification Required",
"override_desc": "Your location could not be verified. Please explain why you are clocking in without location verification.", "override_desc": "Your location could not be verified. Please explain why you are proceeding without location verification.",
"override_hint": "Enter your justification...", "override_hint": "Enter your justification...",
"override_submit": "Clock In", "override_submit": "Submit",
"overridden_title": "Location Not Verified", "overridden_title": "Location Not Verified",
"overridden_desc": "You are clocking in without location verification. Your justification has been recorded.", "overridden_desc": "You are proceeding without location verification. Your justification has been recorded.",
"outside_work_area_warning": "You've moved away from the work area" "outside_work_area_warning": "You've moved away from the work area",
"outside_work_area_title": "You've moved away from the work area",
"outside_work_area_desc": "You are $distance away from your shift location. To clock out, provide a reason below.",
"clock_out_anyway": "Clock out anyway"
} }
}, },
"availability": { "availability": {

View File

@@ -973,12 +973,15 @@
"retry": "Reintentar", "retry": "Reintentar",
"clock_in_anyway": "Registrar Entrada", "clock_in_anyway": "Registrar Entrada",
"override_title": "Justificación Requerida", "override_title": "Justificación Requerida",
"override_desc": "No se pudo verificar su ubicación. Explique por qué registra entrada sin verificación de ubicación.", "override_desc": "No se pudo verificar su ubicación. Explique por qué continúa sin verificación de ubicación.",
"override_hint": "Ingrese su justificación...", "override_hint": "Ingrese su justificación...",
"override_submit": "Registrar Entrada", "override_submit": "Enviar",
"overridden_title": "Ubicación No Verificada", "overridden_title": "Ubicación No Verificada",
"overridden_desc": "Está registrando entrada sin verificación de ubicación. Su justificación ha sido registrada.", "overridden_desc": "Está continuando sin verificación de ubicación. Su justificación ha sido registrada.",
"outside_work_area_warning": "Te has alejado del área de trabajo" "outside_work_area_warning": "Te has alejado del área de trabajo",
"outside_work_area_title": "Te has alejado del área de trabajo",
"outside_work_area_desc": "Estás a $distance de la ubicación de tu turno. Para registrar tu salida, proporciona una razón a continuación.",
"clock_out_anyway": "Registrar salida de todos modos"
} }
}, },
"availability": { "availability": {

View File

@@ -291,6 +291,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
longitude: location?.longitude, longitude: location?.longitude,
accuracyMeters: location?.accuracy, accuracyMeters: location?.accuracy,
capturedAt: location?.timestamp, capturedAt: location?.timestamp,
overrideReason: currentGeofence.isGeofenceOverridden
? currentGeofence.overrideNotes
: null,
), ),
); );
emit(state.copyWith( emit(state.copyWith(

View File

@@ -134,8 +134,9 @@ class ClockInActionSection extends StatelessWidget {
final bool hasCoordinates = final bool hasCoordinates =
selectedShift?.latitude != null && selectedShift?.longitude != null; selectedShift?.latitude != null && selectedShift?.longitude != null;
// Geofence only gates clock-in, never clock-out. When already // Geofence gates both clock-in and clock-out. When outside the
// checked in the swipe must always be enabled for checkout. // geofence, the slider is locked until the worker provides a
// justification via the override modal.
final bool isGeofenceBlocking = final bool isGeofenceBlocking =
hasCoordinates && hasCoordinates &&
!geofenceState.isLocationVerified && !geofenceState.isLocationVerified &&

View File

@@ -91,7 +91,9 @@ class GeofenceStatusBanner extends StatelessWidget {
// instead of the "Clock in anyway" override flow so the clock-out // instead of the "Clock in anyway" override flow so the clock-out
// slider remains accessible. // slider remains accessible.
if (isClockedIn) { if (isClockedIn) {
return const OutsideWorkAreaBanner(); return OutsideWorkAreaBanner(
distanceMeters: state.distanceFromTarget!,
);
} }
return TooFarBanner(distanceMeters: state.distanceFromTarget!); return TooFarBanner(distanceMeters: state.distanceFromTarget!);
} }

View File

@@ -1,15 +1,23 @@
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:krow_core/core.dart';
/// Non-blocking warning banner shown when the worker is already clocked in import 'banner_action_button.dart';
/// but has moved outside the geofence radius. import 'geofence_override_modal.dart';
/// Warning banner shown when the worker is clocked in but has moved outside
/// the geofence radius.
/// ///
/// Unlike [TooFarBanner], this banner does not include a "Clock in anyway" /// Mirrors [TooFarBanner] with a "Clock out anyway" action that opens the
/// action button and does not block the clock-out slider. /// [GeofenceOverrideModal] so the worker can provide justification before
/// the clock-out slider unlocks.
class OutsideWorkAreaBanner extends StatelessWidget { class OutsideWorkAreaBanner extends StatelessWidget {
/// Creates an [OutsideWorkAreaBanner]. /// Creates an [OutsideWorkAreaBanner].
const OutsideWorkAreaBanner({super.key}); const OutsideWorkAreaBanner({required this.distanceMeters, super.key});
/// Distance from the target location in meters.
final double distanceMeters;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -21,8 +29,17 @@ class OutsideWorkAreaBanner extends StatelessWidget {
backgroundColor: UiColors.tagPending, backgroundColor: UiColors.tagPending,
icon: UiIcons.warning, icon: UiIcons.warning,
iconColor: UiColors.textWarning, iconColor: UiColors.textWarning,
title: i18n.outside_work_area_warning, title: i18n.outside_work_area_title,
titleColor: UiColors.textWarning, titleColor: UiColors.textWarning,
description: i18n.outside_work_area_desc(
distance: formatDistance(distanceMeters),
),
descriptionColor: UiColors.textWarning,
action: BannerActionButton(
label: i18n.clock_out_anyway,
color: UiColors.textWarning,
onPressed: () => GeofenceOverrideModal.show(context),
),
); );
} }
} }