import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:prosapp_web_app/models/location_preferences.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/usuario.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/professional_form_provider.dart'; import 'package:prosapp_web_app/providers/professional_provider.dart'; import 'package:prosapp_web_app/providers/profile_form_provider.dart'; import 'package:prosapp_web_app/providers/settings_provider.dart'; import 'package:prosapp_web_app/router/router.dart'; import 'package:prosapp_web_app/services/navigation_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; const _kPrimary = Color(0xFF1565C0); const _kAccent = Color(0xFF42A4EF); // Theme-aware color helpers 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); Color get divider => _t.dividerColor; bool get isDark => _t.brightness == Brightness.dark; Color get shadow => isDark ? Colors.transparent : Colors.black.withOpacity(0.07); Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.04); Color get inputFill => isDark ? _t.cardColor : const Color(0xFFF8FAFC); Color get chipBg => isDark ? _t.colorScheme.onSurface.withOpacity(0.06) : Colors.grey.shade50; } class ProfessionalProfileView extends StatefulWidget { const ProfessionalProfileView({super.key}); @override State createState() => _ProfessionalProfileViewState(); } class _ProfessionalProfileViewState extends State { @override void initState() { super.initState(); final auth = Provider.of(context, listen: false); final pfp = Provider.of(context, listen: false); final fp = Provider.of(context, listen: false); pfp.user = auth.user; Provider.of(context, listen: false) .getProfessional(auth.user!.id) .then(fp.setProfesional); } @override Widget build(BuildContext context) { return Container( color: context.bg, child: ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), children: const [_ProfileHeader(), _ProfileForm()], ), ); } } // ── Profile header ──────────────────────────────────────────────────────── class _ProfileHeader extends StatelessWidget { const _ProfileHeader(); @override Widget build(BuildContext context) { final auth = Provider.of(context).user!; final pfp = Provider.of(context); final fp = Provider.of(context); final picture = pfp.user?.picture; final profession = fp.profesional?.profession; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadow, blurRadius: 16, offset: const Offset(0, 4))], ), child: Column( children: [ Container( height: 110, decoration: const BoxDecoration( gradient: LinearGradient( colors: [_kAccent, _kPrimary], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), ), Transform.translate( offset: const Offset(0, -44), child: Column( children: [ Stack( alignment: Alignment.bottomRight, children: [ Container( width: 88, height: 88, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: context.card, width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 8)], ), child: ClipOval( child: picture != null && picture.isNotEmpty ? FadeInImage.assetNetwork( placeholder: 'loader.gif', image: picture, fit: BoxFit.cover) : Container( color: _kAccent.withOpacity(0.15), child: const Icon(Icons.person, size: 44, color: _kAccent), ), ), ), _CameraButton( onTap: () async { final result = await FilePicker.platform.pickFiles(withData: true); if (result == null) return; final bytes = result.files.first.bytes; if (bytes == null || !context.mounted) return; NotificationsService.showBusyIndicator(context); await pfp.uploadPicture(bytes); if (!context.mounted) return; Provider.of(context, listen: false).refreshUser(); Navigator.pop(context); }, ), ], ), const SizedBox(height: 8), Text(auth.name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: context.onSurface)), if (profession != null && profession.isNotEmpty) ...[ const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3), decoration: BoxDecoration( color: _kAccent.withOpacity(0.12), borderRadius: BorderRadius.circular(20), ), child: Text(profession, style: const TextStyle( color: _kPrimary, fontSize: 12, fontWeight: FontWeight.w600)), ), ], const SizedBox(height: 16), ], ), ), ], ), ), ), ); } } class _CameraButton extends StatelessWidget { final VoidCallback onTap; const _CameraButton({required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 28, height: 28, decoration: BoxDecoration( color: _kPrimary, shape: BoxShape.circle, border: Border.all(color: context.card, width: 2), ), child: const Icon(Icons.camera_alt, size: 14, color: Colors.white), ), ); } } // ── Form ────────────────────────────────────────────────────────────────── class _ProfileForm extends StatefulWidget { const _ProfileForm(); @override State<_ProfileForm> createState() => _ProfileFormState(); } class _ProfileFormState extends State<_ProfileForm> { bool _delivery = false; bool _office = false; bool _rateEnabled = false; bool _saving = false; bool _first = true; @override Widget build(BuildContext context) { final pfp = Provider.of(context); final settings = Provider.of(context).settings; return Consumer(builder: (context, fp, _) { if (fp.profesional == null || settings == null) { return const Padding( padding: EdgeInsets.all(40), child: Center(child: CircularProgressIndicator(color: _kPrimary)), ); } final pro = fp.profesional!; final user = pfp.user!; if (_first) { _first = false; _rateEnabled = settings.tarifas ? pro.ratePreferences : false; if (pro.locationPreferences == LocationPreferences.both) { _delivery = true; _office = true; } else if (pro.locationPreferences == LocationPreferences.office) { _office = true; } else { _delivery = true; } } return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Form( key: fp.profileFormKey, autovalidateMode: AutovalidateMode.always, child: Column( children: [ _SectionCard( icon: Icons.tune_outlined, title: 'Modalidad de servicio', child: Column(children: [ _ToggleRow( icon: Icons.delivery_dining_outlined, title: 'Servicio a domicilio', subtitle: 'Vas donde está el cliente', value: _delivery, onChanged: settings.domicilios ? (v) => setState(() { _delivery = v; if (!v) _office = true; }) : null, ), _Divider(), _ToggleRow( icon: Icons.store_mall_directory_outlined, title: 'Servicio en consultorio / sitio', subtitle: 'El cliente viene a tu local', value: _office, onChanged: (v) => setState(() { if (settings.domicilios == true) { _office = v; if (!v) _delivery = true; } else { _office = true; _delivery = false; } }), ), ]), ), if (_office) _SectionCard( icon: Icons.location_on_outlined, title: 'Dirección del consultorio', child: Column(children: [ _Field( initial: pro.address, hint: 'Calle 123 # 45-67', label: 'Dirección principal', icon: Icons.map_outlined, onChanged: (v) => fp.copyProfesionalWith(address: v), ), const SizedBox(height: 12), _Field( initial: pro.aditionalAddress, hint: 'Piso 2, Consultorio 204', label: 'Piso / Apartamento / Conjunto', icon: Icons.apartment_outlined, onChanged: (v) => fp.copyProfesionalWith(aditionalAddress: v), ), ]), ), if (settings.tarifas) _SectionCard( icon: Icons.payments_outlined, title: 'Tarifa del servicio', child: Column(children: [ _ToggleRow( icon: Icons.attach_money, title: 'Mostrar tarifa en el perfil', subtitle: 'Los clientes verán el precio por consulta', value: _rateEnabled, onChanged: (v) => setState(() => _rateEnabled = v), ), if (_rateEnabled) ...[ _Divider(), _Field( initial: pro.rate, hint: '50000', label: 'Tarifa (COP)', icon: Icons.monetization_on_outlined, keyboardType: TextInputType.number, validator: (v) { if (v == null || v.isEmpty) return 'La tarifa es obligatoria'; if (!RegExp(r'^[0-9]+$').hasMatch(v)) return 'Solo números'; return null; }, onChanged: (v) => fp.copyProfesionalWith(rate: v), ), ], ]), ), _SectionCard( icon: Icons.credit_card_outlined, title: 'Métodos de pago', child: Wrap( spacing: 10, runSpacing: 10, children: [ _PayChip( icon: Icons.point_of_sale_outlined, label: 'Datáfono', selected: pro.paymentMethods.datafono, onTap: () => fp.copyProfesionalWith( paymentMethods: pro.paymentMethods.copyWith(datafono: !pro.paymentMethods.datafono)), ), _PayChip( icon: Icons.phone_android_outlined, label: 'Nequi', selected: pro.paymentMethods.nequi, onTap: () => fp.copyProfesionalWith( paymentMethods: pro.paymentMethods.copyWith(nequi: !pro.paymentMethods.nequi)), ), _PayChip( icon: Icons.account_balance_outlined, label: 'Transferencia', selected: pro.paymentMethods.transferencia, onTap: () => fp.copyProfesionalWith( paymentMethods: pro.paymentMethods.copyWith(transferencia: !pro.paymentMethods.transferencia)), ), ], ), ), _SectionCard( icon: Icons.schedule_outlined, title: 'Horarios de atención', trailing: TextButton.icon( onPressed: () => NavigationService.replaceTo(Flurorouter.professionalScheduleRoute), icon: const Icon(Icons.edit_outlined, size: 16, color: _kPrimary), label: const Text('Editar', style: TextStyle(color: _kPrimary, fontSize: 13)), ), child: Column(children: _buildScheduleRows(pro.schedules, context)), ), Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 4), child: SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _saving ? null : () async { setState(() => _saving = true); LocationPreferences lp; if (_delivery && _office) { lp = LocationPreferences.both; } else if (_delivery) { lp = LocationPreferences.delivery; } else { lp = LocationPreferences.office; } fp.copyProfesionalWith(locationPreferences: lp, ratePreferences: _rateEnabled); await fp.updateProfesionalProfileInfo(user.id); if (mounted) setState(() => _saving = false); }, style: ElevatedButton.styleFrom( backgroundColor: _kPrimary, foregroundColor: Colors.white, disabledBackgroundColor: _kPrimary.withOpacity(0.6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 0, ), child: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Guardar cambios', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), ), ), ), const SizedBox(height: 16), ], ), ), ), ); }); } List _buildScheduleRows(dynamic schedules, BuildContext context) { const days = [ ('Lunes', 'monday'), ('Martes', 'tuesday'), ('Miércoles', 'wednesday'), ('Jueves', 'thursday'), ('Viernes', 'friday'), ('Sábado', 'saturday'), ('Domingo', 'sunday'), ]; return days.asMap().entries.map((entry) { final i = entry.key; final (name, field) = entry.value; final ScheduleEntity? sch = switch (field) { 'monday' => schedules.monday, 'tuesday' => schedules.tuesday, 'wednesday' => schedules.wednesday, 'thursday' => schedules.thursday, 'friday' => schedules.friday, 'saturday' => schedules.saturday, _ => schedules.sunday, }; final time = _formatTime(sch); final active = sch?.enabled == true; return Column(children: [ if (i > 0) _Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), child: Row(children: [ SizedBox( width: 90, child: Text(name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: active ? context.onSurface : context.subtle)), ), Container( width: 7, height: 7, decoration: BoxDecoration( shape: BoxShape.circle, color: active ? Colors.green.shade400 : context.subtle, ), ), const SizedBox(width: 10), Expanded( child: Text(time, style: TextStyle( fontSize: 12, color: active ? context.muted : context.subtle)), ), ]), ), ]); }).toList(); } String _formatTime(ScheduleEntity? s) { if (s == null || !s.enabled) return 'No disponible'; if (s.range1Hour1 == null || s.range2Hour2 == null) return 'Sin horario definido'; if (s.continuousDay) { return '${ScheduleEntity.getFormatTime(s.range1Hour1)} – ${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } if (s.range1Hour2 == null || s.range2Hour1 == null) return 'Sin horario definido'; return '${ScheduleEntity.getFormatTime(s.range1Hour1)}–${ScheduleEntity.getFormatTime(s.range1Hour2)} · ${ScheduleEntity.getFormatTime(s.range2Hour1)}–${ScheduleEntity.getFormatTime(s.range2Hour2)}'; } } // ── Reusable widgets ────────────────────────────────────────────────────── class _SectionCard extends StatelessWidget { final IconData icon; final String title; final Widget child; final Widget? trailing; const _SectionCard({required this.icon, required this.title, required this.child, this.trailing}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 14, 12, 0), child: 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!, ]), ), Padding(padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), child: child), ], ), ); } } class _ToggleRow extends StatelessWidget { final IconData icon; final String title; final String subtitle; final bool value; final ValueChanged? onChanged; const _ToggleRow({required this.icon, required this.title, required this.subtitle, required this.value, this.onChanged}); @override Widget build(BuildContext context) { return Opacity( opacity: onChanged != null ? 1.0 : 0.45, child: Row(children: [ Icon(icon, size: 20, color: value ? _kPrimary : context.subtle), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: context.onSurface)), Text(subtitle, style: TextStyle(fontSize: 11, color: context.subtle)), ])), Switch(value: value, onChanged: onChanged, activeColor: _kPrimary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), ]), ); } } class _PayChip extends StatelessWidget { final IconData icon; final String label; final bool selected; final VoidCallback onTap; const _PayChip({required this.icon, required this.label, required this.selected, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: selected ? _kPrimary.withOpacity(0.08) : context.chipBg, border: Border.all( color: selected ? _kPrimary : context.divider, width: 1.5), borderRadius: BorderRadius.circular(10), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18, color: selected ? _kPrimary : context.subtle), const SizedBox(width: 8), Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: selected ? _kPrimary : context.muted)), const SizedBox(width: 6), AnimatedOpacity( opacity: selected ? 1 : 0, duration: const Duration(milliseconds: 180), child: const Icon(Icons.check_circle_rounded, size: 15, color: _kPrimary), ), ]), ), ); } } class _Field extends StatelessWidget { final String? initial; final String hint; final String label; final IconData icon; final TextInputType? keyboardType; final String? Function(String?)? validator; final ValueChanged onChanged; const _Field({required this.initial, required this.hint, required this.label, required this.icon, required this.onChanged, this.keyboardType, this.validator}); @override Widget build(BuildContext context) { return TextFormField( initialValue: initial, keyboardType: keyboardType, validator: validator, onChanged: onChanged, style: TextStyle(fontSize: 13, color: context.onSurface), decoration: InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon, size: 18, color: context.subtle), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), labelStyle: TextStyle(fontSize: 13, color: context.muted), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: context.divider)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: context.divider)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kPrimary, width: 1.5)), filled: true, fillColor: context.inputFill, ), ); } } class _Divider extends StatelessWidget { @override Widget build(BuildContext context) => Divider(height: 16, color: context.divider); }