import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart' show LatLng; import '../../models/app_settings.dart'; import '../../providers/location_provider.dart'; import '../../providers/workforce_provider.dart'; import '../../providers/profile_provider.dart'; import '../../widgets/responsive_body.dart'; class GeofenceTestScreen extends ConsumerStatefulWidget { const GeofenceTestScreen({super.key}); @override ConsumerState createState() => _GeofenceTestScreenState(); } class _GeofenceTestScreenState extends ConsumerState { final _mapController = MapController(); bool _followMe = true; bool _liveTracking = false; // enable periodic updates from the device stream bool _isInside(Position pos, GeofenceConfig? cfg) { if (cfg == null) return false; if (cfg.hasPolygon) { return cfg.containsPolygon(pos.latitude, pos.longitude); } if (cfg.hasCircle) { final dist = Geolocator.distanceBetween( pos.latitude, pos.longitude, cfg.lat!, cfg.lng!, ); return dist <= (cfg.radiusMeters ?? 0); } return false; } void _centerOn(LatLng latLng) { // flutter_map v8+ MapController does not expose current zoom getter — use a // sensible default zoom when centering. _mapController.move(latLng, 16.0); } @override Widget build(BuildContext context) { final isAdmin = ref.watch(isAdminProvider); final geofenceAsync = ref.watch(geofenceProvider); final positionAsync = ref.watch(currentPositionProvider); // Only watch the live stream when the user enables "Live" so tests that // don't enable live tracking won't need to provide a stream override. final AsyncValue? streamAsync = _liveTracking ? ref.watch(currentPositionStreamProvider) : null; // If live-tracking is active and the stream yields a position while the // UI is following the device, recenter the map. if (_liveTracking && streamAsync != null) { streamAsync.whenData((p) { if (_followMe) { WidgetsBinding.instance.addPostFrameCallback((_) { _centerOn(LatLng(p.latitude, p.longitude)); }); } }); } return ResponsiveBody( child: !isAdmin ? const Center(child: Text('Admin access required.')) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ Expanded( child: Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Geofence status'), const SizedBox(height: 8), // Show either the one-shot position or the live-stream // value depending on the user's toggle. _liveTracking ? (streamAsync == null ? const Text('Live tracking disabled') : streamAsync.when( data: (pos) { final inside = _isInside( pos, geofenceAsync.valueOrNull, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( inside ? Icons .check_circle : Icons.cancel, color: inside ? Colors.green : Colors.red, ), const SizedBox( width: 8, ), Text( inside ? 'Inside geofence' : 'Outside geofence', ), const SizedBox( width: 12, ), Flexible( child: Text( 'Lat: ${pos.latitude.toStringAsFixed(6)}, Lng: ${pos.longitude.toStringAsFixed(6)}', overflow: TextOverflow .ellipsis, ), ), ], ), const SizedBox(height: 8), _buildGeofenceSummary( geofenceAsync.valueOrNull, ), ], ); }, loading: () => const Text( 'Waiting for live position...', ), error: (err, st) => Text( 'Live location error: $err', ), )) : positionAsync.when( data: (pos) { final inside = _isInside( pos, geofenceAsync.valueOrNull, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( inside ? Icons.check_circle : Icons.cancel, color: inside ? Colors.green : Colors.red, ), const SizedBox(width: 8), Text( inside ? 'Inside geofence' : 'Outside geofence', ), const SizedBox(width: 12), Flexible( child: Text( 'Lat: ${pos.latitude.toStringAsFixed(6)}, Lng: ${pos.longitude.toStringAsFixed(6)}', overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), _buildGeofenceSummary( geofenceAsync.valueOrNull, ), ], ); }, loading: () => const Text('Fetching location...'), error: (err, st) => Text('Location error: $err'), ), ], ), ), ), ), const SizedBox(width: 12), Material( type: MaterialType.transparency, child: Column( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Live'), const SizedBox(width: 6), Switch.adaptive( key: const Key('live-switch'), value: _liveTracking, onChanged: (v) => setState(() => _liveTracking = v), ), ], ), const SizedBox(height: 6), SizedBox( height: 48, child: FilledButton.icon( icon: const Icon(Icons.my_location), label: const Text('Refresh'), onPressed: () => ref.refresh(currentPositionProvider), ), ), const SizedBox(height: 8), IconButton( tooltip: 'Center map on current location', onPressed: () { final pos = (_liveTracking ? streamAsync?.valueOrNull : positionAsync.valueOrNull); if (pos != null) { _centerOn( LatLng(pos.latitude, pos.longitude), ); setState(() => _followMe = true); } }, icon: const Icon(Icons.center_focus_strong), ), ], ), ), ], ), ), const SizedBox(height: 12), Expanded( child: Card( margin: const EdgeInsets.symmetric(horizontal: 16.0), child: Padding( padding: const EdgeInsets.all(8.0), child: geofenceAsync.when( data: (cfg) { final polygonPoints = cfg?.polygon ?.map((p) => LatLng(p.lat, p.lng)) .toList() ?? []; final markers = []; if (positionAsync.valueOrNull != null) { final p = positionAsync.value!; markers.add( Marker( point: LatLng(p.latitude, p.longitude), width: 40, height: 40, // `child` is used by newer flutter_map Marker API. child: const Icon( Icons.person_pin_circle, size: 36, color: Colors.blue, ), ), ); } final polygonLayer = polygonPoints.isNotEmpty ? PolygonLayer( polygons: [ Polygon( points: polygonPoints, color: Colors.green.withValues( alpha: 0.15, ), borderColor: Colors.green, borderStrokeWidth: 2, ), ], ) : const SizedBox.shrink(); final circleLayer = (cfg?.hasCircle == true) ? CircleLayer( circles: [ CircleMarker( point: LatLng(cfg!.lat!, cfg.lng!), color: Colors.green.withValues( alpha: 0.10, ), borderStrokeWidth: 2, useRadiusInMeter: true, radius: cfg.radiusMeters ?? 100, ), ], ) : const SizedBox.shrink(); // Prefer the live-stream position when enabled, else // fall back to the one-shot value used by the rest of // the app/tests. final activePos = _liveTracking ? streamAsync?.valueOrNull : positionAsync.valueOrNull; final effectiveCenter = cfg?.hasCircle == true ? LatLng(cfg!.lat!, cfg.lng!) : (activePos != null ? LatLng( activePos.latitude, activePos.longitude, ) : const LatLng(7.2009, 124.2360)); // Build the map inside a Stack so we can overlay a // small legend/radius label on top of the map. return Stack( children: [ FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: effectiveCenter, initialZoom: 16.0, maxZoom: 18, minZoom: 3, onPositionChanged: (pos, hasGesture) { if (hasGesture) { setState(() => _followMe = false); } }, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // Per OSM tile usage policy: set a User-Agent that // identifies this application. Use the Android // applicationId so tile servers can contact the // publisher if necessary. userAgentPackageName: 'com.example.tasq', ), if (polygonPoints.isNotEmpty) polygonLayer, if (cfg?.hasCircle == true) circleLayer, if (markers.isNotEmpty) MarkerLayer(markers: markers), ], ), // Legend / radius label Positioned( right: 12, top: 12, child: Card( elevation: 0, shadowColor: Colors.transparent, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 10.0, vertical: 8.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: Colors.green.withValues( alpha: 0.25, ), border: Border.all( color: Colors.green, ), ), ), const SizedBox(width: 8), Text( cfg?.hasPolygon == true ? 'Geofence (polygon)' : 'Geofence (circle)', ), ], ), if (cfg?.hasCircle == true) Padding( padding: const EdgeInsets.only( top: 6.0, ), child: Text( 'Radius: ${cfg!.radiusMeters?.toStringAsFixed(0) ?? '-'} m', style: Theme.of( context, ).textTheme.bodySmall, ), ), Padding( padding: const EdgeInsets.only( top: 6.0, ), child: Row( children: const [ Icon( Icons.person_pin_circle, size: 14, color: Colors.blue, ), SizedBox(width: 8), Text('You'), ], ), ), const SizedBox(height: 6), Text( 'Map tiles: © OpenStreetMap contributors', style: Theme.of( context, ).textTheme.bodySmall, ), ], ), ), ), ), ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, st) => Center( child: Text('Failed to load geofence: $err'), ), ), ), ), ), const SizedBox(height: 12), ], ), ); } Widget _buildGeofenceSummary(GeofenceConfig? cfg) { if (cfg == null) return const Text('Geofence: not configured'); if (cfg.hasCircle) { return Text( 'Geofence: Circle • Radius: ${cfg.radiusMeters?.toStringAsFixed(0) ?? '-'} m', ); } if (cfg.hasPolygon) { return Text('Geofence: Polygon • ${cfg.polygon?.length ?? 0} points'); } return const Text('Geofence: not configured'); } }