From 7de1bed254641915bc17428a9a8bdbe7727aaf41 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:40:58 -0500 Subject: [PATCH] feat: UI/UX mejorado en servicios, historial, solicitar profesional y logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - logo.dart: path correcto assets/prosapp-logo.png + errorBuilder - services_view.dart: rediseño con tarjetas, dark mode, avatar con inicial, badges de estado - services_history_view.dart: mismo rediseño + badges completado/cancelado/rechazado - request_professional_view.dart: secciones con cards, botones de upload con estado visual, estados pending/rejected mejorados, dark mode completo Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/shared/widgets/logo.dart | 11 +- lib/ui/views/request_professional_view.dart | 952 +++++++++++--------- lib/ui/views/services_history_view.dart | 317 ++++--- lib/ui/views/services_view.dart | 359 +++++--- 4 files changed, 981 insertions(+), 658 deletions(-) diff --git a/lib/ui/shared/widgets/logo.dart b/lib/ui/shared/widgets/logo.dart index 877afad..7ff94ba 100644 --- a/lib/ui/shared/widgets/logo.dart +++ b/lib/ui/shared/widgets/logo.dart @@ -24,9 +24,18 @@ class Logo extends StatelessWidget { ], ), child: Image.asset( - 'prosapp-logo.png', + 'assets/prosapp-logo.png', height: 38, fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Text( + 'ProsApp', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF42A4EF), + ), + ), ), ), const SizedBox(height: 10), diff --git a/lib/ui/views/request_professional_view.dart b/lib/ui/views/request_professional_view.dart index e784c74..aa92b45 100644 --- a/lib/ui/views/request_professional_view.dart +++ b/lib/ui/views/request_professional_view.dart @@ -9,490 +9,604 @@ 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/notifications_service.dart'; -import 'package:prosapp_web_app/ui/cards/white_card.dart'; -import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; import 'package:flutter/material.dart'; -import 'package:prosapp_web_app/ui/views/no_page_found_view.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 { - Usuario? user; - List professions = []; - late ProfessionalFormProvider professionalFormProvider; - @override void initState() { super.initState(); - final authProvider = Provider.of(context, listen: false); - final profileFormProvider = - Provider.of(context, listen: false); - professionalFormProvider = - Provider.of(context, listen: false); - final professionsProvider = - Provider.of(context, listen: false); - final proProvider = - 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); }); - - setState(() { - professions = professionsProvider.professions; - user = authProvider.user; - }); } @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - if (constraints.maxWidth < 900) { - return ListView( - physics: const ClampingScrollPhysics(), - children: const [SizedBox(height: 10), _ProfileViewForm()], - ); - } else { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: ListView( - physics: const ClampingScrollPhysics(), - children: const [SizedBox(height: 10), _ProfileViewForm()], + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 680), + child: const _ProfessionalForm(), ), - ); - } - }); + ), + ], + ); } } -class _ProfileViewForm extends StatefulWidget { - const _ProfileViewForm(); +class _ProfessionalForm extends StatefulWidget { + const _ProfessionalForm(); @override - State<_ProfileViewForm> createState() => _ProfileViewFormState(); + State<_ProfessionalForm> createState() => _ProfessionalFormState(); } -class _ProfileViewFormState extends State<_ProfileViewForm> { +class _ProfessionalFormState extends State<_ProfessionalForm> { final TextEditingController _specialityController = TextEditingController(); - List specializations = []; - void _addItemToList() { - setState(() { - String newItem = _specialityController.text.trim(); - if (newItem.isNotEmpty) { - specializations.add(newItem); + void _addSpecialization() { + final item = _specialityController.text.trim(); + if (item.isNotEmpty && !specializations.contains(item)) { + setState(() { + specializations.add(item); _specialityController.clear(); - } - }); - } - - void _removeItemFromList(String item) { - setState(() { - specializations.remove(item); - }); + }); + } } @override Widget build(BuildContext context) { + final isDark = context.watch().isDark; final authProvider = Provider.of(context); final professionsProvider = Provider.of(context); - final professions = professionsProvider.professions; final user = authProvider.user!; - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 900), - child: Consumer( - builder: (context, professionalFormProvider, child) { - if (professionalFormProvider.profesional == null) { - return const Center( - child: CircularProgressIndicator(), - ); - } + 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 textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); - final professional = professionalFormProvider.profesional; + return Consumer( + builder: (context, fp, _) { + if (fp.profesional == null) { + return const Center(child: Padding( + padding: EdgeInsets.all(40), + child: CircularProgressIndicator(), + )); + } - switch (enumToInt(user.proState)) { - case 0: - return WhiteCard( - title: 'Información profesional', - child: Form( - key: professionalFormProvider.formKey, - autovalidateMode: AutovalidateMode.always, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, + final pro = fp.profesional!; + + switch (enumToInt(user.proState)) { + // ── Estado 0: Formulario ────────────────────────────────────────── + case 0: + return Form( + key: fp.formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: 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: 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), + ], + ), + ), + + const SizedBox(height: 20), + + // ── Sección 1: Identificación ── + _Section( + title: 'Identificación', + icon: Icons.badge_outlined, + cardBg: cardBg, + border: border, + textPrimary: textPrimary, children: [ - const SizedBox(height: 10), TextFormField( - initialValue: professional!.identification, - validator: (value) { - if (value == null || value.isEmpty) { - return 'La cedula es obligatoria'; - } - if (value.trim().length < 6) { - return 'La cedula debe tener al menos 6 caracteres'; - } + 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: (value) { - professionalFormProvider.copyProfesionalWith( - identification: value); - }, - decoration: CustomInputs.formInputDecoration( - hint: 'Ingresa tu cedula', - label: 'Cedula', - icon: Icons.badge_outlined, - ), + onChanged: (v) => fp.copyProfesionalWith(identification: v), + decoration: _inputDec('Número de cédula', Icons.badge_outlined, isDark), ), - const SizedBox(height: 10), - TextFormField( - initialValue: professional!.rethusCode, - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - rethusCode: value); - }, - decoration: CustomInputs.formInputDecoration( - hint: 'Código RETHUS (opcional)', - label: 'Código RETHUS', - icon: Icons.health_and_safety_outlined, - ), - ), - const SizedBox(height: 10), - ElevatedButton.icon( - onPressed: () async { - try { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - withData: true, - ); - - if (result != null) { - PlatformFile file = result.files.first; - Uint8List? fileBytes = file.bytes; - - if (fileBytes != null) { - NotificationsService.showBusyIndicator(context); - - final provider = - Provider.of( - context, - listen: false); - await provider.uploadPdfIdentification( - fileBytes, user.id); - - Navigator.pop(context); - } - } - } catch (e) { - print('debugeando $e'); + 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); } }, - icon: const Icon(Icons.upload_file), - label: const Text('Cargar pdf de la cedula'), ), - const SizedBox(height: 20), - DropdownButtonFormField( - validator: (value) { - if (value == null) { - return 'La profesión es obligatoria'; - } - return null; - }, - value: professional.profession == '' - ? null - : professional.profession, - decoration: CustomInputs.formInputDecoration( - hint: 'Selecciona tu profesión', - label: 'Profesión', - icon: Icons.work_outline_outlined, - ), - items: professions.map((Profession profession) { - return DropdownMenuItem( - value: profession.name, - child: Text(profession.name), - ); - }).toList(), - onChanged: (value) { - professionalFormProvider.copyProfesionalWith( - profession: value); - }), - const SizedBox(height: 10), - ElevatedButton.icon( - onPressed: () async { - try { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['pdf'], - withData: true, - ); - - if (result != null) { - PlatformFile file = result.files.first; - Uint8List? fileBytes = file.bytes; - - if (fileBytes != null) { - NotificationsService.showBusyIndicator(context); - - final provider = - Provider.of( - context, - listen: false); - await provider.uploadPdfCertificate( - fileBytes, user.id); - - Navigator.pop(context); - } - } - } catch (e) { - print('debugeando $e'); - } - }, - icon: const Icon(Icons.upload_file), - label: const Text('Cargar pdf del certificado'), - ), - const SizedBox(height: 20), - TextFormField( - validator: (value) { - if (RegExp(r'\s{2,}').hasMatch(value!)) { - return 'La especialización no es valida'; - } - return null; - }, - onFieldSubmitted: (_) { - _addItemToList(); - }, - controller: _specialityController, - decoration: CustomInputs.formInputDecoration( - hint: 'Ingresa tus especializaciones y agregalas (+)', - label: 'Especializaciones', - icon: Icons.assignment_outlined, - iconButton: IconButton( - onPressed: () { - _addItemToList(); - }, - icon: const Icon(Icons.add), - ), - ), - ), - const SizedBox(height: 10), - ElevatedButton.icon( - onPressed: () async { - try { - FilePickerResult? result = - await FilePicker.platform.pickFiles( - type: FileType.custom, - allowMultiple: true, - allowedExtensions: ['pdf'], - withData: true, - ); - - if (result != null) { - List filesBytes = result.files - .where((file) => file.bytes != null) - .map((file) => file.bytes!) - .toList(); - - if (filesBytes.isNotEmpty) { - NotificationsService.showBusyIndicator(context); - - final provider = - Provider.of( - context, - listen: false); - await provider.uploadPdfSpecializations( - filesBytes, user.id); - - Navigator.pop(context); - } - } - } catch (e) { - print('debugeando $e'); - } - }, - icon: const Icon(Icons.upload_file), - label: - const Text('Cargar pdfs de las especializaciones'), - ), - const SizedBox(height: 10), - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: specializations - .map((item) => Chip( - label: Text(item), - backgroundColor: Colors.blue.withOpacity(0.3), - labelStyle: - const TextStyle(color: Colors.blue), - deleteIconColor: Colors.blue, - onDeleted: () { - _removeItemFromList(item); - }, - shape: RoundedRectangleBorder( - side: const BorderSide( - color: Colors.blue, width: 0.3), - borderRadius: BorderRadius.circular(8), - ), - )) - .toList(), - ), - const SizedBox(height: 20), - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 180), - child: ElevatedButton( - onPressed: () async { - professionalFormProvider.copyProfesionalWith( - specializations: specializations); - final res = await professionalFormProvider - .updateProfesionalInfo(user.id); - - if (res) { - Provider.of(context, - listen: false) - .refreshUser(); - - final profileFormProvider = - Provider.of(context, - listen: false); - profileFormProvider.copyUserWith( - proState: ProState.pending); - profileFormProvider.updateUserInfoNoValid(); - } - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Colors.blue.shade400, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(5)), - ), - ), - shadowColor: - WidgetStateProperty.all(Colors.transparent), - ), - child: const Text( - 'Enviar a revisión', - style: TextStyle( - color: Colors.white, - fontSize: 15, - ), - ), - ), - ), - ), - const SizedBox(height: 15), ], ), - ), - ); - case 1: - return LayoutBuilder( - builder: (context, constraints) { - double screenWidth = constraints.maxWidth; - double baseFontSize = 18; - double responsiveFontSize = - screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize; - return WhiteCard( - child: Column( - children: [ - const Center( - child: Image( - image: AssetImage('checklist.gif'), - width: 320, - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 1020), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Gracias por proporcionar tu información. Actualmente, estamos revisando tus datos y una vez aprobados, podrás acceder al perfil profesional sin problemas. Te notificaremos tan pronto como tu cuenta esté lista.', - style: TextStyle(fontSize: responsiveFontSize), - textAlign: TextAlign.center, + 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: 20), - Text( - '¡Gracias por tu paciencia!', - style: TextStyle(fontSize: responsiveFontSize), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), + ], + 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, + )), ], ), - ); - }, - ); + ), + ], + ), + ); - case 3: - return LayoutBuilder( - builder: (context, constraints) { - double screenWidth = constraints.maxWidth; - double baseFontSize = 18; - double responsiveFontSize = - screenWidth < 600 ? baseFontSize * 0.8 : baseFontSize; - - return WhiteCard( - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 25), - child: const Center( - child: Icon( - Icons.sentiment_dissatisfied_outlined, - size: 100, - color: Colors.red, - ), - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 1020), - child: Container( - margin: const EdgeInsets.only( - left: 8, right: 8, bottom: 10), - child: Text( - 'Lamentablemente, tu solicitud no ha sido aceptada en esta ocasión. Por favor, revisa tus datos y vuelve a intentarlo más tarde.', - style: TextStyle(fontSize: responsiveFontSize), - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox(height: 20), - Text( - '¡Gracias por tu paciencia!', - style: TextStyle(fontSize: responsiveFontSize), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - ], + // ── 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), + ), + ], + ), + ); - return Center(child: Text('No se encontró la sección.')); - // return const NoPageFoundView(); - }), + 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( + children: [ + Icon(icon, color: const Color(0xFF42A4EF), size: 18), + const SizedBox(width: 8), + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textPrimary, + )), + ], + ), + const SizedBox(height: 14), + ...children, + ], + ), + ); + } +} + +class _UploadButton extends StatelessWidget { + final String label; + final bool uploaded; + final bool isDark; + final VoidCallback onTap; + + const _UploadButton({ + required this.label, + required this.uploaded, + required this.isDark, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = uploaded ? const Color(0xFF10B981) : const Color(0xFF42A4EF); + final bg = color.withOpacity(0.08); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withOpacity(0.4), + style: BorderStyle.solid, + ), + ), + child: Row( + children: [ + Icon( + 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, + ), + ), + ), + Icon(Icons.chevron_right, color: color.withOpacity(0.6), size: 18), + ], + ), ), ); } diff --git a/lib/ui/views/services_history_view.dart b/lib/ui/views/services_history_view.dart index 290dc38..7d82cc1 100644 --- a/lib/ui/views/services_history_view.dart +++ b/lib/ui/views/services_history_view.dart @@ -3,133 +3,68 @@ import 'package:intl/intl.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/service_status.dart'; -import 'package:prosapp_web_app/services/navigation_service.dart'; -import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; -import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart'; - -import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/models/servicio_profesional.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/services_provider.dart'; - -import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/providers/theme_provider.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; class ServicesHistoryView extends StatelessWidget { final String type; - const ServicesHistoryView({super.key, required this.type}); @override Widget build(BuildContext context) { - final servicesProvider = - Provider.of(context, listen: false); + final servicesProvider = Provider.of(context, listen: false); + final userId = Provider.of(context, listen: false).user!.id; - if (type == 'user') { - servicesProvider.getServicesHistoryForUser( - Provider.of(context, listen: false).user!.id); - } - if (type == 'professional') { - servicesProvider.getServicesHistoryForProfessional( - Provider.of(context, listen: false).user!.id); - } + if (type == 'user') servicesProvider.getServicesHistoryForUser(userId); + if (type == 'professional') servicesProvider.getServicesHistoryForProfessional(userId); + + final isDark = context.watch().isDark; + final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: Consumer( - builder: (context, servicesProvider, child) { - if (servicesProvider.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); + builder: (context, sp, _) { + if (sp.isLoading) { + return const Center(child: CircularProgressIndicator()); } - - if (servicesProvider.services.isEmpty) { - return ListView( - children: const [ - WhiteCard( - child: Center(child: Text('No hay servicios disponibles.')), - ), - ], + if (sp.services.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.history_outlined, size: 64, + color: textSecondary.withOpacity(0.5)), + const SizedBox(height: 16), + Text('Sin historial', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : const Color(0xFF111827), + )), + const SizedBox(height: 6), + Text('Los servicios completados o cancelados aparecerán aquí.', + style: TextStyle(fontSize: 13, color: textSecondary)), + ], + ), ); } return ListView.builder( - itemCount: servicesProvider.services.length, - itemBuilder: (context, index) { - final data = servicesProvider.services[index]; - - final image = - (data.user.picture == '' || data.user.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: data.user.picture!, - ); - - return Container( - margin: const EdgeInsets.only(bottom: 10), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - NavigationService.replaceTo( - '/dashboard/$type/service/${data.service.id}'); - }, - child: WhiteCard( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: SizedBox( - width: 80, - height: 80, - child: ClipOval( - child: image, - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - data.user.name, - style: CustomLabels.h2, - ), - if (data.service.description != '') - Text( - '"${data.service.description}"', - style: CustomLabels.h5, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(right: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}', - style: const TextStyle( - color: Colors.black54, fontSize: 16), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10), - child: customStatus(data.service), - ), - ], - ), - ), - ], - ), - ), - ), - ), + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: sp.services.length, + itemBuilder: (context, i) { + final data = sp.services[i]; + return _HistoryCard( + data: data, + isDark: isDark, + onTap: () => NavigationService.replaceTo( + '/dashboard/$type/service/${data.service.id}'), ); }, ); @@ -140,16 +75,162 @@ class ServicesHistoryView extends StatelessWidget { } } -Widget customStatus(Service service) { - if (service.status == ServiceStatus.completed) { - return const StatusItem(text: 'Completado', color: Colors.blueAccent); - } - if (service.status == ServiceStatus.cancelled) { - return const StatusItem(text: 'Cancelado', color: Colors.red); - } - if (service.status == ServiceStatus.denied) { - return const StatusItem(text: 'Rechazado', color: Colors.red); - } +class _HistoryCard extends StatelessWidget { + final ServicioProfesional data; + final bool isDark; + final VoidCallback onTap; - return const SizedBox(); + const _HistoryCard({ + required this.data, + required this.isDark, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + 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 textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + + final hasPic = data.user.picture != null && data.user.picture!.isNotEmpty; + final day = DateTime.tryParse(data.service.day); + final dateStr = day != null + ? DateFormat('dd MMM yyyy', 'es').format(day) + : data.service.day; + final timeStr = ScheduleEntity.getFormatTime(data.service.range1Hour1) ?? ''; + + Widget statusBadge; + if (data.service.status == ServiceStatus.completed) { + statusBadge = _Badge(label: 'Completado', color: const Color(0xFF3B82F6)); + } else if (data.service.status == ServiceStatus.cancelled) { + statusBadge = _Badge(label: 'Cancelado', color: const Color(0xFFEF4444)); + } else if (data.service.status == ServiceStatus.denied) { + statusBadge = _Badge(label: 'Rechazado', color: const Color(0xFFEF4444)); + } else { + statusBadge = const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Material( + color: cardBg, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: border), + ), + child: Row( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF42A4EF).withOpacity(0.12), + ), + child: ClipOval( + child: hasPic + ? FadeInImage.assetNetwork( + placeholder: 'loader.gif', + image: data.user.picture!, + fit: BoxFit.cover, + ) + : Center( + child: Text( + data.user.name.isNotEmpty + ? data.user.name[0].toUpperCase() + : '?', + style: const TextStyle( + color: Color(0xFF42A4EF), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.user.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textPrimary, + )), + if (data.service.address.isNotEmpty) ...[ + const SizedBox(height: 3), + Row(children: [ + Icon(Icons.location_on_outlined, + size: 12, color: textSecondary), + const SizedBox(width: 3), + Expanded( + child: Text(data.service.address, + style: TextStyle( + fontSize: 11, color: textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ), + ]), + ], + const SizedBox(height: 5), + Row(children: [ + Icon(Icons.calendar_today_outlined, + size: 12, color: textSecondary), + const SizedBox(width: 4), + Text('$timeStr · $dateStr', + style: TextStyle( + fontSize: 11, color: textSecondary)), + ]), + ], + ), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + statusBadge, + const SizedBox(height: 8), + Icon(Icons.chevron_right, color: textSecondary, size: 18), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class _Badge extends StatelessWidget { + final String label; + final Color color; + const _Badge({required this.label, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.4)), + ), + child: Text(label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + )), + ); + } } diff --git a/lib/ui/views/services_view.dart b/lib/ui/views/services_view.dart index 3ee0f45..87d0282 100644 --- a/lib/ui/views/services_view.dart +++ b/lib/ui/views/services_view.dart @@ -3,133 +3,54 @@ import 'package:intl/intl.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/service_status.dart'; -import 'package:prosapp_web_app/services/navigation_service.dart'; -import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; -import 'package:prosapp_web_app/ui/shared/widgets/status_item.dart'; - -import 'package:provider/provider.dart'; +import 'package:prosapp_web_app/models/servicio_profesional.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/services_provider.dart'; - -import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/providers/theme_provider.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; class ServicesView extends StatelessWidget { final String type; - const ServicesView({super.key, required this.type}); @override Widget build(BuildContext context) { - final servicesProvider = - Provider.of(context, listen: false); + final servicesProvider = Provider.of(context, listen: false); + final userId = Provider.of(context, listen: false).user!.id; - if (type == 'user') { - servicesProvider.getServicesForUser( - Provider.of(context, listen: false).user!.id); - } - if (type == 'professional') { - servicesProvider.getServicesForProfessional( - Provider.of(context, listen: false).user!.id); - } + if (type == 'user') servicesProvider.getServicesForUser(userId); + if (type == 'professional') servicesProvider.getServicesForProfessional(userId); + + final isDark = context.watch().isDark; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: Consumer( - builder: (context, servicesProvider, child) { - if (servicesProvider.isLoading) { - return const Center( - child: CircularProgressIndicator(), + builder: (context, sp, _) { + if (sp.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (sp.services.isEmpty) { + return _EmptyState( + isDark: isDark, + icon: Icons.room_service_outlined, + title: 'Sin servicios activos', + subtitle: 'Aquí verás tus servicios en curso.', ); } - - if (servicesProvider.services.isEmpty) { - return ListView( - children: const [ - WhiteCard( - child: Center(child: Text('No hay servicios disponibles.')), - ), - ], - ); - } - return ListView.builder( - itemCount: servicesProvider.services.length, - itemBuilder: (context, index) { - final data = servicesProvider.services[index]; - - final image = - (data.user.picture == '' || data.user.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: data.user.picture!, - ); - - return Container( - margin: const EdgeInsets.only(bottom: 10), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - NavigationService.replaceTo( - '/dashboard/$type/service/${data.service.id}'); - }, - child: WhiteCard( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: SizedBox( - width: 80, - height: 80, - child: ClipOval( - child: image, - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - data.user.name, - style: CustomLabels.h2, - ), - if (data.service.description != '') - Text( - '"${data.service.description}"', - style: CustomLabels.h5, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(right: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${ScheduleEntity.getFormatTime(data.service.range1Hour1)} - ${DateFormat('dd MMMM yyyy', 'es').format(DateTime.parse(data.service.day))}', - style: const TextStyle( - color: Colors.black54, fontSize: 16), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10), - child: customStatus(data.service), - ), - ], - ), - ), - ], - ), - ), - ), - ), + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: sp.services.length, + itemBuilder: (context, i) { + final data = sp.services[i]; + return _ServiceCard( + data: data, + isDark: isDark, + onTap: () => NavigationService.replaceTo( + '/dashboard/$type/service/${data.service.id}'), + statusWidget: _activeStatus(data.service, isDark), ); }, ); @@ -138,18 +59,216 @@ class ServicesView extends StatelessWidget { ), ); } + + Widget _activeStatus(Service service, bool isDark) { + switch (service.status) { + case ServiceStatus.pending: + return _StatusBadge(label: 'Pendiente', color: const Color(0xFFF59E0B)); + case ServiceStatus.acepted: + return _StatusBadge(label: 'Aceptado', color: const Color(0xFF10B981)); + case ServiceStatus.active: + return _StatusBadge(label: 'En curso', color: const Color(0xFF3B82F6)); + default: + return const SizedBox(); + } + } } -Widget customStatus(Service service) { - if (service.status == ServiceStatus.pending) { - return const StatusItem(text: 'Pendiente', color: Colors.black54); - } - if (service.status == ServiceStatus.acepted) { - return const StatusItem(text: 'Aceptado', color: Colors.green); - } - if (service.status == ServiceStatus.active) { - return const StatusItem(text: 'Activo', color: Colors.blueAccent); - } +// ── Shared widgets ──────────────────────────────────────────────────────────── - return const SizedBox(); +class _ServiceCard extends StatelessWidget { + final ServicioProfesional data; + final bool isDark; + final VoidCallback onTap; + final Widget statusWidget; + + const _ServiceCard({ + required this.data, + required this.isDark, + required this.onTap, + required this.statusWidget, + }); + + @override + Widget build(BuildContext context) { + 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 textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + + final hasPic = data.user.picture != null && data.user.picture!.isNotEmpty; + final day = DateTime.tryParse(data.service.day); + final dateStr = day != null + ? DateFormat('dd MMM yyyy', 'es').format(day) + : data.service.day; + final timeStr = ScheduleEntity.getFormatTime(data.service.range1Hour1) ?? ''; + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Material( + color: cardBg, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: border), + ), + child: Row( + children: [ + // Avatar + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF42A4EF).withOpacity(0.15), + ), + child: ClipOval( + child: hasPic + ? FadeInImage.assetNetwork( + placeholder: 'loader.gif', + image: data.user.picture!, + fit: BoxFit.cover, + ) + : Center( + child: Text( + data.user.name.isNotEmpty + ? data.user.name[0].toUpperCase() + : '?', + style: const TextStyle( + color: Color(0xFF42A4EF), + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(width: 14), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.user.name, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: textPrimary, + )), + if (data.service.address.isNotEmpty) ...[ + const SizedBox(height: 3), + Row( + children: [ + Icon(Icons.location_on_outlined, + size: 13, color: textSecondary), + const SizedBox(width: 3), + Expanded( + child: Text( + data.service.address, + style: + TextStyle(fontSize: 12, color: textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + const SizedBox(height: 6), + Row( + children: [ + Icon(Icons.calendar_today_outlined, + size: 13, color: textSecondary), + const SizedBox(width: 4), + Text('$timeStr · $dateStr', + style: TextStyle(fontSize: 12, color: textSecondary)), + ], + ), + ], + ), + ), + const SizedBox(width: 10), + // Status + arrow + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + statusWidget, + const SizedBox(height: 8), + Icon(Icons.chevron_right, color: textSecondary, size: 18), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class _StatusBadge extends StatelessWidget { + final String label; + final Color color; + const _StatusBadge({required this.label, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.4)), + ), + child: Text(label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + )), + ); + } +} + +class _EmptyState extends StatelessWidget { + final bool isDark; + final IconData icon; + final String title; + final String subtitle; + + const _EmptyState({ + required this.isDark, + required this.icon, + required this.title, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) { + final textPrimary = isDark ? Colors.white : const Color(0xFF111827); + final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: textSecondary.withOpacity(0.5)), + const SizedBox(height: 16), + Text(title, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: textPrimary)), + const SizedBox(height: 6), + Text(subtitle, + style: TextStyle(fontSize: 13, color: textSecondary)), + ], + ), + ); + } }