feat: migrate prosapp_web_app from Firebase to NestJS API

- Replace Firebase Auth with JWT stored in SharedPreferences
- Replace Firestore with REST API calls via new ApiService
- Replace Firebase Storage with POST /storage/upload
- Remove firebase_auth, firebase_core, cloud_firestore,
  firebase_storage, firebase_messaging, google_sign_in deps
- Add api_service.dart as central HTTP client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-17 13:19:28 -05:00
co-authored by Claude Sonnet 4.6
parent 1c20d0c0cd
commit b95e0630de
45 changed files with 2531 additions and 5865 deletions
+73 -206
View File
@@ -1,6 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
@@ -12,13 +9,11 @@ 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/models/user_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';
import 'package:http/http.dart' as http;
import 'dart:convert';
class CitaScreen extends StatefulWidget {
final Event evento;
@@ -30,18 +25,13 @@ class CitaScreen extends StatefulWidget {
class _CitaScreenState extends State<CitaScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
DateTime today = DateTime.now();
String nombre = '';
String userToken = '';
String numberPhone = '';
int tarifa = 0;
Reference? ref_photo;
String? photoUrl;
ScoresModel? scoresModel;
bool? ver = true;
bool? pro;
String proName = '';
late final FirebaseAuth _auth;
String formatCurrency(int number) {
final formatter =
@@ -49,48 +39,11 @@ class _CitaScreenState extends State<CitaScreen> {
return '\$${formatter.format(number)}';
}
Future<void> sendPushNotification(
String user, String accion, String proName) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': accion == 'rechazo'
? '$proName a rechazado tu solicitud de servicio'
: '$proName a aprobado tu solicitud de servicio',
'title': '$proName $accion',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done',
'screen': 'misservicios',
},
'to': user
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
Future<void> _openMap(double lat, double lng) async {
final Uri _url =
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');
if (!await launchUrl(url)) {
throw Exception('Could not launch $url');
}
}
@@ -103,23 +56,14 @@ class _CitaScreenState extends State<CitaScreen> {
}
SettingModel? settings;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
_auth = FirebaseAuth.instance;
final currentUser = _auth.currentUser;
if (currentUser != null && currentUser.displayName != null) {
proName = currentUser.displayName!;
}
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
(SettingModel value) => setState(() => settings = value),
);
}
@@ -140,36 +84,42 @@ class _CitaScreenState extends State<CitaScreen> {
);
}
}
_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');
}
}
Future<void> _updateStatus(String status) async {
try {
await ApiService.instance
.patch('/services/${widget.evento.id}', {'status': status});
} catch (e) {
print('Error updating service status: $e');
}
}
@override
Widget build(BuildContext context) {
today.difference(DateTime.parse(widget.evento.range1Hour1));
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day);
if (nombre == '') {
if (uid != widget.evento.userId) {
UserModel.getUser(widget.evento.userId).then((value) {
UserModel.getUser(uid.toString()).then((me) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
userToken = value.token ?? '';
numberPhone = value.phoneNumber ?? '';
});
});
});
} else {
UserModel.getUser(widget.evento.professionalId).then((value) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
numberPhone = value.phoneNumber ?? '';
});
});
}
}
return Scaffold(
appBar: PopAppbar(
onPressed: () {
@@ -183,7 +133,7 @@ class _CitaScreenState extends State<CitaScreen> {
children: [
ListTile(
leading: ReferencePhoto(
ref: ref_photo,
ref: photoUrl,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
@@ -287,7 +237,6 @@ class _CitaScreenState extends State<CitaScreen> {
)
: const SizedBox(),
const SizedBox(height: 15),
// Text('${widget.evento.range1Hour1} - ${DateTime.now()}'),
Text(
textAlign: TextAlign.center,
'"${widget.evento.description?.trim()}"',
@@ -422,28 +371,12 @@ class _CitaScreenState extends State<CitaScreen> {
ElevatedButton(
onPressed: () async {
if (widget.evento.status != 'pendiente') {
final chatDoc = FirebaseFirestore
.instance
.collection('chats')
.doc(widget.evento.id);
final chatSnapshot =
await chatDoc.get();
if (!chatSnapshot.exists ||
chatSnapshot.data()!['message'] ==
null) {
await chatDoc.set(
{
'professional_id':
widget.evento.professionalId,
'user_id': widget.evento.userId,
'message': [],
},
SetOptions(merge: true),
).catchError((error) => print(
'Error al crear el documento: $error'));
}
// Start or retrieve chat via API
try {
await ApiService.instance.post(
'/chat/start/${widget.evento.professionalId}',
{});
} catch (_) {}
Navigator.push(
context,
CupertinoPageRoute(
@@ -453,35 +386,7 @@ class _CitaScreenState extends State<CitaScreen> {
},
),
);
}
// if (widget.evento.status != 'pendiente') {
// await FirebaseFirestore.instance
// .collection('chats')
// .doc(widget.evento.id)
// .set(
// {
// 'professional_id':
// widget.evento.professionalId,
// 'user_id': widget.evento.userId,
// 'message': [],
// },
// SetOptions(
// merge:
// true)).catchError((error) => print(
// 'Error al crear el documento: $error'));
// Navigator.push(
// context,
// CupertinoPageRoute(
// builder: (BuildContext context) {
// return ChatScreen(
// eventoId: widget.evento.id);
// },
// ),
// );
// }
else {
} else {
Get.snackbar(
'El profesional aun no ha aceptado tu solicitud',
'Debes esperar a que el profesional acepte tu solicitud para poder iniciar un chat.',
@@ -539,7 +444,7 @@ class _CitaScreenState extends State<CitaScreen> {
const Expanded(child: SizedBox()),
],
)
: SizedBox(),
: const SizedBox(),
widget.evento.ubicacion == 'sitio'
? const SizedBox()
: Padding(
@@ -601,16 +506,9 @@ class _CitaScreenState extends State<CitaScreen> {
const Duration(minutes: 30) &&
ver == true)
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "iniciado"}).then(
(value) {
setState(() {
ver = false;
});
});
onPressed: () async {
await _updateStatus('iniciado');
setState(() => ver = false);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
@@ -634,19 +532,10 @@ class _CitaScreenState extends State<CitaScreen> {
),
widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('denegado');
Navigator.pushReplacementNamed(
context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
@@ -679,19 +568,10 @@ class _CitaScreenState extends State<CitaScreen> {
padding: const EdgeInsets.only(bottom: 20),
child: widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "aprobado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'acepto', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('aprobado');
Navigator.pushReplacementNamed(
context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
@@ -713,18 +593,9 @@ class _CitaScreenState extends State<CitaScreen> {
: const SizedBox(),
),
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then((value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('denegado');
Navigator.pushReplacementNamed(context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
@@ -753,23 +624,19 @@ class _CitaScreenState extends State<CitaScreen> {
child: Column(
children: [
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "terminado"}).then((value) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
});
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),