diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 6a2c8eea..b47259ac 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -978,12 +978,15 @@ "retry": "Retry", "clock_in_anyway": "Clock In Anyway", "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_submit": "Clock In", + "override_submit": "Submit", "overridden_title": "Location Not Verified", - "overridden_desc": "You are clocking in without location verification. Your justification has been recorded.", - "outside_work_area_warning": "You've moved away from the work area" + "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_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": { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 710a935f..79fea836 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -973,12 +973,15 @@ "retry": "Reintentar", "clock_in_anyway": "Registrar Entrada", "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_submit": "Registrar Entrada", + "override_submit": "Enviar", "overridden_title": "Ubicación No Verificada", - "overridden_desc": "Está registrando entrada sin verificación de ubicación. Su justificación ha sido registrada.", - "outside_work_area_warning": "Te has alejado del área de trabajo" + "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_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": { diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in/clock_in_bloc.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in/clock_in_bloc.dart index 9a6e9f06..9c107915 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in/clock_in_bloc.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in/clock_in_bloc.dart @@ -291,6 +291,9 @@ class ClockInBloc extends Bloc longitude: location?.longitude, accuracyMeters: location?.accuracy, capturedAt: location?.timestamp, + overrideReason: currentGeofence.isGeofenceOverridden + ? currentGeofence.overrideNotes + : null, ), ); emit(state.copyWith( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart index 49a44979..36b4a446 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/clock_in_action_section.dart @@ -134,8 +134,9 @@ class ClockInActionSection extends StatelessWidget { final bool hasCoordinates = selectedShift?.latitude != null && selectedShift?.longitude != null; - // Geofence only gates clock-in, never clock-out. When already - // checked in the swipe must always be enabled for checkout. + // Geofence gates both clock-in and clock-out. When outside the + // geofence, the slider is locked until the worker provides a + // justification via the override modal. final bool isGeofenceBlocking = hasCoordinates && !geofenceState.isLocationVerified && diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_status_banner.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_status_banner.dart index f6c56f17..e129198e 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_status_banner.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/geofence_status_banner.dart @@ -91,7 +91,9 @@ class GeofenceStatusBanner extends StatelessWidget { // instead of the "Clock in anyway" override flow so the clock-out // slider remains accessible. if (isClockedIn) { - return const OutsideWorkAreaBanner(); + return OutsideWorkAreaBanner( + distanceMeters: state.distanceFromTarget!, + ); } return TooFarBanner(distanceMeters: state.distanceFromTarget!); } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/outside_work_area_banner.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/outside_work_area_banner.dart index 392cd716..84852567 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/outside_work_area_banner.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/geofence_status_banner/outside_work_area_banner.dart @@ -1,15 +1,23 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:krow_core/core.dart'; -/// Non-blocking warning banner shown when the worker is already clocked in -/// but has moved outside the geofence radius. +import 'banner_action_button.dart'; +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" -/// action button and does not block the clock-out slider. +/// Mirrors [TooFarBanner] with a "Clock out anyway" action that opens the +/// [GeofenceOverrideModal] so the worker can provide justification before +/// the clock-out slider unlocks. class OutsideWorkAreaBanner extends StatelessWidget { /// 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 Widget build(BuildContext context) { @@ -21,8 +29,17 @@ class OutsideWorkAreaBanner extends StatelessWidget { backgroundColor: UiColors.tagPending, icon: UiIcons.warning, iconColor: UiColors.textWarning, - title: i18n.outside_work_area_warning, + title: i18n.outside_work_area_title, 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), + ), ); } }