- LocationPickerDialog: detecta ubicación del dispositivo, pin draggable, reverse geocoding para obtener dirección y ciudad - Dashboard: campo de dirección abre el mapa al tocar - index.html: carga Maps JS API (key pendiente de configurar) - pubspec: google_maps_flutter + geolocator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
395 lines
15 KiB
Dart
395 lines
15 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:prosapp_web_app/services/api_service.dart';
|
|
import 'package:prosapp_web_app/models/schedules_entity.dart';
|
|
import 'package:prosapp_web_app/models/service.dart';
|
|
import 'package:prosapp_web_app/models/service_location_preferences.dart';
|
|
import 'package:prosapp_web_app/models/service_status.dart';
|
|
import 'package:prosapp_web_app/models/usuario.dart';
|
|
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
|
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
|
import 'package:prosapp_web_app/providers/cities_provider.dart';
|
|
import 'package:prosapp_web_app/router/router.dart';
|
|
import 'package:prosapp_web_app/services/navigation_service.dart';
|
|
import 'package:prosapp_web_app/services/notifications_service.dart';
|
|
import 'package:prosapp_web_app/ui/cards/white_card.dart';
|
|
import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart';
|
|
import 'package:prosapp_web_app/ui/widgets/location_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:prosapp_web_app/utils/local_notifications.dart';
|
|
import 'package:prosapp_web_app/utils/network_utility.dart';
|
|
import 'package:prosapp_web_app/utils/time_of_day_extension.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class DashboardView extends StatefulWidget {
|
|
const DashboardView({super.key});
|
|
|
|
@override
|
|
State<DashboardView> createState() => _DashboardViewState();
|
|
}
|
|
|
|
class _DashboardViewState extends State<DashboardView> {
|
|
Usuario? user;
|
|
List<dynamic> _placesList = [];
|
|
final TextEditingController _addressController = TextEditingController();
|
|
Timer? _debounce;
|
|
UsuarioProfesional? selectedProfessional;
|
|
String? selectedProfessionalName;
|
|
DateTime? selectedDay;
|
|
TimeOfDay? selectedHour;
|
|
LatLng? _selectedLatLng;
|
|
String? _selectedCity;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
|
|
setState(() {
|
|
user = authProvider.user;
|
|
});
|
|
}
|
|
|
|
void placeAutoComplete(String query, String _coords) async {
|
|
Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", {
|
|
"input": query,
|
|
"location": _coords,
|
|
});
|
|
|
|
String? response = await NetworkUtility.fetchUrl(uri);
|
|
|
|
if (response != null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_placesList = jsonDecode(response.toString())['results'];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _selectProfessional(BuildContext context) async {
|
|
try {
|
|
List<dynamic> result = await NavigationService.navigateToFuture(
|
|
Flurorouter.professionalsRoute);
|
|
|
|
final UsuarioProfesional selectedProfessional = result[0];
|
|
|
|
print('selectedProfessional: $selectedProfessional');
|
|
|
|
setState(() {
|
|
selectedDay = result[1];
|
|
selectedHour = result[2];
|
|
selectedProfessionalName = selectedProfessional.user.name;
|
|
this.selectedProfessional = selectedProfessional;
|
|
_addressController.text = selectedProfessional.professionalInfo.address;
|
|
});
|
|
} catch (e) {
|
|
print('debugeando $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_debounce?.cancel();
|
|
_addressController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _openLocationPicker() async {
|
|
final result = await LocationPickerDialog.show(
|
|
context,
|
|
initialPosition: _selectedLatLng,
|
|
);
|
|
if (result != null && mounted) {
|
|
setState(() {
|
|
_addressController.text = result.address;
|
|
_selectedLatLng = result.position;
|
|
_selectedCity = result.city;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (user == null) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
bool isUserComplete() {
|
|
return user!.name != '' &&
|
|
user!.email != '' &&
|
|
user!.phone != '' &&
|
|
user!.city != '';
|
|
}
|
|
|
|
final citiesProvider = Provider.of<CitiesProvider>(context);
|
|
|
|
if (citiesProvider.isLoading) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
final _coords = citiesProvider.getCoordsOfCity(user!.city ?? '');
|
|
|
|
_createService() async {
|
|
print('debug ${selectedProfessional?.user.id ?? 'user.id'}');
|
|
print('debug ${user!.id ?? 'user.id'}');
|
|
print('debug ${_addressController.text ?? 'user.id'}');
|
|
|
|
try {
|
|
double latitude = 0.0;
|
|
double longitude = 0.0;
|
|
|
|
print('debug 1');
|
|
|
|
Service service = Service(
|
|
id: null,
|
|
professionalId: selectedProfessional!.user.id,
|
|
professionalScored: false,
|
|
userId: user!.id,
|
|
userScored: false,
|
|
address: _addressController.text,
|
|
aditionalAddress: '',
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
day: selectedDay.toString(),
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
description: '',
|
|
range1Hour1: selectedHour!,
|
|
range1Hour2: selectedHour!.add(hour: 2),
|
|
rate: '',
|
|
status: ServiceStatus.pending,
|
|
location: ServiceLocationPreferences.delivery,
|
|
);
|
|
|
|
print('debug 1');
|
|
|
|
await ApiService.instance.post('/services', service.toDocument());
|
|
|
|
print('debug 2');
|
|
|
|
NotificationsService.showSnackbar('Servicio solicitado exitosamente');
|
|
|
|
if (selectedProfessional != null) {
|
|
if (selectedProfessional!.user.token != null) {
|
|
LocalNotifications.sendPushNotification(
|
|
selectedProfessional!.user.token!,
|
|
'Nuevo servicio',
|
|
'Tienes una nueva solicitud de servicio pendiente',
|
|
);
|
|
}
|
|
}
|
|
|
|
_addressController.clear();
|
|
setState(() {
|
|
selectedProfessional = null;
|
|
selectedProfessionalName = null;
|
|
selectedDay = null;
|
|
selectedHour = null;
|
|
});
|
|
} catch (e) {
|
|
NotificationsService.showSnackBarError(
|
|
'$e Error al solicitar el servicio, intenta de nuevo');
|
|
}
|
|
}
|
|
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 900),
|
|
child: Stack(
|
|
children: [
|
|
ListView(
|
|
physics: const ClampingScrollPhysics(),
|
|
children: [
|
|
WhiteCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 10),
|
|
GestureDetector(
|
|
onTap: isUserComplete() ? _openLocationPicker : () =>
|
|
NotificationsService.showSnackBarError('Completa tu perfil para solicitar un servicio'),
|
|
child: AbsorbPointer(
|
|
child: TextFormField(
|
|
controller: _addressController,
|
|
decoration: CustomInputs.formInputDecoration(
|
|
hint: 'Toca para seleccionar tu dirección',
|
|
label: 'Dirección',
|
|
icon: Icons.location_on,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
GestureDetector(
|
|
onTap: () => isUserComplete()
|
|
? _selectProfessional(context)
|
|
: NotificationsService.showSnackBarError(
|
|
'Completa tu perfil para solicitar un servicio'),
|
|
child: AbsorbPointer(
|
|
child: TextFormField(
|
|
controller: TextEditingController(
|
|
text: selectedProfessionalName,
|
|
),
|
|
decoration: CustomInputs.formInputDecoration(
|
|
hint: 'Selecciona un Profesional',
|
|
label: 'Profesional',
|
|
icon: Icons.person_rounded,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (selectedDay != null && selectedHour != null) ...[
|
|
TextFormField(
|
|
readOnly: true,
|
|
controller: TextEditingController(
|
|
text: selectedDay == null
|
|
? ''
|
|
: DateFormat('dd/MM/yyyy').format(selectedDay!),
|
|
),
|
|
decoration: CustomInputs.formInputDecoration(
|
|
hint: 'Fecha',
|
|
label: 'Fecha',
|
|
icon: Icons.calendar_month,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
TextFormField(
|
|
readOnly: true,
|
|
controller: TextEditingController(
|
|
text: selectedHour == null
|
|
? ''
|
|
: ScheduleEntity.getFormatTime(selectedHour),
|
|
),
|
|
decoration: CustomInputs.formInputDecoration(
|
|
hint: 'Hora',
|
|
label: 'Hora',
|
|
icon: Icons.watch_later_outlined,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
],
|
|
const SizedBox(height: 10),
|
|
Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 230),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
if (!isUserComplete()) {
|
|
NotificationsService.showSnackBarError(
|
|
'Completa tu perfil para solicitar un servicio');
|
|
|
|
return;
|
|
}
|
|
|
|
_createService();
|
|
},
|
|
style: ButtonStyle(
|
|
backgroundColor: WidgetStateProperty.all(
|
|
Colors.blue.shade400),
|
|
shape: WidgetStateProperty.all(
|
|
const RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.all(Radius.circular(5)),
|
|
)),
|
|
shadowColor: WidgetStateProperty.all(
|
|
Colors.transparent),
|
|
),
|
|
child: const Text(
|
|
'Solicitar cita',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
if (selectedDay != null &&
|
|
selectedHour != null &&
|
|
selectedProfessional != null) ...[
|
|
const SizedBox(width: 10),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
_addressController.clear();
|
|
selectedProfessional = null;
|
|
selectedDay = null;
|
|
selectedHour = null;
|
|
|
|
setState(() {});
|
|
},
|
|
style: ButtonStyle(
|
|
backgroundColor: WidgetStateProperty.all(
|
|
Colors.red,
|
|
),
|
|
shape: WidgetStateProperty.all(
|
|
const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(5)))),
|
|
shadowColor: WidgetStateProperty.all(
|
|
Colors.transparent)),
|
|
child: const Icon(
|
|
Icons.close_rounded,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Positioned(
|
|
left: 20,
|
|
right: 20,
|
|
top: 80,
|
|
child: _placesList.isNotEmpty
|
|
? Material(
|
|
elevation: 5.0,
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: _placesList.length,
|
|
itemBuilder: (context, index) {
|
|
return ListTile(
|
|
title:
|
|
Text(_placesList[index]['formatted_address']),
|
|
dense: true,
|
|
visualDensity: VisualDensity.compact,
|
|
onTap: () {
|
|
setState(() {
|
|
_addressController.text =
|
|
_placesList[index]['formatted_address'];
|
|
_placesList = [];
|
|
});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
)
|
|
: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|