From 597701f49719634970e21496e541cd8ca6b53525 Mon Sep 17 00:00:00 2001 From: freetlab Date: Sat, 1 Aug 2026 12:06:00 +0200 Subject: [PATCH] fix(alarmas): add a delete action to the vacation range edit sheet The vacation edit sheet could save changes to an existing range but had no way to remove it, forcing users back to the swipe-to-delete gesture on the list. When editing (not creating) a range, the sheet now shows an outlined delete action next to Save; it reuses the existing confirmation dialog and EstadoAlarmas.eliminarRangoVacaciones exactly as the swipe gesture already does, then pops on success. --- lib/pantallas/pantalla_vacaciones.dart | 51 ++++++- test/pantallas/pantalla_vacaciones_test.dart | 140 +++++++++++++++++++ 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/lib/pantallas/pantalla_vacaciones.dart b/lib/pantallas/pantalla_vacaciones.dart index 3fb2d97..79328ad 100644 --- a/lib/pantallas/pantalla_vacaciones.dart +++ b/lib/pantallas/pantalla_vacaciones.dart @@ -842,10 +842,38 @@ class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> { ], ), const SizedBox(height: 16), - FilledButton.icon( - onPressed: _guardar, - icon: const Icon(Icons.check_rounded), - label: Text(l10n.saveRangeAction), + Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: _guardar, + icon: const Icon(Icons.check_rounded), + label: Text(l10n.saveRangeAction), + ), + ), + // Fix `vacaciones-delete`: only when EDITING an existing + // range (never when creating one -- there is nothing to + // delete yet). Reuses the exact same confirmation dialog + // (`_confirmarEliminarRango`) and deletion method + // (`eliminarRangoVacaciones`) the swipe-to-delete gesture + // already uses on both `_HeroRangoActivo` and + // `_TarjetaRangoVacaciones` -- no new deletion path. + if (widget.rango != null) ...[ + const SizedBox(width: 10), + OutlinedButton.icon( + key: const ValueKey('vacation-delete-button'), + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: _eliminar, + icon: const Icon(Icons.delete_outline_rounded), + label: Text(l10n.deleteAction), + ), + ], + ], ), ], ), @@ -903,6 +931,21 @@ class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> { } if (mounted) Navigator.pop(context); } + + /// Fix `vacaciones-delete`: mirrors `_guardar`'s pop-on-success shape, + /// but confirms first (via the same `_confirmarEliminarRange` dialog the + /// swipe gesture uses) and calls `eliminarRangoVacaciones` instead of + /// saving. Only reachable when [widget.rango] is non-null (the delete + /// button itself is hidden otherwise). + Future _eliminar() async { + final rango = widget.rango; + if (rango == null) return; + final l10n = AppLocalizations.of(context); + final confirmado = await _confirmarEliminarRango(context, l10n); + if (!confirmado || !mounted) return; + await context.read().eliminarRangoVacaciones(rango.id); + if (mounted) Navigator.pop(context); + } } class _PickerButton extends StatelessWidget { diff --git a/test/pantallas/pantalla_vacaciones_test.dart b/test/pantallas/pantalla_vacaciones_test.dart index f0e15bd..6cc9dca 100644 --- a/test/pantallas/pantalla_vacaciones_test.dart +++ b/test/pantallas/pantalla_vacaciones_test.dart @@ -614,4 +614,144 @@ void main() { }, ); }); + + group('fix vacaciones-delete: el editor ofrece una accion de eliminar solo ' + 'al editar un rango existente, reusando _confirmarEliminarRango y ' + 'eliminarRangoVacaciones exactamente como el swipe', () { + testWidgets( + 'creando un rango NUEVO (CTA "Anadir rango"), el editor NO muestra ' + 'una accion de eliminar -- no hay nada que borrar todavia', + (tester) async { + final estado = await _crearEstado(); + addTearDown(estado.dispose); + + await tester.pumpWidget(_buildScreen(estado)); + await _pumpEstable(tester); + final l10n = AppLocalizations.of( + tester.element(find.byType(PantallaVacaciones)), + ); + + await tester.tap(find.text(l10n.addVacationRangeCta)); + await _pumpEstable(tester); + + expect(find.text(l10n.newVacationRangeTitle), findsOneWidget); + expect( + find.byKey(const ValueKey('vacation-delete-button')), + findsNothing, + ); + }, + ); + + testWidgets('editando un rango EXISTENTE (tap en su tarjeta), el editor SI ' + 'muestra una accion de eliminar junto al boton de guardar', ( + tester, + ) async { + final estado = await _crearEstado( + vacaciones: [ + RangoVacaciones( + id: 'f2', + nombre: 'Verano', + inicio: _hoyDia.add(const Duration(days: 20)), + fin: _hoyDia.add(const Duration(days: 25)), + ), + ], + ); + addTearDown(estado.dispose); + + await tester.pumpWidget(_buildScreen(estado)); + await _pumpEstable(tester); + + await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2'))); + await _pumpEstable(tester); + + final l10n = AppLocalizations.of( + tester.element(find.byType(PantallaVacaciones)), + ); + expect(find.text(l10n.editVacationRangeTitle), findsOneWidget); + expect( + find.byKey(const ValueKey('vacation-delete-button')), + findsOneWidget, + ); + }); + + testWidgets( + 'tocar eliminar en el editor pide confirmacion (misma que el swipe); ' + 'cancelar conserva el rango y el editor sigue abierto', + (tester) async { + final estado = await _crearEstado( + vacaciones: [ + RangoVacaciones( + id: 'f2', + nombre: 'Verano', + inicio: _hoyDia.add(const Duration(days: 20)), + fin: _hoyDia.add(const Duration(days: 25)), + ), + ], + ); + addTearDown(estado.dispose); + + await tester.pumpWidget(_buildScreen(estado)); + await _pumpEstable(tester); + final l10n = AppLocalizations.of( + tester.element(find.byType(PantallaVacaciones)), + ); + + await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2'))); + await _pumpEstable(tester); + + await tester.tap(find.byKey(const ValueKey('vacation-delete-button'))); + await _pumpEstable(tester); + expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget); + + await tester.tap(find.text(l10n.cancelAction)); + await _pumpEstable(tester); + + expect(estado.vacaciones, hasLength(1)); + expect(find.text(l10n.editVacationRangeTitle), findsOneWidget); + }, + ); + + testWidgets('tocar eliminar en el editor y confirmar llama a ' + 'eliminarRangoVacaciones y cierra el editor (mismo efecto que el ' + 'swipe, sin pasar por _guardar)', (tester) async { + final estado = await _crearEstado( + vacaciones: [ + RangoVacaciones( + id: 'f2', + nombre: 'Verano', + inicio: _hoyDia.add(const Duration(days: 20)), + fin: _hoyDia.add(const Duration(days: 25)), + ), + ], + ); + addTearDown(estado.dispose); + + await tester.pumpWidget(_buildScreen(estado)); + await _pumpEstable(tester); + final l10n = AppLocalizations.of( + tester.element(find.byType(PantallaVacaciones)), + ); + + await tester.tap(find.byKey(const ValueKey('vacaciones-tarjeta-f2'))); + await _pumpEstable(tester); + + await tester.tap(find.byKey(const ValueKey('vacation-delete-button'))); + await _pumpEstable(tester); + expect(find.text(l10n.vacationDeleteConfirmTitle), findsOneWidget); + + // `find.widgetWithText(FilledButton, ...)`, not a bare + // `find.text(...)`: the sheet's own OutlinedButton delete action + // (same "Eliminar" label) is still in the tree behind the dialog, + // so a bare text finder would ambiguously match both. + await tester.tap(find.widgetWithText(FilledButton, l10n.deleteAction)); + await _pumpEstable(tester); + + expect(estado.vacaciones, isEmpty); + expect( + find.text(l10n.editVacationRangeTitle), + findsNothing, + reason: 'the sheet must pop, exactly like a successful _guardar', + ); + }); + }); }