This commit is contained in:
Felipe
2024-04-18 18:29:41 -05:00
parent 8084d5fc7f
commit c874178019
12 changed files with 1042 additions and 557 deletions
+5 -4
View File
@@ -23,9 +23,9 @@ class MainApp extends StatelessWidget {
BlocProvider<MyUserBloc>( BlocProvider<MyUserBloc>(
create: (context) => Injector.appInstance.get<MyUserBloc>(), create: (context) => Injector.appInstance.get<MyUserBloc>(),
), ),
BlocProvider<ScoreBloc>( // BlocProvider<ScoreBloc>(
create: (context) => Injector.appInstance.get(), // create: (context) => Injector.appInstance.get(),
), // ),
BlocProvider<ProfileBloc>( BlocProvider<ProfileBloc>(
create: (context) => Injector.appInstance.get<ProfileBloc>(), create: (context) => Injector.appInstance.get<ProfileBloc>(),
), ),
@@ -33,7 +33,8 @@ class MainApp extends StatelessWidget {
create: (context) => Injector.appInstance.get<ProfessionalBloc>(), create: (context) => Injector.appInstance.get<ProfessionalBloc>(),
), ),
BlocProvider<ProfessionalProfileBloc>( BlocProvider<ProfessionalProfileBloc>(
create: (context) => Injector.appInstance.get<ProfessionalProfileBloc>(), create: (context) =>
Injector.appInstance.get<ProfessionalProfileBloc>(),
) )
], ],
child: BlocBuilder<MyUserBloc, MyUserState>( child: BlocBuilder<MyUserBloc, MyUserState>(
+88 -6
View File
@@ -3,23 +3,27 @@ import 'dart:developer';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:score_repository/score_repository.dart'; import 'package:score_repository/score_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:user_repository/user_repository.dart';
part 'score_event.dart'; part 'score_event.dart';
part 'score_state.dart'; part 'score_state.dart';
class ScoreBloc extends Bloc<ScoreEvent, ScoreState> { class ScoreBloc extends Bloc<ScoreEvent, ScoreState> {
final FirebaseScoreRepository _firebaseScoreRepository; final FirebaseScoreRepository _scoreRepository;
final UserRepository _userRepository;
ScoreBloc({ ScoreBloc({
required FirebaseScoreRepository firebaseScoreRepository, required FirebaseScoreRepository scoreRepository,
}) : _firebaseScoreRepository = firebaseScoreRepository, required UserRepository userRepository,
}) : _scoreRepository = scoreRepository,
_userRepository = userRepository,
super(ScoreInitial()) { super(ScoreInitial()) {
try { try {
emit(ScoreSuccess(_firebaseScoreRepository.getReputation())); emit(ScoreSuccess(_scoreRepository.getReputation()));
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
} }
_firebaseScoreRepository.streamReputation().listen((event) { _scoreRepository.streamReputation().listen((event) {
try { try {
emit(ScoreSuccess(event)); emit(ScoreSuccess(event));
} catch (e) { } catch (e) {
@@ -29,13 +33,91 @@ class ScoreBloc extends Bloc<ScoreEvent, ScoreState> {
}); });
on<SendScoreEvent>(_onSendScoreEvent); on<SendScoreEvent>(_onSendScoreEvent);
on<LoadScoresForUserEvent>(_onLoadScoresForUserEvent);
on<LoadScoresForProfessionalEvent>(_onLoadScoresForProfessionalEvent);
} }
void _onSendScoreEvent(SendScoreEvent event, Emitter<ScoreState> emit) async { void _onSendScoreEvent(SendScoreEvent event, Emitter<ScoreState> emit) async {
try { try {
await _firebaseScoreRepository.addComment(event.comment); await _scoreRepository.addComment(event.comment);
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
} }
} }
void _onLoadScoresForUserEvent(
LoadScoresForUserEvent event, Emitter<ScoreState> 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<MyUser> 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<ScoreState> 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<MyUser> 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});
} }
+18
View File
@@ -15,3 +15,21 @@ class SendScoreEvent extends ScoreEvent {
@override @override
List<Object> get props => [comment]; List<Object> get props => [comment];
} }
class LoadScoresForUserEvent extends ScoreEvent {
final String userId;
const LoadScoresForUserEvent({required this.userId});
@override
List<Object> get props => [userId];
}
class LoadScoresForProfessionalEvent extends ScoreEvent {
final String userId;
const LoadScoresForProfessionalEvent({required this.userId});
@override
List<Object> get props => [userId];
}
+9
View File
@@ -20,3 +20,12 @@ class ScoreSuccess extends ScoreState {
@override @override
List<Object> get props => [reputation]; List<Object> get props => [reputation];
} }
class ScoresForUserLoaded extends ScoreState {
final List<ScoreInfoUI> scores;
const ScoresForUserLoaded(this.scores);
@override
List<Object> get props => [scores];
}
+13 -9
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injector/injector.dart';
import 'package:prosappco/blocs/score_bloc/score_bloc.dart'; import 'package:prosappco/blocs/score_bloc/score_bloc.dart';
import 'package:score_repository/score_repository.dart'; import 'package:score_repository/score_repository.dart';
@@ -10,14 +11,17 @@ class DrawerReputation extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ScoreBloc, ScoreState>(builder: (context, state) { return BlocProvider(
if (state is ScoreSuccess) { create: (context) => Injector.appInstance.get<ScoreBloc>(),
return builder(state.reputation); child: BlocBuilder<ScoreBloc, ScoreState>(builder: (context, state) {
} else if (state is ScoreFailure) { if (state is ScoreSuccess) {
return const Center(child: Text('Error')); return builder(state.reputation);
} else { } else if (state is ScoreFailure) {
return const Center(child: CircularProgressIndicator()); return const Center(child: Text('Error'));
} } else {
}); return const Center(child: CircularProgressIndicator());
}
}),
);
} }
} }
+254 -232
View File
@@ -6,16 +6,20 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.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/my_user_bloc/my_user_bloc.dart';
import 'package:prosappco/blocs/professional_bloc/professional_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/drawer_reputation.dart';
import 'package:prosappco/components/general_drawer_header.dart'; import 'package:prosappco/components/general_drawer_header.dart';
import 'package:prosappco/components/general_drawer_item.dart'; import 'package:prosappco/components/general_drawer_item.dart';
import 'package:prosappco/screens/configuration/configuration_screen.dart'; import 'package:prosappco/screens/configuration/configuration_screen.dart';
import 'package:prosappco/screens/configuration/configuration_support_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_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_history_list_screen.dart';
import 'package:prosappco/screens/lists/professional_service_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_history_list_screen.dart';
import 'package:prosappco/screens/lists/user_service_list_screen.dart'; import 'package:prosappco/screens/lists/user_service_list_screen.dart';
import 'package:prosappco/screens/professional/professional_calendar_screen.dart'; import 'package:prosappco/screens/professional/professional_calendar_screen.dart';
@@ -47,252 +51,274 @@ class GeneralDrawer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<MyUserBloc, MyUserState>( return BlocProvider(
builder: (context, userState) { create: (context) => Injector.appInstance.get<ScoreBloc>(),
return BlocBuilder<ProfessionalBloc, ProfessionalState>( child: BlocBuilder<MyUserBloc, MyUserState>(
builder: (context, professionalState) { builder: (context, userState) {
return Drawer( return BlocBuilder<ProfessionalBloc, ProfessionalState>(
backgroundColor: Theme.of(context).colorScheme.secondary, builder: (context, professionalState) {
child: Column( return Drawer(
crossAxisAlignment: CrossAxisAlignment.stretch, backgroundColor: Theme.of(context).colorScheme.secondary,
children: [ child: Column(
const GeneralDrawerHeader(), crossAxisAlignment: CrossAxisAlignment.stretch,
Divider( children: [
color: Theme.of(context) const GeneralDrawerHeader(),
.colorScheme Divider(
.onSurface color: Theme.of(context)
.withOpacity(0.1), .colorScheme
thickness: 0.5, .onSurface
height: 1, .withOpacity(0.1),
), thickness: 0.5,
Expanded( height: 1,
child: SingleChildScrollView( ),
child: Column( Expanded(
children: [ child: SingleChildScrollView(
shouldProModeActive(context, professionalState) child: Column(
? GeneralDrawerItem( children: [
leading: Icons.person_outline_rounded, shouldProModeActive(context, professionalState)
label: 'Perfil profesional', ? GeneralDrawerItem(
onTap: () { leading: Icons.person_outline_rounded,
Navigator.push( label: 'Perfil profesional',
context, onTap: () {
CupertinoPageRoute( Navigator.push(
builder: (context) => context,
const ProfessionalProfileScreen(), CupertinoPageRoute(
), builder: (context) =>
); const ProfessionalProfileScreen(),
}, ),
) );
: const SizedBox(), },
GeneralDrawerItem( )
leading: Icons.checklist_outlined, : const SizedBox(),
label: 'Mis servicios', GeneralDrawerItem(
onTap: () { leading: Icons.checklist_outlined,
shouldProModeActive(context, professionalState) label: 'Mis servicios',
? Navigator.push( onTap: () {
context, shouldProModeActive(context, professionalState)
CupertinoPageRoute( ? Navigator.push(
builder: (context) => context,
const ProfessionalServiceListScreen(), CupertinoPageRoute(
// const UserServicesScreen(), builder: (context) =>
), const ProfessionalServiceListScreen(),
) // const UserServicesScreen(),
: Navigator.push( ),
context, )
CupertinoPageRoute( : Navigator.push(
builder: (context) => context,
const UserServiceListScreen(), CupertinoPageRoute(
// const UserServicesScreen(), builder: (context) =>
), const UserServiceListScreen(),
); // const UserServicesScreen(),
}, ),
), );
GeneralDrawerItem( },
leading: Icons.access_time_outlined, ),
label: 'Historial', GeneralDrawerItem(
onTap: () { leading: Icons.access_time_outlined,
shouldProModeActive(context, professionalState) label: 'Historial',
? Navigator.push( onTap: () {
context, shouldProModeActive(context, professionalState)
CupertinoPageRoute( ? Navigator.push(
builder: (context) => context,
const ProfessionalServiceHistoryListScreen(), CupertinoPageRoute(
), builder: (context) =>
) const ProfessionalServiceHistoryListScreen(),
: Navigator.push( ),
context, )
CupertinoPageRoute( : Navigator.push(
builder: (context) => context,
const UserServiceHistoryListScreen(), CupertinoPageRoute(
), builder: (context) =>
); const UserServiceHistoryListScreen(),
}, ),
), );
shouldProModeActive(context, professionalState) },
? GeneralDrawerItem( ),
onTap: () { shouldProModeActive(context, professionalState)
Navigator.push( ? GeneralDrawerItem(
context, onTap: () {
CupertinoPageRoute( Navigator.push(
builder: (context) => context,
const ProfessionalCalendarScreen(), CupertinoPageRoute(
), builder: (context) =>
); const ProfessionalCalendarScreen(),
}, ),
label: 'Calendario', );
leading: Icons.calendar_month_outlined, },
) label: 'Calendario',
: const SizedBox(), leading: Icons.calendar_month_outlined,
GeneralDrawerItem( )
leading: Icons.settings_outlined, : const SizedBox(),
label: 'Configuración', GeneralDrawerItem(
onTap: () { leading: Icons.settings_outlined,
Navigator.push( label: 'Configuración',
context, onTap: () {
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 {
Navigator.push( Navigator.push(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
builder: (BuildContext context) { builder: (context) =>
return WebViewScreen( const ConfigurationScreen(),
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, GeneralDrawerItem(
color: Colors.white, leading: Icons.help_outline,
), label: 'Soporte',
title: Text( onTap: () {
shouldProModeActive(context, professionalState) Navigator.push(
? 'Solicitudes' context,
: 'Solicitar servicio', CupertinoPageRoute(
style: const TextStyle( builder: (context) =>
fontSize: 16, 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, 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) {
DrawerReputation(builder: (reputation) { final isProModeActive =
final isProModeActive = (professionalState is LoadedModeProState) &&
(professionalState is LoadedModeProState) && professionalState.isProModeActive;
professionalState.isProModeActive;
final total = isProModeActive final total = isProModeActive
? reputation.totalPro ? reputation.totalPro
: reputation.total; : reputation.total;
final average = isProModeActive final average = isProModeActive
? reputation.averagePro ? reputation.averagePro
: reputation.average; : reputation.average;
return ListTile( return ListTile(
onTap: () {}, onTap: () {
trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black), final isProModeActive = (professionalState
title: Row( is LoadedModeProState) &&
children: [ professionalState.isProModeActive;
RatingBar.builder( isProModeActive
initialRating: calculoRating(average), ? Navigator.push(context,
minRating: 1, CupertinoPageRoute(
direction: Axis.horizontal, builder: (context) {
allowHalfRating: true, return const ProfessionalScoreListScreen();
itemCount: 5, }))
itemSize: 25, : Navigator.push(context,
maxRating: 5, CupertinoPageRoute(
itemBuilder: (context, _) => const Icon( builder: (context) {
Icons.star, return const UserScoreListScreen();
color: Color(0xFF2BA4EC), }));
},
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) {}, const SizedBox(width: 5),
ignoreGestures: true, Text(
), '${average.toStringAsFixed(1)} (${total.toString()})',
const SizedBox(width: 5), ),
Text( ],
'${average.toStringAsFixed(1)} (${total.toString()})', ),
), );
], }),
), const Divider(
); height: 1,
}), thickness: 0.5,
const Divider( ),
height: 1, Padding(
thickness: 0.5, padding: const EdgeInsets.only(top: 10),
), child: Text(
Padding( 'Prosapp ® todos los derechos reservados',
padding: const EdgeInsets.only(top: 10), style: TextStyle(
child: Text( fontSize: 10,
'Prosapp ® todos los derechos reservados', color: Colors.grey[700],
style: TextStyle( ),
fontSize: 10,
color: Colors.grey[700],
), ),
), ),
), ],
], ),
), ),
), ),
), Divider(
Divider( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onSurface
.onSurface .withOpacity(0.1),
.withOpacity(0.1), thickness: 0.5,
thickness: 0.5, height: 1,
height: 1, ),
), const SizedBox(height: 15),
const SizedBox(height: 15), Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 20), child: buttonOfState(context, professionalState),
child: buttonOfState(context, professionalState), ),
), const SizedBox(height: 15),
const SizedBox(height: 15), ],
], ),
), );
); },
}, );
); },
}, ),
); );
} }
@@ -389,10 +415,6 @@ class GeneralDrawer extends StatelessWidget {
'Modo profesional', 'Modo profesional',
style: TextStyle(color: Colors.white, fontSize: 18), style: TextStyle(color: Colors.white, fontSize: 18),
), ),
// const CircularProgressIndicator(
// color: Colors.white,
// ),
); );
} }
+13 -2
View File
@@ -47,8 +47,7 @@ class AppDI {
injector.registerSingleton<MyUserBloc>( injector.registerSingleton<MyUserBloc>(
(() => MyUserBloc(myUserRepository: injector.get<UserRepository>()))); (() => MyUserBloc(myUserRepository: injector.get<UserRepository>())));
injector.registerSingleton(
(() => ScoreBloc(firebaseScoreRepository: injector.get())));
injector.registerSingleton<ProfileBloc>( injector.registerSingleton<ProfileBloc>(
(() => ProfileBloc(userRepository: injector.get<UserRepository>()))); (() => ProfileBloc(userRepository: injector.get<UserRepository>())));
@@ -91,5 +90,17 @@ class AppDI {
professionRepository: injector.get<FirebaseProfessionalRepository>(), professionRepository: injector.get<FirebaseProfessionalRepository>(),
), ),
); );
injector.registerDependency<ScoreBloc>(
() => ScoreBloc(
scoreRepository: injector.get<FirebaseScoreRepository>(),
userRepository: injector.get<UserRepository>(),
),
);
// injector.registerSingleton((() => ScoreBloc(
// firebaseScoreRepository: injector.get(),
// userRepository: injector.get<UserRepository>(),
// )));
} }
} }
@@ -79,8 +79,8 @@ class _ProfessionalListScreenState extends State<ProfessionalListScreen> {
.where((element) => removeDiacritics(element.myUser.name!) .where((element) => removeDiacritics(element.myUser.name!)
.toLowerCase() .toLowerCase()
.contains(removeDiacritics(_searchController.text.toLowerCase()))) .contains(removeDiacritics(_searchController.text.toLowerCase())))
.where((user) => // .where((user) =>
user.myUser.id != FirebaseAuth.instance.currentUser!.uid) // user.myUser.id != FirebaseAuth.instance.currentUser!.uid)
.toList(); .toList();
// } else { // } else {
// filteredUsers = state.users // filteredUsers = state.users
@@ -183,12 +183,19 @@ class _ProfessionalListScreenState extends State<ProfessionalListScreen> {
) )
: null, : null,
), ),
title: Text( title: Row(
'${filteredUsers[index].myUser.name ?? ''}, ${filteredUsers[index].professionalInfo.profession}', children: [
overflow: TextOverflow.ellipsis, Flexible(
style: const TextStyle( child: Text(
fontSize: 15, '${filteredUsers[index].myUser.name ?? ''}, ',
), overflow: TextOverflow.ellipsis,
),
),
Text(
filteredUsers[index].professionalInfo.profession,
overflow: TextOverflow.ellipsis,
),
],
), ),
subtitle: Text( subtitle: Text(
_disponibilidad(filteredUsers[index].professionalInfo), _disponibilidad(filteredUsers[index].professionalInfo),
@@ -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<ProfessionalScoreListScreen> createState() =>
_ProfessionalScoreListScreenState();
}
class _ProfessionalScoreListScreenState
extends State<ProfessionalScoreListScreen> {
late final ScoreBloc scoreBloc;
@override
void initState() {
super.initState();
scoreBloc = Injector.appInstance.get<ScoreBloc>();
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<ScoreBloc, ScoreState>(
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),
),
),
),
),
);
},
),
),
);
}
}
@@ -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<UserScoreListScreen> createState() => _UserScoreListScreenState();
}
class _UserScoreListScreenState extends State<UserScoreListScreen> {
late final ScoreBloc scoreBloc;
@override
void initState() {
super.initState();
scoreBloc = Injector.appInstance.get<ScoreBloc>();
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<ScoreBloc, ScoreState>(
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),
),
),
),
),
);
},
),
),
);
}
}
+306 -296
View File
@@ -43,319 +43,329 @@ class _ScoreScreenState extends State<ScoreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return BlocProvider(
appBar: AppBar( create: (context) => Injector.appInstance.get<ScoreBloc>(),
title: const Text('Calificación'), child: Scaffold(
), appBar: AppBar(
body: Column( title: const Text('Calificación'),
children: [ ),
Expanded( body: Column(
child: SingleChildScrollView( children: [
child: Column( Expanded(
children: [ child: SingleChildScrollView(
FutureBuilder( child: Column(
future: _userInfoFuture, children: [
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) { FutureBuilder(
if (snapshot.connectionState == ConnectionState.waiting) { future: _userInfoFuture,
return const Center(child: CircularProgressIndicator()); builder:
} else { (context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasError) { if (snapshot.connectionState ==
return Center( ConnectionState.waiting) {
child: Text('Error inesperado: ${snapshot.error}'), return const Center(
); child: CircularProgressIndicator());
} else { } 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( return SizedBox(
width: double.infinity, width: double.infinity,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 95, width: 95,
height: 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,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.grey.shade300,
borderRadius: shape: BoxShape.circle,
BorderRadius.circular(20), image: userInfo.picture == null
boxShadow: [ ? null
BoxShadow( : DecorationImage(
color: image: NetworkImage(
Colors.black.withOpacity(0.1), userInfo.picture!),
spreadRadius: 1, fit: BoxFit.contain,
blurRadius: 2, ),
offset: const Offset(
0,
1,
),
),
],
), ),
child: Row( child: userInfo.picture == null
mainAxisSize: MainAxisSize.min, ? Icon(
children: [ CupertinoIcons.person,
const Icon( color: Colors.grey.shade400,
Icons.star, size: 50,
color: Colors.yellow, )
size: 20, : null,
), ),
const SizedBox(width: 4), const SizedBox(height: 8),
Flexible( Text(
child: Text( '${userInfo.name}',
average.toStringAsFixed(2), style: const TextStyle(
style: const TextStyle( fontSize: 20,
fontSize: 15, 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( // padding: const EdgeInsets.symmetric(horizontal: 15),
// padding: const EdgeInsets.symmetric(horizontal: 15), // child: ListTile(
// child: ListTile( // leading: Container(
// leading: Container( // width: 60,
// width: 60, // height: 60,
// height: 60, // decoration: BoxDecoration(
// decoration: BoxDecoration( // color: Colors.grey.shade300,
// color: Colors.grey.shade300, // shape: BoxShape.circle,
// shape: BoxShape.circle, // image: userInfo.picture == null
// image: userInfo.picture == null // ? null
// ? null // : DecorationImage(
// : DecorationImage( // image: NetworkImage(
// image: NetworkImage( // userInfo.picture ?? ''),
// userInfo.picture ?? ''), // fit: BoxFit.contain,
// fit: BoxFit.contain, // ),
// ), // ),
// ), // child: userInfo.picture == null
// child: userInfo.picture == null // ? Icon(
// ? Icon( // CupertinoIcons.person,
// CupertinoIcons.person, // color: Colors.grey.shade400,
// color: Colors.grey.shade400, // size: 40,
// size: 40, // )
// ) // : null,
// : null, // ),
// ), // title: Text(
// title: Text( // userInfo.name ?? '',
// userInfo.name ?? '', // overflow: TextOverflow.ellipsis,
// overflow: TextOverflow.ellipsis, // style: const TextStyle(
// style: const TextStyle( // fontSize: 15,
// fontSize: 15, // fontWeight: FontWeight.w600,
// fontWeight: FontWeight.w600, // ),
// ), // ),
// ), // subtitle: const Text(
// subtitle: const Text( // 'Rating: 5.0',
// 'Rating: 5.0', // style: TextStyle(
// style: TextStyle( // fontSize: 13,
// fontSize: 13, // color: Colors.blue,
// 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) { const SizedBox(height: 10),
setState(() { const Text(
_rating = rating; 'Califica el servicio',
}); style:
}, TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
ignoreGestures: false, ),
), const SizedBox(height: 10),
Padding( RatingBar.builder(
padding: const EdgeInsets.only(top: 8), initialRating: _rating,
child: Text( minRating: 1,
customMessage(_rating), direction: Axis.horizontal,
style: const TextStyle( allowHalfRating: true,
fontSize: 16, itemCount: 5,
itemSize: 40,
glow: false,
maxRating: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 5),
itemBuilder: (context, _) => const Icon(
Icons.star,
color: Color(0xFF2BA4EC), color: Color(0xFF2BA4EC),
), ),
onRatingUpdate: (rating) {
setState(() {
_rating = rating;
});
},
ignoreGestures: false,
), ),
), Padding(
Padding( padding: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric( child: Text(
horizontal: 40, vertical: 40), customMessage(_rating),
child: Column( style: const TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 16,
children: [ color: Color(0xFF2BA4EC),
const Text( ),
'Comentario', ),
),
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<ServiceBloc>(
create: (context) => Injector.appInstance.get<ServiceBloc>(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
child: BlocBuilder<ServiceBloc, ServiceState>(
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<ScoreBloc>(context)
.add(SendScoreEvent(comment: comment));
if (widget.service.userId ==
FirebaseAuth.instance.currentUser!.uid) {
context.read<ServiceBloc>().add(
UpdateProfessionalScored(widget.service.id!));
} else {
context
.read<ServiceBloc>()
.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( style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w600), color: Colors.white,
), fontSize: 18,
TextFormField( ),
maxLines: null,
maxLength: 400,
keyboardType: TextInputType.multiline,
controller: commentController,
),
],
),
),
],
),
),
),
const Divider(
height: 1,
thickness: 0.5,
),
BlocProvider<ServiceBloc>(
create: (context) => Injector.appInstance.get<ServiceBloc>(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
child: BlocBuilder<ServiceBloc, ServiceState>(
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<ScoreBloc>(context)
.add(SendScoreEvent(comment: comment));
if (widget.service.userId ==
FirebaseAuth.instance.currentUser!.uid) {
context
.read<ServiceBloc>()
.add(UpdateProfessionalScored(widget.service.id!));
} else {
context
.read<ServiceBloc>()
.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,
), ),
), ),
), );
); },
}, ),
), ),
), ),
), ],
], ),
), ),
); );
} }
@@ -63,6 +63,26 @@ class FirebaseScoreRepository {
} }
} }
Stream<List<CommentEntity>> 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<List<CommentEntity>> 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 { addComment(CommentEntity comment) async {
await commentsCollection.add(comment.toDocument()); await commentsCollection.add(comment.toDocument());