From df9f44a8f21714fa08c54f57ae90edf7298b702b Mon Sep 17 00:00:00 2001 From: Felipe Date: Fri, 5 Apr 2024 18:40:17 -0500 Subject: [PATCH] lista de servicios --- lib/app.dart | 8 +- lib/blocs/service_bloc/service_bloc.dart | 57 ++- lib/blocs/service_bloc/service_event.dart | 9 + lib/blocs/service_bloc/service_state.dart | 9 + lib/dependency/app_di.dart | 9 +- .../lists/user_service_list_screen.dart | 67 ++- lib/screens/user/user_service_screen.dart | 418 +++++++++--------- lib/screens/user/user_services_screen.dart | 17 - .../firebase_professional_repository.dart | 17 + .../firebase_service_repository.dart | 19 +- .../firebase_user_repository.dart | 16 + .../lib/src/repositories/user_repo.dart | 2 + 12 files changed, 398 insertions(+), 250 deletions(-) delete mode 100644 lib/screens/user/user_services_screen.dart diff --git a/lib/app.dart b/lib/app.dart index fca230b..bf011e6 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -30,12 +30,8 @@ class MainApp extends StatelessWidget { create: (context) => Injector.appInstance.get(), ), BlocProvider( - create: (context) => - Injector.appInstance.get(), - ), - BlocProvider( - create: (context) => Injector.appInstance.get(), - ), + create: (context) => Injector.appInstance.get(), + ) ], child: BlocBuilder( builder: (context, state) { diff --git a/lib/blocs/service_bloc/service_bloc.dart b/lib/blocs/service_bloc/service_bloc.dart index 88f084c..96e9536 100644 --- a/lib/blocs/service_bloc/service_bloc.dart +++ b/lib/blocs/service_bloc/service_bloc.dart @@ -4,21 +4,31 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:professional_repository/professional_repository.dart'; import 'package:service_repository/service_repository.dart'; +import 'package:user_repository/user_repository.dart'; part 'service_event.dart'; part 'service_state.dart'; class ServiceBloc extends Bloc { final FirebaseServiceRepository _serviceRepository; + final UserRepository _userRepository; + final FirebaseProfessionalRepository _professionalRepository; - ServiceBloc({required FirebaseServiceRepository serviceRepository}) - : _serviceRepository = serviceRepository, + ServiceBloc({ + required FirebaseServiceRepository serviceRepository, + required UserRepository userRepository, + required FirebaseProfessionalRepository professionRepository, + }) : _serviceRepository = serviceRepository, + _userRepository = userRepository, + _professionalRepository = professionRepository, super(CreateServiceInitial()) { on(_onCreateService); on(_onLoadService); on(_onUpdateServiceStatus); + on(_onLoadServicesForUser); } void _onCreateService(CreateService event, Emitter emit) async { @@ -66,6 +76,40 @@ class ServiceBloc extends Bloc { } } + void _onLoadServicesForUser( + LoadServicesForUser event, Emitter emit) async { + try { + final servicesStream = + _serviceRepository.getServicesForUser(event.userId); + + await for (var services in servicesStream) { + final professionalIds = services.map((e) => e.professionalId); + final List users = + await _userRepository.getUsersFromIds(professionalIds); + + final usersDir = {for (var e in users) e.id: e}; + + final List professionsList = + await _professionalRepository + .getProfessionsFromIds(professionalIds); + + final professionsDir = {for (var e in professionsList) e.id: e}; + + final servicesInfo = services.map((e) { + return ServiceInfoUI( + service: e, + user: usersDir[e.professionalId]!, + professional: professionsDir[e.professionalId]!); + }).toList(); + + emit(ServicesForUserLoaded(servicesInfo)); + } + } catch (e) { + log(e.toString()); + emit(CreateServiceFailure()); + } + } + void _onUpdateServiceStatus( UpdateServiceStatus event, Emitter emit) async { try { @@ -79,3 +123,12 @@ class ServiceBloc extends Bloc { } } } + +class ServiceInfoUI { + final ServiceEntity service; + final MyUser user; + final ProfessionalEntity professional; + + ServiceInfoUI( + {required this.service, required this.user, required this.professional}); +} diff --git a/lib/blocs/service_bloc/service_event.dart b/lib/blocs/service_bloc/service_event.dart index f8a56e4..c99f680 100644 --- a/lib/blocs/service_bloc/service_event.dart +++ b/lib/blocs/service_bloc/service_event.dart @@ -26,6 +26,15 @@ class UpdateServiceStatus extends ServiceEvent { List get props => [serviceId, newStatus]; } +class LoadServicesForUser extends ServiceEvent { + final String userId; + + const LoadServicesForUser(this.userId); + + @override + List get props => [userId]; +} + class CreateService extends ServiceEvent { final String professionalId; final bool? professionalScored; diff --git a/lib/blocs/service_bloc/service_state.dart b/lib/blocs/service_bloc/service_state.dart index 2db804c..d7aa80e 100644 --- a/lib/blocs/service_bloc/service_state.dart +++ b/lib/blocs/service_bloc/service_state.dart @@ -16,6 +16,15 @@ class ServiceLoaded extends ServiceState { List get props => [service]; } +class ServicesForUserLoaded extends ServiceState { + final List services; + + const ServicesForUserLoaded(this.services); + + @override + List get props => [services]; +} + class CreateServiceInitial extends ServiceState {} class CreateServiceFailure extends ServiceState {} diff --git a/lib/dependency/app_di.dart b/lib/dependency/app_di.dart index 89b1e95..462773b 100644 --- a/lib/dependency/app_di.dart +++ b/lib/dependency/app_di.dart @@ -73,7 +73,12 @@ class AppDI { injector.get()), ); - injector.registerSingleton(() => ServiceBloc( - serviceRepository: injector.get())); + injector.registerDependency( + () => ServiceBloc( + serviceRepository: injector.get(), + userRepository: injector.get(), + professionRepository: injector.get(), + ), + ); } } diff --git a/lib/screens/lists/user_service_list_screen.dart b/lib/screens/lists/user_service_list_screen.dart index 4e291d9..e6fddec 100644 --- a/lib/screens/lists/user_service_list_screen.dart +++ b/lib/screens/lists/user_service_list_screen.dart @@ -1,4 +1,8 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; +import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; class UserServiceListScreen extends StatefulWidget { const UserServiceListScreen({super.key}); @@ -8,20 +12,61 @@ class UserServiceListScreen extends StatefulWidget { } class _UserServiceListScreenState extends State { + late final ServiceBloc serviceBloc; + + @override + void initState() { + super.initState(); + + serviceBloc = Injector.appInstance.get(); + + serviceBloc + .add(LoadServicesForUser(FirebaseAuth.instance.currentUser!.uid)); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Mis Servicios'), - ), - body: ListView.builder( - itemCount: 10, - itemBuilder: (_, __) { - return ListTile( - onTap: () {}, - title: Text('Servicio ${__ + 1}'), + return BlocProvider( + create: (context) => serviceBloc, + child: Scaffold( + appBar: AppBar( + title: const Text('Mis Servicios'), + ), + body: BlocBuilder( + builder: (context, serviceState) { + if (serviceState is ServicesForUserLoaded) { + return serviceState.services.isEmpty + ? const Center( + child: Text('No tienes servicios'), + ) + : ListView.builder( + itemCount: serviceState.services.length, + itemBuilder: (_, index) { + final serviceInfo = serviceState.services[index]; + final user = serviceInfo.user; + final professional = serviceInfo.professional; + final service = serviceInfo.service; + + return ListTile( + onTap: () {}, + title: Column( + children: [ + Text(user.name ?? ''), + Text(professional.identification), + Text(service.address), + ], + ), + ); + }, + ); + } + + return const Center( + child: CircularProgressIndicator(), ); - }), + }, + ), + ), ); } } diff --git a/lib/screens/user/user_service_screen.dart b/lib/screens/user/user_service_screen.dart index b96bb2e..a2f216c 100644 --- a/lib/screens/user/user_service_screen.dart +++ b/lib/screens/user/user_service_screen.dart @@ -41,218 +41,224 @@ class _UserServiceScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Servicio'), - ), - body: BlocBuilder( - builder: (context, state) { - if (state is ServiceLoaded) { - final service = state.service; - return FutureBuilder( - future: _getUserAndProfessionalInfo(service), - builder: (BuildContext context, - AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else { - if (snapshot.hasError) { - return Center( - child: Text('Error inesperado: ${snapshot.error}'), - ); + return BlocProvider( + create: (context) => Injector.appInstance.get(), + child: Scaffold( + appBar: AppBar( + title: const Text('Servicio'), + ), + body: BlocBuilder( + builder: (context, state) { + if (state is ServiceLoaded) { + final service = state.service; + return FutureBuilder( + future: _getUserAndProfessionalInfo(service), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); } else { - final userInfo = snapshot.data![0] as MyUser; - final professionalInfo = - snapshot.data![1] as ProfessionalEntity; + if (snapshot.hasError) { + return Center( + child: Text('Error inesperado: ${snapshot.error}'), + ); + } else { + final userInfo = snapshot.data![0] as MyUser; + final professionalInfo = + snapshot.data![1] as ProfessionalEntity; - return Column( - children: [ - ListTile( - leading: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Colors.grey.shade300, - shape: BoxShape.circle, - image: userInfo.picture == null - ? null - : DecorationImage( - image: NetworkImage(userInfo.picture!), - fit: BoxFit.contain, - ), - ), - child: userInfo.picture == null - ? Icon( - CupertinoIcons.person, - color: Colors.grey.shade400, - size: 40, - ) - : null, - ), - title: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${userInfo.name}', - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - const SizedBox(width: 5), - Text( - '${DateFormat('dd MMMM', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}', - style: TextStyle( - color: Colors.grey[600], - ), - ), - ], - ), - subtitle: const Text('Rating: 5.0'), - ), - Container( - margin: const EdgeInsets.only( - left: 40, right: 40, top: 20, bottom: 20), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 15), - decoration: BoxDecoration( - color: const Color(0xFFD6F4FF), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(1, 3), - ), - ], - ), - child: Row( - children: [ - const Icon( - Icons.error_outline, - size: 27, - color: Colors.black54, - ), - const SizedBox(width: 15), - service.location == - ServiceLocationPreferences.delivery - ? const Text( - 'Servicio a su domicilio.', - style: TextStyle( - color: Colors.black, fontSize: 14), - ) - : const Text( - 'Servicio en sitio / consultorio', - style: TextStyle( - color: Colors.black, fontSize: 14), - ), - ], - ), - ), - Visibility( - visible: settings?.tarifas ?? false, - child: Column( - children: [ - Text( - formatCurrency(int.tryParse(service.rate) ?? 0), - style: const TextStyle( - fontWeight: FontWeight.w600, fontSize: 25), - ), - const Text('Tarifa de consulta'), - const SizedBox(height: 15), - const Text( - 'Metodos de pago', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - professionalInfo.paymentMethods.datafono || - professionalInfo.paymentMethods.nequi || - professionalInfo - .paymentMethods.transferencia - ? Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - Visibility( - visible: professionalInfo - .paymentMethods.datafono, - child: const Chip( - label: Text('Datafono')), - ), - Visibility( - visible: professionalInfo - .paymentMethods.nequi, - child: - const Chip(label: Text('Nequi')), - ), - Visibility( - visible: professionalInfo - .paymentMethods.transferencia, - child: const Chip( - label: Text( - 'Transferencia Bancaria')), - ), - ], - ) - : const Text( - 'No hay metodos de pago registrados', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 15, - color: Colors.black45, + return Column( + children: [ + ListTile( + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + image: userInfo.picture == null + ? null + : DecorationImage( + image: NetworkImage(userInfo.picture!), + fit: BoxFit.contain, ), - ), - ], - ), - ), - ListTile( - leading: const Icon(Icons.near_me), - title: Text( - service.address, - style: TextStyle( - fontSize: 15, color: Colors.grey[600]), - ), - subtitle: service.aditionalAddress.isEmpty - ? null - : Text( - service.aditionalAddress, - style: TextStyle( - fontSize: 15, color: Colors.grey[600]), + ), + child: userInfo.picture == null + ? Icon( + CupertinoIcons.person, + color: Colors.grey.shade400, + size: 40, + ) + : null, + ), + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${userInfo.name}', + style: const TextStyle( + fontWeight: FontWeight.bold), ), - ), - service.description.isEmpty - ? const SizedBox() - : SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - child: Text( - '"${service.description.trim()}"', + const SizedBox(width: 5), + Text( + '${DateFormat('dd MMMM', 'es').format(DateTime.parse(service.day))} - ${ScheduleEntity.getFormatTime(service.range1Hour1)}', style: TextStyle( color: Colors.grey[600], - fontStyle: FontStyle.italic, ), ), - ), - Expanded( - child: customMessageStatus(service), - ), - customButton(service, context), - const SizedBox(height: 20), - ], - ); + ], + ), + subtitle: const Text('Rating: 5.0'), + ), + Container( + margin: const EdgeInsets.only( + left: 40, right: 40, top: 20, bottom: 20), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 15), + decoration: BoxDecoration( + color: const Color(0xFFD6F4FF), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(1, 3), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.error_outline, + size: 27, + color: Colors.black54, + ), + const SizedBox(width: 15), + service.location == + ServiceLocationPreferences.delivery + ? const Text( + 'Servicio a su domicilio.', + style: TextStyle( + color: Colors.black, fontSize: 14), + ) + : const Text( + 'Servicio en sitio / consultorio', + style: TextStyle( + color: Colors.black, fontSize: 14), + ), + ], + ), + ), + Visibility( + visible: settings?.tarifas ?? false, + child: Column( + children: [ + Text( + formatCurrency( + int.tryParse(service.rate) ?? 0), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 25), + ), + const Text('Tarifa de consulta'), + const SizedBox(height: 15), + const Text( + 'Metodos de pago', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + professionalInfo.paymentMethods.datafono || + professionalInfo.paymentMethods.nequi || + professionalInfo + .paymentMethods.transferencia + ? Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: [ + Visibility( + visible: professionalInfo + .paymentMethods.datafono, + child: const Chip( + label: Text('Datafono')), + ), + Visibility( + visible: professionalInfo + .paymentMethods.nequi, + child: const Chip( + label: Text('Nequi')), + ), + Visibility( + visible: professionalInfo + .paymentMethods.transferencia, + child: const Chip( + label: Text( + 'Transferencia Bancaria')), + ), + ], + ) + : const Text( + 'No hay metodos de pago registrados', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + color: Colors.black45, + ), + ), + ], + ), + ), + ListTile( + leading: const Icon(Icons.near_me), + title: Text( + service.address, + style: TextStyle( + fontSize: 15, color: Colors.grey[600]), + ), + subtitle: service.aditionalAddress.isEmpty + ? null + : Text( + service.aditionalAddress, + style: TextStyle( + fontSize: 15, color: Colors.grey[600]), + ), + ), + service.description.isEmpty + ? const SizedBox() + : Container( + alignment: Alignment.center, + width: + MediaQuery.of(context).size.width * 0.8, + child: Text( + '"${service.description.trim()}"', + style: TextStyle( + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ), + Expanded( + child: customMessageStatus(service), + ), + customButton(service, context), + const SizedBox(height: 20), + ], + ); + } } - } - }, - ); - } else { - BlocProvider.of(context) - .add(LoadService(widget.serviceId)); - return const Center( - child: CircularProgressIndicator(), - ); - } - }, + }, + ); + } else { + BlocProvider.of(context) + .add(LoadService(widget.serviceId)); + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), ), ); } @@ -366,14 +372,12 @@ class _UserServiceScreenState extends State { return '\$${formatter.format(number)}'; } - Future> _getUserAndProfessionalInfo( - ServiceEntity service) async { + Future> _getUserAndProfessionalInfo(ServiceEntity service) async { final userRepo = FirebaseUserRepository(FirebaseAuth.instance); final userInfo = await userRepo.getMyUser(service.professionalId); final professionalRepo = FirebaseProfessionalRepository(); - final professionalInfo = - await professionalRepo.getProInfo(service.professionalId); + final professionalInfo = await professionalRepo.getProInfo(service.professionalId); return [userInfo, professionalInfo]; } diff --git a/lib/screens/user/user_services_screen.dart b/lib/screens/user/user_services_screen.dart deleted file mode 100644 index 3d726a3..0000000 --- a/lib/screens/user/user_services_screen.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class UserServicesScreen extends StatelessWidget { - const UserServicesScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Mis servicios'), - ), - body: const Center( - child: Text('Servicios'), - ), - ); - } -} diff --git a/packages/professional_repository/lib/src/repositories/firebase_professional_repository.dart b/packages/professional_repository/lib/src/repositories/firebase_professional_repository.dart index 9c57ac9..2915777 100644 --- a/packages/professional_repository/lib/src/repositories/firebase_professional_repository.dart +++ b/packages/professional_repository/lib/src/repositories/firebase_professional_repository.dart @@ -189,4 +189,21 @@ class FirebaseProfessionalRepository { rethrow; } } + + Future> getProfessionsFromIds( + Iterable ids) async { + try { + // Realizar una consulta única para obtener la información de todos los usuarios + final querySnapshot = await professionalCollection + .where(FieldPath.documentId, whereIn: ids) + .get(); + + return querySnapshot.docs + .map((e) => ProfessionalEntity.fromDocument(e.data())) + .toList(); + } catch (e) { + log('getProfessionsFromIds ${e.toString()}'); + rethrow; + } + } } diff --git a/packages/service_repository/lib/src/repositories/firebase_service_repository.dart b/packages/service_repository/lib/src/repositories/firebase_service_repository.dart index 522efaf..09c4571 100644 --- a/packages/service_repository/lib/src/repositories/firebase_service_repository.dart +++ b/packages/service_repository/lib/src/repositories/firebase_service_repository.dart @@ -11,6 +11,13 @@ class FirebaseServiceRepository { return docRef.id; } + Future updateServiceStatus( + String serviceId, ServiceStatus newStatus) async { + await serviceCollection + .doc(serviceId) + .update({'status': enumToIntService(newStatus)}); + } + Stream getService(String serviceId) { return serviceCollection.doc(serviceId).snapshots().map((snapshot) { if (snapshot.exists) { @@ -21,10 +28,12 @@ class FirebaseServiceRepository { }); } - Future updateServiceStatus( - String serviceId, ServiceStatus newStatus) async { - await serviceCollection - .doc(serviceId) - .update({'status': enumToIntService(newStatus)}); + Stream> getServicesForUser(String userId) { + return serviceCollection + .where('user_id', isEqualTo: userId) + .snapshots() + .map((querySnapshot) => querySnapshot.docs + .map((doc) => ServiceEntity.fromDocument(doc.data())) + .toList()); } } diff --git a/packages/user_repository/lib/src/repositories/firebase_user_repository.dart b/packages/user_repository/lib/src/repositories/firebase_user_repository.dart index df3bb95..6b6d95f 100644 --- a/packages/user_repository/lib/src/repositories/firebase_user_repository.dart +++ b/packages/user_repository/lib/src/repositories/firebase_user_repository.dart @@ -403,4 +403,20 @@ class FirebaseUserRepository implements UserRepository { rethrow; } } + + @override + Future> getUsersFromIds(Iterable ids) async { + try { + // Realizar una consulta única para obtener la información de todos los usuarios + final querySnapshot = + await usersCollection.where(FieldPath.documentId, whereIn: ids).get(); + + return querySnapshot.docs + .map((e) => MyUser.fromEntity(MyUserEntity.fromDocument(e.data()))) + .toList(); + } catch (e) { + log('getUsersFromIds ${e.toString()}'); + rethrow; + } + } } diff --git a/packages/user_repository/lib/src/repositories/user_repo.dart b/packages/user_repository/lib/src/repositories/user_repo.dart index 838009a..6127838 100644 --- a/packages/user_repository/lib/src/repositories/user_repo.dart +++ b/packages/user_repository/lib/src/repositories/user_repo.dart @@ -45,6 +45,8 @@ abstract class UserRepository { Future createUser(MyUser myUser); Future> getUsersProfessionalActive(); + + Future> getUsersFromIds(Iterable ids); } enum UpdatePassworErros { credentialsWrong, userNotFound, unknown }