import 'dart:math'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:url_launcher/url_launcher.dart'; import '../services/odoo_service.dart'; import '../utils/safe_cast.dart'; class BranchesScreen extends StatefulWidget { const BranchesScreen({super.key}); @override State createState() => _BranchesScreenState(); } class _BranchesScreenState extends State { List _branches = []; bool _isLoading = true; Position? _userPosition; bool _locationDenied = false; @override void initState() { super.initState(); _fetchBranchesWithLocation(); } Future _fetchBranchesWithLocation() async { setState(() => _isLoading = true); // Try to get user location Position? pos; try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (serviceEnabled) { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.whileInUse || permission == LocationPermission.always) { pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 8), ), ); } } } catch (_) { // Location not available — proceed without it } // Fetch branches from Odoo try { final branches = await OdooService.getPublicBranches(); if (mounted) { setState(() { _userPosition = pos; _locationDenied = pos == null; if (pos != null) { // Sort branches by distance from user _branches = List.from(branches) ..sort((a, b) { final p = pos!; final da = _distanceTo(p, a); final db = _distanceTo(p, b); return da.compareTo(db); }); } else { // Fallback: alphabetical sort _branches = List.from(branches) ..sort( (a, b) => (safeString(a['name']) ?? '').compareTo( safeString(b['name']) ?? '', ), ); } _isLoading = false; }); } } catch (e) { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Error loading branches. Check connection.'), ), ); } } } /// Haversine formula — returns distance in kilometres. double _distanceTo(Position pos, dynamic branch) { final lat = _toDouble(branch['partner_latitude']); final lng = _toDouble(branch['partner_longitude']); if (lat == 0.0 && lng == 0.0) return double.maxFinite; const R = 6371.0; final dLat = _degToRad(lat - pos.latitude); final dLng = _degToRad(lng - pos.longitude); final a = sin(dLat / 2) * sin(dLat / 2) + cos(_degToRad(pos.latitude)) * cos(_degToRad(lat)) * sin(dLng / 2) * sin(dLng / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } double _degToRad(double deg) => deg * (pi / 180); double _toDouble(dynamic val) { if (val == null || val == false) return 0.0; if (val is num) return val.toDouble(); return double.tryParse(val.toString()) ?? 0.0; } String _formatDistance(double km) { if (km == double.maxFinite) return ''; if (km < 1) return '${(km * 1000).toStringAsFixed(0)} m'; return '${km.toStringAsFixed(1)} km'; } Future _launchMaps(String queryTerm) async { final query = Uri.encodeComponent(queryTerm); final url = Uri.parse( 'https://www.google.com/maps/search/?api=1&query=$query', ); if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Could not open map.'))); } } } Future _launchWhatsApp(String phone) async { final cleanPhone = phone.replaceAll(RegExp(r'\D'), ''); String waPhone = cleanPhone; if (waPhone.startsWith('0')) { waPhone = '62${waPhone.substring(1)}'; } final url = Uri.parse('https://wa.me/$waPhone'); if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Could not open WhatsApp.')), ); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _fetchBranchesWithLocation, child: Column( children: [ // Location status banner if (_locationDenied) Container( width: double.infinity, color: colorScheme.surfaceContainerLow, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), child: Row( children: [ Icon( Icons.location_off, size: 16, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Expanded( child: Text( 'Location not available. Showing branches alphabetically.', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), ], ), ) else if (_userPosition != null) Container( width: double.infinity, color: colorScheme.surfaceContainerLow, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), child: Row( children: [ Icon( Icons.my_location, size: 16, color: colorScheme.secondary, ), const SizedBox(width: 8), Text( 'Sorted by distance from your location', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), ), // Branch list Expanded( child: _branches.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.5, child: const Center( child: Text( 'No branches available.', style: TextStyle(fontSize: 16), ), ), ), ], ) : ListView.builder( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), itemCount: _branches.length, itemBuilder: (context, index) { final branch = _branches[index]; final street = branch['street'] != null && branch['street'] != false ? branch['street'] : ''; final city = branch['city'] != null && branch['city'] != false ? branch['city'] : ''; final phone = branch['phone'] != null && branch['phone'] != false ? branch['phone'] : ''; final addressParts = [ street, city, ].where((e) => e.toString().isNotEmpty).join(', '); final distance = _userPosition != null ? _distanceTo(_userPosition!, branch) : null; final distanceLabel = distance != null ? _formatDistance(distance) : ''; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: ListTile( contentPadding: const EdgeInsets.all(16), onTap: () => _launchMaps( '${branch['name']} $addressParts', ), leading: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.storefront, color: colorScheme.secondary, ), ), title: Text( branch['name'] ?? 'Mapan Branch', style: theme.textTheme.titleMedium?.copyWith( fontFamily: 'serif', fontWeight: FontWeight.bold, ), ), subtitle: Padding( padding: const EdgeInsets.only(top: 6.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (distanceLabel.isNotEmpty) ...[ Row( children: [ Icon( Icons.near_me_rounded, size: 14, color: colorScheme.primary, ), const SizedBox(width: 4), Text( '$distanceLabel away', style: theme.textTheme.bodyMedium ?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 6), ], Text( addressParts.isEmpty ? 'No address specified' : addressParts, style: theme.textTheme.bodyMedium, ), if (phone.isNotEmpty) ...[ const SizedBox(height: 4), Row( children: [ Icon( Icons.phone, size: 14, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( phone, style: theme.textTheme.bodySmall ?.copyWith( color: colorScheme .onSurfaceVariant, ), ), ], ), ], ], ), ), trailing: phone.isNotEmpty ? IconButton( icon: Icon( Icons.chat_bubble, color: colorScheme.onSurface, ), onPressed: () => _launchWhatsApp(phone), tooltip: 'Chat on WhatsApp', ) : Icon( Icons.chevron_right, color: colorScheme.onSurfaceVariant, ), ), ); }, ), ), ], ), ); } }