diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 86167342..dbafb948 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ will automatically block notifications and the user cannot enable them from settings. --> + >(currentProfileProvider, ( + previous, + next, + ) async { + final profile = next.valueOrNull; + final allow = profile?.allowTracking ?? false; + if (allow) { + await startBackgroundLocationUpdates(); + } else { + await stopBackgroundLocationUpdates(); + } + }); + return MaterialApp.router( title: 'TasQ', routerConfig: router, diff --git a/lib/providers/attendance_provider.dart b/lib/providers/attendance_provider.dart index 2312d7ea..fb083044 100644 --- a/lib/providers/attendance_provider.dart +++ b/lib/providers/attendance_provider.dart @@ -99,6 +99,7 @@ class AttendanceController { 'p_attendance_id': attendanceId, 'p_lat': lat, 'p_lng': lng, + // ignore: use_null_aware_elements if (justification != null) 'p_justification': justification, }, ); diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index b005b2f8..531bb94c 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -15,6 +15,8 @@ import '../../models/task.dart'; import '../../models/task_assignment.dart'; import '../../models/ticket.dart'; import '../../models/ticket_message.dart'; +import '../../models/it_service_request.dart'; +import '../../models/it_service_request_assignment.dart'; import '../../providers/attendance_provider.dart'; import '../../providers/leave_provider.dart'; import '../../providers/pass_slip_provider.dart'; @@ -302,6 +304,23 @@ final dashboardMetricsProvider = Provider>((ref) { .add(task.id); } + // Determine which staff members are currently handling an IT service + // request. The dashboard should treat these assignments like being "on + // task" so that the status pill reflects the fact that they are busy. + final isrList = isrAsync.valueOrNull ?? const []; + final isrById = {for (final r in isrList) r.id: r}; + final staffOnService = {}; + for (final assign + in isrAssignmentsAsync.valueOrNull ?? + const []) { + final isr = isrById[assign.requestId]; + if (isr == null) continue; + if (isr.status == ItServiceRequestStatus.inProgress || + isr.status == ItServiceRequestStatus.inProgressDryRun) { + staffOnService.add(assign.userId); + } + } + const triageWindow = Duration(minutes: 1); final triageCutoff = now.subtract(triageWindow); @@ -367,12 +386,14 @@ final dashboardMetricsProvider = Provider>((ref) { final lastMessage = lastStaffMessageByUser[staff.id]; final ticketsResponded = respondedTicketsByUser[staff.id]?.length ?? 0; final tasksClosed = tasksClosedByUser[staff.id]?.length ?? 0; + // users are considered "on task" if they have either a regular task + // assignment or an active service request in progress/dry run. + // determine whether staff have regular tasks or service requests final onTask = staffOnTask.contains(staff.id); - final inTriage = lastMessage != null && lastMessage.isAfter(triageCutoff); - - // Attendance-based status. + final onService = staffOnService.contains(staff.id); final userSchedules = todaySchedulesByUser[staff.id] ?? const []; final userLogs = todayLogsByUser[staff.id] ?? const []; + final inTriage = lastMessage != null && lastMessage.isAfter(triageCutoff); // Whereabouts from live position, with tracking-off inference. final livePos = positionByUser[staff.id]; @@ -410,8 +431,10 @@ final dashboardMetricsProvider = Provider>((ref) { // Active pass slip — user is temporarily away from duty. status = 'PASS SLIP'; } else if (userSchedules.isEmpty) { - // No schedule today — off duty unless actively on task/triage. - status = onTask + // No schedule today — off duty unless actively on task/service/triage. + status = onService + ? 'On event' + : onTask ? 'On task' : inTriage ? 'In triage' @@ -445,7 +468,9 @@ final dashboardMetricsProvider = Provider>((ref) { if (activeLog != null) { // Currently checked in — on-duty, can be overridden. - status = onTask + status = onService + ? 'On event' + : onTask ? 'On task' : inTriage ? 'In triage' @@ -1166,6 +1191,7 @@ class _PulseStatusPill extends StatelessWidget { 'noon break' => (Colors.blue.shade100, Colors.blue.shade900), 'vacant' => (Colors.green.shade100, Colors.green.shade900), 'on task' => (Colors.purple.shade100, Colors.purple.shade900), + 'on event' => (Colors.purple.shade100, Colors.purple.shade900), 'in triage' => (Colors.orange.shade100, Colors.orange.shade900), 'early out' => (Colors.deepOrange.shade100, Colors.deepOrange.shade900), 'on leave' => (Colors.teal.shade100, Colors.teal.shade900), diff --git a/lib/services/background_location_service.dart b/lib/services/background_location_service.dart index dacaed14..f75a68f7 100644 --- a/lib/services/background_location_service.dart +++ b/lib/services/background_location_service.dart @@ -105,7 +105,27 @@ Future onBackgroundServiceStart(ServiceInstance service) async { // Send initial location immediately on service start. await _sendLocationUpdate(); - // Periodic 15-minute timer for location updates. + // Maintain a subscription to the position stream so the service remains + // active when the device is moving. We still keep a periodic timer as a + // backup so that updates fire even when stationary. + StreamSubscription? positionSub; + try { + positionSub = + Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + ), + ).listen((pos) { + debugPrint('[BgLoc] Stream position: $pos'); + _sendLocationUpdate(); + }); + } catch (e) { + debugPrint('[BgLoc] Position stream failed: $e'); + } + + // Periodic 15-minute timer for location updates (backup when stream is + // paused by Doze). Timer.periodic(_updateInterval, (_) async { debugPrint('[BgLoc] Periodic update triggered'); await _sendLocationUpdate(); @@ -114,6 +134,7 @@ Future onBackgroundServiceStart(ServiceInstance service) async { // Listen for stop command from the foreground. service.on('stop').listen((_) { debugPrint('[BgLoc] Stop command received'); + positionSub?.cancel(); service.stopSelf(); }); } @@ -160,6 +181,7 @@ Future initBackgroundLocationService() async { } /// Start the background location service. + Future startBackgroundLocationUpdates() async { if (kIsWeb) return; diff --git a/pubspec.yaml b/pubspec.yaml index 8ac3a759..5c8b6bed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: http: ^1.2.0 flutter_background_service: ^5.0.12 flutter_background_service_android: ^6.2.7 + intl: ^0.20.2 image_picker: ^1.1.2 flutter_liveness_check: ^1.0.3