import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:prosapp_web_app/models/pro_state.dart'; import 'package:prosapp_web_app/models/profesional.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/professions_provider.dart'; import 'package:prosapp_web_app/providers/profile_form_provider.dart'; import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:provider/provider.dart'; class RequestProfessionalView extends StatefulWidget { const RequestProfessionalView({super.key}); @override State createState() => _RequestProfessionalViewState(); } class _RequestProfessionalViewState extends State { @override void initState() { super.initState(); final auth = context.read(); context.read().user = auth.user; context .read() .getProfessional(auth.user!.id) .then((pro) => context.read().setProfesional(pro)) .catchError((_) { context.read().setProfesional(Profesional.empty()); }); } @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), children: [ Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 640), child: const _FormBody(), ), ), ], ); } } // ───────────────────────────────────────────────────────────────────────────── class _FormBody extends StatefulWidget { const _FormBody(); @override State<_FormBody> createState() => _FormBodyState(); } class _FormBodyState extends State<_FormBody> { final _rethusCtrl = TextEditingController(); final _specCtrl = TextEditingController(); final _cedulaNumCtrl = TextEditingController(); String? _selectedProfession; List _specs = []; bool _loading = false; bool _cedulaError = false; bool _cedulaNumError = false; bool _certError = false; bool _profError = false; @override void dispose() { _rethusCtrl.dispose(); _specCtrl.dispose(); _cedulaNumCtrl.dispose(); super.dispose(); } void _addSpec() { final v = _specCtrl.text.trim(); if (v.isNotEmpty && !_specs.contains(v)) { setState(() { _specs.add(v); _specCtrl.clear(); }); } } bool _validate(String cedulaUrl, String certUrl) { final cErr = cedulaUrl.isEmpty; final nErr = _cedulaNumCtrl.text.trim().isEmpty; final dErr = certUrl.isEmpty; final pErr = _selectedProfession == null || _selectedProfession!.isEmpty; setState(() { _cedulaError = cErr; _cedulaNumError = nErr; _certError = dErr; _profError = pErr; }); return !cErr && !nErr && !dErr && !pErr; } Future _submit() async { final fp = context.read(); final cedulaUrl = fp.profesional?.identificationPicture ?? ''; final certUrl = fp.profesional?.certificatePicture ?? ''; if (!_validate(cedulaUrl, certUrl)) return; setState(() => _loading = true); try { fp.copyProfesionalWith( identification: _cedulaNumCtrl.text.trim(), profession: _selectedProfession, rethusCode: _rethusCtrl.text.trim(), specializations: _specs, ); await fp.submitForReview(); if (!mounted) return; final pfp = context.read(); pfp.copyUserWith(proState: ProState.pending); await pfp.updateUserInfoNoValid(); await context.read().isAuthenticated(); } catch (e) { if (mounted) { NotificationsService.showSnackbar('Error al enviar: ${e.toString()}'); } } finally { if (mounted) setState(() => _loading = false); } } Future _pickAndUpload( Future Function(Uint8List) upload, ) async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'], withData: true, ); if (result == null || result.files.first.bytes == null) return; if (!mounted) return; NotificationsService.showBusyIndicator(context); await upload(result.files.first.bytes!); if (mounted) { Navigator.pop(context); setState(() => _cedulaError = false); } } @override Widget build(BuildContext context) { final isDark = context.watch().isDark; final fp = context.watch(); final user = context.watch().user!; final professions = context.watch().professions; final proStateInt = enumToInt(user.proState); final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); final textPrimary = isDark ? Colors.white : const Color(0xFF111827); final textSec = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); if (fp.profesional == null) { return const Padding( padding: EdgeInsets.only(top: 60), child: Center(child: CircularProgressIndicator()), ); } // En revisión if (proStateInt == 1) { return _StatusCard( cardBg: cardBg, border: border, textPrimary: textPrimary, textSec: textSec, isPending: true, ); } // Rechazado if (proStateInt == 3) { return _StatusCard( cardBg: cardBg, border: border, textPrimary: textPrimary, textSec: textSec, isPending: false, ); } final cedulaUploaded = (fp.profesional?.identificationPicture ?? '').isNotEmpty; final certUploaded = (fp.profesional?.certificatePicture ?? '').isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Header ────────────────────────────────────────────────────────── Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF42A4EF), Color(0xFF1565C0)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), ), child: const Column( children: [ Icon(Icons.verified_user_outlined, color: Colors.white, size: 34), SizedBox(height: 8), Text('Solicitud de Profesional', style: TextStyle( color: Colors.white, fontSize: 19, fontWeight: FontWeight.bold)), SizedBox(height: 4), Text('Completa tu información para ofrecer servicios', style: TextStyle(color: Colors.white70, fontSize: 12), textAlign: TextAlign.center), ], ), ), const SizedBox(height: 16), // ── 1. Cédula (número + foto) ─────────────────────────────────────── _SectionCard( cardBg: cardBg, border: border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label('Cédula de ciudadanía', Icons.badge_outlined, required: true), const SizedBox(height: 4), // Tipo fijo Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isDark ? const Color(0xFF334155) : const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: border), ), child: Row( children: [ Icon(Icons.lock_outline, size: 14, color: textSec), const SizedBox(width: 6), Text('Tipo: Cédula de ciudadanía', style: TextStyle(fontSize: 13, color: textSec)), ], ), ), const SizedBox(height: 10), // Número TextField( controller: _cedulaNumCtrl, keyboardType: TextInputType.number, style: TextStyle(color: textPrimary, fontSize: 14), decoration: InputDecoration( hintText: 'Número de cédula', hintStyle: TextStyle(color: textSec, fontSize: 13), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), errorText: _cedulaNumError ? 'Ingresa tu número de cédula' : null, ), onChanged: (_) { if (_cedulaNumError) setState(() => _cedulaNumError = false); }, ), const SizedBox(height: 10), // Foto _UploadTile( label: cedulaUploaded ? 'Foto de cédula subida ✓' : 'Subir foto o PDF de la cédula', uploaded: cedulaUploaded, onTap: () => _pickAndUpload((b) => fp.uploadPdfIdentification(b, user.id)), ), if (_cedulaError) const Padding( padding: EdgeInsets.only(top: 8), child: Text('Debes subir la foto de tu cédula', style: TextStyle(color: Colors.redAccent, fontSize: 12)), ), ], ), ), const SizedBox(height: 10), // ── 2. Diploma / Certificado (obligatorio) ───────────────────────── _SectionCard( cardBg: cardBg, border: border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label('Diploma o certificado profesional', Icons.school_outlined, required: true), const SizedBox(height: 4), Text( 'Sube el diploma, acta de grado o tarjeta profesional', style: TextStyle(fontSize: 11, color: textSec), ), const SizedBox(height: 10), _UploadTile( label: certUploaded ? 'Documento subido ✓' : 'Subir diploma / certificado (PDF o imagen)', uploaded: certUploaded, onTap: () => _pickAndUpload((b) => fp.uploadPdfCertificate(b, user.id)), ), if (_certError) const Padding( padding: EdgeInsets.only(top: 8), child: Text('Debes subir el diploma o certificado', style: TextStyle(color: Colors.redAccent, fontSize: 12)), ), ], ), ), const SizedBox(height: 10), // ── 4. Profesión (obligatorio) ────────────────────────────────────── _SectionCard( cardBg: cardBg, border: border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label('Profesión', Icons.work_outline, required: true), const SizedBox(height: 12), DropdownButtonFormField( value: _selectedProfession, dropdownColor: cardBg, style: TextStyle(color: textPrimary, fontSize: 14), hint: Text('Selecciona tu profesión', style: TextStyle(color: textSec, fontSize: 13)), decoration: _dec(isDark, border), items: professions .map((p) => DropdownMenuItem( value: p.name, child: Text(p.name, style: TextStyle(color: textPrimary)), )) .toList(), onChanged: (v) => setState(() { _selectedProfession = v; _profError = false; }), ), if (_profError) const Padding( padding: EdgeInsets.only(top: 8), child: Text('Selecciona una profesión', style: TextStyle( color: Colors.redAccent, fontSize: 12)), ), ], ), ), const SizedBox(height: 10), // ── 3. RETHUS (opcional) ──────────────────────────────────────────── _SectionCard( cardBg: cardBg, border: border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label('Código RETHUS', Icons.health_and_safety_outlined, required: false), const SizedBox(height: 4), Text( 'Registro del talento humano en salud de Colombia', style: TextStyle(fontSize: 11, color: textSec)), const SizedBox(height: 12), TextField( controller: _rethusCtrl, style: TextStyle(color: textPrimary, fontSize: 14), decoration: _dec(isDark, border) .copyWith(hintText: 'Ej: 12345678'), ), ], ), ), const SizedBox(height: 10), // ── 4. Especializaciones (opcional) ──────────────────────────────── _SectionCard( cardBg: cardBg, border: border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Label('Especializaciones', Icons.assignment_outlined, required: false), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _specCtrl, style: TextStyle(color: textPrimary, fontSize: 14), onSubmitted: (_) => _addSpec(), decoration: _dec(isDark, border).copyWith( hintText: 'Ej: Fisioterapia deportiva'), ), ), const SizedBox(width: 8), Material( color: const Color(0xFF42A4EF), borderRadius: BorderRadius.circular(10), child: InkWell( onTap: _addSpec, borderRadius: BorderRadius.circular(10), child: const Padding( padding: EdgeInsets.all(13), child: Icon(Icons.add, color: Colors.white, size: 20), ), ), ), ], ), if (_specs.isNotEmpty) ...[ const SizedBox(height: 10), Wrap( spacing: 6, runSpacing: 6, children: _specs .map((s) => Chip( label: Text(s, style: const TextStyle( color: Color(0xFF42A4EF), fontSize: 12)), backgroundColor: const Color(0xFF42A4EF) .withOpacity(0.1), side: const BorderSide( color: Color(0xFF42A4EF), width: 0.6), deleteIconColor: const Color(0xFF42A4EF), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), onDeleted: () => setState(() => _specs.remove(s)), )) .toList(), ), ], ], ), ), const SizedBox(height: 20), // ── Botón enviar ──────────────────────────────────────────────────── SizedBox( height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, disabledBackgroundColor: const Color(0xFF42A4EF).withOpacity(0.6), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), elevation: 0, ), onPressed: _loading ? null : _submit, child: _loading ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : const Text('Enviar a revisión', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 24), ], ); } InputDecoration _dec(bool isDark, Color border) { final fill = isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB); final hint = isDark ? const Color(0xFF64748B) : const Color(0xFF9CA3AF); return InputDecoration( hintStyle: TextStyle(color: hint, fontSize: 13), filled: true, fillColor: fill, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), ); } } // ───────────────────────────────────────────────────────────────────────────── // Widgets de apoyo // ───────────────────────────────────────────────────────────────────────────── class _SectionCard extends StatelessWidget { final Color cardBg; final Color border; final Widget child; const _SectionCard( {required this.cardBg, required this.border, required this.child}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(14), border: Border.all(color: border), ), child: child, ); } class _Label extends StatelessWidget { final String text; final IconData icon; final bool required; const _Label(this.text, this.icon, {required this.required}); @override Widget build(BuildContext context) { final isDark = context.watch().isDark; final textPrimary = isDark ? Colors.white : const Color(0xFF111827); return Row( children: [ Icon(icon, size: 16, color: const Color(0xFF42A4EF)), const SizedBox(width: 6), Text(text, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: textPrimary)), if (required) ...[ const SizedBox(width: 3), const Text('*', style: TextStyle( color: Colors.redAccent, fontSize: 15, fontWeight: FontWeight.bold)), ] else Padding( padding: const EdgeInsets.only(left: 6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: const Color(0xFF64748B).withOpacity(0.12), borderRadius: BorderRadius.circular(4), ), child: const Text('opcional', style: TextStyle( fontSize: 10, color: Color(0xFF64748B))), ), ), ], ); } } class _UploadTile extends StatelessWidget { final String label; final bool uploaded; final VoidCallback onTap; const _UploadTile( {required this.label, required this.uploaded, required this.onTap}); @override Widget build(BuildContext context) { final color = uploaded ? const Color(0xFF10B981) : const Color(0xFF42A4EF); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), decoration: BoxDecoration( color: color.withOpacity(0.07), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.35)), ), child: Row( children: [ Icon( uploaded ? Icons.check_circle_outline : Icons.upload_file_outlined, color: color, size: 20), const SizedBox(width: 10), Expanded( child: Text(label, style: TextStyle( color: color, fontSize: 13, fontWeight: FontWeight.w500)), ), Icon( uploaded ? Icons.edit_outlined : Icons.chevron_right, color: color.withOpacity(0.6), size: 18), ], ), ), ); } } class _StatusCard extends StatefulWidget { final Color cardBg; final Color border; final Color textPrimary; final Color textSec; final bool isPending; const _StatusCard({ required this.cardBg, required this.border, required this.textPrimary, required this.textSec, required this.isPending, }); @override State<_StatusCard> createState() => _StatusCardState(); } class _StatusCardState extends State<_StatusCard> { int _waitDays = 7; bool _loadingSettings = false; @override void initState() { super.initState(); if (!widget.isPending) _loadWaitDays(); } Future _loadWaitDays() async { try { final data = await ApiService.instance.get('/settings') as Map; final v = data['rejection_wait_days']; if (v != null) setState(() => _waitDays = (v as num).toInt()); } catch (_) {} if (mounted) setState(() => _loadingSettings = false); } Future _retry() async { try { await ApiService.instance.delete('/professionals/me'); } catch (e) { if (mounted) NotificationsService.showSnackbar('Error al reiniciar solicitud: $e'); return; } if (!mounted) return; await context.read().isAuthenticated(); if (!mounted) return; // Reload professional data so the form shows fresh state final auth = context.read(); context.read() .getProfessional(auth.user!.id) .then((pro) => context.read().setProfesional(pro)) .catchError((_) => context.read().setProfesional(Profesional.empty())); } @override Widget build(BuildContext context) { final accent = widget.isPending ? const Color(0xFFF59E0B) : const Color(0xFFEF4444); final user = context.watch().user; final rejectedAt = user?.proRejectedAt; final daysSince = rejectedAt != null ? DateTime.now().difference(rejectedAt).inDays : _waitDays; final daysLeft = (_waitDays - daysSince).clamp(0, _waitDays); final canRetry = !widget.isPending && daysLeft == 0; return Container( margin: const EdgeInsets.only(top: 24), padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: widget.cardBg, borderRadius: BorderRadius.circular(16), border: Border.all(color: widget.border), ), child: Column( children: [ if (widget.isPending) const Image(image: AssetImage('assets/checklist.gif'), width: 160) else Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: accent.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(Icons.sentiment_dissatisfied_outlined, size: 52, color: accent), ), const SizedBox(height: 18), Text( widget.isPending ? 'Solicitud en revisión' : 'Solicitud no aprobada', style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold, color: widget.textPrimary), ), const SizedBox(height: 10), Text( widget.isPending ? 'Estamos revisando tus datos. Te notificaremos cuando tu cuenta esté aprobada.' : canRetry ? 'Puedes volver a enviar tu solicitud con los documentos corregidos.' : 'Tu solicitud no fue aprobada. Podrás reintentar en $daysLeft día${daysLeft == 1 ? '' : 's'}.', textAlign: TextAlign.center, style: TextStyle(color: widget.textSec, fontSize: 13, height: 1.5), ), const SizedBox(height: 16), if (!widget.isPending && !_loadingSettings) ...[ if (canRetry) FilledButton.icon( onPressed: _retry, icon: const Icon(Icons.refresh, size: 18), label: const Text('Volver a solicitar'), style: FilledButton.styleFrom(backgroundColor: const Color(0xFF42A4EF)), ) else Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: accent.withOpacity(0.12), borderRadius: BorderRadius.circular(10), border: Border.all(color: accent.withOpacity(0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.timer_outlined, color: accent, size: 16), const SizedBox(width: 6), Text( 'Disponible en $daysLeft día${daysLeft == 1 ? '' : 's'}', style: TextStyle(color: accent, fontWeight: FontWeight.w600, fontSize: 13), ), ], ), ), ] else if (widget.isPending) Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: accent.withOpacity(0.12), borderRadius: BorderRadius.circular(10), border: Border.all(color: accent.withOpacity(0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.hourglass_empty_rounded, color: accent, size: 16), const SizedBox(width: 6), Text('Revisión en proceso', style: TextStyle(color: accent, fontWeight: FontWeight.w600, fontSize: 13)), ], ), ), ], ), ); } }