- Fixed _addModifiersRecursively to track OrderLineItemID through recursion - Changed from hardcoded parentOrderLineItemId: 0 to actual parent IDs - Added logic to find root item's OrderLineItemID before starting recursion - Added logic to find each modifier's OrderLineItemID for its children - Fixed API_BASE_URL to AALISTS_API_BASE_URL for environment consistency - Added comprehensive debug logging for troubleshooting This fix ensures nested modifiers (e.g., Customize Spread > Extra) are properly saved to the database with correct parent-child relationships. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
224 lines
6.7 KiB
Dart
224 lines
6.7 KiB
Dart
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<BeaconScanScreen> createState() => _BeaconScanScreenState();
|
|
}
|
|
|
|
class _BeaconScanScreenState extends State<BeaconScanScreen> {
|
|
String _status = 'Initializing...';
|
|
bool _permissionsGranted = false;
|
|
bool _scanning = false;
|
|
|
|
Map<String, int> _uuidToBeaconId = {};
|
|
final Map<String, MapEntry<int, int>> _detectedBeacons = {}; // UUID -> (BeaconID, RSSI)
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_startScanFlow();
|
|
}
|
|
|
|
Future<void> _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<void> _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<RangingResult>? 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<String, int>? _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'),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|