From 654a7f402db420d10979926ed2201ee49df6ce61 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 22 Sep 2026 13:41:14 -0400 Subject: [PATCH] feat(patient): implement doctor-only patient deletion, alert theme styling, and test auto-cleanup --- lib/core/theme/app_theme.dart | 34 +++ .../views/appointments_modal_view.dart | 3 + .../presentation/views/dashboard_view.dart | 57 ++++- .../data/services/patient_service.dart | 14 ++ .../presentation/store/patient_store.dart | 31 +++ .../presentation/store/patient_store.g.dart | 29 +++ .../views/patient_modal_view.dart | 209 ++++++++++++++- test/directus_integration_test.dart | 12 + test/features/patient_delete_test.dart | 237 ++++++++++++++++++ test/features/secretary_role_test.dart | 6 +- 10 files changed, 616 insertions(+), 16 deletions(-) create mode 100644 test/features/patient_delete_test.dart diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 9a3d83a..346f41c 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -104,6 +104,40 @@ class AppTheme { thickness: 1, space: 1, ), + dialogTheme: DialogThemeData( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 10, + shadowColor: Colors.black.withValues(alpha: 0.16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: AppColors.border, width: 1), + ), + titleTextStyle: const TextStyle( + fontFamily: fontFamily, + fontSize: 17, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), + contentTextStyle: const TextStyle( + fontFamily: fontFamily, + fontSize: 13, + color: AppColors.textSecondary, + ), + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.textPrimary, + contentTextStyle: const TextStyle( + fontFamily: fontFamily, + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + behavior: SnackBarBehavior.floating, + ), ); } } diff --git a/lib/features/dashboard/presentation/views/appointments_modal_view.dart b/lib/features/dashboard/presentation/views/appointments_modal_view.dart index fd06b09..6eda39c 100644 --- a/lib/features/dashboard/presentation/views/appointments_modal_view.dart +++ b/lib/features/dashboard/presentation/views/appointments_modal_view.dart @@ -181,6 +181,9 @@ class _AppointmentsModalViewState extends State { widget.onStatusChanged!(patientId, 'atendido'); } }, + onDeleted: (_) { + _fetchAppointments(_selectedDate); + }, ), ); } diff --git a/lib/features/dashboard/presentation/views/dashboard_view.dart b/lib/features/dashboard/presentation/views/dashboard_view.dart index bba7807..e2353e5 100644 --- a/lib/features/dashboard/presentation/views/dashboard_view.dart +++ b/lib/features/dashboard/presentation/views/dashboard_view.dart @@ -50,25 +50,64 @@ class _DashboardViewState extends State { context: context, barrierDismissible: false, builder: (dialogContext) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Row( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: AppColors.border), + ), + titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + actionsPadding: const EdgeInsets.fromLTRB(24, 12, 24, 20), + title: Row( children: [ - Icon(Icons.logout_rounded, color: AppColors.error), - SizedBox(width: 10), - Text('Cerrar Sesión', style: TextStyle(fontWeight: FontWeight.bold)), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.error.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: const Icon(Icons.logout_rounded, color: AppColors.error, size: 22), + ), + const SizedBox(width: 12), + const Text('Cerrar Sesión', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), ], ), - content: const Text('¿Está seguro de que desea salir del sistema Hidalyx Med?'), + content: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primaryContainer.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.2)), + ), + child: const Row( + children: [ + Icon(Icons.info_outline_rounded, color: AppColors.primary, size: 20), + SizedBox(width: 10), + Expanded( + child: Text( + '¿Está seguro de que desea salir del sistema Hidalyx Med?', + style: TextStyle(fontSize: 13, color: AppColors.textPrimary, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), - child: const Text('Cancelar'), + child: const Text('Cancelar', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w600)), ), - ElevatedButton( + ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: AppColors.error, foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), + icon: const Icon(Icons.logout_rounded, size: 16), + label: const Text('Salir del Sistema', style: TextStyle(fontWeight: FontWeight.w700)), onPressed: () async { Navigator.of(dialogContext).pop(); await AuthService().logout(); @@ -76,7 +115,6 @@ class _DashboardViewState extends State { context.go('/login'); } }, - child: const Text('Salir'), ), ], ), @@ -276,6 +314,7 @@ class _DashboardViewState extends State { patientId: patient.id, isSecretary: _isSecretary, onSaved: (_) => _store.loadDashboardData(), + onDeleted: (_) => _store.loadDashboardData(), ), ); } diff --git a/lib/features/patient/data/services/patient_service.dart b/lib/features/patient/data/services/patient_service.dart index e6b67f3..d9df30b 100644 --- a/lib/features/patient/data/services/patient_service.dart +++ b/lib/features/patient/data/services/patient_service.dart @@ -61,4 +61,18 @@ class PatientService { throw Exception('Error al actualizar paciente: $e'); } } + + Future deletePatient(int id) async { + try { + final response = await apiClient.dio.delete('/items/pacientes/$id'); + if (response.statusCode != 200 && response.statusCode != 204) { + throw Exception('Error en el servidor al eliminar el paciente'); + } + } on DioException catch (e) { + final serverMessage = e.response?.data?['errors']?[0]?['message'] ?? e.message; + throw Exception('Error al eliminar paciente: $serverMessage'); + } catch (e) { + throw Exception('Error al eliminar paciente: $e'); + } + } } diff --git a/lib/features/patient/presentation/store/patient_store.dart b/lib/features/patient/presentation/store/patient_store.dart index bb39ed7..297d7bb 100644 --- a/lib/features/patient/presentation/store/patient_store.dart +++ b/lib/features/patient/presentation/store/patient_store.dart @@ -23,6 +23,9 @@ abstract class _PatientStoreBase with Store { @observable bool isSaving = false; + @observable + bool isDeleting = false; + @observable String? errorMessage; @@ -185,6 +188,7 @@ abstract class _PatientStoreBase with Store { successMessage = null; isLoading = false; isSaving = false; + isDeleting = false; final now = DateTime.now(); fecReg = DateFormat('dd/MM/yyyy').format(now); @@ -391,4 +395,31 @@ abstract class _PatientStoreBase with Store { return false; } } + + @action + Future deletePatient() async { + if (id == null) { + errorMessage = 'No se puede eliminar un paciente que no ha sido guardado.'; + return false; + } + + isDeleting = true; + errorMessage = null; + successMessage = null; + + try { + await service.deletePatient(id!); + runInAction(() { + isDeleting = false; + successMessage = 'Paciente eliminado exitosamente'; + }); + return true; + } catch (e) { + runInAction(() { + errorMessage = e.toString().replaceAll('Exception: ', ''); + isDeleting = false; + }); + return false; + } + } } diff --git a/lib/features/patient/presentation/store/patient_store.g.dart b/lib/features/patient/presentation/store/patient_store.g.dart index fbcbf64..22338fc 100644 --- a/lib/features/patient/presentation/store/patient_store.g.dart +++ b/lib/features/patient/presentation/store/patient_store.g.dart @@ -83,6 +83,24 @@ mixin _$PatientStore on _PatientStoreBase, Store { }); } + late final _$isDeletingAtom = Atom( + name: '_PatientStoreBase.isDeleting', + context: context, + ); + + @override + bool get isDeleting { + _$isDeletingAtom.reportRead(); + return super.isDeleting; + } + + @override + set isDeleting(bool value) { + _$isDeletingAtom.reportWrite(value, super.isDeleting, () { + super.isDeleting = value; + }); + } + late final _$errorMessageAtom = Atom( name: '_PatientStoreBase.errorMessage', context: context, @@ -769,6 +787,16 @@ mixin _$PatientStore on _PatientStoreBase, Store { return _$savePatientAsyncAction.run(() => super.savePatient()); } + late final _$deletePatientAsyncAction = AsyncAction( + '_PatientStoreBase.deletePatient', + context: context, + ); + + @override + Future deletePatient() { + return _$deletePatientAsyncAction.run(() => super.deletePatient()); + } + late final _$_PatientStoreBaseActionController = ActionController( name: '_PatientStoreBase', context: context, @@ -828,6 +856,7 @@ mixin _$PatientStore on _PatientStoreBase, Store { id: ${id}, isLoading: ${isLoading}, isSaving: ${isSaving}, +isDeleting: ${isDeleting}, errorMessage: ${errorMessage}, successMessage: ${successMessage}, fecReg: ${fecReg}, diff --git a/lib/features/patient/presentation/views/patient_modal_view.dart b/lib/features/patient/presentation/views/patient_modal_view.dart index 47a2edf..d635c22 100644 --- a/lib/features/patient/presentation/views/patient_modal_view.dart +++ b/lib/features/patient/presentation/views/patient_modal_view.dart @@ -18,14 +18,18 @@ class PatientModalView extends StatefulWidget { final PatientModel? initialPatient; final int? patientId; final ValueChanged? onSaved; + final ValueChanged? onDeleted; final bool isSecretary; + final PatientStore? store; const PatientModalView({ super.key, this.initialPatient, this.patientId, this.onSaved, + this.onDeleted, this.isSecretary = false, + this.store, }); @override @@ -39,13 +43,13 @@ class _PatientModalViewState extends State { @override void initState() { super.initState(); - _store = PatientStore(); + _store = widget.store ?? PatientStore(); if (widget.initialPatient != null) { _store.populateFromModel(widget.initialPatient!); } else if (widget.patientId != null) { _store.loadPatient(widget.patientId!); - } else { + } else if (widget.store == null) { _store.initForCreate(); } } @@ -83,6 +87,176 @@ class _PatientModalViewState extends State { } } + Future _confirmDeletePatient() async { + final patientId = _store.id; + if (patientId == null) return; + final patientName = '${_store.nombre} ${_store.apellido}'.trim(); + final cedula = _store.cedula.trim().isNotEmpty ? _store.cedula.trim() : 'No registrada'; + + final confirmed = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogCtx) => StatefulBuilder( + builder: (builderCtx, setDialogState) { + return AlertDialog( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: AppColors.border), + ), + titlePadding: const EdgeInsets.fromLTRB(20, 20, 20, 10), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + actionsPadding: const EdgeInsets.fromLTRB(20, 10, 20, 16), + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.error.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: const Icon(Icons.delete_forever_rounded, color: AppColors.error, size: 22), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + '¿Eliminar Ficha del Paciente?', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary), + ), + ), + ], + ), + content: SizedBox( + width: 440, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primaryContainer.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + patientName.isNotEmpty ? patientName : 'Paciente sin nombre', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark), + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'ID: #$patientId', + style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600, color: AppColors.primary), + ), + ), + const SizedBox(width: 10), + Text('Cédula: $cedula', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFEF2F2), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.error.withValues(alpha: 0.25)), + ), + child: const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.warning_amber_rounded, color: AppColors.error, size: 20), + SizedBox(width: 10), + Expanded( + child: Text( + 'Esta acción es permanente e irreversible. Se eliminará el expediente completo, antecedentes médicos, consultas, recetas y citas asociadas.', + style: TextStyle( + fontSize: 12.5, + color: Color(0xFF991B1B), + height: 1.35, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _store.isDeleting ? null : () => Navigator.of(dialogCtx).pop(false), + child: const Text('Cancelar', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w600)), + ), + ElevatedButton.icon( + onPressed: _store.isDeleting + ? null + : () async { + setDialogState(() {}); + final success = await _store.deletePatient(); + if (dialogCtx.mounted) { + Navigator.of(dialogCtx).pop(success); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + icon: _store.isDeleting + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.delete_outline_rounded, size: 16), + label: Text( + _store.isDeleting ? 'Eliminando...' : 'Eliminar Paciente', + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + ], + ); + }, + ), + ); + + if (!mounted) return; + if (confirmed == true) { + if (widget.onDeleted != null) { + widget.onDeleted!(patientId); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ficha del paciente $patientName eliminada exitosamente.'), + backgroundColor: const Color(0xFF00A65A), + behavior: SnackBarBehavior.floating, + ), + ); + if (Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + } + } + @override Widget build(BuildContext context) { return Observer( @@ -192,7 +366,31 @@ class _PatientModalViewState extends State { ), const SizedBox(width: 8), ], - if (!widget.isSecretary) + if (!widget.isSecretary) ...[ + if (_store.isEditing) ...[ + OutlinedButton.icon( + onPressed: _store.isDeleting || _store.isSaving + ? null + : () => _confirmDeletePatient(), + icon: const Icon(Icons.delete_outline_rounded, size: 16, color: AppColors.error), + label: const Text( + 'Eliminar', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.error, + ), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 0), + minimumSize: const Size(0, 34), + side: BorderSide(color: AppColors.error.withValues(alpha: 0.35)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + backgroundColor: AppColors.error.withValues(alpha: 0.04), + ), + ), + const SizedBox(width: 8), + ], ClinicalDocumentsDropdown( onSelect: (docType) { if (docType == ClinicalDocumentType.recetaMedica) { @@ -234,12 +432,13 @@ class _PatientModalViewState extends State { } }, ), + ], ], bottomBar: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: const BoxDecoration( - color: Color(0xFFF8FAFC), - border: Border(top: BorderSide(color: Color(0xFFE2E8F0))), + color: Colors.white, + border: Border(top: BorderSide(color: AppColors.border)), ), child: Row( children: [ diff --git a/test/directus_integration_test.dart b/test/directus_integration_test.dart index c74d994..99e04de 100644 --- a/test/directus_integration_test.dart +++ b/test/directus_integration_test.dart @@ -93,6 +93,18 @@ void main() { expect(created.id, isNotNull); expect(created.id! > 0, isTrue); + // Rutina de limpieza automática para no dejar registros basura en la BD + addTearDown(() async { + if (created.id != null) { + try { + await apiClient.dio.delete('/items/pacientes/${created.id}'); + print('🧹 [DIRECTUS TEST] Paciente de prueba ${created.id} eliminado correctamente tras la verificación.'); + } catch (e) { + print('⚠️ [DIRECTUS TEST] No se pudo eliminar el paciente de prueba: $e'); + } + } + }); + // 4. Verificar obtención directa por ID print('\n[DIRECTUS TEST] 4. Verificando getPatientById(${created.id})...'); final fetched = await patientService.getPatientById(created.id!); diff --git a/test/features/patient_delete_test.dart b/test/features/patient_delete_test.dart new file mode 100644 index 0000000..7c97068 --- /dev/null +++ b/test/features/patient_delete_test.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hidalyx_drluiscastro/features/patient/data/models/patient_model.dart'; +import 'package:hidalyx_drluiscastro/features/patient/data/services/patient_service.dart'; +import 'package:hidalyx_drluiscastro/features/patient/presentation/store/patient_store.dart'; +import 'package:hidalyx_drluiscastro/features/patient/presentation/views/patient_modal_view.dart'; + +class MockPatientServiceForDelete extends PatientService { + int? deletedPatientId; + bool shouldFail = false; + + @override + Future deletePatient(int id) async { + if (shouldFail) { + throw Exception('Error simulado al eliminar paciente'); + } + deletedPatientId = id; + } +} + +void main() { + group('Patient Deletion Unit & Store Tests', () { + test('PatientStore.deletePatient deletes patient and updates state', () async { + final mockService = MockPatientServiceForDelete(); + final store = PatientStore(patientService: mockService); + + // Attempt to delete with null id + final nullResult = await store.deletePatient(); + expect(nullResult, isFalse); + expect(store.errorMessage, contains('No se puede eliminar un paciente que no ha sido guardado')); + + // Populate patient + store.populateFromModel(const PatientModel( + id: 777, + nombre: 'CARLOS', + apellido: 'MARTINEZ', + )); + expect(store.id, 777); + + // Execute deletion + final success = await store.deletePatient(); + expect(success, isTrue); + expect(mockService.deletedPatientId, 777); + expect(store.isDeleting, isFalse); + expect(store.successMessage, 'Paciente eliminado exitosamente'); + expect(store.errorMessage, isNull); + }); + + test('PatientStore.deletePatient handles service errors gracefully', () async { + final mockService = MockPatientServiceForDelete()..shouldFail = true; + final store = PatientStore(patientService: mockService); + + store.populateFromModel(const PatientModel( + id: 888, + nombre: 'LUCIA', + apellido: 'MENDEZ', + )); + + final success = await store.deletePatient(); + expect(success, isFalse); + expect(store.isDeleting, isFalse); + expect(store.errorMessage, contains('Error simulado al eliminar paciente')); + }); + }); + + group('Patient Deletion UI & Role Tests', () { + testWidgets('Doctor sees "Eliminar" button when editing an existing patient', (tester) async { + tester.view.physicalSize = const Size(1400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + const patient = PatientModel( + id: 555, + nombre: 'JUAN', + apellido: 'PEREZ', + cedula: '001-9999999-1', + ); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PatientModalView( + initialPatient: patient, + isSecretary: false, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Eliminar'), findsOneWidget); + }); + + testWidgets('Doctor does NOT see "Eliminar" button when creating a new patient', (tester) async { + tester.view.physicalSize = const Size(1400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PatientModalView( + isSecretary: false, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Eliminar'), findsNothing); + }); + + testWidgets('Secretary NEVER sees "Eliminar" button even when editing an existing patient', (tester) async { + tester.view.physicalSize = const Size(1400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + const patient = PatientModel( + id: 555, + nombre: 'JUAN', + apellido: 'PEREZ', + ); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PatientModalView( + initialPatient: patient, + isSecretary: true, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Eliminar'), findsNothing); + }); + + testWidgets('Clicking "Eliminar" opens confirmation dialog, Cancel does not delete', (tester) async { + tester.view.physicalSize = const Size(1400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final mockService = MockPatientServiceForDelete(); + final store = PatientStore(patientService: mockService); + store.populateFromModel(const PatientModel( + id: 123, + nombre: 'ROBERTO', + apellido: 'GARCIA', + cedula: '402-1234567-8', + )); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PatientModalView( + store: store, + isSecretary: false, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Click Eliminar button + await tester.tap(find.text('Eliminar')); + await tester.pumpAndSettle(); + + // Verify confirmation dialog + expect(find.text('¿Eliminar Ficha del Paciente?'), findsOneWidget); + expect(find.text('ROBERTO GARCIA'), findsOneWidget); + expect(find.textContaining('#123'), findsOneWidget); + expect(find.textContaining('Esta acción es permanente e irreversible'), findsOneWidget); + + // Click Cancelar + await tester.tap(find.text('Cancelar')); + await tester.pumpAndSettle(); + + // Dialog dismissed, patient NOT deleted + expect(find.text('¿Eliminar Ficha del Paciente?'), findsNothing); + expect(mockService.deletedPatientId, isNull); + }); + + testWidgets('Confirming deletion invokes service, calls onDeleted, and closes modal', (tester) async { + tester.view.physicalSize = const Size(1400, 1000); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final mockService = MockPatientServiceForDelete(); + final store = PatientStore(patientService: mockService); + store.populateFromModel(const PatientModel( + id: 999, + nombre: 'ALBERTO', + apellido: 'MOREL', + )); + + int? deletedIdReceived; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (ctx) => ElevatedButton( + onPressed: () => showDialog( + context: ctx, + builder: (_) => PatientModalView( + store: store, + isSecretary: false, + onDeleted: (id) => deletedIdReceived = id, + ), + ), + child: const Text('Open Modal'), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Open modal + await tester.tap(find.text('Open Modal')); + await tester.pumpAndSettle(); + + // Click Eliminar + await tester.tap(find.text('Eliminar')); + await tester.pumpAndSettle(); + + // Click Eliminar Paciente inside confirmation dialog + await tester.tap(find.text('Eliminar Paciente')); + await tester.pumpAndSettle(); + + expect(mockService.deletedPatientId, 999); + expect(deletedIdReceived, 999); + expect(find.textContaining('eliminada exitosamente'), findsOneWidget); + }); + }); +} diff --git a/test/features/secretary_role_test.dart b/test/features/secretary_role_test.dart index 0ecd5cf..bc65642 100644 --- a/test/features/secretary_role_test.dart +++ b/test/features/secretary_role_test.dart @@ -127,9 +127,10 @@ void main() { expect(find.text('Ficha de Recepción'), findsOneWidget); expect(find.text('Recepción, datos personales y programación de citas'), findsOneWidget); - // Confidential clinical documents and clinical tabs MUST NOT exist + // Confidential clinical documents, clinical tabs and delete button MUST NOT exist expect(find.byType(ClinicalDocumentsDropdown), findsNothing); expect(find.byType(PatientClinicalTabs), findsNothing); + expect(find.text('Eliminar'), findsNothing); // Secretary appointment section MUST exist expect(find.byType(SecretaryAppointmentSection), findsOneWidget); @@ -160,9 +161,10 @@ void main() { // Verify title reflects doctor editing expect(find.text('Editar Paciente'), findsOneWidget); - // Doctor gets clinical documents dropdown and clinical tabs + // Doctor gets clinical documents dropdown, clinical tabs and delete button expect(find.byType(ClinicalDocumentsDropdown), findsOneWidget); expect(find.byType(PatientClinicalTabs), findsOneWidget); + expect(find.text('Eliminar'), findsOneWidget); expect(find.byType(SecretaryAppointmentSection), findsNothing); }); });