import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:injector/injector.dart'; import 'package:professional_repository/professional_repository.dart'; import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart'; import 'package:prosappco/blocs/professional_profile_bloc/professional_profile_bloc.dart'; import 'package:prosappco/screens/professional/professional_map_screen.dart'; import 'package:prosappco/screens/professional/professional_schedule_screen.dart'; import 'package:setting_repository/setting_repository.dart'; const _kPrimary = Color(0xFF1565C0); const _kAccent = Color(0xFF42A4EF); 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 ProfessionalProfileScreen extends StatefulWidget { const ProfessionalProfileScreen({super.key}); @override State createState() => _ProfessionalProfileScreenState(); } class _ProfessionalProfileScreenState extends State { final settingRepository = Injector.appInstance.get(); SettingEntity? settings; XFile? _imageFile; bool loadFinish = false; bool officeValue = false; bool deliveryValue = false; bool rateValue = false; final _addressController = TextEditingController(); final _aditionalAddressController = TextEditingController(); final _rateController = TextEditingController(); Schedules schedules = Schedules.empty; bool isInit = false; double _longitudeController = 0; double _latitudeController = 0; bool isNequiActive = false; bool isDatafonoActive = false; bool isTransferActive = false; @override void initState() { super.initState(); settingRepository.getSettings().then( (v) => setState(() => settings = v), ); } @override void dispose() { _addressController.dispose(); _aditionalAddressController.dispose(); _rateController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: context.bg, appBar: AppBar( title: const Text('Perfil Profesional'), backgroundColor: _kPrimary, foregroundColor: Colors.white, elevation: 0, ), body: BlocBuilder( builder: (context, state) { if (state is LoadedModeProState) { if (state.isProModeActive) { if (state.proInfo == null) { return const Center(child: CircularProgressIndicator(color: _kPrimary)); } if (!isInit) { schedules = state.proInfo!.schedules; isInit = true; } return SingleChildScrollView( child: _body(context, state.proInfo!), ); } } return const Center(child: Text('Vuelve atrás')); }, ), ); } Widget _body(BuildContext context, ProfessionalEntity proInfo) { if (!loadFinish) { _addressController.text = proInfo.address; _aditionalAddressController.text = proInfo.aditionalAddress; _longitudeController = proInfo.longitude; _latitudeController = proInfo.latitude; isNequiActive = proInfo.paymentMethods.nequi; isDatafonoActive = proInfo.paymentMethods.datafono; isTransferActive = proInfo.paymentMethods.transferencia; if (proInfo.locationPreferences == LocationPreferences.both) { officeValue = true; deliveryValue = true; } else if (proInfo.locationPreferences == LocationPreferences.office) { officeValue = true; deliveryValue = false; } else if (proInfo.locationPreferences == LocationPreferences.delivery) { officeValue = false; deliveryValue = true; } _rateController.text = proInfo.rate; loadFinish = true; } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _bannerHeader(context, proInfo), const SizedBox(height: 16), _sectionCard( context, icon: Icons.place_outlined, title: 'Modalidad de servicio', child: Column(children: [ if (settings?.domicilios ?? false) _toggleRow( context, icon: Icons.delivery_dining_outlined, title: 'Servicio a domicilio', subtitle: 'El profesional se desplaza al cliente', value: deliveryValue, onChanged: (v) => setState(() { deliveryValue = v; if (!deliveryValue) officeValue = true; }), ), _toggleRow( context, icon: Icons.storefront_outlined, title: 'Servicio en sitio', subtitle: 'El cliente asiste al lugar del profesional', value: officeValue, onChanged: (v) => setState(() { if (settings?.domicilios == true) { officeValue = v; if (!officeValue) deliveryValue = true; } else { officeValue = true; deliveryValue = false; } }), ), if (officeValue) ...[ const SizedBox(height: 8), _addressField(context), ], ]), ), const SizedBox(height: 12), _sectionCard( context, icon: Icons.attach_money_outlined, title: 'Tarifa', child: Column(children: [ _toggleRow( context, icon: Icons.monetization_on_outlined, title: 'Mostrar tarifa', subtitle: 'Visible para los clientes', value: rateValue, onChanged: (v) => setState(() { rateValue = (settings?.tarifas == true) ? v : false; }), ), if (rateValue) ...[ const SizedBox(height: 8), TextField( controller: _rateController, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: InputDecoration( prefixIcon: Icon(Icons.attach_money, color: context.muted), hintText: 'Valor en COP', filled: true, fillColor: context.isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.04), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), ), ], ]), ), const SizedBox(height: 12), _sectionCard( context, icon: Icons.credit_card_outlined, title: 'Métodos de pago', child: Wrap(spacing: 8, runSpacing: 8, children: [ _PayChip( label: 'Datafono', icon: Icons.credit_card, active: isDatafonoActive, onTap: () => setState(() => isDatafonoActive = !isDatafonoActive), ), _PayChip( label: 'Nequi', icon: Icons.phone_android_outlined, active: isNequiActive, onTap: () => setState(() => isNequiActive = !isNequiActive), ), _PayChip( label: 'Transferencia', icon: Icons.swap_horiz, active: isTransferActive, onTap: () => setState(() => isTransferActive = !isTransferActive), ), ]), ), const SizedBox(height: 12), _sectionCard( context, icon: Icons.calendar_month_outlined, title: 'Horarios', trailing: GestureDetector( onTap: _openScheduleEditor, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: _kPrimary.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: const Text('Editar', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _kPrimary)), ), ), child: _scheduleRows(context), ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ElevatedButton( onPressed: _save, style: ElevatedButton.styleFrom( backgroundColor: _kPrimary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), elevation: 0, ), child: const Text('Guardar cambios', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), ), ), const SizedBox(height: 32), ], ); } Widget _bannerHeader(BuildContext context, ProfessionalEntity proInfo) { final pathImageFile = _imageFile?.path; ImageProvider? bannerImage; if (pathImageFile != null && pathImageFile.isNotEmpty) { bannerImage = FileImage(File(pathImageFile)); } else if (proInfo.bannerPicture.isNotEmpty) { bannerImage = NetworkImage(proInfo.bannerPicture); } return GestureDetector( onTap: _pickBannerImage, child: Container( height: 180, decoration: BoxDecoration( gradient: const LinearGradient( colors: [_kPrimary, _kAccent], begin: Alignment.topLeft, end: Alignment.bottomRight, ), image: bannerImage != null ? DecorationImage(image: bannerImage, fit: BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.35), BlendMode.darken)) : null, ), child: Stack(children: [ Positioned( bottom: 14, right: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(20), ), child: const Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.camera_alt_outlined, color: Colors.white, size: 14), SizedBox(width: 5), Text('Cambiar banner', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600)), ]), ), ), if (proInfo.profession.isNotEmpty) Positioned( bottom: 14, left: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white38), ), child: Text(proInfo.profession, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600)), ), ), ]), ), ); } Widget _sectionCard( BuildContext context, { required IconData icon, required String title, required Widget child, Widget? trailing, }) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: context.shadowSm, blurRadius: 10, offset: const Offset(0, 2)) ], ), child: Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: _kPrimary.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 17, color: _kPrimary), ), const SizedBox(width: 10), Expanded( child: Text(title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: context.onSurface)), ), if (trailing != null) trailing, ]), const SizedBox(height: 14), child, ]), ), ); } Widget _toggleRow( BuildContext context, { required IconData icon, required String title, required String subtitle, required bool value, required ValueChanged onChanged, }) { return Row(children: [ Icon(icon, size: 18, color: context.muted), const SizedBox(width: 10), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: context.onSurface)), Text(subtitle, style: TextStyle(fontSize: 11, color: context.muted)), ]), ), Switch(value: value, onChanged: onChanged, activeColor: _kPrimary), ]); } Widget _addressField(BuildContext context) { return Column(children: [ GestureDetector( onTap: () async { final Map? mapInfo = await Navigator.push( context, CupertinoPageRoute( builder: (_) => const ProfessionalMapScreen()), ); if (mapInfo != null) { setState(() { _addressController.text = mapInfo['address'] ?? ''; _latitudeController = mapInfo['latitude'] ?? 0; _longitudeController = mapInfo['longitude'] ?? 0; }); } }, child: AbsorbPointer( child: TextField( controller: _addressController, decoration: InputDecoration( prefixIcon: Icon(Icons.location_on_outlined, color: context.muted), hintText: 'Dirección', filled: true, fillColor: context.isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.04), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), ), ), ), ), const SizedBox(height: 8), TextField( controller: _aditionalAddressController, decoration: InputDecoration( prefixIcon: Icon(Icons.house_outlined, color: context.muted), hintText: 'Piso / Conjunto / Apartamento', filled: true, fillColor: context.isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.04), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), ), ), ]); } Widget _scheduleRows(BuildContext context) { final days = <_DayEntry>[ _DayEntry('Lunes', schedules.monday), _DayEntry('Martes', schedules.tuesday), _DayEntry('Miércoles', schedules.wednesday), _DayEntry('Jueves', schedules.thursday), _DayEntry('Viernes', schedules.friday), _DayEntry('Sábado', schedules.saturday), _DayEntry('Domingo', schedules.sunday), ]; return Column( children: days.map((entry) { final name = entry.name; final s = entry.schedule; final active = s != null && s.enabled; return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: active ? const Color(0xFF16A34A) : context.subtle, shape: BoxShape.circle), ), const SizedBox(width: 10), SizedBox( width: 80, child: Text(name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: context.onSurface)), ), Expanded( child: Text( _formatSchedule(s), style: TextStyle(fontSize: 12, color: context.muted), overflow: TextOverflow.ellipsis, ), ), ]), ); }).toList(), ); } String _formatSchedule(ScheduleEntity? s) { if (s == null || !s.enabled || s.range1Hour1 == null || s.range2Hour2 == null) { return 'Sin horario'; } if (s.continuousDay) { return '${ScheduleEntity.getFormatTime(s.range1Hour1)} - ${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } if (s.range1Hour2 == null || s.range2Hour1 == null) return 'Sin horario'; return '${ScheduleEntity.getFormatTime(s.range1Hour1)} a ${ScheduleEntity.getFormatTime(s.range1Hour2)} · ' '${ScheduleEntity.getFormatTime(s.range2Hour1)} a ${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } Future _pickBannerImage() async { final picker = ImagePicker(); final image = await picker.pickImage( source: ImageSource.gallery, maxHeight: 1000, maxWidth: 2000, imageQuality: 40, ); if (image != null) setState(() => _imageFile = image); } Future _openScheduleEditor() async { final result = await Navigator.push( context, CupertinoPageRoute( builder: (_) => ProfessionalScheduleScreen(schedules: schedules)), ); if (result != null) setState(() => schedules = result); } void _save() { context.read().add(UpdateProfessionalProfileInfo( address: _addressController.text, aditionalAddress: _aditionalAddressController.text, latitude: _latitudeController, longitude: _longitudeController, locationPreferences: officeValue && deliveryValue ? LocationPreferences.both : officeValue && !deliveryValue ? LocationPreferences.office : deliveryValue && !officeValue ? LocationPreferences.delivery : LocationPreferences.office, paymentMethods: PaymentMethodEntity( datafono: isDatafonoActive, nequi: isNequiActive, transferencia: isTransferActive, ), schedules: schedules, ratePreferences: rateValue, rate: _rateController.text, )); context.read().add( UpdateProfessionalBannerInfo(fileBanner: _imageFile?.path)); ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Información actualizada correctamente')), ); } } class _DayEntry { final String name; final ScheduleEntity? schedule; const _DayEntry(this.name, this.schedule); } class _PayChip extends StatelessWidget { final String label; final IconData icon; final bool active; final VoidCallback onTap; const _PayChip({ required this.label, required this.icon, required this.active, required this.onTap, }); @override Widget build(BuildContext context) { final color = active ? _kPrimary : context.subtle; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: active ? _kPrimary.withOpacity(0.1) : context.isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.03), borderRadius: BorderRadius.circular(24), border: Border.all( color: active ? _kPrimary.withOpacity(0.5) : color.withOpacity(0.2)), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ if (active) const Padding( padding: EdgeInsets.only(right: 5), child: Icon(Icons.check, size: 13, color: _kPrimary), ), Icon(icon, size: 15, color: color), const SizedBox(width: 6), Text(label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: color)), ]), ), ); } }