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