diff --git a/lib/providers/professional_form_provider.dart b/lib/providers/professional_form_provider.dart index 8c1751a..27b8aa7 100644 --- a/lib/providers/professional_form_provider.dart +++ b/lib/providers/professional_form_provider.dart @@ -72,6 +72,11 @@ class ProfessionalFormProvider with ChangeNotifier { return true; } + Future submitForReview() async { + await _api.patch('/professionals/me', profesional!.toDocument()); + NotificationsService.showSnackbar('Solicitud enviada a revisión'); + } + Future updateProfesionalProfileInfo(String userId) async { if (!_validProfileForm()) return false; await _api.patch('/professionals/me', profesional!.toDocument()); diff --git a/lib/ui/views/request_professional_view.dart b/lib/ui/views/request_professional_view.dart index aa92b45..5def760 100644 --- a/lib/ui/views/request_professional_view.dart +++ b/lib/ui/views/request_professional_view.dart @@ -1,9 +1,8 @@ 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/profession.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'; @@ -11,29 +10,27 @@ 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/notifications_service.dart'; -import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class RequestProfessionalView extends StatefulWidget { const RequestProfessionalView({super.key}); @override - State createState() => _RequestProfessionalViewState(); + State createState() => + _RequestProfessionalViewState(); } class _RequestProfessionalViewState extends State { @override void initState() { super.initState(); - final authProvider = Provider.of(context, listen: false); - final profileFormProvider = Provider.of(context, listen: false); - final professionalFormProvider = Provider.of(context, listen: false); - final proProvider = Provider.of(context, listen: false); - - profileFormProvider.user = authProvider.user; - proProvider.getProfessional(authProvider.user!.id).then((value) { - professionalFormProvider.setProfesional(value); - }); + final auth = context.read(); + context.read().user = auth.user; + context + .read() + .getProfessional(auth.user!.id) + .then((pro) => + context.read().setProfesional(pro)); } @override @@ -43,8 +40,8 @@ class _RequestProfessionalViewState extends State { children: [ Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 680), - child: const _ProfessionalForm(), + constraints: const BoxConstraints(maxWidth: 640), + child: const _FormBody(), ), ), ], @@ -52,562 +49,632 @@ class _RequestProfessionalViewState extends State { } } -class _ProfessionalForm extends StatefulWidget { - const _ProfessionalForm(); +// ───────────────────────────────────────────────────────────────────────────── + +class _FormBody extends StatefulWidget { + const _FormBody(); @override - State<_ProfessionalForm> createState() => _ProfessionalFormState(); + State<_FormBody> createState() => _FormBodyState(); } -class _ProfessionalFormState extends State<_ProfessionalForm> { - final TextEditingController _specialityController = TextEditingController(); - List specializations = []; +class _FormBodyState extends State<_FormBody> { + final _rethusCtrl = TextEditingController(); + final _specCtrl = TextEditingController(); + String? _selectedProfession; + List _specs = []; + bool _loading = false; + bool _cedulaError = false; + bool _profError = false; - void _addSpecialization() { - final item = _specialityController.text.trim(); - if (item.isNotEmpty && !specializations.contains(item)) { + @override + void dispose() { + _rethusCtrl.dispose(); + _specCtrl.dispose(); + super.dispose(); + } + + void _addSpec() { + final v = _specCtrl.text.trim(); + if (v.isNotEmpty && !_specs.contains(v)) { setState(() { - specializations.add(item); - _specialityController.clear(); + _specs.add(v); + _specCtrl.clear(); }); } } + bool _validate(String cedulaUrl) { + final cErr = cedulaUrl.isEmpty; + final pErr = + _selectedProfession == null || _selectedProfession!.isEmpty; + setState(() { + _cedulaError = cErr; + _profError = pErr; + }); + return !cErr && !pErr; + } + + Future _submit() async { + final fp = context.read(); + final cedulaUrl = fp.profesional?.identificationPicture ?? ''; + if (!_validate(cedulaUrl)) return; + + setState(() => _loading = true); + try { + fp.copyProfesionalWith( + 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 authProvider = Provider.of(context); - final professionsProvider = Provider.of(context); - final user = authProvider.user!; + final fp = context.watch(); + final user = context.read().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 border = + isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); final textPrimary = isDark ? Colors.white : const Color(0xFF111827); - final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + final textSec = + isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); - return Consumer( - builder: (context, fp, _) { - if (fp.profesional == null) { - return const Center(child: Padding( - padding: EdgeInsets.all(40), - child: CircularProgressIndicator(), - )); - } + if (fp.profesional == null) { + return const Padding( + padding: EdgeInsets.only(top: 60), + child: Center(child: CircularProgressIndicator()), + ); + } - final pro = fp.profesional!; + // En revisión + if (proStateInt == 1) { + return _StatusCard( + cardBg: cardBg, + border: border, + textPrimary: textPrimary, + textSec: textSec, + isPending: true, + ); + } - switch (enumToInt(user.proState)) { - // ── Estado 0: Formulario ────────────────────────────────────────── - case 0: - return Form( - key: fp.formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + // Rechazado + if (proStateInt == 3) { + return _StatusCard( + cardBg: cardBg, + border: border, + textPrimary: textPrimary, + textSec: textSec, + isPending: false, + ); + } - // 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: Column( - children: const [ - Icon(Icons.work_outline_rounded, color: Colors.white, size: 36), - SizedBox(height: 10), - Text('Solicitud de Profesional', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - )), - SizedBox(height: 4), - Text('Completa tu información para comenzar a ofrecer servicios', - style: TextStyle(color: Colors.white70, fontSize: 12), - textAlign: TextAlign.center), - ], - ), - ), + final cedulaUploaded = + (fp.profesional?.identificationPicture ?? '').isNotEmpty; - const SizedBox(height: 20), - - // ── Sección 1: Identificación ── - _Section( - title: 'Identificación', - icon: Icons.badge_outlined, - cardBg: cardBg, - border: border, - textPrimary: textPrimary, - children: [ - TextFormField( - initialValue: pro.identification, - style: TextStyle(color: textPrimary), - validator: (v) { - if (v == null || v.isEmpty) return 'La cédula es obligatoria'; - if (v.trim().length < 6) return 'Mínimo 6 caracteres'; - return null; - }, - onChanged: (v) => fp.copyProfesionalWith(identification: v), - decoration: _inputDec('Número de cédula', Icons.badge_outlined, isDark), - ), - const SizedBox(height: 12), - _UploadButton( - label: 'PDF de la cédula', - uploaded: pro.identificationPicture.isNotEmpty, - isDark: isDark, - onTap: () async { - final bytes = await _pickPdf(); - if (bytes != null) { - NotificationsService.showBusyIndicator(context); - await fp.uploadPdfIdentification(bytes, user.id); - if (context.mounted) Navigator.pop(context); - } - }, - ), - ], - ), - - const SizedBox(height: 12), - - // ── Sección 2: Certificación ── - _Section( - title: 'Certificación', - icon: Icons.health_and_safety_outlined, - cardBg: cardBg, - border: border, - textPrimary: textPrimary, - children: [ - TextFormField( - initialValue: pro.rethusCode, - style: TextStyle(color: textPrimary), - onChanged: (v) => fp.copyProfesionalWith(rethusCode: v), - decoration: _inputDec('Código RETHUS (opcional)', Icons.health_and_safety_outlined, isDark), - ), - const SizedBox(height: 6), - Text('El código RETHUS es el registro del talento humano en salud de Colombia.', - style: TextStyle(fontSize: 11, color: textSecondary)), - const SizedBox(height: 12), - _UploadButton( - label: 'PDF del certificado profesional', - uploaded: pro.certificatePicture.isNotEmpty, - isDark: isDark, - onTap: () async { - final bytes = await _pickPdf(); - if (bytes != null) { - NotificationsService.showBusyIndicator(context); - await fp.uploadPdfCertificate(bytes, user.id); - if (context.mounted) Navigator.pop(context); - } - }, - ), - ], - ), - - const SizedBox(height: 12), - - // ── Sección 3: Profesión ── - _Section( - title: 'Profesión', - icon: Icons.work_outline_outlined, - cardBg: cardBg, - border: border, - textPrimary: textPrimary, - children: [ - DropdownButtonFormField( - value: pro.profession.isEmpty ? null : pro.profession, - dropdownColor: cardBg, - style: TextStyle(color: textPrimary), - validator: (v) => v == null ? 'La profesión es obligatoria' : null, - decoration: _inputDec('Selecciona tu profesión', Icons.work_outline_outlined, isDark), - items: professionsProvider.professions - .map((p) => DropdownMenuItem( - value: p.name, - child: Text(p.name, - style: TextStyle(color: textPrimary)), - )) - .toList(), - onChanged: (v) => fp.copyProfesionalWith(profession: v), - ), - ], - ), - - const SizedBox(height: 12), - - // ── Sección 4: Especializaciones ── - _Section( - title: 'Especializaciones', - icon: Icons.assignment_outlined, - cardBg: cardBg, - border: border, - textPrimary: textPrimary, - children: [ - Row( - children: [ - Expanded( - child: TextFormField( - controller: _specialityController, - style: TextStyle(color: textPrimary), - onFieldSubmitted: (_) => _addSpecialization(), - decoration: _inputDec( - 'Agregar especialización', - Icons.assignment_outlined, - isDark, - ), - ), - ), - const SizedBox(width: 8), - Material( - color: const Color(0xFF42A4EF), - borderRadius: BorderRadius.circular(10), - child: InkWell( - onTap: _addSpecialization, - borderRadius: BorderRadius.circular(10), - child: const Padding( - padding: EdgeInsets.all(14), - child: Icon(Icons.add, color: Colors.white, size: 20), - ), - ), - ), - ], - ), - if (specializations.isNotEmpty) ...[ - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 6, - children: specializations - .map((item) => Chip( - label: Text(item, - style: const TextStyle( - color: Color(0xFF42A4EF), fontSize: 12)), - backgroundColor: - const Color(0xFF42A4EF).withOpacity(0.1), - deleteIconColor: const Color(0xFF42A4EF), - side: const BorderSide( - color: Color(0xFF42A4EF), width: 0.5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - onDeleted: () => setState( - () => specializations.remove(item)), - )) - .toList(), - ), - ], - const SizedBox(height: 12), - _UploadButton( - label: 'PDFs de especializaciones (múltiples)', - uploaded: pro.specializationsPictures.isNotEmpty, - isDark: isDark, - onTap: () async { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowMultiple: true, - allowedExtensions: ['pdf'], - withData: true, - ); - if (result != null) { - final filesBytes = result.files - .where((f) => f.bytes != null) - .map((f) => f.bytes!) - .toList(); - if (filesBytes.isNotEmpty && context.mounted) { - NotificationsService.showBusyIndicator(context); - await fp.uploadPdfSpecializations( - filesBytes, user.id); - if (context.mounted) Navigator.pop(context); - } - } - }, - ), - ], - ), - - const SizedBox(height: 24), - - // ── Botón enviar ── - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.send_rounded, size: 18), - label: const Text('Enviar a revisión', - style: TextStyle( - fontSize: 15, fontWeight: FontWeight.w600)), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF42A4EF), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - elevation: 0, - ), - onPressed: () async { - fp.copyProfesionalWith(specializations: specializations); - final res = await fp.updateProfesionalInfo(user.id); - if (res && context.mounted) { - Provider.of(context, listen: false) - .refreshUser(); - final pfp = Provider.of(context, - listen: false); - pfp.copyUserWith(proState: ProState.pending); - pfp.updateUserInfoNoValid(); - } - }, - ), - ), - - const SizedBox(height: 20), - ], - ), - ); - - // ── Estado 1: En revisión ───────────────────────────────────────── - case 1: - return Container( - margin: const EdgeInsets.only(top: 20), - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: cardBg, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: border), - ), - child: Column( - children: [ - const Image(image: AssetImage('assets/checklist.gif'), width: 200), - const SizedBox(height: 20), - Text('Solicitud en revisión', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: textPrimary, - )), - const SizedBox(height: 12), - Text( - 'Gracias por enviar tu información. Estamos revisando tus datos y te notificaremos cuando tu cuenta esté aprobada.', - textAlign: TextAlign.center, - style: TextStyle(color: textSecondary, fontSize: 14, height: 1.5), - ), - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xFFF59E0B).withOpacity(0.12), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFF59E0B).withOpacity(0.4)), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.hourglass_empty_rounded, - color: Color(0xFFF59E0B), size: 18), - SizedBox(width: 8), - Text('Revisión en proceso', - style: TextStyle( - color: Color(0xFFF59E0B), - fontWeight: FontWeight.w600, - fontSize: 13, - )), - ], - ), - ), - ], - ), - ); - - // ── Estado 3: Rechazado ─────────────────────────────────────────── - case 3: - return Container( - margin: const EdgeInsets.only(top: 20), - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: cardBg, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: border), - ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFEF4444).withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon(Icons.sentiment_dissatisfied_outlined, - size: 56, color: Color(0xFFEF4444)), - ), - const SizedBox(height: 20), - Text('Solicitud no aprobada', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: textPrimary, - )), - const SizedBox(height: 12), - Text( - 'Tu solicitud no fue aprobada en esta ocasión. Por favor revisa tus documentos y vuelve a intentarlo.', - textAlign: TextAlign.center, - style: TextStyle(color: textSecondary, fontSize: 14, height: 1.5), - ), - ], - ), - ); - - default: - return const SizedBox(); - } - }, - ); - } - - Future _pickPdf() async { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - withData: true, - ); - return result?.files.first.bytes; - } - - InputDecoration _inputDec(String label, IconData icon, bool isDark) { - final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFD1D5DB); - final labelColor = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); - return InputDecoration( - labelText: label, - labelStyle: TextStyle(color: labelColor, fontSize: 13), - hintStyle: TextStyle(color: labelColor), - prefixIcon: Icon(icon, color: const Color(0xFF42A4EF), size: 20), - filled: true, - fillColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: borderColor), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: borderColor), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), - ); - } -} - -// ── Helpers ────────────────────────────────────────────────────────────────── - -class _Section extends StatelessWidget { - final String title; - final IconData icon; - final Color cardBg; - final Color border; - final Color textPrimary; - final List children; - - const _Section({ - required this.title, - required this.icon, - required this.cardBg, - required this.border, - required this.textPrimary, - required this.children, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: cardBg, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + 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(icon, color: const Color(0xFF42A4EF), size: 18), - const SizedBox(width: 8), - Text(title, + Icon(Icons.verified_user_outlined, + color: Colors.white, size: 34), + SizedBox(height: 8), + Text('Solicitud de Profesional', style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: textPrimary, - )), + 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: 14), - ...children, - ], - ), + ), + + const SizedBox(height: 16), + + // ── 1. Foto / PDF de cédula (obligatorio) ────────────────────────── + _SectionCard( + cardBg: cardBg, + border: border, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Label('Foto / PDF de cédula', Icons.badge_outlined, + required: true), + const SizedBox(height: 12), + _UploadTile( + label: cedulaUploaded + ? '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. 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), ); } } -class _UploadButton extends StatelessWidget { - final String label; - final bool uploaded; - final bool isDark; - final VoidCallback onTap; +// ───────────────────────────────────────────────────────────────────────────── +// Widgets de apoyo +// ───────────────────────────────────────────────────────────────────────────── - const _UploadButton({ - required this.label, - required this.uploaded, - required this.isDark, - required this.onTap, - }); +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 color = uploaded ? const Color(0xFF10B981) : const Color(0xFF42A4EF); - final bg = color.withOpacity(0.08); + 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: 12), + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 13), decoration: BoxDecoration( - color: bg, + color: color.withOpacity(0.07), borderRadius: BorderRadius.circular(10), - border: Border.all( - color: color.withOpacity(0.4), - style: BorderStyle.solid, - ), + 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, - ), + uploaded + ? Icons.check_circle_outline + : Icons.upload_file_outlined, + color: color, + size: 20), const SizedBox(width: 10), Expanded( - child: Text( - uploaded ? '$label (subido ✓)' : label, - style: TextStyle( - fontSize: 13, - color: color, - fontWeight: FontWeight.w500, - ), - ), + child: Text(label, + style: TextStyle( + color: color, + fontSize: 13, + fontWeight: FontWeight.w500)), ), - Icon(Icons.chevron_right, color: color.withOpacity(0.6), size: 18), + Icon( + uploaded + ? Icons.edit_outlined + : Icons.chevron_right, + color: color.withOpacity(0.6), + size: 18), ], ), ), ); } } + +class _StatusCard extends StatelessWidget { + 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 + Widget build(BuildContext context) { + final accent = isPending + ? const Color(0xFFF59E0B) + : const Color(0xFFEF4444); + + return Container( + margin: const EdgeInsets.only(top: 24), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: border), + ), + child: Column( + children: [ + if (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( + isPending + ? Icons.hourglass_empty_rounded + : Icons.sentiment_dissatisfied_outlined, + size: 52, + color: accent), + ), + const SizedBox(height: 18), + Text( + isPending + ? 'Solicitud en revisión' + : 'Solicitud no aprobada', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.bold, + color: textPrimary), + ), + const SizedBox(height: 10), + Text( + isPending + ? 'Estamos revisando tus datos. Te notificaremos cuando tu cuenta esté aprobada.' + : 'Tu solicitud no fue aprobada. Revisa tus documentos y vuelve a intentarlo.', + textAlign: TextAlign.center, + style: + TextStyle(color: textSec, fontSize: 13, height: 1.5), + ), + const SizedBox(height: 16), + 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( + isPending + ? Icons.hourglass_empty_rounded + : Icons.cancel_outlined, + color: accent, + size: 16, + ), + const SizedBox(width: 6), + Text( + isPending ? 'Revisión en proceso' : 'No aprobado', + style: TextStyle( + color: accent, + fontWeight: FontWeight.w600, + fontSize: 13), + ), + ], + ), + ), + ], + ), + ); + } +}