import 'package:flutter/cupertino.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/score_bloc/score_bloc.dart'; import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; import 'package:prosappco/components/general_reputation.dart'; import 'package:score_repository/score_repository.dart'; import 'package:service_repository/service_repository.dart'; import 'package:user_repository/user_repository.dart'; const _kPrimary = Color(0xFF1565C0); extension _Th on BuildContext { ThemeData get _t => Theme.of(this); Color get bg => _t.scaffoldBackgroundColor; Color get card => _t.cardColor; Color get onSurface => _t.colorScheme.onSurface; Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); bool get isDark => _t.brightness == Brightness.dark; Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.05); } class ScoreScreen extends StatefulWidget { final ServiceEntity service; const ScoreScreen({Key? key, required this.service}) : super(key: key); @override State createState() => _ScoreScreenState(); } class _ScoreScreenState extends State { double _rating = 1.0; final TextEditingController _commentController = TextEditingController(); late Future> _userInfoFuture; late bool isProfessional; late String userId; bool _isSending = false; /// True when the person rating is the client (they rate the professional). bool get isUser => widget.service.userId == (ApiUserRepository.currentUserId ?? ''); @override void initState() { super.initState(); _userInfoFuture = _getUserInfo(widget.service); isProfessional = (ApiUserRepository.currentUserId ?? '') == widget.service.userId; userId = isProfessional ? widget.service.professionalId : widget.service.userId; } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => Injector.appInstance.get(), child: BlocListener( listener: (context, state) { if (state is ScoreSending) { setState(() => _isSending = true); } else if (state is ScoreSent) { setState(() => _isSending = false); // Only now is the rating actually stored, so only now do we mark // the service as scored and leave the screen. if (isUser) { context .read() .add(UpdateProfessionalScored(widget.service.id!)); } else { context .read() .add(UpdateUserScored(widget.service.id!)); } ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('¡Gracias por tu calificación!')), ); Navigator.pop(context); } else if (state is ScoreSendFailure) { setState(() => _isSending = false); ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'No se pudo enviar tu calificación. Inténtalo de nuevo.')), ); } }, child: Scaffold( backgroundColor: context.bg, appBar: AppBar( title: const Text('Calificar servicio'), backgroundColor: _kPrimary, foregroundColor: Colors.white, elevation: 0, ), body: Column( children: [ Expanded( child: SingleChildScrollView( child: Column( children: [ FutureBuilder>( future: _userInfoFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Padding( padding: EdgeInsets.all(40), child: CircularProgressIndicator(), ); } if (snapshot.hasError) { return Padding( padding: const EdgeInsets.all(24), child: Text('Error: ${snapshot.error}'), ); } final userInfo = snapshot.data![0] as MyUser; return _profileHeader(context, userInfo); }, ), const SizedBox(height: 4), _ratingSection(context), _commentSection(context), ], ), ), ), const Divider(height: 1, thickness: 0.5), BlocProvider( create: (_) => Injector.appInstance.get(), child: Padding( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 16), child: BlocBuilder( builder: (context, serviceState) { return _sendButton(context); }, ), ), ), ], ), ), ), ); } Widget _profileHeader(BuildContext context, MyUser user) { return Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2)) ], ), child: Stack( alignment: Alignment.center, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 84, height: 84, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: _kPrimary.withOpacity(0.25), width: 3), color: Colors.grey.shade200, image: user.picture != null && user.picture!.isNotEmpty ? DecorationImage( image: NetworkImage(user.picture!), fit: BoxFit.cover) : null, ), child: user.picture == null || user.picture!.isEmpty ? Icon(CupertinoIcons.person, color: Colors.grey.shade500, size: 38) : null, ), const SizedBox(height: 10), Text(user.name ?? '', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: context.onSurface)), ], ), GeneralReputation( userId: userId, builder: (_, ReputationEntity reputation) { final average = isProfessional ? reputation.averagePro : reputation.average; return Positioned( top: 0, right: MediaQuery.of(context).size.width * 0.18, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: context.shadowSm, blurRadius: 6, spreadRadius: 1) ], ), child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.star, color: Colors.amber, size: 14), const SizedBox(width: 3), Text(average.toStringAsFixed(1), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: context.onSurface)), ]), ), ); }, ), ], ), ); } Widget _ratingSection(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8)], ), child: Column( children: [ Text('¿Cómo fue el servicio?', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: context.onSurface)), const SizedBox(height: 14), RatingBar.builder( initialRating: _rating, minRating: 1, direction: Axis.horizontal, allowHalfRating: true, itemCount: 5, itemSize: 42, glow: false, maxRating: 5, itemPadding: const EdgeInsets.symmetric(horizontal: 4), itemBuilder: (_, __) => const Icon(Icons.star_rounded, color: _kPrimary), onRatingUpdate: (r) => setState(() => _rating = r), ), const SizedBox(height: 8), Text( _ratingMessage(_rating), style: TextStyle(fontSize: 14, color: _kPrimary, fontWeight: FontWeight.w500), ), ], ), ); } Widget _commentSection(BuildContext context) { return Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8)], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Comentario (opcional)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: context.onSurface)), const SizedBox(height: 10), TextFormField( controller: _commentController, maxLines: 4, maxLength: 400, keyboardType: TextInputType.multiline, decoration: InputDecoration( hintText: 'Cuéntanos tu experiencia...', hintStyle: TextStyle(color: context.muted, fontSize: 13), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kPrimary, width: 2), ), contentPadding: const EdgeInsets.all(12), ), ), ], ), ); } Widget _sendButton(BuildContext context) { return SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isSending ? null : () { final comment = CommentEntity( serviceId: widget.service.id!, authorId: ApiUserRepository.currentUserId ?? '', isFromUser: isUser, score: _rating, destinationId: isUser ? widget.service.professionalId : widget.service.userId, content: _commentController.text.trim(), createdAt: DateTime.now().toIso8601String(), ); // No pop here: closing the screen used to kill the BlocProvider // while the request was still in flight, so a failed rating looked // exactly like a saved one. The listener closes it on success. BlocProvider.of(context) .add(SendScoreEvent(comment: comment)); }, icon: const Icon(Icons.send_rounded, size: 18), label: const Text('Enviar calificación', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: _kPrimary, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ); } String _ratingMessage(double rating) { if (rating > 4.0) return '¡Excelente!'; if (rating < 2.0) return 'Necesita mejorar'; if (rating >= 3.0) return '¡Bien!'; return 'Regular'; } Future> _getUserInfo(ServiceEntity service) async { final userRepo = Injector.appInstance.get(); final currentId = ApiUserRepository.currentUserId ?? ''; final targetId = currentId == service.userId ? service.professionalId : service.userId; final userInfo = await userRepo.getMyUser(targetId); return [userInfo]; } }