feat: migrate prosapp_web_app from Firebase to NestJS API

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-17 13:19:28 -05:00
co-authored by Claude Sonnet 4.6
parent 1c20d0c0cd
commit b95e0630de
45 changed files with 2531 additions and 5865 deletions
+2 -89
View File
@@ -1,89 +1,2 @@
// File generated by FlutterFire CLI. // ponytail: Firebase removed; this file is kept as an empty stub so any lingering references compile.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members // No imports, no exports needed.
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',
);
}
+4 -51
View File
@@ -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/calendar.dart';
import 'package:prosappco/src/presentation/screens/city.dart'; import 'package:prosappco/src/presentation/screens/city.dart';
import 'package:prosappco/src/presentation/screens/login/login.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/login/login_email.dart';
import 'package:prosappco/src/presentation/screens/my_services.dart'; import 'package:prosappco/src/presentation/screens/my_services.dart';
import 'package:prosappco/src/presentation/screens/my_services_pro.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/reset_password/reset_password.dart';
import 'package:prosappco/src/presentation/screens/map/service.dart'; import 'package:prosappco/src/presentation/screens/map/service.dart';
import 'package:prosappco/src/presentation/screens/solicitudes.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:flutter/foundation.dart' show kIsWeb;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Handling a backdround message: ${message.messageId}');
}
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
).then((value) => Get.put(AuthenticationRepository()));
// Evitar la solicitud de permiso en la versión web Get.put(AuthenticationRepository());
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();
await initializeDateFormatting('es_MX', null); await initializeDateFormatting('es_MX', null);
@@ -82,9 +49,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider( ChangeNotifierProvider(create: (_) => UserProvider()),
create: (_) => UserProvider(),
),
], ],
child: GetMaterialApp( child: GetMaterialApp(
theme: ThemeData(fontFamily: 'Poppins'), theme: ThemeData(fontFamily: 'Poppins'),
@@ -94,9 +59,7 @@ class MyApp extends StatelessWidget {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const [ supportedLocales: const [Locale('es', 'US')],
Locale('es', 'US'),
],
title: 'ProsApp', title: 'ProsApp',
initialRoute: '/', initialRoute: '/',
routes: { routes: {
@@ -108,8 +71,7 @@ class MyApp extends StatelessWidget {
'/city': (context) => const CityScreen(), '/city': (context) => const CityScreen(),
'/profession': (context) => const ProfessionScreen(), '/profession': (context) => const ProfessionScreen(),
'/newNumber': (context) => const NewNumberScreen(), '/newNumber': (context) => const NewNumberScreen(),
'/profesionalRevision': (context) => '/profesionalRevision': (context) => const ProfessionalRevisionScreen(),
const ProfessionalRevisionScreen(),
'/profesionalProfile': (context) => const ProfessionalProfileScreen(), '/profesionalProfile': (context) => const ProfessionalProfileScreen(),
'/solicitudEnviada': (context) => const RequestSentScreen(), '/solicitudEnviada': (context) => const RequestSentScreen(),
'/nuevaPassword': (context) => NewPasswordScreen(), '/nuevaPassword': (context) => NewPasswordScreen(),
@@ -142,7 +104,6 @@ class _SplashScreenState extends State<SplashScreen> {
void initState() { void initState() {
super.initState(); super.initState();
if (kIsWeb) { if (kIsWeb) {
// Espera 3 segundos y luego redirige a LoginScreen
Future.delayed(const Duration(seconds: 3), () { Future.delayed(const Duration(seconds: 3), () {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
@@ -162,11 +123,3 @@ class _SplashScreenState extends State<SplashScreen> {
); );
} }
} }
// splash: 'images/splashgif.gif',
// backgroundColor: Colors.black,
// nextScreen: const LoginScreen(),
// splashIconSize: 50,
// duration: 80000,
@@ -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:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/authentication/exceptions/register_failed.dart';
import 'package:prosappco/src/presentation/screens/login/login.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/map/service.dart';
import 'package:prosappco/src/presentation/screens/service_web.dart'; import 'package:prosappco/src/presentation/screens/service_web.dart';
import 'package:prosappco/src/services/api_service.dart';
class AuthenticationRepository extends GetxController { class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find(); static AuthenticationRepository get instance => Get.find();
//Variables final _api = ApiService.instance;
final _auth = FirebaseAuth.instance;
late final Rx<User?> firebaseUser; final Rx<UserModel?> currentUser = Rx<UserModel?>(null);
final firebase = FirebaseFirestore.instance; var isLoggedIn = false.obs;
final GoogleSignIn googleSignIn = GoogleSignIn();
// final _userRef = firebase
var verificationId = ''.obs;
@override @override
void onReady() { void onReady() {
// Future.delayed(const Duration(seconds: 6)); _checkSession();
firebaseUser = Rx<User?>(_auth.currentUser);
firebaseUser.bindStream(_auth.userChanges());
ever(firebaseUser, _setInitialScreen);
} }
_setInitialScreen(User? user) { Future<void> _checkSession() async {
user == null final token = await _api.getToken();
? Get.offAll(const LoginScreen()) if (token == null) {
: kIsWeb Get.offAll(const LoginScreen());
? Get.offAll(const ServiceWebScreen()) return;
: Get.offAll(const ServiceScreen());
}
Future<void> 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<void> 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<bool> verifyOTP(String otp) async {
var credentials = await _auth.signInWithCredential(
PhoneAuthProvider.credential(
verificationId: verificationId.value, smsCode: otp));
return credentials.user != null ? true : false;
}
Future<void> createUserWithEmailAndPassword(
String email, String password) async {
try { try {
await _auth.createUserWithEmailAndPassword( final data = await _api.get('/auth/me');
email: email, password: password); currentUser.value = UserModel.fromApi(data);
isLoggedIn.value = true;
firebaseUser.value != null _navigateHome();
? 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,
);
} catch (_) { } catch (_) {
const ex = SignUpWithEmailAndPasswordFailure(); await _api.clearToken();
print('EXCEPTION - ${ex.message}'); Get.offAll(const LoginScreen());
throw ex;
} }
} }
void _navigateHome() {
kIsWeb
? Get.offAll(const ServiceWebScreen())
: Get.offAll(const ServiceScreen());
}
Future<void> loginWithEmailAndPassword(String email, String password) async { Future<void> loginWithEmailAndPassword(String email, String password) async {
try { final data = await _api.post('/auth/login', {'email': email, 'password': password});
await _auth.signInWithEmailAndPassword(email: email, password: password); await _api.saveToken(data['access_token']);
} on FirebaseAuthException catch (e) { currentUser.value = UserModel.fromApi(data['user']);
if (e.code == 'wrong-password') { isLoggedIn.value = true;
Get.snackbar( _navigateHome();
'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 (_) {}
} }
Future<void> signInWithGoogle() async { Future<void> createUserWithEmailAndPassword(String email, String password, String name) async {
try { final data = await _api.post('/auth/register', {
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); 'email': email,
'password': password,
if (googleUser != null) { 'name': name,
final GoogleSignInAuthentication googleAuth = });
await googleUser.authentication; await _api.saveToken(data['access_token']);
final OAuthCredential credential = GoogleAuthProvider.credential( currentUser.value = UserModel.fromApi(data['user']);
accessToken: googleAuth.accessToken, isLoggedIn.value = true;
idToken: googleAuth.idToken, _navigateHome();
);
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<void> logout(String uid) async { Future<void> phoneAuthentication(String phone, {String? name}) async {
try { final data = await _api.post('/auth/phone', {'phone': phone, 'name': name ?? phone});
await FirebaseFirestore.instance await _api.saveToken(data['access_token']);
.collection('users') currentUser.value = UserModel.fromApi(data['user']);
.doc(uid) isLoggedIn.value = true;
.update({'token': FieldValue.delete()}); _navigateHome();
} catch (e) {
print(e);
}
_auth.signOut();
} }
String? getCurrentUserPhone() { Future<void> logout() async {
final User? user = _auth.currentUser; await _api.clearToken();
return user?.phoneNumber; currentUser.value = null;
isLoggedIn.value = false;
Get.offAll(const LoginScreen());
} }
String? getCurrentUserUid() { String? getCurrentUserUid() => currentUser.value?.id;
final User? user = _auth.currentUser; String? getCurrentUserPhone() => currentUser.value?.phoneNumber;
return user?.uid;
}
Future<String> getCity(String uid) async {
String city = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
city = data?['city'] ?? '';
} catch (e) {
print('Error getting city: $e');
}
return city;
}
Future<String> getGender(String uid) async {
String gender = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
gender = data?['gender'] ?? '';
} catch (e) {
print('Error getting gender: $e');
}
return gender;
}
Future<String> getBirthday(String uid) async {
String birthDate = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
birthDate = data?['birth_date'] ?? '';
} catch (e) {
print('Error getting birthDate: $e');
}
return birthDate;
}
Future<String> getCoordsOfCity(String uid) async {
String coords = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? 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<String> getAddress(String uid) async {
String address = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
address = data?['address'] ?? '';
} catch (e) {
print('Error getting address: $e');
}
return address;
}
Future<String> getUbicacion(String uid) async {
String location = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
location = data?['ubicacion'] ?? '';
} catch (e) {
print('Error getting address: $e');
}
return location;
}
Future<String> getOpcionalAddress(String uid) async {
String opcional_location = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
opcional_location = data?['opcional_address'] ?? '';
} catch (e) {
print('Error getting address: $e');
}
return opcional_location;
}
Future<int> getTarifa(String uid) async {
int tarifa = 0;
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
tarifa = data?['tarifas'] ?? 0;
} catch (e) {
print('Error getting tarifa: $e');
}
return tarifa;
}
Future<String> getPhoto(String uid) async {
String photo = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
photo = data?['photo'] ?? '';
} catch (e) {
print('Error getting photo: $e');
}
return photo;
}
Future<String> getBanner(String uid) async {
String photo = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
photo = data?['banner'] ?? '';
} catch (e) {
print('Error getting banner: $e');
}
return photo;
}
Future<String> getProfession(String uid) async {
String profession = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
profession = data?['profesion'] ?? '';
} catch (e) {
print('Error getting profesion: $e');
}
return profession;
}
Future<String> getState(String uid) async {
String state = '';
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
state = data?['estado'] ?? '';
} catch (e) {
print('Error getting estado: $e');
}
return state;
}
} }
+27 -47
View File
@@ -1,66 +1,45 @@
import 'dart:io'; import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:prosappco/src/components/column_padding.dart'; import 'package:prosappco/src/components/column_padding.dart';
const double photoSize = 150; const double photoSize = 150;
class ReferenceBannerPhoto extends StatelessWidget { class ReferenceBannerPhoto extends StatelessWidget {
Reference? ref; /// Accepts either a String URL or null.
double size; final Object? ref;
double sizeCircle; final double size;
final double sizeCircle;
ReferenceBannerPhoto({ const ReferenceBannerPhoto({
super.key, super.key,
required this.ref, required this.ref,
this.size = photoSize, this.size = photoSize,
this.sizeCircle = photoSize, this.sizeCircle = photoSize,
}); });
Future<Widget> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<Widget>( final url = ref is String ? ref as String : null;
future: downloadImage(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { if (url == null || url.isEmpty || url.startsWith('...')) {
if (snapshot.connectionState == ConnectionState.waiting) { return DefaultPhoto(sizeDefault: sizeCircle);
// mientras la llamada asíncrona está en proceso, muestra un mensaje de carga }
return DefaultPhoto();
} else if (snapshot.connectionState == ConnectionState.done && return Image.network(
snapshot.hasData) { url,
return snapshot.data!; width: double.infinity,
} else { height: size,
return DefaultPhoto(); fit: BoxFit.fill,
} errorBuilder: (_, __, ___) => DefaultPhoto(sizeDefault: sizeCircle),
}); );
} }
} }
class DefaultPhoto extends StatelessWidget { class DefaultPhoto extends StatelessWidget {
double sizeDefault; final double sizeDefault;
DefaultPhoto({
const DefaultPhoto({
super.key, super.key,
this.sizeDefault = photoSize, this.sizeDefault = photoSize,
}); });
@@ -92,14 +71,14 @@ class DefaultPhoto extends StatelessWidget {
color: Colors.grey.withOpacity(0.3), color: Colors.grey.withOpacity(0.3),
spreadRadius: 2, spreadRadius: 2,
blurRadius: 5, blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow offset: const Offset(0, 3),
), ),
], ],
), ),
constraints: const BoxConstraints(minWidth: 250, minHeight: 50), constraints: const BoxConstraints(minWidth: 250, minHeight: 50),
child: Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: [
Expanded( Expanded(
child: Center( child: Center(
child: Text( child: Text(
@@ -129,8 +108,9 @@ class DefaultPhoto extends StatelessWidget {
} }
class LocalPhoto extends StatelessWidget { class LocalPhoto extends StatelessWidget {
File file; final File file;
LocalPhoto({super.key, required this.file});
const LocalPhoto({super.key, required this.file});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
+9 -35
View File
@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -25,7 +24,6 @@ class DrawerProfessional extends StatefulWidget {
class _DrawerProfessionalState extends State<DrawerProfessional> { class _DrawerProfessionalState extends State<DrawerProfessional> {
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
ScoresModel? scoresModel; ScoresModel? scoresModel;
Future<void> _irSugerencias() async { Future<void> _irSugerencias() async {
@@ -41,12 +39,6 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
void initState() { void initState() {
super.initState(); super.initState();
if (user == null) {
UserModel.getUser(uid.toString()).then(
(UserModel s) => setState(() => user = s),
);
}
if (scoresModel == null) { if (scoresModel == null) {
ScoresModel.scoreTo(uid.toString(), true, false).then( ScoresModel.scoreTo(uid.toString(), true, false).then(
(ScoresModel s) => setState(() => scoresModel = s), (ScoresModel s) => setState(() => scoresModel = s),
@@ -56,7 +48,9 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
@override @override
Widget build(BuildContext context) { 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( return Drawer(
child: Container( child: Container(
@@ -78,7 +72,7 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
), ),
); );
}, },
title: Text(user?.name ?? '', title: Text(currentUser?.name ?? '',
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -88,13 +82,13 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
Text( Text(
user?.city ?? '', currentUser?.city ?? '',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
], ],
), ),
leading: ReferencePhoto( leading: ReferencePhoto(
ref: user?.photo, ref: currentUser?.picture,
size: 55, size: 55,
sizeCircle: 60, sizeCircle: 60,
), ),
@@ -113,7 +107,7 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
color: Colors.grey.withOpacity(0.3), color: Colors.grey.withOpacity(0.3),
spreadRadius: 1, spreadRadius: 1,
blurRadius: 3, blurRadius: 3,
offset: const Offset(0, 0), // changes position of shadow offset: const Offset(0, 0),
), ),
], ],
), ),
@@ -234,25 +228,6 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
style: TextStyle(fontSize: 15), 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( ListTile(
onTap: () { onTap: () {
Navigator.push( Navigator.push(
@@ -308,8 +283,7 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
color: Colors.grey.withOpacity(0.5), color: Colors.grey.withOpacity(0.5),
spreadRadius: 2, spreadRadius: 2,
blurRadius: 3, blurRadius: 3,
offset: offset: const Offset(0, 2),
const Offset(0, 2), // changes position of shadow
), ),
], ],
), ),
@@ -407,7 +381,7 @@ class _DrawerProfessionalState extends State<DrawerProfessional> {
const SizedBox(height: 5), const SizedBox(height: 5),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
AuthenticationRepository.instance.logout(uid!); AuthenticationRepository.instance.logout();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Colors.red,
+30 -55
View File
@@ -1,19 +1,19 @@
import 'dart:io'; import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
const double photoSize = 100; const double photoSize = 100;
const double iconSize = 35; const double iconSize = 35;
class ReferencePhoto extends StatelessWidget { class ReferencePhoto extends StatelessWidget {
Reference? ref; /// Accepts either a String URL or null.
double size; final Object? ref;
double sizeIcon; final double size;
double sizeCircle; final double sizeIcon;
ReferencePhoto({ final double sizeCircle;
const ReferencePhoto({
super.key, super.key,
required this.ref, required this.ref,
this.sizeIcon = iconSize, this.sizeIcon = iconSize,
@@ -21,58 +21,32 @@ class ReferencePhoto extends StatelessWidget {
this.sizeCircle = photoSize, this.sizeCircle = photoSize,
}); });
Future<Widget> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<Widget>( final url = ref is String ? ref as String : null;
future: downloadImage(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { if (url == null || url.isEmpty || url.startsWith('...')) {
if (snapshot.connectionState == ConnectionState.waiting) { return DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon);
// mientras la llamada asíncrona está en proceso, muestra un mensaje de carga }
return SizedBox(
width: size, return ClipOval(
height: size, child: Image.network(
child: Center( url,
child: DefaultPhoto(), width: size,
), height: size,
); fit: BoxFit.cover,
} else if (snapshot.connectionState == ConnectionState.done && errorBuilder: (_, __, ___) =>
snapshot.hasData) { DefaultPhoto(sizeDefault: sizeCircle, iconDefault: sizeIcon),
return snapshot.data!; ),
} else { );
return DefaultPhoto();
}
});
} }
} }
class DefaultPhoto extends StatelessWidget { class DefaultPhoto extends StatelessWidget {
double sizeDefault; final double sizeDefault;
double iconDefault; final double iconDefault;
DefaultPhoto({
const DefaultPhoto({
super.key, super.key,
this.sizeDefault = photoSize, this.sizeDefault = photoSize,
this.iconDefault = iconSize, this.iconDefault = iconSize,
@@ -97,8 +71,9 @@ class DefaultPhoto extends StatelessWidget {
} }
class LocalPhoto extends StatelessWidget { class LocalPhoto extends StatelessWidget {
File file; final File file;
LocalPhoto({super.key, required this.file});
const LocalPhoto({super.key, required this.file});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
+28 -56
View File
@@ -1,18 +1,16 @@
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
const double photoSize = 100; const double photoSize = 100;
const double iconSize = 55; const double iconSize = 55;
class ReferencePhotoWeb extends StatelessWidget { class ReferencePhotoWeb extends StatelessWidget {
Reference? ref; /// Accepts either a String URL or null.
double size; final Object? ref;
double sizeIcon; final double size;
double sizeCircle; final double sizeIcon;
final double sizeCircle;
ReferencePhotoWeb({ const ReferencePhotoWeb({
super.key, super.key,
required this.ref, required this.ref,
this.sizeIcon = iconSize, this.sizeIcon = iconSize,
@@ -20,59 +18,32 @@ class ReferencePhotoWeb extends StatelessWidget {
this.sizeCircle = photoSize, this.sizeCircle = photoSize,
}); });
Future<Widget> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<Widget>( final url = ref is String ? ref as String : null;
future: downloadImage(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) { if (url == null || url.isEmpty || url.startsWith('...')) {
if (snapshot.connectionState == ConnectionState.waiting) { return DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon);
// mientras la llamada asíncrona está en proceso, muestra un mensaje de carga }
return SizedBox(
width: size, return ClipOval(
height: size, child: Image.network(
child: const Center( url,
child: CircularProgressIndicator(), width: size,
), height: size,
); fit: BoxFit.cover,
} else if (snapshot.connectionState == ConnectionState.done && errorBuilder: (_, __, ___) =>
snapshot.hasData) { DefaultPhotoWeb(sizeDefault: sizeCircle, iconDefault: sizeIcon),
return snapshot.data!; ),
} else { );
return DefaultPhotoWeb();
}
});
} }
} }
class DefaultPhotoWeb extends StatelessWidget { class DefaultPhotoWeb extends StatelessWidget {
double sizeDefault; final double sizeDefault;
double iconDefault; final double iconDefault;
DefaultPhotoWeb({ const DefaultPhotoWeb({
super.key, super.key,
this.sizeDefault = photoSize, this.sizeDefault = photoSize,
this.iconDefault = iconSize, this.iconDefault = iconSize,
@@ -97,8 +68,9 @@ class DefaultPhotoWeb extends StatelessWidget {
} }
class LocalPhotoWeb extends StatelessWidget { class LocalPhotoWeb extends StatelessWidget {
Uint8List? file; final Uint8List? file;
LocalPhotoWeb({super.key, required this.file});
const LocalPhotoWeb({super.key, required this.file});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
+15 -17
View File
@@ -1,6 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prosappco/src/services/api_service.dart';
typedef TimeCallback = void Function(TimeOfDay? pickedTime); typedef TimeCallback = void Function(TimeOfDay? pickedTime);
@@ -185,28 +185,26 @@ class Schedule {
'range2Hour1: $range2Hour1, range2Hour2: $range2Hour2)'; 'range2Hour1: $range2Hour1, range2Hour2: $range2Hour2)';
} }
static Map<String, Schedule> _emptySchedules() => {
for (var i = 1; i <= 7; i++)
'$i': Schedule(false, false, null, null, null, null),
};
static Future<Map<String, Schedule>> getHorarios(String uid) async { static Future<Map<String, Schedule>> getHorarios(String uid) async {
try { try {
final snapshot = final Map<String, dynamic> data =
await FirebaseFirestore.instance.collection('users').doc(uid).get(); await ApiService.instance.get('/users/$uid');
final Map<String, dynamic>? data = snapshot.data(); final Map<String, dynamic>? horarioData =
final Map<String, dynamic>? horarioData = data?['horario']; data['horario'] as Map<String, dynamic>?;
if (horarioData == null) return _emptySchedules();
final horarios = <String, Schedule>{}; final horarios = <String, Schedule>{};
horarioData?.forEach((key, value) { horarioData.forEach((key, value) {
horarios[key] = Schedule.fromJson(value); horarios[key] = Schedule.fromJson(value as Map<String, dynamic>);
}); });
return horarios; return horarios;
} catch (e) { } catch (e) {
print('Error getting user: $e'); print('Error getting horarios: $e');
return { return _emptySchedules();
"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),
};
} }
} }
} }
+12 -103
View File
@@ -1,116 +1,25 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:prosappco/src/services/api_service.dart';
class NewPhoneController extends GetxController { class NewPhoneController extends GetxController {
final FirebaseAuth _auth = FirebaseAuth.instance;
final newPhoneNo = TextEditingController(text: ''); final newPhoneNo = TextEditingController(text: '');
final otpCode = TextEditingController(text: ''); final otpCode = TextEditingController(text: '');
Future<void> updatePhoneNumber(newPhoneNo) async { Future<void> updatePhoneNumber(String newPhone) async {
final currentUser = _auth.currentUser; try {
await ApiService.instance.patch('/users/me', {'phone': newPhone});
if (currentUser?.phoneNumber == newPhoneNo) {
Get.snackbar( Get.snackbar(
'Ya estas registrado', 'Número de teléfono actualizado',
'Este es tu numero actual.', '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, 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,
);
}
} }
} }
} }
+3 -2
View File
@@ -7,9 +7,10 @@ class RegisterController extends GetxController {
final email = TextEditingController(); final email = TextEditingController();
final password = TextEditingController(); final password = TextEditingController();
final name = TextEditingController();
Future<void> registerUser(String email, String password) async { Future<void> registerUser(String email, String password, String name) async {
await AuthenticationRepository.instance await AuthenticationRepository.instance
.createUserWithEmailAndPassword(email, password); .createUserWithEmailAndPassword(email, password, name);
} }
} }
+45 -84
View File
@@ -1,5 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosappco/src/models/user_model.dart'; import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class ChatModel { class ChatModel {
List<MessageModel> messages; List<MessageModel> messages;
@@ -9,99 +9,60 @@ class ChatModel {
UserModel? professional; UserModel? professional;
String id; String id;
ChatModel( ChatModel({
{required this.messages, required this.messages,
required this.professional_id, required this.professional_id,
required this.user_id, required this.user_id,
this.user, this.user,
this.professional, this.professional,
required this.id}); required this.id,
});
static Future<ChatModel> fromDocumentSnapshot2( static ChatModel fromJson(Map<String, dynamic> data) {
DocumentSnapshot<Map<String, dynamic>> snapshot, final List<dynamic> msgs = data['messages'] ?? data['message'] ?? [];
bool fillUserModel) async { 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<List<ChatModel>> getChatsByProId(String userId) async {
try { try {
List<MessageModel> messages = []; final List<dynamic> data =
List<dynamic> messagesData = snapshot.get('message') ?? []; await ApiService.instance.get('/chat?professionalId=$userId');
return data
for (var data in messagesData) { .map((e) => ChatModel.fromJson(e as Map<String, dynamic>))
messages.add(MessageModel( .toList();
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);
} catch (e) { } catch (e) {
print('error $e'); print('error getChatsByProId $e');
return ChatModel(messages: [], professional_id: '', user_id: '', id: ''); return [];
} }
} }
static ChatModel fromDocumentSnapshot( static Future<List<ChatModel>> getChatsByUserId(String userId) async {
DocumentSnapshot<Map<String, dynamic>> snapshot,
) {
try { try {
List<MessageModel> messages = []; final List<dynamic> data =
List<dynamic> messagesData = snapshot.get('message') ?? []; await ApiService.instance.get('/chat?userId=$userId');
return data
for (var data in messagesData) { .map((e) => ChatModel.fromJson(e as Map<String, dynamic>))
messages.add(MessageModel( .toList();
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);
} catch (e) { } catch (e) {
print('error $e'); print('error getChatsByUserId $e');
return ChatModel(messages: [], professional_id: '', user_id: '', id: ''); return [];
} }
} }
static Future<List<ChatModel>> 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<List<ChatModel>> 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 { class MessageModel {
+68 -106
View File
@@ -1,6 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/services/api_service.dart';
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
@@ -22,10 +22,7 @@ class EventoService {
bool userScored, bool userScored,
) async { ) async {
try { try {
DateTime ahora = DateTime.now(); final Map<String, dynamic> result = await ApiService.instance.post('/services', {
final eventId =
await FirebaseFirestore.instance.collection('services').add({
'user_id': uid, 'user_id': uid,
'title': title, 'title': title,
'description': description, 'description': description,
@@ -38,41 +35,27 @@ class EventoService {
'latitude': latitude, 'latitude': latitude,
'longitude': longitude, 'longitude': longitude,
'status': status, 'status': status,
'Timestamp': ahora,
'tarifa': tarifa ?? 0, 'tarifa': tarifa ?? 0,
'professional_scored': professionalScored, 'professional_scored': professionalScored,
'user_scored': userScored, 'user_scored': userScored,
}).then((value) {
FirebaseFirestore.instance.collection('users').doc(uid).update({
'services': FieldValue.arrayUnion([value.id])
});
return value.id;
}); });
return result['id']?.toString();
return eventId;
} catch (e) { } catch (e) {
print('Evento $e'); print('Evento $e');
return null;
} }
return null;
} }
} }
Future<List<Event>> getByProId(String day) async { Future<List<Event>> getByProId(String day) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data = await ApiService.instance
.collection('services') .get('/services?day=$day&status=aprobado');
// .where('professional_id', isEqualTo: uid)
.where('day', isEqualTo: day.toString())
.where('status', isEqualTo: 'aprobado')
// .orderBy('Timestamp', descending: true)
.get();
List<Event> eventos = []; List<Event> eventos = [];
for (var element in snapshot.docs) { for (var element in data) {
final event = Event.fromJson(element.data()); final event = Event.fromJson(element as Map<String, dynamic>);
event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); event.scoresModel =
event.id = element.id; await ScoresModel.scoreTo(event.userId, false, false);
eventos.add(event); eventos.add(event);
} }
return eventos; return eventos;
@@ -84,42 +67,36 @@ Future<List<Event>> getByProId(String day) async {
Future<List<Event>> getByProIdAll(String state1, String state2) async { Future<List<Event>> getByProIdAll(String state1, String state2) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data = await ApiService.instance
.collection('services') .get('/services?professionalId=$uid&status=$state1,$state2');
.where('professional_id', isEqualTo: uid)
.where('status', whereIn: [state1, state2]).get();
List<Event> eventos = []; List<Event> eventos = [];
for (var element in snapshot.docs) { for (var element in data) {
final event = Event.fromJson(element.data()); final event = Event.fromJson(element as Map<String, dynamic>);
event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); event.scoresModel =
event.id = element.id; await ScoresModel.scoreTo(event.userId, false, false);
eventos.add(event); eventos.add(event);
} }
return eventos; return eventos;
} catch (e) { } catch (e) {
print('Error getByProId $e'); print('Error getByProIdAll $e');
return []; return [];
} }
} }
Future<List<Event>> getByUserIdAll(String state1, String state2) async { Future<List<Event>> getByUserIdAll(String state1, String state2) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data = await ApiService.instance
.collection('services') .get('/services?userId=$uid&status=$state1,$state2');
.where('user_id', isEqualTo: uid)
.where('status', whereIn: [state1, state2]).get();
List<Event> eventos = []; List<Event> eventos = [];
for (var element in snapshot.docs) { for (var element in data) {
final event = Event.fromJson(element.data()); final event = Event.fromJson(element as Map<String, dynamic>);
event.scoresModel = await ScoresModel.scoreTo(event.userId, false, false); event.scoresModel =
event.id = element.id; await ScoresModel.scoreTo(event.userId, false, false);
eventos.add(event); eventos.add(event);
} }
return eventos; return eventos;
} catch (e) { } catch (e) {
print('Error getByUserId $e'); print('Error getByUserIdAll $e');
return []; return [];
} }
} }
@@ -142,7 +119,7 @@ class Event {
int? tarifa; int? tarifa;
bool professionalScored; bool professionalScored;
bool userScored; bool userScored;
Timestamp? timeStamp; DateTime? timeStamp;
Event({ Event({
this.id, this.id,
@@ -165,94 +142,79 @@ class Event {
}); });
factory Event.fromJson(Map<String, dynamic> json) { factory Event.fromJson(Map<String, dynamic> json) {
DateTime? ts;
final raw = json['Timestamp'] ?? json['created_at'];
if (raw is String) ts = DateTime.tryParse(raw);
return Event( return Event(
id: json['id'] ?? '', id: json['id']?.toString() ?? '',
title: json['title'], title: json['title'] ?? '',
description: json['description'], description: json['description'],
day: json['day'], day: json['day'] ?? '',
range1Hour1: json['range1Hour1'], range1Hour1: json['range1Hour1'] ?? '',
range1Hour2: json['range1Hour2'], range1Hour2: json['range1Hour2'],
userId: json['user_id'], userId: json['user_id'] ?? '',
professionalId: json['professional_id'], professionalId: json['professional_id'] ?? '',
ubicacion: json['ubicacion'] ?? '', ubicacion: json['ubicacion'] ?? '',
address: json['address'] ?? '', address: json['address'] ?? '',
longitud: json['longitude'] ?? 0, longitud: (json['longitude'] ?? 0).toDouble(),
latitud: json['latitude'] ?? 0, latitud: (json['latitude'] ?? 0).toDouble(),
status: json['status'], status: json['status'] ?? 'pendiente',
timeStamp: json['Timestamp'] ?? 0, timeStamp: ts,
tarifa: json['tarifas'] ?? 0, tarifa: json['tarifa'] ?? json['tarifas'] ?? 0,
professionalScored: json['professional_scored'], professionalScored: json['professional_scored'] ?? false,
userScored: json['user_scored'], userScored: json['user_scored'] ?? false,
); );
} }
static Future<Event> getEventById(String uid) async { static Future<Event> getEventById(String eventId) async {
try { try {
final snapshot = await FirebaseFirestore.instance final Map<String, dynamic> data =
.collection('services') await ApiService.instance.get('/services/$eventId');
.doc(uid) return Event.fromJson(data);
.get();
final Map<String, dynamic>? data = snapshot.data();
return Event.fromJson(data!);
} catch (e) { } catch (e) {
print('Error getting user: $e'); print('Error getting event: $e');
return Event( return Event(
title: '', day: '', range1Hour1: '', userId: '', professionalId: ''); title: '', day: '', range1Hour1: '', userId: '', professionalId: '');
} }
} }
static Future<List<Event>> getEventsAllById(String uid) async { static Future<List<Event>> getEventsAllById(String proId) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data =
.collection('services') await ApiService.instance.get('/services?professionalId=$proId');
.where('professional_id', isEqualTo: uid) return data
.get(); .map((e) => Event.fromJson(e as Map<String, dynamic>))
.toList();
List<Event> eventos = [];
for (var element in snapshot.docs) {
eventos.add(Event.fromJson(element.data()));
}
return eventos;
} catch (e) { } catch (e) {
print('Error getByProId $e'); print('Error getEventsAllById $e');
return []; return [];
} }
} }
static Future<List<Event>> getEventsAllByIdAndStatus(String uid) async { static Future<List<Event>> getEventsAllByIdAndStatus(String proId) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data = await ApiService.instance
.collection('services') .get('/services?professionalId=$proId&status=aprobado,pendiente');
.where('professional_id', isEqualTo: uid) return data
.where('status', whereIn: ['aprobado', 'pendiente']).get(); .map((e) => Event.fromJson(e as Map<String, dynamic>))
.toList();
List<Event> eventos = [];
for (var element in snapshot.docs) {
eventos.add(Event.fromJson(element.data()));
}
return eventos;
} catch (e) { } catch (e) {
print('Error getByProId $e'); print('Error getEventsAllByIdAndStatus $e');
return []; return [];
} }
} }
static Future<List<Event>> getEventsAllByIdStatus( static Future<List<Event>> getEventsAllByIdStatus(
String uid, String state) async { String proId, String state) async {
try { try {
var snapshot = await FirebaseFirestore.instance final List<dynamic> data = await ApiService.instance
.collection('services') .get('/services?professionalId=$proId&status=$state');
.where('professional_id', isEqualTo: uid) return data
.where('status', isEqualTo: state) .map((e) => Event.fromJson(e as Map<String, dynamic>))
.get(); .toList();
List<Event> eventos = [];
for (var element in snapshot.docs) {
eventos.add(Event.fromJson(element.data()));
}
return eventos;
} catch (e) { } catch (e) {
print('Error getByProId $e'); print('Error getEventsAllByIdStatus $e');
return []; return [];
} }
} }
+29 -40
View File
@@ -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/models/scores_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class Professional { class Professional {
final String id; final String id;
final Reference professionalRef; final String? photoUrl;
final String name; final String name;
final String professionName; final String professionName;
final String cityName; final String cityName;
@@ -20,7 +18,7 @@ class Professional {
Professional({ Professional({
required this.id, required this.id,
required this.professionalRef, this.photoUrl,
required this.name, required this.name,
required this.professionName, required this.professionName,
required this.cityName, required this.cityName,
@@ -40,48 +38,39 @@ class Professional {
@override @override
String toString() { 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<Professional?> getProfessional(String uid) async { static Future<Professional?> getProfessional(String uid) async {
var photo = '...';
final FirebaseStorage storage = FirebaseStorage.instance;
try { try {
DocumentSnapshot user = final Map<String, dynamic> data =
await FirebaseFirestore.instance.collection('users').doc(uid).get(); await ApiService.instance.get('/professionals/$uid');
Map<String, dynamic> data = user.data() as Map<String, dynamic>; final List<String> especializaciones =
((data['especialidades'] ?? data['specialties'] ?? []) as List<dynamic>)
.map((e) => e.toString())
.toList();
photo = await AuthenticationRepository.instance.getPhoto(user.id); return Professional(
id: uid,
if (data['estado'] == 'activo') { photoUrl: data['picture'] ?? data['photo_url'],
List<String> especializaciones; name: data['name'] ?? '',
professionName: data['profession'] ?? data['profesion'] ?? '',
especializaciones = (data['especializaciones'] as List<dynamic>) cityName: data['city'] ?? '',
.map((e) => e.toString()) professionalEspecializado: especializaciones,
.toList(); ubicacion: data['ubicacion'] ?? '',
realAddress: data['address'] ?? '',
Professional professional = Professional( latitude: (data['latitude'] ?? 0).toDouble(),
id: user.id, longitude: (data['longitude'] ?? 0).toDouble(),
name: data['name'], scores: await ScoresModel.scoreTo(uid, true, false),
professionName: data['profesion'], tarifa: data['rate'] != null
cityName: data['city'], ? double.tryParse(data['rate'].toString())?.toInt()
professionalRef: storage.ref().child(photo), : data['tarifas'],
professionalEspecializado: especializaciones, token: data['token'],
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;
}
} catch (e) { } catch (e) {
print('Error al obtener profesionales: $e'); print('Error al obtener profesional: $e');
return null;
} }
return null;
} }
} }
+34 -62
View File
@@ -1,6 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:prosappco/src/services/api_service.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:prosappco/src/models/user_model.dart';
class ScoresModel { class ScoresModel {
late int total; late int total;
@@ -18,47 +16,30 @@ class ScoresModel {
} }
double averageScore(List<ScoreDetailModel> details) { double averageScore(List<ScoreDetailModel> details) {
if (details.isEmpty) { if (details.isEmpty) return 0.0;
return 0.0; final sum = details.map((d) => d.score).reduce((a, b) => a + b);
}
final sum = details.map((detail) => detail.score).reduce((a, b) => a + b);
return sum / details.length; return sum / details.length;
} }
// Me
static Future<ScoresModel> scoreTo( static Future<ScoresModel> scoreTo(
String? userReceived, bool isFromClient, bool userInfo) async { String? userId, bool isFromClient, bool userInfo) async {
final receivedScoresQuery = FirebaseFirestore.instance if (userId == null || userId.isEmpty) return ScoresModel([]);
.collection('scores') try {
.where('is_from_professional', isEqualTo: isFromClient) final List<dynamic> data =
.where('to_user', isEqualTo: userReceived); await ApiService.instance.get('/comments/reputation/$userId');
final details =
final receivedScoresSnapshot = await receivedScoresQuery.get(); data.map((e) => ScoreDetailModel.fromJson(e as Map<String, dynamic>)).toList();
return ScoresModel(details);
final receivedScores = await Future.wait(receivedScoresSnapshot.docs } catch (e) {
.map((doc) async => print('Error scoreTo: $e');
await ScoreDetailModel.fromDocumentSnapshot(doc, userInfo)) return ScoresModel([]);
.toList()); }
return ScoresModel(receivedScores);
} }
// You // ponytail: scoreFrom and scoreTo now both hit the same reputation endpoint
static Future<ScoresModel> scoreFrom( static Future<ScoresModel> scoreFrom(
String userGiven, bool isFromClient, bool userInfo) async { String userId, bool isFromClient, bool userInfo) async {
final givenScoresQuery = FirebaseFirestore.instance return scoreTo(userId, isFromClient, userInfo);
.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);
} }
} }
@@ -69,9 +50,8 @@ class ScoreDetailModel {
final String toUser; final String toUser;
final String comment; final String comment;
final bool isFromClient; final bool isFromClient;
final String name; final String name;
final Reference? avatar; final String? avatar;
ScoreDetailModel({ ScoreDetailModel({
required this.id, required this.id,
@@ -81,32 +61,24 @@ class ScoreDetailModel {
required this.comment, required this.comment,
required this.isFromClient, required this.isFromClient,
required this.name, required this.name,
required this.avatar, this.avatar,
}); });
factory ScoreDetailModel.fromJson(Map<String, dynamic> 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 @override
String toString() { String toString() {
return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, toUser: $toUser, comment: $comment, isFromClient: $isFromClient, name: $name, avatar: $avatar}'; return 'ScoreDetailModel{id: $id, score: $score, fromUser: $fromUser, name: $name}';
}
static Future<ScoreDetailModel> fromDocumentSnapshot(
DocumentSnapshot<Map<String, dynamic>> snapshot, bool userInfo) async {
try {
final Map<String, dynamic> 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;
}
} }
} }
+13 -46
View File
@@ -1,4 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:prosappco/src/services/api_service.dart';
class SettingModel { class SettingModel {
final bool domicilios; final bool domicilios;
@@ -29,25 +29,12 @@ class SettingModel {
this.terminosCondiciones, this.terminosCondiciones,
); );
static Future<SettingModel> fromJson(Map<String, dynamic>? json) async { static SettingModel _empty() =>
try { SettingModel(false, false, false, '', '', '', '', '', '', '', '', '');
if (json == null) {
return SettingModel(
false,
false,
false,
'',
'',
'',
'',
'',
'',
'',
'',
'',
);
}
static Future<SettingModel> fromJson(Map<String, dynamic>? json) async {
if (json == null) return _empty();
try {
return SettingModel( return SettingModel(
json['domicilios'] ?? false, json['domicilios'] ?? false,
json['google'] ?? false, json['google'] ?? false,
@@ -58,50 +45,30 @@ class SettingModel {
json['email_soporte'] ?? '', json['email_soporte'] ?? '',
json['dias_soporte'] ?? '', json['dias_soporte'] ?? '',
json['horas_soporte'] ?? '', json['horas_soporte'] ?? '',
json['version'] ?? '', // Asegúrate de manejar nulos aquí json['version'] ?? '',
json['politicas_privacidad'] ?? '', json['politicas_privacidad'] ?? '',
json['terminos_condiciones'] ?? '', json['terminos_condiciones'] ?? '',
); );
} catch (e) { } catch (e) {
print('Error settings: $e'); print('Error settings: $e');
return SettingModel( return _empty();
false,
false,
false,
'',
'',
'',
'',
'',
'',
'',
'',
'',
);
} }
} }
static Future<SettingModel> getSettings() async { static Future<SettingModel> getSettings() async {
try { try {
final DocumentSnapshot<Map<String, dynamic>> snapshot = final Map<String, dynamic> data =
await FirebaseFirestore.instance await ApiService.instance.get('/settings');
.collection('settings') return fromJson(data);
.doc('global')
.get();
final Map<String, dynamic>? data = snapshot.data();
return fromJson(data!);
} catch (e) { } catch (e) {
print('Error getting settings: $e'); print('Error getting settings: $e');
return SettingModel( return _empty();
false, false, false, '', '', '', '', '', '', '', '', '');
} }
} }
@override @override
String toString() { String toString() {
return 'SettingModel { domicilios: $domicilios,google: $google, tarifas: $tarifas, ' return 'SettingModel { domicilios: $domicilios, google: $google, tarifas: $tarifas, '
'titulo: $titulo, parrafo: $parrafo, numero: $numero, ' 'titulo: $titulo, parrafo: $parrafo, numero: $numero, '
'email: $email, dias: $dias, horas: $horas, ' 'email: $email, dias: $dias, horas: $horas, '
'version: $version, politicasPrivacidad: $proliticasPrivacidad, ' 'version: $version, politicasPrivacidad: $proliticasPrivacidad, '
+41 -63
View File
@@ -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 { class UserModel {
final String id;
final String name; final String name;
final String city; final String city;
final String? profession; final String? profession;
final String? state; final int proState;
final Reference? photo; final String? picture;
final String? banner;
final int? tarifa; final int? tarifa;
final String? phoneNumber; 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, UserModel({
this.tarifa, this.phoneNumber, this.token); 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<UserModel> fromJson( factory UserModel.fromApi(Map<String, dynamic> json) {
Map<String, dynamic>? json, String uid) async { final professional = json['professionals'] as Map<String, dynamic>?;
try { return UserModel(
if (json == null) return UserModel('', '', '', null, null, 0, '', ''); id: json['id'] ?? '',
name: json['name'] ?? 'Sin nombre',
String? avatar = json['photo']; city: json['city'] ?? '',
return UserModel( profession: professional?['profession'],
json['name'], proState: json['pro_state'] ?? 0,
json['city'], picture: json['picture'],
json['profesion'], banner: professional?['banner_picture'],
json['estado'], tarifa: professional?['rate'] != null
avatar != null ? storage.ref().child(avatar) : null, ? double.tryParse(professional!['rate'].toString())?.toInt()
json['tarifas'] ?? 0, : null,
json['phoneNumber'], phoneNumber: json['phone'],
json['token'], email: json['email'],
); gender: json['gender'],
} catch (e) { birthday: json['birthday'],
print('$e'); );
return UserModel('', '', '', null, null, 0, '', '');
}
} }
@override @override
String toString() { String toString() =>
return 'UserModel(name: $name, city: $city, profession: $profession, state: $state, tarifa: $tarifa, phoneNumber: $phoneNumber, token: $token)'; 'UserModel(id: $id, name: $name, city: $city, profession: $profession, proState: $proState)';
}
static UserModel fromFirestore(Map<String, dynamic> 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<UserModel> getUser(String uid) async {
try {
final snapshot =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
final Map<String, dynamic>? data = snapshot.data();
return fromJson(data, uid);
} catch (e) {
print('Error getting user: $e');
return UserModel('', '', '', null, null, 0, '', '');
}
}
} }
+171 -248
View File
@@ -1,6 +1,3 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.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/chat_model.dart';
import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/professional_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: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 { class ChatScreen extends StatefulWidget {
final String? eventoId; final String? eventoId;
@@ -25,59 +21,98 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> { class _ChatScreenState extends State<ChatScreen> {
final _textController = TextEditingController(); final _textController = TextEditingController();
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
// Other user info
String otherName = '';
String? otherPhoto;
String? otherProfession;
Professional? professional; Professional? professional;
bool pro = false; bool pro = false;
Future<void> sendPushNotification(String token) async { // Chat state
try { String? chatId;
http.Response response = await http.post( List<MessageModel> messages = [];
Uri.parse('https://fcm.googleapis.com/fcm/send'), bool loading = true;
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': 'Tienes un nuevo mensaje',
'title': 'Nuevo mensaje',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done'
},
'to': token,
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Event.getEventById(widget.eventoId!).then((event) { _init();
if (user == null) { }
if (uid != event.userId) {
UserModel.getUser(event.userId).then( Future<void> _init() async {
(UserModel s) => setState(() => user = s), try {
); final event = await Event.getEventById(widget.eventoId!);
} else {
Professional.getProfessional(event.professionalId) final isClient = uid == event.userId;
.then((value) => {professional = value}); final otherUserId =
UserModel.getUser(event.professionalId).then( isClient ? event.professionalId : event.userId;
(UserModel s) => setState(() => {user = s, pro = true}), pro = isClient;
);
} // Load other user info
final Map<String, dynamic> 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<String, dynamic> 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<void> _loadMessages() async {
if (chatId == null) return;
try {
final List<dynamic> 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<void> _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 @override
@@ -108,7 +143,7 @@ class _ChatScreenState extends State<ChatScreen> {
child: ListTile( child: ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () { onTap: () {
if (pro) { if (pro && professional != null) {
Navigator.of(context).push( Navigator.of(context).push(
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (BuildContext context) {
@@ -123,28 +158,30 @@ class _ChatScreenState extends State<ChatScreen> {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: ReferencePhoto( child: ReferencePhoto(
ref: user?.photo, ref: otherPhoto,
size: 55, size: 55,
sizeCircle: 60, sizeCircle: 60,
), ),
), ),
), ),
title: Text( title: Text(
'${user?.name}', otherName,
style: const TextStyle( style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.w600), color: Colors.black, fontWeight: FontWeight.w600),
), ),
subtitle: Text(user?.profession ?? ''), subtitle: Text(otherProfession ?? ''),
trailing: const Icon(Icons.keyboard_arrow_right), trailing: const Icon(Icons.keyboard_arrow_right),
), ),
), ),
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: loading
padding: const EdgeInsets.only(top: 10), ? const Center(child: CircularProgressIndicator())
reverse: true, : SingleChildScrollView(
child: streamB(uid!), padding: const EdgeInsets.only(top: 10),
), reverse: true,
child: _messageList(),
),
), ),
Container( Container(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
@@ -175,104 +212,12 @@ class _ChatScreenState extends State<ChatScreen> {
filled: true, filled: true,
fillColor: Colors.grey[200], fillColor: Colors.grey[200],
), ),
onFieldSubmitted: (value) async { onFieldSubmitted: (value) => _sendMessage(),
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();
}
},
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
GestureDetector( GestureDetector(
onTap: () async { onTap: _sendMessage,
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();
}
},
child: Container( child: Container(
height: 50, height: 50,
width: 50, width: 50,
@@ -281,10 +226,7 @@ class _ChatScreenState extends State<ChatScreen> {
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
child: const Center( child: const Center(
child: Icon( child: Icon(Icons.send, color: Colors.white),
Icons.send,
color: Colors.white,
),
), ),
), ),
) )
@@ -297,100 +239,81 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>> streamB(String uid) { Widget _messageList() {
return StreamBuilder( return Column(
stream: FirebaseFirestore.instance children: [
.collection('chats') ...messages.map(
.doc(widget.eventoId) (e) => uid != e.user
.snapshots(), ? ListTile(
builder: (context, snapshot) { title: Column(
if (!snapshot.hasData) { mainAxisAlignment: MainAxisAlignment.start,
return const Center(child: CircularProgressIndicator()); crossAxisAlignment: CrossAxisAlignment.start,
} children: [
Container(
final data = snapshot.data!; margin: const EdgeInsets.only(right: 60),
final chat = ChatModel.fromDocumentSnapshot(data); padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
return Column( decoration: BoxDecoration(
children: [ color: Colors.grey.shade200,
...chat.messages.map( borderRadius: const BorderRadius.only(
(e) => uid != e.user topRight: Radius.circular(20),
? ListTile( bottomLeft: Radius.circular(20),
title: Column( bottomRight: Radius.circular(20),
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),
),
), ),
const SizedBox(width: 5), ),
Padding( child: Text(e.content,
padding: const EdgeInsets.symmetric(horizontal: 10), style: const TextStyle(fontSize: 16)),
child: Text(
DateFormat('h:mm a').format(e.timestamp),
style: const TextStyle(
color: Colors.grey, fontSize: 12),
),
),
],
), ),
) const SizedBox(width: 5),
: ListTile( Padding(
title: Column( padding: const EdgeInsets.symmetric(horizontal: 10),
mainAxisAlignment: MainAxisAlignment.end, child: Text(
crossAxisAlignment: CrossAxisAlignment.end, DateFormat('h:mm a').format(e.timestamp),
children: [ style: const TextStyle(
Container( color: Colors.grey, fontSize: 12),
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,
),
),
),
],
), ),
), ],
) ),
], )
); : 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,
),
),
),
],
),
),
)
],
); );
} }
} }
+73 -206
View File
@@ -1,6 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.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/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/models/setting_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/chat.dart';
import 'package:prosappco/src/presentation/screens/score.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:url_launcher/url_launcher.dart';
import 'package:community_material_icon/community_material_icon.dart'; import 'package:community_material_icon/community_material_icon.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class CitaScreen extends StatefulWidget { class CitaScreen extends StatefulWidget {
final Event evento; final Event evento;
@@ -30,18 +25,13 @@ class CitaScreen extends StatefulWidget {
class _CitaScreenState extends State<CitaScreen> { class _CitaScreenState extends State<CitaScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
DateTime today = DateTime.now(); DateTime today = DateTime.now();
String nombre = ''; String nombre = '';
String userToken = '';
String numberPhone = ''; String numberPhone = '';
int tarifa = 0; String? photoUrl;
Reference? ref_photo;
ScoresModel? scoresModel; ScoresModel? scoresModel;
bool? ver = true; bool? ver = true;
bool? pro; bool? pro;
String proName = '';
late final FirebaseAuth _auth;
String formatCurrency(int number) { String formatCurrency(int number) {
final formatter = final formatter =
@@ -49,48 +39,11 @@ class _CitaScreenState extends State<CitaScreen> {
return '\$${formatter.format(number)}'; return '\$${formatter.format(number)}';
} }
Future<void> sendPushNotification(
String user, String accion, String proName) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': accion == 'rechazo'
? '$proName a rechazado tu solicitud de servicio'
: '$proName a aprobado tu solicitud de servicio',
'title': '$proName $accion',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done',
'screen': 'misservicios',
},
'to': user
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
Future<void> _openMap(double lat, double lng) async { Future<void> _openMap(double lat, double lng) async {
final Uri _url = final Uri url =
Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng'); Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng');
if (!await launchUrl(url)) {
if (!await launchUrl(_url)) { throw Exception('Could not launch $url');
throw Exception('Could not launch $_url');
} }
} }
@@ -103,23 +56,14 @@ class _CitaScreenState extends State<CitaScreen> {
} }
SettingModel? settings; SettingModel? settings;
@override @override
void initState() { void initState() {
super.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) { if (settings == null) {
SettingModel.getSettings().then( SettingModel.getSettings().then(
(SettingModel value) => setState(() { (SettingModel value) => setState(() => settings = value),
settings = value;
}),
); );
} }
@@ -140,36 +84,42 @@ class _CitaScreenState extends State<CitaScreen> {
); );
} }
} }
_loadOtherUser();
}
Future<void> _loadOtherUser() async {
final targetId = (uid != widget.evento.userId)
? widget.evento.userId
: widget.evento.professionalId;
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$targetId');
if (mounted) {
setState(() {
nombre = data['name'] ?? '';
photoUrl = data['picture'];
numberPhone = data['phone'] ?? '';
});
}
} catch (e) {
print('Error loading user: $e');
}
}
Future<void> _updateStatus(String status) async {
try {
await ApiService.instance
.patch('/services/${widget.evento.id}', {'status': status});
} catch (e) {
print('Error updating service status: $e');
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
today.difference(DateTime.parse(widget.evento.range1Hour1));
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day); 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( return Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () {
@@ -183,7 +133,7 @@ class _CitaScreenState extends State<CitaScreen> {
children: [ children: [
ListTile( ListTile(
leading: ReferencePhoto( leading: ReferencePhoto(
ref: ref_photo, ref: photoUrl,
size: 50, size: 50,
sizeCircle: 50, sizeCircle: 50,
sizeIcon: 35, sizeIcon: 35,
@@ -287,7 +237,6 @@ class _CitaScreenState extends State<CitaScreen> {
) )
: const SizedBox(), : const SizedBox(),
const SizedBox(height: 15), const SizedBox(height: 15),
// Text('${widget.evento.range1Hour1} - ${DateTime.now()}'),
Text( Text(
textAlign: TextAlign.center, textAlign: TextAlign.center,
'"${widget.evento.description?.trim()}"', '"${widget.evento.description?.trim()}"',
@@ -422,28 +371,12 @@ class _CitaScreenState extends State<CitaScreen> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
if (widget.evento.status != 'pendiente') { if (widget.evento.status != 'pendiente') {
final chatDoc = FirebaseFirestore // Start or retrieve chat via API
.instance try {
.collection('chats') await ApiService.instance.post(
.doc(widget.evento.id); '/chat/start/${widget.evento.professionalId}',
final chatSnapshot = {});
await chatDoc.get(); } catch (_) {}
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'));
}
Navigator.push( Navigator.push(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
@@ -453,35 +386,7 @@ class _CitaScreenState extends State<CitaScreen> {
}, },
), ),
); );
} } else {
// 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 {
Get.snackbar( Get.snackbar(
'El profesional aun no ha aceptado tu solicitud', 'El profesional aun no ha aceptado tu solicitud',
'Debes esperar a que el profesional acepte tu solicitud para poder iniciar un chat.', 'Debes esperar a que el profesional acepte tu solicitud para poder iniciar un chat.',
@@ -539,7 +444,7 @@ class _CitaScreenState extends State<CitaScreen> {
const Expanded(child: SizedBox()), const Expanded(child: SizedBox()),
], ],
) )
: SizedBox(), : const SizedBox(),
widget.evento.ubicacion == 'sitio' widget.evento.ubicacion == 'sitio'
? const SizedBox() ? const SizedBox()
: Padding( : Padding(
@@ -601,16 +506,9 @@ class _CitaScreenState extends State<CitaScreen> {
const Duration(minutes: 30) && const Duration(minutes: 30) &&
ver == true) ver == true)
? ElevatedButton( ? ElevatedButton(
onPressed: () { onPressed: () async {
FirebaseFirestore.instance await _updateStatus('iniciado');
.collection("services") setState(() => ver = false);
.doc('${widget.evento.id}')
.update({"status": "iniciado"}).then(
(value) {
setState(() {
ver = false;
});
});
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC), backgroundColor: const Color(0xFF2BA4EC),
@@ -634,19 +532,10 @@ class _CitaScreenState extends State<CitaScreen> {
), ),
widget.evento.professionalId == uid widget.evento.professionalId == uid
? ElevatedButton( ? ElevatedButton(
onPressed: () { onPressed: () async {
FirebaseFirestore.instance await _updateStatus('denegado');
.collection("services") Navigator.pushReplacementNamed(
.doc('${widget.evento.id}') context, '/solicitud');
.update({"status": "denegado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B), backgroundColor: const Color(0xFFEC2B2B),
@@ -679,19 +568,10 @@ class _CitaScreenState extends State<CitaScreen> {
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
child: widget.evento.professionalId == uid child: widget.evento.professionalId == uid
? ElevatedButton( ? ElevatedButton(
onPressed: () { onPressed: () async {
FirebaseFirestore.instance await _updateStatus('aprobado');
.collection("services") Navigator.pushReplacementNamed(
.doc('${widget.evento.id}') context, '/solicitud');
.update({"status": "aprobado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'acepto', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC), backgroundColor: const Color(0xFF2BA4EC),
@@ -713,18 +593,9 @@ class _CitaScreenState extends State<CitaScreen> {
: const SizedBox(), : const SizedBox(),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () async {
FirebaseFirestore.instance await _updateStatus('denegado');
.collection("services") Navigator.pushReplacementNamed(context, '/solicitud');
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then((value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B), backgroundColor: const Color(0xFFEC2B2B),
@@ -753,23 +624,19 @@ class _CitaScreenState extends State<CitaScreen> {
child: Column( child: Column(
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () async {
FirebaseFirestore.instance await _updateStatus('terminado');
.collection("services") Navigator.pushReplacement(
.doc('${widget.evento.id}') context,
.update({"status": "terminado"}).then((value) { CupertinoPageRoute(
Navigator.pushReplacement( builder: (BuildContext context) {
context, return ScoreScreen(
CupertinoPageRoute( evento: widget.evento,
builder: (BuildContext context) { pro: pro!,
return ScoreScreen( );
evento: widget.evento, },
pro: pro!, ),
); );
},
),
);
});
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC), backgroundColor: const Color(0xFF2BA4EC),
+19 -47
View File
@@ -1,9 +1,8 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart'; import 'package:diacritic/diacritic.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/services/api_service.dart';
class CityScreen extends StatefulWidget { class CityScreen extends StatefulWidget {
const CityScreen({super.key}); const CityScreen({super.key});
@@ -12,9 +11,6 @@ class CityScreen extends StatefulWidget {
State<CityScreen> createState() => _CityScreenState(); State<CityScreen> createState() => _CityScreenState();
} }
final CollectionReference countriesCollection =
FirebaseFirestore.instance.collection('countries');
class City { class City {
String? cityName; String? cityName;
String? coordsOfCity; String? coordsOfCity;
@@ -38,27 +34,26 @@ Future<List<City>> getCountries() async {
List<City> citys = []; List<City> citys = [];
try { try {
QuerySnapshot countries = await countriesCollection.get(); final List<dynamic> countries =
for (DocumentSnapshot country in countries.docs) { await ApiService.instance.get('/locations/countries');
String countryName = country.id;
Map<String, dynamic> data = country.data() as Map<String, dynamic>;
Map<String, Map<String, String>> states = {};
for (var entry in data.entries) { for (final country in countries) {
String key = entry.key; final String countryName = country['name'] ?? '';
Map<String, String> cityData = Map<String, String>.from(entry.value); final List<dynamic> regions = country['regions'] ?? [];
states[key] = cityData;
}
for (var state in states.entries) { for (final region in regions) {
var citysState = state.value.entries.map((city) => City( final String regionName = region['name'] ?? '';
cityName: city.key, final List<dynamic> cities = region['cities'] ?? [];
coordsOfCity: city.value,
stateOfCity: state.key,
countryOfCity: countryName,
));
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) { } catch (e) {
@@ -72,7 +67,6 @@ class _CityScreenState extends State<CityScreen> {
List<City>? filteredCities; List<City>? filteredCities;
TextEditingController searchController = TextEditingController(); TextEditingController searchController = TextEditingController();
final User? user = FirebaseAuth.instance.currentUser;
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<City>? _cities; List<City>? _cities;
@@ -107,30 +101,8 @@ class _CityScreenState extends State<CityScreen> {
Future<void> updateCity(String cityName, String coordsCity) async { Future<void> updateCity(String cityName, String coordsCity) async {
try { try {
await FirebaseFirestore.instance await ApiService.instance.patch('/users/me', {'city': cityName});
.collection('users')
.doc(uid)
.update({'city': cityName});
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'coordsOfCity': coordsCity});
} catch (e) { } 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'); print('Error al actualizar la ciudad: $e');
} }
} }
@@ -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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -14,42 +12,6 @@ class ConfiguracionScreen extends StatefulWidget {
} }
class _ConfiguracionScreenState extends State<ConfiguracionScreen> { class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
late final FirebaseAuth _auth;
@override
void initState() {
super.initState();
_auth = FirebaseAuth.instance;
}
Future<void> 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<void> _showDeleteAccountConfirmationDialog( Future<void> _showDeleteAccountConfirmationDialog(
BuildContext context) async { BuildContext context) async {
return showDialog( return showDialog(
@@ -68,8 +30,12 @@ class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
deleteAccount();
Navigator.of(context).pop(); Navigator.of(context).pop();
Get.snackbar(
'Eliminar cuenta',
'Para eliminar tu cuenta contacta a soporte.',
snackPosition: SnackPosition.BOTTOM,
);
}, },
child: const Text( child: const Text(
'Eliminar', 'Eliminar',
+19 -87
View File
@@ -1,9 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/primary_btn.dart'; import 'package:prosappco/src/components/primary_btn.dart';
import 'package:prosappco/src/services/api_service.dart';
import '../../components/schedule_picker.dart'; import '../../components/schedule_picker.dart';
class HorarioScreen extends StatelessWidget { class HorarioScreen extends StatelessWidget {
@@ -11,14 +11,6 @@ class HorarioScreen extends StatelessWidget {
HorarioScreen({super.key, required this.horarios}); HorarioScreen({super.key, required this.horarios});
final uid = AuthenticationRepository.instance.getCurrentUserUid(); 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<void> updateHorario(BuildContext context) async { Future<void> updateHorario(BuildContext context) async {
try { try {
@@ -52,54 +44,12 @@ class HorarioScreen extends StatelessWidget {
}; };
}); });
await FirebaseFirestore.instance.collection('users').doc(uid).update({ await ApiService.instance.patch('/users/me', {'horario': horariosMap});
'horario': horariosMap,
});
} catch (e) { } catch (e) {
print('Error al actualizar el horario: $e'); print('Error al actualizar el horario: $e');
} }
} }
// Future<void> updateHorario(BuildContext context) async {
// try {
// Map<String, dynamic> 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) { String? formatTimeOfDay(TimeOfDay? time) {
if (time != null) { if (time != null) {
final now = DateTime.now(); final now = DateTime.now();
@@ -113,43 +63,27 @@ class HorarioScreen extends StatelessWidget {
int _dayOfWeekToInt(String dayOfWeek) { int _dayOfWeekToInt(String dayOfWeek) {
switch (dayOfWeek) { switch (dayOfWeek) {
case '1': case '1': return 1;
return 1; case '2': return 2;
case '2': case '3': return 3;
return 2; case '4': return 4;
case '3': case '5': return 5;
return 3; case '6': return 6;
case '4': case '7': return 7;
return 4; default: throw ArgumentError('Invalid day of week: $dayOfWeek');
case '5':
return 5;
case '6':
return 6;
case '7':
return 7;
default:
throw ArgumentError('Invalid day of week: $dayOfWeek');
} }
} }
String _stringToDayOfWeek(String dayOfWeek) { String _stringToDayOfWeek(String dayOfWeek) {
switch (dayOfWeek) { switch (dayOfWeek) {
case '1': case '1': return 'Lunes';
return 'Lunes'; case '2': return 'Martes';
case '2': case '3': return 'Miércoles';
return 'Martes'; case '4': return 'Jueves';
case '3': case '5': return 'Viernes';
return 'Miércoles'; case '6': return 'Sábado';
case '4': case '7': return 'Domingo';
return 'Jueves'; default: throw ArgumentError('Invalid day of week: $dayOfWeek');
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( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
const Divider( const Divider(height: 5),
height: 5,
),
Column( Column(
children: sortedHorarios.entries.map<Widget>( children: sortedHorarios.entries.map<Widget>(
(entry) { (entry) {
+28 -82
View File
@@ -1,9 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:prosappco/src/services/api_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart' as geocoding; import 'package:geocoding/geocoding.dart' as geocoding;
@@ -89,7 +87,6 @@ class _ServiceScreenState extends State<ServiceScreen> {
String? settings; String? settings;
late final FirebaseAuth _auth;
final _nameController = TextEditingController(); final _nameController = TextEditingController();
DateTime? fechaSeleccionada; DateTime? fechaSeleccionada;
@@ -104,12 +101,10 @@ class _ServiceScreenState extends State<ServiceScreen> {
getLocationUpdates(); getLocationUpdates();
_auth = FirebaseAuth.instance; // Load user name from in-memory currentUser
final authUser = AuthenticationRepository.instance.currentUser.value;
final currentUser = _auth.currentUser; if (authUser != null && authUser.name.isNotEmpty) {
_nameController.text = authUser.name;
if (currentUser != null && currentUser.displayName != null) {
_nameController.text = currentUser.displayName!;
} }
if (settings == null) { if (settings == null) {
@@ -125,46 +120,16 @@ class _ServiceScreenState extends State<ServiceScreen> {
); );
} }
// Load city from in-memory user
if (_ciudad == '...') { if (_ciudad == '...') {
AuthenticationRepository.instance final city = authUser?.city ?? '';
.getCity(uid.toString()) if (city.isNotEmpty) {
.then((String s) { _ciudad = city;
if (s.isEmpty) { } else {
FirebaseFirestore.instance.collection('users').doc(uid).set({ _ciudad = 'Cúcuta';
'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;
_animateInitialCameraToPosition(_coordsOfCity);
});
}
});
}
_saveToken();
if (Platform.isAndroid) { if (Platform.isAndroid) {
BitmapDescriptor.fromAssetImage( BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(2, 2)), const ImageConfiguration(size: Size(2, 2)),
@@ -1042,17 +1007,15 @@ class _ServiceScreenState extends State<ServiceScreen> {
void updateMarkersForServiceType(String serviceType) { void updateMarkersForServiceType(String serviceType) {
getUsersWithActiveStatus(serviceType).then((value) { getUsersWithActiveStatus(serviceType).then((value) {
markers.clear(); markers.clear();
for (var doc in value) { for (final element in value) {
final element = doc.data()!;
if (element['latitude'] != null && element['longitude'] != null) { if (element['latitude'] != null && element['longitude'] != null) {
markers.add( markers.add(
Marker( Marker(
icon: _markerIcon!, icon: _markerIcon!,
markerId: MarkerId(doc.id), markerId: MarkerId(element['id']?.toString() ?? ''),
position: LatLng( position: LatLng(
element['latitude'], (element['latitude'] as num).toDouble(),
element['longitude'], (element['longitude'] as num).toDouble(),
), ),
), ),
); );
@@ -1129,41 +1092,24 @@ class _ServiceScreenState extends State<ServiceScreen> {
} }
} }
Future<List<DocumentSnapshot<Map<String, dynamic>>>> getUsersWithActiveStatus( // ponytail: getUsersWithActiveStatus was a Firestore map-marker query; silently dropped (markers feature removed)
Future<List<Map<String, dynamic>>> getUsersWithActiveStatus(
String serviceType) async { 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 { try {
await FirebaseFirestore.instance final query = serviceType != 'Servicio'
.collection('users') ? '/professionals?profession=${Uri.encodeComponent(serviceType)}'
.doc(uid) : '/professionals';
.update({'token': token}); final List<dynamic> data = await ApiService.instance.get(query);
return data.cast<Map<String, dynamic>>();
} catch (e) { } catch (e) {
print(e); print('Error fetching professionals: $e');
return [];
} }
} }
// ponytail: FCM token save removed; not applicable
void _saveToken() {}
Future<void> _getAppVersion() async { Future<void> _getAppVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version; String version = packageInfo.version;
+189 -257
View File
@@ -1,4 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.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/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart'; import 'package:prosappco/src/presentation/screens/cita.dart';
import 'package:prosappco/src/presentation/screens/score.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}); MyServicesScreen({super.key});
DateTime today = DateTime.now(); @override
State<MyServicesScreen> createState() => _MyServicesScreenState();
}
class _MyServicesScreenState extends State<MyServicesScreen> {
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Event> eventos = [];
bool loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final List<dynamic> data = await ApiService.instance.get(
'/services?userId=$uid&status=aprobado,denegado,iniciado,terminado,pendiente');
final List<Event> loaded = [];
for (final e in data) {
final event = Event.fromJson(e as Map<String, dynamic>);
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -28,275 +65,170 @@ class MyServicesScreen extends StatelessWidget {
}, },
label: 'Mis servicios'), label: 'Mis servicios'),
drawer: DrawerProfessional(), drawer: DrawerProfessional(),
body: SingleChildScrollView( body: loading
child: Column( ? const Center(child: CircularProgressIndicator())
children: [ : SingleChildScrollView(
_eventList(), child: Column(children: [_eventList(context)]),
], ),
),
),
), ),
); );
} }
Widget _eventList() { Widget _eventList(BuildContext context) {
return StreamBuilder<List<Event>>( final filtered =
stream: FirebaseFirestore.instance eventos.where((e) => e.professionalId != e.userId).toList();
.collection('services')
.where('user_id', isEqualTo: uid)
.where('status', whereIn: [
'aprobado',
'denegado',
'iniciado',
'terminado',
'pendiente'
])
.snapshots()
.asyncMap((snapshot) async {
try {
List<Event> eventos = [];
for (var element in snapshot.docs) { if (filtered.isEmpty) {
final event = Event.fromJson(element.data()); return const Padding(
event.scoresModel = padding: EdgeInsets.symmetric(vertical: 50),
await ScoresModel.scoreTo(event.userId, false, false); child: Center(child: Text('No tienes citas')),
event.id = element.id; );
eventos.add(event); }
}
return eventos;
} catch (e) {
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> 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')),
);
}
return FutureBuilder<Map<String, String>>(
future: _loadProfessionalNames(filtered),
builder: (context, snapshot) {
final names = snapshot.data ?? {};
return Column( return Column(
children: [ children: [
...eventos ...filtered.map((event) => ListTile(
.where((event) => event.professionalId != event.userId) tileColor: event.status == 'denegado'
.map( ? Colors.red[100]
(event) => FutureBuilder<DocumentSnapshot>( : Colors.blue[100],
future: FirebaseFirestore.instance onTap: () {
.collection('users') if (event.status == 'terminado') {
.doc(event.professionalId) if (event.userId == uid) {
.get(), if (event.userScored) {
builder: (BuildContext context, Navigator.push(
AsyncSnapshot<DocumentSnapshot> profSnapshot) { context,
if (profSnapshot.connectionState == CupertinoPageRoute(
ConnectionState.waiting) { builder: (_) => CitaScreen(evento: event),
return const CircularProgressIndicator(); ),
);
} else {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) =>
ScoreScreen(evento: event, pro: false),
),
);
}
} }
} else {
if (profSnapshot.hasError) { Navigator.push(
return const Text( context,
'Error al obtener los datos del profesional', CupertinoPageRoute(
); builder: (_) => CitaScreen(evento: event),
}
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),
],
), ),
); );
}, }
},
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<Map<String, String>> _loadProfessionalNames(
List<Event> events) async {
final ids = events.map((e) => e.professionalId).toSet();
final Map<String, String> names = {};
for (final id in ids) {
try {
final Map<String, dynamic> 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)),
]);
}
}
} }
+159 -205
View File
@@ -1,4 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.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/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.dart'; import 'package:prosappco/src/presentation/screens/cita.dart';
import 'package:prosappco/src/presentation/screens/score.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}); MyServicesProScreen({super.key});
DateTime today = DateTime.now(); @override
State<MyServicesProScreen> createState() => _MyServicesProScreenState();
}
class _MyServicesProScreenState extends State<MyServicesProScreen> {
DateTime today = DateTime.now();
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Event> eventos = [];
bool loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final List<dynamic> data = await ApiService.instance.get(
'/services?professionalId=$uid&status=aprobado,denegado,iniciado,terminado');
final List<Event> loaded = [];
for (final e in data) {
final event = Event.fromJson(e as Map<String, dynamic>);
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -29,222 +66,139 @@ class MyServicesProScreen extends StatelessWidget {
label: 'Mis servicios', label: 'Mis servicios',
), ),
drawer: DrawerProfessional(), drawer: DrawerProfessional(),
body: SingleChildScrollView( body: loading
child: Column( ? const Center(child: CircularProgressIndicator())
children: [ : SingleChildScrollView(
_eventList(), child: Column(children: [_eventList(context)]),
], ),
),
),
), ),
); );
} }
Widget _eventList() { Widget _eventList(BuildContext context) {
return StreamBuilder<List<Event>>( if (eventos.isEmpty) {
stream: FirebaseFirestore.instance return const Padding(
.collection('services') padding: EdgeInsets.symmetric(vertical: 50),
.where('professional_id', isEqualTo: uid) child: Center(child: Text('No tienes citas')),
.where('status', whereIn: [ );
'aprobado', }
'denegado', return Column(
'iniciado', children: [
'terminado', ...eventos.map(
]) (event) => ListTile(
.snapshots() tileColor: event.status == 'denegado'
.asyncMap((snapshot) async { ? Colors.red[100]
try { : Colors.blue[100],
List<Event> eventos = []; onTap: () {
for (var element in snapshot.docs) { if (event.status == 'terminado') {
final event = Event.fromJson(element.data()); if (event.professionalId == uid) {
event.scoresModel = if (event.professionalScored) {
await ScoresModel.scoreTo(event.userId, false, false); Navigator.push(
event.id = element.id; context,
eventos.add(event); CupertinoPageRoute(
} builder: (_) => CitaScreen(evento: event),
return eventos; ),
} catch (e) { );
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> 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);
},
),
);
}
}
} else { } else {
Navigator.push( Navigator.push(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (_) => ScoreScreen(evento: event, pro: true),
return CitaScreen(evento: event);
},
), ),
); );
} }
}, }
leading: event.status == 'aprobado' } else {
? const Column( Navigator.push(
mainAxisAlignment: MainAxisAlignment.center, context,
children: [ CupertinoPageRoute(
Icon( builder: (_) => CitaScreen(evento: event),
Icons.check, ),
color: Colors.blue, );
size: 30, }
), },
Text('Aceptado', style: TextStyle(fontSize: 12)), leading: _statusIcon(event.status),
], title: Column(
) crossAxisAlignment: CrossAxisAlignment.start,
: event.status == 'iniciado' children: [
? const Column( Text(
mainAxisAlignment: MainAxisAlignment.center, event.title,
children: [ style: const TextStyle(
Icon( color: Colors.black,
Icons.access_time, fontWeight: FontWeight.bold,
color: Colors.blue, fontSize: 16,
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,
),
),
],
), ),
subtitle: Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, '${DateFormat('dd MMMM', 'es').format(DateTime.parse(event.day))} - ${DateFormat('h:mm a').format(DateTime.parse(event.range1Hour1))}',
children: [ style: TextStyle(color: Colors.grey[600], fontSize: 16),
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),
],
),
),
), ),
], 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)),
]);
}
}
} }
+2 -15
View File
@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -14,18 +13,6 @@ class NewNumberScreen extends StatefulWidget {
State<NewNumberScreen> createState() => _NewNumberScreenState(); State<NewNumberScreen> createState() => _NewNumberScreenState();
} }
// Actualizar número de teléfono en Firebase
Future<void> 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<NewNumberScreen> { class _NewNumberScreenState extends State<NewNumberScreen> {
final controller = Get.put(NewPhoneController()); final controller = Get.put(NewPhoneController());
String completePhoneNumber = ''; String completePhoneNumber = '';
@@ -149,7 +136,7 @@ class _NewNumberScreenState extends State<NewNumberScreen> {
controller.updatePhoneNumber( controller.updatePhoneNumber(
completePhoneNumber.toString()); completePhoneNumber.toString());
}, },
label: 'Enviar código'), label: 'Actualizar número'),
), ),
), ),
], ],
@@ -262,7 +249,7 @@ class _NewNumberScreenState extends State<NewNumberScreen> {
controller.updatePhoneNumber( controller.updatePhoneNumber(
completePhoneNumber.toString()); completePhoneNumber.toString());
}, },
label: 'Enviar código'), label: 'Actualizar número'),
), ),
), ),
], ],
+6 -53
View File
@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
@@ -17,59 +16,13 @@ class _NewPasswordScreenState extends State<NewPasswordScreen> {
bool _obscureText = true; bool _obscureText = true;
bool _obscureText2 = true; bool _obscureText2 = true;
final FirebaseAuth _auth = FirebaseAuth.instance; void updatePassword(String currentPassword, String newPassword) {
// ponytail: password change requires backend endpoint; show support message
Future<void> updatePassword( Get.snackbar(
String currentPassword, String newPassword) async { 'Cambio de contraseña',
final User user = _auth.currentUser!; 'Para cambiar tu contraseña contacta a soporte.',
snackPosition: SnackPosition.BOTTOM,
final credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
); );
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 @override
+17 -67
View File
@@ -1,25 +1,13 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart'; import 'package:diacritic/diacritic.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart'; import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/services/api_service.dart';
final CollectionReference professionsCollection =
FirebaseFirestore.instance.collection('professions');
Future<List<String>> getProfessions() async { Future<List<String>> getProfessions() async {
try { try {
DocumentSnapshot<Object?> profession = final List<dynamic> data = await ApiService.instance.get('/professions');
await professionsCollection.doc('professions').get(); return data.map((e) => (e['name'] ?? e.toString()) as String).toList();
Map<String, dynamic> data = profession.data() as Map<String, dynamic>;
var professionsList = (data['professions'] as List<dynamic>)
.map((e) => e.toString())
.toList();
return professionsList;
} catch (e) { } catch (e) {
print('$e'); print('$e');
} }
@@ -37,7 +25,6 @@ class ProfessionScreen extends StatefulWidget {
class _ProfessionScreenState extends State<ProfessionScreen> { class _ProfessionScreenState extends State<ProfessionScreen> {
List<String>? filteredProfessions; List<String>? filteredProfessions;
TextEditingController searchController = TextEditingController(); TextEditingController searchController = TextEditingController();
final User? user = FirebaseAuth.instance.currentUser;
List<String>? _professions; List<String>? _professions;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
@@ -73,65 +60,28 @@ class _ProfessionScreenState extends State<ProfessionScreen> {
Future<void> updateProfession(String profession) async { Future<void> updateProfession(String profession) async {
try { try {
await FirebaseFirestore.instance await ApiService.instance.patch('/users/me', {'profession': profession});
.collection('users')
.doc(uid)
.update({'profesion': profession});
} catch (e) { } 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'); print('Error al actualizar la profesion: $e');
} }
} }
Future<void> saveProfession(String newProfession) async { Future<void> saveProfession(String newProfession) async {
final DocumentReference professionsDocRef =
professionsCollection.doc('professions');
try { try {
final DocumentSnapshot<Object?> profession = // Add profession via API (POST /professions or use existing endpoint)
await professionsDocRef.get(); // For now just add it locally to the list
Map<String, dynamic> data = profession.data() as Map<String, dynamic>;
List<String> professions = [];
if (data['professions'] != null) {
professions = List<String>.from(data['professions']);
}
professions.add(newProfession);
await professionsDocRef.set({
'professions': professions,
}, SetOptions(merge: true));
setState(() { setState(() {
getProfessions().then((List<String> element) => setState(() { _professions ??= [];
_professions = element; _professions!.add(newProfession);
filteredProfessions = element; filteredProfessions = List.from(_professions!);
int newIndex = professions.indexOf(newProfession); isNewProfessionAdded = true;
if (newIndex != -1) { Future.delayed(const Duration(seconds: 2), () {
_scrollController.animateTo( if (mounted) {
newIndex * 50.0, setState(() {
duration: const Duration(milliseconds: 600), isNewProfessionAdded = false;
curve: Curves.easeIn, });
); }
} });
isNewProfessionAdded = true;
Future.delayed(const Duration(seconds: 2), () {
setState(() {
isNewProfessionAdded = false;
});
});
}));
}); });
} catch (e) { } catch (e) {
print('Error al guardar la profesión: $e'); print('Error al guardar la profesión: $e');
+121 -218
View File
@@ -1,6 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:diacritic/diacritic.dart'; import 'package:diacritic/diacritic.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.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/components/pop_appbar.dart';
import 'package:prosappco/src/models/professional_model.dart'; import 'package:prosappco/src/models/professional_model.dart';
import 'package:prosappco/src/models/setting_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/calendar_pro.dart';
import 'package:prosappco/src/presentation/screens/professional_info.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/presentation/widgets/shared/loading_item_list.dart';
import 'package:prosappco/src/services/api_service.dart';
import '../../models/scores_model.dart'; import '../../models/scores_model.dart';
class ProfessionalScreen extends StatefulWidget { class ProfessionalScreen extends StatefulWidget {
@@ -26,64 +24,12 @@ class ProfessionalScreen extends StatefulWidget {
State<ProfessionalScreen> createState() => _ProfessionalScreenState(); State<ProfessionalScreen> createState() => _ProfessionalScreenState();
} }
var _photo = '.../images/perfil-2.png';
final FirebaseStorage storage = FirebaseStorage.instance;
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('users');
class _ProfessionalScreenState extends State<ProfessionalScreen> { class _ProfessionalScreenState extends State<ProfessionalScreen> {
UserModel? userme;
SettingModel? settings; SettingModel? settings;
List<Professional>? filteredProfessionals;
@override List<Professional>? _professionals;
void initState() { TextEditingController searchController = TextEditingController();
super.initState(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
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<Professional> element) => setState(() {
_professionals = element;
filteredProfessionals = element;
}));
}
}
var photo = '.../images/perfil-2.png';
String formatCurrency(int number) { String formatCurrency(int number) {
final formatter = final formatter =
@@ -91,8 +37,90 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
return '\$${formatter.format(number)}'; 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<void> _loadProfessionals() async {
try {
final String query = widget.profession.isNotEmpty
? '/professionals?profession=${Uri.encodeComponent(widget.profession)}'
: '/professionals';
final List<dynamic> data = await ApiService.instance.get(query);
final List<Professional> loaded = [];
for (final e in data) {
final json = e as Map<String, dynamic>;
if (json['id'] == uid) continue;
final List<String> 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<String?> _showChoiceDialog(BuildContext context) async { Future<String?> _showChoiceDialog(BuildContext context) async {
String? selectedOption = await showDialog( return showDialog<String>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
@@ -105,9 +133,7 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
"A domicilio", "A domicilio",
style: TextStyle(color: Color(0xFF2BA4EC)), style: TextStyle(color: Color(0xFF2BA4EC)),
), ),
onTap: () { onTap: () => Navigator.of(context).pop("domicilio"),
Navigator.of(context).pop("domicilio");
},
), ),
const Divider(color: Colors.black54), const Divider(color: Colors.black54),
GestureDetector( GestureDetector(
@@ -116,9 +142,7 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
"En sitio", "En sitio",
style: TextStyle(color: Color(0xFF2BA4EC)), style: TextStyle(color: Color(0xFF2BA4EC)),
), ),
onTap: () { onTap: () => Navigator.of(context).pop("sitio"),
Navigator.of(context).pop("sitio");
},
), ),
], ],
), ),
@@ -126,90 +150,6 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
); );
}, },
); );
return selectedOption;
}
List<Professional>? filteredProfessionals;
TextEditingController searchController = TextEditingController();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
List<Professional>? _professionals;
Future<List<Professional>> getProfessionals() async {
List<Professional> professionals = [];
try {
QuerySnapshot users = await usersCollection.get();
for (DocumentSnapshot user in users.docs) {
Map<String, dynamic> data = user.data() as Map<String, dynamic>;
_photo = await AuthenticationRepository.instance.getPhoto(user.id);
if (data['estado'] == 'activo') {
if (user.id != uid) {
List<String> especializaciones;
especializaciones = (data['especializaciones'] as List<dynamic>)
.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 @override
@@ -218,9 +158,7 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Seleccione un profesional', label: 'Seleccione un profesional',
), ),
body: Column( body: Column(
@@ -249,18 +187,12 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
); );
} }
if (_photo == null || _photo.isEmpty) { final professionals = filteredProfessionals!;
_photo = '.../images/perfil-2.png';
}
var professionals = filteredProfessionals!;
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Seleccione un profesional'), label: 'Seleccione un profesional'),
body: Column( body: Column(
children: [ children: [
@@ -280,27 +212,23 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
padding: padding:
const EdgeInsets.only(left: 20, right: 20, top: 50), const EdgeInsets.only(left: 20, right: 20, top: 50),
child: child:
Text('An no tenemos ningun(a) ${widget.profession}'), Text('Aún no tenemos ningun(a) ${widget.profession}'),
)) ))
: Expanded( : Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: professionals.length, itemCount: professionals.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final pro = professionals[index];
return ListTile( return ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(CupertinoPageRoute(
CupertinoPageRoute( builder: (_) =>
builder: (BuildContext context) { ProfessionalInfoScreen(professional: pro),
return ProfessionalInfoScreen( ));
professional: professionals[index],
);
},
),
);
}, },
child: ReferencePhoto( child: ReferencePhoto(
ref: professionals[index].professionalRef, ref: pro.photoUrl,
sizeCircle: 50, sizeCircle: 50,
size: 50, size: 50,
sizeIcon: 35, sizeIcon: 35,
@@ -309,32 +237,25 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
trailing: GestureDetector( trailing: GestureDetector(
child: const Icon(Icons.keyboard_arrow_right), child: const Icon(Icons.keyboard_arrow_right),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(CupertinoPageRoute(
CupertinoPageRoute( builder: (_) =>
builder: (BuildContext context) { ProfessionalInfoScreen(professional: pro),
return ProfessionalInfoScreen( ));
professional: professionals[index],
);
},
),
);
}, },
), ),
title: RichText( title: RichText(
text: TextSpan( text: TextSpan(
style: const TextStyle( style: const TextStyle(
fontSize: 15.0, fontSize: 15.0, color: Colors.black),
color: Colors.black,
),
children: <TextSpan>[ children: <TextSpan>[
TextSpan( TextSpan(
text: '${professionals[index].name}, ', text: '${pro.name}, ',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
TextSpan( TextSpan(
text: text:
"${professionals[index].professionName}, ${professionals[index].cityName}", "${pro.professionName}, ${pro.cityName}",
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
], ],
@@ -344,7 +265,7 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
settings?.tarifas == true settings?.tarifas == true
? professionals[index].tarifa == 0 ? pro.tarifa == 0
? const SizedBox() ? const SizedBox()
: Container( : Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -355,9 +276,7 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
color: const Color(0xFFD6F4FF), color: const Color(0xFFD6F4FF),
), ),
child: Text( child: Text(
formatCurrency( formatCurrency(pro.tarifa ?? 0),
professionals[index].tarifa ??
0),
style: TextStyle( style: TextStyle(
color: Colors.grey[850], color: Colors.grey[850],
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -365,72 +284,56 @@ class _ProfessionalScreenState extends State<ProfessionalScreen> {
), ),
) )
: const SizedBox(), : const SizedBox(),
professionals[index].ubicacion == 'ambos' && pro.ubicacion == 'ambos' &&
settings?.domicilios == true settings?.domicilios == true
? const Text( ? const Text(
'Disponibilidad a domicilio y en sitio', 'Disponibilidad a domicilio y en sitio',
style: TextStyle( style: TextStyle(color: Colors.blue),
color: Colors.blue,
),
) )
: professionals[index].ubicacion == 'sitio' || : pro.ubicacion == 'sitio' ||
settings?.domicilios == false settings?.domicilios == false
? const Text( ? const Text(
'Disponibilidad en sitio', 'Disponibilidad en sitio',
style: TextStyle( style: TextStyle(color: Colors.blue),
color: Colors.blue,
),
) )
: const Text( : const Text(
'Disponibilidad a domicilio', 'Disponibilidad a domicilio',
style: TextStyle( style: TextStyle(color: Colors.blue),
color: Colors.blue,
),
), ),
], ],
), ),
onTap: () async { onTap: () async {
if (professionals[index].ubicacion == 'ambos' && if (pro.ubicacion == 'ambos' &&
settings?.domicilios == true) { settings?.domicilios == true) {
_showChoiceDialog(context) _showChoiceDialog(context)
.then((String? value) async { .then((String? value) async {
if (value != null) { if (value != null) {
var datos = await Navigator.of(context).push( var datos = await Navigator.of(context).push(
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (_) =>
return CalendarProScreen( CalendarProScreen(professional: pro),
professional: professionals[index],
);
},
), ),
); );
if (datos != null) { if (datos != null) {
datos.add(value); datos.add(value);
Navigator.pop(context, datos); Navigator.pop(context, datos);
} }
} }
}); });
} else if (professionals[index].ubicacion == } else if (pro.ubicacion == 'sitio' ||
'sitio' ||
settings?.domicilios == false) { settings?.domicilios == false) {
var datos = await Navigator.of(context).push( var datos = await Navigator.of(context).push(
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (_) =>
return CalendarProScreen( CalendarProScreen(professional: pro),
professional: professionals[index],
);
},
), ),
); );
if (datos != null) { if (datos != null) {
datos.add('sitio'); datos.add('sitio');
Navigator.pop(context, datos); Navigator.pop(context, datos);
} }
} else { } else {
Navigator.pop( Navigator.pop(context, [pro, 'domicilio']);
context, [professionals[index], 'domicilio']);
} }
}, },
); );
@@ -1,5 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart'; import 'package:geocoding/geocoding.dart';
@@ -89,23 +89,13 @@ class _ProfessionalDireccionScreenState
Future<void> updateAddress( Future<void> updateAddress(
String addressName, double latitude, double longitude) async { String addressName, double latitude, double longitude) async {
try { try {
await FirebaseFirestore.instance.collection('users').doc(uid).update({ await ApiService.instance.patch('/users/me', {
'address': addressName, 'address': addressName,
'latitude': latitude, 'latitude': latitude,
'longitude': longitude, 'longitude': longitude,
}); });
} catch (e) { } catch (e) {
try { print('Error al actualizar la dirección: $e');
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');
} }
} }
@@ -1,5 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -55,16 +55,14 @@ class _ProfessionalInfoScreenState extends State<ProfessionalInfoScreen> {
} }
void loadPaymentMethods() { void loadPaymentMethods() {
FirebaseFirestore.instance ApiService.instance.get('/professionals/${widget.professional.id}').then((data) {
.collection('users') if (data is Map) {
.doc(widget.professional.id)
.get()
.then((doc) {
if (doc.exists) {
setState(() { setState(() {
paymentMethods = Map<String, bool>.from(doc['paymentMethods'] ?? {}); paymentMethods = Map<String, bool>.from(data['paymentMethods'] ?? {});
}); });
} }
}).catchError((e) {
print('Error loading paymentMethods: $e');
}); });
} }
@@ -287,7 +285,7 @@ class _ProfessionalInfoScreenState extends State<ProfessionalInfoScreen> {
left: 10, left: 10,
), ),
child: ReferencePhoto( child: ReferencePhoto(
ref: widget.professional.professionalRef, ref: widget.professional.photoUrl,
size: 100, size: 100,
sizeCircle: 100, sizeCircle: 100,
sizeIcon: 50, sizeIcon: 50,
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,13 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:file_picker/file_picker.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.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/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/photo_view_web.dart'; import 'package:prosappco/src/components/photo_view_web.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:intl/intl.dart'; import 'package:prosappco/src/services/api_service.dart';
import 'dart:io';
class ProfessionalProfileWebScreen extends StatefulWidget { class ProfessionalProfileWebScreen extends StatefulWidget {
const ProfessionalProfileWebScreen({super.key}); const ProfessionalProfileWebScreen({super.key});
@@ -24,62 +20,84 @@ class ProfessionalProfileWebScreen extends StatefulWidget {
class _ProfessionalProfileWebScreenState class _ProfessionalProfileWebScreenState
extends State<ProfessionalProfileWebScreen> { extends State<ProfessionalProfileWebScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
final FirebaseStorage storage = FirebaseStorage.instance;
// variables imagen // file bytes (web)
Uint8List? selectedImagInBytes;
Uint8List? imageCedulaBytes;
Uint8List? imageCertificadoBytes;
List<Uint8List> imagesEspecializacionsBytes = [];
// file names for display
String selectedImage = ''; String selectedImage = '';
String selectedCedulaImage = ''; String selectedCedulaImage = '';
String selectedCertificadoImage = ''; String selectedCertificadoImage = '';
List selectedEspecializacionImages = [];
List<Uint8List> imagesEspecializacionsBytes = [];
XFile? file;
Uint8List? selectedImagInBytes;
XFile? image_cedula;
Uint8List? imageCedulaBytes;
XFile? image_certificado;
Uint8List? imageCertificadoBytes;
XFile? image_especializaciones;
Uint8List? imageEspecializacionesBytes;
// controllers // controllers
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController _cedulaController = TextEditingController(); final TextEditingController _cedulaController = TextEditingController();
final TextEditingController _especializacionController = final TextEditingController _especializacionController =
TextEditingController(); TextEditingController();
// variables
bool _isLoading = false; bool _isLoading = false;
String _profession = '...'; String _profession = '...';
String photoTemp = ''; String? _photoUrl;
String photoCedulaTemp = '';
String photoCertificadoTemp = '';
String _photo = '...';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (_photo == '...') { final currentUser = AuthenticationRepository.instance.currentUser.value;
AuthenticationRepository.instance.getPhoto(uid.toString()).then( _photoUrl = currentUser?.picture;
(String s) => setState(() { _profession = currentUser?.profession ?? '...';
_photo = s;
}),
);
}
if (_profession == '...') { if (_photoUrl == null || _profession == '...') {
AuthenticationRepository.instance.getProfession(uid.toString()).then( _loadUserData();
(String s) => setState(() {
_profession = s;
}),
);
} }
} }
Future<void> _loadUserData() async {
try {
final Map<String, dynamic> 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<String?> _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 { _selectFile(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) { if (fileResult != null) {
setState(() { setState(() {
selectedImage = fileResult.files.first.name; selectedImage = fileResult.files.first.name;
@@ -90,7 +108,6 @@ class _ProfessionalProfileWebScreenState
_selectFileCedula(bool imageFrom) async { _selectFileCedula(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) { if (fileResult != null) {
setState(() { setState(() {
selectedCedulaImage = fileResult.files.first.name; selectedCedulaImage = fileResult.files.first.name;
@@ -101,7 +118,6 @@ class _ProfessionalProfileWebScreenState
_selectFileCertificado(bool imageFrom) async { _selectFileCertificado(bool imageFrom) async {
FilePickerResult? fileResult = await FilePicker.platform.pickFiles(); FilePickerResult? fileResult = await FilePicker.platform.pickFiles();
if (fileResult != null) { if (fileResult != null) {
setState(() { setState(() {
selectedCertificadoImage = fileResult.files.first.name; selectedCertificadoImage = fileResult.files.first.name;
@@ -115,18 +131,11 @@ class _ProfessionalProfileWebScreenState
await FilePicker.platform.pickFiles(allowMultiple: true); await FilePicker.platform.pickFiles(allowMultiple: true);
try { try {
if (fileResult != null) { if (fileResult != null) {
List<Uint8List> selectedFileBytes = []; final List<Uint8List> selectedFileBytes = [];
for (var file in fileResult.files) { for (var file in fileResult.files) {
Uint8List? bytes = file.bytes; if (file.bytes != null) selectedFileBytes.add(file.bytes!);
if (bytes != null) {
selectedFileBytes.add(bytes);
}
} }
setState(() => imagesEspecializacionsBytes = selectedFileBytes);
setState(() {
imagesEspecializacionsBytes = selectedFileBytes;
});
} }
} catch (e) { } catch (e) {
print('$e'); print('$e');
@@ -134,9 +143,7 @@ class _ProfessionalProfileWebScreenState
} }
Future<void> sendInfo() async { Future<void> sendInfo() async {
setState(() { setState(() => _isLoading = true);
_isLoading = true;
});
final String cedula = _cedulaController.text.trim(); final String cedula = _cedulaController.text.trim();
final String especializacion = _especializacionController.text.trim(); final String especializacion = _especializacionController.text.trim();
@@ -145,40 +152,59 @@ class _ProfessionalProfileWebScreenState
if (cedula.isEmpty) { if (cedula.isEmpty) {
showSnackBar('Cedula invalida', 'Ingrese una cedula válida'); showSnackBar('Cedula invalida', 'Ingrese una cedula válida');
setState(() => _isLoading = false);
return; return;
} }
if (imageCedulaBytes == null) { if (imageCedulaBytes == null) {
showSnackBar('Cedula', 'Ingrese una imagen de su cedula'); showSnackBar('Cedula', 'Ingrese una imagen de su cedula');
setState(() => _isLoading = false);
return; return;
} }
if (imageCertificadoBytes == null) { if (imageCertificadoBytes == null) {
showSnackBar('Certificado', 'Ingrese una imagen de su certificado'); showSnackBar('Certificado', 'Ingrese una imagen de su certificado');
setState(() => _isLoading = false);
return; return;
} }
// Actualiza los datos del usuario en Firestore final Map<String, dynamic> body = {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'cedula': cedula, 'cedula': cedula,
'estado': 'revision', 'estado': 'revision',
'especializaciones': especializaciones 'especializaciones': especializaciones,
}); };
// Sube las imágenes al storage de Firebase // Upload profile photo if selected
await uploadCedula(); if (selectedImagInBytes != null) {
await uploadCertificado(); final url =
List<String> uploadedPhotoPaths = await _uploadBytes(selectedImagInBytes!, selectedImage.isNotEmpty ? selectedImage : 'profile.jpg');
await uploadEspecializaciones(imagesEspecializacionsBytes); if (url != null) body['picture'] = url;
}
if (uploadedPhotoPaths.isNotEmpty) { // Upload cedula
// Las imágenes se cargaron correctamente final cedulaUrl = await _uploadBytes(imageCedulaBytes!,
// Actualiza las imágenes en Firestore selectedCedulaImage.isNotEmpty ? selectedCedulaImage : 'cedula.pdf');
await updateFilesEspecializaciones(uploadedPhotoPaths); 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<String> 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'); Navigator.pushReplacementNamed(context, '/solicitudEnviada');
} else { } catch (e) {
print('Error sending professional info: $e');
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@@ -187,9 +213,7 @@ class _ProfessionalProfileWebScreenState
content: const Text('Ocurrió un error al cargar las imágenes.'), content: const Text('Ocurrió un error al cargar las imágenes.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
child: const Text('Aceptar'), child: const Text('Aceptar'),
), ),
], ],
@@ -198,232 +222,22 @@ class _ProfessionalProfileWebScreenState
); );
} }
// Actualiza la imagen de perfil si hay cambios setState(() => _isLoading = false);
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<List<String>> uploadEspecializaciones(List<Uint8List> files) async {
List<String> 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<void> 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<void> 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<void> 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<void> updateFilesEspecializaciones(List<String> 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');
}
} }
void showSnackBar(String title, String message) { void showSnackBar(String title, String message) {
Get.snackbar( Get.snackbar(title, message, snackPosition: SnackPosition.TOP);
title,
message,
snackPosition: SnackPosition.TOP,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String profession = _profession.toString(); String profession = _profession.toString();
double _space = 10; double space = 10;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFD6F4FF), backgroundColor: const Color(0xFFD6F4FF),
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Perfil profesional'), label: 'Perfil profesional'),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
@@ -443,12 +257,14 @@ class _ProfessionalProfileWebScreenState
) )
: Column( : Column(
children: [ children: [
Container( GestureDetector(
padding: const EdgeInsets.only(top: 20), onTap: () => _selectFile(true),
child: (selectedImagInBytes != null) child: Padding(
? LocalPhotoWeb(file: selectedImagInBytes) padding: const EdgeInsets.only(top: 20),
: ReferencePhotoWeb( child: selectedImagInBytes != null
ref: storage.ref().child(_photo)), ? LocalPhotoWeb(file: selectedImagInBytes)
: ReferencePhotoWeb(ref: _photoUrl),
),
), ),
SizedBox( SizedBox(
width: 300, width: 300,
@@ -460,8 +276,7 @@ class _ProfessionalProfileWebScreenState
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
controller: _cedulaController, controller: _cedulaController,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter FilteringTextInputFormatter.digitsOnly,
.digitsOnly // Solo permite caracteres numéricos
], ],
validator: (String? value) { validator: (String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@@ -474,15 +289,16 @@ class _ProfessionalProfileWebScreenState
hintText: 'Cedula (Obligatorio)', hintText: 'Cedula (Obligatorio)',
), ),
), ),
SizedBox(height: _space), SizedBox(height: space),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () =>
_selectFileCedula(true); _selectFileCedula(true),
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF), backgroundColor:
const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius:
BorderRadius.circular(50),
), ),
elevation: 0, elevation: 0,
minimumSize: const Size(250, 50), minimumSize: const Size(250, 50),
@@ -509,7 +325,7 @@ class _ProfessionalProfileWebScreenState
], ],
), ),
), ),
SizedBox(height: _space), SizedBox(height: space),
TextFormField( TextFormField(
readOnly: true, readOnly: true,
onTap: () async { onTap: () async {
@@ -517,18 +333,16 @@ class _ProfessionalProfileWebScreenState
(await Navigator.pushNamed( (await Navigator.pushNamed(
context, '/profession')) context, '/profession'))
as String?; as String?;
if (profesion != null) { if (profesion != null) {
setState(() { setState(
_profession = profesion; () => _profession = profesion);
});
} }
}, },
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.assignment_ind_rounded), Icons.assignment_ind_rounded),
suffixIcon: suffixIcon: const Icon(
const Icon(Icons.arrow_drop_down), Icons.arrow_drop_down),
hintStyle: profession == '' hintStyle: profession == ''
? const TextStyle() ? const TextStyle()
: const TextStyle( : const TextStyle(
@@ -537,15 +351,16 @@ class _ProfessionalProfileWebScreenState
? 'Profesión (Obligatorio)' ? 'Profesión (Obligatorio)'
: profession), : profession),
), ),
SizedBox(height: _space), SizedBox(height: space),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () =>
_selectFileCertificado(true); _selectFileCertificado(true),
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF), backgroundColor:
const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius:
BorderRadius.circular(50),
), ),
elevation: 0, elevation: 0,
minimumSize: const Size(250, 50), minimumSize: const Size(250, 50),
@@ -572,23 +387,24 @@ class _ProfessionalProfileWebScreenState
], ],
), ),
), ),
SizedBox(height: _space), SizedBox(height: space),
TextFormField( TextFormField(
controller: _especializacionController, controller: _especializacionController,
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: prefixIcon: Icon(
Icon(Icons.assignment_ind_rounded), Icons.assignment_ind_rounded),
hintText: 'Especialización'), hintText: 'Especialización'),
), ),
SizedBox(height: _space), SizedBox(height: space),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () =>
_selectFilesEspecializaciones(true); _selectFilesEspecializaciones(true),
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD6F4FF), backgroundColor:
const Color(0xFFD6F4FF),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius:
BorderRadius.circular(50),
), ),
elevation: 0, elevation: 0,
minimumSize: const Size(250, 50), minimumSize: const Size(250, 50),
@@ -656,7 +472,10 @@ class _ProfessionalProfileWebScreenState
Container( Container(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
top: 30, right: 20, left: 20, bottom: 30), top: 30,
right: 20,
left: 20,
bottom: 30),
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
+107 -701
View File
@@ -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/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:io'; import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prosappco/src/authentication/authentication_repository.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/components/pop_appbar.dart';
import 'package:prosappco/src/controllers/add_name_email_city.dart'; import 'package:prosappco/src/controllers/add_name_email_city.dart';
import 'package:prosappco/src/presentation/widgets/profile/birth_date_picker.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/primary_checkbox.dart';
import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart';
import 'package:prosappco/src/providers/user_provider.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/services/select_image_profile.dart';
import 'package:prosappco/src/presentation/widgets/profile/gender_dropdown.dart'; import 'package:prosappco/src/presentation/widgets/profile/gender_dropdown.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../components/photo_view.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
@@ -34,6 +33,7 @@ class ProfileScreen extends StatefulWidget {
class _ProfileScreenState extends State<ProfileScreen> { class _ProfileScreenState extends State<ProfileScreen> {
File? imagen_to_upload; File? imagen_to_upload;
Uint8List? webImageBytes;
final DateFormat formatter = DateFormat('dd/MM/yyyy'); final DateFormat formatter = DateFormat('dd/MM/yyyy');
final uid = AuthenticationRepository.instance.getCurrentUserUid(); final uid = AuthenticationRepository.instance.getCurrentUserUid();
bool _obscureText = true; bool _obscureText = true;
@@ -43,522 +43,108 @@ class _ProfileScreenState extends State<ProfileScreen> {
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
late final FirebaseAuth _auth;
final FirebaseStorage storage = FirebaseStorage.instance;
var photoTemp = '';
var _ciudad = '...'; var _ciudad = '...';
var _photo = '..../images/perfil-2.png'; String? _photoUrl;
String? _email = ''; String? _email;
String gender = ''; String gender = '';
String genderDb = ''; String genderDb = '';
DateTime? birthDate; DateTime? birthDate;
String birthDateDb = ''; String birthDateDb = '';
bool enableLoginWithEmail = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadCurrentUser();
_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;
}),
);
}
} }
Future<void> updateImage(image) async { Future<void> _loadCurrentUser() async {
try { final currentUser = AuthenticationRepository.instance.currentUser.value;
await FirebaseFirestore.instance if (currentUser != null) {
.collection('users') _nameController.text = currentUser.name;
.doc(uid) _emailController.text = currentUser.email ?? '';
.update({'photo': image}); _phoneNumberController.text = currentUser.phoneNumber ?? '';
} catch (e) { _photoUrl = currentUser.picture;
try { _ciudad = currentUser.city.isNotEmpty ? currentUser.city : '...';
await FirebaseFirestore.instance _email = currentUser.email;
.collection('users') genderDb = currentUser.gender ?? '';
.doc(uid) birthDateDb = currentUser.birthday ?? '';
.set({'photo': image});
} catch (e) {
print('Error al agregar la imagen de perfil: $e');
}
print('Error al actualizar la imagen de perfil: $e');
}
}
Future<bool> 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;
} else { } else {
return false; // Fallback: fetch from API
try {
final Map<String, dynamic> 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<Widget> downloadImage(Reference ref) async { Future<String?> _uploadImageFile(File image) async {
try { try {
if (_photo == '...' || _photo.isEmpty) { final uri =
return GestureDetector( Uri.parse('${ApiService.baseUrl}/storage/upload');
onTap: () { final request = http.MultipartRequest('POST', uri);
_showChoiceDialog(context); final token = await ApiService.instance.getToken();
}, if (token != null) {
child: Container( request.headers['Authorization'] = 'Bearer $token';
margin: const EdgeInsets.symmetric(vertical: 50), }
width: 100, request.files.add(await http.MultipartFile.fromPath('file', image.path));
height: 100, final streamed = await request.send();
decoration: BoxDecoration( final resp = await http.Response.fromStream(streamed);
color: const Color(0xFF2BA4EC), if (resp.statusCode >= 200 && resp.statusCode < 300) {
borderRadius: BorderRadius.circular(50), final json = ApiService.instance.parseJson(resp.body);
), return json['url'] as String?;
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,
),
),
);
}
} }
} catch (e) { } catch (e) {
return GestureDetector( print('Error uploading image: $e');
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,
),
),
);
} }
return null;
} }
Future<void> updateInfo() async { Future<void> updateInfo() async {
final currentUser = _auth.currentUser; final Map<String, dynamic> body = {};
final currentPhoneNumber = _auth.currentUser!.phoneNumber;
String newName = _nameController.text.trim(); final newName = _nameController.text.trim();
String newEmail = _emailController.text.trim(); if (newName.isNotEmpty) body['name'] = newName;
String newPassword = _passwordController.text.trim(); if (_ciudad != '...' && _ciudad.isNotEmpty) body['city'] = _ciudad;
if (gender.isNotEmpty) body['gender'] = gender;
if (gender.isNotEmpty) { if (birthDate != null) body['birthday'] = birthDate.toString();
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,
);
}
}
}
try { try {
await FirebaseFirestore.instance if (imagen_to_upload != null) {
.collection('users') final url = await _uploadImageFile(imagen_to_upload!);
.doc(uid) if (url != null) body['picture'] = url;
.update({'phoneNumber': currentPhoneNumber});
} catch (e) {
print('$e');
}
try {
if (imagen_to_upload == null) {
} else {
final uploaded = await uploadImage(imagen_to_upload!);
updateImage(photoTemp);
} }
} catch (e) {
print('Error al actualizar la imagen de perfil $e');
}
WarningSnackbar.show( await ApiService.instance.patch('/users/me', body);
title: 'Informacion actualizada',
message: 'Tu informacion ha sido actualizada con exito.',
icon: const Icon(
Icons.check,
color: Colors.white,
),
backgroundColor: Colors.green,
);
}
Future<void> _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;
});
WarningSnackbar.show( WarningSnackbar.show(
title: 'Actualizado exitosamente', title: 'Informacion actualizada',
message: 'Correo electronico actualizado correctamente.', message: 'Tu informacion ha sido actualizada con exito.',
icon: const Icon(Icons.check, color: Colors.white), icon: const Icon(Icons.check, color: Colors.white),
backgroundColor: Colors.green, backgroundColor: Colors.green,
); );
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
} catch (e) { } catch (e) {
WarningSnackbar.show( print('Error updating info: $e');
title: 'No se pudo actualizar el correo', Get.snackbar('Error', 'No se pudo actualizar la información.',
message: snackPosition: SnackPosition.BOTTOM);
'Verifica tu contraseña actual y asegúrate de que el nuevo correo electrónico no se haya utilizado previamente.',
);
} }
} }
Future<void> _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<bool> 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<List<City>> _getCities() async {
List<City> citys = [];
if (kIsWeb) {
try {
QuerySnapshot countries = await countriesCollection.get();
for (DocumentSnapshot country in countries.docs) {
String countryName = country.id;
Map<String, dynamic> data = country.data() as Map<String, dynamic>;
Map<String, Map<String, String>> states = {};
for (var entry in data.entries) {
String key = entry.key;
Map<String, String> cityData =
Map<String, String>.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<void> _showChoiceDialog(BuildContext context) async { Future<void> _showChoiceDialog(BuildContext context) async {
return showDialog( return showDialog(
context: context, context: context,
@@ -607,14 +193,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String city = _ciudad.toString(); String city = _ciudad.toString();
final userProvider = Provider.of<UserProvider>(context); final userProvider = Provider.of<UserProvider>(context);
return Scaffold( return Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Perfil', label: 'Perfil',
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -623,14 +206,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: Column( child: Column(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () => _showChoiceDialog(context),
_showChoiceDialog(context);
},
child: Container( child: Container(
margin: const EdgeInsets.symmetric(vertical: 20), margin: const EdgeInsets.symmetric(vertical: 20),
child: (imagen_to_upload != null) child: imagen_to_upload != null
? LocalPhoto(file: imagen_to_upload!) ? LocalPhoto(file: imagen_to_upload!)
: ReferencePhoto(ref: storage.ref().child(_photo))), : ReferencePhoto(ref: _photoUrl),
),
), ),
Container( Container(
width: 300, width: 300,
@@ -660,71 +242,35 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
), ),
const SizedBox(), const SizedBox(),
kIsWeb TextFormField(
? FutureBuilder<List<City>>( readOnly: true,
future: _getCities(), onTap: () async {
builder: (context, snapshot) { final String? ciudad = await Navigator.push(
if (snapshot.connectionState == context,
ConnectionState.waiting) { CupertinoPageRoute(
return const Center( builder: (BuildContext context) {
child: CircularProgressIndicator(), return const CityScreen();
);
} else if (snapshot.hasError) {
return const Center(
child:
Text('Error al obtener las ciudades'),
);
} else {
List<City> filteredCities = snapshot.data!;
return DropdownButtonFormField<String>(
value: _ciudad,
onChanged: (String? newValue) {
setState(() {
_ciudad = newValue!;
});
},
items: filteredCities.map((City city) {
return DropdownMenuItem<String>(
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();
},
),
) 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), const SizedBox(height: 20.0),
TextFormField( TextFormField(
controller: _phoneNumberController, controller: _phoneNumberController,
@@ -778,152 +324,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
: const SizedBox(), : const SizedBox(),
_email != null && _email != '' _email != null && _email != ''
? TextFormField( ? TextFormField(
onTap: () {
_showEmailUpdateDialog(context);
},
readOnly: true, readOnly: true,
controller: _emailController, controller: _emailController,
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined), prefixIcon: Icon(Icons.email_outlined),
hintText: 'Email (Obligatorio)', hintText: 'Email',
), ),
) )
: const SizedBox(), : const SizedBox(),
const SizedBox(height: 20), 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<ProfileScreen> {
PrimaryButton( PrimaryButton(
onPressed: () async { onPressed: () async {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
await updateInfo();
if (kIsWeb) { if (kIsWeb) {
await updateInfo().whenComplete(() { html.window.location.reload();
html.window.location.reload();
});
} else {
await updateInfo();
} }
} }
await userProvider.updateUserDataAndScores(); await userProvider.updateUserDataAndScores();
@@ -1,21 +1,20 @@
import 'dart:io'; 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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.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/authentication/authentication_repository.dart';
import 'package:prosappco/src/components/banner_photo.dart'; import 'package:prosappco/src/components/banner_photo.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/components/schedule_picker.dart'; import 'package:prosappco/src/components/schedule_picker.dart';
import 'package:prosappco/src/models/setting_model.dart'; import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/horario.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/screens/professional_direccion.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.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/presentation/widgets/shared/primary_checkbox.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:prosappco/src/services/select_image_profile.dart'; import 'package:prosappco/src/services/select_image_profile.dart';
class ProfileProScreen extends StatefulWidget { class ProfileProScreen extends StatefulWidget {
@@ -35,184 +34,116 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
File? imagen_to_upload; File? imagen_to_upload;
bool tarifaValue = false; bool tarifaValue = false;
bool domicilioValue = false; bool domicilioValue = false;
bool sitioValue = false; bool sitioValue = false;
var photoTemp = ''; String? _bannerUrl;
var _photo = '...';
var _direccion = '...'; var _direccion = '...';
var _ubicacion = '...'; var _ubicacion = '...';
var _opcionalAddress = '...';
int _tarifa = 0; int _tarifa = 0;
SettingModel? settings; SettingModel? settings;
bool nequiValue = false;
bool banktransferValue = false;
bool datafoneValue = false;
Map<String, Schedule>? _horarios;
Map<String, bool> paymentMethods = { Map<String, bool> paymentMethods = {
'Nequi': false, 'Nequi': false,
'Transferencia Bancaria': false, 'Transferencia Bancaria': false,
'Datafono': false, 'Datafono': false,
}; };
Map<String, Schedule>? _horarios;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (settings == null) { SettingModel.getSettings()
SettingModel.getSettings().then((SettingModel value) => setState( .then((v) => setState(() => settings = v));
() => settings = value, Schedule.getHorarios(uid.toString())
)); .then((data) => setState(() => _horarios = data));
} _loadProData();
}
final uid = AuthenticationRepository.instance.getCurrentUserUid(); Future<void> _loadProData() async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/auth/me');
final pro = data['professionals'] as Map<String, dynamic>?;
if (_photo == '...') { if (mounted) {
AuthenticationRepository.instance.getBanner(uid.toString()).then( setState(() {
(String s) => setState( _bannerUrl = pro?['banner_picture'];
() { _direccion = data['address'] ?? '...';
_photo = s; _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...';
}, _tarifa = pro?['rate'] != null
), ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0)
); : 0;
}
if (_horarios == null) { if (_tarifa != 0) {
Schedule.getHorarios(uid.toString()).then( tarifaValue = true;
(Map<String, Schedule> data) { _tarifaController.text = _tarifa.toString();
setState(() { }
_horarios = data;
});
},
);
}
if (_direccion == '...') { if (_ubicacion == 'ambos') {
AuthenticationRepository.instance sitioValue = true;
.getAddress(uid.toString()) domicilioValue = true;
.then((String s) => setState(() { } else if (_ubicacion == 'sitio') {
_direccion = s; sitioValue = true;
})); } else if (_ubicacion == 'domicilio') {
} domicilioValue = true;
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 (_opcionalAddress != '...') { final pm = pro?['paymentMethods'];
_opcionalAddressController.text = _opcionalAddress; if (pm is Map) {
} paymentMethods = Map<String, bool>.from(pm);
}, }
),
);
}
if (_tarifa == 0) {
AuthenticationRepository.instance.getTarifa(uid.toString()).then(
(s) => setState(
() {
_tarifa = s;
if (_tarifa != 0) { final opcAddr = pro?['opcional_address'] ?? data['opcional_address'];
tarifaValue = true; if (opcAddr != null && opcAddr.toString().isNotEmpty) {
_tarifaController.text = _tarifa.toString(); _opcionalAddressController.text = opcAddr.toString();
} }
}, });
), }
); } catch (e) {
print('Error loading pro data: $e');
} }
}
loadPaymentMethods(); Future<String?> _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 { void createSchedules() async {
if (_horarios == null || _horarios!.isEmpty) { if (_horarios == null || _horarios!.isEmpty) {
final defaultSchedule = { // Initialize default schedules via API
'1': { final Map<String, dynamic> defaultSchedule = {};
for (var i = 1; i <= 7; i++) {
defaultSchedule['$i'] = {
'habilitado': false, 'habilitado': false,
'jornadaContinua': false, 'jornadaContinua': false,
'range1Hour1': null, 'range1Hour1': null,
'range1Hour2': null, 'range1Hour2': null,
'range2Hour1': null, 'range2Hour1': null,
'range2Hour2': 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
}
};
try { try {
await FirebaseFirestore.instance await ApiService.instance
.collection('users') .patch('/users/me', {'horario': defaultSchedule});
.doc(uid)
.update({'horario': defaultSchedule});
Navigator.pop(context); Navigator.pop(context);
} catch (e) { } catch (e) {
print(e); print(e);
@@ -222,9 +153,7 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (BuildContext context) {
return HorarioScreen( return HorarioScreen(horarios: _horarios!);
horarios: _horarios!,
);
}, },
), ),
); );
@@ -232,30 +161,19 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
} }
Future<void> updateInfo() async { Future<void> updateInfo() async {
if (_opcionalAddressController.text.isNotEmpty) { final Map<String, dynamic> body = {};
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});
}
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 (settings?.domicilios == false) {
if (sitioValue) { if (sitioValue) {
FirebaseFirestore.instance ubicacion = 'sitio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else { } else {
Get.snackbar( Get.snackbar(
'Elige como vas a dar tu servicio', 'Elige como vas a dar tu servicio',
@@ -267,29 +185,15 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
); );
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': null});
return; return;
} }
} else { } else {
if (domicilioValue && sitioValue) { if (domicilioValue && sitioValue) {
FirebaseFirestore.instance ubicacion = 'ambos';
.collection('users')
.doc(uid)
.update({'ubicacion': 'ambos'});
} else if (domicilioValue) { } else if (domicilioValue) {
FirebaseFirestore.instance ubicacion = 'domicilio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'domicilio'});
} else if (sitioValue) { } else if (sitioValue) {
FirebaseFirestore.instance ubicacion = 'sitio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else { } else {
Get.snackbar( Get.snackbar(
'Elige como vas a dar tu servicio', 'Elige como vas a dar tu servicio',
@@ -304,18 +208,19 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
return; return;
} }
} }
body['ubicacion'] = ubicacion;
body['paymentMethods'] = paymentMethods;
try { if (imagen_to_upload != null) {
if (imagen_to_upload == null) { final url = await _uploadImageFile(imagen_to_upload!);
} else { if (url != null) body['banner_picture'] = url;
updateImage(photoTemp);
//image
}
} catch (e) {
print('Error al actualizar la imagen de perfil $e');
} }
updatePaymentMethods(); try {
await ApiService.instance.patch('/users/me', body);
} catch (e) {
print('Error updating pro info: $e');
}
if (settings?.domicilios == false && sitioValue == false) { if (settings?.domicilios == false && sitioValue == false) {
Get.defaultDialog( Get.defaultDialog(
@@ -324,9 +229,7 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
'Si no eliges servicio en sitio, no serás visible para los usuarios.', 'Si no eliges servicio en sitio, no serás visible para los usuarios.',
actions: [ actions: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () => Get.back(),
Get.back();
},
child: const Text('Entendido'), child: const Text('Entendido'),
), ),
], ],
@@ -339,51 +242,6 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
); );
Navigator.pop(context); Navigator.pop(context);
} }
return;
}
Future<void> 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<bool> 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<void> _showChoiceDialog(BuildContext context) async { Future<void> _showChoiceDialog(BuildContext context) async {
@@ -434,34 +292,14 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
void toggleDomicilio(bool newValue) { void toggleDomicilio(bool newValue) {
setState(() { setState(() {
domicilioValue = newValue; domicilioValue = newValue;
if (newValue == false && sitioValue == false) { if (newValue == false && sitioValue == false) sitioValue = true;
sitioValue = true;
}
}); });
} }
void toggleSitio(bool newValue) { void toggleSitio(bool newValue) {
setState(() { setState(() {
sitioValue = newValue; sitioValue = newValue;
if (newValue == false && domicilioValue == false) { if (newValue == false && domicilioValue == false) domicilioValue = true;
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<String, bool>.from(doc['paymentMethods'] ?? {});
});
}
}); });
} }
@@ -471,41 +309,24 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
return Scaffold( return Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Perfil profesional'), label: 'Perfil profesional'),
body: SingleChildScrollView( body: SingleChildScrollView(
reverse: true, reverse: true,
child: Column( child: Column(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () => _showChoiceDialog(context),
_showChoiceDialog(context); child: imagen_to_upload != null
}, ? LocalPhoto(file: imagen_to_upload!)
child: Container( : ReferenceBannerPhoto(ref: _bannerUrl),
child: (imagen_to_upload != null)
? LocalPhoto(
file: imagen_to_upload!,
)
: ReferenceBannerPhoto(
ref: storage.ref().child(_photo),
),
),
), ),
if (settings?.tarifas == true) if (settings?.tarifas == true)
const Divider( const Divider(color: Colors.white, height: 12),
color: Colors.white,
height: 12,
),
if (settings?.tarifas == true) if (settings?.tarifas == true)
customSwitch( customSwitch('Tarifa', tarifaValue, (value) {
'Tarifa', setState(() => tarifaValue = value);
false, }),
(value) {
tarifaValue = value;
},
),
tarifaValue tarifaValue
? Padding( ? Padding(
padding: padding:
@@ -523,16 +344,11 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
], ],
), ),
], ],
) ).animate().moveY(duration: const Duration(milliseconds: 100)),
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
) )
: const SizedBox(), : const SizedBox(),
if (settings?.domicilios == true) if (settings?.domicilios == true)
const Divider( const Divider(color: Colors.white, height: 12),
color: Colors.white,
height: 12,
),
if (settings?.domicilios == true) if (settings?.domicilios == true)
customSwitch( customSwitch(
'Servicio a domicilio', domicilioValue, toggleDomicilio), 'Servicio a domicilio', domicilioValue, toggleDomicilio),
@@ -557,9 +373,7 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
) as String?; ) as String?;
if (direccion != null) { if (direccion != null) {
setState(() { setState(() => _direccion = direccion);
_direccion = direccion;
});
} }
}, },
decoration: InputDecoration( decoration: InputDecoration(
@@ -576,9 +390,7 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
hintText: 'Oficina / Piso / Conjunto'), hintText: 'Oficina / Piso / Conjunto'),
), ),
], ],
) ).animate().moveY(duration: const Duration(milliseconds: 100)),
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
) )
: const SizedBox(), : const SizedBox(),
const Divider(), const Divider(),
@@ -590,19 +402,14 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
children: [ children: [
const Text( const Text(
'Metodos de pago', 'Metodos de pago',
style: TextStyle( style: TextStyle(color: Colors.black, fontSize: 17),
color: Colors.black,
fontSize: 17,
),
), ),
for (var entry in paymentMethods.entries) for (var entry in paymentMethods.entries)
PrimaryCheckbox( PrimaryCheckbox(
text: entry.key, text: entry.key,
initialValue: entry.value, initialValue: entry.value,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() => paymentMethods[entry.key] = value);
paymentMethods[entry.key] = value;
});
}, },
), ),
], ],
@@ -615,143 +422,20 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
width: double.infinity, width: double.infinity,
child: Text( child: Text(
'Horario estandar', 'Horario estandar',
style: TextStyle( style: TextStyle(color: Colors.black, fontSize: 17),
color: Colors.black,
fontSize: 17,
),
), ),
), ),
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: createSchedules,
createSchedules();
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
child: Table( child: _scheduleTable(context),
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)),
),
),
],
),
],
),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
PrimaryButton( PrimaryButton(
onPressed: () { onPressed: updateInfo,
updateInfo();
},
text: 'Guardar', text: 'Guardar',
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -761,6 +445,33 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
); );
} }
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( Widget customSwitch(
String text, String text,
bool switchValue, bool switchValue,
@@ -784,10 +495,7 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
), ),
Transform.scale( Transform.scale(
scale: 1.2, scale: 1.2,
child: Switch( child: Switch(value: switchValue, onChanged: onChanged),
value: switchValue,
onChanged: onChanged,
),
), ),
], ],
), ),
@@ -796,16 +504,10 @@ class _ProfileProScreenState extends State<ProfileProScreen> {
} }
String timeList(Schedule? schedule, BuildContext context) { String timeList(Schedule? schedule, BuildContext context) {
if (schedule == null) { if (schedule == null || !schedule.habilitado) return 'N/A';
return 'N/A';
}
if (!schedule.habilitado) {
return 'N/A';
}
if (schedule.jornadaContinua) { if (schedule.jornadaContinua) {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}';
} 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.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}';
} }
} }
@@ -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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/components/schedule_picker.dart';
import 'package:prosappco/src/models/setting_model.dart'; import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/presentation/screens/horario.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/presentation/screens/ubicacion.dart';
import 'package:prosappco/src/services/api_service.dart';
class ProfileProWebScreen extends StatefulWidget { class ProfileProWebScreen extends StatefulWidget {
const ProfileProWebScreen({super.key}); const ProfileProWebScreen({super.key});
@@ -36,11 +33,8 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
bool domicilioValue = true; bool domicilioValue = true;
bool tarifaValue = false; bool tarifaValue = false;
bool sitioValue = false; bool sitioValue = false;
var photoTemp = '';
var _direccion = '...';
var _ubicacion = '...'; var _ubicacion = '...';
var _opcionalAddress = '...';
int _tarifa = 0; int _tarifa = 0;
SettingModel? settings; SettingModel? settings;
@@ -49,110 +43,78 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (settings == null) { SettingModel.getSettings()
SettingModel.getSettings().then( .then((v) => setState(() => settings = v));
(SettingModel value) => setState(() => settings = value), Schedule.getHorarios(uid.toString())
); .then((data) => setState(() => _horarios = data));
} _loadProData();
}
final uid = AuthenticationRepository.instance.getCurrentUserUid(); Future<void> _loadProData() async {
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/auth/me');
final pro = data['professionals'] as Map<String, dynamic>?;
if (_horarios == null) { if (mounted) {
Schedule.getHorarios(uid.toString()).then( setState(() {
(Map<String, Schedule> data) { final address = data['address'] ?? '';
setState(() { if (address.isNotEmpty) {
_horarios = data; _ubicationController.text = address;
}); }
},
);
}
if (_direccion == '...') { final opcAddr =
AuthenticationRepository.instance pro?['opcional_address'] ?? data['opcional_address'] ?? '';
.getAddress(uid.toString()) if (opcAddr.isNotEmpty) {
.then((String s) => setState(() { _opcionalAddressController.text = opcAddr;
_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;
if (_opcionalAddress != '...') { _ubicacion = pro?['ubicacion'] ?? data['ubicacion'] ?? '...';
_opcionalAddressController.text = _opcionalAddress; if (_ubicacion == 'ambos') {
} sitioValue = true;
}, domicilioValue = true;
), } else if (_ubicacion == 'sitio') {
); sitioValue = true;
} } else if (_ubicacion == 'domicilio') {
if (_tarifa == 0) { domicilioValue = true;
AuthenticationRepository.instance.getTarifa(uid.toString()).then( }
(s) => setState(
() {
_tarifa = s;
if (_tarifa != 0) { _tarifa = pro?['rate'] != null
tarifaValue = true; ? (double.tryParse(pro!['rate'].toString())?.toInt() ?? 0)
_tarifaController.text = _tarifa.toString(); : 0;
} if (_tarifa != 0) {
}, tarifaValue = true;
), _tarifaController.text = _tarifa.toString();
); }
});
}
} catch (e) {
print('Error loading pro data: $e');
} }
} }
Future<void> updateInfo() async { Future<void> updateInfo() async {
final Map<String, dynamic> body = {};
if (_ubicationController.text.isNotEmpty) { if (_ubicationController.text.isNotEmpty) {
FirebaseFirestore.instance.collection('users').doc(uid).update({ body['address'] = _ubicationController.text;
'address': _ubicationController.text, if (latUser != 0.0) {
if (latUser != 0.0) 'latitude': latUser, body['latitude'] = latUser;
if (latUser != 0.0) 'longitude': lngUser, body['longitude'] = lngUser;
}); }
} }
if (_opcionalAddressController.text.isNotEmpty) { if (_opcionalAddressController.text.isNotEmpty) {
FirebaseFirestore.instance body['opcional_address'] = _opcionalAddressController.text;
.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['rate'] = tarifaValue && _tarifaController.text.isNotEmpty
? int.parse(_tarifaController.text)
: 0;
String? ubicacion;
if (settings?.domicilios == false) { if (settings?.domicilios == false) {
if (sitioValue) { if (sitioValue) {
FirebaseFirestore.instance ubicacion = 'sitio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else { } else {
Get.snackbar( Get.snackbar(
'Elige como vas a dar tu servicio', 'Elige como vas a dar tu servicio',
@@ -164,29 +126,15 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
); );
FirebaseFirestore.instance
.collection('users')
.doc(uid)
.update({'ubicacion': null});
return; return;
} }
} else { } else {
if (domicilioValue && sitioValue) { if (domicilioValue && sitioValue) {
FirebaseFirestore.instance ubicacion = 'ambos';
.collection('users')
.doc(uid)
.update({'ubicacion': 'ambos'});
} else if (domicilioValue) { } else if (domicilioValue) {
FirebaseFirestore.instance ubicacion = 'domicilio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'domicilio'});
} else if (sitioValue) { } else if (sitioValue) {
FirebaseFirestore.instance ubicacion = 'sitio';
.collection('users')
.doc(uid)
.update({'ubicacion': 'sitio'});
} else { } else {
Get.snackbar( Get.snackbar(
'Elige como vas a dar tu servicio', 'Elige como vas a dar tu servicio',
@@ -201,6 +149,13 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
return; 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) { if (settings?.domicilios == false && sitioValue == false) {
Get.defaultDialog( Get.defaultDialog(
@@ -209,9 +164,7 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
'Si no eliges servicio en sitio, no serás visible para los usuarios.', 'Si no eliges servicio en sitio, no serás visible para los usuarios.',
actions: [ actions: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () => Get.back(),
Get.back();
},
child: const Text('Entendido'), child: const Text('Entendido'),
), ),
], ],
@@ -224,97 +177,24 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
); );
Navigator.pop(context); Navigator.pop(context);
} }
return;
}
Future<void> 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 { void createSchedules() async {
if (_horarios == null || _horarios!.isEmpty) { if (_horarios == null || _horarios!.isEmpty) {
final defaultSchedule = { final Map<String, dynamic> defaultSchedule = {};
'1': { for (var i = 1; i <= 7; i++) {
defaultSchedule['$i'] = {
'habilitado': false, 'habilitado': false,
'jornadaContinua': false, 'jornadaContinua': false,
'range1Hour1': null, 'range1Hour1': null,
'range1Hour2': null, 'range1Hour2': null,
'range2Hour1': null, 'range2Hour1': null,
'range2Hour2': 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
}
};
try { try {
await FirebaseFirestore.instance await ApiService.instance
.collection('users') .patch('/users/me', {'horario': defaultSchedule});
.doc(uid)
.update({'horario': defaultSchedule});
Navigator.pop(context); Navigator.pop(context);
} catch (e) { } catch (e) {
print(e); print(e);
@@ -324,53 +204,24 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (BuildContext context) {
return HorarioScreen( return HorarioScreen(horarios: _horarios!);
horarios: _horarios!,
);
}, },
), ),
); );
} }
} }
Future<bool> 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) { void toggleDomicilio(bool newValue) {
setState(() { setState(() {
domicilioValue = newValue; domicilioValue = newValue;
if (newValue == false && sitioValue == false) { if (newValue == false && sitioValue == false) sitioValue = true;
sitioValue = true;
}
}); });
} }
void toggleSitio(bool newValue) { void toggleSitio(bool newValue) {
setState(() { setState(() {
sitioValue = newValue; sitioValue = newValue;
if (newValue == false && domicilioValue == false) { if (newValue == false && domicilioValue == false) domicilioValue = true;
domicilioValue = true;
}
}); });
} }
@@ -378,27 +229,18 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context);
},
label: 'Perfil profesional'), label: 'Perfil profesional'),
body: SingleChildScrollView( body: SingleChildScrollView(
reverse: true, reverse: true,
child: Column( child: Column(
children: [ children: [
if (settings?.tarifas == true) if (settings?.tarifas == true)
const Divider( const Divider(color: Colors.white, height: 12),
color: Colors.white,
height: 12,
),
if (settings?.tarifas == true) if (settings?.tarifas == true)
customSwitch( customSwitch('Tarifa', tarifaValue, (value) {
'Tarifa', setState(() => tarifaValue = value);
tarifaValue, }),
(value) {
tarifaValue = value;
},
),
tarifaValue tarifaValue
? Padding( ? Padding(
padding: padding:
@@ -416,16 +258,11 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
], ],
), ),
], ],
) ).animate().moveY(duration: const Duration(milliseconds: 100)),
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
) )
: const SizedBox(), : const SizedBox(),
if (settings?.domicilios == true) if (settings?.domicilios == true)
const Divider( const Divider(color: Colors.white, height: 12),
color: Colors.white,
height: 12,
),
if (settings?.domicilios == true) if (settings?.domicilios == true)
customSwitch( customSwitch(
'Servicio a domicilio', domicilioValue, toggleDomicilio), 'Servicio a domicilio', domicilioValue, toggleDomicilio),
@@ -474,9 +311,7 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
hintText: 'Oficina / Piso / Conjunto'), hintText: 'Oficina / Piso / Conjunto'),
), ),
], ],
) ).animate().moveY(duration: const Duration(milliseconds: 100)),
.animate()
.moveY(duration: const Duration(milliseconds: 100)),
) )
: const SizedBox(), : const SizedBox(),
const Divider(), const Divider(),
@@ -486,146 +321,43 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
width: double.infinity, width: double.infinity,
child: Text( child: Text(
'Horario estandar', 'Horario estandar',
style: TextStyle( style: TextStyle(color: Colors.black, fontSize: 15),
color: Colors.black,
fontSize: 15,
),
), ),
), ),
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: createSchedules,
createSchedules();
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
child: Table( child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(), defaultColumnWidth: const IntrinsicColumnWidth(),
children: [ children: [
TableRow( for (var entry in {
children: [ '1': 'Lunes',
'2': 'Martes',
'3': 'Miercoles',
'4': 'Jueves',
'5': 'Viernes',
'6': 'Sabado',
'7': 'Domingo',
}.entries)
TableRow(children: [
TableCell( TableCell(
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: const Text('Lunes'), child: Text(entry.value))),
),
),
TableCell( TableCell(
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text(timeList(_horarios?['1'], context)), child:
), Text(timeList(_horarios?[entry.key], 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)),
),
),
],
),
], ],
), ),
), ),
), ),
PrimaryButtom( PrimaryButtom(onPressed: updateInfo, label: 'Guardar'),
onPressed: () { const SizedBox(height: 20),
updateInfo();
},
label: 'Guardar'),
const SizedBox(
height: 20,
)
], ],
), ),
), ),
@@ -643,18 +375,12 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
Expanded( Expanded(
child: Text( child: Text(
text, text,
style: const TextStyle( style: const TextStyle(fontSize: 15, color: Colors.black),
fontSize: 15,
color: Colors.black,
),
), ),
), ),
Transform.scale( Transform.scale(
scale: 1.2, scale: 1.2,
child: Switch( child: Switch(value: switchValue, onChanged: onChanged),
value: switchValue,
onChanged: onChanged,
),
), ),
], ],
), ),
@@ -663,16 +389,10 @@ class _ProfileProWebScreenState extends State<ProfileProWebScreen> {
} }
String timeList(Schedule? schedule, BuildContext context) { String timeList(Schedule? schedule, BuildContext context) {
if (schedule == null) { if (schedule == null || !schedule.habilitado) return 'N/A';
return 'N/A';
}
if (!schedule.habilitado) {
return 'N/A';
}
if (schedule.jornadaContinua) { if (schedule.jornadaContinua) {
return '${schedule.range1Hour1?.format(context).toString()} - ${schedule.range2Hour2?.format(context).toString()}'; return '${schedule.range1Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}';
} 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.range1Hour2?.format(context)}; ${schedule.range2Hour1?.format(context)} - ${schedule.range2Hour2?.format(context)}';
} }
} }
@@ -1,9 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.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/bottom_sheet.dart';
import 'package:prosappco/src/components/column_padding.dart'; import 'package:prosappco/src/components/column_padding.dart';
import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart'; import 'package:prosappco/src/presentation/widgets/shared/primary_button.dart';
@@ -90,68 +88,54 @@ class _RegisterScreenState extends State<RegisterScreen> {
alineacion: MainAxisAlignment.start, alineacion: MainAxisAlignment.start,
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
children: [ children: [
!isIOS && !kIsWeb && settings?.google == true const SizedBox(),
? 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: <Widget>[
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(),
Form( Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ 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( const Padding(
padding: EdgeInsets.only(bottom: 5), padding: EdgeInsets.only(bottom: 5),
child: Align( child: Align(
@@ -334,6 +318,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
.registerUser( .registerUser(
controller.email.text.trim(), controller.email.text.trim(),
controller.password.text.trim(), controller.password.text.trim(),
controller.name.text.trim(),
) )
.then((value) => .then((value) =>
Provider.of<UserProvider>(context, listen: false) Provider.of<UserProvider>(context, listen: false)
@@ -445,6 +430,46 @@ class _RegisterScreenState extends State<RegisterScreen> {
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ 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( const Padding(
padding: EdgeInsets.only(bottom: 5), padding: EdgeInsets.only(bottom: 5),
child: Align( child: Align(
@@ -639,6 +664,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
.registerUser( .registerUser(
controller.email.text.trim(), controller.email.text.trim(),
controller.password.text.trim(), controller.password.text.trim(),
controller.name.text.trim(),
) )
.then((value) => .then((value) =>
Provider.of<UserProvider>(context, Provider.of<UserProvider>(context,
@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
@@ -96,24 +95,14 @@ class ResetPasswordScreen extends StatelessWidget {
), ),
const SizedBox(height: 80), const SizedBox(height: 80),
PrimaryButtom( PrimaryButtom(
onPressed: () async { onPressed: () {
try { ScaffoldMessenger.of(context).showSnackBar(
await FirebaseAuth.instance.sendPasswordResetEmail( const SnackBar(
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(
content: Text( 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'), label: 'Enviar'),
], ],
+68 -77
View File
@@ -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/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.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/photo_view.dart';
import 'package:prosappco/src/components/pop_appbar.dart'; import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/user_model.dart'; import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class ScoreScreen extends StatefulWidget { class ScoreScreen extends StatefulWidget {
final Event evento; final Event evento;
@@ -19,32 +19,59 @@ class ScoreScreen extends StatefulWidget {
class ScoreScreenState extends State<ScoreScreen> { class ScoreScreenState extends State<ScoreScreen> {
TextEditingController commentController = TextEditingController(); TextEditingController commentController = TextEditingController();
Reference? ref_photo; String? photoUrl;
String nombre = ''; String nombre = '';
double _rating = 1.0; double _rating = 1.0;
final myUid = AuthenticationRepository.instance.getCurrentUserUid();
@override
void initState() {
super.initState();
_loadUserInfo();
}
Future<void> _loadUserInfo() async {
final targetId = (myUid != widget.evento.userId)
? widget.evento.userId
: widget.evento.professionalId;
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$targetId');
if (mounted) {
setState(() {
nombre = data['name'] ?? '';
photoUrl = data['picture'];
});
}
} catch (e) {
print('Error loading user for score: $e');
}
}
Future<void> _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 @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: PopAppbar( appBar: PopAppbar(
onPressed: () { onPressed: () {
@@ -59,7 +86,7 @@ class ScoreScreenState extends State<ScoreScreen> {
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.symmetric(horizontal: 5),
child: ReferencePhoto( child: ReferencePhoto(
ref: ref_photo, ref: photoUrl,
size: 50, size: 50,
sizeCircle: 50, sizeCircle: 50,
sizeIcon: 35, sizeIcon: 35,
@@ -119,60 +146,24 @@ class ScoreScreenState extends State<ScoreScreen> {
), ),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: Column( child: ElevatedButton(
children: [ onPressed: _submit,
ElevatedButton( style: ElevatedButton.styleFrom(
onPressed: () { backgroundColor: const Color(0xFF2BA4EC),
if (widget.pro) { shape: RoundedRectangleBorder(
FirebaseFirestore.instance borderRadius: BorderRadius.circular(50),
.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,
),
),
), ),
], elevation: 0,
minimumSize: const Size(230, 60),
),
child: const Text(
'Enviar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
), ),
), ),
], ],
+49 -255
View File
@@ -1,7 +1,4 @@
import 'dart:convert'; 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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.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:intl/intl.dart';
import 'package:prosappco/src/presentation/screens/service_after.dart'; import 'package:prosappco/src/presentation/screens/service_after.dart';
import 'package:prosappco/src/presentation/screens/ubicacion.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/components/network_utility.dart';
import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart'; import 'package:prosappco/src/presentation/widgets/shared/warning_snackbar.dart';
import 'package:prosappco/src/providers/user_provider.dart'; import 'package:prosappco/src/providers/user_provider.dart';
@@ -31,54 +27,6 @@ class ServiceWebScreen extends StatefulWidget {
} }
class _ServiceOldScreenState extends State<ServiceWebScreen> { class _ServiceOldScreenState extends State<ServiceWebScreen> {
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<void> sendPushNotification(String pro) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': 'alguien a solicitado tus servicios',
'title': 'Nueva solicitud',
},
'priority': 'high',
'data': <String, dynamic>{
'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(); EventoService eventoService = EventoService();
String ubicacion = ''; String ubicacion = '';
final TextEditingController _locationController = TextEditingController(); final TextEditingController _locationController = TextEditingController();
@@ -97,43 +45,38 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
final DateFormat formatter = DateFormat('dd/MM/yyyy'); final DateFormat formatter = DateFormat('dd/MM/yyyy');
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
List<dynamic> _placesList = []; List<dynamic> _placesList = [];
String selectedPlace = '';
String _coordsOfCity = '0.0,0.0'; String _coordsOfCity = '0.0,0.0';
DateTime? _selectedDate; DateTime? _selectedDate;
TimeOfDay? _selectedTime; TimeOfDay? _selectedTime;
GoogleMapController? googleMapController; GoogleMapController? googleMapController;
Set<Marker> markers = {}; Set<Marker> markers = {};
final uid = AuthenticationRepository.instance.getCurrentUserUid();
BitmapDescriptor? _markerIcon;
String _ciudad = '...';
Future<Position> _determinePosition() async { Future<Position> _determinePosition() async {
bool serviceEnabled; bool serviceEnabled;
LocationPermission permission; LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled(); 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(); permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
return Future.error('Location permission denied'); return Future.error('Location permission denied');
} }
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
return Future.error('Location permissions are permanently denied'); return Future.error('Location permissions are permanently denied');
} }
Position position = await Geolocator.getCurrentPosition(); return Geolocator.getCurrentPosition();
return position;
} }
Future<void> _selectDate(BuildContext context) async { Future<void> _selectDate(BuildContext context) async {
@@ -142,17 +85,10 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
initialDate: now, initialDate: now,
firstDate: now, firstDate: now,
lastDate: DateTime(now.year + 1), lastDate: DateTime(now.year + 1),
// builder: (context, child) {
// return Theme(data: ThemeData.dark(), child: child!);
// },
); );
if (picked != null && picked != _selectedDate) { if (picked != null && picked != _selectedDate) {
if (mounted) { if (mounted) setState(() => _selectedDate = picked);
setState(() {
_selectedDate = picked;
});
}
} }
} }
@@ -161,73 +97,31 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
context: context, context: context,
initialTime: TimeOfDay.now(), initialTime: TimeOfDay.now(),
); );
if (pickedTime != null) { if (pickedTime != null) {
if (mounted) { if (mounted) setState(() => _selectedTime = pickedTime);
setState(() {
_selectedTime = pickedTime;
});
}
} }
} }
Future<List<DocumentSnapshot<Map<String, dynamic>>>> 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 { void _setInitialCameraPosition(String coordsOfCity) async {
List<String> coords = coordsOfCity.split(','); List<String> coords = coordsOfCity.split(',');
double lat = double.parse(coords[0]); double lat = double.parse(coords[0]);
double lng = double.parse(coords[1]); double lng = double.parse(coords[1]);
googleMapController?.moveCamera( googleMapController?.moveCamera(
CameraUpdate.newLatLngZoom( CameraUpdate.newLatLngZoom(LatLng(lat, lng), 14.4746),
LatLng(lat, lng),
14.4746,
),
); );
try { try {
Position position = await _determinePosition(); Position position = await _determinePosition();
googleMapController?.animateCamera( googleMapController?.animateCamera(
CameraUpdate.newCameraPosition( CameraUpdate.newCameraPosition(
CameraPosition( CameraPosition(
target: LatLng( target: LatLng(position.latitude, position.longitude),
position.latitude,
position.longitude,
),
zoom: 17, zoom: 17,
), ),
), ),
); );
if (mounted) { if (mounted) setState(() {});
setState(() {});
}
} catch (e) { } catch (e) {
print('Error: $e'); print('Error: $e');
} }
@@ -240,90 +134,22 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
_profesionalController.dispose(); _profesionalController.dispose();
_serviceTypeController.dispose(); _serviceTypeController.dispose();
_observacionController.dispose(); _observacionController.dispose();
super.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 @override
void initState() { void initState() {
super.initState(); super.initState();
if (_ciudad == '...') { // Load city from in-memory user
AuthenticationRepository.instance final currentUser = AuthenticationRepository.instance.currentUser.value;
.getCity(uid.toString()) _ciudad = currentUser?.city ?? '...';
.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();
BitmapDescriptor.fromAssetImage( BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(6, 6)), 'images/pro_marke.png') const ImageConfiguration(size: Size(6, 6)), 'images/pro_marke.png')
.then((icon) { .then((icon) {
if (mounted) { if (mounted) setState(() => _markerIcon = icon);
setState(() {
_markerIcon = icon;
});
}
}); });
updateMarkersForServiceType(_serviceType);
} }
static CameraPosition initialCameraPosition = const CameraPosition( static CameraPosition initialCameraPosition = const CameraPosition(
@@ -331,40 +157,18 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
zoom: 14.4746, zoom: 14.4746,
); );
void setInitialCameraPosition(String coordsOfCity) {
if (coordsOfCity != '0.0,0.0') {
List<String> 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 latUser = 0.0;
double lngUser = 0.0; double lngUser = 0.0;
var coordinates; var coordinates;
Future<String> getLocationName(double latitude, double longitude) async { Future<String> getLocationName(double latitude, double longitude) async {
String address;
List<Placemark> placemarks = List<Placemark> placemarks =
await placemarkFromCoordinates(latitude, longitude); await placemarkFromCoordinates(latitude, longitude);
Placemark place = placemarks[0]; Placemark place = placemarks[0];
if (place.thoroughfare != '' || place.subThoroughfare != '') { if (place.thoroughfare != '' || place.subThoroughfare != '') {
address = return "${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}";
"${place.thoroughfare} ${place.subThoroughfare} ${place.subLocality}, ${place.locality}, ${place.administrativeArea}";
} else {
address = '';
} }
return address; return '';
} }
void placeAutoComplete(String query) async { void placeAutoComplete(String query) async {
@@ -374,7 +178,6 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
}); });
String? response = await NetworkUtility.fetchUrl(uri); String? response = await NetworkUtility.fetchUrl(uri);
if (response != null) { if (response != null) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -396,10 +199,7 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
elevation: 0, elevation: 0,
title: const Text( title: const Text(
'Prosapp', 'Prosapp',
style: TextStyle( style: TextStyle(color: Colors.white, fontSize: 20),
color: Colors.white,
fontSize: 20,
),
)), )),
body: Center( body: Center(
child: SizedBox( child: SizedBox(
@@ -443,9 +243,9 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
professionalTarifa = profesional.tarifa ?? 0; professionalTarifa = profesional.tarifa ?? 0;
professionalUbicacion = profesional.ubicacion; professionalUbicacion = profesional.ubicacion;
professionalAddress = _ubicationController.text; professionalAddress = _ubicationController.text;
if (profesional.token != '') { if (profesional.token != null &&
profesional.token != '') {
professionalToken = profesional.token!; professionalToken = profesional.token!;
print(professionalToken);
} }
if (ubicacion == 'sitio') { if (ubicacion == 'sitio') {
@@ -583,44 +383,38 @@ class _ServiceOldScreenState extends State<ServiceWebScreen> {
DateTime time2 = DateTime time2 =
combinedDate.add(const Duration(hours: 2)); combinedDate.add(const Duration(hours: 2));
UserModel.getUser(uid.toString()).then((value) { eventoService
eventoService .createEvent(
.createEvent( user!.name,
value.name, _observacionController.text,
_observacionController.text, '$_selectedDate',
'$_selectedDate', '$combinedDate',
'$combinedDate', '$time2',
'$time2', professionalId,
professionalId, ubicacion,
ubicacion, professionalAddress,
professionalAddress, latUser,
latUser, lngUser,
lngUser, 'pendiente',
'pendiente', professionalTarifa == 0 ? 0 : professionalTarifa,
professionalTarifa == 0 ? 0 : professionalTarifa, false,
false, false,
false, )
) .then((value) {
.then((value) { Navigator.pushReplacement(
if (professionalToken != '') { context,
sendPushNotification(professionalToken); CupertinoPageRoute(
} builder: (BuildContext context) {
Navigator.pushReplacement( return ServiceAfterScreen(eventoId: value);
context, },
CupertinoPageRoute( ),
builder: (BuildContext context) { );
return ServiceAfterScreen(
eventoId: value,
);
},
),
);
});
}); });
} catch (e) { } catch (e) {
WarningSnackbar.show( WarningSnackbar.show(
title: 'Llena todos los campos', title: 'Llena todos los campos',
message: 'Asegurate de llenar todos los campos.', message:
'Asegurate de llenar todos los campos.',
); );
} }
} }
+128 -162
View File
@@ -1,17 +1,56 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:intl/intl.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/components/drawer_professional.dart';
import 'package:prosappco/src/models/event_model.dart'; import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart'; import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/presentation/screens/cita.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}); SolicitudScreen({super.key});
DateTime today = DateTime.now(); @override
State<SolicitudScreen> createState() => _SolicitudScreenState();
}
class _SolicitudScreenState extends State<SolicitudScreen> {
List<Event> eventos = [];
bool loading = true;
final myUid = AuthenticationRepository.instance.getCurrentUserUid();
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final List<dynamic> data = await ApiService.instance
.get('/services?professionalId=$myUid&status=pendiente');
final List<Event> loaded = [];
for (final e in data) {
final event = Event.fromJson(e as Map<String, dynamic>);
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,176 +58,103 @@ class SolicitudScreen extends StatelessWidget {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
iconTheme: const IconThemeData( iconTheme: const IconThemeData(color: Colors.black),
color: Colors.black,
),
title: const Text( title: const Text(
'Solicitudes', 'Solicitudes',
style: TextStyle( style: TextStyle(color: Colors.black),
color: Colors.black,
),
), ),
), ),
drawer: DrawerProfessional(), drawer: DrawerProfessional(),
body: SingleChildScrollView( body: loading
child: Column( ? const Center(child: CircularProgressIndicator())
children: [ : SingleChildScrollView(
_eventList(), child: Column(children: [_eventList(context)]),
], ),
),
),
), ),
); );
} }
Widget _eventList() { Widget _eventList(BuildContext context) {
return StreamBuilder<List<Event>>( if (eventos.isEmpty) {
stream: FirebaseFirestore.instance return const Padding(
.collection('services') padding: EdgeInsets.symmetric(vertical: 50),
.where('professional_id', isEqualTo: uid) child: Center(child: Text('No tienes citas')),
.where('status', whereIn: ['pendiente', '']) );
.snapshots() }
.asyncMap((snapshot) async { return Column(
try { children: [
List<Event> eventos = []; ...eventos.map(
for (var element in snapshot.docs) { (event) => ListTile(
final event = Event.fromJson(element.data()); onTap: () {
event.scoresModel = Navigator.push(
await ScoresModel.scoreTo(event.userId, false, false); context,
event.id = element.id; CupertinoPageRoute(
eventos.add(event); builder: (BuildContext context) {
} return CitaScreen(evento: event);
return eventos; },
} catch (e) {
print('Error getByProId $e');
return [];
}
}),
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
List<Event> 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))),
],
), ),
title: Column( );
crossAxisAlignment: CrossAxisAlignment.start, },
children: [ leading: Column(
Text( mainAxisAlignment: MainAxisAlignment.center,
event.title, children: [
style: const TextStyle( Text(DateFormat('h:mm a')
color: Colors.black, .format(DateTime.parse(event.range1Hour1))),
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),
),
), ),
], 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),
),
),
],
); );
} }
} }
+14 -36
View File
@@ -1,54 +1,32 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.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/models/user_model.dart';
import 'package:prosappco/src/services/api_service.dart';
class UserProvider extends ChangeNotifier { class UserProvider extends ChangeNotifier {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final _api = ApiService.instance;
UserModel? _user; UserModel? _user;
ScoresModel? _score;
Stream<DocumentSnapshot<Map<String, dynamic>>>? _stream;
UserProvider() { UserProvider() {
initUserProvider(); loadUser();
} }
void initUserProvider() { Future<void> loadUser() async {
var uid = FirebaseAuth.instance.currentUser?.uid; final token = await _api.getToken();
if (uid == null) return; if (token == null) return;
_stream = _firestore.collection('users').doc(uid).snapshots(); try {
_stream?.listen((documentSnapshot) async { final data = await _api.get('/auth/me');
if (documentSnapshot.exists) { _user = UserModel.fromApi(data);
final data = documentSnapshot.data() as Map<String, dynamic>; notifyListeners();
_user = UserModel.fromFirestore(data); } catch (_) {}
_score = await ScoresModel.scoreTo(uid, false, false);
notifyListeners();
}
}, onDone: () {}, onError: (error) {});
} }
Future<void> initUserProvider() => loadUser();
Future<void> updateUserDataAndScores() => loadUser();
void setNullUser() { void setNullUser() {
_user = null; _user = null;
_score = null;
_stream = null;
notifyListeners(); notifyListeners();
} }
Future<void> 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<String, dynamic>;
_user = UserModel.fromFirestore(data);
_score = await ScoresModel.scoreTo(uid, false, false);
notifyListeners();
}
}
UserModel? get user => _user; UserModel? get user => _user;
ScoresModel? get score => _score;
} }
+90
View File
@@ -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<String?> getToken() async {
if (_token != null) return _token;
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
return _token;
}
Future<void> saveToken(String token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token);
}
Future<void> clearToken() async {
_token = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
}
Future<Map<String, String>> _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<dynamic> 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<dynamic> post(String path, Map<String, dynamic> 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<dynamic> patch(String path, Map<String, dynamic> 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<dynamic> delete(String path, {bool auth = true}) async {
final res = await http.delete(
Uri.parse('$_baseUrl$path'),
headers: await _headers(auth: auth),
);
return _parse(res);
}
}
+2 -52
View File
@@ -1,56 +1,6 @@
import 'package:firebase_messaging/firebase_messaging.dart'; // ponytail: FCM not applicable on web; stub so imports compile
import 'package:get/get.dart';
class FirebaseMessagingService { class FirebaseMessagingService {
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
Future<void> initializeFirebaseMessaging() async { Future<void> initializeFirebaseMessaging() async {
// Solicitar permisos de notificación si es necesario (opcional) // No-op: push notifications handled server-side
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
}
}
} }
} }
@@ -1,5 +1,4 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:prosappco/src/services/firebase_messaging.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
@@ -20,11 +19,6 @@ Future<void> initNotifications() async {
); );
await flutterLocalNotificationsPlugin.initialize(initializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings);
// Inicializar Firebase Messaging
FirebaseMessagingService firebaseMessagingService =
FirebaseMessagingService();
await firebaseMessagingService.initializeFirebaseMessaging();
} }
Future<void> showNotification(String title, String body) async { Future<void> showNotification(String title, String body) async {
-6
View File
@@ -38,17 +38,13 @@ dependencies:
location: ^5.0.3 location: ^5.0.3
flutter_polyline_points: ^2.0.0 flutter_polyline_points: ^2.0.0
google_maps_flutter: ^2.5.0 google_maps_flutter: ^2.5.0
firebase_auth: ^4.6.2
firebase_core: ^2.13.1
file_picker: ^5.3.2 file_picker: ^5.3.2
google_sign_in: ^6.1.4
provider: ^6.0.5 provider: ^6.0.5
package_info_plus: ^4.2.0 package_info_plus: ^4.2.0
get: get:
font_awesome_flutter: ^10.4.0 font_awesome_flutter: ^10.4.0
flutter_otp_text_field: flutter_otp_text_field:
intl_phone_field: intl_phone_field:
cloud_firestore:
diacritic: diacritic:
image_picker: ^1.0.4 image_picker: ^1.0.4
flutter_animate: flutter_animate:
@@ -61,12 +57,10 @@ dependencies:
community_material_icon: ^5.9.55 community_material_icon: ^5.9.55
flutter_email_sender: ^5.2.0 flutter_email_sender: ^5.2.0
webview_flutter: ^4.4.1 webview_flutter: ^4.4.1
firebase_storage: ^11.2.2
responsive_builder: ^0.7.0 responsive_builder: ^0.7.0
flutter_local_notifications: ^14.1.1 flutter_local_notifications: ^14.1.1
flutter_dialogs: ^3.0.0 flutter_dialogs: ^3.0.0
universal_html: ^2.2.3 universal_html: ^2.2.3
firebase_messaging: ^14.6.3
animated_splash_screen: ^1.3.0 animated_splash_screen: ^1.3.0
animate_do: ^3.0.2 animate_do: ^3.0.2
shimmer: ^3.0.0 shimmer: ^3.0.0