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