feat: migrate prosapp_web_app from Firebase to NestJS API

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-17 13:19:28 -05:00
co-authored by Claude Sonnet 4.6
parent 1c20d0c0cd
commit b95e0630de
45 changed files with 2531 additions and 5865 deletions
+2 -89
View File
@@ -1,89 +1,2 @@
// File generated by FlutterFire CLI.
// 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
View File
@@ -8,7 +8,6 @@ import 'package:prosappco/src/providers/user_provider.dart';
import 'package:prosappco/src/presentation/screens/calendar.dart';
import 'package:prosappco/src/presentation/screens/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;
}
+27 -47
View File
@@ -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) {
+9 -35
View File
@@ -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,
+30 -55
View File
@@ -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) {
+28 -56
View File
@@ -1,18 +1,16 @@
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_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) {
+15 -17
View File
@@ -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();
}
}
}
+12 -103
View File
@@ -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,
);
}
}
}
}
+3 -2
View File
@@ -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);
}
}
+45 -84
View File
@@ -1,5 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/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
View File
@@ -1,6 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/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 [];
}
}
+29 -40
View File
@@ -1,11 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:prosappco/src/authentication/authentication_repository.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/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;
}
}
+34 -62
View File
@@ -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}';
}
}
+13 -46
View File
@@ -1,4 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:prosappco/src/services/api_service.dart';
class SettingModel {
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, '
+41 -63
View File
@@ -1,75 +1,53 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:prosappco/src/presentation/screens/professional.dart';
class UserModel {
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)';
}
+171 -248
View File
@@ -1,6 +1,3 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/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,
),
),
),
],
),
),
)
],
);
}
}
+73 -206
View File
@@ -1,6 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
@@ -12,13 +9,11 @@ import 'package:prosappco/src/components/pop_appbar.dart';
import 'package:prosappco/src/models/event_model.dart';
import 'package:prosappco/src/models/scores_model.dart';
import 'package:prosappco/src/models/setting_model.dart';
import 'package:prosappco/src/models/user_model.dart';
import 'package:prosappco/src/presentation/screens/chat.dart';
import 'package:prosappco/src/presentation/screens/score.dart';
import 'package:prosappco/src/services/api_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:community_material_icon/community_material_icon.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class CitaScreen extends StatefulWidget {
final Event evento;
@@ -30,18 +25,13 @@ class CitaScreen extends StatefulWidget {
class _CitaScreenState extends State<CitaScreen> {
final uid = AuthenticationRepository.instance.getCurrentUserUid();
UserModel? user;
DateTime today = DateTime.now();
String nombre = '';
String userToken = '';
String numberPhone = '';
int tarifa = 0;
Reference? ref_photo;
String? photoUrl;
ScoresModel? scoresModel;
bool? ver = true;
bool? pro;
String proName = '';
late final FirebaseAuth _auth;
String formatCurrency(int number) {
final formatter =
@@ -49,48 +39,11 @@ class _CitaScreenState extends State<CitaScreen> {
return '\$${formatter.format(number)}';
}
Future<void> sendPushNotification(
String user, String accion, String proName) async {
try {
http.Response response = await http.post(
Uri.parse('https://fcm.googleapis.com/fcm/send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization':
'key=AAAAORdR-xU:APA91bF_wblg86jHAC-uexrXPHavYRlk5wge1Gf46m56V4J2D2L37Cp_hf46JZUzpvsWPSpqc5ewHelKI9LTifUG_s2mciMI6e5VLKo7E1R8btbNo7iaM9do2ctoyHKUm1atlZBdKaN2',
},
body: jsonEncode(
<String, dynamic>{
'notification': <String, dynamic>{
'body': accion == 'rechazo'
? '$proName a rechazado tu solicitud de servicio'
: '$proName a aprobado tu solicitud de servicio',
'title': '$proName $accion',
},
'priority': 'high',
'data': <String, dynamic>{
'click_action': 'FLUTTER_NOTIFICATION_CLICK',
'id': '1',
'status': 'done',
'screen': 'misservicios',
},
'to': user
},
),
);
response;
} catch (e) {
print('error al enviar notificacion $e');
}
}
Future<void> _openMap(double lat, double lng) async {
final Uri _url =
final Uri url =
Uri.parse('https://www.google.com/maps/search/?api=1&query=$lat,$lng');
if (!await launchUrl(_url)) {
throw Exception('Could not launch $_url');
if (!await launchUrl(url)) {
throw Exception('Could not launch $url');
}
}
@@ -103,23 +56,14 @@ class _CitaScreenState extends State<CitaScreen> {
}
SettingModel? settings;
@override
void initState() {
super.initState();
final uid = AuthenticationRepository.instance.getCurrentUserUid();
_auth = FirebaseAuth.instance;
final currentUser = _auth.currentUser;
if (currentUser != null && currentUser.displayName != null) {
proName = currentUser.displayName!;
}
if (settings == null) {
SettingModel.getSettings().then(
(SettingModel value) => setState(() {
settings = value;
}),
(SettingModel value) => setState(() => settings = value),
);
}
@@ -140,36 +84,42 @@ class _CitaScreenState extends State<CitaScreen> {
);
}
}
_loadOtherUser();
}
Future<void> _loadOtherUser() async {
final targetId = (uid != widget.evento.userId)
? widget.evento.userId
: widget.evento.professionalId;
try {
final Map<String, dynamic> data =
await ApiService.instance.get('/users/$targetId');
if (mounted) {
setState(() {
nombre = data['name'] ?? '';
photoUrl = data['picture'];
numberPhone = data['phone'] ?? '';
});
}
} catch (e) {
print('Error loading user: $e');
}
}
Future<void> _updateStatus(String status) async {
try {
await ApiService.instance
.patch('/services/${widget.evento.id}', {'status': status});
} catch (e) {
print('Error updating service status: $e');
}
}
@override
Widget build(BuildContext context) {
today.difference(DateTime.parse(widget.evento.range1Hour1));
final eventDate = DateFormat('yyyy-MM-dd').parse(widget.evento.day);
if (nombre == '') {
if (uid != widget.evento.userId) {
UserModel.getUser(widget.evento.userId).then((value) {
UserModel.getUser(uid.toString()).then((me) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
userToken = value.token ?? '';
numberPhone = value.phoneNumber ?? '';
});
});
});
} else {
UserModel.getUser(widget.evento.professionalId).then((value) {
setState(() {
nombre = value.name;
ref_photo = value.photo;
numberPhone = value.phoneNumber ?? '';
});
});
}
}
return Scaffold(
appBar: PopAppbar(
onPressed: () {
@@ -183,7 +133,7 @@ class _CitaScreenState extends State<CitaScreen> {
children: [
ListTile(
leading: ReferencePhoto(
ref: ref_photo,
ref: photoUrl,
size: 50,
sizeCircle: 50,
sizeIcon: 35,
@@ -287,7 +237,6 @@ class _CitaScreenState extends State<CitaScreen> {
)
: const SizedBox(),
const SizedBox(height: 15),
// Text('${widget.evento.range1Hour1} - ${DateTime.now()}'),
Text(
textAlign: TextAlign.center,
'"${widget.evento.description?.trim()}"',
@@ -422,28 +371,12 @@ class _CitaScreenState extends State<CitaScreen> {
ElevatedButton(
onPressed: () async {
if (widget.evento.status != 'pendiente') {
final chatDoc = FirebaseFirestore
.instance
.collection('chats')
.doc(widget.evento.id);
final chatSnapshot =
await chatDoc.get();
if (!chatSnapshot.exists ||
chatSnapshot.data()!['message'] ==
null) {
await chatDoc.set(
{
'professional_id':
widget.evento.professionalId,
'user_id': widget.evento.userId,
'message': [],
},
SetOptions(merge: true),
).catchError((error) => print(
'Error al crear el documento: $error'));
}
// Start or retrieve chat via API
try {
await ApiService.instance.post(
'/chat/start/${widget.evento.professionalId}',
{});
} catch (_) {}
Navigator.push(
context,
CupertinoPageRoute(
@@ -453,35 +386,7 @@ class _CitaScreenState extends State<CitaScreen> {
},
),
);
}
// if (widget.evento.status != 'pendiente') {
// await FirebaseFirestore.instance
// .collection('chats')
// .doc(widget.evento.id)
// .set(
// {
// 'professional_id':
// widget.evento.professionalId,
// 'user_id': widget.evento.userId,
// 'message': [],
// },
// SetOptions(
// merge:
// true)).catchError((error) => print(
// 'Error al crear el documento: $error'));
// Navigator.push(
// context,
// CupertinoPageRoute(
// builder: (BuildContext context) {
// return ChatScreen(
// eventoId: widget.evento.id);
// },
// ),
// );
// }
else {
} else {
Get.snackbar(
'El profesional aun no ha aceptado tu solicitud',
'Debes esperar a que el profesional acepte tu solicitud para poder iniciar un chat.',
@@ -539,7 +444,7 @@ class _CitaScreenState extends State<CitaScreen> {
const Expanded(child: SizedBox()),
],
)
: SizedBox(),
: const SizedBox(),
widget.evento.ubicacion == 'sitio'
? const SizedBox()
: Padding(
@@ -601,16 +506,9 @@ class _CitaScreenState extends State<CitaScreen> {
const Duration(minutes: 30) &&
ver == true)
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "iniciado"}).then(
(value) {
setState(() {
ver = false;
});
});
onPressed: () async {
await _updateStatus('iniciado');
setState(() => ver = false);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
@@ -634,19 +532,10 @@ class _CitaScreenState extends State<CitaScreen> {
),
widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('denegado');
Navigator.pushReplacementNamed(
context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
@@ -679,19 +568,10 @@ class _CitaScreenState extends State<CitaScreen> {
padding: const EdgeInsets.only(bottom: 20),
child: widget.evento.professionalId == uid
? ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "aprobado"}).then(
(value) {
if (userToken != '') {
sendPushNotification(
userToken, 'acepto', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('aprobado');
Navigator.pushReplacementNamed(
context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
@@ -713,18 +593,9 @@ class _CitaScreenState extends State<CitaScreen> {
: const SizedBox(),
),
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "denegado"}).then((value) {
if (userToken != '') {
sendPushNotification(
userToken, 'rechazo', proName);
}
Navigator.pushReplacementNamed(
context, '/solicitud');
});
onPressed: () async {
await _updateStatus('denegado');
Navigator.pushReplacementNamed(context, '/solicitud');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEC2B2B),
@@ -753,23 +624,19 @@ class _CitaScreenState extends State<CitaScreen> {
child: Column(
children: [
ElevatedButton(
onPressed: () {
FirebaseFirestore.instance
.collection("services")
.doc('${widget.evento.id}')
.update({"status": "terminado"}).then((value) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
});
onPressed: () async {
await _updateStatus('terminado');
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (BuildContext context) {
return ScoreScreen(
evento: widget.evento,
pro: pro!,
);
},
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2BA4EC),
+19 -47
View File
@@ -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',
+19 -87
View File
@@ -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) {
+28 -82
View File
@@ -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;
+189 -257
View File
@@ -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)),
]);
}
}
}
+159 -205
View File
@@ -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)),
]);
}
}
}
+2 -15
View File
@@ -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'),
),
),
],
+6 -53
View File
@@ -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
+17 -67
View File
@@ -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');
+121 -218
View File
@@ -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('An 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()) {
+107 -701
View File
@@ -1,14 +1,13 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/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'),
],
+68 -77
View File
@@ -1,12 +1,12 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_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,
),
),
),
),
],
+49 -255
View File
@@ -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.',
);
}
}
+128 -162
View File
@@ -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),
),
),
],
);
}
}
+14 -36
View File
@@ -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;
}
+90
View File
@@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
const String _baseUrl = 'https://backend.prosapp.co/api/v1';
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException(this.statusCode, this.message);
@override
String toString() => 'ApiException($statusCode): $message';
}
class ApiService {
static ApiService? _instance;
static ApiService get instance => _instance ??= ApiService._();
ApiService._();
String? _token;
Future<String?> getToken() async {
if (_token != null) return _token;
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
return _token;
}
Future<void> saveToken(String token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token);
}
Future<void> clearToken() async {
_token = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
}
Future<Map<String, String>> _headers({bool auth = true}) async {
final headers = {'Content-Type': 'application/json'};
if (auth) {
final token = await getToken();
if (token != null) headers['Authorization'] = 'Bearer $token';
}
return headers;
}
dynamic _parse(http.Response res) {
final body = jsonDecode(res.body);
if (res.statusCode >= 200 && res.statusCode < 300) return body;
final msg = body is Map ? (body['message'] ?? res.reasonPhrase) : res.reasonPhrase;
throw ApiException(res.statusCode, msg.toString());
}
Future<dynamic> get(String path, {bool auth = true}) async {
final res = await http.get(
Uri.parse('$_baseUrl$path'),
headers: await _headers(auth: auth),
);
return _parse(res);
}
Future<dynamic> post(String path, Map<String, dynamic> body, {bool auth = false}) async {
final res = await http.post(
Uri.parse('$_baseUrl$path'),
headers: await _headers(auth: auth),
body: jsonEncode(body),
);
return _parse(res);
}
Future<dynamic> patch(String path, Map<String, dynamic> body, {bool auth = true}) async {
final res = await http.patch(
Uri.parse('$_baseUrl$path'),
headers: await _headers(auth: auth),
body: jsonEncode(body),
);
return _parse(res);
}
Future<dynamic> delete(String path, {bool auth = true}) async {
final res = await http.delete(
Uri.parse('$_baseUrl$path'),
headers: await _headers(auth: auth),
);
return _parse(res);
}
}
+2 -52
View File
@@ -1,56 +1,6 @@
import 'package:firebase_messaging/firebase_messaging.dart';
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 {
-6
View File
@@ -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