import 'dart:async'; import 'package:community_material_icon/community_material_icon.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:professional_repository/professional_repository.dart'; import 'package:prosappco/blocs/service_bloc/service_bloc.dart'; import 'package:prosappco/components/general_reputation.dart'; import 'package:prosappco/components/general_secondary_button.dart'; import 'package:prosappco/screens/chat/chat_screen.dart'; import 'package:prosappco/screens/score/score_screen.dart'; import 'package:score_repository/score_repository.dart'; import 'package:service_repository/service_repository.dart'; import 'package:setting_repository/setting_repository.dart'; import 'package:url_launcher/url_launcher.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); Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35); bool get isDark => _t.brightness == Brightness.dark; Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.05); } class ProfessionalServiceScreen extends StatefulWidget { final String serviceId; const ProfessionalServiceScreen({super.key, required this.serviceId}); @override State createState() => _ProfessionalServiceScreenState(); } class _ProfessionalServiceScreenState extends State { final settingRepository = Injector.appInstance.get(); SettingEntity? settings; Timer? _timer; @override void initState() { super.initState(); _loadSettings(); _startTimer(); } @override void dispose() { _timer?.cancel(); super.dispose(); } void _startTimer() { _timer = Timer.periodic(const Duration(minutes: 5), (_) { setState(() {}); }); } void _loadSettings() { settingRepository.getSettings().then((v) => setState(() => settings = v)); } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => Injector.appInstance.get(), child: Scaffold( backgroundColor: context.bg, appBar: AppBar( title: const Text('Detalle del servicio'), backgroundColor: _kPrimary, foregroundColor: Colors.white, elevation: 0, ), body: BlocBuilder( builder: (context, state) { if (state is ServiceLoaded) { final service = state.service; return FutureBuilder>( future: _getUserInfo(service), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center( child: Text('Error: ${snapshot.error}')); } final userInfo = snapshot.data![0] as MyUser; return Column( children: [ Expanded( child: SingleChildScrollView( child: Column( children: [ _headerCard(context, service, userInfo), _locationCard(context, service), if (service.description.isNotEmpty) _descriptionCard(context, service), _actionButtons(context, service, userInfo), _statusBanner(context, service), const SizedBox(height: 24), ], ), ), ), const Divider(height: 1, thickness: 0.5), Padding( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 16), child: _bottomButton(context, service), ), ], ); }, ); } else { BlocProvider.of(context) .add(LoadService(widget.serviceId)); return const Center(child: CircularProgressIndicator()); } }, ), ), ); } Widget _headerCard( BuildContext context, ServiceEntity service, 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: Column( children: [ Stack( alignment: Alignment.center, children: [ // Avatar Container( width: 90, height: 90, 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: 40) : null, ), // Star rating badge GeneralReputation( userId: service.userId, builder: (_, ReputationEntity reputation) { 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(reputation.average.toStringAsFixed(1), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: context.onSurface)), ]), ), ); }, ), ], ), const SizedBox(height: 10), Text(user.name ?? '', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: context.onSurface)), const SizedBox(height: 4), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.calendar_today_outlined, size: 13, color: context.muted), const SizedBox(width: 5), Text( '${DateFormat('dd MMMM', 'es').format(DateTime.parse(service.day))} · ${ScheduleEntity.getFormatTime(service.range1Hour1) ?? ''}', style: TextStyle( fontSize: 13, color: context.muted, fontWeight: FontWeight.w500), ), ]), ], ), ); } Widget _locationCard(BuildContext context, ServiceEntity service) { final isDelivery = service.location == ServiceLocationPreferences.delivery; if (!isDelivery && service.status != ServiceStatus.acepted && service.status != ServiceStatus.pending && service.status != ServiceStatus.active) { return const SizedBox(); } return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: _kPrimary.withOpacity(0.06), borderRadius: BorderRadius.circular(12), border: Border.all(color: _kPrimary.withOpacity(0.15)), ), child: Row( children: [ Icon( isDelivery ? Icons.home_outlined : Icons.business_outlined, color: _kPrimary, size: 20), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isDelivery ? 'Servicio a domicilio' : 'Servicio en consultorio', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _kPrimary)), if (isDelivery && (service.status == ServiceStatus.acepted || service.status == ServiceStatus.pending || service.status == ServiceStatus.active) && service.address.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text(service.address, style: TextStyle( fontSize: 12, color: context.muted)), ), ], ), ), if (isDelivery && (service.status == ServiceStatus.acepted || service.status == ServiceStatus.pending || service.status == ServiceStatus.active)) IconButton( icon: Icon(Icons.near_me, color: _kPrimary), onPressed: () async { final url = Uri.parse( 'https://www.google.com/maps/search/?api=1&query=${service.latitude},${service.longitude}'); if (!await launchUrl(url)) throw Exception('No se pudo abrir mapa'); }, ), ], ), ); } Widget _descriptionCard(BuildContext context, ServiceEntity service) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow(color: context.shadowSm, blurRadius: 8) ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.format_quote_rounded, color: context.subtle, size: 20), const SizedBox(width: 8), Expanded( child: Text( service.description.trim(), style: TextStyle( fontSize: 13, color: context.muted, fontStyle: FontStyle.italic, height: 1.5), ), ), ], ), ); } Widget _actionButtons( BuildContext context, ServiceEntity service, MyUser user) { final serviceDate = DateTime.parse(service.day); final now = DateTime.now(); final serviceDateTime = DateTime(serviceDate.year, serviceDate.month, serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute); if ((service.status == ServiceStatus.acepted || service.status == ServiceStatus.active) && !now.isAfter(serviceDateTime)) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (user.phone != null) _ActionBtn( icon: Icons.phone_outlined, label: 'Llamar', onTap: () => launchUrl(Uri.parse('tel:${user.phone}')), ), if (user.phone != null) const SizedBox(width: 10), _ActionBtn( icon: Icons.chat_bubble_outline_rounded, label: 'Chat', onTap: () { if (service.id != null) { Navigator.push( context, CupertinoPageRoute( builder: (_) => ChatScreen(service: service))); } }, ), if (user.phone != null) const SizedBox(width: 10), if (user.phone != null) _ActionBtn( icon: CommunityMaterialIcons.whatsapp, label: 'WhatsApp', onTap: () async { final url = 'https://wa.me/${user.phone}?text=${Uri.encodeFull('Hola! me contactaste por Prossapp')}'; if (!await launchUrl(Uri.parse(url))) { throw Exception('No se pudo abrir WhatsApp'); } }, ), ], ), ); } return const SizedBox(); } Widget _statusBanner(BuildContext context, ServiceEntity service) { final serviceDate = DateTime.parse(service.day); final now = DateTime.now(); final serviceDateTime = DateTime(serviceDate.year, serviceDate.month, serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute); final bool expired = now.isAfter(serviceDateTime); if (expired) { return _StatusCard( color: const Color(0xFFDC2626), icon: Icons.timer_off_outlined, title: 'Caducado', subtitle: 'El tiempo del servicio ha expirado', ); } switch (service.status) { case ServiceStatus.cancelled: return _StatusCard( color: const Color(0xFF9CA3AF), icon: Icons.remove_circle_outline, title: 'Cancelado', subtitle: 'El servicio fue cancelado', ); case ServiceStatus.denied: return _StatusCard( color: const Color(0xFFDC2626), icon: Icons.cancel_outlined, title: 'Rechazado', subtitle: 'El servicio fue rechazado', ); case ServiceStatus.completed: if (service.userScored == false) { return _StatusCard( color: const Color(0xFF16A34A), icon: Icons.star_outline_rounded, title: 'Completado', subtitle: 'Califica el servicio', action: _ScoreButton(service: service), ); } return _StatusCard( color: const Color(0xFF16A34A), icon: Icons.task_alt_outlined, title: 'Completado', subtitle: '¡Servicio finalizado exitosamente!', ); default: return const SizedBox(); } } Widget _bottomButton(BuildContext context, ServiceEntity service) { final serviceDate = DateTime.parse(service.day); final now = DateTime.now(); final serviceDateTime = DateTime(serviceDate.year, serviceDate.month, serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute); if (now.isAfter(serviceDateTime)) { return GeneralSecondaryButton( label: 'Volver', onPressed: () => Navigator.pop(context)); } if (service.status == ServiceStatus.pending) { return Column( children: [ GeneralSecondaryButton( label: 'Rechazar servicio', color: Theme.of(context).colorScheme.error, onPressed: () { final s = context.read().state; if (s is ServiceLoaded) { context.read().add( UpdateServiceStatus(widget.serviceId, ServiceStatus.denied)); } }, ), const SizedBox(height: 10), GeneralSecondaryButton( label: 'Aceptar servicio', onPressed: () { final s = context.read().state; if (s is ServiceLoaded) { context.read().add(UpdateServiceStatus( widget.serviceId, ServiceStatus.acepted)); } }, ), ], ); } if (service.status == ServiceStatus.acepted) { if (serviceDateTime.difference(now).inHours > 1) { return GeneralSecondaryButton( label: 'Cancelar servicio', color: Theme.of(context).colorScheme.error, onPressed: () { final s = context.read().state; if (s is ServiceLoaded) { context.read().add(UpdateServiceStatus( widget.serviceId, ServiceStatus.cancelled)); } }, ); } else { return GeneralSecondaryButton( label: 'Iniciar servicio', onPressed: () { final s = context.read().state; if (s is ServiceLoaded) { context.read().add( UpdateServiceStatus(widget.serviceId, ServiceStatus.active)); } }, ); } } if (service.status == ServiceStatus.active) { return GeneralSecondaryButton( label: 'Terminar servicio', onPressed: () { final s = context.read().state; if (s is ServiceLoaded) { context.read().add(UpdateServiceStatus( widget.serviceId, ServiceStatus.completed)); } }, ); } return GeneralSecondaryButton( label: 'Volver', onPressed: () => Navigator.pop(context)); } String formatCurrency(int number) { final formatter = NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: ''); return '\$${formatter.format(number)}'; } Future> _getUserInfo(ServiceEntity service) async { final userRepo = Injector.appInstance.get(); final userInfo = await userRepo.getMyUser(service.userId); return [userInfo]; } } // ── shared widgets ────────────────────────────────────────────────────────── class _ActionBtn extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; const _ActionBtn( {required this.icon, required this.label, required this.onTap}); @override Widget build(BuildContext context) { return OutlinedButton.icon( onPressed: onTap, icon: Icon(icon, size: 16), label: Text(label, style: const TextStyle(fontSize: 12)), style: OutlinedButton.styleFrom( foregroundColor: _kPrimary, side: BorderSide(color: _kPrimary.withOpacity(0.5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), ), ); } } class _StatusCard extends StatelessWidget { final Color color; final IconData icon; final String title; final String subtitle; final Widget? action; const _StatusCard({ required this.color, required this.icon, required this.title, required this.subtitle, this.action, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(16), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: color.withOpacity(0.15), shape: BoxShape.circle, ), child: Icon(icon, color: color, size: 24), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: color)), Text(subtitle, style: TextStyle( fontSize: 12, color: color.withOpacity(0.75))), ], ), ), ], ), if (action != null) ...[ const SizedBox(height: 12), action!, ], ], ), ); } } class _ScoreButton extends StatelessWidget { final ServiceEntity service; const _ScoreButton({required this.service}); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => Navigator.push( context, CupertinoPageRoute(builder: (_) => ScoreScreen(service: service)), ), icon: const Icon(Icons.star_outline_rounded, size: 18), label: const Text('Calificar servicio'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF16A34A), foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ); } }