- Remove `get` package (incompatible with Flutter 3.44/Dart 3.12 due to removed ThemeData.backgroundColor and final IconData class) - Replace GetMaterialApp → MaterialApp with GlobalKey navigatorKey - Convert AuthenticationRepository and all 7 controllers from GetxController to plain singletons - Replace Get.snackbar/offAll/to/back/defaultDialog with app_navigator helpers and native Flutter APIs - Add ApiService.baseUrl static + parseJson method - Add UserModel.getUser static method - Add UserProvider.score via ScoresModel API - Fix user?.photo → user?.picture in drawer_menu, service_after - Fix logout(uid!) → logout() in drawer_menu - Fix photo_view_web.dart missing dart:typed_data import - Remove getCoordsOfCity call from ubicacion.dart - Fix UserModel.getUser?.name null-safety in map/service.dart - Upgrade: google_maps_flutter ^2.9.0, url_launcher ^6.3.0, font_awesome_flutter ^10.8.0, image_picker ^1.1.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
676 lines
30 KiB
Dart
676 lines
30 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:prosappco/src/utils/app_navigator.dart';
|
|
import 'package:prosappco/src/authentication/authentication_repository.dart';
|
|
import 'package:prosappco/src/components/photo_view.dart';
|
|
import 'package:prosappco/src/components/pop_appbar.dart';
|
|
import 'package:prosappco/src/models/event_model.dart';
|
|
import 'package:prosappco/src/models/scores_model.dart';
|
|
import 'package:prosappco/src/models/setting_model.dart';
|
|
import 'package:prosappco/src/presentation/screens/chat.dart';
|
|
import 'package:prosappco/src/presentation/screens/score.dart';
|
|
import 'package:prosappco/src/services/api_service.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:community_material_icon/community_material_icon.dart';
|
|
|
|
class CitaScreen extends StatefulWidget {
|
|
final Event evento;
|
|
const CitaScreen({super.key, required this.evento});
|
|
|
|
@override
|
|
State<CitaScreen> createState() => _CitaScreenState();
|
|
}
|
|
|
|
class _CitaScreenState extends State<CitaScreen> {
|
|
final uid = AuthenticationRepository.instance.getCurrentUserUid();
|
|
DateTime today = DateTime.now();
|
|
String nombre = '';
|
|
String numberPhone = '';
|
|
String? photoUrl;
|
|
ScoresModel? scoresModel;
|
|
bool? ver = true;
|
|
bool? pro;
|
|
|
|
String formatCurrency(int number) {
|
|
final formatter =
|
|
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
|
|
return '\$${formatter.format(number)}';
|
|
}
|
|
|
|
Future<void> _openMap(double lat, double lng) async {
|
|
final Uri url =
|
|
Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng');
|
|
if (!await launchUrl(url)) {
|
|
throw Exception('Could not launch $url');
|
|
}
|
|
}
|
|
|
|
Future<void> _sendWhatsapp(String phoneNumber) async {
|
|
final whatsappUrl =
|
|
'https://wa.me/$phoneNumber?text=${Uri.parse('Hola! me contactaste por Prossapp')}';
|
|
if (!await launch(whatsappUrl)) {
|
|
throw Exception('Could not launch $whatsappUrl');
|
|
}
|
|
}
|
|
|
|
SettingModel? settings;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
if (settings == null) {
|
|
SettingModel.getSettings().then(
|
|
(SettingModel value) => setState(() => settings = value),
|
|
);
|
|
}
|
|
|
|
if (scoresModel == null) {
|
|
if (uid != widget.evento.userId) {
|
|
ScoresModel.scoreTo(widget.evento.userId, false, false).then(
|
|
(ScoresModel s) => setState(() {
|
|
scoresModel = s;
|
|
pro = true;
|
|
}),
|
|
);
|
|
} else {
|
|
ScoresModel.scoreTo(widget.evento.professionalId, true, false).then(
|
|
(ScoresModel s) => setState(() {
|
|
scoresModel = s;
|
|
pro = false;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
_loadOtherUser();
|
|
}
|
|
|
|
Future<void> _loadOtherUser() async {
|
|
final targetId = (uid != widget.evento.userId)
|
|
? widget.evento.userId
|
|
: widget.evento.professionalId;
|
|
try {
|
|
final Map<String, dynamic> data =
|
|
await ApiService.instance.get('/users/$targetId');
|
|
if (mounted) {
|
|
setState(() {
|
|
nombre = data['name'] ?? '';
|
|
photoUrl = data['picture'];
|
|
numberPhone = data['phone'] ?? '';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('Error loading user: $e');
|
|
}
|
|
}
|
|
|
|
static const _toBackendStatus = {
|
|
'pendiente': 'pending',
|
|
'aprobado': 'accepted',
|
|
'denegado': 'cancelled',
|
|
'iniciado': 'active',
|
|
'terminado': 'completed',
|
|
};
|
|
|
|
Future<void> _updateStatus(String status) async {
|
|
try {
|
|
final backendStatus = _toBackendStatus[status] ?? status;
|
|
await ApiService.instance
|
|
.patch('/services/${widget.evento.id}/status', {'status': backendStatus});
|
|
} catch (e) {
|
|
print('Error updating service status: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day);
|
|
|
|
return Scaffold(
|
|
appBar: PopAppbar(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
label: 'Servicio'),
|
|
body: Column(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
leading: ReferencePhoto(
|
|
ref: photoUrl,
|
|
size: 50,
|
|
sizeCircle: 50,
|
|
sizeIcon: 35,
|
|
),
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
nombre,
|
|
style: const TextStyle(
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
Text(
|
|
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(widget.evento.day))} ${DateFormat('h:mm a').format(DateTime.parse(widget.evento.range1Hour1))}',
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 16,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
subtitle: Row(
|
|
children: [
|
|
RatingBar.builder(
|
|
initialRating: scoresModel?.average ?? 0,
|
|
minRating: 1,
|
|
direction: Axis.horizontal,
|
|
allowHalfRating: true,
|
|
itemCount: 5,
|
|
itemSize: 25,
|
|
maxRating: 5,
|
|
itemPadding: const EdgeInsets.symmetric(horizontal: 0),
|
|
itemBuilder: (context, _) => const Icon(
|
|
Icons.star,
|
|
color: Color(0xFF2BA4EC),
|
|
),
|
|
onRatingUpdate: (rating) {},
|
|
ignoreGestures: true,
|
|
),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
'(${scoresModel?.total.toString()}) ${scoresModel?.average.toStringAsFixed(1)}'),
|
|
],
|
|
),
|
|
),
|
|
widget.evento.userId == widget.evento.professionalId
|
|
? const SizedBox()
|
|
: Container(
|
|
margin: const EdgeInsets.only(
|
|
left: 40, right: 40, top: 20, bottom: 20),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20, vertical: 15),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFD6F4FF),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.5),
|
|
spreadRadius: 1,
|
|
blurRadius: 5,
|
|
offset: const Offset(1, 3),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
size: 27,
|
|
color: Colors.black54,
|
|
),
|
|
const SizedBox(width: 15),
|
|
widget.evento.ubicacion != 'sitio'
|
|
? const Text(
|
|
'Servicio a domicilio.',
|
|
style: TextStyle(
|
|
color: Colors.black, fontSize: 14),
|
|
)
|
|
: const Text(
|
|
'Servicio en su sitio / consultorio',
|
|
style: TextStyle(
|
|
color: Colors.black, fontSize: 14),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
settings?.tarifas == true && widget.evento.tarifa != 0
|
|
? Column(
|
|
children: [
|
|
Text(
|
|
formatCurrency(widget.evento.tarifa ?? 0),
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600, fontSize: 25),
|
|
),
|
|
const Text('Tarifa consulta',
|
|
style: TextStyle(fontSize: 15)),
|
|
],
|
|
)
|
|
: const SizedBox(),
|
|
const SizedBox(height: 15),
|
|
Text(
|
|
textAlign: TextAlign.center,
|
|
'"${widget.evento.description?.trim()}"',
|
|
style: const TextStyle(
|
|
color: Colors.grey, fontStyle: FontStyle.italic),
|
|
),
|
|
widget.evento.userId == widget.evento.professionalId
|
|
? const SizedBox()
|
|
: widget.evento.status == 'accepted' ||
|
|
widget.evento.status == 'active'
|
|
? const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 20),
|
|
child: Text(
|
|
'Medios de comunicación con el usuario.',
|
|
style: TextStyle(color: Color(0xFF2BA4EC)),
|
|
),
|
|
)
|
|
: const SizedBox(height: 10),
|
|
widget.evento.userId == widget.evento.professionalId
|
|
? const SizedBox()
|
|
: widget.evento.status == 'completed'
|
|
? pro == true
|
|
? widget.evento.professionalScored == true
|
|
? const SizedBox()
|
|
: Column(
|
|
children: [
|
|
const SizedBox(height: 120),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (BuildContext context) {
|
|
return ScoreScreen(
|
|
evento: widget.evento,
|
|
pro: pro!,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Puntuar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: widget.evento.userScored == true
|
|
? const SizedBox()
|
|
: Column(
|
|
children: [
|
|
const SizedBox(height: 120),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (BuildContext context) {
|
|
return ScoreScreen(
|
|
evento: widget.evento,
|
|
pro: pro!,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Puntuar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: widget.evento.status == 'accepted' ||
|
|
widget.evento.status == 'active'
|
|
? Row(
|
|
children: [
|
|
const Expanded(child: SizedBox()),
|
|
ElevatedButton(
|
|
onPressed: () => launch("tel:$numberPhone"),
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF2BA4EC),
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
side: const BorderSide(
|
|
color: Color(0xFF2BA4EC),
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 18, horizontal: 0),
|
|
child: Icon(
|
|
Icons.phone_android,
|
|
size: 30,
|
|
color: Color(0xFF2BA4EC),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
if (widget.evento.status != 'pending') {
|
|
// Start or retrieve chat via API
|
|
try {
|
|
await ApiService.instance.post(
|
|
'/chat/start/${widget.evento.professionalId}',
|
|
{});
|
|
} catch (_) {}
|
|
Navigator.push(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (BuildContext context) {
|
|
return ChatScreen(
|
|
eventoId: widget.evento.id);
|
|
},
|
|
),
|
|
);
|
|
} else {
|
|
showAppSnackBar(
|
|
'El profesional aún no ha aceptado',
|
|
'Debes esperar a que el profesional acepte tu solicitud.',
|
|
color: Colors.grey.shade700,
|
|
);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF2BA4EC),
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
side: const BorderSide(
|
|
color: Color(0xFF2BA4EC),
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 18, horizontal: 0),
|
|
child: Icon(
|
|
Icons.message,
|
|
size: 30,
|
|
color: Color(0xFF2BA4EC),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_sendWhatsapp(numberPhone);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF2BA4EC),
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
side: const BorderSide(
|
|
color: Color(0xFF2BA4EC),
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 18, horizontal: 0),
|
|
child: Icon(
|
|
CommunityMaterialIcons.whatsapp,
|
|
size: 30,
|
|
color: Color(0xFF2BA4EC),
|
|
),
|
|
),
|
|
),
|
|
const Expanded(child: SizedBox()),
|
|
],
|
|
)
|
|
: const SizedBox(),
|
|
widget.evento.ubicacion == 'sitio'
|
|
? const SizedBox()
|
|
: Padding(
|
|
padding: const EdgeInsets.only(top: 40),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_openMap(widget.evento.latitud!,
|
|
widget.evento.longitud!);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF2BA4EC),
|
|
backgroundColor: const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
side: const BorderSide(
|
|
color: Color(0xFF2BA4EC),
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: const Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 18, horizontal: 0),
|
|
child: Icon(
|
|
Icons.near_me,
|
|
size: 30,
|
|
color: Color(0xFFFFFFFF),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
SizedBox(
|
|
width: 200,
|
|
child: Text('${widget.evento.address}'),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
widget.evento.status == 'accepted'
|
|
? Padding(
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: eventDate.year == today.year &&
|
|
eventDate.month == today.month &&
|
|
eventDate.day == today.day
|
|
? (DateTime.now()
|
|
.difference(DateTime.parse(
|
|
widget.evento.range1Hour1))
|
|
.abs() <=
|
|
const Duration(minutes: 30) &&
|
|
ver == true)
|
|
? ElevatedButton(
|
|
onPressed: () async {
|
|
await _updateStatus('iniciado');
|
|
setState(() => ver = false);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Iniciar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox()
|
|
: const SizedBox(),
|
|
),
|
|
widget.evento.professionalId == uid
|
|
? ElevatedButton(
|
|
onPressed: () async {
|
|
await _updateStatus('denegado');
|
|
Navigator.pushReplacementNamed(
|
|
context, '/solicitud');
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFEC2B2B),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Cancelar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
widget.evento.status == 'pending'
|
|
? Padding(
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: widget.evento.professionalId == uid
|
|
? ElevatedButton(
|
|
onPressed: () async {
|
|
await _updateStatus('aprobado');
|
|
Navigator.pushReplacementNamed(
|
|
context, '/solicitud');
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Aceptar',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
await _updateStatus('denegado');
|
|
Navigator.pushReplacementNamed(context, '/solicitud');
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFEC2B2B),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Cancelar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
widget.evento.status == 'active' || ver == false
|
|
? Padding(
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Column(
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
await _updateStatus('terminado');
|
|
Navigator.pushReplacement(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (BuildContext context) {
|
|
return ScoreScreen(
|
|
evento: widget.evento,
|
|
pro: pro!,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF2BA4EC),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(50),
|
|
),
|
|
elevation: 0,
|
|
minimumSize: const Size(230, 60),
|
|
),
|
|
child: const Text(
|
|
'Terminar servicio',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|