import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:injector/injector.dart'; import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:professional_repository/professional_repository.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/blocs/notification_bloc/notification_bloc.dart'; import 'package:prosappco/blocs/professional_list_bloc/professional_list_bloc.dart'; import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; import 'package:prosappco/constansts.dart'; import 'package:prosappco/local_notifications/local_notifications.dart'; import 'package:prosappco/screens/lists/professional_list_screen.dart'; import 'package:prosappco/screens/user/user_service_screen.dart'; import 'package:prosappco/utils/time_of_day_extension.dart'; import 'package:service_repository/service_repository.dart'; import 'package:setting_repository/setting_repository.dart'; import 'package:url_launcher/url_launcher.dart'; class UserMapScreen extends StatefulWidget { const UserMapScreen({super.key}); @override State createState() => _UserMapScreenState(); } class _UserMapScreenState extends State { final Completer _mapController = Completer(); LatLng? _currentP; final TextEditingController _addressController = TextEditingController(); final TextEditingController _observationController = TextEditingController(); final settingRepository = Injector.appInstance.get(); SettingEntity? settings; late String appVersion; final DateFormat formatter = DateFormat('dd/MM/yyyy'); LatLng coordenadas = const LatLng(7.1253900, -73.1198000); DateTime? fechaSeleccionada; TimeOfDay? horaSeleccionada; ServiceLocationPreferences? serviceLocationPreference; UserProfessional? profesionalSeleccionado; Map polylines = {}; bool isClearButtonVisible = false; bool isLoading = false; Set markers = {}; BitmapDescriptor? _markerIcon; final Uri _urlIos = Uri.parse('https://apps.apple.com/co/app/prosapp/id6469028900'); final Uri _urlAndroid = Uri.parse( 'https://play.google.com/store/apps/details?id=com.prosapp.prosapp'); @override void initState() { super.initState(); _loadSettings(); getLocation(); if (Platform.isAndroid) { BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(2, 2)), 'images/pro_marke_android.png', ).then((icon) { setState(() { _markerIcon = icon; }); }); } else { BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(1, 1)), 'images/pro_marke.png', ).then((icon) { setState(() { _markerIcon = icon; }); }); } } // void requestNotificationPermission() async { // context.read().requestPermission(); // } _loadSettings() { settingRepository.getSettings().then( (value) => setState(() { settings = value; _getAppVersion(); }), ); } void _getAppVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); String version = packageInfo.version; if (mounted) { setState(() { appVersion = version; }); } if (settings != null) { _checkForUpdate(settings!); } } void _checkForUpdate(SettingEntity settings) { if (Platform.isAndroid) { if (appVersion != settings.versionAndroid) { showDialog( context: context, builder: (context) { return WillPopScope( onWillPop: () async { return false; }, child: Center( child: AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), title: const Text( "Debes Actualizar la Aplicación", style: TextStyle(fontWeight: FontWeight.bold), ), content: const Text( "Tienes una versión desactualizada de nuestra aplicación. Te recomendamos actualizarla para disfrutar de las últimas características y mejoras.", ), actions: [ TextButton( child: const Text( "Actualizar", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 20), ), onPressed: () { launchUrl(_urlAndroid); }, ), ], ), ), ); }, ); } } if (Platform.isIOS) { if (appVersion != settings.versionIos) { showDialog( context: context, builder: (context) { return WillPopScope( onWillPop: () async { return false; }, child: Center( child: AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), title: const Text( "Debes Actualizar la Aplicación", style: TextStyle(fontWeight: FontWeight.bold), ), content: const Text( "Tienes una versión desactualizada de nuestra aplicación. Te recomendamos actualizarla para disfrutar de las últimas características y mejoras.", ), actions: [ TextButton( child: const Text( "Actualizar", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 20), ), onPressed: () { launchUrl(_urlIos); }, ), ], ), ), ); }, ); } } } @override Widget build(BuildContext context) { return BlocProvider( create: (context) => Injector.appInstance.get(), child: BlocConsumer( listener: (context, serviceState) { if (serviceState is CreateServiceLoading) { isLoading = true; } if (serviceState is CreateServiceFailure) { isLoading = false; } if (serviceState is CreateServiceSuccess) { isLoading = false; Navigator.push( context, CupertinoPageRoute( builder: (BuildContext context) { return UserServiceScreen(serviceId: serviceState.serviceId); }, ), ); } }, builder: (context, state) { return BlocBuilder( builder: (context, myUserState) { return Column( children: [ Expanded( child: Stack( children: [ GoogleMap( myLocationEnabled: true, polylines: Set.of(polylines.values), onMapCreated: (GoogleMapController controller) { _mapController.complete(controller); }, markers: { ...markers, Marker( markerId: const MarkerId('currentLocation'), position: _currentP ?? const LatLng(0, 0), ), }, onCameraIdle: () { getLocationName( coordenadas.latitude, coordenadas.longitude) .then((value) => setState(() { _addressController.text = value; })); }, onCameraMove: (position) { if (serviceLocationPreference != ServiceLocationPreferences.office) { coordenadas = position.target; } }, initialCameraPosition: const CameraPosition( target: LatLng(7.1253900, -73.1198000), zoom: 16, ), myLocationButtonEnabled: false, ), Positioned( top: 10, left: 0, child: Builder( builder: (context) { return ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.white, shape: const CircleBorder(), elevation: 3, minimumSize: const Size(50, 50), ), child: Icon( Icons.menu, color: Colors.black, size: context.select( (NotificationBloc bloc) => bloc.state.status.index > 0 ? 35 : 35, ), ), onPressed: () { Scaffold.of(context).openDrawer(); }, ); }, ), ), const Positioned( bottom: 30, right: 0, left: 0, top: 0, child: Icon( Icons.location_on, size: 40, color: Color(0xFFFF0000), ), ), Positioned( top: 10, right: 10, child: FloatingActionButton( onPressed: () async { try { Position position = await _determinePosition(); setState(() { _currentP = LatLng( position.latitude, position.longitude, ); }); _animateCameraToPosition(_currentP!); } catch (e) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Por favor activa la ubicacion'), ), ); } }, elevation: 0, child: const Icon( Icons.gps_fixed, size: 30, ), ), ), ], ), ), buildBottom(context, myUserState), ], ); }, ); }, ), ); } Container buildBottom(BuildContext context, MyUserState state) { return Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: TextEditingController( text: profesionalSeleccionado == null ? '' : '${profesionalSeleccionado?.myUser.name ?? ''} - ${profesionalSeleccionado?.professionalInfo.profession ?? ''}', ), readOnly: true, onTap: () async { if (state.user?.name == null || state.user?.name == '' || state.user?.phone == '' || state.user?.phone == null) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Por favor completa tu perfil y agrega tu celular'), ), ); return; } var datos = await Navigator.push(context, CupertinoPageRoute(builder: (context) { return const ProfessionalListScreen(); })); if (datos != null) { polylines.clear(); fechaSeleccionada = datos[0]; horaSeleccionada = datos[1]; serviceLocationPreference = datos[2]; profesionalSeleccionado = datos[3]; setState(() {}); if (serviceLocationPreference == ServiceLocationPreferences.office) { _addressController.text = profesionalSeleccionado?.professionalInfo.address ?? ''; _animateCameraToPosition( LatLng( profesionalSeleccionado!.professionalInfo.latitude, profesionalSeleccionado!.professionalInfo.longitude, ), ); coordenadas = LatLng( profesionalSeleccionado!.professionalInfo.latitude, profesionalSeleccionado!.professionalInfo.longitude, ); if (_currentP != null) { getPolylinePoints( _currentP!, LatLng( profesionalSeleccionado!.professionalInfo.latitude, profesionalSeleccionado!.professionalInfo.longitude, )).then( (coordinates) => { generatePolyLineFromPoints(coordinates), }, ); } markers.add( Marker( icon: _markerIcon!, markerId: MarkerId( profesionalSeleccionado!.professionalInfo.id), position: LatLng( profesionalSeleccionado!.professionalInfo.latitude, profesionalSeleccionado!.professionalInfo.longitude, ), ), ); } else {} isClearButtonVisible = true; } }, decoration: const InputDecoration( prefixIcon: Icon(Icons.assignment_ind_rounded), suffixIcon: Icon(Icons.arrow_drop_down), hintText: 'Selecciona un profesional', ), ), const SizedBox(height: 15), TextField( readOnly: true, controller: _addressController, decoration: const InputDecoration( prefixIcon: Icon(Icons.location_on), hintText: 'Dirección', ), ), Visibility( visible: fechaSeleccionada != null && horaSeleccionada != null, child: Column( children: [ const SizedBox(height: 15), Row( children: [ Expanded( child: TextField( readOnly: true, controller: TextEditingController( text: fechaSeleccionada == null ? '' : formatter.format(fechaSeleccionada!), ), decoration: const InputDecoration( prefixIcon: Icon(Icons.calendar_month), hintText: 'Fecha', ), ), ), Expanded( child: TextField( readOnly: true, controller: TextEditingController( text: horaSeleccionada == null ? '' : ScheduleEntity.getFormatTime(horaSeleccionada), ), decoration: const InputDecoration( prefixIcon: Icon(Icons.watch_later_outlined), hintText: 'Hora', ), ), ), ], ), const SizedBox(height: 15), TextField( controller: _observationController, decoration: const InputDecoration( prefixIcon: Icon(Icons.message_outlined), hintText: 'Observaciones', ), ), ], ), ), const SizedBox(height: 15), Row( children: [ Expanded( child: FilledButton( onPressed: isLoading ? null : () { if (profesionalSeleccionado == null) return; if (state.user?.name == null || state.user?.name == '' || state.user?.phone == '' || state.user?.phone == null) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Por favor completa tu perfil y agrega tu celular'), ), ); return; } if (serviceLocationPreference == ServiceLocationPreferences.office) { if (settings?.tarifas == true) { context.read().add( CreateService( professionalId: profesionalSeleccionado! .professionalInfo.id, userId: state.user!.id, address: profesionalSeleccionado! .professionalInfo.address, aditionalAddress: profesionalSeleccionado! .professionalInfo.aditionalAddress, latitude: profesionalSeleccionado! .professionalInfo.latitude, longitude: profesionalSeleccionado! .professionalInfo.longitude, day: fechaSeleccionada.toString(), createdAt: DateTime.now().toIso8601String(), description: _observationController.text, range1Hour1: horaSeleccionada!, range1Hour2: horaSeleccionada!.add(hour: 2), rate: profesionalSeleccionado! .professionalInfo.rate, location: serviceLocationPreference!, ), ); } else { context.read().add( CreateService( professionalId: profesionalSeleccionado! .professionalInfo.id, userId: state.user!.id, address: profesionalSeleccionado! .professionalInfo.address, aditionalAddress: profesionalSeleccionado! .professionalInfo.aditionalAddress, latitude: profesionalSeleccionado! .professionalInfo.latitude, longitude: profesionalSeleccionado! .professionalInfo.longitude, day: fechaSeleccionada.toString(), createdAt: DateTime.now().toIso8601String(), description: _observationController.text, range1Hour1: horaSeleccionada!, range1Hour2: horaSeleccionada!.add(hour: 2), rate: '0', location: serviceLocationPreference!, ), ); } } else if (serviceLocationPreference == ServiceLocationPreferences.delivery) { if (settings?.tarifas == true) { context.read().add( CreateService( professionalId: profesionalSeleccionado! .professionalInfo.id, userId: state.user!.id, address: _addressController.text, aditionalAddress: '', latitude: 0, longitude: 0, day: fechaSeleccionada.toString(), createdAt: DateTime.now().toIso8601String(), description: _observationController.text, range1Hour1: horaSeleccionada!, range1Hour2: horaSeleccionada!.add(hour: 2), rate: profesionalSeleccionado! .professionalInfo.rate, location: serviceLocationPreference!, ), ); } else { context.read().add( CreateService( professionalId: profesionalSeleccionado!.professionalInfo.id, userId: state.user!.id, address: _addressController.text, aditionalAddress: '', latitude: 0, longitude: 0, day: fechaSeleccionada.toString(), createdAt: DateTime.now().toIso8601String(), description: _observationController.text, range1Hour1: horaSeleccionada!, range1Hour2: horaSeleccionada!.add(hour: 2), rate: '0', location: serviceLocationPreference!, ), ); } } else { // TODO: error inesperado } if (profesionalSeleccionado!.myUser.token != null) { LocalNotifications.sendPushNotification( profesionalSeleccionado!.myUser.token!, 'Nuevo servicio', 'Tienes una nueva solicitud de servicio pendiente', ); } fechaSeleccionada = null; horaSeleccionada = null; profesionalSeleccionado = null; isClearButtonVisible = false; _observationController.text = ''; serviceLocationPreference = null; polylines.clear(); markers.clear(); setState(() {}); }, style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: const Text( 'Pedir cita', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), Visibility( visible: isClearButtonVisible, child: const SizedBox(width: 10), ), Visibility( visible: isClearButtonVisible, child: FilledButton( onPressed: () { fechaSeleccionada = null; horaSeleccionada = null; profesionalSeleccionado = null; isClearButtonVisible = false; _observationController.text = ''; serviceLocationPreference = null; polylines.clear(); markers.clear(); setState(() {}); }, style: FilledButton.styleFrom( backgroundColor: Colors.red, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: const Icon( CupertinoIcons.xmark, color: Colors.white, size: 30, ), ), ), ], ) ], ), ); } Future _animateCameraToPosition(LatLng position) async { final GoogleMapController controller = await _mapController.future; controller.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: position, zoom: 17.5, ), ), ); } Future> getPolylinePoints( LatLng originP, LatLng destinationP) async { List polylineCoordinates = []; PolylinePoints polylinePoints = PolylinePoints(); PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( GOOGLE_MAPS_API_KEY, PointLatLng(originP.latitude, originP.longitude), PointLatLng(destinationP.latitude, destinationP.longitude), travelMode: TravelMode.driving, ); if (result.points.isNotEmpty) { for (var point in result.points) { polylineCoordinates.add(LatLng(point.latitude, point.longitude)); } } else { print(result.errorMessage); } return polylineCoordinates; } void getLocation() async { try { Position position = await _determinePosition(); setState(() { _currentP = LatLng( position.latitude, position.longitude, ); }); _animateCameraToPosition(_currentP!); } catch (e) { log(e.toString()); } } void generatePolyLineFromPoints(List polylineCoordinates) async { PolylineId id = const PolylineId("poly"); Polyline polyline = Polyline( polylineId: id, color: Colors.blue, points: polylineCoordinates, width: 6, ); setState(() { polylines[id] = polyline; }); } Future getLocationName(double latitude, double longitude) async { String address; List placemarks = await placemarkFromCoordinates(latitude, longitude); Placemark place = placemarks[0]; if (place.thoroughfare != '' || place.subThoroughfare != '') { address = "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}"; } else { address = ''; } return address; } Future _determinePosition() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { return Future.error('Location services are disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { return Future.error('Location permission denied'); } } if (permission == LocationPermission.deniedForever) { return Future.error('Location permissions are permanently denied'); } Position position = await Geolocator.getCurrentPosition(); return position; } }