/services/public-calendar also returns the professional's current opening hours; the client threw them away and drew the day from the snapshot the search list carried, which can be days old. It now uses the fresh ones. Between picking an hour and pressing "Pedir cita" another patient can take the slot. The confirm step asks the agenda again and, if the hour is gone, says so and clears the selection instead of creating a service the professional would have to deny. A failed re-check lets the booking through: a network hiccup should not block a patient. Also: - tapping a busy slot with no id explains itself instead of doing nothing - the profile refuses to save an empty rate with the rate switch on, which was reaching the backend as rate: '' Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
920 lines
36 KiB
Dart
920 lines
36 KiB
Dart
import 'dart:async';
|
|
import 'package:prosappco/utils/slot_generator.dart';
|
|
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/profile/profile_screen.dart';
|
|
import 'package:prosappco/screens/user/user_service_screen.dart';
|
|
import 'package:prosappco/utils/nominatim_geocoder.dart';
|
|
import 'package:prosappco/utils/service_day.dart';
|
|
import 'package:prosappco/utils/service_day_param.dart';
|
|
import 'package:prosappco/utils/version_utils.dart';
|
|
import 'package:service_repository/service_repository.dart';
|
|
import 'package:service_repository/service_repository.dart' as srv;
|
|
import 'package:setting_repository/setting_repository.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class UserMapScreen extends StatefulWidget {
|
|
const UserMapScreen({super.key});
|
|
|
|
@override
|
|
State<UserMapScreen> createState() => _UserMapScreenState();
|
|
}
|
|
|
|
class _UserMapScreenState extends State<UserMapScreen> {
|
|
final _serviceRepository = Injector.appInstance.get<ApiServiceRepository>();
|
|
final Completer<GoogleMapController> _mapController =
|
|
Completer<GoogleMapController>();
|
|
LatLng? _currentP;
|
|
|
|
final TextEditingController _addressController = TextEditingController();
|
|
final TextEditingController _observationController = TextEditingController();
|
|
|
|
final settingRepository = Injector.appInstance.get<SettingRepository>();
|
|
SettingEntity? settings;
|
|
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<PolylineId, Polyline> polylines = {};
|
|
|
|
bool isClearButtonVisible = false;
|
|
|
|
bool isLoading = false;
|
|
|
|
Set<Marker> 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<NotificationBloc>().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 (isUpdateRequired(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 (isUpdateRequired(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);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// Re-reads the professional's agenda right before creating the service.
|
|
///
|
|
/// Returns true when the slot is still free, or when the agenda cannot be
|
|
/// read: blocking a booking because the network hiccuped would be worse
|
|
/// than the rare collision the backend can still reject.
|
|
Future<bool> _slotStillFree() async {
|
|
final pro = profesionalSeleccionado;
|
|
final day = fechaSeleccionada;
|
|
final hour = horaSeleccionada;
|
|
if (pro == null || day == null || hour == null) return false;
|
|
try {
|
|
final calendar = await _serviceRepository
|
|
.getPublicCalendar(pro.professionalInfo.recordId);
|
|
final taken = calendar.services.any((s) =>
|
|
s.status != srv.ServiceStatus.cancelled &&
|
|
s.status != srv.ServiceStatus.denied &&
|
|
isSameServiceDay(day, s.day) &&
|
|
s.range1Hour1 == hour);
|
|
if (!taken) return true;
|
|
if (!mounted) return false;
|
|
ScaffoldMessenger.of(context).clearSnackBars();
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
content: Text('Ese horario acaba de ocuparse. Elige otro.'),
|
|
));
|
|
setState(() => horaSeleccionada = null);
|
|
return false;
|
|
} catch (_) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider<ServiceBloc>(
|
|
create: (context) => Injector.appInstance.get<ServiceBloc>(),
|
|
child: BlocConsumer<ServiceBloc, ServiceState>(
|
|
listener: (context, serviceState) {
|
|
if (serviceState is CreateServiceLoading) {
|
|
isLoading = true;
|
|
}
|
|
if (serviceState is CreateServiceFailure) {
|
|
isLoading = false;
|
|
ScaffoldMessenger.of(context).clearSnackBars();
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
content: Text(
|
|
'No pudimos agendar tu cita. Revisa tu conexión e inténtalo otra vez.'),
|
|
));
|
|
}
|
|
|
|
if (serviceState is CreateServiceSuccess) {
|
|
isLoading = false;
|
|
|
|
final token = profesionalSeleccionado?.myUser.token;
|
|
if (token != null) {
|
|
LocalNotifications.sendPushNotification(
|
|
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();
|
|
|
|
Navigator.push(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (BuildContext context) {
|
|
return UserServiceScreen(serviceId: serviceState.serviceId);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
return BlocBuilder<MyUserBloc, MyUserState>(
|
|
builder: (context, myUserState) {
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: Stack(
|
|
children: [
|
|
GoogleMap(
|
|
myLocationEnabled: true,
|
|
polylines: Set<Polyline>.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(
|
|
SnackBar(
|
|
content: const Text('Por favor completa tu perfil y agrega tu celular'),
|
|
action: SnackBarAction(
|
|
label: 'Ir al perfil',
|
|
onPressed: () {
|
|
Navigator.push(context, CupertinoPageRoute(
|
|
builder: (context) => const ProfileScreen(),
|
|
));
|
|
},
|
|
),
|
|
),
|
|
);
|
|
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
|
|
: () async {
|
|
// Used to `return` in silence when anything was
|
|
// missing, so the main button simply did nothing.
|
|
final faltan = <String>[
|
|
if (profesionalSeleccionado == null)
|
|
'un profesional',
|
|
if (fechaSeleccionada == null) 'la fecha',
|
|
if (horaSeleccionada == null) 'la hora',
|
|
];
|
|
if (faltan.isNotEmpty) {
|
|
ScaffoldMessenger.of(context).clearSnackBars();
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text(
|
|
'Falta ${faltan.join(' y ')} para pedir la cita'),
|
|
));
|
|
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;
|
|
}
|
|
|
|
// Minutes can pass between picking the hour and
|
|
// confirming. Ask the agenda again so two patients
|
|
// do not walk away holding the same slot.
|
|
if (!await _slotStillFree()) return;
|
|
if (!context.mounted) return;
|
|
|
|
if (serviceLocationPreference ==
|
|
ServiceLocationPreferences.office) {
|
|
if (settings?.tarifas == true) {
|
|
context.read<ServiceBloc>().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: serviceDayParam(fechaSeleccionada!),
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
description: _observationController.text,
|
|
range1Hour1: horaSeleccionada!,
|
|
range1Hour2: SlotGenerator.endOf(
|
|
horaSeleccionada!,
|
|
profesionalSeleccionado!
|
|
.professionalInfo
|
|
.slotDurationMinutes),
|
|
rate: profesionalSeleccionado!
|
|
.professionalInfo.rate,
|
|
location: serviceLocationPreference!,
|
|
),
|
|
);
|
|
} else {
|
|
context.read<ServiceBloc>().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: serviceDayParam(fechaSeleccionada!),
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
description: _observationController.text,
|
|
range1Hour1: horaSeleccionada!,
|
|
range1Hour2: SlotGenerator.endOf(
|
|
horaSeleccionada!,
|
|
profesionalSeleccionado!
|
|
.professionalInfo
|
|
.slotDurationMinutes),
|
|
rate: '0',
|
|
location: serviceLocationPreference!,
|
|
),
|
|
);
|
|
}
|
|
} else if (serviceLocationPreference ==
|
|
ServiceLocationPreferences.delivery) {
|
|
if (settings?.tarifas == true) {
|
|
context.read<ServiceBloc>().add(
|
|
CreateService(
|
|
professionalId: profesionalSeleccionado!
|
|
.professionalInfo.id,
|
|
userId: state.user!.id,
|
|
address: _addressController.text,
|
|
aditionalAddress: '',
|
|
latitude: 0,
|
|
longitude: 0,
|
|
day: serviceDayParam(fechaSeleccionada!),
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
description: _observationController.text,
|
|
range1Hour1: horaSeleccionada!,
|
|
range1Hour2: SlotGenerator.endOf(
|
|
horaSeleccionada!,
|
|
profesionalSeleccionado!
|
|
.professionalInfo
|
|
.slotDurationMinutes),
|
|
rate: profesionalSeleccionado!
|
|
.professionalInfo.rate,
|
|
location: serviceLocationPreference!,
|
|
),
|
|
);
|
|
} else {
|
|
context.read<ServiceBloc>().add(
|
|
CreateService(
|
|
professionalId: profesionalSeleccionado!.professionalInfo.id,
|
|
userId: state.user!.id,
|
|
address: _addressController.text,
|
|
aditionalAddress: '',
|
|
latitude: 0,
|
|
longitude: 0,
|
|
day: serviceDayParam(fechaSeleccionada!),
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
description: _observationController.text,
|
|
range1Hour1: horaSeleccionada!,
|
|
range1Hour2: SlotGenerator.endOf(
|
|
horaSeleccionada!,
|
|
profesionalSeleccionado!
|
|
.professionalInfo
|
|
.slotDurationMinutes),
|
|
rate: '0',
|
|
location: serviceLocationPreference!,
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
// TODO: error inesperado
|
|
}
|
|
|
|
// The form is NOT cleared here and the professional
|
|
// is NOT notified here: both used to run right after
|
|
// dispatching the event, so a failed booking still
|
|
// sent "tienes una nueva solicitud" and wiped the
|
|
// patient's date, hour and notes. See the
|
|
// CreateServiceSuccess branch in the listener.
|
|
},
|
|
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<void> _animateCameraToPosition(LatLng position) async {
|
|
final GoogleMapController controller = await _mapController.future;
|
|
controller.animateCamera(
|
|
CameraUpdate.newCameraPosition(
|
|
CameraPosition(
|
|
target: position,
|
|
zoom: 17.5,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<List<LatLng>> getPolylinePoints(
|
|
LatLng originP, LatLng destinationP) async {
|
|
List<LatLng> 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());
|
|
await _locateByUserCity();
|
|
}
|
|
}
|
|
|
|
Future<void> _locateByUserCity() async {
|
|
try {
|
|
final city = Injector.appInstance.get<MyUserBloc>().state.user?.city;
|
|
if (city == null || city.isEmpty) return;
|
|
|
|
final coords = await NominatimGeocoder.forwardGeocodeCity(city);
|
|
if (coords == null) return;
|
|
|
|
setState(() {
|
|
_currentP = LatLng(coords.$1, coords.$2);
|
|
});
|
|
|
|
_animateCameraToPosition(_currentP!);
|
|
} catch (e) {
|
|
log(e.toString());
|
|
}
|
|
}
|
|
|
|
void generatePolyLineFromPoints(List<LatLng> polylineCoordinates) async {
|
|
PolylineId id = const PolylineId("poly");
|
|
Polyline polyline = Polyline(
|
|
polylineId: id,
|
|
color: Colors.blue,
|
|
points: polylineCoordinates,
|
|
width: 6,
|
|
);
|
|
setState(() {
|
|
polylines[id] = polyline;
|
|
});
|
|
}
|
|
|
|
Future<String> getLocationName(double latitude, double longitude) async {
|
|
String address;
|
|
List<Placemark> 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<Position> _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;
|
|
}
|
|
}
|