import 'dart:async'; import 'package:flutter/material.dart'; import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import '../app/app_router.dart'; import '../services/beacon_permissions.dart'; import '../services/api.dart'; class BeaconScanScreen extends StatefulWidget { const BeaconScanScreen({super.key}); @override State createState() => _BeaconScanScreenState(); } class _BeaconScanScreenState extends State { String _status = 'Initializing...'; bool _permissionsGranted = false; bool _scanning = false; Map _uuidToBeaconId = {}; final Map> _detectedBeacons = {}; // UUID -> (BeaconID, RSSI) @override void initState() { super.initState(); _startScanFlow(); } Future _startScanFlow() async { // Step 1: Request permissions setState(() => _status = 'Requesting permissions...'); final granted = await BeaconPermissions.requestPermissions(); if (!granted) { setState(() { _status = 'Permissions denied - Please enable Location & Bluetooth'; _permissionsGranted = false; }); return; } setState(() => _permissionsGranted = true); // Step 2: Fetch all active beacons from server setState(() => _status = 'Loading beacon data...'); try { _uuidToBeaconId = await Api.listAllBeacons(); } catch (e) { debugPrint('[BeaconScan] Error loading beacons: $e'); _uuidToBeaconId = {}; } if (_uuidToBeaconId.isEmpty) { debugPrint('[BeaconScan] No beacons in database, going to restaurant select'); if (mounted) _navigateToRestaurantSelect(); return; } // Step 3: Perform initial scan setState(() { _status = 'Scanning for nearby beacons...'; _scanning = true; }); await _performInitialScan(); } Future _performInitialScan() async { try { // Initialize beacon monitoring await flutterBeacon.initializeScanning; // Create regions for all known UUIDs final regions = _uuidToBeaconId.keys.map((uuid) { // Format UUID with dashes for the plugin final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}'; return Region(identifier: uuid, proximityUUID: formattedUUID); }).toList(); if (regions.isEmpty) { if (mounted) _navigateToRestaurantSelect(); return; } StreamSubscription? subscription; // Start ranging for 3 seconds subscription = flutterBeacon.ranging(regions).listen((result) { for (var beacon in result.beacons) { final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', ''); final rssi = beacon.rssi; if (_uuidToBeaconId.containsKey(uuid)) { final beaconId = _uuidToBeaconId[uuid]!; // Update if new or better RSSI if (!_detectedBeacons.containsKey(uuid) || _detectedBeacons[uuid]!.value < rssi) { setState(() { _detectedBeacons[uuid] = MapEntry(beaconId, rssi); }); debugPrint('[BeaconScan] Detected: UUID=$uuid, BeaconID=$beaconId, RSSI=$rssi'); } } } }); // Wait 3 seconds await Future.delayed(const Duration(seconds: 3)); await subscription.cancel(); if (!mounted) return; if (_detectedBeacons.isEmpty) { setState(() => _status = 'No beacons nearby'); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) _navigateToRestaurantSelect(); } else { // Find beacon with highest RSSI final best = _findBestBeacon(); if (best != null) { setState(() => _status = 'Beacon detected! BeaconID=${best.value}'); await Future.delayed(const Duration(milliseconds: 800)); // TODO: Auto-select business from beacon if (mounted) _navigateToRestaurantSelect(); } else { _navigateToRestaurantSelect(); } } } catch (e) { debugPrint('[BeaconScan] Error during scan: $e'); if (mounted) { setState(() => _status = 'Scan error - continuing to manual selection'); await Future.delayed(const Duration(seconds: 1)); if (mounted) _navigateToRestaurantSelect(); } } } MapEntry? _findBestBeacon() { if (_detectedBeacons.isEmpty) return null; String? bestUUID; int bestRSSI = -200; for (final entry in _detectedBeacons.entries) { if (entry.value.value > bestRSSI) { bestRSSI = entry.value.value; bestUUID = entry.key; } } if (bestUUID != null) { final beaconId = _detectedBeacons[bestUUID]!.key; return MapEntry(bestUUID, beaconId); } return null; } void _navigateToRestaurantSelect() { Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); } void _retryPermissions() async { await BeaconPermissions.openSettings(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_scanning) const CircularProgressIndicator(color: Colors.white) else if (_permissionsGranted) const Icon(Icons.bluetooth_searching, color: Colors.white70, size: 64) else const Icon(Icons.bluetooth_disabled, color: Colors.white70, size: 64), const SizedBox(height: 24), Text( _status, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), if (_detectedBeacons.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Found ${_detectedBeacons.length} beacon(s)', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], if (!_permissionsGranted && _status.contains('denied')) ...[ const SizedBox(height: 24), FilledButton( onPressed: _retryPermissions, child: const Text('Open Settings'), ), const SizedBox(height: 12), TextButton( onPressed: _navigateToRestaurantSelect, style: TextButton.styleFrom(foregroundColor: Colors.white70), child: const Text('Skip and select manually'), ), ], ], ), ), ); } }