import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart' show LatLng; import '../../models/app_settings.dart'; import '../../models/live_position.dart'; import '../../models/profile.dart'; import '../../providers/profile_provider.dart'; import '../../providers/whereabouts_provider.dart'; import '../../providers/workforce_provider.dart'; import '../../widgets/responsive_body.dart'; import '../../utils/app_time.dart'; class WhereaboutsScreen extends ConsumerStatefulWidget { const WhereaboutsScreen({super.key}); @override ConsumerState createState() => _WhereaboutsScreenState(); } class _WhereaboutsScreenState extends ConsumerState { final _mapController = MapController(); @override Widget build(BuildContext context) { final positionsAsync = ref.watch(livePositionsProvider); final profilesAsync = ref.watch(profilesProvider); final geofenceAsync = ref.watch(geofenceProvider); final Map profileById = { for (final p in profilesAsync.valueOrNull ?? []) p.id: p, }; return ResponsiveBody( maxWidth: 1200, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( 'Whereabouts', style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700), ), ), Expanded( child: positionsAsync.when( data: (positions) => _buildMap( context, positions, profileById, geofenceAsync.valueOrNull, ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Failed to load positions: $e')), ), ), // Staff list below the map positionsAsync.when( data: (positions) => _buildStaffList(context, positions, profileById), loading: () => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(), ), ], ), ); } Widget _buildMap( BuildContext context, List positions, Map profileById, GeofenceConfig? geofenceConfig, ) { // Only pin users who are in-premise (privacy: don't reveal off-site locations). final inPremisePositions = positions.where((pos) => pos.inPremise).toList(); final markers = inPremisePositions.map((pos) { final profile = profileById[pos.userId]; final name = profile?.fullName ?? 'Unknown'; final pinColor = _roleColor(profile?.role); return Marker( point: LatLng(pos.lat, pos.lng), width: 80, height: 60, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 4, ), ], ), child: Text( name.split(' ').first, style: Theme.of( context, ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ), Icon(Icons.location_pin, size: 28, color: pinColor), ], ), ); }).toList(); // Build geofence polygon overlay if available final polygonLayers = []; if (geofenceConfig != null && geofenceConfig.hasPolygon) { final List points = geofenceConfig.polygon! .map((p) => LatLng(p.lat, p.lng)) .toList(); if (points.isNotEmpty) { polygonLayers.add( PolygonLayer( polygons: [ Polygon( points: points, color: Colors.blue.withValues(alpha: 0.1), borderColor: Colors.blue, borderStrokeWidth: 2.0, ), ], ), ); } } // Default center: CRMC Cotabato City area const defaultCenter = LatLng(7.2046, 124.2460); return FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: positions.isNotEmpty ? LatLng(positions.first.lat, positions.first.lng) : defaultCenter, initialZoom: 16.0, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.tasq.app', ), ...polygonLayers, MarkerLayer(markers: markers), ], ); } Widget _buildStaffList( BuildContext context, List positions, Map profileById, ) { if (positions.isEmpty) return const SizedBox.shrink(); // Only include Admin, IT Staff, and Dispatcher in the legend. final relevantRoles = {'admin', 'dispatcher', 'it_staff'}; final legendEntries = positions.where((pos) { final role = profileById[pos.userId]?.role; return role != null && relevantRoles.contains(role); }).toList(); if (legendEntries.isEmpty) return const SizedBox.shrink(); return Container( constraints: const BoxConstraints(maxHeight: 220), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Role color legend header Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), child: Row( children: [ _legendDot(Colors.blue.shade700), const SizedBox(width: 4), Text('Admin', style: Theme.of(context).textTheme.labelSmall), const SizedBox(width: 12), _legendDot(Colors.green.shade700), const SizedBox(width: 4), Text('IT Staff', style: Theme.of(context).textTheme.labelSmall), const SizedBox(width: 12), _legendDot(Colors.orange.shade700), const SizedBox(width: 4), Text( 'Dispatcher', style: Theme.of(context).textTheme.labelSmall, ), ], ), ), // Staff entries Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), itemCount: legendEntries.length, itemBuilder: (context, index) { final pos = legendEntries[index]; final p = profileById[pos.userId]; final name = p?.fullName ?? 'Unknown'; final role = p?.role ?? '-'; final isInPremise = pos.inPremise; final pinColor = _roleColor(role); final timeAgo = _timeAgo(pos.updatedAt); // Grey out outside-premise users for privacy. final effectiveColor = isInPremise ? pinColor : Colors.grey.shade400; return ListTile( dense: true, leading: CircleAvatar( radius: 16, backgroundColor: effectiveColor.withValues(alpha: 0.2), child: Icon( isInPremise ? Icons.location_pin : Icons.location_off, size: 16, color: effectiveColor, ), ), title: Text( name, style: TextStyle( color: isInPremise ? null : Colors.grey, fontWeight: isInPremise ? FontWeight.w600 : FontWeight.normal, ), ), subtitle: Text( '${_roleLabel(role)} ยท ${isInPremise ? timeAgo : 'Outside premise'}', style: TextStyle(color: isInPremise ? null : Colors.grey), ), trailing: isInPremise ? Icon(Icons.circle, size: 10, color: pinColor) : Icon( Icons.circle, size: 10, color: Colors.grey.shade300, ), onTap: isInPremise ? () => _mapController.move(LatLng(pos.lat, pos.lng), 17.0) : null, ); }, ), ), ], ), ); } Widget _legendDot(Color color) { return Container( width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ); } /// Returns the pin color for a given role. static Color _roleColor(String? role) { switch (role) { case 'admin': return Colors.blue.shade700; case 'it_staff': return Colors.green.shade700; case 'dispatcher': return Colors.orange.shade700; default: return Colors.grey; } } String _timeAgo(DateTime dt) { final diff = AppTime.now().difference(dt); if (diff.inMinutes < 1) return 'Just now'; if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; return '${diff.inHours}h ago'; } String _roleLabel(String role) { switch (role) { case 'admin': return 'Admin'; case 'dispatcher': return 'Dispatcher'; case 'it_staff': return 'IT Staff'; default: return 'Standard'; } } }