Files
prosappweb/lib/src/presentation/screens/cita.dart
T
Lizandro GuarnizoandClaude Sonnet 4.6 eb01e96e5d fix: remove get package, upgrade Flutter 3.44 compat deps
- 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>
2026-06-18 14:33:22 -05:00

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(),
],
),
);
}
}