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'; // ── Constants ────────────────────────────────────────────────────────────── const _kPrimary = Color(0xFF1565C0); const _kAccent = Color(0xFF42A4EF); const _kBg = Color(0xFFF4F6FA); const _kCard = Colors.white; // ── Entry ────────────────────────────────────────────────────────────────── 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: _kBg, child: ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), children: const [_ProfileHeader(), _ProfileForm()], ), ); } } // ── Profile header card ─────────────────────────────────────────────────── 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: _kCard, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 16, offset: const Offset(0, 4), ) ], ), child: Column( children: [ // Banner gradient Container( height: 110, decoration: const BoxDecoration( gradient: LinearGradient( colors: [_kAccent, _kPrimary], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), ), // Avatar overlapping the banner 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: _kCard, 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) return; if (!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: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), ), ), if (profession != null && profession.isNotEmpty) ...[ const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 3), decoration: BoxDecoration( color: _kAccent.withOpacity(0.1), 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: _kCard, 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: [ // ── Modalidad ────────────────────────────────────────── _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, ), const _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; } }), ), ], ), ), // ── Dirección ───────────────────────────────────────── 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), ), ], ), ), // ── Tarifa ──────────────────────────────────────────── 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) ...[ const _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), ), ], ], ), ), // ── Métodos de pago ─────────────────────────────────── _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, ), ), ), ], ), ), // ── Horarios ────────────────────────────────────────── _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), ), ), // ── Botón guardar ───────────────────────────────────── 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) { 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) const _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 ? const Color(0xFF1E293B) : Colors.grey.shade400, ), ), ), Container( width: 7, height: 7, decoration: BoxDecoration( shape: BoxShape.circle, color: active ? Colors.green.shade400 : Colors.grey.shade300, ), ), const SizedBox(width: 10), Expanded( child: Text( time, style: TextStyle( fontSize: 12, color: active ? const Color(0xFF475569) : Colors.grey.shade400, ), ), ), ], ), ), ], ); }).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: _kCard, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), 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: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), ), ), ), 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) { final enabled = onChanged != null; return Opacity( opacity: enabled ? 1.0 : 0.45, child: Row( children: [ Icon(icon, size: 20, color: value ? _kPrimary : Colors.grey.shade400), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), Text( subtitle, style: const TextStyle( fontSize: 11, color: Color(0xFF94A3B8), ), ), ], ), ), 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) : Colors.grey.shade50, border: Border.all( color: selected ? _kPrimary : Colors.grey.shade200, width: 1.5, ), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18, color: selected ? _kPrimary : Colors.grey.shade400), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: selected ? _kPrimary : Colors.grey.shade500, ), ), 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: const TextStyle(fontSize: 13), decoration: InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon, size: 18, color: Colors.grey.shade500), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), labelStyle: const TextStyle( fontSize: 13, color: Color(0xFF64748B), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade200), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade200), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kPrimary, width: 1.5), ), filled: true, fillColor: const Color(0xFFF8FAFC), ), ); } } class _Divider extends StatelessWidget { const _Divider(); @override Widget build(BuildContext context) => Divider(height: 16, color: Colors.grey.shade100); }