From c87417801949e652fef5205bc6ce0ccadc249a72 Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 18 Apr 2024 18:29:41 -0500 Subject: [PATCH] score --- lib/app.dart | 9 +- lib/blocs/score_bloc/score_bloc.dart | 94 ++- lib/blocs/score_bloc/score_event.dart | 18 + lib/blocs/score_bloc/score_state.dart | 9 + lib/components/drawer_reputation.dart | 22 +- lib/components/general_drawer.dart | 486 +++++++------- lib/dependency/app_di.dart | 15 +- .../lists/professional_list_screen.dart | 23 +- .../lists/professional_score_list_screen.dart | 151 +++++ lib/screens/lists/user_score_list_screen.dart | 150 +++++ lib/screens/score/score_screen.dart | 602 +++++++++--------- .../firebase_score_repository.dart | 20 + 12 files changed, 1042 insertions(+), 557 deletions(-) create mode 100644 lib/screens/lists/professional_score_list_screen.dart create mode 100644 lib/screens/lists/user_score_list_screen.dart diff --git a/lib/app.dart b/lib/app.dart index e8cbdcf..2817b2c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -23,9 +23,9 @@ class MainApp extends StatelessWidget { BlocProvider( create: (context) => Injector.appInstance.get(), ), - BlocProvider( - create: (context) => Injector.appInstance.get(), - ), + // BlocProvider( + // create: (context) => Injector.appInstance.get(), + // ), BlocProvider( create: (context) => Injector.appInstance.get(), ), @@ -33,7 +33,8 @@ class MainApp extends StatelessWidget { create: (context) => Injector.appInstance.get(), ), BlocProvider( - create: (context) => Injector.appInstance.get(), + create: (context) => + Injector.appInstance.get(), ) ], child: BlocBuilder( diff --git a/lib/blocs/score_bloc/score_bloc.dart b/lib/blocs/score_bloc/score_bloc.dart index a018919..725055d 100644 --- a/lib/blocs/score_bloc/score_bloc.dart +++ b/lib/blocs/score_bloc/score_bloc.dart @@ -3,23 +3,27 @@ import 'dart:developer'; import 'package:equatable/equatable.dart'; import 'package:score_repository/score_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:user_repository/user_repository.dart'; part 'score_event.dart'; part 'score_state.dart'; class ScoreBloc extends Bloc { - final FirebaseScoreRepository _firebaseScoreRepository; + final FirebaseScoreRepository _scoreRepository; + final UserRepository _userRepository; ScoreBloc({ - required FirebaseScoreRepository firebaseScoreRepository, - }) : _firebaseScoreRepository = firebaseScoreRepository, + required FirebaseScoreRepository scoreRepository, + required UserRepository userRepository, + }) : _scoreRepository = scoreRepository, + _userRepository = userRepository, super(ScoreInitial()) { try { - emit(ScoreSuccess(_firebaseScoreRepository.getReputation())); + emit(ScoreSuccess(_scoreRepository.getReputation())); } catch (e) { log(e.toString()); } - _firebaseScoreRepository.streamReputation().listen((event) { + _scoreRepository.streamReputation().listen((event) { try { emit(ScoreSuccess(event)); } catch (e) { @@ -29,13 +33,91 @@ class ScoreBloc extends Bloc { }); on(_onSendScoreEvent); + on(_onLoadScoresForUserEvent); + on(_onLoadScoresForProfessionalEvent); } void _onSendScoreEvent(SendScoreEvent event, Emitter emit) async { try { - await _firebaseScoreRepository.addComment(event.comment); + await _scoreRepository.addComment(event.comment); } catch (e) { log(e.toString()); } } + + void _onLoadScoresForUserEvent( + LoadScoresForUserEvent event, Emitter emit) async { + try { + final scoresStream = _scoreRepository.getScoresForUser(event.userId); + + await for (var scores in scoresStream) { + if (scores.isEmpty) { + emit(const ScoresForUserLoaded([])); + return; + } + final userIds = scores.map((e) => e.authorId); + + final List users = + await _userRepository.getUsersFromIds(userIds); + + final usersDir = {for (var e in users) e.id: e}; + + final scoresInfo = scores.map((e) { + return ScoreInfoUI( + score: e, + user: usersDir[e.authorId]!, + ); + }).toList(); + + log(scoresInfo.toString()); + + emit(ScoresForUserLoaded(scoresInfo)); + } + } catch (e) { + log(e.toString()); + emit(ScoreFailure()); + } + } + + void _onLoadScoresForProfessionalEvent( + LoadScoresForProfessionalEvent event, + Emitter emit, + ) async { + try { + final scoresStream = + _scoreRepository.getScoresForProfessional(event.userId); + + await for (var scores in scoresStream) { + if (scores.isEmpty) { + emit(const ScoresForUserLoaded([])); + return; + } + final userIds = scores.map((e) => e.authorId); + + final List users = + await _userRepository.getUsersFromIds(userIds); + + final usersDir = {for (var e in users) e.id: e}; + + final scoresInfo = scores.map((e) { + return ScoreInfoUI( + score: e, + user: usersDir[e.authorId]!, + ); + }).toList(); + + emit(ScoresForUserLoaded(scoresInfo)); + } + } catch (e) { + log(e.toString()); + emit(ScoreFailure()); + } + } +} + +class ScoreInfoUI { + final CommentEntity score; + final MyUser user; + + ScoreInfoUI({required this.score, required this.user}); } diff --git a/lib/blocs/score_bloc/score_event.dart b/lib/blocs/score_bloc/score_event.dart index 34805c3..8ea5cb6 100644 --- a/lib/blocs/score_bloc/score_event.dart +++ b/lib/blocs/score_bloc/score_event.dart @@ -15,3 +15,21 @@ class SendScoreEvent extends ScoreEvent { @override List get props => [comment]; } + +class LoadScoresForUserEvent extends ScoreEvent { + final String userId; + + const LoadScoresForUserEvent({required this.userId}); + + @override + List get props => [userId]; +} + +class LoadScoresForProfessionalEvent extends ScoreEvent { + final String userId; + + const LoadScoresForProfessionalEvent({required this.userId}); + + @override + List get props => [userId]; +} diff --git a/lib/blocs/score_bloc/score_state.dart b/lib/blocs/score_bloc/score_state.dart index 8ac7657..71c7c84 100644 --- a/lib/blocs/score_bloc/score_state.dart +++ b/lib/blocs/score_bloc/score_state.dart @@ -20,3 +20,12 @@ class ScoreSuccess extends ScoreState { @override List get props => [reputation]; } + +class ScoresForUserLoaded extends ScoreState { + final List scores; + + const ScoresForUserLoaded(this.scores); + + @override + List get props => [scores]; +} diff --git a/lib/components/drawer_reputation.dart b/lib/components/drawer_reputation.dart index 71be926..c8e19b2 100644 --- a/lib/components/drawer_reputation.dart +++ b/lib/components/drawer_reputation.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; import 'package:score_repository/score_repository.dart'; @@ -10,14 +11,17 @@ class DrawerReputation extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - if (state is ScoreSuccess) { - return builder(state.reputation); - } else if (state is ScoreFailure) { - return const Center(child: Text('Error')); - } else { - return const Center(child: CircularProgressIndicator()); - } - }); + return BlocProvider( + create: (context) => Injector.appInstance.get(), + child: BlocBuilder(builder: (context, state) { + if (state is ScoreSuccess) { + return builder(state.reputation); + } else if (state is ScoreFailure) { + return const Center(child: Text('Error')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }), + ); } } diff --git a/lib/components/general_drawer.dart b/lib/components/general_drawer.dart index d95ffc3..803a07f 100644 --- a/lib/components/general_drawer.dart +++ b/lib/components/general_drawer.dart @@ -6,16 +6,20 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:injector/injector.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart'; +import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; import 'package:prosappco/components/drawer_reputation.dart'; import 'package:prosappco/components/general_drawer_header.dart'; import 'package:prosappco/components/general_drawer_item.dart'; import 'package:prosappco/screens/configuration/configuration_screen.dart'; import 'package:prosappco/screens/configuration/configuration_support_screen.dart'; import 'package:prosappco/screens/lists/professional_list_screen.dart'; +import 'package:prosappco/screens/lists/professional_score_list_screen.dart'; import 'package:prosappco/screens/lists/professional_service_history_list_screen.dart'; import 'package:prosappco/screens/lists/professional_service_list_screen.dart'; +import 'package:prosappco/screens/lists/user_score_list_screen.dart'; import 'package:prosappco/screens/lists/user_service_history_list_screen.dart'; import 'package:prosappco/screens/lists/user_service_list_screen.dart'; import 'package:prosappco/screens/professional/professional_calendar_screen.dart'; @@ -47,252 +51,274 @@ class GeneralDrawer extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, userState) { - return BlocBuilder( - builder: (context, professionalState) { - return Drawer( - backgroundColor: Theme.of(context).colorScheme.secondary, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const GeneralDrawerHeader(), - Divider( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.1), - thickness: 0.5, - height: 1, - ), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - shouldProModeActive(context, professionalState) - ? GeneralDrawerItem( - leading: Icons.person_outline_rounded, - label: 'Perfil profesional', - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfessionalProfileScreen(), - ), - ); - }, - ) - : const SizedBox(), - GeneralDrawerItem( - leading: Icons.checklist_outlined, - label: 'Mis servicios', - onTap: () { - shouldProModeActive(context, professionalState) - ? Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfessionalServiceListScreen(), - // const UserServicesScreen(), - ), - ) - : Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const UserServiceListScreen(), - // const UserServicesScreen(), - ), - ); - }, - ), - GeneralDrawerItem( - leading: Icons.access_time_outlined, - label: 'Historial', - onTap: () { - shouldProModeActive(context, professionalState) - ? Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfessionalServiceHistoryListScreen(), - ), - ) - : Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const UserServiceHistoryListScreen(), - ), - ); - }, - ), - shouldProModeActive(context, professionalState) - ? GeneralDrawerItem( - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfessionalCalendarScreen(), - ), - ); - }, - label: 'Calendario', - leading: Icons.calendar_month_outlined, - ) - : const SizedBox(), - GeneralDrawerItem( - leading: Icons.settings_outlined, - label: 'Configuración', - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ConfigurationScreen(), - ), - ); - }, - ), - GeneralDrawerItem( - leading: Icons.help_outline, - label: 'Soporte', - onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ConfigurationSupportScreen(), - ), - ); - }, - ), - GeneralDrawerItem( - leading: Icons.campaign_outlined, - label: 'Sugerencias', - onTap: () { - if (kIsWeb) { - _irSugerencias(); - } else { + return BlocProvider( + create: (context) => Injector.appInstance.get(), + child: BlocBuilder( + builder: (context, userState) { + return BlocBuilder( + builder: (context, professionalState) { + return Drawer( + backgroundColor: Theme.of(context).colorScheme.secondary, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const GeneralDrawerHeader(), + Divider( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.1), + thickness: 0.5, + height: 1, + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + shouldProModeActive(context, professionalState) + ? GeneralDrawerItem( + leading: Icons.person_outline_rounded, + label: 'Perfil profesional', + onTap: () { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfessionalProfileScreen(), + ), + ); + }, + ) + : const SizedBox(), + GeneralDrawerItem( + leading: Icons.checklist_outlined, + label: 'Mis servicios', + onTap: () { + shouldProModeActive(context, professionalState) + ? Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfessionalServiceListScreen(), + // const UserServicesScreen(), + ), + ) + : Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const UserServiceListScreen(), + // const UserServicesScreen(), + ), + ); + }, + ), + GeneralDrawerItem( + leading: Icons.access_time_outlined, + label: 'Historial', + onTap: () { + shouldProModeActive(context, professionalState) + ? Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfessionalServiceHistoryListScreen(), + ), + ) + : Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const UserServiceHistoryListScreen(), + ), + ); + }, + ), + shouldProModeActive(context, professionalState) + ? GeneralDrawerItem( + onTap: () { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfessionalCalendarScreen(), + ), + ); + }, + label: 'Calendario', + leading: Icons.calendar_month_outlined, + ) + : const SizedBox(), + GeneralDrawerItem( + leading: Icons.settings_outlined, + label: 'Configuración', + onTap: () { Navigator.push( context, CupertinoPageRoute( - builder: (BuildContext context) { - return WebViewScreen( - label: 'Sugerencias', - link: - 'https://admin.prosapp.co/sugerencias'); - }, + builder: (context) => + const ConfigurationScreen(), ), ); - } - }, - ), - Container( - color: const Color(0xFF2BA4EC), - child: ListTile( - onTap: () { - shouldProModeActive(context, professionalState) - ? Navigator.pop(context) - : Navigator.pop(context); }, - trailing: const Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - ), - title: Text( - shouldProModeActive(context, professionalState) - ? 'Solicitudes' - : 'Solicitar servicio', - style: const TextStyle( - fontSize: 16, + ), + GeneralDrawerItem( + leading: Icons.help_outline, + label: 'Soporte', + onTap: () { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ConfigurationSupportScreen(), + ), + ); + }, + ), + GeneralDrawerItem( + leading: Icons.campaign_outlined, + label: 'Sugerencias', + onTap: () { + if (kIsWeb) { + _irSugerencias(); + } else { + Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return WebViewScreen( + label: 'Sugerencias', + link: + 'https://admin.prosapp.co/sugerencias'); + }, + ), + ); + } + }, + ), + Container( + color: const Color(0xFF2BA4EC), + child: ListTile( + onTap: () { + shouldProModeActive( + context, professionalState) + ? Navigator.pop(context) + : Navigator.pop(context); + }, + trailing: const Icon( + Icons.keyboard_arrow_right, color: Colors.white, - fontWeight: FontWeight.bold, + ), + title: Text( + shouldProModeActive( + context, professionalState) + ? 'Solicitudes' + : 'Solicitar servicio', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - DrawerReputation(builder: (reputation) { - final isProModeActive = - (professionalState is LoadedModeProState) && - professionalState.isProModeActive; + DrawerReputation(builder: (reputation) { + final isProModeActive = + (professionalState is LoadedModeProState) && + professionalState.isProModeActive; - final total = isProModeActive - ? reputation.totalPro - : reputation.total; + final total = isProModeActive + ? reputation.totalPro + : reputation.total; - final average = isProModeActive - ? reputation.averagePro - : reputation.average; + final average = isProModeActive + ? reputation.averagePro + : reputation.average; - return ListTile( - onTap: () {}, - trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black), - title: Row( - children: [ - RatingBar.builder( - initialRating: calculoRating(average), - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 25, - maxRating: 5, - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), + return ListTile( + onTap: () { + final isProModeActive = (professionalState + is LoadedModeProState) && + professionalState.isProModeActive; + isProModeActive + ? Navigator.push(context, + CupertinoPageRoute( + builder: (context) { + return const ProfessionalScoreListScreen(); + })) + : Navigator.push(context, + CupertinoPageRoute( + builder: (context) { + return const UserScoreListScreen(); + })); + }, + trailing: const Icon(Icons.keyboard_arrow_right, + color: Colors.black), + title: Row( + children: [ + RatingBar.builder( + // initialRating: calculoRating(average), + initialRating: 3, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 25, + maxRating: 5, + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Color(0xFF2BA4EC), + ), + onRatingUpdate: (rating) {}, + ignoreGestures: true, ), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox(width: 5), - Text( - '${average.toStringAsFixed(1)} (${total.toString()})', - ), - ], - ), - ); - }), - const Divider( - height: 1, - thickness: 0.5, - ), - Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - 'Prosapp ® todos los derechos reservados', - style: TextStyle( - fontSize: 10, - color: Colors.grey[700], + const SizedBox(width: 5), + Text( + '${average.toStringAsFixed(1)} (${total.toString()})', + ), + ], + ), + ); + }), + const Divider( + height: 1, + thickness: 0.5, + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + 'Prosapp ® todos los derechos reservados', + style: TextStyle( + fontSize: 10, + color: Colors.grey[700], + ), ), ), - ), - ], + ], + ), ), ), - ), - Divider( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.1), - thickness: 0.5, - height: 1, - ), - const SizedBox(height: 15), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: buttonOfState(context, professionalState), - ), - const SizedBox(height: 15), - ], - ), - ); - }, - ); - }, + Divider( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.1), + thickness: 0.5, + height: 1, + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: buttonOfState(context, professionalState), + ), + const SizedBox(height: 15), + ], + ), + ); + }, + ); + }, + ), ); } @@ -389,10 +415,6 @@ class GeneralDrawer extends StatelessWidget { 'Modo profesional', style: TextStyle(color: Colors.white, fontSize: 18), ), - - // const CircularProgressIndicator( - // color: Colors.white, - // ), ); } diff --git a/lib/dependency/app_di.dart b/lib/dependency/app_di.dart index 3c0b14f..5577dd2 100644 --- a/lib/dependency/app_di.dart +++ b/lib/dependency/app_di.dart @@ -47,8 +47,7 @@ class AppDI { injector.registerSingleton( (() => MyUserBloc(myUserRepository: injector.get()))); - injector.registerSingleton( - (() => ScoreBloc(firebaseScoreRepository: injector.get()))); + injector.registerSingleton( (() => ProfileBloc(userRepository: injector.get()))); @@ -91,5 +90,17 @@ class AppDI { professionRepository: injector.get(), ), ); + + injector.registerDependency( + () => ScoreBloc( + scoreRepository: injector.get(), + userRepository: injector.get(), + ), + ); + + // injector.registerSingleton((() => ScoreBloc( + // firebaseScoreRepository: injector.get(), + // userRepository: injector.get(), + // ))); } } diff --git a/lib/screens/lists/professional_list_screen.dart b/lib/screens/lists/professional_list_screen.dart index 5c4cbe6..61ea42d 100644 --- a/lib/screens/lists/professional_list_screen.dart +++ b/lib/screens/lists/professional_list_screen.dart @@ -79,8 +79,8 @@ class _ProfessionalListScreenState extends State { .where((element) => removeDiacritics(element.myUser.name!) .toLowerCase() .contains(removeDiacritics(_searchController.text.toLowerCase()))) - .where((user) => - user.myUser.id != FirebaseAuth.instance.currentUser!.uid) + // .where((user) => + // user.myUser.id != FirebaseAuth.instance.currentUser!.uid) .toList(); // } else { // filteredUsers = state.users @@ -183,12 +183,19 @@ class _ProfessionalListScreenState extends State { ) : null, ), - title: Text( - '${filteredUsers[index].myUser.name ?? ''}, ${filteredUsers[index].professionalInfo.profession}', - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 15, - ), + title: Row( + children: [ + Flexible( + child: Text( + '${filteredUsers[index].myUser.name ?? ''}, ', + overflow: TextOverflow.ellipsis, + ), + ), + Text( + filteredUsers[index].professionalInfo.profession, + overflow: TextOverflow.ellipsis, + ), + ], ), subtitle: Text( _disponibilidad(filteredUsers[index].professionalInfo), diff --git a/lib/screens/lists/professional_score_list_screen.dart b/lib/screens/lists/professional_score_list_screen.dart new file mode 100644 index 0000000..fc3f887 --- /dev/null +++ b/lib/screens/lists/professional_score_list_screen.dart @@ -0,0 +1,151 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; +import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; +import 'package:shimmer/shimmer.dart'; + +class ProfessionalScoreListScreen extends StatefulWidget { + const ProfessionalScoreListScreen({super.key}); + + @override + State createState() => + _ProfessionalScoreListScreenState(); +} + +class _ProfessionalScoreListScreenState + extends State { + late final ScoreBloc scoreBloc; + + @override + void initState() { + super.initState(); + + scoreBloc = Injector.appInstance.get(); + + scoreBloc.add(LoadScoresForProfessionalEvent( + userId: FirebaseAuth.instance.currentUser!.uid)); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => scoreBloc, + child: Scaffold( + appBar: AppBar( + title: const Text('Reputación'), + ), + body: BlocBuilder( + builder: (context, scoreState) { + if (scoreState is ScoresForUserLoaded) { + return scoreState.scores.isEmpty + ? const Center( + child: Text('No tienes calificaciones'), + ) + : ListView.builder( + itemCount: scoreState.scores.length, + itemBuilder: (_, index) { + final scoreInfo = scoreState.scores[index]; + final score = scoreInfo.score; + final user = scoreInfo.user; + + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.withOpacity(0.2)), + ), + ), + child: ListTile( + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + image: user.picture == null + ? null + : DecorationImage( + image: NetworkImage(user.picture!), + fit: BoxFit.contain, + ), + ), + child: user.picture == null + ? Icon( + CupertinoIcons.person, + color: Colors.grey.shade400, + size: 40, + ) + : null, + ), + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + '${user.name}', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 5), + Text( + '${score.createdAt.toDate().day}/${score.createdAt.toDate().month}/${score.createdAt.toDate().year}', + style: TextStyle( + color: Colors.grey[600], + ), + ), + ], + ), + subtitle: Text( + '"${score.content.trim()}"', + overflow: TextOverflow.ellipsis, + ), + // icono de calificación + trailing: Text( + '⭐ ${score.score}', + style: const TextStyle( + fontSize: 13, + ), + ), + ), + ); + }, + ); + } + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: ListView.builder( + itemCount: 10, + itemBuilder: (_, __) => ListTile( + leading: CircleAvatar( + backgroundColor: Colors.grey[300], + radius: 30, + ), + title: Container( + height: 20, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + ), + subtitle: Container( + height: 15, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screens/lists/user_score_list_screen.dart b/lib/screens/lists/user_score_list_screen.dart new file mode 100644 index 0000000..c4e920a --- /dev/null +++ b/lib/screens/lists/user_score_list_screen.dart @@ -0,0 +1,150 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; +import 'package:intl/intl.dart'; +import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; +import 'package:shimmer/shimmer.dart'; + +class UserScoreListScreen extends StatefulWidget { + const UserScoreListScreen({super.key}); + + @override + State createState() => _UserScoreListScreenState(); +} + +class _UserScoreListScreenState extends State { + late final ScoreBloc scoreBloc; + + @override + void initState() { + super.initState(); + + scoreBloc = Injector.appInstance.get(); + + scoreBloc.add( + LoadScoresForUserEvent(userId: FirebaseAuth.instance.currentUser!.uid)); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => scoreBloc, + child: Scaffold( + appBar: AppBar( + title: const Text('Reputación'), + ), + body: BlocBuilder( + builder: (context, scoreState) { + if (scoreState is ScoresForUserLoaded) { + return scoreState.scores.isEmpty + ? const Center( + child: Text('No tienes calificaciones'), + ) + : ListView.builder( + itemCount: scoreState.scores.length, + itemBuilder: (_, index) { + final scoreInfo = scoreState.scores[index]; + final score = scoreInfo.score; + final user = scoreInfo.user; + + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.withOpacity(0.2)), + ), + ), + child: ListTile( + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + image: user.picture == null + ? null + : DecorationImage( + image: NetworkImage(user.picture!), + fit: BoxFit.contain, + ), + ), + child: user.picture == null + ? Icon( + CupertinoIcons.person, + color: Colors.grey.shade400, + size: 40, + ) + : null, + ), + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + '${user.name}', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 5), + Text( + '${score.createdAt.toDate().day}/${score.createdAt.toDate().month}/${score.createdAt.toDate().year}', + style: TextStyle( + color: Colors.grey[600], + ), + ), + ], + ), + subtitle: Text( + '"${score.content.trim()}"', + overflow: TextOverflow.ellipsis, + ), + // icono de calificación + trailing: Text( + '⭐ ${score.score}', + style: const TextStyle( + fontSize: 13, + ), + ), + ), + ); + }, + ); + } + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: ListView.builder( + itemCount: 10, + itemBuilder: (_, __) => ListTile( + leading: CircleAvatar( + backgroundColor: Colors.grey[300], + radius: 30, + ), + title: Container( + height: 20, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + ), + subtitle: Container( + height: 15, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screens/score/score_screen.dart b/lib/screens/score/score_screen.dart index c4f3ad9..3fdeae7 100644 --- a/lib/screens/score/score_screen.dart +++ b/lib/screens/score/score_screen.dart @@ -43,319 +43,329 @@ class _ScoreScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Calificación'), - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - FutureBuilder( - future: _userInfoFuture, - builder: (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('Calificación'), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + FutureBuilder( + future: _userInfoFuture, + builder: + (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator()); } else { - final userInfo = snapshot.data![0] as MyUser; + if (snapshot.hasError) { + return Center( + child: + Text('Error inesperado: ${snapshot.error}'), + ); + } else { + final userInfo = snapshot.data![0] as MyUser; - return SizedBox( - width: double.infinity, - child: Stack( - alignment: Alignment.center, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 95, - height: 95, - 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: 50, - ) - : null, - ), - const SizedBox(height: 8), - Text( - '${userInfo.name}', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - height: 15, - ) - ], - ), - GeneralReputation( - userId: userId, - builder: (BuildContext context, - ReputationEntity reputation) { - final double average; - - if (isProfessional) { - average = reputation.averagePro; - } else { - average = reputation.average; - } - - return Positioned( - top: 3, - right: MediaQuery.of(context).size.width * - 0.3, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 3, - ), + return SizedBox( + width: double.infinity, + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 95, + height: 95, decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: - Colors.black.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset( - 0, - 1, - ), - ), - ], + color: Colors.grey.shade300, + shape: BoxShape.circle, + image: userInfo.picture == null + ? null + : DecorationImage( + image: NetworkImage( + userInfo.picture!), + fit: BoxFit.contain, + ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.star, - color: Colors.yellow, - size: 20, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - average.toStringAsFixed(2), - style: const TextStyle( - fontSize: 15, + child: userInfo.picture == null + ? Icon( + CupertinoIcons.person, + color: Colors.grey.shade400, + size: 50, + ) + : null, + ), + const SizedBox(height: 8), + Text( + '${userInfo.name}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 15, + ) + ], + ), + GeneralReputation( + userId: userId, + builder: (BuildContext context, + ReputationEntity reputation) { + final double average; + + if (isProfessional) { + average = reputation.averagePro; + } else { + average = reputation.average; + } + + return Positioned( + top: 3, + right: + MediaQuery.of(context).size.width * + 0.3, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 3, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.1), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset( + 0, + 1, ), ), - ), - ], + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + color: Colors.yellow, + size: 20, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + average.toStringAsFixed(2), + style: const TextStyle( + fontSize: 15, + ), + ), + ), + ], + ), ), - ), - ); - }, - ) - ], - ), - ); - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 15), - // child: 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: Text( - // userInfo.name ?? '', - // overflow: TextOverflow.ellipsis, - // style: const TextStyle( - // fontSize: 15, - // fontWeight: FontWeight.w600, - // ), - // ), - // subtitle: const Text( - // 'Rating: 5.0', - // style: TextStyle( - // fontSize: 13, - // color: Colors.blue, - // ), - // ), - // ), - // ); + ); + }, + ) + ], + ), + ); + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 15), + // child: 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: Text( + // userInfo.name ?? '', + // overflow: TextOverflow.ellipsis, + // style: const TextStyle( + // fontSize: 15, + // fontWeight: FontWeight.w600, + // ), + // ), + // subtitle: const Text( + // 'Rating: 5.0', + // style: TextStyle( + // fontSize: 13, + // color: Colors.blue, + // ), + // ), + // ), + // ); + } } - } - }, - ), - const SizedBox(height: 10), - const Text( - 'Califica el servicio', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 10), - RatingBar.builder( - initialRating: _rating, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 40, - glow: false, - maxRating: 5, - itemPadding: const EdgeInsets.symmetric(horizontal: 5), - itemBuilder: (context, _) => const Icon( - Icons.star, - color: Color(0xFF2BA4EC), + }, ), - onRatingUpdate: (rating) { - setState(() { - _rating = rating; - }); - }, - ignoreGestures: false, - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - customMessage(_rating), - style: const TextStyle( - fontSize: 16, + const SizedBox(height: 10), + const Text( + 'Califica el servicio', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 10), + RatingBar.builder( + initialRating: _rating, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 40, + glow: false, + maxRating: 5, + itemPadding: const EdgeInsets.symmetric(horizontal: 5), + itemBuilder: (context, _) => const Icon( + Icons.star, color: Color(0xFF2BA4EC), ), + onRatingUpdate: (rating) { + setState(() { + _rating = rating; + }); + }, + ignoreGestures: false, ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 40, vertical: 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Comentario', + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + customMessage(_rating), + style: const TextStyle( + fontSize: 16, + color: Color(0xFF2BA4EC), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Comentario', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w600), + ), + TextFormField( + maxLines: null, + maxLength: 400, + keyboardType: TextInputType.multiline, + controller: commentController, + ), + ], + ), + ), + ], + ), + ), + ), + const Divider( + height: 1, + thickness: 0.5, + ), + BlocProvider( + create: (context) => Injector.appInstance.get(), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + child: BlocBuilder( + builder: (context, serviceState) { + return FilledButton( + onPressed: () { + final CommentEntity comment = widget.service.userId == + FirebaseAuth.instance.currentUser!.uid + ? CommentEntity( + serviceId: widget.service.id!, + authorId: + FirebaseAuth.instance.currentUser!.uid, + isFromUser: true, + score: _rating, + destinationId: widget.service.professionalId, + content: commentController.text.trim(), + createdAt: Timestamp.now(), + ) + : CommentEntity( + serviceId: widget.service.id!, + authorId: + FirebaseAuth.instance.currentUser!.uid, + isFromUser: false, + score: _rating, + destinationId: widget.service.userId, + content: commentController.text.trim(), + createdAt: Timestamp.now(), + ); + + BlocProvider.of(context) + .add(SendScoreEvent(comment: comment)); + + if (widget.service.userId == + FirebaseAuth.instance.currentUser!.uid) { + context.read().add( + UpdateProfessionalScored(widget.service.id!)); + } else { + context + .read() + .add(UpdateUserScored(widget.service.id!)); + } + + Navigator.pop(context); + }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Container( + alignment: Alignment.center, + width: double.infinity, + child: const Text( + 'Enviar calificación', style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w600), - ), - TextFormField( - maxLines: null, - maxLength: 400, - keyboardType: TextInputType.multiline, - controller: commentController, - ), - ], - ), - ), - ], - ), - ), - ), - const Divider( - height: 1, - thickness: 0.5, - ), - BlocProvider( - create: (context) => Injector.appInstance.get(), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 15, - ), - child: BlocBuilder( - builder: (context, state) { - return FilledButton( - onPressed: () { - final CommentEntity comment = widget.service.userId == - FirebaseAuth.instance.currentUser!.uid - ? CommentEntity( - serviceId: widget.service.id!, - authorId: FirebaseAuth.instance.currentUser!.uid, - isFromUser: true, - score: _rating, - destinationId: widget.service.professionalId, - content: commentController.text.trim(), - createdAt: Timestamp.now(), - ) - : CommentEntity( - serviceId: widget.service.id!, - authorId: FirebaseAuth.instance.currentUser!.uid, - isFromUser: false, - score: _rating, - destinationId: widget.service.userId, - content: commentController.text.trim(), - createdAt: Timestamp.now(), - ); - - BlocProvider.of(context) - .add(SendScoreEvent(comment: comment)); - - if (widget.service.userId == - FirebaseAuth.instance.currentUser!.uid) { - context - .read() - .add(UpdateProfessionalScored(widget.service.id!)); - } else { - context - .read() - .add(UpdateUserScored(widget.service.id!)); - } - - Navigator.pop(context); - }, - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - padding: const EdgeInsets.symmetric(vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: Container( - alignment: Alignment.center, - width: double.infinity, - child: const Text( - 'Enviar calificación', - style: TextStyle( - color: Colors.white, - fontSize: 18, + color: Colors.white, + fontSize: 18, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/packages/score_repository/lib/src/repositories/firebase_score_repository.dart b/packages/score_repository/lib/src/repositories/firebase_score_repository.dart index fac82fb..d22e55b 100644 --- a/packages/score_repository/lib/src/repositories/firebase_score_repository.dart +++ b/packages/score_repository/lib/src/repositories/firebase_score_repository.dart @@ -63,6 +63,26 @@ class FirebaseScoreRepository { } } + Stream> getScoresForUser(String userId) { + return commentsCollection + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: false) + .snapshots() + .map((querySnapshot) => querySnapshot.docs + .map((doc) => CommentEntity.fromDocument(doc.data())) + .toList()); + } + + Stream> getScoresForProfessional(String userId) { + return commentsCollection + .where('destination_id', isEqualTo: userId) + .where('is_from_user', isEqualTo: true) + .snapshots() + .map((querySnapshot) => querySnapshot.docs + .map((doc) => CommentEntity.fromDocument(doc.data())) + .toList()); + } + addComment(CommentEntity comment) async { await commentsCollection.add(comment.toDocument());