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 '../theme/app_theme.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) => (a['name'] as String? ?? '') .compareTo(b['name'] as String? ?? '')); } _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 ? 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: Row( children: [ Expanded( child: Text( branch['name'] ?? 'Mapan Branch', style: theme .textTheme .titleMedium ?.copyWith( fontFamily: 'serif', fontWeight: FontWeight.bold, ), ), ), if (distanceLabel.isNotEmpty) Container( margin: const EdgeInsets.only(left: 8), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(12), ), child: Text( distanceLabel, style: theme .textTheme .labelLarge ?.copyWith( color: colorScheme.onSecondaryContainer, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), ), ); }, ), ), ], ), ); } }