Fixed In Progress ISR not reflecting on IT Staff Pulse Dashboard Status Pill
Made the Location Tracking more persistent
This commit is contained in:
parent
24ecca9f06
commit
21e6d68910
|
|
@ -12,6 +12,7 @@
|
||||||
will automatically block notifications and the user cannot enable them
|
will automatically block notifications and the user cannot enable them
|
||||||
from settings. -->
|
from settings. -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="tasq"
|
android:label="tasq"
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
|
|
||||||
20
lib/app.dart
20
lib/app.dart
|
|
@ -4,6 +4,9 @@ import 'package:flutter_quill/flutter_quill.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'routing/app_router.dart';
|
import 'routing/app_router.dart';
|
||||||
|
import 'models/profile.dart';
|
||||||
|
import 'providers/profile_provider.dart';
|
||||||
|
import 'services/background_location_service.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
|
|
||||||
class TasqApp extends ConsumerWidget {
|
class TasqApp extends ConsumerWidget {
|
||||||
|
|
@ -13,6 +16,23 @@ class TasqApp extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|
||||||
|
// Ensure background service is running if the profile indicates tracking.
|
||||||
|
// This handles the case where the app restarts while tracking was already
|
||||||
|
// enabled (service should persist, but this guarantees it). It also stops
|
||||||
|
// the service when tracking is disabled externally.
|
||||||
|
ref.listen<AsyncValue<Profile?>>(currentProfileProvider, (
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
) async {
|
||||||
|
final profile = next.valueOrNull;
|
||||||
|
final allow = profile?.allowTracking ?? false;
|
||||||
|
if (allow) {
|
||||||
|
await startBackgroundLocationUpdates();
|
||||||
|
} else {
|
||||||
|
await stopBackgroundLocationUpdates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'TasQ',
|
title: 'TasQ',
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ class AttendanceController {
|
||||||
'p_attendance_id': attendanceId,
|
'p_attendance_id': attendanceId,
|
||||||
'p_lat': lat,
|
'p_lat': lat,
|
||||||
'p_lng': lng,
|
'p_lng': lng,
|
||||||
|
// ignore: use_null_aware_elements
|
||||||
if (justification != null) 'p_justification': justification,
|
if (justification != null) 'p_justification': justification,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import '../../models/task.dart';
|
||||||
import '../../models/task_assignment.dart';
|
import '../../models/task_assignment.dart';
|
||||||
import '../../models/ticket.dart';
|
import '../../models/ticket.dart';
|
||||||
import '../../models/ticket_message.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/attendance_provider.dart';
|
||||||
import '../../providers/leave_provider.dart';
|
import '../../providers/leave_provider.dart';
|
||||||
import '../../providers/pass_slip_provider.dart';
|
import '../../providers/pass_slip_provider.dart';
|
||||||
|
|
@ -302,6 +304,23 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
||||||
.add(task.id);
|
.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 <ItServiceRequest>[];
|
||||||
|
final isrById = {for (final r in isrList) r.id: r};
|
||||||
|
final staffOnService = <String>{};
|
||||||
|
for (final assign
|
||||||
|
in isrAssignmentsAsync.valueOrNull ??
|
||||||
|
const <ItServiceRequestAssignment>[]) {
|
||||||
|
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);
|
const triageWindow = Duration(minutes: 1);
|
||||||
final triageCutoff = now.subtract(triageWindow);
|
final triageCutoff = now.subtract(triageWindow);
|
||||||
|
|
||||||
|
|
@ -367,12 +386,14 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
||||||
final lastMessage = lastStaffMessageByUser[staff.id];
|
final lastMessage = lastStaffMessageByUser[staff.id];
|
||||||
final ticketsResponded = respondedTicketsByUser[staff.id]?.length ?? 0;
|
final ticketsResponded = respondedTicketsByUser[staff.id]?.length ?? 0;
|
||||||
final tasksClosed = tasksClosedByUser[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 onTask = staffOnTask.contains(staff.id);
|
||||||
final inTriage = lastMessage != null && lastMessage.isAfter(triageCutoff);
|
final onService = staffOnService.contains(staff.id);
|
||||||
|
|
||||||
// Attendance-based status.
|
|
||||||
final userSchedules = todaySchedulesByUser[staff.id] ?? const [];
|
final userSchedules = todaySchedulesByUser[staff.id] ?? const [];
|
||||||
final userLogs = todayLogsByUser[staff.id] ?? const [];
|
final userLogs = todayLogsByUser[staff.id] ?? const [];
|
||||||
|
final inTriage = lastMessage != null && lastMessage.isAfter(triageCutoff);
|
||||||
|
|
||||||
// Whereabouts from live position, with tracking-off inference.
|
// Whereabouts from live position, with tracking-off inference.
|
||||||
final livePos = positionByUser[staff.id];
|
final livePos = positionByUser[staff.id];
|
||||||
|
|
@ -410,8 +431,10 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
||||||
// Active pass slip — user is temporarily away from duty.
|
// Active pass slip — user is temporarily away from duty.
|
||||||
status = 'PASS SLIP';
|
status = 'PASS SLIP';
|
||||||
} else if (userSchedules.isEmpty) {
|
} else if (userSchedules.isEmpty) {
|
||||||
// No schedule today — off duty unless actively on task/triage.
|
// No schedule today — off duty unless actively on task/service/triage.
|
||||||
status = onTask
|
status = onService
|
||||||
|
? 'On event'
|
||||||
|
: onTask
|
||||||
? 'On task'
|
? 'On task'
|
||||||
: inTriage
|
: inTriage
|
||||||
? 'In triage'
|
? 'In triage'
|
||||||
|
|
@ -445,7 +468,9 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
||||||
|
|
||||||
if (activeLog != null) {
|
if (activeLog != null) {
|
||||||
// Currently checked in — on-duty, can be overridden.
|
// Currently checked in — on-duty, can be overridden.
|
||||||
status = onTask
|
status = onService
|
||||||
|
? 'On event'
|
||||||
|
: onTask
|
||||||
? 'On task'
|
? 'On task'
|
||||||
: inTriage
|
: inTriage
|
||||||
? 'In triage'
|
? 'In triage'
|
||||||
|
|
@ -1166,6 +1191,7 @@ class _PulseStatusPill extends StatelessWidget {
|
||||||
'noon break' => (Colors.blue.shade100, Colors.blue.shade900),
|
'noon break' => (Colors.blue.shade100, Colors.blue.shade900),
|
||||||
'vacant' => (Colors.green.shade100, Colors.green.shade900),
|
'vacant' => (Colors.green.shade100, Colors.green.shade900),
|
||||||
'on task' => (Colors.purple.shade100, Colors.purple.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),
|
'in triage' => (Colors.orange.shade100, Colors.orange.shade900),
|
||||||
'early out' => (Colors.deepOrange.shade100, Colors.deepOrange.shade900),
|
'early out' => (Colors.deepOrange.shade100, Colors.deepOrange.shade900),
|
||||||
'on leave' => (Colors.teal.shade100, Colors.teal.shade900),
|
'on leave' => (Colors.teal.shade100, Colors.teal.shade900),
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,27 @@ Future<void> onBackgroundServiceStart(ServiceInstance service) async {
|
||||||
// Send initial location immediately on service start.
|
// Send initial location immediately on service start.
|
||||||
await _sendLocationUpdate();
|
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<Position>? 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 {
|
Timer.periodic(_updateInterval, (_) async {
|
||||||
debugPrint('[BgLoc] Periodic update triggered');
|
debugPrint('[BgLoc] Periodic update triggered');
|
||||||
await _sendLocationUpdate();
|
await _sendLocationUpdate();
|
||||||
|
|
@ -114,6 +134,7 @@ Future<void> onBackgroundServiceStart(ServiceInstance service) async {
|
||||||
// Listen for stop command from the foreground.
|
// Listen for stop command from the foreground.
|
||||||
service.on('stop').listen((_) {
|
service.on('stop').listen((_) {
|
||||||
debugPrint('[BgLoc] Stop command received');
|
debugPrint('[BgLoc] Stop command received');
|
||||||
|
positionSub?.cancel();
|
||||||
service.stopSelf();
|
service.stopSelf();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +181,7 @@ Future<void> initBackgroundLocationService() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the background location service.
|
/// Start the background location service.
|
||||||
|
|
||||||
Future<void> startBackgroundLocationUpdates() async {
|
Future<void> startBackgroundLocationUpdates() async {
|
||||||
if (kIsWeb) return;
|
if (kIsWeb) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies:
|
||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
flutter_background_service: ^5.0.12
|
flutter_background_service: ^5.0.12
|
||||||
flutter_background_service_android: ^6.2.7
|
flutter_background_service_android: ^6.2.7
|
||||||
|
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
flutter_liveness_check: ^1.0.3
|
flutter_liveness_check: ^1.0.3
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user