feat: Implement notification and storage services, geofence management, and BLoC for geofence verification

- Add NotificationService for handling local notifications.
- Introduce StorageService for key-value storage using SharedPreferences.
- Create DeviceLocation model to represent geographic locations.
- Define LocationPermissionStatus enum for managing location permissions.
- Implement BackgroundGeofenceService for periodic geofence checks while clocked in.
- Develop GeofenceServiceImpl for geofence proximity verification using LocationService.
- Create GeofenceResult model to encapsulate geofence check results.
- Define GeofenceServiceInterface for geofence service abstraction.
- Implement GeofenceBloc to manage geofence verification and background tracking.
- Create events and states for GeofenceBloc to handle various geofence scenarios.
- Add GeofenceStatusBanner widget to display geofence verification status in the UI.
This commit is contained in:
Achintha Isuru
2026-03-13 16:01:26 -04:00
parent 2fc6b3139e
commit 7b576c0ed4
54 changed files with 2216 additions and 493 deletions

View File

@@ -926,6 +926,28 @@
"submit": "Submit",
"success_title": "Break Logged!",
"close": "Close"
},
"geofence": {
"service_disabled": "Location services are turned off. Enable them to clock in.",
"permission_required": "Location permission is required to clock in.",
"permission_denied_forever": "Location was permanently denied. Enable it in Settings.",
"open_settings": "Open Settings",
"grant_permission": "Grant Permission",
"verifying": "Verifying your location...",
"too_far_title": "You're Too Far Away",
"too_far_desc": "You are $distance away. Move within 500m to clock in.",
"verified": "Location Verified",
"not_in_range": "You must be at the workplace to clock in.",
"timeout_title": "Can't Verify Location",
"timeout_desc": "Unable to determine your location. You can still clock in with a note.",
"timeout_note_hint": "Why can't your location be verified?",
"clock_in_greeting_title": "You're Clocked In!",
"clock_in_greeting_body": "Have a great shift. We'll keep track of your location.",
"background_left_title": "You've Left the Workplace",
"background_left_body": "You appear to be more than 500m from your shift location.",
"always_permission_title": "Background Location Needed",
"always_permission_desc": "To verify your location during shifts, please allow location access 'Always'.",
"retry": "Retry"
}
},
"availability": {
@@ -1416,6 +1438,10 @@
"application_not_found": "Your application couldn't be found.",
"no_active_shift": "You don't have an active shift to clock out from."
},
"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."
},
"generic": {
"unknown": "Something went wrong. Please try again.",
"no_connection": "No internet connection. Please check your network and try again.",

View File

@@ -921,6 +921,28 @@
"submit": "Enviar",
"success_title": "\u00a1Descanso registrado!",
"close": "Cerrar"
},
"geofence": {
"service_disabled": "Los servicios de ubicación están desactivados. Actívelos para registrar entrada.",
"permission_required": "Se requiere permiso de ubicación para registrar entrada.",
"permission_denied_forever": "La ubicación fue denegada permanentemente. Actívela en Configuración.",
"open_settings": "Abrir Configuración",
"grant_permission": "Otorgar Permiso",
"verifying": "Verificando su ubicación...",
"too_far_title": "Está Demasiado Lejos",
"too_far_desc": "Está a $distance de distancia. Acérquese a 500m para registrar entrada.",
"verified": "Ubicación Verificada",
"not_in_range": "Debe estar en el lugar de trabajo para registrar entrada.",
"timeout_title": "No se Puede Verificar la Ubicación",
"timeout_desc": "No se pudo determinar su ubicación. Puede registrar entrada con una nota.",
"timeout_note_hint": "¿Por qué no se puede verificar su ubicación?",
"clock_in_greeting_title": "¡Entrada Registrada!",
"clock_in_greeting_body": "Buen turno. Seguiremos el registro de su ubicación.",
"background_left_title": "Ha Salido del Lugar de Trabajo",
"background_left_body": "Parece que está a más de 500m de la ubicación de su turno.",
"always_permission_title": "Se Necesita Ubicación en Segundo Plano",
"always_permission_desc": "Para verificar su ubicación durante los turnos, permita el acceso a la ubicación 'Siempre'.",
"retry": "Reintentar"
}
},
"availability": {
@@ -1411,6 +1433,10 @@
"application_not_found": "No se pudo encontrar tu solicitud.",
"no_active_shift": "No tienes un turno activo para registrar salida."
},
"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."
},
"generic": {
"unknown": "Algo sali\u00f3 mal. Por favor, intenta de nuevo.",
"no_connection": "Sin conexi\u00f3n a internet. Por favor, verifica tu red e intenta de nuevo.",

View File

@@ -35,6 +35,8 @@ String translateErrorKey(String key) {
return _translateProfileError(errorType);
case 'shift':
return _translateShiftError(errorType);
case 'clock_in':
return _translateClockInError(errorType);
case 'generic':
return _translateGenericError(errorType);
default:
@@ -127,6 +129,18 @@ String _translateShiftError(String errorType) {
}
}
/// Translates clock-in error keys to localized strings.
String _translateClockInError(String errorType) {
switch (errorType) {
case 'location_verification_required':
return t.errors.clock_in.location_verification_required;
case 'notes_required_for_timeout':
return t.errors.clock_in.notes_required_for_timeout;
default:
return t.errors.generic.unknown;
}
}
String _translateGenericError(String errorType) {
switch (errorType) {
case 'unknown':