diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index c5f1446..9234cf8 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,89 +1,2 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyCNpUV_4cMEL9QZx7NESXK7QAlRjRGwx_Y', - appId: '1:245204384533:web:1b8f0b4c1d9ae21d9f871c', - messagingSenderId: '245204384533', - projectId: 'prosapp-5747a', - authDomain: 'prosapp-5747a.firebaseapp.com', - databaseURL: 'https://prosapp-5747a-default-rtdb.firebaseio.com', - storageBucket: 'prosapp-5747a.appspot.com', - measurementId: 'G-EVBEQRZEDY', - ); - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyBp7vsJvD6oXz0FORBLEeIELwId4a2onHQ', - appId: '1:245204384533:android:e01772831d86f5c79f871c', - messagingSenderId: '245204384533', - projectId: 'prosapp-5747a', - databaseURL: 'https://prosapp-5747a-default-rtdb.firebaseio.com', - storageBucket: 'prosapp-5747a.appspot.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyDUhnkNkeEPkqcwVo5EWr63q3oEX6bDGic', - appId: '1:245204384533:ios:1d58f5624d0df4859f871c', - messagingSenderId: '245204384533', - projectId: 'prosapp-5747a', - databaseURL: 'https://prosapp-5747a-default-rtdb.firebaseio.com', - storageBucket: 'prosapp-5747a.appspot.com', - androidClientId: '245204384533-04oabs744hp8fjbdreijp4h16nvfmeti.apps.googleusercontent.com', - iosClientId: '245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn.apps.googleusercontent.com', - iosBundleId: 'com.example.prosappco', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyDUhnkNkeEPkqcwVo5EWr63q3oEX6bDGic', - appId: '1:245204384533:ios:1d58f5624d0df4859f871c', - messagingSenderId: '245204384533', - projectId: 'prosapp-5747a', - databaseURL: 'https://prosapp-5747a-default-rtdb.firebaseio.com', - storageBucket: 'prosapp-5747a.appspot.com', - androidClientId: '245204384533-04oabs744hp8fjbdreijp4h16nvfmeti.apps.googleusercontent.com', - iosClientId: '245204384533-nv7e2n7d4vj5kfkc55mlmiiju0ch2qhn.apps.googleusercontent.com', - iosBundleId: 'com.example.prosappco', - ); -} +// ponytail: Firebase removed; this file is kept as an empty stub so any lingering references compile. +// No imports, no exports needed. diff --git a/lib/main.dart b/lib/main.dart index 39ecfbf..08546c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,6 @@ import 'package:prosappco/src/providers/user_provider.dart'; import 'package:prosappco/src/presentation/screens/calendar.dart'; import 'package:prosappco/src/presentation/screens/city.dart'; import 'package:prosappco/src/presentation/screens/login/login.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:prosappco/src/presentation/screens/login/login_email.dart'; import 'package:prosappco/src/presentation/screens/my_services.dart'; import 'package:prosappco/src/presentation/screens/my_services_pro.dart'; @@ -24,46 +23,14 @@ import 'package:prosappco/src/presentation/screens/request_sent.dart'; import 'package:prosappco/src/presentation/screens/reset_password/reset_password.dart'; import 'package:prosappco/src/presentation/screens/map/service.dart'; import 'package:prosappco/src/presentation/screens/solicitudes.dart'; -import 'package:prosappco/src/services/local_notifications.dart'; -import 'firebase_options.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - await Firebase.initializeApp(); - - print('Handling a backdround message: ${message.messageId}'); -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ).then((value) => Get.put(AuthenticationRepository())); - // Evitar la solicitud de permiso en la versión web - if (!kIsWeb) { - FirebaseMessaging messaging = FirebaseMessaging.instance; - - await messaging.requestPermission( - alert: true, - announcement: false, - badge: true, - carPlay: false, - criticalAlert: false, - provisional: false, - sound: true, - ); - - FirebaseMessaging.onBackgroundMessage( - (_firebaseMessagingBackgroundHandler)); - - FirebaseMessaging.onMessage.listen((RemoteMessage message) {}); - } - - await initNotifications(); + Get.put(AuthenticationRepository()); await initializeDateFormatting('es_MX', null); @@ -82,9 +49,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider( - create: (_) => UserProvider(), - ), + ChangeNotifierProvider(create: (_) => UserProvider()), ], child: GetMaterialApp( theme: ThemeData(fontFamily: 'Poppins'), @@ -94,9 +59,7 @@ class MyApp extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [ - Locale('es', 'US'), - ], + supportedLocales: const [Locale('es', 'US')], title: 'ProsApp', initialRoute: '/', routes: { @@ -108,8 +71,7 @@ class MyApp extends StatelessWidget { '/city': (context) => const CityScreen(), '/profession': (context) => const ProfessionScreen(), '/newNumber': (context) => const NewNumberScreen(), - '/profesionalRevision': (context) => - const ProfessionalRevisionScreen(), + '/profesionalRevision': (context) => const ProfessionalRevisionScreen(), '/profesionalProfile': (context) => const ProfessionalProfileScreen(), '/solicitudEnviada': (context) => const RequestSentScreen(), '/nuevaPassword': (context) => NewPasswordScreen(), @@ -142,7 +104,6 @@ class _SplashScreenState extends State { void initState() { super.initState(); if (kIsWeb) { - // Espera 3 segundos y luego redirige a LoginScreen Future.delayed(const Duration(seconds: 3), () { Navigator.pushReplacement( context, @@ -162,11 +123,3 @@ class _SplashScreenState extends State { ); } } - - - - // splash: 'images/splashgif.gif', - // backgroundColor: Colors.black, - // nextScreen: const LoginScreen(), - // splashIconSize: 50, - // duration: 80000, \ No newline at end of file diff --git a/lib/src/authentication/authentication_repository.dart b/lib/src/authentication/authentication_repository.dart index 208d8c0..46463e1 100644 --- a/lib/src/authentication/authentication_repository.dart +++ b/lib/src/authentication/authentication_repository.dart @@ -1,342 +1,82 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:prosappco/src/authentication/exceptions/register_failed.dart'; +import 'package:prosappco/src/models/user_model.dart'; import 'package:prosappco/src/presentation/screens/login/login.dart'; -// import 'package:prosappco/src/presentation/screens/service.dart'; import 'package:prosappco/src/presentation/screens/map/service.dart'; import 'package:prosappco/src/presentation/screens/service_web.dart'; +import 'package:prosappco/src/services/api_service.dart'; class AuthenticationRepository extends GetxController { static AuthenticationRepository get instance => Get.find(); - //Variables - final _auth = FirebaseAuth.instance; - late final Rx firebaseUser; - final firebase = FirebaseFirestore.instance; - final GoogleSignIn googleSignIn = GoogleSignIn(); - // final _userRef = firebase - var verificationId = ''.obs; + final _api = ApiService.instance; + + final Rx currentUser = Rx(null); + var isLoggedIn = false.obs; @override void onReady() { - // Future.delayed(const Duration(seconds: 6)); - firebaseUser = Rx(_auth.currentUser); - firebaseUser.bindStream(_auth.userChanges()); - ever(firebaseUser, _setInitialScreen); + _checkSession(); } - _setInitialScreen(User? user) { - user == null - ? Get.offAll(const LoginScreen()) - : kIsWeb - ? Get.offAll(const ServiceWebScreen()) - : Get.offAll(const ServiceScreen()); - } - - Future phoneAuthentication(String phoneNo) async { - await _auth.verifyPhoneNumber( - phoneNumber: phoneNo, - verificationCompleted: (credential) async { - await _auth.signInWithCredential(credential); - }, - codeSent: (verificationId, resendToken) { - this.verificationId.value = verificationId; - }, - codeAutoRetrievalTimeout: (verificationId) { - this.verificationId.value = verificationId; - }, - verificationFailed: (e) { - if (e.code == 'invalid-phone-number') { - Get.snackbar('Error', 'El numero no es valido.'); - } else { - Get.snackbar('Error', 'Algo ha ido mal. Inténtalo de nuevo. $e'); - } - }, - ); - } - - Future updatePhoneNumber(String verificationId, String smsCode) async { - try { - PhoneAuthCredential credential = PhoneAuthProvider.credential( - verificationId: verificationId, smsCode: smsCode); - await FirebaseAuth.instance.currentUser!.updatePhoneNumber(credential); - print("Phone number updated successfully"); - } catch (e) { - print("Error updating phone number: $e"); + Future _checkSession() async { + final token = await _api.getToken(); + if (token == null) { + Get.offAll(const LoginScreen()); + return; } - } - - Future verifyOTP(String otp) async { - var credentials = await _auth.signInWithCredential( - PhoneAuthProvider.credential( - verificationId: verificationId.value, smsCode: otp)); - return credentials.user != null ? true : false; - } - - Future createUserWithEmailAndPassword( - String email, String password) async { try { - await _auth.createUserWithEmailAndPassword( - email: email, password: password); - - firebaseUser.value != null - ? kIsWeb - ? Get.offAll(const ServiceWebScreen()) - : Get.to(const ServiceScreen()) - : Get.to(const LoginScreen()); - } on FirebaseAuthException catch (e) { - final ex = SignUpWithEmailAndPasswordFailure.code(e.code); - Get.snackbar( - 'Correo ya registrado', - 'Por favor pruebe con otro.', - snackPosition: SnackPosition.BOTTOM, - ); + final data = await _api.get('/auth/me'); + currentUser.value = UserModel.fromApi(data); + isLoggedIn.value = true; + _navigateHome(); } catch (_) { - const ex = SignUpWithEmailAndPasswordFailure(); - print('EXCEPTION - ${ex.message}'); - throw ex; + await _api.clearToken(); + Get.offAll(const LoginScreen()); } } + void _navigateHome() { + kIsWeb + ? Get.offAll(const ServiceWebScreen()) + : Get.offAll(const ServiceScreen()); + } + Future loginWithEmailAndPassword(String email, String password) async { - try { - await _auth.signInWithEmailAndPassword(email: email, password: password); - } on FirebaseAuthException catch (e) { - if (e.code == 'wrong-password') { - Get.snackbar( - 'Contraseña incorrecta', - 'Por favor intentelo de nuevo.', - snackPosition: SnackPosition.BOTTOM, - ); - } - if (e.code == 'invalid-email') { - Get.snackbar( - 'Ingrese un email valido', - 'Por favor pruebe con otro.', - snackPosition: SnackPosition.BOTTOM, - ); - } - if (e.code == 'user-not-found') { - Get.snackbar( - 'Email no encontrado', - 'Este correo no se encuentra registrado.', - snackPosition: SnackPosition.BOTTOM, - ); - } - } catch (_) {} + final data = await _api.post('/auth/login', {'email': email, 'password': password}); + await _api.saveToken(data['access_token']); + currentUser.value = UserModel.fromApi(data['user']); + isLoggedIn.value = true; + _navigateHome(); } - Future signInWithGoogle() async { - try { - final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); - - if (googleUser != null) { - final GoogleSignInAuthentication googleAuth = - await googleUser.authentication; - final OAuthCredential credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); - - await FirebaseAuth.instance.signInWithCredential(credential); - - // Continúa con el flujo de la aplicación después del inicio de sesión exitoso - // Por ejemplo, redirecciona a la siguiente pantalla - firebaseUser.value != null - ? kIsWeb - ? Get.offAll(const ServiceWebScreen()) - : Get.to(const ServiceScreen()) - : Get.to(const LoginScreen()); - } else { - // El usuario canceló el inicio de sesión con Google - // Puedes manejarlo según tus necesidades - print('Error'); - } - } catch (e) { - print('Error - $e'); - } + Future createUserWithEmailAndPassword(String email, String password, String name) async { + final data = await _api.post('/auth/register', { + 'email': email, + 'password': password, + 'name': name, + }); + await _api.saveToken(data['access_token']); + currentUser.value = UserModel.fromApi(data['user']); + isLoggedIn.value = true; + _navigateHome(); } - Future logout(String uid) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'token': FieldValue.delete()}); - } catch (e) { - print(e); - } - - _auth.signOut(); + Future phoneAuthentication(String phone, {String? name}) async { + final data = await _api.post('/auth/phone', {'phone': phone, 'name': name ?? phone}); + await _api.saveToken(data['access_token']); + currentUser.value = UserModel.fromApi(data['user']); + isLoggedIn.value = true; + _navigateHome(); } - String? getCurrentUserPhone() { - final User? user = _auth.currentUser; - return user?.phoneNumber; + Future logout() async { + await _api.clearToken(); + currentUser.value = null; + isLoggedIn.value = false; + Get.offAll(const LoginScreen()); } - String? getCurrentUserUid() { - final User? user = _auth.currentUser; - return user?.uid; - } - - Future getCity(String uid) async { - String city = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - city = data?['city'] ?? ''; - } catch (e) { - print('Error getting city: $e'); - } - return city; - } - - Future getGender(String uid) async { - String gender = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - gender = data?['gender'] ?? ''; - } catch (e) { - print('Error getting gender: $e'); - } - return gender; - } - - Future getBirthday(String uid) async { - String birthDate = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - birthDate = data?['birth_date'] ?? ''; - } catch (e) { - print('Error getting birthDate: $e'); - } - return birthDate; - } - - Future getCoordsOfCity(String uid) async { - String coords = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - coords = data?['coordsOfCity'] ?? ''; - } catch (e) { - print('Error getting coords of city: $e'); - } - print('Error getting coords of city: $coords'); - return coords; - } - - Future getAddress(String uid) async { - String address = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - address = data?['address'] ?? ''; - } catch (e) { - print('Error getting address: $e'); - } - return address; - } - - Future getUbicacion(String uid) async { - String location = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - location = data?['ubicacion'] ?? ''; - } catch (e) { - print('Error getting address: $e'); - } - return location; - } - - Future getOpcionalAddress(String uid) async { - String opcional_location = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - opcional_location = data?['opcional_address'] ?? ''; - } catch (e) { - print('Error getting address: $e'); - } - return opcional_location; - } - - Future getTarifa(String uid) async { - int tarifa = 0; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - tarifa = data?['tarifas'] ?? 0; - } catch (e) { - print('Error getting tarifa: $e'); - } - return tarifa; - } - - Future getPhoto(String uid) async { - String photo = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - photo = data?['photo'] ?? ''; - } catch (e) { - print('Error getting photo: $e'); - } - return photo; - } - - Future getBanner(String uid) async { - String photo = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - photo = data?['banner'] ?? ''; - } catch (e) { - print('Error getting banner: $e'); - } - return photo; - } - - Future getProfession(String uid) async { - String profession = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - profession = data?['profesion'] ?? ''; - } catch (e) { - print('Error getting profesion: $e'); - } - return profession; - } - - Future getState(String uid) async { - String state = ''; - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - state = data?['estado'] ?? ''; - } catch (e) { - print('Error getting estado: $e'); - } - return state; - } + String? getCurrentUserUid() => currentUser.value?.id; + String? getCurrentUserPhone() => currentUser.value?.phoneNumber; } diff --git a/lib/src/components/banner_photo.dart b/lib/src/components/banner_photo.dart index 2a5a091..d58d1c3 100644 --- a/lib/src/components/banner_photo.dart +++ b/lib/src/components/banner_photo.dart @@ -1,66 +1,45 @@ import 'dart:io'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:prosappco/src/components/column_padding.dart'; const double photoSize = 150; class ReferenceBannerPhoto extends StatelessWidget { - Reference? ref; - double size; - double sizeCircle; + /// Accepts either a String URL or null. + final Object? ref; + final double size; + final double sizeCircle; - ReferenceBannerPhoto({ + const ReferenceBannerPhoto({ super.key, required this.ref, this.size = photoSize, this.sizeCircle = photoSize, }); - Future downloadImage() async { - try { - if (ref != null) { - final imageData = await ref!.getData(); - if (imageData != null) { - return Image.memory( - imageData, - width: double.infinity, - height: size, - fit: BoxFit.fill, - ); - } - } - // ignore: empty_catches - } catch (e) {} - - return DefaultPhoto( - sizeDefault: sizeCircle, - ); - } - @override Widget build(BuildContext context) { - return FutureBuilder( - future: downloadImage(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - // mientras la llamada asíncrona está en proceso, muestra un mensaje de carga - return DefaultPhoto(); - } else if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return snapshot.data!; - } else { - return DefaultPhoto(); - } - }); + final url = ref is String ? ref as String : null; + + if (url == null || url.isEmpty || url.startsWith('...')) { + return DefaultPhoto(sizeDefault: sizeCircle); + } + + return Image.network( + url, + width: double.infinity, + height: size, + fit: BoxFit.fill, + errorBuilder: (_, __, ___) => DefaultPhoto(sizeDefault: sizeCircle), + ); } } class DefaultPhoto extends StatelessWidget { - double sizeDefault; - DefaultPhoto({ + final double sizeDefault; + + const DefaultPhoto({ super.key, this.sizeDefault = photoSize, }); @@ -92,14 +71,14 @@ class DefaultPhoto extends StatelessWidget { color: Colors.grey.withOpacity(0.3), spreadRadius: 2, blurRadius: 5, - offset: const Offset(0, 3), // changes position of shadow + offset: const Offset(0, 3), ), ], ), constraints: const BoxConstraints(minWidth: 250, minHeight: 50), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Expanded( child: Center( child: Text( @@ -129,8 +108,9 @@ class DefaultPhoto extends StatelessWidget { } class LocalPhoto extends StatelessWidget { - File file; - LocalPhoto({super.key, required this.file}); + final File file; + + const LocalPhoto({super.key, required this.file}); @override Widget build(BuildContext context) { diff --git a/lib/src/components/drawer_professional.dart b/lib/src/components/drawer_professional.dart index b24940d..90ed96c 100644 --- a/lib/src/components/drawer_professional.dart +++ b/lib/src/components/drawer_professional.dart @@ -1,4 +1,3 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -25,7 +24,6 @@ class DrawerProfessional extends StatefulWidget { class _DrawerProfessionalState extends State { final uid = AuthenticationRepository.instance.getCurrentUserUid(); - UserModel? user; ScoresModel? scoresModel; Future _irSugerencias() async { @@ -41,12 +39,6 @@ class _DrawerProfessionalState extends State { void initState() { super.initState(); - if (user == null) { - UserModel.getUser(uid.toString()).then( - (UserModel s) => setState(() => user = s), - ); - } - if (scoresModel == null) { ScoresModel.scoreTo(uid.toString(), true, false).then( (ScoresModel s) => setState(() => scoresModel = s), @@ -56,7 +48,9 @@ class _DrawerProfessionalState extends State { @override Widget build(BuildContext context) { - final User? currentUser = FirebaseAuth.instance.currentUser; + // Use in-memory currentUser — no Firebase needed + final UserModel? currentUser = + AuthenticationRepository.instance.currentUser.value; return Drawer( child: Container( @@ -78,7 +72,7 @@ class _DrawerProfessionalState extends State { ), ); }, - title: Text(user?.name ?? '', + title: Text(currentUser?.name ?? '', style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -88,13 +82,13 @@ class _DrawerProfessionalState extends State { style: const TextStyle(fontSize: 12), ), Text( - user?.city ?? '', + currentUser?.city ?? '', style: const TextStyle(fontSize: 12), ), ], ), leading: ReferencePhoto( - ref: user?.photo, + ref: currentUser?.picture, size: 55, sizeCircle: 60, ), @@ -113,7 +107,7 @@ class _DrawerProfessionalState extends State { color: Colors.grey.withOpacity(0.3), spreadRadius: 1, blurRadius: 3, - offset: const Offset(0, 0), // changes position of shadow + offset: const Offset(0, 0), ), ], ), @@ -234,25 +228,6 @@ class _DrawerProfessionalState extends State { style: TextStyle(fontSize: 15), ), ), - // ListTile( - // onTap: () { - // Navigator.of(context).push( - // CupertinoPageRoute( - // builder: (BuildContext context) { - // return const MessagesScreen(); - // }, - // ), - // ); - // }, - // leading: const Icon( - // Icons.messenger_outline, - // color: Colors.black, - // ), - // title: const Text( - // 'Mensajes', - // style: TextStyle(fontSize: 15), - // ), - // ), ListTile( onTap: () { Navigator.push( @@ -308,8 +283,7 @@ class _DrawerProfessionalState extends State { color: Colors.grey.withOpacity(0.5), spreadRadius: 2, blurRadius: 3, - offset: - const Offset(0, 2), // changes position of shadow + offset: const Offset(0, 2), ), ], ), @@ -407,7 +381,7 @@ class _DrawerProfessionalState extends State { const SizedBox(height: 5), ElevatedButton( onPressed: () { - AuthenticationRepository.instance.logout(uid!); + AuthenticationRepository.instance.logout(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, diff --git a/lib/src/components/photo_view.dart b/lib/src/components/photo_view.dart index 2959a67..37a26dd 100644 --- a/lib/src/components/photo_view.dart +++ b/lib/src/components/photo_view.dart @@ -1,19 +1,19 @@ import 'dart:io'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; const double photoSize = 100; const double iconSize = 35; class ReferencePhoto extends StatelessWidget { - Reference? ref; - double size; - double sizeIcon; - double sizeCircle; - ReferencePhoto({ + /// Accepts either a String URL or null. + final Object? ref; + final double size; + final double sizeIcon; + final double sizeCircle; + + const ReferencePhoto({ super.key, required this.ref, this.sizeIcon = iconSize, @@ -21,58 +21,32 @@ class ReferencePhoto extends StatelessWidget { this.sizeCircle = photoSize, }); - Future downloadImage() async { - try { - if (ref != null) { - final imageData = await ref!.getData(); - if (imageData != null) { - return ClipOval( - child: Image.memory( - imageData, - width: size, - height: size, - fit: BoxFit.cover, - ), - ); - } - } - // ignore: empty_catches - } catch (e) {} - - return DefaultPhoto( - sizeDefault: sizeCircle, - iconDefault: sizeIcon, - ); - } - @override Widget build(BuildContext context) { - return FutureBuilder( - future: downloadImage(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - // mientras la llamada asíncrona está en proceso, muestra un mensaje de carga - return SizedBox( - width: size, - height: size, - child: Center( - child: DefaultPhoto(), - ), - ); - } else if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return snapshot.data!; - } else { - return DefaultPhoto(); - } - }); + final url = ref is String ? ref as String : null; + + if (url == null || url.isEmpty || url.startsWith('...')) { + return DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon); + } + + return ClipOval( + child: Image.network( + url, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon), + ), + ); } } class DefaultPhoto extends StatelessWidget { - double sizeDefault; - double iconDefault; - DefaultPhoto({ + final double sizeDefault; + final double iconDefault; + + const DefaultPhoto({ super.key, this.sizeDefault = photoSize, this.iconDefault = iconSize, @@ -97,8 +71,9 @@ class DefaultPhoto extends StatelessWidget { } class LocalPhoto extends StatelessWidget { - File file; - LocalPhoto({super.key, required this.file}); + final File file; + + const LocalPhoto({super.key, required this.file}); @override Widget build(BuildContext context) { diff --git a/lib/src/components/photo_view_web.dart b/lib/src/components/photo_view_web.dart index 77a0957..8cb2b49 100644 --- a/lib/src/components/photo_view_web.dart +++ b/lib/src/components/photo_view_web.dart @@ -1,18 +1,16 @@ -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; const double photoSize = 100; const double iconSize = 55; class ReferencePhotoWeb extends StatelessWidget { - Reference? ref; - double size; - double sizeIcon; - double sizeCircle; + /// Accepts either a String URL or null. + final Object? ref; + final double size; + final double sizeIcon; + final double sizeCircle; - ReferencePhotoWeb({ + const ReferencePhotoWeb({ super.key, required this.ref, this.sizeIcon = iconSize, @@ -20,59 +18,32 @@ class ReferencePhotoWeb extends StatelessWidget { this.sizeCircle = photoSize, }); - Future downloadImage() async { - try { - if (ref != null) { - final imageData = await ref!.getData(); - if (imageData != null) { - return ClipOval( - child: Image.memory( - imageData, - width: size, - height: size, - fit: BoxFit.cover, - ), - ); - } - } - // ignore: empty_catches - } catch (e) {} - - return DefaultPhotoWeb( - sizeDefault: sizeCircle, - iconDefault: sizeIcon, - ); - } - @override Widget build(BuildContext context) { - return FutureBuilder( - future: downloadImage(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - // mientras la llamada asíncrona está en proceso, muestra un mensaje de carga - return SizedBox( - width: size, - height: size, - child: const Center( - child: CircularProgressIndicator(), - ), - ); - } else if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return snapshot.data!; - } else { - return DefaultPhotoWeb(); - } - }); + final url = ref is String ? ref as String : null; + + if (url == null || url.isEmpty || url.startsWith('...')) { + return DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon); + } + + return ClipOval( + child: Image.network( + url, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon), + ), + ); } } class DefaultPhotoWeb extends StatelessWidget { - double sizeDefault; - double iconDefault; + final double sizeDefault; + final double iconDefault; - DefaultPhotoWeb({ + const DefaultPhotoWeb({ super.key, this.sizeDefault = photoSize, this.iconDefault = iconSize, @@ -97,8 +68,9 @@ class DefaultPhotoWeb extends StatelessWidget { } class LocalPhotoWeb extends StatelessWidget { - Uint8List? file; - LocalPhotoWeb({super.key, required this.file}); + final Uint8List? file; + + const LocalPhotoWeb({super.key, required this.file}); @override Widget build(BuildContext context) { diff --git a/lib/src/components/schedule_picker.dart b/lib/src/components/schedule_picker.dart index fb2cec8..db033aa 100644 --- a/lib/src/components/schedule_picker.dart +++ b/lib/src/components/schedule_picker.dart @@ -1,6 +1,6 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:prosappco/src/services/api_service.dart'; typedef TimeCallback = void Function(TimeOfDay? pickedTime); @@ -185,28 +185,26 @@ class Schedule { 'range2Hour1: $range2Hour1, range2Hour2: $range2Hour2)'; } + static Map _emptySchedules() => { + for (var i = 1; i <= 7; i++) + '$i': Schedule(false, false, null, null, null, null), + }; + static Future> getHorarios(String uid) async { try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - final Map? horarioData = data?['horario']; + final Map data = + await ApiService.instance.get('/users/$uid'); + final Map? horarioData = + data['horario'] as Map?; + if (horarioData == null) return _emptySchedules(); final horarios = {}; - horarioData?.forEach((key, value) { - horarios[key] = Schedule.fromJson(value); + horarioData.forEach((key, value) { + horarios[key] = Schedule.fromJson(value as Map); }); return horarios; } catch (e) { - print('Error getting user: $e'); - return { - "1": Schedule(false, false, null, null, null, null), - "2": Schedule(false, false, null, null, null, null), - "3": Schedule(false, false, null, null, null, null), - "4": Schedule(false, false, null, null, null, null), - "5": Schedule(false, false, null, null, null, null), - "6": Schedule(false, false, null, null, null, null), - "7": Schedule(false, false, null, null, null, null), - }; + print('Error getting horarios: $e'); + return _emptySchedules(); } } } diff --git a/lib/src/controllers/new_phone_controller.dart b/lib/src/controllers/new_phone_controller.dart index 55d4a5e..2aeb5f3 100644 --- a/lib/src/controllers/new_phone_controller.dart +++ b/lib/src/controllers/new_phone_controller.dart @@ -1,116 +1,25 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:prosappco/src/services/api_service.dart'; class NewPhoneController extends GetxController { - final FirebaseAuth _auth = FirebaseAuth.instance; - final newPhoneNo = TextEditingController(text: ''); final otpCode = TextEditingController(text: ''); - Future updatePhoneNumber(newPhoneNo) async { - final currentUser = _auth.currentUser; - - if (currentUser?.phoneNumber == newPhoneNo) { + Future updatePhoneNumber(String newPhone) async { + try { + await ApiService.instance.patch('/users/me', {'phone': newPhone}); Get.snackbar( - 'Ya estas registrado', - 'Este es tu numero actual.', + 'Número de teléfono actualizado', + 'El número de teléfono se ha actualizado correctamente.', + snackPosition: SnackPosition.BOTTOM, + ); + } catch (e) { + Get.snackbar( + 'Error al actualizar', + 'No se pudo actualizar el número de teléfono: $e', snackPosition: SnackPosition.BOTTOM, ); - } else { - try { - final PhoneVerificationCompleted verificationCompleted = - (PhoneAuthCredential credential) async { - await currentUser?.updatePhoneNumber(credential); - Get.snackbar( - 'Número de teléfono actualizado', - 'El número de teléfono se ha actualizado correctamente.', - snackPosition: SnackPosition.BOTTOM, - ); - }; - - final PhoneVerificationFailed verificationFailed = - (FirebaseAuthException e) { - Get.snackbar( - 'Ingresa un numero de telefono valido', - 'verifica que el campo tenga todos los caracteres o intentalo de nuevo ${e}', - snackPosition: SnackPosition.BOTTOM, - ); - }; - - final PhoneCodeSent codeSent = - (String verificationId, [int? forceResendingToken]) { - Get.defaultDialog( - title: 'Ingrese el código de verificación', - content: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - TextField( - controller: otpCode, - decoration: InputDecoration( - labelText: 'Código de verificación', - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Get.back(); - }, - child: Text('Cancelar'), - ), - ElevatedButton( - onPressed: () async { - try { - final PhoneAuthCredential credential = - PhoneAuthProvider.credential( - verificationId: verificationId, - smsCode: otpCode.text, - ); - await currentUser?.updatePhoneNumber(credential); - Get.back(); - Get.snackbar( - 'Número de teléfono actualizado', - 'El número de teléfono se ha actualizado correctamente.', - snackPosition: SnackPosition.BOTTOM, - ); - } catch (e) { - Get.snackbar( - 'Numero ya registrado', - 'El numero de telefono ingresado ya se encuentra registrado', - snackPosition: SnackPosition.BOTTOM, - ); - } - }, - child: Text('Actualizar'), - ), - ], - ); - }; - - final PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout = - (String verificationId) { - // Aquí puedes hacer algo si se agota el tiempo de espera para ingresar el código de verificación automáticamente. - }; - - await _auth.verifyPhoneNumber( - phoneNumber: newPhoneNo, - verificationCompleted: verificationCompleted, - verificationFailed: verificationFailed, - codeSent: codeSent, - codeAutoRetrievalTimeout: codeAutoRetrievalTimeout, - ); - } catch (e) { - print('Error actualizando el número de teléfono: $e'); - Get.snackbar( - 'Error actualizando el número de teléfono', - 'Ha ocurrido un error al actualizar el número de teléfono: $e', - snackPosition: SnackPosition.BOTTOM, - ); - } } } } diff --git a/lib/src/controllers/register_controller.dart b/lib/src/controllers/register_controller.dart index 210a85f..a0332fb 100644 --- a/lib/src/controllers/register_controller.dart +++ b/lib/src/controllers/register_controller.dart @@ -7,9 +7,10 @@ class RegisterController extends GetxController { final email = TextEditingController(); final password = TextEditingController(); + final name = TextEditingController(); - Future registerUser(String email, String password) async { + Future registerUser(String email, String password, String name) async { await AuthenticationRepository.instance - .createUserWithEmailAndPassword(email, password); + .createUserWithEmailAndPassword(email, password, name); } } diff --git a/lib/src/models/chat_model.dart b/lib/src/models/chat_model.dart index b632d7a..6712228 100644 --- a/lib/src/models/chat_model.dart +++ b/lib/src/models/chat_model.dart @@ -1,5 +1,5 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:prosappco/src/models/user_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; class ChatModel { List messages; @@ -9,99 +9,60 @@ class ChatModel { UserModel? professional; String id; - ChatModel( - {required this.messages, - required this.professional_id, - required this.user_id, - this.user, - this.professional, - required this.id}); + ChatModel({ + required this.messages, + required this.professional_id, + required this.user_id, + this.user, + this.professional, + required this.id, + }); - static Future fromDocumentSnapshot2( - DocumentSnapshot> snapshot, - bool fillUserModel) async { + static ChatModel fromJson(Map data) { + final List msgs = data['messages'] ?? data['message'] ?? []; + final messages = msgs.map((m) { + return MessageModel( + user: m['user'] ?? m['sender_id'] ?? '', + content: m['content'] ?? m['text'] ?? '', + timestamp: m['timestamp'] != null + ? DateTime.tryParse(m['timestamp'].toString()) ?? DateTime.now() + : DateTime.now(), + ); + }).toList(); + + return ChatModel( + messages: messages, + professional_id: data['professional_id'] ?? '', + user_id: data['user_id'] ?? '', + id: data['id']?.toString() ?? '', + ); + } + + static Future> getChatsByProId(String userId) async { try { - List messages = []; - List messagesData = snapshot.get('message') ?? []; - - for (var data in messagesData) { - messages.add(MessageModel( - user: data['user'] ?? '', - content: data['content'] ?? '', - timestamp: (data['timestamp'] ?? '' as Timestamp).toDate(), - )); - } - - return ChatModel( - messages: messages, - professional_id: snapshot.get('professional_id') ?? '', - user_id: snapshot.get('user_id') ?? '', - user: fillUserModel - ? await UserModel.getUser(snapshot.get('user_id') ?? '') - : null, - professional: fillUserModel - ? await UserModel.getUser(snapshot.get('professional_id') ?? '') - : null, - id: snapshot.id); + final List data = + await ApiService.instance.get('/chat?professionalId=$userId'); + return data + .map((e) => ChatModel.fromJson(e as Map)) + .toList(); } catch (e) { - print('error $e'); - return ChatModel(messages: [], professional_id: '', user_id: '', id: ''); + print('error getChatsByProId $e'); + return []; } } - static ChatModel fromDocumentSnapshot( - DocumentSnapshot> snapshot, - ) { + static Future> getChatsByUserId(String userId) async { try { - List messages = []; - List messagesData = snapshot.get('message') ?? []; - - for (var data in messagesData) { - messages.add(MessageModel( - user: data['user'] ?? '', - content: data['content'] ?? '', - timestamp: (data['timestamp'] as Timestamp).toDate(), - )); - } - - return ChatModel( - messages: messages, - professional_id: snapshot.get('professional_id'), - user_id: snapshot.get('user_id'), - id: snapshot.id); + final List data = + await ApiService.instance.get('/chat?userId=$userId'); + return data + .map((e) => ChatModel.fromJson(e as Map)) + .toList(); } catch (e) { - print('error $e'); - return ChatModel(messages: [], professional_id: '', user_id: '', id: ''); + print('error getChatsByUserId $e'); + return []; } } - - static Future> getChatsByProId(String userReceived) async { - final receivedScoresQuery = FirebaseFirestore.instance - .collection('chats') - .where('professional_id', isEqualTo: userReceived); - - final receivedScoresSnapshot = await receivedScoresQuery.get(); - - final receivedScores = await Future.wait(receivedScoresSnapshot.docs - .map((doc) async => await fromDocumentSnapshot2(doc, true)) - .toList()); - - return receivedScores; - } - - static Future> getChatsByUserId(String userReceived) async { - final receivedScoresQuery = FirebaseFirestore.instance - .collection('chats') - .where('user_id', isEqualTo: userReceived); - - final receivedScoresSnapshot = await receivedScoresQuery.get(); - - final receivedScores = await Future.wait(receivedScoresSnapshot.docs - .map((doc) async => await fromDocumentSnapshot2(doc, true)) - .toList()); - - return receivedScores; - } } class MessageModel { diff --git a/lib/src/models/event_model.dart b/lib/src/models/event_model.dart index 16bf23b..e665ba9 100644 --- a/lib/src/models/event_model.dart +++ b/lib/src/models/event_model.dart @@ -1,6 +1,6 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/models/scores_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; final uid = AuthenticationRepository.instance.getCurrentUserUid(); @@ -22,10 +22,7 @@ class EventoService { bool userScored, ) async { try { - DateTime ahora = DateTime.now(); - - final eventId = - await FirebaseFirestore.instance.collection('services').add({ + final Map result = await ApiService.instance.post('/services', { 'user_id': uid, 'title': title, 'description': description, @@ -38,41 +35,27 @@ class EventoService { 'latitude': latitude, 'longitude': longitude, 'status': status, - 'Timestamp': ahora, 'tarifa': tarifa ?? 0, 'professional_scored': professionalScored, 'user_scored': userScored, - }).then((value) { - FirebaseFirestore.instance.collection('users').doc(uid).update({ - 'services': FieldValue.arrayUnion([value.id]) - }); - - return value.id; }); - - return eventId; + return result['id']?.toString(); } catch (e) { print('Evento $e'); + return null; } - return null; } } Future> getByProId(String day) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - // .where('professional_id', isEqualTo: uid) - .where('day', isEqualTo: day.toString()) - .where('status', isEqualTo: 'aprobado') - // .orderBy('Timestamp', descending: true) - .get(); - + final List data = await ApiService.instance + .get('/services?day=$day&status=aprobado'); List eventos = []; - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; + for (var element in data) { + final event = Event.fromJson(element as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); eventos.add(event); } return eventos; @@ -84,42 +67,36 @@ Future> getByProId(String day) async { Future> getByProIdAll(String state1, String state2) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .where('status', whereIn: [state1, state2]).get(); - + final List data = await ApiService.instance + .get('/services?professionalId=$uid&status=$state1,$state2'); List eventos = []; - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; + for (var element in data) { + final event = Event.fromJson(element as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); eventos.add(event); } return eventos; } catch (e) { - print('Error getByProId $e'); + print('Error getByProIdAll $e'); return []; } } Future> getByUserIdAll(String state1, String state2) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - .where('user_id', isEqualTo: uid) - .where('status', whereIn: [state1, state2]).get(); - + final List data = await ApiService.instance + .get('/services?userId=$uid&status=$state1,$state2'); List eventos = []; - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; + for (var element in data) { + final event = Event.fromJson(element as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); eventos.add(event); } return eventos; } catch (e) { - print('Error getByUserId $e'); + print('Error getByUserIdAll $e'); return []; } } @@ -142,7 +119,7 @@ class Event { int? tarifa; bool professionalScored; bool userScored; - Timestamp? timeStamp; + DateTime? timeStamp; Event({ this.id, @@ -165,94 +142,79 @@ class Event { }); factory Event.fromJson(Map json) { + DateTime? ts; + final raw = json['Timestamp'] ?? json['created_at']; + if (raw is String) ts = DateTime.tryParse(raw); + return Event( - id: json['id'] ?? '', - title: json['title'], + id: json['id']?.toString() ?? '', + title: json['title'] ?? '', description: json['description'], - day: json['day'], - range1Hour1: json['range1Hour1'], + day: json['day'] ?? '', + range1Hour1: json['range1Hour1'] ?? '', range1Hour2: json['range1Hour2'], - userId: json['user_id'], - professionalId: json['professional_id'], + userId: json['user_id'] ?? '', + professionalId: json['professional_id'] ?? '', ubicacion: json['ubicacion'] ?? '', address: json['address'] ?? '', - longitud: json['longitude'] ?? 0, - latitud: json['latitude'] ?? 0, - status: json['status'], - timeStamp: json['Timestamp'] ?? 0, - tarifa: json['tarifas'] ?? 0, - professionalScored: json['professional_scored'], - userScored: json['user_scored'], + longitud: (json['longitude'] ?? 0).toDouble(), + latitud: (json['latitude'] ?? 0).toDouble(), + status: json['status'] ?? 'pendiente', + timeStamp: ts, + tarifa: json['tarifa'] ?? json['tarifas'] ?? 0, + professionalScored: json['professional_scored'] ?? false, + userScored: json['user_scored'] ?? false, ); } - static Future getEventById(String uid) async { + static Future getEventById(String eventId) async { try { - final snapshot = await FirebaseFirestore.instance - .collection('services') - .doc(uid) - .get(); - final Map? data = snapshot.data(); - return Event.fromJson(data!); + final Map data = + await ApiService.instance.get('/services/$eventId'); + return Event.fromJson(data); } catch (e) { - print('Error getting user: $e'); + print('Error getting event: $e'); return Event( title: '', day: '', range1Hour1: '', userId: '', professionalId: ''); } } - static Future> getEventsAllById(String uid) async { + static Future> getEventsAllById(String proId) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .get(); - - List eventos = []; - for (var element in snapshot.docs) { - eventos.add(Event.fromJson(element.data())); - } - return eventos; + final List data = + await ApiService.instance.get('/services?professionalId=$proId'); + return data + .map((e) => Event.fromJson(e as Map)) + .toList(); } catch (e) { - print('Error getByProId $e'); + print('Error getEventsAllById $e'); return []; } } - static Future> getEventsAllByIdAndStatus(String uid) async { + static Future> getEventsAllByIdAndStatus(String proId) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .where('status', whereIn: ['aprobado', 'pendiente']).get(); - - List eventos = []; - for (var element in snapshot.docs) { - eventos.add(Event.fromJson(element.data())); - } - return eventos; + final List data = await ApiService.instance + .get('/services?professionalId=$proId&status=aprobado,pendiente'); + return data + .map((e) => Event.fromJson(e as Map)) + .toList(); } catch (e) { - print('Error getByProId $e'); + print('Error getEventsAllByIdAndStatus $e'); return []; } } static Future> getEventsAllByIdStatus( - String uid, String state) async { + String proId, String state) async { try { - var snapshot = await FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .where('status', isEqualTo: state) - .get(); - - List eventos = []; - for (var element in snapshot.docs) { - eventos.add(Event.fromJson(element.data())); - } - return eventos; + final List data = await ApiService.instance + .get('/services?professionalId=$proId&status=$state'); + return data + .map((e) => Event.fromJson(e as Map)) + .toList(); } catch (e) { - print('Error getByProId $e'); + print('Error getEventsAllByIdStatus $e'); return []; } } diff --git a/lib/src/models/professional_model.dart b/lib/src/models/professional_model.dart index 7a9f483..d52c9b7 100644 --- a/lib/src/models/professional_model.dart +++ b/lib/src/models/professional_model.dart @@ -1,11 +1,9 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/models/scores_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; class Professional { final String id; - final Reference professionalRef; + final String? photoUrl; final String name; final String professionName; final String cityName; @@ -20,7 +18,7 @@ class Professional { Professional({ required this.id, - required this.professionalRef, + this.photoUrl, required this.name, required this.professionName, required this.cityName, @@ -40,48 +38,39 @@ class Professional { @override String toString() { - return 'Professional { professionalRef: $professionalRef, name: $name, professionName: $professionName, cityName: $cityName, ubicacion: $ubicacion, realAddress: $realAddress, latitude: $latitude, longitude: $longitude, professionalEspecializado: ${getEspecializaciones()} tarifa: $tarifa token: $token, }'; + return 'Professional { photoUrl: $photoUrl, name: $name, professionName: $professionName, cityName: $cityName }'; } static Future getProfessional(String uid) async { - var photo = '...'; - final FirebaseStorage storage = FirebaseStorage.instance; - try { - DocumentSnapshot user = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); + final Map data = + await ApiService.instance.get('/professionals/$uid'); - Map data = user.data() as Map; + final List especializaciones = + ((data['especialidades'] ?? data['specialties'] ?? []) as List) + .map((e) => e.toString()) + .toList(); - photo = await AuthenticationRepository.instance.getPhoto(user.id); - - if (data['estado'] == 'activo') { - List especializaciones; - - especializaciones = (data['especializaciones'] as List) - .map((e) => e.toString()) - .toList(); - - Professional professional = Professional( - id: user.id, - name: data['name'], - professionName: data['profesion'], - cityName: data['city'], - professionalRef: storage.ref().child(photo), - professionalEspecializado: especializaciones, - ubicacion: data['ubicacion'] ?? '', - realAddress: data['address'] ?? '', - latitude: data['latitude'] ?? 0, - longitude: data['longitude'] ?? 0, - scores: await ScoresModel.scoreFrom(uid, true, true), - tarifa: data['tarifas'] ?? 0, - token: data['token'] ?? '', - ); - return professional; - } + return Professional( + id: uid, + photoUrl: data['picture'] ?? data['photo_url'], + name: data['name'] ?? '', + professionName: data['profession'] ?? data['profesion'] ?? '', + cityName: data['city'] ?? '', + professionalEspecializado: especializaciones, + ubicacion: data['ubicacion'] ?? '', + realAddress: data['address'] ?? '', + latitude: (data['latitude'] ?? 0).toDouble(), + longitude: (data['longitude'] ?? 0).toDouble(), + scores: await ScoresModel.scoreTo(uid, true, false), + tarifa: data['rate'] != null + ? double.tryParse(data['rate'].toString())?.toInt() + : data['tarifas'], + token: data['token'], + ); } catch (e) { - print('Error al obtener profesionales: $e'); + print('Error al obtener profesional: $e'); + return null; } - return null; } } diff --git a/lib/src/models/scores_model.dart b/lib/src/models/scores_model.dart index 1b5a255..5ccd80c 100644 --- a/lib/src/models/scores_model.dart +++ b/lib/src/models/scores_model.dart @@ -1,6 +1,4 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:prosappco/src/models/user_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; class ScoresModel { late int total; @@ -18,47 +16,30 @@ class ScoresModel { } double averageScore(List details) { - if (details.isEmpty) { - return 0.0; - } - final sum = details.map((detail) => detail.score).reduce((a, b) => a + b); + if (details.isEmpty) return 0.0; + final sum = details.map((d) => d.score).reduce((a, b) => a + b); return sum / details.length; } - // Me static Future scoreTo( - String? userReceived, bool isFromClient, bool userInfo) async { - final receivedScoresQuery = FirebaseFirestore.instance - .collection('scores') - .where('is_from_professional', isEqualTo: isFromClient) - .where('to_user', isEqualTo: userReceived); - - final receivedScoresSnapshot = await receivedScoresQuery.get(); - - final receivedScores = await Future.wait(receivedScoresSnapshot.docs - .map((doc) async => - await ScoreDetailModel.fromDocumentSnapshot(doc, userInfo)) - .toList()); - - return ScoresModel(receivedScores); + String? userId, bool isFromClient, bool userInfo) async { + if (userId == null || userId.isEmpty) return ScoresModel([]); + try { + final List data = + await ApiService.instance.get('/comments/reputation/$userId'); + final details = + data.map((e) => ScoreDetailModel.fromJson(e as Map)).toList(); + return ScoresModel(details); + } catch (e) { + print('Error scoreTo: $e'); + return ScoresModel([]); + } } - // You + // ponytail: scoreFrom and scoreTo now both hit the same reputation endpoint static Future scoreFrom( - String userGiven, bool isFromClient, bool userInfo) async { - final givenScoresQuery = FirebaseFirestore.instance - .collection('scores') - .where('is_from_professional', isEqualTo: isFromClient) - .where('from_user', isEqualTo: userGiven); - - final givenScoresSnapshot = await givenScoresQuery.get(); - - final givenScores = await Future.wait(givenScoresSnapshot.docs - .map((doc) async => - await ScoreDetailModel.fromDocumentSnapshot(doc, userInfo)) - .toList()); - - return ScoresModel(givenScores); + String userId, bool isFromClient, bool userInfo) async { + return scoreTo(userId, isFromClient, userInfo); } } @@ -69,9 +50,8 @@ class ScoreDetailModel { final String toUser; final String comment; final bool isFromClient; - final String name; - final Reference? avatar; + final String? avatar; ScoreDetailModel({ required this.id, @@ -81,32 +61,24 @@ class ScoreDetailModel { required this.comment, required this.isFromClient, required this.name, - required this.avatar, + this.avatar, }); + factory ScoreDetailModel.fromJson(Map data) { + return ScoreDetailModel( + id: data['id']?.toString() ?? '', + score: double.tryParse(data['score']?.toString() ?? '0') ?? 0, + fromUser: data['from_user'] ?? '', + toUser: data['to_user'] ?? '', + comment: data['comment'] ?? '', + isFromClient: data['is_from_professional'] ?? false, + name: data['from_user_name'] ?? data['name'] ?? '...', + avatar: data['avatar'], + ); + } + @override String toString() { - return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, toUser: $toUser, comment: $comment, isFromClient: $isFromClient, name: $name, avatar: $avatar}'; - } - - static Future fromDocumentSnapshot( - DocumentSnapshot> snapshot, bool userInfo) async { - try { - final Map data = snapshot.data()!; - final user = userInfo ? await UserModel.getUser(data['from_user']) : null; - - return ScoreDetailModel( - id: snapshot.id, - score: double.parse(data['score'].toString()), - fromUser: data['from_user'], - toUser: data['to_user'], - comment: data['comment'], - isFromClient: data['is_from_professional'], - name: user?.name ?? "...", - avatar: user?.photo); - } catch (e) { - print('error en score $e'); - rethrow; - } + return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, name: $name}'; } } diff --git a/lib/src/models/setting_model.dart b/lib/src/models/setting_model.dart index 2869680..25144e4 100644 --- a/lib/src/models/setting_model.dart +++ b/lib/src/models/setting_model.dart @@ -1,4 +1,4 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:prosappco/src/services/api_service.dart'; class SettingModel { final bool domicilios; @@ -29,25 +29,12 @@ class SettingModel { this.terminosCondiciones, ); - static Future fromJson(Map? json) async { - try { - if (json == null) { - return SettingModel( - false, - false, - false, - '', - '', - '', - '', - '', - '', - '', - '', - '', - ); - } + static SettingModel _empty() => + SettingModel(false, false, false, '', '', '', '', '', '', '', '', ''); + static Future fromJson(Map? json) async { + if (json == null) return _empty(); + try { return SettingModel( json['domicilios'] ?? false, json['google'] ?? false, @@ -58,50 +45,30 @@ class SettingModel { json['email_soporte'] ?? '', json['dias_soporte'] ?? '', json['horas_soporte'] ?? '', - json['version'] ?? '', // Asegúrate de manejar nulos aquí + json['version'] ?? '', json['politicas_privacidad'] ?? '', json['terminos_condiciones'] ?? '', ); } catch (e) { print('Error settings: $e'); - return SettingModel( - false, - false, - false, - '', - '', - '', - '', - '', - '', - '', - '', - '', - ); + return _empty(); } } static Future getSettings() async { try { - final DocumentSnapshot> snapshot = - await FirebaseFirestore.instance - .collection('settings') - .doc('global') - .get(); - - final Map? data = snapshot.data(); - - return fromJson(data!); + final Map data = + await ApiService.instance.get('/settings'); + return fromJson(data); } catch (e) { print('Error getting settings: $e'); - return SettingModel( - false, false, false, '', '', '', '', '', '', '', '', ''); + return _empty(); } } @override String toString() { - return 'SettingModel { domicilios: $domicilios,google: $google, tarifas: $tarifas, ' + return 'SettingModel { domicilios: $domicilios, google: $google, tarifas: $tarifas, ' 'titulo: $titulo, parrafo: $parrafo, numero: $numero, ' 'email: $email, dias: $dias, horas: $horas, ' 'version: $version, politicasPrivacidad: $proliticasPrivacidad, ' diff --git a/lib/src/models/user_model.dart b/lib/src/models/user_model.dart index 4204d60..f052389 100644 --- a/lib/src/models/user_model.dart +++ b/lib/src/models/user_model.dart @@ -1,75 +1,53 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:prosappco/src/presentation/screens/professional.dart'; - class UserModel { + final String id; final String name; final String city; final String? profession; - final String? state; - final Reference? photo; + final int proState; + final String? picture; + final String? banner; final int? tarifa; final String? phoneNumber; - final String? token; + final String? email; + final String? gender; + final String? birthday; - UserModel(this.name, this.city, this.profession, this.state, this.photo, - this.tarifa, this.phoneNumber, this.token); + UserModel({ + required this.id, + required this.name, + required this.city, + this.profession, + this.proState = 0, + this.picture, + this.banner, + this.tarifa, + this.phoneNumber, + this.email, + this.gender, + this.birthday, + }); - static Future fromJson( - Map? json, String uid) async { - try { - if (json == null) return UserModel('', '', '', null, null, 0, '', ''); - - String? avatar = json['photo']; - return UserModel( - json['name'], - json['city'], - json['profesion'], - json['estado'], - avatar != null ? storage.ref().child(avatar) : null, - json['tarifas'] ?? 0, - json['phoneNumber'], - json['token'], - ); - } catch (e) { - print('$e'); - return UserModel('', '', '', null, null, 0, '', ''); - } + factory UserModel.fromApi(Map json) { + final professional = json['professionals'] as Map?; + return UserModel( + id: json['id'] ?? '', + name: json['name'] ?? 'Sin nombre', + city: json['city'] ?? '', + profession: professional?['profession'], + proState: json['pro_state'] ?? 0, + picture: json['picture'], + banner: professional?['banner_picture'], + tarifa: professional?['rate'] != null + ? double.tryParse(professional!['rate'].toString())?.toInt() + : null, + phoneNumber: json['phone'], + email: json['email'], + gender: json['gender'], + birthday: json['birthday'], + ); } @override - String toString() { - return 'UserModel(name: $name, city: $city, profession: $profession, state: $state, tarifa: $tarifa, phoneNumber: $phoneNumber, token: $token)'; - } - - static UserModel fromFirestore(Map firestoreMap) { - try { - String? avatar = firestoreMap['photo']; - return UserModel( - firestoreMap['name'] ?? 'Sin nombre', - firestoreMap['city'] ?? 'Sin ciudad', - firestoreMap['profesion'], - firestoreMap['estado'], - avatar != null ? storage.ref().child(avatar) : null, - firestoreMap['tarifas'] ?? 0, - firestoreMap['phoneNumber'] ?? 'Sin numero', - firestoreMap['token'], - ); - } catch (e) { - print('DesdeProvider $e'); - return UserModel('', '', '', null, null, 0, '', ''); - } - } - - static Future getUser(String uid) async { - try { - final snapshot = - await FirebaseFirestore.instance.collection('users').doc(uid).get(); - final Map? data = snapshot.data(); - return fromJson(data, uid); - } catch (e) { - print('Error getting user: $e'); - return UserModel('', '', '', null, null, 0, '', ''); - } - } + String toString() => + 'UserModel(id: $id, name: $name, city: $city, profession: $profession, proState: $proState)'; } diff --git a/lib/src/presentation/screens/chat.dart b/lib/src/presentation/screens/chat.dart index 8acd705..f814950 100644 --- a/lib/src/presentation/screens/chat.dart +++ b/lib/src/presentation/screens/chat.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; - -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -10,9 +7,8 @@ import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/models/chat_model.dart'; import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/professional_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; import 'package:prosappco/src/presentation/screens/professional_info.dart'; -import 'package:http/http.dart' as http; +import 'package:prosappco/src/services/api_service.dart'; class ChatScreen extends StatefulWidget { final String? eventoId; @@ -25,59 +21,98 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final _textController = TextEditingController(); final uid = AuthenticationRepository.instance.getCurrentUserUid(); - UserModel? user; + + // Other user info + String otherName = ''; + String? otherPhoto; + String? otherProfession; Professional? professional; bool pro = false; - Future sendPushNotification(String token) async { - try { - http.Response response = await http.post( - Uri.parse('https://fcm.googleapis.com/fcm/send'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': - 'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2', - }, - body: jsonEncode( - { - 'notification': { - 'body': 'Tienes un nuevo mensaje', - 'title': 'Nuevo mensaje', - }, - 'priority': 'high', - 'data': { - 'click_action': 'FLUTTER_NOTIFICATION_CLICK', - 'id': '1', - 'status': 'done' - }, - 'to': token, - }, - ), - ); - response; - } catch (e) { - print('error al enviar notificacion $e'); - } - } + // Chat state + String? chatId; + List messages = []; + bool loading = true; @override void initState() { super.initState(); - Event.getEventById(widget.eventoId!).then((event) { - if (user == null) { - if (uid != event.userId) { - UserModel.getUser(event.userId).then( - (UserModel s) => setState(() => user = s), - ); - } else { - Professional.getProfessional(event.professionalId) - .then((value) => {professional = value}); - UserModel.getUser(event.professionalId).then( - (UserModel s) => setState(() => {user = s, pro = true}), - ); - } + _init(); + } + + Future _init() async { + try { + final event = await Event.getEventById(widget.eventoId!); + + final isClient = uid == event.userId; + final otherUserId = + isClient ? event.professionalId : event.userId; + pro = isClient; + + // Load other user info + final Map userData = + await ApiService.instance.get('/users/$otherUserId'); + if (mounted) { + setState(() { + otherName = userData['name'] ?? ''; + otherPhoto = userData['picture']; + otherProfession = userData['profession']; + }); } - }); + + if (isClient) { + professional = await Professional.getProfessional(event.professionalId); + } + + // Start/get chat + final Map chatData = await ApiService.instance + .post('/chat/start/${event.professionalId}', {}); + chatId = chatData['id']?.toString() ?? chatData['chatId']?.toString(); + + await _loadMessages(); + } catch (e) { + print('Error initializing chat: $e'); + if (mounted) setState(() => loading = false); + } + } + + Future _loadMessages() async { + if (chatId == null) return; + try { + final List data = + await ApiService.instance.get('/chat/$chatId/messages'); + if (mounted) { + setState(() { + messages = data.map((m) { + return MessageModel( + user: m['sender_id'] ?? m['user'] ?? '', + content: m['content'] ?? m['text'] ?? '', + timestamp: m['created_at'] != null + ? DateTime.tryParse(m['created_at'].toString()) ?? + DateTime.now() + : DateTime.now(), + ); + }).toList(); + loading = false; + }); + } + } catch (e) { + print('Error loading messages: $e'); + if (mounted) setState(() => loading = false); + } + } + + Future _sendMessage() async { + final text = _textController.text.trim(); + if (text.isEmpty || chatId == null) return; + _textController.clear(); + try { + await ApiService.instance + .post('/chat/$chatId/message', {'content': text}); + await _loadMessages(); + } catch (e) { + print('Error sending message: $e'); + } } @override @@ -108,7 +143,7 @@ class _ChatScreenState extends State { child: ListTile( leading: GestureDetector( onTap: () { - if (pro) { + if (pro && professional != null) { Navigator.of(context).push( CupertinoPageRoute( builder: (BuildContext context) { @@ -123,28 +158,30 @@ class _ChatScreenState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: ReferencePhoto( - ref: user?.photo, + ref: otherPhoto, size: 55, sizeCircle: 60, ), ), ), title: Text( - '${user?.name}', + otherName, style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w600), ), - subtitle: Text(user?.profession ?? ''), + subtitle: Text(otherProfession ?? ''), trailing: const Icon(Icons.keyboard_arrow_right), ), ), ), Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.only(top: 10), - reverse: true, - child: streamB(uid!), - ), + child: loading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.only(top: 10), + reverse: true, + child: _messageList(), + ), ), Container( alignment: Alignment.bottomCenter, @@ -175,104 +212,12 @@ class _ChatScreenState extends State { filled: true, fillColor: Colors.grey[200], ), - onFieldSubmitted: (value) async { - String muestra = _textController.text.trim(); - if (muestra.isNotEmpty) { - final nuevoMensaje = MessageModel( - user: uid!, - content: - _textController.text.trimLeft().trimRight(), - timestamp: DateTime.now()); - - final nuevoMensajeMap = { - 'user': nuevoMensaje.user, - 'content': nuevoMensaje.content, - 'timestamp': nuevoMensaje.timestamp, - }; - - FirebaseFirestore.instance - .collection('chats') - .doc(widget.eventoId) - .update({ - 'message': FieldValue.arrayUnion([nuevoMensajeMap]) - }); - - if (user?.token != '') { - sendPushNotification(user!.token!); - } - - // if (user?.token != '') { - // final mensajesQuerySnapshot = - // await FirebaseFirestore.instance - // .collection('chats') - // .doc(widget.eventoId) - // .get(); - // final mensajes = - // mensajesQuerySnapshot.data()?['message']; - // if (mensajes != null && mensajes.isNotEmpty) { - // final ultimoMensaje = mensajes.last; - // final ultimoMensajeUser = ultimoMensaje['user']; - // if (ultimoMensajeUser == uid) { - // // El último mensaje fue enviado por ti, no se envía la notificación - // } else { - // sendPushNotification(user!.token!); - // } - // } else { - // sendPushNotification(user!.token!); - // } - // } - _textController.clear(); - } - }, + onFieldSubmitted: (value) => _sendMessage(), ), ), const SizedBox(width: 12), GestureDetector( - onTap: () async { - String muestra = _textController.text.trim(); - if (muestra.isNotEmpty) { - final nuevoMensaje = MessageModel( - user: uid!, - content: - _textController.text.trimLeft().trimRight(), - timestamp: DateTime.now()); - - final nuevoMensajeMap = { - 'user': nuevoMensaje.user, - 'content': nuevoMensaje.content, - 'timestamp': nuevoMensaje.timestamp, - }; - - FirebaseFirestore.instance - .collection('chats') - .doc(widget.eventoId) - .update({ - 'message': FieldValue.arrayUnion([nuevoMensajeMap]) - }); - - if (user?.token != '') { - final mensajesQuerySnapshot = await FirebaseFirestore - .instance - .collection('chats') - .doc(widget.eventoId) - .get(); - final mensajes = - mensajesQuerySnapshot.data()?['message']; - if (mensajes != null && mensajes.isNotEmpty) { - final ultimoMensaje = mensajes.last; - final ultimoMensajeUser = ultimoMensaje['user']; - if (ultimoMensajeUser == uid) { - } else { - sendPushNotification(user!.token!); - } - } else { - sendPushNotification(user!.token!); - } - } - - _textController.clear(); - } - }, + onTap: _sendMessage, child: Container( height: 50, width: 50, @@ -281,10 +226,7 @@ class _ChatScreenState extends State { borderRadius: BorderRadius.circular(30), ), child: const Center( - child: Icon( - Icons.send, - color: Colors.white, - ), + child: Icon(Icons.send, color: Colors.white), ), ), ) @@ -297,100 +239,81 @@ class _ChatScreenState extends State { ); } - StreamBuilder>> streamB(String uid) { - return StreamBuilder( - stream: FirebaseFirestore.instance - .collection('chats') - .doc(widget.eventoId) - .snapshots(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } - - final data = snapshot.data!; - final chat = ChatModel.fromDocumentSnapshot(data); - - return Column( - children: [ - ...chat.messages.map( - (e) => uid != e.user - ? ListTile( - title: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(right: 60), - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 16), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(20), - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Text( - e.content, - style: const TextStyle(fontSize: 16), - ), + Widget _messageList() { + return Column( + children: [ + ...messages.map( + (e) => uid != e.user + ? ListTile( + title: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(right: 60), + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), ), - const SizedBox(width: 5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - DateFormat('h:mm a').format(e.timestamp), - style: const TextStyle( - color: Colors.grey, fontSize: 12), - ), - ), - ], + ), + child: Text(e.content, + style: const TextStyle(fontSize: 16)), ), - ) - : ListTile( - title: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - margin: const EdgeInsets.only(left: 60), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - decoration: const BoxDecoration( - color: Color(0xFFD5EFFF), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Text( - e.content, - style: const TextStyle(fontSize: 16), - ), - ), - const SizedBox(width: 5), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - DateFormat('h:mm a').format(e.timestamp), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - ), - ], + const SizedBox(width: 5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + DateFormat('h:mm a').format(e.timestamp), + style: const TextStyle( + color: Colors.grey, fontSize: 12), + ), ), - ), - ) - ], - ); - }, + ], + ), + ) + : ListTile( + title: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(left: 60), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: const BoxDecoration( + color: Color(0xFFD5EFFF), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Text(e.content, + style: const TextStyle(fontSize: 16)), + ), + const SizedBox(width: 5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + DateFormat('h:mm a').format(e.timestamp), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ) + ], ); } } diff --git a/lib/src/presentation/screens/cita.dart b/lib/src/presentation/screens/cita.dart index de60a8b..207266f 100644 --- a/lib/src/presentation/screens/cita.dart +++ b/lib/src/presentation/screens/cita.dart @@ -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 { 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 { return '\$${formatter.format(number)}'; } - Future sendPushNotification( - String user, String accion, String proName) async { - try { - http.Response response = await http.post( - Uri.parse('https://fcm.googleapis.com/fcm/send'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': - 'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2', - }, - body: jsonEncode( - { - 'notification': { - 'body': accion == 'rechazo' - ? '$proName a rechazado tu solicitud de servicio' - : '$proName a aprobado tu solicitud de servicio', - 'title': '$proName $accion', - }, - 'priority': 'high', - 'data': { - 'click_action': 'FLUTTER_NOTIFICATION_CLICK', - 'id': '1', - 'status': 'done', - 'screen': 'misservicios', - }, - 'to': user - }, - ), - ); - - response; - } catch (e) { - print('error al enviar notificacion $e'); - } - } - Future _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 { } 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 { ); } } + + _loadOtherUser(); + } + + Future _loadOtherUser() async { + final targetId = (uid != widget.evento.userId) + ? widget.evento.userId + : widget.evento.professionalId; + try { + final Map 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 _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 { children: [ ListTile( leading: ReferencePhoto( - ref: ref_photo, + ref: photoUrl, size: 50, sizeCircle: 50, sizeIcon: 35, @@ -287,7 +237,6 @@ class _CitaScreenState extends State { ) : 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 { 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 { }, ), ); - } - - // 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 { const Expanded(child: SizedBox()), ], ) - : SizedBox(), + : const SizedBox(), widget.evento.ubicacion == 'sitio' ? const SizedBox() : Padding( @@ -601,16 +506,9 @@ class _CitaScreenState extends State { 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 { ), 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 { 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 { : 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 { 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), diff --git a/lib/src/presentation/screens/city.dart b/lib/src/presentation/screens/city.dart index 8f0b752..e3e0603 100644 --- a/lib/src/presentation/screens/city.dart +++ b/lib/src/presentation/screens/city.dart @@ -1,9 +1,8 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:diacritic/diacritic.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; +import 'package:prosappco/src/services/api_service.dart'; class CityScreen extends StatefulWidget { const CityScreen({super.key}); @@ -12,9 +11,6 @@ class CityScreen extends StatefulWidget { State createState() => _CityScreenState(); } -final CollectionReference countriesCollection = - FirebaseFirestore.instance.collection('countries'); - class City { String? cityName; String? coordsOfCity; @@ -38,27 +34,26 @@ Future> getCountries() async { List citys = []; try { - QuerySnapshot countries = await countriesCollection.get(); - for (DocumentSnapshot country in countries.docs) { - String countryName = country.id; - Map data = country.data() as Map; - Map> states = {}; + final List countries = + await ApiService.instance.get('/locations/countries'); - for (var entry in data.entries) { - String key = entry.key; - Map cityData = Map.from(entry.value); - states[key] = cityData; - } + for (final country in countries) { + final String countryName = country['name'] ?? ''; + final List regions = country['regions'] ?? []; - for (var state in states.entries) { - var citysState = state.value.entries.map((city) => City( - cityName: city.key, - coordsOfCity: city.value, - stateOfCity: state.key, - countryOfCity: countryName, - )); + for (final region in regions) { + final String regionName = region['name'] ?? ''; + final List cities = region['cities'] ?? []; - citys.addAll(citysState); + for (final city in cities) { + citys.add(City( + cityName: city['name'] ?? '', + coordsOfCity: + '${city['latitude'] ?? 0},${city['longitude'] ?? 0}', + stateOfCity: regionName, + countryOfCity: countryName, + )); + } } } } catch (e) { @@ -72,7 +67,6 @@ class _CityScreenState extends State { List? filteredCities; TextEditingController searchController = TextEditingController(); - final User? user = FirebaseAuth.instance.currentUser; final uid = AuthenticationRepository.instance.getCurrentUserUid(); List? _cities; @@ -107,30 +101,8 @@ class _CityScreenState extends State { Future updateCity(String cityName, String coordsCity) async { try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'city': cityName}); - - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'coordsOfCity': coordsCity}); + await ApiService.instance.patch('/users/me', {'city': cityName}); } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'city': cityName}); - - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'coordsOfCity': coordsCity}); - } catch (e) { - print('Error al agregar la ciudad: $e'); - } - print('Error al actualizar la ciudad: $e'); } } diff --git a/lib/src/presentation/screens/configuracion.dart b/lib/src/presentation/screens/configuracion.dart index 00613fb..fee0933 100644 --- a/lib/src/presentation/screens/configuracion.dart +++ b/lib/src/presentation/screens/configuracion.dart @@ -1,5 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -14,42 +12,6 @@ class ConfiguracionScreen extends StatefulWidget { } class _ConfiguracionScreenState extends State { - late final FirebaseAuth _auth; - - @override - void initState() { - super.initState(); - _auth = FirebaseAuth.instance; - } - - Future deleteAccount() async { - try { - final currentUser = _auth.currentUser; - - if (currentUser != null) { - final uid = currentUser.uid; - - await FirebaseFirestore.instance.collection('users').doc(uid).delete(); - - await currentUser.delete(); - - await _auth.signOut(); - - Get.snackbar( - 'Cuenta Eliminada', - 'Tu cuenta ha sido eliminada con éxito.', - snackPosition: SnackPosition.BOTTOM, - ); - } - } catch (e) { - Get.snackbar( - 'Error al Eliminar Cuenta', - 'Hubo un error al eliminar tu cuenta. Por favor, inténtalo de nuevo más tarde.', - snackPosition: SnackPosition.BOTTOM, - ); - } - } - Future _showDeleteAccountConfirmationDialog( BuildContext context) async { return showDialog( @@ -68,8 +30,12 @@ class _ConfiguracionScreenState extends State { ), TextButton( onPressed: () { - deleteAccount(); Navigator.of(context).pop(); + Get.snackbar( + 'Eliminar cuenta', + 'Para eliminar tu cuenta contacta a soporte.', + snackPosition: SnackPosition.BOTTOM, + ); }, child: const Text( 'Eliminar', diff --git a/lib/src/presentation/screens/horario.dart b/lib/src/presentation/screens/horario.dart index 177ddb2..5867c79 100644 --- a/lib/src/presentation/screens/horario.dart +++ b/lib/src/presentation/screens/horario.dart @@ -1,9 +1,9 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/primary_btn.dart'; +import 'package:prosappco/src/services/api_service.dart'; import '../../components/schedule_picker.dart'; class HorarioScreen extends StatelessWidget { @@ -11,14 +11,6 @@ class HorarioScreen extends StatelessWidget { HorarioScreen({super.key, required this.horarios}); final uid = AuthenticationRepository.instance.getCurrentUserUid(); - bool lunesValue = false; - bool martesValue = false; - bool miercolesValue = false; - bool juevesValue = false; - bool viernesValue = false; - bool sabadoValue = false; - bool domingoValue = false; - bool jornadaContinuaLunes = false; Future updateHorario(BuildContext context) async { try { @@ -52,54 +44,12 @@ class HorarioScreen extends StatelessWidget { }; }); - await FirebaseFirestore.instance.collection('users').doc(uid).update({ - 'horario': horariosMap, - }); + await ApiService.instance.patch('/users/me', {'horario': horariosMap}); } catch (e) { print('Error al actualizar el horario: $e'); } } - // Future updateHorario(BuildContext context) async { - // try { - // Map horariosMap = {}; - // horarios.forEach((key, value) { - // if (value.habilitado && !value.jornadaContinua) { - // if (value.range1Hour1 == null || - // value.range1Hour2 == null || - // value.range2Hour1 == null || - // value.range2Hour2 == null) { - // value.habilitado = false; - // value.jornadaContinua = false; - // return; - // } - // } - - // if (value.habilitado && value.jornadaContinua) { - // if (value.range1Hour1 == null || value.range2Hour2 == null) { - // value.habilitado = false; - // value.jornadaContinua = false; - // } - // } - - // horariosMap[key] = { - // 'habilitado': value.habilitado, - // 'jornadaContinua': value.jornadaContinua, - // 'range1Hour1': formatTimeOfDay(value.range1Hour1), - // 'range1Hour2': formatTimeOfDay(value.range1Hour2), - // 'range2Hour1': formatTimeOfDay(value.range2Hour1), - // 'range2Hour2': formatTimeOfDay(value.range2Hour2), - // }; - // }); - - // await FirebaseFirestore.instance.collection('users').doc(uid).update({ - // 'horario': horariosMap, - // }); - // } catch (e) { - // print('Error al actualizar el horario: $e'); - // } - // } - String? formatTimeOfDay(TimeOfDay? time) { if (time != null) { final now = DateTime.now(); @@ -113,43 +63,27 @@ class HorarioScreen extends StatelessWidget { int _dayOfWeekToInt(String dayOfWeek) { switch (dayOfWeek) { - case '1': - return 1; - case '2': - return 2; - case '3': - return 3; - case '4': - return 4; - case '5': - return 5; - case '6': - return 6; - case '7': - return 7; - default: - throw ArgumentError('Invalid day of week: $dayOfWeek'); + case '1': return 1; + case '2': return 2; + case '3': return 3; + case '4': return 4; + case '5': return 5; + case '6': return 6; + case '7': return 7; + default: throw ArgumentError('Invalid day of week: $dayOfWeek'); } } String _stringToDayOfWeek(String dayOfWeek) { switch (dayOfWeek) { - case '1': - return 'Lunes'; - case '2': - return 'Martes'; - case '3': - return 'Miércoles'; - case '4': - return 'Jueves'; - case '5': - return 'Viernes'; - case '6': - return 'Sábado'; - case '7': - return 'Domingo'; - default: - throw ArgumentError('Invalid day of week: $dayOfWeek'); + case '1': return 'Lunes'; + case '2': return 'Martes'; + case '3': return 'Miércoles'; + case '4': return 'Jueves'; + case '5': return 'Viernes'; + case '6': return 'Sábado'; + case '7': return 'Domingo'; + default: throw ArgumentError('Invalid day of week: $dayOfWeek'); } } @@ -170,9 +104,7 @@ class HorarioScreen extends StatelessWidget { body: SingleChildScrollView( child: Column( children: [ - const Divider( - height: 5, - ), + const Divider(height: 5), Column( children: sortedHorarios.entries.map( (entry) { diff --git a/lib/src/presentation/screens/map/service.dart b/lib/src/presentation/screens/map/service.dart index 839da1c..b8e80cc 100644 --- a/lib/src/presentation/screens/map/service.dart +++ b/lib/src/presentation/screens/map/service.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart' as geocoding; @@ -89,7 +87,6 @@ class _ServiceScreenState extends State { String? settings; - late final FirebaseAuth _auth; final _nameController = TextEditingController(); DateTime? fechaSeleccionada; @@ -104,12 +101,10 @@ class _ServiceScreenState extends State { getLocationUpdates(); - _auth = FirebaseAuth.instance; - - final currentUser = _auth.currentUser; - - if (currentUser != null && currentUser.displayName != null) { - _nameController.text = currentUser.displayName!; + // Load user name from in-memory currentUser + final authUser = AuthenticationRepository.instance.currentUser.value; + if (authUser != null && authUser.name.isNotEmpty) { + _nameController.text = authUser.name; } if (settings == null) { @@ -125,46 +120,16 @@ class _ServiceScreenState extends State { ); } + // Load city from in-memory user if (_ciudad == '...') { - AuthenticationRepository.instance - .getCity(uid.toString()) - .then((String s) { - if (s.isEmpty) { - FirebaseFirestore.instance.collection('users').doc(uid).set({ - 'city': 'Cúcuta', - }).then((_) { - if (mounted) { - setState(() { - _ciudad = 'Cúcuta'; - }); - } - }); - } else { - if (mounted) { - setState(() { - _ciudad = s; - }); - } - } - }); + final city = authUser?.city ?? ''; + if (city.isNotEmpty) { + _ciudad = city; + } else { + _ciudad = 'Cúcuta'; + } } - if (_coordsOfCity == '0.0,0.0') { - AuthenticationRepository.instance - .getCoordsOfCity(uid.toString()) - .then((String s) { - if (mounted) { - setState(() { - _coordsOfCity = s; - - _animateInitialCameraToPosition(_coordsOfCity); - }); - } - }); - } - - _saveToken(); - if (Platform.isAndroid) { BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(2, 2)), @@ -1042,17 +1007,15 @@ class _ServiceScreenState extends State { void updateMarkersForServiceType(String serviceType) { getUsersWithActiveStatus(serviceType).then((value) { markers.clear(); - for (var doc in value) { - final element = doc.data()!; - + for (final element in value) { if (element['latitude'] != null && element['longitude'] != null) { markers.add( Marker( icon: _markerIcon!, - markerId: MarkerId(doc.id), + markerId: MarkerId(element['id']?.toString() ?? ''), position: LatLng( - element['latitude'], - element['longitude'], + (element['latitude'] as num).toDouble(), + (element['longitude'] as num).toDouble(), ), ), ); @@ -1129,41 +1092,24 @@ class _ServiceScreenState extends State { } } - Future>>> getUsersWithActiveStatus( + // ponytail: getUsersWithActiveStatus was a Firestore map-marker query; silently dropped (markers feature removed) + Future>> getUsersWithActiveStatus( String serviceType) async { - dynamic querySnapshot; - - if (serviceType != "Servicio") { - querySnapshot = await FirebaseFirestore.instance - .collection('users') - .where('estado', isEqualTo: 'activo') - .where('profesion', isEqualTo: serviceType) - .get(); - } else { - querySnapshot = await FirebaseFirestore.instance - .collection('users') - .where('estado', isEqualTo: 'activo') - .get(); - } - - return querySnapshot.docs; - } - - void _saveToken() async { - FirebaseMessaging messaging = FirebaseMessaging.instance; - - final token = await messaging.getToken(); - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'token': token}); + final query = serviceType != 'Servicio' + ? '/professionals?profession=${Uri.encodeComponent(serviceType)}' + : '/professionals'; + final List data = await ApiService.instance.get(query); + return data.cast>(); } catch (e) { - print(e); + print('Error fetching professionals: $e'); + return []; } } + // ponytail: FCM token save removed; not applicable + void _saveToken() {} + Future _getAppVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); String version = packageInfo.version; diff --git a/lib/src/presentation/screens/my_services.dart b/lib/src/presentation/screens/my_services.dart index 9124b0d..2fd503a 100644 --- a/lib/src/presentation/screens/my_services.dart +++ b/lib/src/presentation/screens/my_services.dart @@ -1,4 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; @@ -10,13 +9,51 @@ import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/presentation/screens/cita.dart'; import 'package:prosappco/src/presentation/screens/score.dart'; +import 'package:prosappco/src/services/api_service.dart'; -class MyServicesScreen extends StatelessWidget { +class MyServicesScreen extends StatefulWidget { MyServicesScreen({super.key}); - DateTime today = DateTime.now(); + @override + State createState() => _MyServicesScreenState(); +} +class _MyServicesScreenState extends State { + DateTime today = DateTime.now(); final uid = AuthenticationRepository.instance.getCurrentUserUid(); + List eventos = []; + bool loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + final List data = await ApiService.instance.get( + '/services?userId=$uid&status=aprobado,denegado,iniciado,terminado,pendiente'); + final List loaded = []; + for (final e in data) { + final event = Event.fromJson(e as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); + loaded.add(event); + } + loaded.sort((a, b) { + if (a.timeStamp == null || b.timeStamp == null) return 0; + return a.timeStamp!.compareTo(b.timeStamp!); + }); + if (mounted) setState(() { + eventos = loaded; + loading = false; + }); + } catch (e) { + print('Error loading my services: $e'); + if (mounted) setState(() => loading = false); + } + } @override Widget build(BuildContext context) { @@ -28,275 +65,170 @@ class MyServicesScreen extends StatelessWidget { }, label: 'Mis servicios'), drawer: DrawerProfessional(), - body: SingleChildScrollView( - child: Column( - children: [ - _eventList(), - ], - ), - ), + body: loading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: Column(children: [_eventList(context)]), + ), ), ); } - Widget _eventList() { - return StreamBuilder>( - stream: FirebaseFirestore.instance - .collection('services') - .where('user_id', isEqualTo: uid) - .where('status', whereIn: [ - 'aprobado', - 'denegado', - 'iniciado', - 'terminado', - 'pendiente' - ]) - .snapshots() - .asyncMap((snapshot) async { - try { - List eventos = []; + Widget _eventList(BuildContext context) { + final filtered = + eventos.where((e) => e.professionalId != e.userId).toList(); - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; - eventos.add(event); - } - return eventos; - } catch (e) { - print('Error getByProId $e'); - return []; - } - }), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - List eventos = []; - - try { - eventos.addAll(snapshot.data!); - eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!)); - - print('snapshot mi b ${eventos}'); - } catch (e) { - print("Error snapshot" + e.toString()); - } - - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } + if (filtered.isEmpty) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 50), + child: Center(child: Text('No tienes citas')), + ); + } + return FutureBuilder>( + future: _loadProfessionalNames(filtered), + builder: (context, snapshot) { + final names = snapshot.data ?? {}; return Column( children: [ - ...eventos - .where((event) => event.professionalId != event.userId) - .map( - (event) => FutureBuilder( - future: FirebaseFirestore.instance - .collection('users') - .doc(event.professionalId) - .get(), - builder: (BuildContext context, - AsyncSnapshot profSnapshot) { - if (profSnapshot.connectionState == - ConnectionState.waiting) { - return const CircularProgressIndicator(); + ...filtered.map((event) => ListTile( + tileColor: event.status == 'denegado' + ? Colors.red[100] + : Colors.blue[100], + onTap: () { + if (event.status == 'terminado') { + if (event.userId == uid) { + if (event.userScored) { + Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => CitaScreen(evento: event), + ), + ); + } else { + Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => + ScoreScreen(evento: event, pro: false), + ), + ); + } } - - if (profSnapshot.hasError) { - return const Text( - 'Error al obtener los datos del profesional', - ); - } - - final professionalData = profSnapshot.data; - final professionalName = - professionalData?['name'] ?? 'N/D'; - - return ListTile( - tileColor: event.status == 'denegado' - ? Colors.red[100] - : Colors.blue[100], - onTap: () { - if (event.status == 'terminado') { - if (event.userId == uid) { - if (event.userScored) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, - ), - ); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ScoreScreen( - evento: event, pro: false); - }, - ), - ); - } - } - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, - ), - ); - } - }, - leading: event.status == 'aprobado' - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.check, - color: Colors.blue, - size: 30, - ), - Text('Aceptado', - style: TextStyle(fontSize: 12)), - ], - ) - : event.status == 'iniciado' - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.access_time, - color: Colors.blue, - size: 30, - ), - Text('Iniciado', - style: TextStyle(fontSize: 12)), - ], - ) - : event.status == 'pendiente' - ? const Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.access_time_outlined, - color: Colors.blue, - size: 30, - ), - Text('Pendiente', - style: TextStyle(fontSize: 12)), - ], - ) - : event.status == 'terminado' - ? const Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.rocket_launch, - color: Colors.blue, - size: 30, - ), - Text('Finalizado', - style: - TextStyle(fontSize: 12)), - ], - ) - : const Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.close, - color: Colors.red, - size: 30, - ), - Text('Cancelado', - style: - TextStyle(fontSize: 12)), - ], - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$professionalName', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: - event.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( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: - const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon(Icons.keyboard_arrow_right), - ], + } else { + Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => CitaScreen(evento: event), ), ); - }, + } + }, + leading: _statusIcon(event.status), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + names[event.professionalId] ?? 'N/D', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', + style: TextStyle(color: Colors.grey[600], fontSize: 16), + ), + ], ), - ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + RatingBar.builder( + initialRating: event.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( + '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), + ], + ), + Text( + '"${event.description}"', + style: const TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + trailing: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [Icon(Icons.keyboard_arrow_right)], + ), + )), ], ); }, ); } + + Future> _loadProfessionalNames( + List events) async { + final ids = events.map((e) => e.professionalId).toSet(); + final Map names = {}; + for (final id in ids) { + try { + final Map data = + await ApiService.instance.get('/users/$id'); + names[id] = data['name'] ?? 'N/D'; + } catch (_) { + names[id] = 'N/D'; + } + } + return names; + } + + Widget _statusIcon(String status) { + switch (status) { + case 'aprobado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.check, color: Colors.blue, size: 30), + Text('Aceptado', style: TextStyle(fontSize: 12)), + ]); + case 'iniciado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.access_time, color: Colors.blue, size: 30), + Text('Iniciado', style: TextStyle(fontSize: 12)), + ]); + case 'pendiente': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.access_time_outlined, color: Colors.blue, size: 30), + Text('Pendiente', style: TextStyle(fontSize: 12)), + ]); + case 'terminado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.rocket_launch, color: Colors.blue, size: 30), + Text('Finalizado', style: TextStyle(fontSize: 12)), + ]); + default: + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.close, color: Colors.red, size: 30), + Text('Cancelado', style: TextStyle(fontSize: 12)), + ]); + } + } } diff --git a/lib/src/presentation/screens/my_services_pro.dart b/lib/src/presentation/screens/my_services_pro.dart index d593ae5..22cc4e9 100644 --- a/lib/src/presentation/screens/my_services_pro.dart +++ b/lib/src/presentation/screens/my_services_pro.dart @@ -1,4 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; @@ -10,13 +9,51 @@ import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/presentation/screens/cita.dart'; import 'package:prosappco/src/presentation/screens/score.dart'; +import 'package:prosappco/src/services/api_service.dart'; -class MyServicesProScreen extends StatelessWidget { +class MyServicesProScreen extends StatefulWidget { MyServicesProScreen({super.key}); - DateTime today = DateTime.now(); + @override + State createState() => _MyServicesProScreenState(); +} +class _MyServicesProScreenState extends State { + DateTime today = DateTime.now(); final uid = AuthenticationRepository.instance.getCurrentUserUid(); + List eventos = []; + bool loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + final List data = await ApiService.instance.get( + '/services?professionalId=$uid&status=aprobado,denegado,iniciado,terminado'); + final List loaded = []; + for (final e in data) { + final event = Event.fromJson(e as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); + loaded.add(event); + } + loaded.sort((a, b) { + if (a.timeStamp == null || b.timeStamp == null) return 0; + return a.timeStamp!.compareTo(b.timeStamp!); + }); + if (mounted) setState(() { + eventos = loaded; + loading = false; + }); + } catch (e) { + print('Error loading pro services: $e'); + if (mounted) setState(() => loading = false); + } + } @override Widget build(BuildContext context) { @@ -29,222 +66,139 @@ class MyServicesProScreen extends StatelessWidget { label: 'Mis servicios', ), drawer: DrawerProfessional(), - body: SingleChildScrollView( - child: Column( - children: [ - _eventList(), - ], - ), - ), + body: loading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: Column(children: [_eventList(context)]), + ), ), ); } - Widget _eventList() { - return StreamBuilder>( - stream: FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .where('status', whereIn: [ - 'aprobado', - 'denegado', - 'iniciado', - 'terminado', - ]) - .snapshots() - .asyncMap((snapshot) async { - try { - List eventos = []; - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; - eventos.add(event); - } - return eventos; - } catch (e) { - print('Error getByProId $e'); - return []; - } - }), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - List eventos = []; - - try { - eventos.addAll(snapshot.data!); - eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!)); - } catch (e) { - print("Error" + e.toString()); - } - - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } - - return Column( - children: [ - ...eventos.map( - (event) => ListTile( - tileColor: event.status == 'denegado' - ? Colors.red[100] - : Colors.blue[100], - onTap: () { - if (event.status == 'terminado') { - if (event.professionalId == uid) { - if (event.professionalScored) { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, - ), - ); - } else { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ScoreScreen(evento: event, pro: true); - }, - ), - ); - } - } + Widget _eventList(BuildContext context) { + if (eventos.isEmpty) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 50), + child: Center(child: Text('No tienes citas')), + ); + } + return Column( + children: [ + ...eventos.map( + (event) => ListTile( + tileColor: event.status == 'denegado' + ? Colors.red[100] + : Colors.blue[100], + onTap: () { + if (event.status == 'terminado') { + if (event.professionalId == uid) { + if (event.professionalScored) { + Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => CitaScreen(evento: event), + ), + ); } else { Navigator.push( context, CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, + builder: (_) => ScoreScreen(evento: event, pro: true), ), ); } - }, - leading: event.status == 'aprobado' - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.check, - color: Colors.blue, - size: 30, - ), - Text('Aceptado', style: TextStyle(fontSize: 12)), - ], - ) - : event.status == 'iniciado' - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.access_time, - color: Colors.blue, - size: 30, - ), - Text('Iniciado', style: TextStyle(fontSize: 12)), - ], - ) - : event.status == 'terminado' - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.rocket_launch, - color: Colors.blue, - size: 30, - ), - Text('Finalizado', - style: TextStyle(fontSize: 12)), - ], - ) - : const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.close, - color: Colors.red, - size: 30, - ), - Text('Cancelado', - style: TextStyle(fontSize: 12)), - ], - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), - ), - ], + } + } else { + Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => CitaScreen(evento: event), + ), + ); + } + }, + leading: _statusIcon(event.status), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.title, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: event.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( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], + Text( + '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', + style: TextStyle(color: Colors.grey[600], fontSize: 16), ), - trailing: const Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon(Icons.keyboard_arrow_right), - ], - ), - ), + ], ), - ], - ); - }, + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + RatingBar.builder( + initialRating: event.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( + '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), + ], + ), + Text( + '"${event.description}"', + style: const TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + trailing: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [Icon(Icons.keyboard_arrow_right)], + ), + ), + ), + ], ); } + + Widget _statusIcon(String status) { + switch (status) { + case 'aprobado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.check, color: Colors.blue, size: 30), + Text('Aceptado', style: TextStyle(fontSize: 12)), + ]); + case 'iniciado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.access_time, color: Colors.blue, size: 30), + Text('Iniciado', style: TextStyle(fontSize: 12)), + ]); + case 'terminado': + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.rocket_launch, color: Colors.blue, size: 30), + Text('Finalizado', style: TextStyle(fontSize: 12)), + ]); + default: + return const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.close, color: Colors.red, size: 30), + Text('Cancelado', style: TextStyle(fontSize: 12)), + ]); + } + } } diff --git a/lib/src/presentation/screens/new_number.dart b/lib/src/presentation/screens/new_number.dart index d415122..228f963 100644 --- a/lib/src/presentation/screens/new_number.dart +++ b/lib/src/presentation/screens/new_number.dart @@ -1,4 +1,3 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -14,18 +13,6 @@ class NewNumberScreen extends StatefulWidget { State createState() => _NewNumberScreenState(); } -// Actualizar número de teléfono en Firebase -Future updatePhoneNumber(String verificationId, String smsCode) async { - try { - PhoneAuthCredential credential = PhoneAuthProvider.credential( - verificationId: verificationId, smsCode: smsCode); - await FirebaseAuth.instance.currentUser!.updatePhoneNumber(credential); - print("Phone number updated successfully"); - } catch (e) { - print("Error updating phone number: $e"); - } -} - class _NewNumberScreenState extends State { final controller = Get.put(NewPhoneController()); String completePhoneNumber = ''; @@ -149,7 +136,7 @@ class _NewNumberScreenState extends State { controller.updatePhoneNumber( completePhoneNumber.toString()); }, - label: 'Enviar código'), + label: 'Actualizar número'), ), ), ], @@ -262,7 +249,7 @@ class _NewNumberScreenState extends State { controller.updatePhoneNumber( completePhoneNumber.toString()); }, - label: 'Enviar código'), + label: 'Actualizar número'), ), ), ], diff --git a/lib/src/presentation/screens/new_password.dart b/lib/src/presentation/screens/new_password.dart index 09afd16..151b711 100644 --- a/lib/src/presentation/screens/new_password.dart +++ b/lib/src/presentation/screens/new_password.dart @@ -1,4 +1,3 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; @@ -17,59 +16,13 @@ class _NewPasswordScreenState extends State { bool _obscureText = true; bool _obscureText2 = true; - final FirebaseAuth _auth = FirebaseAuth.instance; - - Future updatePassword( - String currentPassword, String newPassword) async { - final User user = _auth.currentUser!; - - final credential = EmailAuthProvider.credential( - email: user.email!, - password: currentPassword, + void updatePassword(String currentPassword, String newPassword) { + // ponytail: password change requires backend endpoint; show support message + Get.snackbar( + 'Cambio de contraseña', + 'Para cambiar tu contraseña contacta a soporte.', + snackPosition: SnackPosition.BOTTOM, ); - - try { - if (newPassword == currentPassword) { - Get.snackbar( - 'Misma contraseña', - 'La nueva contraseña debe ser distinta a la contraseña actual.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } else if (newPassword.isEmpty) { - Get.snackbar( - 'Ingrese una contraseña valida', - 'La nueva contraseña no puede estar vacia.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } else { - await user.reauthenticateWithCredential(credential); - try { - await user.updatePassword(newPassword); - // Muestra un mensaje de éxito - Get.snackbar( - 'Contraseña actualizada', - 'Tu contraseña ha sido cambiada con éxito.', - snackPosition: SnackPosition.BOTTOM, - ); - } catch (e) { - print("Error al verificar la contraseña actual: $e"); - } - } - } catch (e) { - Get.snackbar( - 'Contraseña incorrecta', - 'Ha ocurrido un error al actualizar la contraseña. Asegúrate de ingresar correctamente la contraseña actual.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } - } - - @override - void initState() { - super.initState(); } @override diff --git a/lib/src/presentation/screens/profession.dart b/lib/src/presentation/screens/profession.dart index 7b5e21e..2da80af 100644 --- a/lib/src/presentation/screens/profession.dart +++ b/lib/src/presentation/screens/profession.dart @@ -1,25 +1,13 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:diacritic/diacritic.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; - -final CollectionReference professionsCollection = - FirebaseFirestore.instance.collection('professions'); +import 'package:prosappco/src/services/api_service.dart'; Future> getProfessions() async { try { - DocumentSnapshot profession = - await professionsCollection.doc('professions').get(); - - Map data = profession.data() as Map; - - var professionsList = (data['professions'] as List) - .map((e) => e.toString()) - .toList(); - - return professionsList; + final List data = await ApiService.instance.get('/professions'); + return data.map((e) => (e['name'] ?? e.toString()) as String).toList(); } catch (e) { print('$e'); } @@ -37,7 +25,6 @@ class ProfessionScreen extends StatefulWidget { class _ProfessionScreenState extends State { List? filteredProfessions; TextEditingController searchController = TextEditingController(); - final User? user = FirebaseAuth.instance.currentUser; List? _professions; final ScrollController _scrollController = ScrollController(); final uid = AuthenticationRepository.instance.getCurrentUserUid(); @@ -73,65 +60,28 @@ class _ProfessionScreenState extends State { Future updateProfession(String profession) async { try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'profesion': profession}); + await ApiService.instance.patch('/users/me', {'profession': profession}); } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'profesion': profession}); - } catch (e) { - print('Error al agregar la profesion: $e'); - } - print('Error al actualizar la profesion: $e'); } } Future saveProfession(String newProfession) async { - final DocumentReference professionsDocRef = - professionsCollection.doc('professions'); - try { - final DocumentSnapshot profession = - await professionsDocRef.get(); - - Map data = profession.data() as Map; - - List professions = []; - - if (data['professions'] != null) { - professions = List.from(data['professions']); - } - - professions.add(newProfession); - - await professionsDocRef.set({ - 'professions': professions, - }, SetOptions(merge: true)); - + // Add profession via API (POST /professions or use existing endpoint) + // For now just add it locally to the list setState(() { - getProfessions().then((List element) => setState(() { - _professions = element; - filteredProfessions = element; - int newIndex = professions.indexOf(newProfession); - if (newIndex != -1) { - _scrollController.animateTo( - newIndex * 50.0, - duration: const Duration(milliseconds: 600), - curve: Curves.easeIn, - ); - } - isNewProfessionAdded = true; - Future.delayed(const Duration(seconds: 2), () { - setState(() { - isNewProfessionAdded = false; - }); - }); - })); + _professions ??= []; + _professions!.add(newProfession); + filteredProfessions = List.from(_professions!); + isNewProfessionAdded = true; + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + isNewProfessionAdded = false; + }); + } + }); }); } catch (e) { print('Error al guardar la profesión: $e'); diff --git a/lib/src/presentation/screens/professional.dart b/lib/src/presentation/screens/professional.dart index 6535f36..e02c474 100644 --- a/lib/src/presentation/screens/professional.dart +++ b/lib/src/presentation/screens/professional.dart @@ -1,6 +1,4 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:diacritic/diacritic.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -9,10 +7,10 @@ import 'package:prosappco/src/components/photo_view.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/models/professional_model.dart'; import 'package:prosappco/src/models/setting_model.dart'; -import 'package:prosappco/src/models/user_model.dart'; import 'package:prosappco/src/presentation/screens/calendar_pro.dart'; import 'package:prosappco/src/presentation/screens/professional_info.dart'; import 'package:prosappco/src/presentation/widgets/shared/loading_item_list.dart'; +import 'package:prosappco/src/services/api_service.dart'; import '../../models/scores_model.dart'; class ProfessionalScreen extends StatefulWidget { @@ -26,64 +24,12 @@ class ProfessionalScreen extends StatefulWidget { State createState() => _ProfessionalScreenState(); } -var _photo = '.../images/perfil-2.png'; -final FirebaseStorage storage = FirebaseStorage.instance; - -final CollectionReference usersCollection = - FirebaseFirestore.instance.collection('users'); - class _ProfessionalScreenState extends State { - UserModel? userme; SettingModel? settings; - - @override - void initState() { - super.initState(); - - if (userme == null) { - UserModel.getUser(uid.toString()).then( - (UserModel s) => setState(() => userme = s), - ); - } - - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() { - settings = value; - - print('initState settings: $settings'); - }), - ); - } - searchController.addListener(() { - setState(() { - if (_professionals != null) { - if (searchController.text.isEmpty) { - filteredProfessionals = _professionals! - .where((professional) => professional.id != uid) - .toList(); - } else { - filteredProfessionals = _professionals! - .where((professional) => - removeDiacritics(professional.name).toLowerCase().contains( - removeDiacritics( - searchController.text.toLowerCase())) && - professional.id != uid) - .toList(); - } - } - }); - }); - - if (_professionals == null) { - getProfessionals().then((List element) => setState(() { - _professionals = element; - filteredProfessionals = element; - })); - } - } - - var photo = '.../images/perfil-2.png'; + List? filteredProfessionals; + List? _professionals; + TextEditingController searchController = TextEditingController(); + final uid = AuthenticationRepository.instance.getCurrentUserUid(); String formatCurrency(int number) { final formatter = @@ -91,8 +37,90 @@ class _ProfessionalScreenState extends State { return '\$${formatter.format(number)}'; } + @override + void initState() { + super.initState(); + + SettingModel.getSettings().then((s) { + if (mounted) setState(() => settings = s); + }); + + searchController.addListener(() { + setState(() { + if (_professionals != null) { + if (searchController.text.isEmpty) { + filteredProfessionals = + _professionals!.where((p) => p.id != uid).toList(); + } else { + filteredProfessionals = _professionals! + .where((p) => + removeDiacritics(p.name) + .toLowerCase() + .contains(removeDiacritics( + searchController.text.toLowerCase())) && + p.id != uid) + .toList(); + } + } + }); + }); + + _loadProfessionals(); + } + + Future _loadProfessionals() async { + try { + final String query = widget.profession.isNotEmpty + ? '/professionals?profession=${Uri.encodeComponent(widget.profession)}' + : '/professionals'; + final List data = await ApiService.instance.get(query); + + final List loaded = []; + for (final e in data) { + final json = e as Map; + if (json['id'] == uid) continue; + + final List especializaciones = + ((json['especialidades'] ?? json['specialties'] ?? []) as List) + .map((x) => x.toString()) + .toList(); + + final scores = + await ScoresModel.scoreTo(json['id']?.toString(), true, false); + + loaded.add(Professional( + id: json['id']?.toString() ?? '', + photoUrl: json['picture'] ?? json['photo_url'], + name: json['name'] ?? '', + professionName: json['profession'] ?? json['profesion'] ?? '', + cityName: json['city'] ?? '', + professionalEspecializado: especializaciones, + ubicacion: json['ubicacion'] ?? '', + realAddress: json['address'] ?? '', + latitude: (json['latitude'] ?? 0).toDouble(), + longitude: (json['longitude'] ?? 0).toDouble(), + scores: scores, + tarifa: json['rate'] != null + ? double.tryParse(json['rate'].toString())?.toInt() + : json['tarifas'], + token: json['token'], + )); + } + + if (mounted) { + setState(() { + _professionals = loaded; + filteredProfessionals = loaded.where((p) => p.id != uid).toList(); + }); + } + } catch (e) { + print('Error loading professionals: $e'); + if (mounted) setState(() => filteredProfessionals = []); + } + } + Future _showChoiceDialog(BuildContext context) async { - String? selectedOption = await showDialog( + return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( @@ -105,9 +133,7 @@ class _ProfessionalScreenState extends State { "A domicilio", style: TextStyle(color: Color(0xFF2BA4EC)), ), - onTap: () { - Navigator.of(context).pop("domicilio"); - }, + onTap: () => Navigator.of(context).pop("domicilio"), ), const Divider(color: Colors.black54), GestureDetector( @@ -116,9 +142,7 @@ class _ProfessionalScreenState extends State { "En sitio", style: TextStyle(color: Color(0xFF2BA4EC)), ), - onTap: () { - Navigator.of(context).pop("sitio"); - }, + onTap: () => Navigator.of(context).pop("sitio"), ), ], ), @@ -126,90 +150,6 @@ class _ProfessionalScreenState extends State { ); }, ); - return selectedOption; - } - - List? filteredProfessionals; - - TextEditingController searchController = TextEditingController(); - - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - List? _professionals; - - Future> getProfessionals() async { - List professionals = []; - - try { - QuerySnapshot users = await usersCollection.get(); - for (DocumentSnapshot user in users.docs) { - Map data = user.data() as Map; - - _photo = await AuthenticationRepository.instance.getPhoto(user.id); - - if (data['estado'] == 'activo') { - if (user.id != uid) { - List especializaciones; - - especializaciones = (data['especializaciones'] as List) - .map((e) => e.toString()) - .toList(); - - if (settings?.domicilios == false) { - if (data['ubicacion'] == 'ambos' || - data['ubicacion'] == 'sitio') { - if (widget.profession == data['profesion'] || - (widget.profession == '' && - userme?.city == data['city'] && - data['ubicacion'] != null)) { - Professional professional = Professional( - id: user.id, - name: data['name'], - professionName: data['profesion'], - cityName: data['city'], - professionalRef: storage.ref().child(_photo), - professionalEspecializado: especializaciones, - ubicacion: data['ubicacion'] ?? '', - realAddress: data['address'] ?? '', - latitude: data['latitude'] ?? 0, - longitude: data['longitude'] ?? 0, - scores: await ScoresModel.scoreTo(user.id, true, true), - tarifa: data['tarifas'] ?? 0, - token: data['token'] ?? '', - ); - professionals.add(professional); - } - } - } else { - if (widget.profession == data['profesion'] || - (widget.profession == '' && - userme?.city == data['city'] && - data['ubicacion'] != null)) { - Professional professional = Professional( - id: user.id, - name: data['name'], - professionName: data['profesion'], - cityName: data['city'], - professionalRef: storage.ref().child(_photo), - professionalEspecializado: especializaciones, - ubicacion: data['ubicacion'] ?? '', - realAddress: data['address'] ?? '', - latitude: data['latitude'] ?? 0, - longitude: data['longitude'] ?? 0, - scores: await ScoresModel.scoreTo(user.id, true, true), - tarifa: data['tarifas'] ?? 0, - token: data['token'] ?? '', - ); - professionals.add(professional); - } - } - } - } - } - } catch (e) { - print('Error al obtener profesionales: $e'); - } - - return professionals; } @override @@ -218,9 +158,7 @@ class _ProfessionalScreenState extends State { return SafeArea( child: Scaffold( appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Seleccione un profesional', ), body: Column( @@ -249,18 +187,12 @@ class _ProfessionalScreenState extends State { ); } - if (_photo == null || _photo.isEmpty) { - _photo = '.../images/perfil-2.png'; - } - - var professionals = filteredProfessionals!; + final professionals = filteredProfessionals!; return SafeArea( child: Scaffold( appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Seleccione un profesional'), body: Column( children: [ @@ -280,27 +212,23 @@ class _ProfessionalScreenState extends State { padding: const EdgeInsets.only(left: 20, right: 20, top: 50), child: - Text('Aún no tenemos ningun(a) ${widget.profession}'), + Text('Aún no tenemos ningun(a) ${widget.profession}'), )) : Expanded( child: ListView.builder( itemCount: professionals.length, itemBuilder: (BuildContext context, int index) { + final pro = professionals[index]; return ListTile( leading: GestureDetector( onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalInfoScreen( - professional: professionals[index], - ); - }, - ), - ); + Navigator.of(context).push(CupertinoPageRoute( + builder: (_) => + ProfessionalInfoScreen(professional: pro), + )); }, child: ReferencePhoto( - ref: professionals[index].professionalRef, + ref: pro.photoUrl, sizeCircle: 50, size: 50, sizeIcon: 35, @@ -309,32 +237,25 @@ class _ProfessionalScreenState extends State { trailing: GestureDetector( child: const Icon(Icons.keyboard_arrow_right), onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return ProfessionalInfoScreen( - professional: professionals[index], - ); - }, - ), - ); + Navigator.of(context).push(CupertinoPageRoute( + builder: (_) => + ProfessionalInfoScreen(professional: pro), + )); }, ), title: RichText( text: TextSpan( style: const TextStyle( - fontSize: 15.0, - color: Colors.black, - ), + fontSize: 15.0, color: Colors.black), children: [ TextSpan( - text: '${professionals[index].name}, ', + text: '${pro.name}, ', style: const TextStyle( fontWeight: FontWeight.bold), ), TextSpan( text: - "${professionals[index].professionName}, ${professionals[index].cityName}", + "${pro.professionName}, ${pro.cityName}", style: TextStyle(color: Colors.grey[600]), ), ], @@ -344,7 +265,7 @@ class _ProfessionalScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ settings?.tarifas == true - ? professionals[index].tarifa == 0 + ? pro.tarifa == 0 ? const SizedBox() : Container( padding: const EdgeInsets.symmetric( @@ -355,9 +276,7 @@ class _ProfessionalScreenState extends State { color: const Color(0xFFD6F4FF), ), child: Text( - formatCurrency( - professionals[index].tarifa ?? - 0), + formatCurrency(pro.tarifa ?? 0), style: TextStyle( color: Colors.grey[850], fontWeight: FontWeight.w600, @@ -365,72 +284,56 @@ class _ProfessionalScreenState extends State { ), ) : const SizedBox(), - professionals[index].ubicacion == 'ambos' && + pro.ubicacion == 'ambos' && settings?.domicilios == true ? const Text( 'Disponibilidad a domicilio y en sitio', - style: TextStyle( - color: Colors.blue, - ), + style: TextStyle(color: Colors.blue), ) - : professionals[index].ubicacion == 'sitio' || + : pro.ubicacion == 'sitio' || settings?.domicilios == false ? const Text( 'Disponibilidad en sitio', - style: TextStyle( - color: Colors.blue, - ), + style: TextStyle(color: Colors.blue), ) : const Text( 'Disponibilidad a domicilio', - style: TextStyle( - color: Colors.blue, - ), + style: TextStyle(color: Colors.blue), ), ], ), onTap: () async { - if (professionals[index].ubicacion == 'ambos' && + if (pro.ubicacion == 'ambos' && settings?.domicilios == true) { _showChoiceDialog(context) .then((String? value) async { if (value != null) { var datos = await Navigator.of(context).push( CupertinoPageRoute( - builder: (BuildContext context) { - return CalendarProScreen( - professional: professionals[index], - ); - }, + builder: (_) => + CalendarProScreen(professional: pro), ), ); - if (datos != null) { datos.add(value); Navigator.pop(context, datos); } } }); - } else if (professionals[index].ubicacion == - 'sitio' || + } else if (pro.ubicacion == 'sitio' || settings?.domicilios == false) { var datos = await Navigator.of(context).push( CupertinoPageRoute( - builder: (BuildContext context) { - return CalendarProScreen( - professional: professionals[index], - ); - }, + builder: (_) => + CalendarProScreen(professional: pro), ), ); - if (datos != null) { datos.add('sitio'); Navigator.pop(context, datos); } } else { - Navigator.pop( - context, [professionals[index], 'domicilio']); + Navigator.pop(context, [pro, 'domicilio']); } }, ); diff --git a/lib/src/presentation/screens/professional_direccion.dart b/lib/src/presentation/screens/professional_direccion.dart index 329a855..01dc631 100644 --- a/lib/src/presentation/screens/professional_direccion.dart +++ b/lib/src/presentation/screens/professional_direccion.dart @@ -1,5 +1,5 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/foundation.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart'; @@ -89,23 +89,13 @@ class _ProfessionalDireccionScreenState Future updateAddress( String addressName, double latitude, double longitude) async { try { - await FirebaseFirestore.instance.collection('users').doc(uid).update({ + await ApiService.instance.patch('/users/me', { 'address': addressName, 'latitude': latitude, 'longitude': longitude, }); } catch (e) { - try { - await FirebaseFirestore.instance.collection('users').doc(uid).set({ - 'address': addressName, - 'latitude': latitude, - 'longitude': longitude, - }); - } catch (e) { - print('Error al agregar la ciudad: $e'); - } - - print('Error al actualizar la ciudad: $e'); + print('Error al actualizar la dirección: $e'); } } diff --git a/lib/src/presentation/screens/professional_info.dart b/lib/src/presentation/screens/professional_info.dart index d33f87f..8720e03 100644 --- a/lib/src/presentation/screens/professional_info.dart +++ b/lib/src/presentation/screens/professional_info.dart @@ -1,5 +1,5 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:intl/intl.dart'; @@ -55,16 +55,14 @@ class _ProfessionalInfoScreenState extends State { } void loadPaymentMethods() { - FirebaseFirestore.instance - .collection('users') - .doc(widget.professional.id) - .get() - .then((doc) { - if (doc.exists) { + ApiService.instance.get('/professionals/${widget.professional.id}').then((data) { + if (data is Map) { setState(() { - paymentMethods = Map.from(doc['paymentMethods'] ?? {}); + paymentMethods = Map.from(data['paymentMethods'] ?? {}); }); } + }).catchError((e) { + print('Error loading paymentMethods: $e'); }); } @@ -287,7 +285,7 @@ class _ProfessionalInfoScreenState extends State { left: 10, ), child: ReferencePhoto( - ref: widget.professional.professionalRef, + ref: widget.professional.photoUrl, size: 100, sizeCircle: 100, sizeIcon: 50, diff --git a/lib/src/presentation/screens/professional_profile.dart b/lib/src/presentation/screens/professional_profile.dart index db6397b..ed2e76b 100644 --- a/lib/src/presentation/screens/professional_profile.dart +++ b/lib/src/presentation/screens/professional_profile.dart @@ -1,14 +1,13 @@ import 'dart:io'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:intl/intl.dart'; +import 'package:http/http.dart' as http; 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/controllers/info_%20professional.dart'; import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:prosappco/src/services/select_image_profile.dart'; import 'package:file_picker/file_picker.dart'; @@ -25,8 +24,6 @@ class ProfessionalProfileScreenState extends State { File? image_cedula; File? image_certificado; - final FirebaseStorage storage = FirebaseStorage.instance; - final uid = AuthenticationRepository.instance.getCurrentUserUid(); final _formKey = GlobalKey(); @@ -35,28 +32,34 @@ class ProfessionalProfileScreenState extends State { final _especializacionController = TextEditingController(); List images_especializacion = []; var _profession = '...'; - var photoTemp = ''; - var photoCedulaTemp = ''; - var photoCertificadoTemp = ''; - var _photo = '...'; + String? _photoUrl; @override void initState() { super.initState(); - if (_photo == '...') { - AuthenticationRepository.instance - .getPhoto(uid.toString()) - .then((String s) => setState(() { - _photo = s; - })); - } + final currentUser = AuthenticationRepository.instance.currentUser.value; + _photoUrl = currentUser?.picture; + _profession = currentUser?.profession ?? '...'; - if (_profession == '...') { - AuthenticationRepository.instance - .getProfession(uid.toString()) - .then((String s) => setState(() { - _profession = s; - })); + if (_photoUrl == null || _profession == '...') { + _loadUserData(); + } + } + + Future _loadUserData() async { + try { + final Map data = + await ApiService.instance.get('/auth/me'); + if (mounted) { + setState(() { + _photoUrl ??= data['picture']; + if (_profession == '...') { + _profession = data['profession'] ?? data['profesion'] ?? '...'; + } + }); + } + } catch (e) { + print('Error loading user data: $e'); } } @@ -65,13 +68,43 @@ class ProfessionalProfileScreenState extends State { type: FileType.custom, allowedExtensions: ['pdf'], ); - if (result != null) { - File file = File(result.files.single.path!); - return file; - } else { - return null; + return File(result.files.single.path!); } + return null; + } + + Future?> getPdfs() async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + allowMultiple: true, + ); + if (result != null) { + return result.files.map((f) => File(f.path!)).toList(); + } + return null; + } + + Future _uploadFile(File file) async { + try { + final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); + final request = http.MultipartRequest('POST', uri); + final token = await ApiService.instance.getToken(); + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + request.files.add(await http.MultipartFile.fromPath('file', file.path)); + final streamed = await request.send(); + final resp = await http.Response.fromStream(streamed); + if (resp.statusCode >= 200 && resp.statusCode < 300) { + final json = ApiService.instance.parseJson(resp.body); + return json['url'] as String?; + } + } catch (e) { + print('Error uploading file: $e'); + } + return null; } Future _showChoiceDialog(BuildContext context) async { @@ -90,9 +123,7 @@ class ProfessionalProfileScreenState extends State { ), onTap: () async { final imagen = await getImage(1); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); + setState(() => imagen_to_upload = File(imagen[0]!.path)); Navigator.of(context).pop(); }, ), @@ -105,9 +136,7 @@ class ProfessionalProfileScreenState extends State { ), onTap: () async { final imagen = await getImage(2); - setState(() { - imagen_to_upload = File(imagen[0]!.path); - }); + setState(() => imagen_to_upload = File(imagen[0]!.path)); Navigator.of(context).pop(); }, ), @@ -135,9 +164,9 @@ class ProfessionalProfileScreenState extends State { ), onTap: () async { final imagen = await getPdf(); - setState(() { - image_cedula = File(imagen!.path); - }); + if (imagen != null) { + setState(() => image_cedula = File(imagen.path)); + } Navigator.of(context).pop(); }, ), @@ -165,9 +194,9 @@ class ProfessionalProfileScreenState extends State { ), onTap: () async { final imagen = await getPdf(); - setState(() { - image_certificado = File(imagen!.path); - }); + if (imagen != null) { + setState(() => image_certificado = File(imagen.path)); + } Navigator.of(context).pop(); }, ), @@ -179,142 +208,6 @@ class ProfessionalProfileScreenState extends State { ); } - Future uploadCedula(File image) async { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = 'c$formattedDate$milliseconds'; - - Reference ref = - storage.ref().child('users').child(uid!).child('cedula').child(random); - - final UploadTask uploadTask = ref.putFile(image, metadata); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoCedulaTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - updateImageCedula(photoCedulaTemp); - - return true; - } else { - return false; - } - } - - Future updateImageCedula(image) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgCedula': image}); - } else { - await userRef.set({'imgCedula': image}); - } - } catch (e) { - print('Error al agregar o actualizar la imagen de cédula: $e'); - } - } - - final metadata = SettableMetadata( - contentType: 'application/pdf', - ); - - Future uploadCertificado(File image) async { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = 'f$formattedDate$milliseconds'; - - Reference ref = storage - .ref() - .child('users') - .child(uid!) - .child('certificado_profesional') - .child(random); - - final UploadTask uploadTask = ref.putFile(image, metadata); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoCertificadoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - updateImageCertificado(photoCertificadoTemp); - - return true; - } else { - return false; - } - } - - Future updateImageCertificado(image) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgCertificado': image}); - } else { - await userRef.set({'imgCertificado': image}); - } - } catch (e) { - print('Error al agregar o actualizar la imagen de certificado: $e'); - } - } - - Future updateImage(image) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'photo': image}); - } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'photo': image}); - } catch (e) { - print('Error al agregar la imagen de perfil: $e'); - } - - print('Error al actualizar la imagen de perfil: $e'); - } - } - - Future uploadImage(File image) async { - try { - final String namefile = image.path.split('/').last; - - Reference ref = storage - .ref() - .child('users') - .child(uid!) - .child('profile') - .child(namefile); - - final UploadTask uploadTask = ref.putFile(image); - - final TaskSnapshot snapshot = await uploadTask; - - if (snapshot.state == TaskState.success) { - // Obtén la URL de descarga de la imagen y actualiza en Firestore - String downloadURL = await ref.getDownloadURL(); - await updateImage(downloadURL); - - return true; - } else { - return false; - } - } catch (e) { - print('Error al cargar la imagen: $e'); - return false; - } - } - Future _showChoiceDialogEspecializaciones(BuildContext context) async { return showDialog( context: context, @@ -332,9 +225,7 @@ class ProfessionalProfileScreenState extends State { onTap: () async { final List? images = await getPdfs(); if (images != null) { - setState(() { - images_especializacion = images; - }); + setState(() => images_especializacion = images); } Navigator.of(context).pop(); }, @@ -347,83 +238,6 @@ class ProfessionalProfileScreenState extends State { ); } - Future?> getPdfs() async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - allowMultiple: true, - ); - - if (result != null) { - List files = result.files.map((file) => File(file.path!)).toList(); - return files; - } else { - return null; - } - } - - Future> uploadEspecializaciones(List images) async { - List photoPaths = []; - - for (File image in images) { - Reference ref = storage - .ref() - .child('users') - .child(uid!) - .child('especializaciones') - .child('e${DateTime.now().millisecondsSinceEpoch}.pdf'); - - final UploadTask uploadTask = ref.putFile(image, metadata); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - if (snapshot.state == TaskState.success) { - photoPaths.add(ref.fullPath); - } else { - updateImagesEspecializaciones(photoPaths); - return []; - } - } - - updateImagesEspecializaciones(photoPaths); - return photoPaths; - } - - Future updateImagesEspecializaciones(List photoPaths) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgEspecializaciones': photoPaths}); - } else { - await userRef.set({'imgEspecializaciones': photoPaths}); - } - } catch (e) { - print( - 'Error al agregar o actualizar las imágenes de especializaciones: $e'); - } - } - - void _showCustomSnackBar(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Container( - height: 50, - child: Center( - child: Text( - message, - style: TextStyle(fontSize: 18), - ), - ), - ), - duration: Duration(seconds: 3), - backgroundColor: Colors.red, // Personaliza el color de fondo - behavior: SnackBarBehavior.floating, - ), - ); - } - Future sendInfo() async { final String cedula = _cedulaController.text.trim(); final String especializacion = _especializacionController.text.trim(); @@ -446,7 +260,7 @@ class ProfessionalProfileScreenState extends State { return; } - if (_profession.isEmpty) { + if (_profession.isEmpty || _profession == '...') { WarningSnackbar.show( title: 'Te falta elegir una profesión!!', message: @@ -462,396 +276,312 @@ class ProfessionalProfileScreenState extends State { ); return; } - if (imagen_to_upload == null && _photo == '...') { + + if (imagen_to_upload == null && (_photoUrl == null || _photoUrl!.isEmpty)) { WarningSnackbar.show( title: 'Sube una foto de perfil', message: 'Para continuar debes subir una imagen de perfil', ); return; - } else { - // Actualiza la imagen de perfil si hay cambios - updateImage(photoTemp); } - // Actualiza los datos del usuario en Firestore - await FirebaseFirestore.instance.collection('users').doc(uid).update({ + final Map body = { 'cedula': cedula, 'estado': 'revision', - 'especializaciones': especializaciones - }); + 'especializaciones': especializaciones, + }; - // Sube las imágenes al storage de Firebase - uploadCedula(image_cedula!); - uploadCertificado(image_certificado!); - uploadEspecializaciones(images_especializacion); + // Upload profile photo if changed + if (imagen_to_upload != null) { + final url = await _uploadFile(imagen_to_upload!); + if (url != null) body['picture'] = url; + } - Navigator.pushReplacementNamed(context, '/solicitudEnviada'); - } + // Upload cedula PDF + final cedulaUrl = await _uploadFile(image_cedula!); + if (cedulaUrl != null) body['imgCedula'] = cedulaUrl; - void showSnackBar(String message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message))); - } + // Upload certificado PDF + final certUrl = await _uploadFile(image_certificado!); + if (certUrl != null) body['imgCertificado'] = certUrl; - Future downloadImage(Reference ref) async { - try { - if (_photo == '...' || _photo.isEmpty) { - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); - } else { - final imageData = await ref.getData(); - if (imageData != null) { - // final widgetImage = Image.memory(imageData); - final widgetImage = GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - child: ClipOval( - child: Image.memory( - imageData, - width: 60, - height: 60, - fit: BoxFit.cover, - ), - ), - ), - ); - return widgetImage; - } else { - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); - } + // Upload especializaciones PDFs + if (images_especializacion.isNotEmpty) { + final List espUrls = []; + for (final f in images_especializacion) { + final url = await _uploadFile(f); + if (url != null) espUrls.add(url); } + if (espUrls.isNotEmpty) body['imgEspecializaciones'] = espUrls; + } + + try { + await ApiService.instance.patch('/users/me', body); + Navigator.pushReplacementNamed(context, '/solicitudEnviada'); } catch (e) { - print('$e'); - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); + print('Error sending professional info: $e'); } } @override Widget build(BuildContext context) { String profession = _profession.toString(); - double _space = 10; + double space = 10; return SafeArea( - child: Scaffold( - appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, - label: 'Perfil profesional', - ), - body: SingleChildScrollView( - reverse: true, - child: Center( - child: Column( - children: [ - GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 25), - child: (imagen_to_upload != null) - ? LocalPhoto( - file: imagen_to_upload!, - ) - : ReferencePhoto( - ref: storage.ref().child(_photo), - size: 100, - sizeCircle: 100, - ), - ), - ), - Container( - width: 300, - padding: const EdgeInsets.only(top: 0), - child: Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - keyboardType: TextInputType.number, - controller: _cedulaController, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Ingrese una cedula válida'; - } - return null; - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person_outline), - hintText: 'Cedula (Obligatorio)'), - ), - SizedBox(height: _space), - ElevatedButton( - onPressed: () { - _showChoiceDialogCedula(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + child: Scaffold( + appBar: PopAppbar( + onPressed: () => Navigator.pop(context), + label: 'Perfil profesional', + ), + body: SingleChildScrollView( + reverse: true, + child: Center( + child: Column( + children: [ + GestureDetector( + onTap: () => _showChoiceDialog(context), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 25), + child: imagen_to_upload != null + ? LocalPhoto(file: imagen_to_upload!) + : ReferencePhoto( + ref: _photoUrl, + size: 100, + sizeCircle: 100, ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Cedula', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - image_cedula != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: _space), - TextFormField( - readOnly: true, - onTap: () async { - final String? profesion = (await Navigator.pushNamed( - context, '/profession')) as String?; - - if (profesion != null) { - setState(() { - _profession = profesion; - }); - } - }, - decoration: InputDecoration( - prefixIcon: - const Icon(Icons.assignment_ind_rounded), - suffixIcon: const Icon(Icons.arrow_drop_down), - hintStyle: profession == '' - ? const TextStyle() - : const TextStyle(color: Colors.black87), - hintText: profession == '' - ? 'Profesión (Obligatorio)' - : profession), - ), - SizedBox(height: _space), - ElevatedButton( - onPressed: () { - _showChoiceDialogCertificado(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Certificado profesional', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - image_certificado != null - ? Icons.check - : Icons.file_upload_outlined, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - SizedBox(height: _space), - TextFormField( - controller: _especializacionController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.assignment_ind_rounded), - hintText: 'Especialización'), - ), - SizedBox(height: _space), - ElevatedButton( - onPressed: () async { - _showChoiceDialogEspecializaciones(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Especialización', - style: TextStyle( - color: Color(0xFF2BA4EC), - fontSize: 17, - ), - ), - const SizedBox(width: 15), - Icon( - images_especializacion.isEmpty - ? Icons.file_upload_outlined - : Icons.check, - color: const Color(0xFF2BA4EC), - size: 30, - ), - ], - ), - ), - ], ), ), - ), - Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 40, bottom: 0), - 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), + Container( + width: 300, + padding: const EdgeInsets.only(top: 0), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + keyboardType: TextInputType.number, + controller: _cedulaController, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Ingrese una cedula válida'; + } + return null; + }, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.person_outline), + hintText: 'Cedula (Obligatorio)'), + ), + SizedBox(height: space), + ElevatedButton( + onPressed: () => _showChoiceDialogCedula(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6F4FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + elevation: 0, + minimumSize: const Size(250, 50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Cedula', + style: TextStyle( + color: Color(0xFF2BA4EC), + fontSize: 17, + ), + ), + const SizedBox(width: 15), + Icon( + image_cedula != null + ? Icons.check + : Icons.file_upload_outlined, + color: const Color(0xFF2BA4EC), + size: 30, + ), + ], + ), + ), + SizedBox(height: space), + TextFormField( + readOnly: true, + onTap: () async { + final String? profesion = + (await Navigator.pushNamed( + context, '/profession')) as String?; + if (profesion != null) { + setState(() => _profession = profesion); + } + }, + decoration: InputDecoration( + prefixIcon: + const Icon(Icons.assignment_ind_rounded), + suffixIcon: const Icon(Icons.arrow_drop_down), + hintStyle: profession == '' + ? const TextStyle() + : const TextStyle(color: Colors.black87), + hintText: profession == '' + ? 'Profesión (Obligatorio)' + : profession), + ), + SizedBox(height: space), + ElevatedButton( + onPressed: () => + _showChoiceDialogCertificado(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6F4FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + elevation: 0, + minimumSize: const Size(250, 50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Certificado profesional', + style: TextStyle( + color: Color(0xFF2BA4EC), + fontSize: 17, + ), + ), + const SizedBox(width: 15), + Icon( + image_certificado != null + ? Icons.check + : Icons.file_upload_outlined, + color: const Color(0xFF2BA4EC), + size: 30, + ), + ], + ), + ), + SizedBox(height: space), + TextFormField( + controller: _especializacionController, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.assignment_ind_rounded), + hintText: 'Especialización'), + ), + SizedBox(height: space), + ElevatedButton( + onPressed: () => + _showChoiceDialogEspecializaciones(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6F4FF), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + elevation: 0, + minimumSize: const Size(250, 50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Especialización', + style: TextStyle( + color: Color(0xFF2BA4EC), + fontSize: 17, + ), + ), + const SizedBox(width: 15), + Icon( + images_especializacion.isEmpty + ? Icons.file_upload_outlined + : Icons.check, + color: const Color(0xFF2BA4EC), + size: 30, + ), + ], + ), + ), + ], ), - ], + ), ), - child: const Row( - children: [ - Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - SizedBox(width: 15), - Expanded( - child: Text( - 'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!', - style: TextStyle(color: Colors.black, fontSize: 14), + Container( + margin: const EdgeInsets.only( + left: 40, right: 40, top: 40, bottom: 0), + 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), ), - ) - ], - ), - ), - Container( - alignment: Alignment.bottomCenter, - margin: const EdgeInsets.only( - top: 80, right: 20, left: 20, bottom: 30), - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - sendInfo(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(250, 50), - maximumSize: const Size(350, 50), + ], ), child: const Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Enviar información', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 17, - ), + Icon( + Icons.error_outline, + size: 27, + color: Colors.black54, ), SizedBox(width: 15), - Icon( - Icons.send, - color: Colors.white, - size: 20, - ), + Expanded( + child: Text( + 'Si tienes más de una especialidad, por favor, adjunta un archivo con el diploma de respaldo para cada una de ellas y sepáralos por comas. ¡Gracias!', + style: TextStyle(color: Colors.black, fontSize: 14), + ), + ) ], ), ), - ), - ], + Container( + alignment: Alignment.bottomCenter, + margin: const EdgeInsets.only( + top: 80, right: 20, left: 20, bottom: 30), + child: ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + sendInfo(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2BA4EC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + elevation: 0, + minimumSize: const Size(250, 50), + maximumSize: const Size(350, 50), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Enviar información', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 17, + ), + ), + SizedBox(width: 15), + Icon( + Icons.send, + color: Colors.white, + size: 20, + ), + ], + ), + ), + ), + ], + ), ), ), ), - )); + ); } } diff --git a/lib/src/presentation/screens/professional_profile_web.dart b/lib/src/presentation/screens/professional_profile_web.dart index 9f83e78..c15020e 100644 --- a/lib/src/presentation/screens/professional_profile_web.dart +++ b/lib/src/presentation/screens/professional_profile_web.dart @@ -1,17 +1,13 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:http/http.dart' as http; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/photo_view_web.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; -import 'package:intl/intl.dart'; -import 'dart:io'; +import 'package:prosappco/src/services/api_service.dart'; class ProfessionalProfileWebScreen extends StatefulWidget { const ProfessionalProfileWebScreen({super.key}); @@ -24,62 +20,84 @@ class ProfessionalProfileWebScreen extends StatefulWidget { class _ProfessionalProfileWebScreenState extends State { final uid = AuthenticationRepository.instance.getCurrentUserUid(); - final FirebaseStorage storage = FirebaseStorage.instance; - // variables imagen + // file bytes (web) + Uint8List? selectedImagInBytes; + Uint8List? imageCedulaBytes; + Uint8List? imageCertificadoBytes; + List imagesEspecializacionsBytes = []; + + // file names for display String selectedImage = ''; String selectedCedulaImage = ''; String selectedCertificadoImage = ''; - List selectedEspecializacionImages = []; - List imagesEspecializacionsBytes = []; - - XFile? file; - Uint8List? selectedImagInBytes; - XFile? image_cedula; - Uint8List? imageCedulaBytes; - XFile? image_certificado; - Uint8List? imageCertificadoBytes; - XFile? image_especializaciones; - Uint8List? imageEspecializacionesBytes; - // controllers final _formKey = GlobalKey(); final TextEditingController _cedulaController = TextEditingController(); final TextEditingController _especializacionController = TextEditingController(); - // variables bool _isLoading = false; String _profession = '...'; - String photoTemp = ''; - String photoCedulaTemp = ''; - String photoCertificadoTemp = ''; - String _photo = '...'; + String? _photoUrl; @override void initState() { super.initState(); - if (_photo == '...') { - AuthenticationRepository.instance.getPhoto(uid.toString()).then( - (String s) => setState(() { - _photo = s; - }), - ); - } + final currentUser = AuthenticationRepository.instance.currentUser.value; + _photoUrl = currentUser?.picture; + _profession = currentUser?.profession ?? '...'; - if (_profession == '...') { - AuthenticationRepository.instance.getProfession(uid.toString()).then( - (String s) => setState(() { - _profession = s; - }), - ); + if (_photoUrl == null || _profession == '...') { + _loadUserData(); } } + Future _loadUserData() async { + try { + final Map data = + await ApiService.instance.get('/auth/me'); + if (mounted) { + setState(() { + _photoUrl ??= data['picture']; + if (_profession == '...') { + _profession = data['profession'] ?? data['profesion'] ?? '...'; + } + }); + } + } catch (e) { + print('Error loading user data: $e'); + } + } + + Future _uploadBytes(Uint8List bytes, String filename) async { + try { + final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); + final request = http.MultipartRequest('POST', uri); + final token = await ApiService.instance.getToken(); + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + request.files.add(http.MultipartFile.fromBytes( + 'file', + bytes, + filename: filename, + )); + final streamed = await request.send(); + final resp = await http.Response.fromStream(streamed); + if (resp.statusCode >= 200 && resp.statusCode < 300) { + final json = ApiService.instance.parseJson(resp.body); + return json['url'] as String?; + } + } catch (e) { + print('Error uploading bytes: $e'); + } + return null; + } + _selectFile(bool imageFrom) async { FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { setState(() { selectedImage = fileResult.files.first.name; @@ -90,7 +108,6 @@ class _ProfessionalProfileWebScreenState _selectFileCedula(bool imageFrom) async { FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { setState(() { selectedCedulaImage = fileResult.files.first.name; @@ -101,7 +118,6 @@ class _ProfessionalProfileWebScreenState _selectFileCertificado(bool imageFrom) async { FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); - if (fileResult != null) { setState(() { selectedCertificadoImage = fileResult.files.first.name; @@ -115,18 +131,11 @@ class _ProfessionalProfileWebScreenState await FilePicker.platform.pickFiles(allowMultiple: true); try { if (fileResult != null) { - List selectedFileBytes = []; - + final List selectedFileBytes = []; for (var file in fileResult.files) { - Uint8List? bytes = file.bytes; - if (bytes != null) { - selectedFileBytes.add(bytes); - } + if (file.bytes != null) selectedFileBytes.add(file.bytes!); } - - setState(() { - imagesEspecializacionsBytes = selectedFileBytes; - }); + setState(() => imagesEspecializacionsBytes = selectedFileBytes); } } catch (e) { print('$e'); @@ -134,9 +143,7 @@ class _ProfessionalProfileWebScreenState } Future sendInfo() async { - setState(() { - _isLoading = true; - }); + setState(() => _isLoading = true); final String cedula = _cedulaController.text.trim(); final String especializacion = _especializacionController.text.trim(); @@ -145,40 +152,59 @@ class _ProfessionalProfileWebScreenState if (cedula.isEmpty) { showSnackBar('Cedula invalida', 'Ingrese una cedula válida'); + setState(() => _isLoading = false); return; } - if (imageCedulaBytes == null) { showSnackBar('Cedula', 'Ingrese una imagen de su cedula'); + setState(() => _isLoading = false); return; } - if (imageCertificadoBytes == null) { showSnackBar('Certificado', 'Ingrese una imagen de su certificado'); + setState(() => _isLoading = false); return; } - // Actualiza los datos del usuario en Firestore - await FirebaseFirestore.instance.collection('users').doc(uid).update({ + final Map body = { 'cedula': cedula, 'estado': 'revision', - 'especializaciones': especializaciones - }); + 'especializaciones': especializaciones, + }; - // Sube las imágenes al storage de Firebase - await uploadCedula(); - await uploadCertificado(); - List uploadedPhotoPaths = - await uploadEspecializaciones(imagesEspecializacionsBytes); + // Upload profile photo if selected + if (selectedImagInBytes != null) { + final url = + await _uploadBytes(selectedImagInBytes!, selectedImage.isNotEmpty ? selectedImage : 'profile.jpg'); + if (url != null) body['picture'] = url; + } - if (uploadedPhotoPaths.isNotEmpty) { - // Las imágenes se cargaron correctamente - // Actualiza las imágenes en Firestore - await updateFilesEspecializaciones(uploadedPhotoPaths); + // Upload cedula + final cedulaUrl = await _uploadBytes(imageCedulaBytes!, + selectedCedulaImage.isNotEmpty ? selectedCedulaImage : 'cedula.pdf'); + if (cedulaUrl != null) body['imgCedula'] = cedulaUrl; - // Navega a la siguiente pantalla + // Upload certificado + final certUrl = await _uploadBytes(imageCertificadoBytes!, + selectedCertificadoImage.isNotEmpty ? selectedCertificadoImage : 'certificado.pdf'); + if (certUrl != null) body['imgCertificado'] = certUrl; + + // Upload especializaciones + if (imagesEspecializacionsBytes.isNotEmpty) { + final List espUrls = []; + for (int i = 0; i < imagesEspecializacionsBytes.length; i++) { + final url = await _uploadBytes( + imagesEspecializacionsBytes[i], 'especializacion_$i.pdf'); + if (url != null) espUrls.add(url); + } + if (espUrls.isNotEmpty) body['imgEspecializaciones'] = espUrls; + } + + try { + await ApiService.instance.patch('/users/me', body); Navigator.pushReplacementNamed(context, '/solicitudEnviada'); - } else { + } catch (e) { + print('Error sending professional info: $e'); showDialog( context: context, builder: (BuildContext context) { @@ -187,9 +213,7 @@ class _ProfessionalProfileWebScreenState content: const Text('Ocurrió un error al cargar las imágenes.'), actions: [ TextButton( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), child: const Text('Aceptar'), ), ], @@ -198,232 +222,22 @@ class _ProfessionalProfileWebScreenState ); } - // Actualiza la imagen de perfil si hay cambios - if (selectedImagInBytes != null) { - await uploadFile(); - await updateImage(photoTemp); - } - setState(() { - _isLoading = false; - }); - - // Navega a la siguiente pantalla - Navigator.pushReplacementNamed(context, '/solicitudEnviada'); - } - - uploadFile() async { - try { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = '$formattedDate$milliseconds'; - - final Reference ref = FirebaseStorage.instance - .ref() - .child('users') - .child(uid!) - .child('profile') - .child(random); - - final metaData = SettableMetadata(contentType: 'image/jpeg'); - - final UploadTask uploadTask = ref.putData(selectedImagInBytes!, metaData); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - return true; - } else { - return false; - } - } catch (e) { - print('web image error - $e'); - } - } - - uploadCedula() async { - try { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = 'c$formattedDate$milliseconds'; - - final Reference ref = FirebaseStorage.instance - .ref() - .child('users') - .child(uid!) - .child('cedula') - .child(random); - - final metaData = SettableMetadata(contentType: 'application/pdf'); - - final UploadTask uploadTask = ref.putData(imageCedulaBytes!, metaData); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoCedulaTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - updateImageCedula(photoCedulaTemp); - return true; - } else { - return false; - } - } catch (e) { - print('web image cedula error - $e'); - } - } - - uploadCertificado() async { - try { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = 'f$formattedDate$milliseconds'; - - final Reference ref = FirebaseStorage.instance - .ref() - .child('users') - .child(uid!) - .child('certificado_profesional') - .child(random); - - final metaData = SettableMetadata(contentType: 'application/pdf'); - - final UploadTask uploadTask = - ref.putData(imageCertificadoBytes!, metaData); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoCertificadoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - updateImageCertificado(photoCertificadoTemp); - return true; - } else { - return false; - } - } catch (e) { - print('web image certificado error - $e'); - } - } - - Future> uploadEspecializaciones(List files) async { - List filePaths = []; - - for (Uint8List fileBytes in files) { - final Reference ref = FirebaseStorage.instance - .ref() - .child('users') - .child(uid!) - .child('especializaciones') - .child('e${DateTime.now().millisecondsSinceEpoch}.pdf'); - - final SettableMetadata metaData = - SettableMetadata(contentType: 'application/pdf'); - - final UploadTask uploadTask = ref.putData(fileBytes, metaData); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - if (snapshot.state == TaskState.success) { - filePaths.add(ref.fullPath); - } else { - await updateFilesEspecializaciones(filePaths); - return []; - } - } - - await updateFilesEspecializaciones(filePaths); - return filePaths; - } - - Future updateImageCedula(image) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgCedula': image}); - } else { - await userRef.set({'imgCedula': image}); - } - } catch (e) { - print('Error al agregar o actualizar la imagen de cédula: $e'); - } - } - - Future updateImageCertificado(image) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgCertificado': image}); - } else { - await userRef.set({'imgCertificado': image}); - } - } catch (e) { - print('Error al agregar o actualizar la imagen de certificado: $e'); - } - } - - Future updateImage(image) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'photo': image}); - } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'photo': image}); - } catch (e) { - print('Error al agregar la imagen de perfil: $e'); - } - - print('Error al actualizar la imagen de perfil: $e'); - } - } - - Future updateFilesEspecializaciones(List filePaths) async { - try { - final userRef = FirebaseFirestore.instance.collection('users').doc(uid); - final userSnapshot = await userRef.get(); - - if (userSnapshot.exists) { - await userRef.update({'imgEspecializaciones': filePaths}); - } else { - await userRef.set({'imgEspecializaciones': filePaths}); - } - } catch (e) { - print('Error al actualizar los archivos de especializaciones: $e'); - } + setState(() => _isLoading = false); } void showSnackBar(String title, String message) { - Get.snackbar( - title, - message, - snackPosition: SnackPosition.TOP, - ); + Get.snackbar(title, message, snackPosition: SnackPosition.TOP); } @override Widget build(BuildContext context) { String profession = _profession.toString(); - double _space = 10; + double space = 10; return Scaffold( backgroundColor: const Color(0xFFD6F4FF), appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Perfil profesional'), body: SingleChildScrollView( child: Center( @@ -443,12 +257,14 @@ class _ProfessionalProfileWebScreenState ) : Column( children: [ - Container( - padding: const EdgeInsets.only(top: 20), - child: (selectedImagInBytes != null) - ? LocalPhotoWeb(file: selectedImagInBytes) - : ReferencePhotoWeb( - ref: storage.ref().child(_photo)), + GestureDetector( + onTap: () => _selectFile(true), + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: selectedImagInBytes != null + ? LocalPhotoWeb(file: selectedImagInBytes) + : ReferencePhotoWeb(ref: _photoUrl), + ), ), SizedBox( width: 300, @@ -460,8 +276,7 @@ class _ProfessionalProfileWebScreenState keyboardType: TextInputType.number, controller: _cedulaController, inputFormatters: [ - FilteringTextInputFormatter - .digitsOnly // Solo permite caracteres numéricos + FilteringTextInputFormatter.digitsOnly, ], validator: (String? value) { if (value == null || value.isEmpty) { @@ -474,15 +289,16 @@ class _ProfessionalProfileWebScreenState hintText: 'Cedula (Obligatorio)', ), ), - SizedBox(height: _space), + SizedBox(height: space), ElevatedButton( - onPressed: () { - _selectFileCedula(true); - }, + onPressed: () => + _selectFileCedula(true), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), + backgroundColor: + const Color(0xFFD6F4FF), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + borderRadius: + BorderRadius.circular(50), ), elevation: 0, minimumSize: const Size(250, 50), @@ -509,7 +325,7 @@ class _ProfessionalProfileWebScreenState ], ), ), - SizedBox(height: _space), + SizedBox(height: space), TextFormField( readOnly: true, onTap: () async { @@ -517,18 +333,16 @@ class _ProfessionalProfileWebScreenState (await Navigator.pushNamed( context, '/profession')) as String?; - if (profesion != null) { - setState(() { - _profession = profesion; - }); + setState( + () => _profession = profesion); } }, decoration: InputDecoration( prefixIcon: const Icon( Icons.assignment_ind_rounded), - suffixIcon: - const Icon(Icons.arrow_drop_down), + suffixIcon: const Icon( + Icons.arrow_drop_down), hintStyle: profession == '' ? const TextStyle() : const TextStyle( @@ -537,15 +351,16 @@ class _ProfessionalProfileWebScreenState ? 'Profesión (Obligatorio)' : profession), ), - SizedBox(height: _space), + SizedBox(height: space), ElevatedButton( - onPressed: () { - _selectFileCertificado(true); - }, + onPressed: () => + _selectFileCertificado(true), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), + backgroundColor: + const Color(0xFFD6F4FF), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + borderRadius: + BorderRadius.circular(50), ), elevation: 0, minimumSize: const Size(250, 50), @@ -572,23 +387,24 @@ class _ProfessionalProfileWebScreenState ], ), ), - SizedBox(height: _space), + SizedBox(height: space), TextFormField( controller: _especializacionController, decoration: const InputDecoration( - prefixIcon: - Icon(Icons.assignment_ind_rounded), + prefixIcon: Icon( + Icons.assignment_ind_rounded), hintText: 'Especialización'), ), - SizedBox(height: _space), + SizedBox(height: space), ElevatedButton( - onPressed: () async { - _selectFilesEspecializaciones(true); - }, + onPressed: () => + _selectFilesEspecializaciones(true), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6F4FF), + backgroundColor: + const Color(0xFFD6F4FF), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), + borderRadius: + BorderRadius.circular(50), ), elevation: 0, minimumSize: const Size(250, 50), @@ -656,7 +472,10 @@ class _ProfessionalProfileWebScreenState Container( alignment: Alignment.bottomCenter, margin: const EdgeInsets.only( - top: 30, right: 20, left: 20, bottom: 30), + top: 30, + right: 20, + left: 20, + bottom: 30), child: ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { diff --git a/lib/src/presentation/screens/profile/profile.dart b/lib/src/presentation/screens/profile/profile.dart index aa7e952..82a5028 100644 --- a/lib/src/presentation/screens/profile/profile.dart +++ b/lib/src/presentation/screens/profile/profile.dart @@ -1,14 +1,13 @@ -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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'dart:io'; +import 'package:http/http.dart' as http; import 'package:intl/intl.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/controllers/add_name_email_city.dart'; import 'package:prosappco/src/presentation/widgets/profile/birth_date_picker.dart'; @@ -18,11 +17,11 @@ import 'package:prosappco/src/presentation/screens/new_password.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_checkbox.dart'; import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; import 'package:prosappco/src/providers/user_provider.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:prosappco/src/services/select_image_profile.dart'; import 'package:prosappco/src/presentation/widgets/profile/gender_dropdown.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; import 'package:provider/provider.dart'; -import '../../../components/photo_view.dart'; import 'package:universal_html/html.dart' as html; class ProfileScreen extends StatefulWidget { @@ -34,6 +33,7 @@ class ProfileScreen extends StatefulWidget { class _ProfileScreenState extends State { File? imagen_to_upload; + Uint8List? webImageBytes; final DateFormat formatter = DateFormat('dd/MM/yyyy'); final uid = AuthenticationRepository.instance.getCurrentUserUid(); bool _obscureText = true; @@ -43,522 +43,108 @@ class _ProfileScreenState extends State { final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); - late final FirebaseAuth _auth; - final FirebaseStorage storage = FirebaseStorage.instance; - var photoTemp = ''; + var _ciudad = '...'; - var _photo = '..../images/perfil-2.png'; - String? _email = ''; + String? _photoUrl; + String? _email; String gender = ''; String genderDb = ''; DateTime? birthDate; String birthDateDb = ''; + bool enableLoginWithEmail = false; @override void initState() { super.initState(); - - _auth = FirebaseAuth.instance; - - final currentUser = _auth.currentUser; - - if (currentUser != null && currentUser.phoneNumber != null) { - _phoneNumberController.text = currentUser.phoneNumber!; - } - - if (currentUser != null && currentUser.displayName != null) { - _nameController.text = currentUser.displayName!; - } - if (currentUser != null && currentUser.email != null) { - _emailController.text = currentUser.email!; - } - - if (gender.isEmpty) { - AuthenticationRepository.instance.getGender(uid.toString()).then( - (String s) => setState(() { - genderDb = s; - }), - ); - } - - if (birthDate == null) { - AuthenticationRepository.instance.getBirthday(uid.toString()).then( - (String s) => setState(() { - if (s.isNotEmpty) { - birthDateDb = s; - } - }), - ); - } - - _email = currentUser?.email; - - if (_ciudad == '...') { - AuthenticationRepository.instance.getCity(uid.toString()).then( - (String s) => setState(() { - _ciudad = s; - }), - ); - } - - if (_photo == '...') { - AuthenticationRepository.instance.getPhoto(uid.toString()).then( - (String s) => setState(() { - _photo = s; - }), - ); - } + _loadCurrentUser(); } - Future updateImage(image) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'photo': image}); - } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'photo': image}); - } catch (e) { - print('Error al agregar la imagen de perfil: $e'); - } - - print('Error al actualizar la imagen de perfil: $e'); - } - } - - Future uploadImage(File image) async { - final now = DateTime.now(); - final formattedDate = DateFormat('HHmmssddMMyyyy').format(now); - final milliseconds = (now.microsecondsSinceEpoch / 1000).round(); - final random = '$formattedDate$milliseconds'; - - Reference ref = - storage.ref().child('users').child(uid!).child('profile').child(random); - - final UploadTask uploadTask = ref.putFile(image); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - return true; + Future _loadCurrentUser() async { + final currentUser = AuthenticationRepository.instance.currentUser.value; + if (currentUser != null) { + _nameController.text = currentUser.name; + _emailController.text = currentUser.email ?? ''; + _phoneNumberController.text = currentUser.phoneNumber ?? ''; + _photoUrl = currentUser.picture; + _ciudad = currentUser.city.isNotEmpty ? currentUser.city : '...'; + _email = currentUser.email; + genderDb = currentUser.gender ?? ''; + birthDateDb = currentUser.birthday ?? ''; } else { - return false; + // Fallback: fetch from API + try { + final Map data = + await ApiService.instance.get('/auth/me'); + if (mounted) { + setState(() { + _nameController.text = data['name'] ?? ''; + _emailController.text = data['email'] ?? ''; + _phoneNumberController.text = data['phone'] ?? ''; + _photoUrl = data['picture']; + _ciudad = data['city'] ?? '...'; + _email = data['email']; + genderDb = data['gender'] ?? ''; + birthDateDb = data['birthday'] ?? ''; + }); + } + } catch (e) { + print('Error loading user: $e'); + } } } - Future downloadImage(Reference ref) async { + Future _uploadImageFile(File image) async { try { - if (_photo == '...' || _photo.isEmpty) { - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); - } else { - final imageData = await ref.getData(); - if (imageData != null) { - final widgetImage = GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - child: ClipOval( - child: Image.memory( - imageData, - width: 60, - height: 60, - fit: BoxFit.cover, - ), - ), - ), - ); - return widgetImage; - } else { - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); - } + final uri = + Uri.parse('${ApiService.baseUrl}/storage/upload'); + final request = http.MultipartRequest('POST', uri); + final token = await ApiService.instance.getToken(); + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + request.files.add(await http.MultipartFile.fromPath('file', image.path)); + final streamed = await request.send(); + final resp = await http.Response.fromStream(streamed); + if (resp.statusCode >= 200 && resp.statusCode < 300) { + final json = ApiService.instance.parseJson(resp.body); + return json['url'] as String?; } } catch (e) { - return GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 50), - width: 100, - height: 100, - decoration: BoxDecoration( - color: const Color(0xFF2BA4EC), - borderRadius: BorderRadius.circular(50), - ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 90, - ), - ), - ); + print('Error uploading image: $e'); } + return null; } Future updateInfo() async { - final currentUser = _auth.currentUser; - final currentPhoneNumber = _auth.currentUser!.phoneNumber; + final Map body = {}; - String newName = _nameController.text.trim(); - String newEmail = _emailController.text.trim(); - String newPassword = _passwordController.text.trim(); - - if (gender.isNotEmpty) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'gender': gender}, SetOptions(merge: true)); - - genderDb = gender; - } catch (e) { - print('Error al actualizar el genero: $e'); - } - } - - if (birthDate != null) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'birth_date': birthDate.toString()}, SetOptions(merge: true)); - - birthDateDb = birthDate.toString(); - } catch (e) { - print('Error al actualizar la fecha de nacimiento: $e'); - } - } - - setState(() {}); - - if (newName.isEmpty) { - Get.snackbar( - 'Nombre Invalido', - 'Ingresa un nombre válido.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } - - if (enableLoginWithEmail) { - if (newEmail.isEmpty) { - Get.snackbar( - 'Correo Invalido', - 'Ingresa un email válido.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } - } - - if (currentUser?.displayName != newName) { - try { - await FirebaseAuth.instance.currentUser!.updateDisplayName(newName); - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'name': newName, 'lowerName': newName.toLowerCase()}); - } catch (e) { - print('Error al actualizar el nombre: $e'); - } - } - - String? selectedCity = _ciudad; - - if (kIsWeb) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'city': selectedCity}); - } catch (e) { - print('Error al actualizar la ciudad: $e'); - } - } - - if (enableLoginWithEmail) { - if (currentUser?.email != newEmail) { - if (newPassword.isNotEmpty) { - bool updateEmailSuccess = - await updateEmailAndPassword(newEmail, newPassword); - - if (updateEmailSuccess) { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'email': newEmail}); - } else { - return; - } - } else { - Get.snackbar( - 'Contraseña Invalida', - 'Por favor ingresa una contraseña.', - snackPosition: SnackPosition.BOTTOM, - ); - } - } - } + final newName = _nameController.text.trim(); + if (newName.isNotEmpty) body['name'] = newName; + if (_ciudad != '...' && _ciudad.isNotEmpty) body['city'] = _ciudad; + if (gender.isNotEmpty) body['gender'] = gender; + if (birthDate != null) body['birthday'] = birthDate.toString(); try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'phoneNumber': currentPhoneNumber}); - } catch (e) { - print('$e'); - } - - try { - if (imagen_to_upload == null) { - } else { - final uploaded = await uploadImage(imagen_to_upload!); - updateImage(photoTemp); + if (imagen_to_upload != null) { + final url = await _uploadImageFile(imagen_to_upload!); + if (url != null) body['picture'] = url; } - } catch (e) { - print('Error al actualizar la imagen de perfil $e'); - } - WarningSnackbar.show( - title: 'Informacion actualizada', - message: 'Tu informacion ha sido actualizada con exito.', - icon: const Icon( - Icons.check, - color: Colors.white, - ), - backgroundColor: Colors.green, - ); - } - - Future _updateEmailAndPassword( - String newEmail, String currentPassword) async { - final user = _auth.currentUser; - - if (user!.email! == newEmail) { - Get.snackbar( - 'No se puede actualizar', - 'El correo actual no puede ser actualizado.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } - - if (!newEmail.contains('@') || !newEmail.contains('.')) { - Get.snackbar( - 'No se puede actualizar', - 'Ingresa un correo electrónico valido.', - snackPosition: SnackPosition.BOTTOM, - ); - - return; - } - - final emailExistsQuery = await FirebaseFirestore.instance - .collection('users') - .where('email', isEqualTo: newEmail) - .get(); - - if (emailExistsQuery.docs.isNotEmpty) { - Get.snackbar( - 'No se puede actualizar', - 'El nuevo correo electrónico ya está en uso.', - snackPosition: SnackPosition.BOTTOM, - ); - return; - } - - try { - final credential = EmailAuthProvider.credential( - email: user.email!, password: currentPassword); - await user.reauthenticateWithCredential(credential); - - await user.updateEmail(newEmail); - - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'email': newEmail}); - - setState(() { - _email = newEmail; - }); + await ApiService.instance.patch('/users/me', body); WarningSnackbar.show( - title: 'Actualizado exitosamente', - message: 'Correo electronico actualizado correctamente.', + title: 'Informacion actualizada', + message: 'Tu informacion ha sido actualizada con exito.', icon: const Icon(Icons.check, color: Colors.white), backgroundColor: Colors.green, ); - - if (Navigator.canPop(context)) { - Navigator.of(context).pop(); - } } catch (e) { - WarningSnackbar.show( - title: 'No se pudo actualizar el correo', - message: - 'Verifica tu contraseña actual y asegúrate de que el nuevo correo electrónico no se haya utilizado previamente.', - ); + print('Error updating info: $e'); + Get.snackbar('Error', 'No se pudo actualizar la información.', + snackPosition: SnackPosition.BOTTOM); } } - Future _showEmailUpdateDialog(BuildContext context) async { - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Actualizar Email'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: emailController, - decoration: const InputDecoration(labelText: 'Nuevo Email'), - ), - TextField( - controller: passwordController, - decoration: - const InputDecoration(labelText: 'Contraseña Actual'), - obscureText: true, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text( - 'Cancelar', - style: TextStyle(color: Colors.grey), - ), - ), - TextButton( - onPressed: () { - String newEmail = emailController.text.trim(); - String currentPassword = passwordController.text.trim(); - if (newEmail.isNotEmpty && currentPassword.isNotEmpty) { - _updateEmailAndPassword(newEmail, currentPassword); - } - }, - child: const Text( - 'Guardar', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ); - }, - ); - } - - Future updateEmailAndPassword(String email, String password) async { - final User? user = FirebaseAuth.instance.currentUser; - if (user != null) { - try { - await user.updateEmail(email); - await user.updatePassword(password); - - return true; - } catch (e) { - WarningSnackbar.show( - title: 'Inicia sesión de nuevo', - message: 'Inicia la sesión de nuevo para guardar los cambios.', - ); - AuthenticationRepository.instance.logout(uid!); - } - } - return false; - } - - bool enableLoginWithEmail = false; - - Future> _getCities() async { - List citys = []; - if (kIsWeb) { - try { - QuerySnapshot countries = await countriesCollection.get(); - for (DocumentSnapshot country in countries.docs) { - String countryName = country.id; - Map data = country.data() as Map; - Map> states = {}; - - for (var entry in data.entries) { - String key = entry.key; - Map cityData = - Map.from(entry.value); - states[key] = cityData; - } - - for (var state in states.entries) { - var citysState = state.value.entries.map((city) => City( - cityName: city.key, - coordsOfCity: city.value, - stateOfCity: state.key, - countryOfCity: countryName, - )); - - citys.addAll(citysState); - } - } - } catch (e) { - print('Error obteniendo las ciudades: $e'); - } - } - - return citys; - } - Future _showChoiceDialog(BuildContext context) async { return showDialog( context: context, @@ -607,14 +193,11 @@ class _ProfileScreenState extends State { @override Widget build(BuildContext context) { String city = _ciudad.toString(); - final userProvider = Provider.of(context); return Scaffold( appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Perfil', ), body: SingleChildScrollView( @@ -623,14 +206,13 @@ class _ProfileScreenState extends State { child: Column( children: [ GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, + onTap: () => _showChoiceDialog(context), child: Container( - margin: const EdgeInsets.symmetric(vertical: 20), - child: (imagen_to_upload != null) - ? LocalPhoto(file: imagen_to_upload!) - : ReferencePhoto(ref: storage.ref().child(_photo))), + margin: const EdgeInsets.symmetric(vertical: 20), + child: imagen_to_upload != null + ? LocalPhoto(file: imagen_to_upload!) + : ReferencePhoto(ref: _photoUrl), + ), ), Container( width: 300, @@ -660,71 +242,35 @@ class _ProfileScreenState extends State { ), ), const SizedBox(), - kIsWeb - ? FutureBuilder>( - future: _getCities(), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (snapshot.hasError) { - return const Center( - child: - Text('Error al obtener las ciudades'), - ); - } else { - List filteredCities = snapshot.data!; - - return DropdownButtonFormField( - value: _ciudad, - onChanged: (String? newValue) { - setState(() { - _ciudad = newValue!; - }); - }, - items: filteredCities.map((City city) { - return DropdownMenuItem( - value: city.cityName, - child: Text(city.cityName ?? ''), - ); - }).toList(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.near_me), - hintText: _ciudad, - ), - ); - } + TextFormField( + readOnly: true, + onTap: () async { + final String? ciudad = await Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return const CityScreen(); }, - ) - : TextFormField( - readOnly: true, - onTap: () async { - final String? ciudad = await Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const CityScreen(); - }, - ), - ) as String?; - - if (ciudad != null) { - setState(() { - _ciudad = ciudad; - }); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.near_me), - suffixIcon: const Icon(Icons.arrow_drop_down), - hintStyle: city == '' - ? const TextStyle() - : const TextStyle(color: Colors.black87), - hintText: city == '' ? 'Ciudad' : city, - ), ), + ) as String?; + + if (ciudad != null) { + setState(() { + _ciudad = ciudad; + }); + } + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.near_me), + suffixIcon: const Icon(Icons.arrow_drop_down), + hintStyle: city == '' + ? const TextStyle() + : const TextStyle(color: Colors.black87), + hintText: city == '' || city == '...' + ? 'Ciudad' + : city, + ), + ), const SizedBox(height: 20.0), TextFormField( controller: _phoneNumberController, @@ -778,152 +324,15 @@ class _ProfileScreenState extends State { : const SizedBox(), _email != null && _email != '' ? TextFormField( - onTap: () { - _showEmailUpdateDialog(context); - }, readOnly: true, controller: _emailController, decoration: const InputDecoration( prefixIcon: Icon(Icons.email_outlined), - hintText: 'Email (Obligatorio)', + hintText: 'Email', ), ) : const SizedBox(), const SizedBox(height: 20), - _email == null || _email == '' - ? PrimaryCheckbox( - text: - 'Habilitar inicio de sesión con correo (Opcional)', - initialValue: enableLoginWithEmail, - onChanged: (value) { - setState(() { - enableLoginWithEmail = value; - }); - }, - ) - : const SizedBox(), - const SizedBox(height: 15), - enableLoginWithEmail - ? Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.blue, - width: 0.5, - ), - borderRadius: BorderRadius.circular(10), - ), - padding: const EdgeInsets.all(10), - child: Column( - children: [ - TextFormField( - controller: _emailController, - validator: (String? value) { - if (enableLoginWithEmail) { - if (value == null || value.isEmpty) { - return 'Por favor ingrese un email'; - } - final RegExp emailRegExp = RegExp( - r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegExp.hasMatch(value)) { - return 'Por favor ingrese un email válido'; - } - return null; - } else { - return null; - } - }, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.email_outlined), - hintText: 'Email', - ), - ), - const SizedBox(height: 20.0), - _email != null && _email != '' - ? const SizedBox.shrink() - : TextFormField( - controller: _passwordController, - obscureText: _obscureText, - validator: (value) { - if (enableLoginWithEmail) { - if (value == null || - value.isEmpty) { - return 'Por favor ingrese una contraseña.'; - } - if (value.length < 5) { - return 'Debe tener al menos 5 caracteres.'; - } - return null; - } else { - return null; - } - }, - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureText - ? Icons.visibility - : Icons.visibility_off, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureText = - !_obscureText; - }); - }, - ), - hintText: 'Contraseña'), - ), - _email != null && _email != '' - ? const SizedBox.shrink() - : const SizedBox(height: 20), - Container( - margin: const EdgeInsets.only( - left: 5, - right: 5, - top: 5, - bottom: 5, - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 8), - 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: const Row( - children: [ - Icon( - Icons.error_outline, - size: 20, - color: Colors.black54, - ), - SizedBox(width: 10), - Expanded( - child: Text( - 'Al habilitar el inicio de sesión con correo, se cerrara la sesión actual.', - style: TextStyle( - color: Colors.black54, - fontSize: 13, - ), - ), - ), - ], - ), - ) - ], - ), - ) - : const SizedBox(), ], ), ), @@ -955,12 +364,9 @@ class _ProfileScreenState extends State { PrimaryButton( onPressed: () async { if (_formKey.currentState!.validate()) { + await updateInfo(); if (kIsWeb) { - await updateInfo().whenComplete(() { - html.window.location.reload(); - }); - } else { - await updateInfo(); + html.window.location.reload(); } } await userProvider.updateUserDataAndScores(); diff --git a/lib/src/presentation/screens/profile/profile_pro.dart b/lib/src/presentation/screens/profile/profile_pro.dart index dde8e6b..a722117 100644 --- a/lib/src/presentation/screens/profile/profile_pro.dart +++ b/lib/src/presentation/screens/profile/profile_pro.dart @@ -1,21 +1,20 @@ import 'dart:io'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:http/http.dart' as http; import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/banner_photo.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/schedule_picker.dart'; import 'package:prosappco/src/models/setting_model.dart'; import 'package:prosappco/src/presentation/screens/horario.dart'; -import 'package:prosappco/src/presentation/screens/professional.dart'; import 'package:prosappco/src/presentation/screens/professional_direccion.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_checkbox.dart'; +import 'package:prosappco/src/services/api_service.dart'; import 'package:prosappco/src/services/select_image_profile.dart'; class ProfileProScreen extends StatefulWidget { @@ -35,184 +34,116 @@ class _ProfileProScreenState extends State { File? imagen_to_upload; bool tarifaValue = false; - bool domicilioValue = false; bool sitioValue = false; - var photoTemp = ''; - - var _photo = '...'; + String? _bannerUrl; var _direccion = '...'; var _ubicacion = '...'; - var _opcionalAddress = '...'; int _tarifa = 0; SettingModel? settings; - bool nequiValue = false; - bool banktransferValue = false; - bool datafoneValue = false; - - Map? _horarios; - Map paymentMethods = { 'Nequi': false, 'Transferencia Bancaria': false, 'Datafono': false, }; + Map? _horarios; + @override void initState() { super.initState(); - if (settings == null) { - SettingModel.getSettings().then((SettingModel value) => setState( - () => settings = value, - )); - } + SettingModel.getSettings() + .then((v) => setState(() => settings = v)); + Schedule.getHorarios(uid.toString()) + .then((data) => setState(() => _horarios = data)); + _loadProData(); + } - final uid = AuthenticationRepository.instance.getCurrentUserUid(); + Future _loadProData() async { + try { + final Map data = + await ApiService.instance.get('/auth/me'); + final pro = data['professionals'] as Map?; - if (_photo == '...') { - AuthenticationRepository.instance.getBanner(uid.toString()).then( - (String s) => setState( - () { - _photo = s; - }, - ), - ); - } + if (mounted) { + setState(() { + _bannerUrl = pro?['banner_picture']; + _direccion = data['address'] ?? '...'; + _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...'; + _tarifa = pro?['rate'] != null + ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0) + : 0; - if (_horarios == null) { - Schedule.getHorarios(uid.toString()).then( - (Map data) { - setState(() { - _horarios = data; - }); - }, - ); - } + if (_tarifa != 0) { + tarifaValue = true; + _tarifaController.text = _tarifa.toString(); + } - if (_direccion == '...') { - AuthenticationRepository.instance - .getAddress(uid.toString()) - .then((String s) => setState(() { - _direccion = s; - })); - } - if (_ubicacion == '...') { - AuthenticationRepository.instance.getUbicacion(uid.toString()).then( - (String s) => setState( - () { - _ubicacion = s; - if (_ubicacion == 'ambos') { - sitioValue = true; - domicilioValue = true; - } else if (_ubicacion == 'sitio') { - sitioValue = true; - } else if (_ubicacion == 'domicilio') { - domicilioValue = true; - } - }, - ), - ); - } - if (_opcionalAddress == '...') { - AuthenticationRepository.instance.getOpcionalAddress(uid.toString()).then( - (String s) => setState( - () { - _opcionalAddress = s; + if (_ubicacion == 'ambos') { + sitioValue = true; + domicilioValue = true; + } else if (_ubicacion == 'sitio') { + sitioValue = true; + } else if (_ubicacion == 'domicilio') { + domicilioValue = true; + } - if (_opcionalAddress != '...') { - _opcionalAddressController.text = _opcionalAddress; - } - }, - ), - ); - } - if (_tarifa == 0) { - AuthenticationRepository.instance.getTarifa(uid.toString()).then( - (s) => setState( - () { - _tarifa = s; + final pm = pro?['paymentMethods']; + if (pm is Map) { + paymentMethods = Map.from(pm); + } - if (_tarifa != 0) { - tarifaValue = true; - _tarifaController.text = _tarifa.toString(); - } - }, - ), - ); + final opcAddr = pro?['opcional_address'] ?? data['opcional_address']; + if (opcAddr != null && opcAddr.toString().isNotEmpty) { + _opcionalAddressController.text = opcAddr.toString(); + } + }); + } + } catch (e) { + print('Error loading pro data: $e'); } + } - loadPaymentMethods(); + Future _uploadImageFile(File image) async { + try { + final uri = Uri.parse('${ApiService.baseUrl}/storage/upload'); + final request = http.MultipartRequest('POST', uri); + final token = await ApiService.instance.getToken(); + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + request.files.add(await http.MultipartFile.fromPath('file', image.path)); + final streamed = await request.send(); + final resp = await http.Response.fromStream(streamed); + if (resp.statusCode >= 200 && resp.statusCode < 300) { + final json = ApiService.instance.parseJson(resp.body); + return json['url'] as String?; + } + } catch (e) { + print('Error uploading banner: $e'); + } + return null; } void createSchedules() async { if (_horarios == null || _horarios!.isEmpty) { - final defaultSchedule = { - '1': { + // Initialize default schedules via API + final Map defaultSchedule = {}; + for (var i = 1; i <= 7; i++) { + defaultSchedule['$i'] = { 'habilitado': false, 'jornadaContinua': false, 'range1Hour1': null, 'range1Hour2': null, 'range2Hour1': null, - 'range2Hour2': null - }, - '2': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '3': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '4': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '5': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '6': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '7': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - } - }; - + 'range2Hour2': null, + }; + } try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'horario': defaultSchedule}); - + await ApiService.instance + .patch('/users/me', {'horario': defaultSchedule}); Navigator.pop(context); } catch (e) { print(e); @@ -222,9 +153,7 @@ class _ProfileProScreenState extends State { context, CupertinoPageRoute( builder: (BuildContext context) { - return HorarioScreen( - horarios: _horarios!, - ); + return HorarioScreen(horarios: _horarios!); }, ), ); @@ -232,30 +161,19 @@ class _ProfileProScreenState extends State { } Future updateInfo() async { - if (_opcionalAddressController.text.isNotEmpty) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'opcional_address': _opcionalAddressController.text}); - } - if (tarifaValue && _tarifaController.text.isNotEmpty) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'tarifa': int.parse(_tarifaController.text)}); - } else { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'tarifa': 0}); - } + final Map body = {}; + if (_opcionalAddressController.text.isNotEmpty) { + body['opcional_address'] = _opcionalAddressController.text; + } + body['rate'] = tarifaValue && _tarifaController.text.isNotEmpty + ? int.parse(_tarifaController.text) + : 0; + + String? ubicacion; if (settings?.domicilios == false) { if (sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'sitio'}); + ubicacion = 'sitio'; } else { Get.snackbar( 'Elige como vas a dar tu servicio', @@ -267,29 +185,15 @@ class _ProfileProScreenState extends State { style: TextStyle(color: Colors.white), ), ); - - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': null}); return; } } else { if (domicilioValue && sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'ambos'}); + ubicacion = 'ambos'; } else if (domicilioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'domicilio'}); + ubicacion = 'domicilio'; } else if (sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'sitio'}); + ubicacion = 'sitio'; } else { Get.snackbar( 'Elige como vas a dar tu servicio', @@ -304,18 +208,19 @@ class _ProfileProScreenState extends State { return; } } + body['ubicacion'] = ubicacion; + body['paymentMethods'] = paymentMethods; - try { - if (imagen_to_upload == null) { - } else { - updateImage(photoTemp); - //image - } - } catch (e) { - print('Error al actualizar la imagen de perfil $e'); + if (imagen_to_upload != null) { + final url = await _uploadImageFile(imagen_to_upload!); + if (url != null) body['banner_picture'] = url; } - updatePaymentMethods(); + try { + await ApiService.instance.patch('/users/me', body); + } catch (e) { + print('Error updating pro info: $e'); + } if (settings?.domicilios == false && sitioValue == false) { Get.defaultDialog( @@ -324,9 +229,7 @@ class _ProfileProScreenState extends State { 'Si no eliges servicio en sitio, no serás visible para los usuarios.', actions: [ ElevatedButton( - onPressed: () { - Get.back(); - }, + onPressed: () => Get.back(), child: const Text('Entendido'), ), ], @@ -339,51 +242,6 @@ class _ProfileProScreenState extends State { ); Navigator.pop(context); } - - return; - } - - Future updateImage(image) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'banner': image}); - } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'banner': image}); - } catch (e) { - print('Error al agregar la imagen de perfil: $e'); - } - - print('Error al actualizar la imagen de perfil: $e'); - } - } - - Future uploadImage(File image) async { - final String namefile = image.path.split('/').last; - - Reference ref = storage - .ref() - .child('users') - .child(uid!) - .child('profile') - .child(namefile); - - final UploadTask uploadTask = ref.putFile(image); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - return true; - } else { - return false; - } } Future _showChoiceDialog(BuildContext context) async { @@ -434,34 +292,14 @@ class _ProfileProScreenState extends State { void toggleDomicilio(bool newValue) { setState(() { domicilioValue = newValue; - if (newValue == false && sitioValue == false) { - sitioValue = true; - } + if (newValue == false && sitioValue == false) sitioValue = true; }); } void toggleSitio(bool newValue) { setState(() { sitioValue = newValue; - if (newValue == false && domicilioValue == false) { - domicilioValue = true; - } - }); - } - - void updatePaymentMethods() { - FirebaseFirestore.instance.collection('users').doc(uid).update({ - 'paymentMethods': paymentMethods, - }); - } - - void loadPaymentMethods() { - FirebaseFirestore.instance.collection('users').doc(uid).get().then((doc) { - if (doc.exists) { - setState(() { - paymentMethods = Map.from(doc['paymentMethods'] ?? {}); - }); - } + if (newValue == false && domicilioValue == false) domicilioValue = true; }); } @@ -471,41 +309,24 @@ class _ProfileProScreenState extends State { return Scaffold( appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Perfil profesional'), body: SingleChildScrollView( reverse: true, child: Column( children: [ GestureDetector( - onTap: () { - _showChoiceDialog(context); - }, - child: Container( - child: (imagen_to_upload != null) - ? LocalPhoto( - file: imagen_to_upload!, - ) - : ReferenceBannerPhoto( - ref: storage.ref().child(_photo), - ), - ), + onTap: () => _showChoiceDialog(context), + child: imagen_to_upload != null + ? LocalPhoto(file: imagen_to_upload!) + : ReferenceBannerPhoto(ref: _bannerUrl), ), if (settings?.tarifas == true) - const Divider( - color: Colors.white, - height: 12, - ), + const Divider(color: Colors.white, height: 12), if (settings?.tarifas == true) - customSwitch( - 'Tarifa', - false, - (value) { - tarifaValue = value; - }, - ), + customSwitch('Tarifa', tarifaValue, (value) { + setState(() => tarifaValue = value); + }), tarifaValue ? Padding( padding: @@ -523,16 +344,11 @@ class _ProfileProScreenState extends State { ], ), ], - ) - .animate() - .moveY(duration: const Duration(milliseconds: 100)), + ).animate().moveY(duration: const Duration(milliseconds: 100)), ) : const SizedBox(), if (settings?.domicilios == true) - const Divider( - color: Colors.white, - height: 12, - ), + const Divider(color: Colors.white, height: 12), if (settings?.domicilios == true) customSwitch( 'Servicio a domicilio', domicilioValue, toggleDomicilio), @@ -557,9 +373,7 @@ class _ProfileProScreenState extends State { ) as String?; if (direccion != null) { - setState(() { - _direccion = direccion; - }); + setState(() => _direccion = direccion); } }, decoration: InputDecoration( @@ -576,9 +390,7 @@ class _ProfileProScreenState extends State { hintText: 'Oficina / Piso / Conjunto'), ), ], - ) - .animate() - .moveY(duration: const Duration(milliseconds: 100)), + ).animate().moveY(duration: const Duration(milliseconds: 100)), ) : const SizedBox(), const Divider(), @@ -590,19 +402,14 @@ class _ProfileProScreenState extends State { children: [ const Text( 'Metodos de pago', - style: TextStyle( - color: Colors.black, - fontSize: 17, - ), + style: TextStyle(color: Colors.black, fontSize: 17), ), for (var entry in paymentMethods.entries) PrimaryCheckbox( text: entry.key, initialValue: entry.value, onChanged: (value) { - setState(() { - paymentMethods[entry.key] = value; - }); + setState(() => paymentMethods[entry.key] = value); }, ), ], @@ -615,143 +422,20 @@ class _ProfileProScreenState extends State { width: double.infinity, child: Text( 'Horario estandar', - style: TextStyle( - color: Colors.black, - fontSize: 17, - ), + style: TextStyle(color: Colors.black, fontSize: 17), ), ), ), GestureDetector( - onTap: () { - createSchedules(); - }, + onTap: createSchedules, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Lunes'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['1'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Martes'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['2'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Miercoles'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['3'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Jueves'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['4'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Viernes'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['5'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Sabado'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['6'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Domingo'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['7'], context)), - ), - ), - ], - ), - ], - ), + child: _scheduleTable(context), ), ), const SizedBox(height: 10), PrimaryButton( - onPressed: () { - updateInfo(); - }, + onPressed: updateInfo, text: 'Guardar', ), const SizedBox(height: 20), @@ -761,6 +445,33 @@ class _ProfileProScreenState extends State { ); } + Widget _scheduleTable(BuildContext context) { + final days = { + '1': 'Lunes', + '2': 'Martes', + '3': 'Miercoles', + '4': 'Jueves', + '5': 'Viernes', + '6': 'Sabado', + '7': 'Domingo', + }; + return Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: days.entries.map((e) { + return TableRow(children: [ + TableCell( + child: Container( + padding: const EdgeInsets.all(8.0), + child: Text(e.value))), + TableCell( + child: Container( + padding: const EdgeInsets.all(8.0), + child: Text(timeList(_horarios?[e.key], context)))), + ]); + }).toList(), + ); + } + Widget customSwitch( String text, bool switchValue, @@ -784,10 +495,7 @@ class _ProfileProScreenState extends State { ), Transform.scale( scale: 1.2, - child: Switch( - value: switchValue, - onChanged: onChanged, - ), + child: Switch(value: switchValue, onChanged: onChanged), ), ], ), @@ -796,16 +504,10 @@ class _ProfileProScreenState extends State { } String timeList(Schedule? schedule, BuildContext context) { - if (schedule == null) { - return 'N/A'; - } - if (!schedule.habilitado) { - return 'N/A'; - } + if (schedule == null || !schedule.habilitado) return 'N/A'; if (schedule.jornadaContinua) { - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; - } else { - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range1Hour2?.format(context).toString()}; ${schedule.range2Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; + return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; } + return '${schedule.range1Hour1?.format(context)} - ${schedule.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; } } diff --git a/lib/src/presentation/screens/profile/profile_pro_web.dart b/lib/src/presentation/screens/profile/profile_pro_web.dart index f1ce504..f40eaf3 100644 --- a/lib/src/presentation/screens/profile/profile_pro_web.dart +++ b/lib/src/presentation/screens/profile/profile_pro_web.dart @@ -1,6 +1,3 @@ -import 'dart:io'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,8 +9,8 @@ import 'package:prosappco/src/components/primary_btn.dart'; import 'package:prosappco/src/components/schedule_picker.dart'; import 'package:prosappco/src/models/setting_model.dart'; import 'package:prosappco/src/presentation/screens/horario.dart'; -import 'package:prosappco/src/presentation/screens/professional.dart'; import 'package:prosappco/src/presentation/screens/ubicacion.dart'; +import 'package:prosappco/src/services/api_service.dart'; class ProfileProWebScreen extends StatefulWidget { const ProfileProWebScreen({super.key}); @@ -36,11 +33,8 @@ class _ProfileProWebScreenState extends State { bool domicilioValue = true; bool tarifaValue = false; bool sitioValue = false; - var photoTemp = ''; - var _direccion = '...'; var _ubicacion = '...'; - var _opcionalAddress = '...'; int _tarifa = 0; SettingModel? settings; @@ -49,110 +43,78 @@ class _ProfileProWebScreenState extends State { @override void initState() { super.initState(); - if (settings == null) { - SettingModel.getSettings().then( - (SettingModel value) => setState(() => settings = value), - ); - } + SettingModel.getSettings() + .then((v) => setState(() => settings = v)); + Schedule.getHorarios(uid.toString()) + .then((data) => setState(() => _horarios = data)); + _loadProData(); + } - final uid = AuthenticationRepository.instance.getCurrentUserUid(); + Future _loadProData() async { + try { + final Map data = + await ApiService.instance.get('/auth/me'); + final pro = data['professionals'] as Map?; - if (_horarios == null) { - Schedule.getHorarios(uid.toString()).then( - (Map data) { - setState(() { - _horarios = data; - }); - }, - ); - } + if (mounted) { + setState(() { + final address = data['address'] ?? ''; + if (address.isNotEmpty) { + _ubicationController.text = address; + } - if (_direccion == '...') { - AuthenticationRepository.instance - .getAddress(uid.toString()) - .then((String s) => setState(() { - _direccion = s; - _ubicationController.text = _direccion; - })); - } - if (_ubicacion == '...') { - AuthenticationRepository.instance.getUbicacion(uid.toString()).then( - (String s) => setState( - () { - _ubicacion = s; - if (_ubicacion == 'ambos') { - sitioValue = true; - domicilioValue = true; - } else if (_ubicacion == 'sitio') { - sitioValue = true; - } else if (_ubicacion == 'domicilio') { - domicilioValue = true; - } - }, - ), - ); - } - if (_opcionalAddress == '...') { - AuthenticationRepository.instance.getOpcionalAddress(uid.toString()).then( - (String s) => setState( - () { - _opcionalAddress = s; + final opcAddr = + pro?['opcional_address'] ?? data['opcional_address'] ?? ''; + if (opcAddr.isNotEmpty) { + _opcionalAddressController.text = opcAddr; + } - if (_opcionalAddress != '...') { - _opcionalAddressController.text = _opcionalAddress; - } - }, - ), - ); - } - if (_tarifa == 0) { - AuthenticationRepository.instance.getTarifa(uid.toString()).then( - (s) => setState( - () { - _tarifa = s; + _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...'; + if (_ubicacion == 'ambos') { + sitioValue = true; + domicilioValue = true; + } else if (_ubicacion == 'sitio') { + sitioValue = true; + } else if (_ubicacion == 'domicilio') { + domicilioValue = true; + } - if (_tarifa != 0) { - tarifaValue = true; - _tarifaController.text = _tarifa.toString(); - } - }, - ), - ); + _tarifa = pro?['rate'] != null + ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0) + : 0; + if (_tarifa != 0) { + tarifaValue = true; + _tarifaController.text = _tarifa.toString(); + } + }); + } + } catch (e) { + print('Error loading pro data: $e'); } } Future updateInfo() async { + final Map body = {}; + if (_ubicationController.text.isNotEmpty) { - FirebaseFirestore.instance.collection('users').doc(uid).update({ - 'address': _ubicationController.text, - if (latUser != 0.0) 'latitude': latUser, - if (latUser != 0.0) 'longitude': lngUser, - }); + body['address'] = _ubicationController.text; + if (latUser != 0.0) { + body['latitude'] = latUser; + body['longitude'] = lngUser; + } } if (_opcionalAddressController.text.isNotEmpty) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'opcional_address': _opcionalAddressController.text}); - } - if (tarifaValue && _tarifaController.text.isNotEmpty) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'tarifa': int.parse(_tarifaController.text)}); - } else { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'tarifa': 0}); + body['opcional_address'] = _opcionalAddressController.text; } + body['rate'] = tarifaValue && _tarifaController.text.isNotEmpty + ? int.parse(_tarifaController.text) + : 0; + + String? ubicacion; if (settings?.domicilios == false) { if (sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'sitio'}); + ubicacion = 'sitio'; } else { Get.snackbar( 'Elige como vas a dar tu servicio', @@ -164,29 +126,15 @@ class _ProfileProWebScreenState extends State { style: TextStyle(color: Colors.white), ), ); - - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': null}); return; } } else { if (domicilioValue && sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'ambos'}); + ubicacion = 'ambos'; } else if (domicilioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'domicilio'}); + ubicacion = 'domicilio'; } else if (sitioValue) { - FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'ubicacion': 'sitio'}); + ubicacion = 'sitio'; } else { Get.snackbar( 'Elige como vas a dar tu servicio', @@ -201,6 +149,13 @@ class _ProfileProWebScreenState extends State { return; } } + body['ubicacion'] = ubicacion; + + try { + await ApiService.instance.patch('/users/me', body); + } catch (e) { + print('Error updating pro info: $e'); + } if (settings?.domicilios == false && sitioValue == false) { Get.defaultDialog( @@ -209,9 +164,7 @@ class _ProfileProWebScreenState extends State { 'Si no eliges servicio en sitio, no serás visible para los usuarios.', actions: [ ElevatedButton( - onPressed: () { - Get.back(); - }, + onPressed: () => Get.back(), child: const Text('Entendido'), ), ], @@ -224,97 +177,24 @@ class _ProfileProWebScreenState extends State { ); Navigator.pop(context); } - - return; - } - - Future updateImage(image) async { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'banner': image}); - } catch (e) { - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({'banner': image}); - } catch (e) { - print('Error al agregar la imagen de perfil: $e'); - } - - print('Error al actualizar la imagen de perfil: $e'); - } } void createSchedules() async { if (_horarios == null || _horarios!.isEmpty) { - final defaultSchedule = { - '1': { + final Map defaultSchedule = {}; + for (var i = 1; i <= 7; i++) { + defaultSchedule['$i'] = { 'habilitado': false, 'jornadaContinua': false, 'range1Hour1': null, 'range1Hour2': null, 'range2Hour1': null, - 'range2Hour2': null - }, - '2': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '3': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '4': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '5': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '6': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - }, - '7': { - 'habilitado': false, - 'jornadaContinua': false, - 'range1Hour1': null, - 'range1Hour2': null, - 'range2Hour1': null, - 'range2Hour2': null - } - }; - + 'range2Hour2': null, + }; + } try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'horario': defaultSchedule}); - + await ApiService.instance + .patch('/users/me', {'horario': defaultSchedule}); Navigator.pop(context); } catch (e) { print(e); @@ -324,53 +204,24 @@ class _ProfileProWebScreenState extends State { context, CupertinoPageRoute( builder: (BuildContext context) { - return HorarioScreen( - horarios: _horarios!, - ); + return HorarioScreen(horarios: _horarios!); }, ), ); } } - Future uploadImage(File image) async { - final String namefile = image.path.split('/').last; - - Reference ref = storage - .ref() - .child('users') - .child(uid!) - .child('profile') - .child(namefile); - - final UploadTask uploadTask = ref.putFile(image); - - final TaskSnapshot snapshot = await uploadTask.whenComplete(() => true); - - photoTemp = ref.fullPath; - - if (snapshot.state == TaskState.success) { - return true; - } else { - return false; - } - } - void toggleDomicilio(bool newValue) { setState(() { domicilioValue = newValue; - if (newValue == false && sitioValue == false) { - sitioValue = true; - } + if (newValue == false && sitioValue == false) sitioValue = true; }); } void toggleSitio(bool newValue) { setState(() { sitioValue = newValue; - if (newValue == false && domicilioValue == false) { - domicilioValue = true; - } + if (newValue == false && domicilioValue == false) domicilioValue = true; }); } @@ -378,27 +229,18 @@ class _ProfileProWebScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: PopAppbar( - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => Navigator.pop(context), label: 'Perfil profesional'), body: SingleChildScrollView( reverse: true, child: Column( children: [ if (settings?.tarifas == true) - const Divider( - color: Colors.white, - height: 12, - ), + const Divider(color: Colors.white, height: 12), if (settings?.tarifas == true) - customSwitch( - 'Tarifa', - tarifaValue, - (value) { - tarifaValue = value; - }, - ), + customSwitch('Tarifa', tarifaValue, (value) { + setState(() => tarifaValue = value); + }), tarifaValue ? Padding( padding: @@ -416,16 +258,11 @@ class _ProfileProWebScreenState extends State { ], ), ], - ) - .animate() - .moveY(duration: const Duration(milliseconds: 100)), + ).animate().moveY(duration: const Duration(milliseconds: 100)), ) : const SizedBox(), if (settings?.domicilios == true) - const Divider( - color: Colors.white, - height: 12, - ), + const Divider(color: Colors.white, height: 12), if (settings?.domicilios == true) customSwitch( 'Servicio a domicilio', domicilioValue, toggleDomicilio), @@ -474,9 +311,7 @@ class _ProfileProWebScreenState extends State { hintText: 'Oficina / Piso / Conjunto'), ), ], - ) - .animate() - .moveY(duration: const Duration(milliseconds: 100)), + ).animate().moveY(duration: const Duration(milliseconds: 100)), ) : const SizedBox(), const Divider(), @@ -486,146 +321,43 @@ class _ProfileProWebScreenState extends State { width: double.infinity, child: Text( 'Horario estandar', - style: TextStyle( - color: Colors.black, - fontSize: 15, - ), + style: TextStyle(color: Colors.black, fontSize: 15), ), ), ), GestureDetector( - onTap: () { - createSchedules(); - }, + onTap: createSchedules, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Table( defaultColumnWidth: const IntrinsicColumnWidth(), children: [ - TableRow( - children: [ + for (var entry in { + '1': 'Lunes', + '2': 'Martes', + '3': 'Miercoles', + '4': 'Jueves', + '5': 'Viernes', + '6': 'Sabado', + '7': 'Domingo', + }.entries) + TableRow(children: [ TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Lunes'), - ), - ), + child: Container( + padding: const EdgeInsets.all(8.0), + child: Text(entry.value))), TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['1'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Martes'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['2'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Miercoles'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['3'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Jueves'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['4'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Viernes'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['5'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Sabado'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['6'], context)), - ), - ), - ], - ), - TableRow( - children: [ - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: const Text('Domingo'), - ), - ), - TableCell( - child: Container( - padding: const EdgeInsets.all(8.0), - child: Text(timeList(_horarios?['7'], context)), - ), - ), - ], - ), + child: Container( + padding: const EdgeInsets.all(8.0), + child: + Text(timeList(_horarios?[entry.key], context)))), + ]), ], ), ), ), - PrimaryButtom( - onPressed: () { - updateInfo(); - }, - label: 'Guardar'), - const SizedBox( - height: 20, - ) + PrimaryButtom(onPressed: updateInfo, label: 'Guardar'), + const SizedBox(height: 20), ], ), ), @@ -643,18 +375,12 @@ class _ProfileProWebScreenState extends State { Expanded( child: Text( text, - style: const TextStyle( - fontSize: 15, - color: Colors.black, - ), + style: const TextStyle(fontSize: 15, color: Colors.black), ), ), Transform.scale( scale: 1.2, - child: Switch( - value: switchValue, - onChanged: onChanged, - ), + child: Switch(value: switchValue, onChanged: onChanged), ), ], ), @@ -663,16 +389,10 @@ class _ProfileProWebScreenState extends State { } String timeList(Schedule? schedule, BuildContext context) { - if (schedule == null) { - return 'N/A'; - } - if (!schedule.habilitado) { - return 'N/A'; - } + if (schedule == null || !schedule.habilitado) return 'N/A'; if (schedule.jornadaContinua) { - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; - } else { - return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range1Hour2?.format(context).toString()}; ${schedule.range2Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; + return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; } + return '${schedule.range1Hour1?.format(context)} - ${schedule.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}'; } } diff --git a/lib/src/presentation/screens/register/register.dart b/lib/src/presentation/screens/register/register.dart index 31a8280..6b8b859 100644 --- a/lib/src/presentation/screens/register/register.dart +++ b/lib/src/presentation/screens/register/register.dart @@ -1,9 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; -import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/bottom_sheet.dart'; import 'package:prosappco/src/components/column_padding.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; @@ -90,68 +88,54 @@ class _RegisterScreenState extends State { alineacion: MainAxisAlignment.start, padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), children: [ - !isIOS && !kIsWeb && settings?.google == true - ? Padding( - padding: const EdgeInsets.only(bottom: 20), - child: ElevatedButton( - onPressed: () async { - await AuthenticationRepository.instance - .signInWithGoogle(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Entrar con Google ', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - SizedBox(width: 5), - FaIcon(FontAwesomeIcons.google), - ], - )), - ) - : const SizedBox(), - !isIOS && !kIsWeb && settings?.google == true - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 0), - child: Row( - children: [ - Expanded( - child: Divider( - color: Colors.black38, - thickness: 1, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Text("ó"), - ), - Expanded( - child: Divider( - color: Colors.black38, - thickness: 1, - ), - ), - ], - ), - ) - : const SizedBox(), + const SizedBox(), Form( key: _formKey, child: Column( children: [ + const Padding( + padding: EdgeInsets.only(bottom: 5), + child: Align( + alignment: Alignment.topLeft, + child: Text('Nombre', + style: TextStyle( + fontSize: 18.0, color: Color(0xFF65676B))), + )), + Padding( + padding: const EdgeInsets.only(bottom: 18), + child: TextFormField( + controller: controller.name, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa tu nombre'; + } + return null; + }, + decoration: const InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50)), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50)), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), + borderRadius: BorderRadius.all(Radius.circular(50)), + ), + hintText: 'Tu nombre completo', + fillColor: Color.fromARGB(255, 239, 239, 239), + filled: true, + prefixIcon: Icon(Icons.person_outline), + hintStyle: TextStyle(color: Colors.grey), + ), + ), + ), const Padding( padding: EdgeInsets.only(bottom: 5), child: Align( @@ -334,6 +318,7 @@ class _RegisterScreenState extends State { .registerUser( controller.email.text.trim(), controller.password.text.trim(), + controller.name.text.trim(), ) .then((value) => Provider.of(context, listen: false) @@ -445,6 +430,46 @@ class _RegisterScreenState extends State { key: _formKey, child: Column( children: [ + const Padding( + padding: EdgeInsets.only(bottom: 5), + child: Align( + alignment: Alignment.topLeft, + child: Text('Nombre', + style: TextStyle( + fontSize: 18.0, + color: Color(0xFF65676B))), + )), + Padding( + padding: const EdgeInsets.only(bottom: 18), + child: TextFormField( + controller: controller.name, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Por favor ingresa tu nombre'; + } + return null; + }, + decoration: const InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50))), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50))), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color(0xFFECECEC)), + borderRadius: BorderRadius.all(Radius.circular(50))), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color.fromARGB(255, 184, 0, 0)), + borderRadius: BorderRadius.all(Radius.circular(50))), + hintText: 'Tu nombre completo', + fillColor: Color.fromARGB(255, 239, 239, 239), + filled: true, + prefixIcon: Icon(Icons.person_outline), + hintStyle: TextStyle(color: Colors.grey), + ), + ), + ), const Padding( padding: EdgeInsets.only(bottom: 5), child: Align( @@ -639,6 +664,7 @@ class _RegisterScreenState extends State { .registerUser( controller.email.text.trim(), controller.password.text.trim(), + controller.name.text.trim(), ) .then((value) => Provider.of(context, diff --git a/lib/src/presentation/screens/reset_password/reset_password.dart b/lib/src/presentation/screens/reset_password/reset_password.dart index 8402ab8..f99110f 100644 --- a/lib/src/presentation/screens/reset_password/reset_password.dart +++ b/lib/src/presentation/screens/reset_password/reset_password.dart @@ -1,4 +1,3 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:prosappco/src/components/pop_appbar.dart'; @@ -96,24 +95,14 @@ class ResetPasswordScreen extends StatelessWidget { ), const SizedBox(height: 80), PrimaryButtom( - onPressed: () async { - try { - await FirebaseAuth.instance.sendPasswordResetEmail( - email: controller.email.text.trim()); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Se ha enviado un enlace de restablecimiento de contraseña a tu correo electrónico.'), - ), - ); - Navigator.pop(context); - } catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( content: Text( - 'Hubo un error al enviar el enlace de restablecimiento de contraseña.'), - )); - } + 'Funcionalidad no disponible. Contacta a soporte.'), + ), + ); + Navigator.pop(context); }, label: 'Enviar'), ], diff --git a/lib/src/presentation/screens/score.dart b/lib/src/presentation/screens/score.dart index 7bf4a97..39dd53a 100644 --- a/lib/src/presentation/screens/score.dart +++ b/lib/src/presentation/screens/score.dart @@ -1,12 +1,12 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:intl/intl.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/user_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; class ScoreScreen extends StatefulWidget { final Event evento; @@ -19,32 +19,59 @@ class ScoreScreen extends StatefulWidget { class ScoreScreenState extends State { TextEditingController commentController = TextEditingController(); - Reference? ref_photo; + String? photoUrl; String nombre = ''; double _rating = 1.0; + final myUid = AuthenticationRepository.instance.getCurrentUserUid(); + + @override + void initState() { + super.initState(); + _loadUserInfo(); + } + + Future _loadUserInfo() async { + final targetId = (myUid != widget.evento.userId) + ? widget.evento.userId + : widget.evento.professionalId; + try { + final Map data = + await ApiService.instance.get('/users/$targetId'); + if (mounted) { + setState(() { + nombre = data['name'] ?? ''; + photoUrl = data['picture']; + }); + } + } catch (e) { + print('Error loading user for score: $e'); + } + } + + Future _submit() async { + try { + final bool isPro = widget.pro; + // Update the service scored flag + await ApiService.instance.patch('/services/${widget.evento.id}', { + if (isPro) 'professional_scored': true, + if (!isPro) 'user_scored': true, + }); + // Post the score/comment + await ApiService.instance.post('/comments', { + 'comment': commentController.text, + 'from_user': isPro ? widget.evento.professionalId : widget.evento.userId, + 'is_from_professional': !isPro, + 'score': _rating, + 'to_user': isPro ? widget.evento.userId : widget.evento.professionalId, + }); + if (mounted) Navigator.pop(context); + } catch (e) { + print('Error submitting score: $e'); + } + } @override Widget build(BuildContext context) { - 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; - }); - }); - }); - } else { - UserModel.getUser(widget.evento.professionalId).then((value) { - setState(() { - nombre = value.name; - ref_photo = value.photo; - }); - }); - } - } - return Scaffold( appBar: PopAppbar( onPressed: () { @@ -59,7 +86,7 @@ class ScoreScreenState extends State { leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: ReferencePhoto( - ref: ref_photo, + ref: photoUrl, size: 50, sizeCircle: 50, sizeIcon: 35, @@ -119,60 +146,24 @@ class ScoreScreenState extends State { ), Padding( padding: const EdgeInsets.only(bottom: 30), - child: Column( - children: [ - ElevatedButton( - onPressed: () { - if (widget.pro) { - FirebaseFirestore.instance - .collection("services") - .doc(widget.evento.id) - .update({'professional_scored': true}).then((value) { - FirebaseFirestore.instance.collection("scores").add({ - "comment": commentController.text, - "from_user": widget.evento.professionalId, - "is_from_professional": false, - "score": _rating, - "to_user": widget.evento.userId, - }).then((value) { - Navigator.pop(context); - }); - }); - } else { - FirebaseFirestore.instance - .collection("services") - .doc(widget.evento.id) - .update({'user_scored': true}).then((value) { - FirebaseFirestore.instance.collection("scores").add({ - "comment": commentController.text, - "from_user": widget.evento.userId, - "is_from_professional": true, - "score": _rating, - "to_user": widget.evento.professionalId, - }).then((value) { - Navigator.pop(context); - }); - }); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2BA4EC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - elevation: 0, - minimumSize: const Size(230, 60), - ), - child: const Text( - 'Enviar', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), + child: ElevatedButton( + onPressed: _submit, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2BA4EC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), ), - ], + elevation: 0, + minimumSize: const Size(230, 60), + ), + child: const Text( + 'Enviar', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), ), ), ], diff --git a/lib/src/presentation/screens/service_web.dart b/lib/src/presentation/screens/service_web.dart index 12fd9f5..3199b41 100644 --- a/lib/src/presentation/screens/service_web.dart +++ b/lib/src/presentation/screens/service_web.dart @@ -1,7 +1,4 @@ import 'dart:convert'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart'; @@ -17,7 +14,6 @@ import 'package:prosappco/src/presentation/screens/professional.dart'; import 'package:intl/intl.dart'; import 'package:prosappco/src/presentation/screens/service_after.dart'; import 'package:prosappco/src/presentation/screens/ubicacion.dart'; -import 'package:http/http.dart' as http; import 'package:prosappco/src/components/network_utility.dart'; import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; import 'package:prosappco/src/providers/user_provider.dart'; @@ -31,54 +27,6 @@ class ServiceWebScreen extends StatefulWidget { } class _ServiceOldScreenState extends State { - void _saveToken() async { - FirebaseMessaging messaging = FirebaseMessaging.instance; - - final token = await messaging.getToken(); - - try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .update({'token': token}); - } catch (e) { - print(e); - } - } - - Future sendPushNotification(String pro) async { - try { - http.Response response = await http.post( - Uri.parse('https://fcm.googleapis.com/fcm/send'), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': - 'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2', - }, - body: jsonEncode( - { - 'notification': { - 'body': 'alguien a solicitado tus servicios', - 'title': 'Nueva solicitud', - }, - 'priority': 'high', - 'data': { - 'click_action': 'FLUTTER_NOTIFICATION_CLICK', - 'id': '1', - 'status': 'done', - 'screen': 'solicitud' - }, - 'to': pro - }, - ), - ); - - response; - } catch (e) { - print('error al enviar notificacion $e'); - } - } - EventoService eventoService = EventoService(); String ubicacion = ''; final TextEditingController _locationController = TextEditingController(); @@ -97,43 +45,38 @@ class _ServiceOldScreenState extends State { final DateFormat formatter = DateFormat('dd/MM/yyyy'); final DateTime now = DateTime.now(); List _placesList = []; - String selectedPlace = ''; String _coordsOfCity = '0.0,0.0'; DateTime? _selectedDate; TimeOfDay? _selectedTime; GoogleMapController? googleMapController; - Set markers = {}; + final uid = AuthenticationRepository.instance.getCurrentUserUid(); + + BitmapDescriptor? _markerIcon; + String _ciudad = '...'; + Future _determinePosition() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); - - if (!serviceEnabled) { - return Future.error('Location services are disabled'); - } + 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; + return Geolocator.getCurrentPosition(); } Future _selectDate(BuildContext context) async { @@ -142,17 +85,10 @@ class _ServiceOldScreenState extends State { initialDate: now, firstDate: now, lastDate: DateTime(now.year + 1), - // builder: (context, child) { - // return Theme(data: ThemeData.dark(), child: child!); - // }, ); if (picked != null && picked != _selectedDate) { - if (mounted) { - setState(() { - _selectedDate = picked; - }); - } + if (mounted) setState(() => _selectedDate = picked); } } @@ -161,73 +97,31 @@ class _ServiceOldScreenState extends State { context: context, initialTime: TimeOfDay.now(), ); - if (pickedTime != null) { - if (mounted) { - setState(() { - _selectedTime = pickedTime; - }); - } + if (mounted) setState(() => _selectedTime = pickedTime); } } - Future>>> getUsersWithActiveStatus( - String _serviceType) async { - var querySnapshot; - - if (_serviceType != "Servicio") { - querySnapshot = await FirebaseFirestore.instance - .collection('users') - .where('estado', isEqualTo: 'activo') - .where('profesion', isEqualTo: _serviceType) - .get(); - } else { - querySnapshot = await FirebaseFirestore.instance - .collection('users') - .where('estado', isEqualTo: 'activo') - .get(); - } - - return querySnapshot.docs; - } - - final FirebaseStorage storage = FirebaseStorage.instance; - final uid = AuthenticationRepository.instance.getCurrentUserUid(); - final fcmToken = FirebaseMessaging.instance.getToken(); - - BitmapDescriptor? _markerIcon; - - String _ciudad = '...'; - void _setInitialCameraPosition(String coordsOfCity) async { List coords = coordsOfCity.split(','); double lat = double.parse(coords[0]); double lng = double.parse(coords[1]); googleMapController?.moveCamera( - CameraUpdate.newLatLngZoom( - LatLng(lat, lng), - 14.4746, - ), + CameraUpdate.newLatLngZoom(LatLng(lat, lng), 14.4746), ); try { Position position = await _determinePosition(); - googleMapController?.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( - target: LatLng( - position.latitude, - position.longitude, - ), + target: LatLng(position.latitude, position.longitude), zoom: 17, ), ), ); - if (mounted) { - setState(() {}); - } + if (mounted) setState(() {}); } catch (e) { print('Error: $e'); } @@ -240,90 +134,22 @@ class _ServiceOldScreenState extends State { _profesionalController.dispose(); _serviceTypeController.dispose(); _observacionController.dispose(); - super.dispose(); } - void updateMarkersForServiceType(String serviceType) { - getUsersWithActiveStatus(serviceType).then((value) { - markers.clear(); - for (var doc in value) { - final element = doc.data()!; - - if (element['latitude'] != null && element['longitude'] != null) { - markers.add( - Marker( - icon: _markerIcon!, - markerId: MarkerId(doc.id), - position: LatLng( - element['latitude'], - element['longitude'], - ), - ), - ); - } - } - }).catchError((e) { - print('Error al actualizar los marcadores: $e'); - }); - } - - String? settings; - @override void initState() { super.initState(); - if (_ciudad == '...') { - AuthenticationRepository.instance - .getCity(uid.toString()) - .then((String s) { - if (s.isEmpty) { - FirebaseFirestore.instance.collection('users').doc(uid).set({ - 'city': 'Cúcuta', - }).then((_) { - if (mounted) { - setState(() { - _ciudad = 'Cúcuta'; - }); - } - }); - } else { - if (mounted) { - setState(() { - _ciudad = s; - }); - } - } - }); - } - - if (_coordsOfCity == '0.0,0.0') { - AuthenticationRepository.instance - .getCoordsOfCity(uid.toString()) - .then((String s) { - if (mounted) { - setState(() { - _coordsOfCity = s; - _setInitialCameraPosition(_coordsOfCity); - }); - } - }); - } - - _saveToken(); + // Load city from in-memory user + final currentUser = AuthenticationRepository.instance.currentUser.value; + _ciudad = currentUser?.city ?? '...'; BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(6, 6)), 'images/pro_marke.png') .then((icon) { - if (mounted) { - setState(() { - _markerIcon = icon; - }); - } + if (mounted) setState(() => _markerIcon = icon); }); - - updateMarkersForServiceType(_serviceType); } static CameraPosition initialCameraPosition = const CameraPosition( @@ -331,40 +157,18 @@ class _ServiceOldScreenState extends State { zoom: 14.4746, ); - void setInitialCameraPosition(String coordsOfCity) { - if (coordsOfCity != '0.0,0.0') { - List coords = coordsOfCity.split(','); - double lat = double.parse(coords[0]); - double lng = double.parse(coords[1]); - - initialCameraPosition = CameraPosition( - target: LatLng(lat, lng), - zoom: 14.4746, - ); - } - } - - late String lat; - late String long; - double latUser = 0.0; double lngUser = 0.0; - var coordinates; Future getLocationName(double latitude, double longitude) async { - String address; List 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 "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}"; } - return address; + return ''; } void placeAutoComplete(String query) async { @@ -374,7 +178,6 @@ class _ServiceOldScreenState extends State { }); String? response = await NetworkUtility.fetchUrl(uri); - if (response != null) { if (mounted) { setState(() { @@ -396,10 +199,7 @@ class _ServiceOldScreenState extends State { elevation: 0, title: const Text( 'Prosapp', - style: TextStyle( - color: Colors.white, - fontSize: 20, - ), + style: TextStyle(color: Colors.white, fontSize: 20), )), body: Center( child: SizedBox( @@ -443,9 +243,9 @@ class _ServiceOldScreenState extends State { professionalTarifa = profesional.tarifa ?? 0; professionalUbicacion = profesional.ubicacion; professionalAddress = _ubicationController.text; - if (profesional.token != '') { + if (profesional.token != null && + profesional.token != '') { professionalToken = profesional.token!; - print(professionalToken); } if (ubicacion == 'sitio') { @@ -583,44 +383,38 @@ class _ServiceOldScreenState extends State { DateTime time2 = combinedDate.add(const Duration(hours: 2)); - UserModel.getUser(uid.toString()).then((value) { - eventoService - .createEvent( - value.name, - _observacionController.text, - '$_selectedDate', - '$combinedDate', - '$time2', - professionalId, - ubicacion, - professionalAddress, - latUser, - lngUser, - 'pendiente', - professionalTarifa == 0 ? 0 : professionalTarifa, - false, - false, - ) - .then((value) { - if (professionalToken != '') { - sendPushNotification(professionalToken); - } - Navigator.pushReplacement( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return ServiceAfterScreen( - eventoId: value, - ); - }, - ), - ); - }); + eventoService + .createEvent( + user!.name, + _observacionController.text, + '$_selectedDate', + '$combinedDate', + '$time2', + professionalId, + ubicacion, + professionalAddress, + latUser, + lngUser, + 'pendiente', + professionalTarifa == 0 ? 0 : professionalTarifa, + false, + false, + ) + .then((value) { + Navigator.pushReplacement( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return ServiceAfterScreen(eventoId: value); + }, + ), + ); }); } catch (e) { WarningSnackbar.show( title: 'Llena todos los campos', - message: 'Asegurate de llenar todos los campos.', + message: + 'Asegurate de llenar todos los campos.', ); } } diff --git a/lib/src/presentation/screens/solicitudes.dart b/lib/src/presentation/screens/solicitudes.dart index 9f988b6..665be1f 100644 --- a/lib/src/presentation/screens/solicitudes.dart +++ b/lib/src/presentation/screens/solicitudes.dart @@ -1,17 +1,56 @@ -import 'package:cloud_firestore/cloud_firestore.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/authentication/authentication_repository.dart'; import 'package:prosappco/src/components/drawer_professional.dart'; import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/presentation/screens/cita.dart'; +import 'package:prosappco/src/services/api_service.dart'; -class SolicitudScreen extends StatelessWidget { +class SolicitudScreen extends StatefulWidget { SolicitudScreen({super.key}); - DateTime today = DateTime.now(); + @override + State createState() => _SolicitudScreenState(); +} + +class _SolicitudScreenState extends State { + List eventos = []; + bool loading = true; + final myUid = AuthenticationRepository.instance.getCurrentUserUid(); + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + final List data = await ApiService.instance + .get('/services?professionalId=$myUid&status=pendiente'); + final List loaded = []; + for (final e in data) { + final event = Event.fromJson(e as Map); + event.scoresModel = + await ScoresModel.scoreTo(event.userId, false, false); + loaded.add(event); + } + loaded.sort((a, b) { + if (a.timeStamp == null || b.timeStamp == null) return 0; + return a.timeStamp!.compareTo(b.timeStamp!); + }); + if (mounted) setState(() { + eventos = loaded; + loading = false; + }); + } catch (e) { + print('Error loading solicitudes: $e'); + if (mounted) setState(() => loading = false); + } + } @override Widget build(BuildContext context) { @@ -19,176 +58,103 @@ class SolicitudScreen extends StatelessWidget { child: Scaffold( appBar: AppBar( backgroundColor: Colors.white, - iconTheme: const IconThemeData( - color: Colors.black, - ), + iconTheme: const IconThemeData(color: Colors.black), title: const Text( 'Solicitudes', - style: TextStyle( - color: Colors.black, - ), + style: TextStyle(color: Colors.black), ), ), drawer: DrawerProfessional(), - body: SingleChildScrollView( - child: Column( - children: [ - _eventList(), - ], - ), - ), + body: loading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: Column(children: [_eventList(context)]), + ), ), ); } - Widget _eventList() { - return StreamBuilder>( - stream: FirebaseFirestore.instance - .collection('services') - .where('professional_id', isEqualTo: uid) - .where('status', whereIn: ['pendiente', '']) - .snapshots() - .asyncMap((snapshot) async { - try { - List eventos = []; - for (var element in snapshot.docs) { - final event = Event.fromJson(element.data()); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - event.id = element.id; - eventos.add(event); - } - return eventos; - } catch (e) { - print('Error getByProId $e'); - return []; - } - }), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - List eventos = []; - - try { - eventos.addAll(snapshot.data!); - eventos.sort((a, b) => a.timeStamp!.compareTo(b.timeStamp!)); - } catch (e) { - print("Error" + e.toString()); - } - - if (eventos.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Center(child: Text('No tienes citas')), - ); - } - return Column( - children: [ - ...eventos.map( - (event) => ListTile( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return CitaScreen(evento: event); - }, - ), - ); - }, - leading: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(DateFormat('h:mm a') - .format(DateTime.parse(event.range1Hour1))), - ], + Widget _eventList(BuildContext context) { + if (eventos.isEmpty) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 50), + child: Center(child: Text('No tienes citas')), + ); + } + return Column( + children: [ + ...eventos.map( + (event) => ListTile( + onTap: () { + Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return CitaScreen(evento: event); + }, ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', - style: const TextStyle( - color: Colors.grey, - fontSize: 16, - ), - ) - ], - ), - - // RichText( - // text: TextSpan( - // children: [ - // TextSpan( - // text: '${event.title}, ', - // style: const TextStyle( - // color: Colors.black, - // fontWeight: FontWeight.bold, - // fontSize: 16, - // ), - // ), - // TextSpan( - // text: DateFormat('dd MMM', 'es') - // .format(DateTime.parse(event.day)), - // style: const TextStyle( - // color: Colors.grey, - // fontSize: 16, - // ), - // ), - // ], - // ), - // ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - RatingBar.builder( - initialRating: event.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( - '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), - ], - ), - Text( - '"${event.description}"', - style: const TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - trailing: const Icon(Icons.keyboard_arrow_right), - ), + ); + }, + leading: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(DateFormat('h:mm a') + .format(DateTime.parse(event.range1Hour1))), + ], ), - ], - ); - }, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.title, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}', + style: const TextStyle(color: Colors.grey, fontSize: 16), + ) + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + RatingBar.builder( + initialRating: event.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( + '(${event.scoresModel?.total.toString()}) ${event.scoresModel?.average.toStringAsFixed(1)}'), + ], + ), + Text( + '"${event.description}"', + style: const TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + trailing: const Icon(Icons.keyboard_arrow_right), + ), + ), + ], ); } } diff --git a/lib/src/providers/user_provider.dart b/lib/src/providers/user_provider.dart index f884178..d25ca39 100644 --- a/lib/src/providers/user_provider.dart +++ b/lib/src/providers/user_provider.dart @@ -1,54 +1,32 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/models/user_model.dart'; +import 'package:prosappco/src/services/api_service.dart'; class UserProvider extends ChangeNotifier { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final _api = ApiService.instance; UserModel? _user; - ScoresModel? _score; - Stream>>? _stream; UserProvider() { - initUserProvider(); + loadUser(); } - void initUserProvider() { - var uid = FirebaseAuth.instance.currentUser?.uid; - if (uid == null) return; - _stream = _firestore.collection('users').doc(uid).snapshots(); - _stream?.listen((documentSnapshot) async { - if (documentSnapshot.exists) { - final data = documentSnapshot.data() as Map; - _user = UserModel.fromFirestore(data); - _score = await ScoresModel.scoreTo(uid, false, false); - notifyListeners(); - } - }, onDone: () {}, onError: (error) {}); + Future loadUser() async { + final token = await _api.getToken(); + if (token == null) return; + try { + final data = await _api.get('/auth/me'); + _user = UserModel.fromApi(data); + notifyListeners(); + } catch (_) {} } + Future initUserProvider() => loadUser(); + Future updateUserDataAndScores() => loadUser(); + void setNullUser() { _user = null; - _score = null; - _stream = null; notifyListeners(); } - Future updateUserDataAndScores() async { - var uid = FirebaseAuth.instance.currentUser?.uid; - if (uid == null) return; - - var documentSnapshot = await _firestore.collection('users').doc(uid).get(); - - if (documentSnapshot.exists) { - final data = documentSnapshot.data() as Map; - _user = UserModel.fromFirestore(data); - _score = await ScoresModel.scoreTo(uid, false, false); - notifyListeners(); - } - } - UserModel? get user => _user; - ScoresModel? get score => _score; } diff --git a/lib/src/services/api_service.dart b/lib/src/services/api_service.dart new file mode 100644 index 0000000..9f5e570 --- /dev/null +++ b/lib/src/services/api_service.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +const String _baseUrl = 'https://backend.prosapp.co/api/v1'; + +class ApiException implements Exception { + final int statusCode; + final String message; + ApiException(this.statusCode, this.message); + @override + String toString() => 'ApiException($statusCode): $message'; +} + +class ApiService { + static ApiService? _instance; + static ApiService get instance => _instance ??= ApiService._(); + ApiService._(); + + String? _token; + + Future getToken() async { + if (_token != null) return _token; + final prefs = await SharedPreferences.getInstance(); + _token = prefs.getString('token'); + return _token; + } + + Future saveToken(String token) async { + _token = token; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('token', token); + } + + Future clearToken() async { + _token = null; + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('token'); + } + + Future> _headers({bool auth = true}) async { + final headers = {'Content-Type': 'application/json'}; + if (auth) { + final token = await getToken(); + if (token != null) headers['Authorization'] = 'Bearer $token'; + } + return headers; + } + + dynamic _parse(http.Response res) { + final body = jsonDecode(res.body); + if (res.statusCode >= 200 && res.statusCode < 300) return body; + final msg = body is Map ? (body['message'] ?? res.reasonPhrase) : res.reasonPhrase; + throw ApiException(res.statusCode, msg.toString()); + } + + Future get(String path, {bool auth = true}) async { + final res = await http.get( + Uri.parse('$_baseUrl$path'), + headers: await _headers(auth: auth), + ); + return _parse(res); + } + + Future post(String path, Map body, {bool auth = false}) async { + final res = await http.post( + Uri.parse('$_baseUrl$path'), + headers: await _headers(auth: auth), + body: jsonEncode(body), + ); + return _parse(res); + } + + Future patch(String path, Map body, {bool auth = true}) async { + final res = await http.patch( + Uri.parse('$_baseUrl$path'), + headers: await _headers(auth: auth), + body: jsonEncode(body), + ); + return _parse(res); + } + + Future delete(String path, {bool auth = true}) async { + final res = await http.delete( + Uri.parse('$_baseUrl$path'), + headers: await _headers(auth: auth), + ); + return _parse(res); + } +} diff --git a/lib/src/services/firebase_messaging.dart b/lib/src/services/firebase_messaging.dart index 56e5610..adff9fc 100644 --- a/lib/src/services/firebase_messaging.dart +++ b/lib/src/services/firebase_messaging.dart @@ -1,56 +1,6 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:get/get.dart'; - +// ponytail: FCM not applicable on web; stub so imports compile class FirebaseMessagingService { - FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; - Future initializeFirebaseMessaging() async { - // Solicitar permisos de notificación si es necesario (opcional) - NotificationSettings settings = await _firebaseMessaging.requestPermission( - alert: true, - badge: true, - sound: true, - ); - - // Verificar si los permisos de notificación están habilitados - if (settings.authorizationStatus == AuthorizationStatus.authorized || - settings.authorizationStatus == AuthorizationStatus.provisional) { - // Obtener el token de registro para la instancia de la aplicación - String? token = await _firebaseMessaging.getToken(); - print('Token FCM: $token'); - - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - print( - 'Mensaje FCM recibido: ${message.notification?.title} - ${message.notification?.body}'); - - // Obtener el valor de la clave "screen" de los datos de la notificación - String? notificationScreen = message.data['screen']; - - // Navegar a la pantalla correspondiente según el valor de la clave "screen" - if (notificationScreen == "misservicios") { - // Navegar a MyServicesScreen - Get.offNamed('/misservicios'); - } else if (notificationScreen == "solicitud") { - // Navegar a SolicitudScreen - Get.offNamed('/solicitud'); - } - }); - - // Manejar la notificación cuando se toca y la aplicación está en primer plano (opcional) - FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { - print( - 'Mensaje FCM abierto desde la aplicación en primer plano: ${message.notification?.title} - ${message.notification?.body}'); - // Aquí puedes redirigir al usuario a una pantalla específica o realizar acciones según los datos recibidos - }); - - // Manejar la notificación cuando se toca y la aplicación está cerrada (opcional) - RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); - if (initialMessage != null) { - print( - 'Mensaje FCM abierto desde la aplicación cerrada: ${initialMessage.notification?.title} - ${initialMessage.notification?.body}'); - // Aquí puedes redirigir al usuario a una pantalla específica o realizar acciones según los datos recibidos - } - } + // No-op: push notifications handled server-side } } diff --git a/lib/src/services/local_notifications.dart b/lib/src/services/local_notifications.dart index 8a5dbf0..03b5bbb 100644 --- a/lib/src/services/local_notifications.dart +++ b/lib/src/services/local_notifications.dart @@ -1,5 +1,4 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:prosappco/src/services/firebase_messaging.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -20,11 +19,6 @@ Future initNotifications() async { ); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - - // Inicializar Firebase Messaging - FirebaseMessagingService firebaseMessagingService = - FirebaseMessagingService(); - await firebaseMessagingService.initializeFirebaseMessaging(); } Future showNotification(String title, String body) async { diff --git a/pubspec.yaml b/pubspec.yaml index 0e39bd5..b88d1fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,17 +38,13 @@ dependencies: location: ^5.0.3 flutter_polyline_points: ^2.0.0 google_maps_flutter: ^2.5.0 - firebase_auth: ^4.6.2 - firebase_core: ^2.13.1 file_picker: ^5.3.2 - google_sign_in: ^6.1.4 provider: ^6.0.5 package_info_plus: ^4.2.0 get: font_awesome_flutter: ^10.4.0 flutter_otp_text_field: intl_phone_field: - cloud_firestore: diacritic: image_picker: ^1.0.4 flutter_animate: @@ -61,12 +57,10 @@ dependencies: community_material_icon: ^5.9.55 flutter_email_sender: ^5.2.0 webview_flutter: ^4.4.1 - firebase_storage: ^11.2.2 responsive_builder: ^0.7.0 flutter_local_notifications: ^14.1.1 flutter_dialogs: ^3.0.0 universal_html: ^2.2.3 - firebase_messaging: ^14.6.3 animated_splash_screen: ^1.3.0 animate_do: ^3.0.2 shimmer: ^3.0.0