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 be7f2b3a..5645f0c3 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 @@ -1479,7 +1479,9 @@ }, "clock_in": { "location_verification_required": "Please wait for location verification before clocking in.", - "notes_required_for_timeout": "Please add a note explaining why your location can't be verified." + "notes_required_for_timeout": "Please add a note explaining why your location can't be verified.", + "already_clocked_in": "You're already clocked in to this shift.", + "already_clocked_out": "You've already clocked out of this shift." }, "generic": { "unknown": "Something went wrong. Please try again.", 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 420d0ef6..389f4e87 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 @@ -1474,7 +1474,9 @@ }, "clock_in": { "location_verification_required": "Por favor, espera la verificaci\u00f3n de ubicaci\u00f3n antes de registrar entrada.", - "notes_required_for_timeout": "Por favor, agrega una nota explicando por qu\u00e9 no se puede verificar tu ubicaci\u00f3n." + "notes_required_for_timeout": "Por favor, agrega una nota explicando por qu\u00e9 no se puede verificar tu ubicaci\u00f3n.", + "already_clocked_in": "Ya est\u00e1s registrado en este turno.", + "already_clocked_out": "Ya registraste tu salida de este turno." }, "generic": { "unknown": "Algo sali\u00f3 mal. Por favor, intenta de nuevo.", 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 4661fae4..9a6e9f06 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 @@ -204,29 +204,49 @@ class ClockInBloc extends Bloc action: () async { final DeviceLocation? location = geofenceState.currentLocation; - final AttendanceStatus newStatus = await _clockIn( - ClockInArguments( - shiftId: event.shiftId, - notes: event.notes, - latitude: location?.latitude, - longitude: location?.longitude, - accuracyMeters: location?.accuracy, - capturedAt: location?.timestamp, - overrideReason: geofenceState.isGeofenceOverridden - ? geofenceState.overrideNotes - : null, - ), - ); - emit(state.copyWith( - status: ClockInStatus.success, - attendance: newStatus, - )); + try { + final AttendanceStatus newStatus = await _clockIn( + ClockInArguments( + shiftId: event.shiftId, + notes: event.notes, + latitude: location?.latitude, + longitude: location?.longitude, + accuracyMeters: location?.accuracy, + capturedAt: location?.timestamp, + overrideReason: geofenceState.isGeofenceOverridden + ? geofenceState.overrideNotes + : null, + ), + ); + emit(state.copyWith( + status: ClockInStatus.success, + attendance: newStatus, + )); - // Start background tracking after successful clock-in. - _dispatchBackgroundTrackingStarted( - event: event, - activeShiftId: newStatus.activeShiftId, - ); + // Start background tracking after successful clock-in. + _dispatchBackgroundTrackingStarted( + event: event, + activeShiftId: newStatus.activeShiftId, + ); + } on AppException catch (_) { + // The clock-in API call failed. Re-fetch attendance status to + // reconcile: if the worker is already clocked in (e.g. duplicate + // session from Postgres constraint 23505), treat it as success. + final AttendanceStatus currentStatus = await _getAttendanceStatus(); + if (currentStatus.isClockedIn) { + emit(state.copyWith( + status: ClockInStatus.success, + attendance: currentStatus, + )); + _dispatchBackgroundTrackingStarted( + event: event, + activeShiftId: currentStatus.activeShiftId, + ); + } else { + // Worker is genuinely not clocked in — surface the error. + rethrow; + } + } }, onError: (String errorKey) => state.copyWith( status: ClockInStatus.failure, @@ -261,29 +281,51 @@ class ClockInBloc extends Bloc final GeofenceState currentGeofence = _geofenceBloc.state; final DeviceLocation? location = currentGeofence.currentLocation; - final AttendanceStatus newStatus = await _clockOut( - ClockOutArguments( - notes: event.notes, - breakTimeMinutes: event.breakTimeMinutes, - shiftId: activeShiftId, - latitude: location?.latitude, - longitude: location?.longitude, - accuracyMeters: location?.accuracy, - capturedAt: location?.timestamp, - ), - ); - emit(state.copyWith( - status: ClockInStatus.success, - attendance: newStatus, - )); + try { + final AttendanceStatus newStatus = await _clockOut( + ClockOutArguments( + notes: event.notes, + breakTimeMinutes: event.breakTimeMinutes, + shiftId: activeShiftId, + latitude: location?.latitude, + longitude: location?.longitude, + accuracyMeters: location?.accuracy, + capturedAt: location?.timestamp, + ), + ); + emit(state.copyWith( + status: ClockInStatus.success, + attendance: newStatus, + )); - // Stop background tracking after successful clock-out. - _geofenceBloc.add( - BackgroundTrackingStopped( - clockOutTitle: event.clockOutTitle, - clockOutBody: event.clockOutBody, - ), - ); + // Stop background tracking after successful clock-out. + _geofenceBloc.add( + BackgroundTrackingStopped( + clockOutTitle: event.clockOutTitle, + clockOutBody: event.clockOutBody, + ), + ); + } on AppException catch (_) { + // The clock-out API call failed. Re-fetch attendance status to + // reconcile: if the worker is already clocked out (e.g. duplicate + // end-session), treat it as success. + final AttendanceStatus currentStatus = await _getAttendanceStatus(); + if (!currentStatus.isClockedIn) { + emit(state.copyWith( + status: ClockInStatus.success, + attendance: currentStatus, + )); + _geofenceBloc.add( + BackgroundTrackingStopped( + clockOutTitle: event.clockOutTitle, + clockOutBody: event.clockOutBody, + ), + ); + } else { + // Worker is still clocked in — surface the error. + rethrow; + } + } }, onError: (String errorKey) => state.copyWith( status: ClockInStatus.failure,