diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bd17283..91d5c62 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -422,6 +422,9 @@ "editAction": "Edit", "skipNextAction": "Skip next", "deleteTooltip": "Delete", + "alarmHeroSkipAction": "Skip", + "alarmDeleteConfirmTitle": "Delete alarm?", + "alarmDeleteConfirmMessage": "This can't be undone.", "alarmSkippedNoNextSnackbar": "Alarm skipped. There is no next occurrence left.", "alarmSkippedReturnsSnackbar": "Alarm skipped. It will return on {date}.", "@alarmSkippedReturnsSnackbar": { @@ -508,6 +511,30 @@ "vacationRangesHint": "If an alarm is set to \"Paused during vacations\", it automatically skips these ranges.", "noVacationRangesLoaded": "No ranges loaded.", "deleteRangeTooltip": "Delete range", + "vacationRangesCount": "{count} ranges", + "@vacationRangesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "vacationSummaryActiveCountdown": "Active now · {days} days left", + "@vacationSummaryActiveCountdown": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "vacationSummaryUpcomingCountdown": "Next range in {days} days", + "@vacationSummaryUpcomingCountdown": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "vacationsDefaultName": "Vacation", "newVacationRangeTitle": "New vacation range", "startField": "Start", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1cfb40d..2dd45e2 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -422,6 +422,9 @@ "editAction": "Editar", "skipNextAction": "Omitir siguiente", "deleteTooltip": "Eliminar", + "alarmHeroSkipAction": "Saltar", + "alarmDeleteConfirmTitle": "¿Eliminar alarma?", + "alarmDeleteConfirmMessage": "Esta acción no se puede deshacer.", "alarmSkippedNoNextSnackbar": "Alarma omitida. No queda próxima ejecución.", "alarmSkippedReturnsSnackbar": "Alarma omitida. Volverá el {date}.", "@alarmSkippedReturnsSnackbar": { @@ -508,6 +511,30 @@ "vacationRangesHint": "Si una alarma tiene \"Pausa en vacaciones\", se salta automáticamente estos rangos.", "noVacationRangesLoaded": "Sin rangos cargados.", "deleteRangeTooltip": "Eliminar rango", + "vacationRangesCount": "{count} rangos", + "@vacationRangesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "vacationSummaryActiveCountdown": "En curso · quedan {days} días", + "@vacationSummaryActiveCountdown": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "vacationSummaryUpcomingCountdown": "Próximo rango en {days} días", + "@vacationSummaryUpcomingCountdown": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "vacationsDefaultName": "Vacaciones", "newVacationRangeTitle": "Nuevo rango de vacaciones", "startField": "Inicio", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index c369987..9a0dfb1 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -1570,6 +1570,24 @@ abstract class AppLocalizations { /// **'Eliminar'** String get deleteTooltip; + /// No description provided for @alarmHeroSkipAction. + /// + /// In es, this message translates to: + /// **'Saltar'** + String get alarmHeroSkipAction; + + /// No description provided for @alarmDeleteConfirmTitle. + /// + /// In es, this message translates to: + /// **'¿Eliminar alarma?'** + String get alarmDeleteConfirmTitle; + + /// No description provided for @alarmDeleteConfirmMessage. + /// + /// In es, this message translates to: + /// **'Esta acción no se puede deshacer.'** + String get alarmDeleteConfirmMessage; + /// No description provided for @alarmSkippedNoNextSnackbar. /// /// In es, this message translates to: @@ -1838,6 +1856,24 @@ abstract class AppLocalizations { /// **'Eliminar rango'** String get deleteRangeTooltip; + /// No description provided for @vacationRangesCount. + /// + /// In es, this message translates to: + /// **'{count} rangos'** + String vacationRangesCount(int count); + + /// No description provided for @vacationSummaryActiveCountdown. + /// + /// In es, this message translates to: + /// **'En curso · quedan {days} días'** + String vacationSummaryActiveCountdown(int days); + + /// No description provided for @vacationSummaryUpcomingCountdown. + /// + /// In es, this message translates to: + /// **'Próximo rango en {days} días'** + String vacationSummaryUpcomingCountdown(int days); + /// No description provided for @vacationsDefaultName. /// /// In es, this message translates to: diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index eb52346..0b934da 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -840,6 +840,15 @@ class AppLocalizationsAr extends AppLocalizations { @override String get deleteTooltip => 'حذف'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'تم تخطي المنبه. لا يوجد تشغيل قادم.'; @@ -994,6 +1003,21 @@ class AppLocalizationsAr extends AppLocalizations { @override String get deleteRangeTooltip => 'حذف النطاق'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'إجازات'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index 5e5d9d3..e5226b6 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -845,6 +845,15 @@ class AppLocalizationsBn extends AppLocalizations { @override String get deleteTooltip => 'মুছুন'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'অ্যালার্ম এড়ানো হয়েছে। আর কোনো পরবর্তী চালনা নেই।'; @@ -1001,6 +1010,21 @@ class AppLocalizationsBn extends AppLocalizations { @override String get deleteRangeTooltip => 'পরিসর মুছুন'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'ছুটি'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index 7d1a732..7c9ef2a 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -848,6 +848,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get deleteTooltip => 'Löschen'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarm ausgelassen. Es bleibt keine nächste Ausführung.'; @@ -1002,6 +1011,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get deleteRangeTooltip => 'Zeitraum löschen'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Ferien'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index eae8ffa..f15f4d7 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -842,6 +842,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteTooltip => 'Delete'; + @override + String get alarmHeroSkipAction => 'Skip'; + + @override + String get alarmDeleteConfirmTitle => 'Delete alarm?'; + + @override + String get alarmDeleteConfirmMessage => 'This can\'t be undone.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarm skipped. There is no next occurrence left.'; @@ -996,6 +1005,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteRangeTooltip => 'Delete range'; + @override + String vacationRangesCount(int count) { + return '$count ranges'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'Active now · $days days left'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Next range in $days days'; + } + @override String get vacationsDefaultName => 'Vacation'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index 0fa9cfd..be5bcb5 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -846,6 +846,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get deleteTooltip => 'Eliminar'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarma omitida. No queda próxima ejecución.'; @@ -1000,6 +1009,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String get deleteRangeTooltip => 'Eliminar rango'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Vacaciones'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 064847e..be5eab0 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -850,6 +850,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get deleteTooltip => 'Supprimer'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarme ignorée. Il ne reste aucune prochaine exécution.'; @@ -1006,6 +1015,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get deleteRangeTooltip => 'Supprimer la période'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Vacances'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index 11a3108..1f4cb42 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -842,6 +842,15 @@ class AppLocalizationsHi extends AppLocalizations { @override String get deleteTooltip => 'हटाएँ'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'अलार्म छोड़ा गया। कोई अगली चाल बाकी नहीं।'; @@ -997,6 +1006,21 @@ class AppLocalizationsHi extends AppLocalizations { @override String get deleteRangeTooltip => 'अवधि हटाएँ'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'छुट्टियाँ'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 3ecb762..1bd3207 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -845,6 +845,15 @@ class AppLocalizationsId extends AppLocalizations { @override String get deleteTooltip => 'Hapus'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarm dilewati. Tidak ada eksekusi berikutnya.'; @@ -1001,6 +1010,21 @@ class AppLocalizationsId extends AppLocalizations { @override String get deleteRangeTooltip => 'Hapus rentang'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Liburan'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 4b2b60f..e327682 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -846,6 +846,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get deleteTooltip => 'Elimina'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Sveglia saltata. Non resta alcuna prossima esecuzione.'; @@ -1002,6 +1011,21 @@ class AppLocalizationsIt extends AppLocalizations { @override String get deleteRangeTooltip => 'Elimina periodo'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Vacanze'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index b0865fe..3dfc365 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -820,6 +820,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get deleteTooltip => '削除'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'アラームをスキップしました。次回実行は残っていません。'; @@ -970,6 +979,21 @@ class AppLocalizationsJa extends AppLocalizations { @override String get deleteRangeTooltip => '期間を削除'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => '休暇'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index 82a34d2..bfd819b 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -845,6 +845,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get deleteTooltip => 'Excluir'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Alarme ignorado. Não resta próxima execução.'; @@ -999,6 +1008,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get deleteRangeTooltip => 'Excluir período'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Férias'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 9ee755a..a95fb97 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -846,6 +846,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get deleteTooltip => 'Удалить'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => 'Будильник пропущен. Следующего запуска нет.'; @@ -1001,6 +1010,21 @@ class AppLocalizationsRu extends AppLocalizations { @override String get deleteRangeTooltip => 'Удалить период'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => 'Отпуск'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 9d8b01c..960292c 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -816,6 +816,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get deleteTooltip => '删除'; + @override + String get alarmHeroSkipAction => 'Saltar'; + + @override + String get alarmDeleteConfirmTitle => '¿Eliminar alarma?'; + + @override + String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.'; + @override String get alarmSkippedNoNextSnackbar => '已跳过闹钟。没有剩余的下次执行。'; @@ -966,6 +975,21 @@ class AppLocalizationsZh extends AppLocalizations { @override String get deleteRangeTooltip => '删除范围'; + @override + String vacationRangesCount(int count) { + return '$count rangos'; + } + + @override + String vacationSummaryActiveCountdown(int days) { + return 'En curso · quedan $days días'; + } + + @override + String vacationSummaryUpcomingCountdown(int days) { + return 'Próximo rango en $days días'; + } + @override String get vacationsDefaultName => '假期'; diff --git a/lib/pantallas/pantalla_alarmas.dart b/lib/pantallas/pantalla_alarmas.dart index dc537ce..91e7d8f 100644 --- a/lib/pantallas/pantalla_alarmas.dart +++ b/lib/pantallas/pantalla_alarmas.dart @@ -16,6 +16,7 @@ import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_premium_widgets.dart'; +import '../widgets/pluri_push_scaffold.dart'; class PantallaAlarmas extends StatelessWidget { const PantallaAlarmas({super.key}); @@ -128,6 +129,18 @@ class _PanelProximaAlarma extends StatelessWidget { : l10n.createAlarmHint : '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}', ), + if (proxima != null) ...[ + const SizedBox(height: 10), + Align( + alignment: Alignment.centerLeft, + child: OutlinedButton.icon( + key: const ValueKey('hero-skip-next'), + onPressed: () => _saltarDesdeHero(context, proxima), + icon: const Icon(Icons.skip_next_rounded, size: 18), + label: Text(l10n.alarmHeroSkipAction), + ), + ), + ], ], ), ), @@ -137,6 +150,40 @@ class _PanelProximaAlarma extends StatelessWidget { } } +/// Hero banner inline skip (native-alarms delta, WU8): skips the featured +/// (soonest-firing) alarm via the SAME `saltarProxima` path the old +/// always-visible per-card skip button used to call — only the trigger +/// location moved. +Future _saltarDesdeHero( + BuildContext context, + AlarmaMusical proxima, +) async { + final l10n = AppLocalizations.of(context); + final estado = context.read(); + await estado.saltarProxima(proxima.id); + if (!context.mounted) return; + final actualizada = context.read().alarmas.firstWhere( + (item) => item.id == proxima.id, + orElse: () => proxima, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + actualizada.proximaProgramable == null + ? l10n.alarmSkippedNoNextSnackbar + : l10n.alarmSkippedReturnsSnackbar( + _fechaHora(l10n, actualizada.proximaProgramable!), + ), + ), + ), + ); +} + +/// Simplified alarm card (native-alarms delta, WU8): giant time + station + +/// switch, no always-visible action row. Edit/skip/delete are NEVER lost — +/// they move behind gestures: tap opens the editor, swipe deletes (with +/// confirmation), skip lives on the hero banner (`_PanelProximaAlarma`) +/// instead of a per-card button. class _TarjetaAlarma extends StatelessWidget { const _TarjetaAlarma({required this.alarma}); @@ -146,187 +193,67 @@ class _TarjetaAlarma extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final estado = context.watch(); - final excepcion = estado.ultimaExcepcionPara(alarma.id); - final mensajeVacaciones = _mensajeVacaciones(l10n, estado.vacaciones); - return PluriGlassSurface( - glowColor: context.pluriTokens.electricMagenta.withValues(alpha: 0.22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _AssetIcon( - 'assets/icons/alarmas/alarm_music.png', - size: 64, - semanticLabel: l10n.alarmIconLabel, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _hora(alarma), - style: Theme.of( - context, - ).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w900, - letterSpacing: -1, - ), + final tokens = context.pluriTokens; + final estacion = + alarma.emisora == null + ? l10n.noStationUseInternalSound + : localizedStationName(l10n, alarma.emisora!.nombre); + + return Dismissible( + key: ValueKey('tarjeta-alarma-${alarma.id}'), + direction: DismissDirection.horizontal, + background: const _FondoSwipeEliminarAlarma( + alignment: Alignment.centerLeft, + ), + secondaryBackground: const _FondoSwipeEliminarAlarma( + alignment: Alignment.centerRight, + ), + confirmDismiss: (_) => _confirmarEliminarAlarma(context, l10n), + onDismissed: (_) => estado.eliminarAlarma(alarma.id), + child: PluriGlassSurface( + glowColor: tokens.electricMagenta.withValues(alpha: 0.22), + padding: EdgeInsets.zero, + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(tokens.radiusMd), + onTap: () => _abrirEditor(context, alarma: alarma), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _hora(alarma), + style: Theme.of( + context, + ).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: -1.5, + ), + ), + const SizedBox(height: 2), + Text(estacion, overflow: TextOverflow.ellipsis), + ], ), - Text(_nombreVisibleAlarma(l10n, alarma)), - ], - ), - ), - Switch.adaptive( - value: alarma.activa, - onChanged: (value) => estado.cambiarActiva(alarma, value), - ), - ], - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _InfoChip( - icon: Icons.repeat_rounded, - label: _programacion(l10n, alarma), - ), - _InfoChip( - icon: Icons.beach_access_rounded, - label: - alarma.sonarEnVacaciones - ? l10n.alarmVacationPlay - : l10n.alarmVacationPause, - ), - _InfoChip( - icon: Icons.volume_up_rounded, - label: '${(alarma.volumen * 100).round()}%', - ), - _InfoChip( - icon: Icons.trending_up_rounded, - label: l10n.alarmFadeInLabel(alarma.fadeInSegundos), - ), - ], - ), - const SizedBox(height: 12), - if (alarma.proximaProgramable != null) - _NoticeLine( - icon: Icons.event_available_rounded, - text: l10n.alarmNextExecution( - _fechaHora(l10n, alarma.proximaProgramable!), - ), - ) - else - _NoticeLine( - icon: Icons.pause_circle_outline_rounded, - text: l10n.alarmNoNextExecution, - ), - if (excepcion != null) ...[ - const SizedBox(height: 6), - _NoticeLine( - icon: Icons.skip_next_rounded, - text: l10n.alarmSkippedExecution( - _fechaHora(l10n, excepcion.ejecucion), + ), + const SizedBox(width: 8), + Switch.adaptive( + value: alarma.activa, + onChanged: (value) => estado.cambiarActiva(alarma, value), + ), + ], ), ), - ], - if (mensajeVacaciones != null) ...[ - const SizedBox(height: 6), - _NoticeLine( - icon: Icons.beach_access_rounded, - text: mensajeVacaciones, - ), - ], - const SizedBox(height: 12), - Row( - children: [ - TextButton.icon( - icon: const Icon(Icons.edit_rounded), - label: Text(l10n.editAction), - onPressed: () => _abrirEditor(context, alarma: alarma), - ), - TextButton.icon( - icon: const Icon(Icons.skip_next_rounded), - label: Text(l10n.skipNextAction), - onPressed: - alarma.proximaProgramable == null - ? null - : () async { - await estado.saltarProxima(alarma.id); - if (context.mounted) { - final alarmas = - context.read().alarmas; - AlarmaMusical? actualizada; - for (final item in alarmas) { - if (item.id == alarma.id) { - actualizada = item; - break; - } - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - actualizada?.proximaProgramable == null - ? l10n.alarmSkippedNoNextSnackbar - : l10n.alarmSkippedReturnsSnackbar( - _fechaHora( - l10n, - actualizada!.proximaProgramable!, - ), - ), - ), - ), - ); - } - }, - ), - const Spacer(), - IconButton( - tooltip: l10n.deleteAction, - icon: const Icon(Icons.delete_outline_rounded), - onPressed: () => estado.eliminarAlarma(alarma.id), - ), - ], ), - ], + ), ), ); } - String? _mensajeVacaciones( - AppLocalizations l10n, - List vacaciones, - ) { - if (alarma.sonarEnVacaciones) return null; - final ahora = DateTime.now(); - RangoVacaciones? actual; - for (final rango in vacaciones) { - if (rango.contiene(ahora)) { - actual = rango; - break; - } - } - if (actual != null) { - if (alarma.proximaProgramable == null) { - return l10n.alarmVacationPausedNoNext( - _nombreVisibleVacaciones(l10n, actual), - ); - } - return l10n.alarmVacationPausedReturns( - _nombreVisibleVacaciones(l10n, actual), - _fechaHora(l10n, alarma.proximaProgramable!), - ); - } - if (alarma.proximaProgramable != null) { - return l10n.alarmVacationReturns( - _fechaHora(l10n, alarma.proximaProgramable!), - ); - } - return null; - } - void _abrirEditor(BuildContext context, {required AlarmaMusical alarma}) { showModalBottomSheet( context: context, @@ -336,6 +263,56 @@ class _TarjetaAlarma extends StatelessWidget { builder: (_) => _EditorAlarmaSheet(alarma: alarma), ); } + + Future _confirmarEliminarAlarma( + BuildContext context, + AppLocalizations l10n, + ) async { + final confirmado = await showDialog( + context: context, + builder: + (ctx) => AlertDialog( + title: Text(l10n.alarmDeleteConfirmTitle), + content: Text(l10n.alarmDeleteConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text(l10n.cancelAction), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(l10n.deleteAction), + ), + ], + ), + ); + return confirmado ?? false; + } +} + +/// Swipe-to-delete reveal, shown on both sides so either swipe direction +/// works regardless of locale text direction. +class _FondoSwipeEliminarAlarma extends StatelessWidget { + const _FondoSwipeEliminarAlarma({required this.alignment}); + + final Alignment alignment; + + @override + Widget build(BuildContext context) { + final tokens = context.pluriTokens; + return Container( + alignment: alignment, + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.error, + borderRadius: BorderRadius.circular(tokens.radiusMd), + ), + child: Icon( + Icons.delete_outline_rounded, + color: Theme.of(context).colorScheme.onError, + ), + ); + } } class _EditorAlarmaSheet extends StatefulWidget { @@ -1009,6 +986,12 @@ class _AccesoDiagnostico extends StatelessWidget { } } +/// Vacation summary row (alarm-vacation-ranges delta, WU8): replaces the old +/// always-inline range list with a tap target showing range count + next- +/// range countdown, pushing the Vacaciones manager screen. WU9 owns the +/// destination screen's real content (`EstadoAlarmas` query additions per +/// design ADR-6); this row's tap target is a temporary placeholder until then +/// (see `_PantallaVacacionesTemporal` below). class _PanelVacaciones extends StatelessWidget { const _PanelVacaciones({required this.estado}); @@ -1017,38 +1000,118 @@ class _PanelVacaciones extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); - final vacaciones = [...estado.vacaciones] - ..sort((a, b) => a.inicioDia.compareTo(b.inicioDia)); + final tokens = context.pluriTokens; + final resumen = _resumenVacaciones(l10n, estado.vacaciones); return PluriGlassSurface( glowColor: PluriWaveTokens.skyBlue.withValues(alpha: 0.22), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _AssetIcon( - 'assets/icons/alarmas/vacation_wave.png', - size: 48, - semanticLabel: l10n.vacationIconLabel, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - l10n.vacationRangesTitle, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w900, + padding: EdgeInsets.zero, + child: Material( + type: MaterialType.transparency, + child: InkWell( + key: const ValueKey('vacaciones-resumen'), + borderRadius: BorderRadius.circular(tokens.radiusMd), + onTap: () => _abrirVacaciones(context), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _AssetIcon( + 'assets/icons/alarmas/vacation_wave.png', + size: 48, + semanticLabel: l10n.vacationIconLabel, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.vacationRangesTitle, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w900), + ), + const SizedBox(height: 4), + Text(resumen), + ], ), ), - ), - FilledButton.tonalIcon( - onPressed: () => _abrirAlta(context), - icon: const Icon(Icons.add_rounded), - label: Text(l10n.addAction), - ), - ], + const Icon(Icons.chevron_right_rounded), + ], + ), ), - const SizedBox(height: 8), + ), + ), + ); + } + + void _abrirVacaciones(BuildContext context) { + PluriPushScaffold.push(context, (_) => const _PantallaVacacionesTemporal()); + } + + /// Range count + next-range countdown, computed directly over the existing + /// `estado.vacaciones` (no new `EstadoAlarmas` query method — those are + /// design ADR-6's pure additions, owned by WU9). + String _resumenVacaciones( + AppLocalizations l10n, + List vacaciones, + ) { + if (vacaciones.isEmpty) return l10n.noVacationRangesLoaded; + final ahora = DateTime.now(); + final hoy = DateTime(ahora.year, ahora.month, ahora.day); + final conteo = l10n.vacationRangesCount(vacaciones.length); + RangoVacaciones? activo; + for (final rango in vacaciones) { + if (rango.contiene(ahora)) { + activo = rango; + break; + } + } + if (activo != null) { + final dias = activo.finDia.difference(hoy).inDays; + return '$conteo · ${l10n.vacationSummaryActiveCountdown(dias)}'; + } + final futuros = + vacaciones.where((rango) => rango.inicioDia.isAfter(hoy)).toList() + ..sort((a, b) => a.inicioDia.compareTo(b.inicioDia)); + if (futuros.isNotEmpty) { + final dias = futuros.first.inicioDia.difference(hoy).inDays; + return '$conteo · ${l10n.vacationSummaryUpcomingCountdown(dias)}'; + } + return conteo; + } +} + +/// Temporary push destination (WU8). WU9 replaces this whole widget with the +/// real `PantallaVacaciones` (active-range hero, per-alarm impact line, +/// upcoming/past sections) from `lib/pantallas/pantalla_vacaciones.dart`. The +/// body below is the OLD inline panel's content, moved verbatim — same +/// "keep the real action, drop only the header" rule WU3a/WU3b established — +/// so add/delete range capability is never dropped even for one commit. +class _PantallaVacacionesTemporal extends StatelessWidget { + const _PantallaVacacionesTemporal(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final estado = context.watch(); + final vacaciones = [...estado.vacaciones] + ..sort((a, b) => a.inicioDia.compareTo(b.inicioDia)); + return PluriPushScaffold( + title: l10n.vacationRangesTitle, + body: ListView( + padding: PluriLayout.pageContentPadding, + children: [ Text(l10n.vacationRangesHint), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: FilledButton.tonalIcon( + onPressed: () => _abrirAlta(context), + icon: const Icon(Icons.add_rounded), + label: Text(l10n.addAction), + ), + ), + const SizedBox(height: 12), if (vacaciones.isEmpty) Text(l10n.noVacationRangesLoaded) else @@ -1285,18 +1348,6 @@ class _SectionLabel extends StatelessWidget { } } -class _InfoChip extends StatelessWidget { - const _InfoChip({required this.icon, required this.label}); - - final IconData icon; - final String label; - - @override - Widget build(BuildContext context) { - return Chip(avatar: Icon(icon, size: 16), label: Text(label)); - } -} - class _NoticeLine extends StatelessWidget { const _NoticeLine({super.key, required this.icon, required this.text}); @@ -1351,18 +1402,6 @@ String _nombreVisibleVacaciones(AppLocalizations l10n, RangoVacaciones rango) { String _hora(AlarmaMusical alarma) => '${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}'; -String _programacion(AppLocalizations l10n, AlarmaMusical alarma) { - return switch (alarma.tipoProgramacion) { - TipoProgramacionAlarma.unica => l10n.alarmScheduleOnce( - _fechaCorta(l10n, alarma.fechaUnica ?? DateTime.now()), - ), - TipoProgramacionAlarma.diaria => l10n.dailyOption, - TipoProgramacionAlarma.diasSemana => l10n.alarmScheduleWeekdays( - alarma.diasSemana.map((day) => _weekdayShort(l10n, day)).join(', '), - ), - }; -} - String _fechaHora(AppLocalizations l10n, DateTime fecha) => l10n.dateTimeSentence(fecha); diff --git a/openspec/changes/rediseno-funcional/tasks.md b/openspec/changes/rediseno-funcional/tasks.md index b32df60..766caae 100644 --- a/openspec/changes/rediseno-funcional/tasks.md +++ b/openspec/changes/rediseno-funcional/tasks.md @@ -502,21 +502,36 @@ Calls Unchanged by Transport Extraction **Modified tests**: `pantalla_alarmas_editor_test.dart` (+ new card-gesture scenarios); `estado_alarmas_test.dart`, `estado_alarmas_snooze_test.dart` must pass unmodified -- [ ] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled; +- [x] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled; swiping triggers delete with confirmation; no capability from the old always-visible button row is lost. -- [ ] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with +- [x] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with existing skip-next behaviour. -- [ ] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen +- [x] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen on tap (destination screen not yet built — assert `PluriPushScaffold.push` is invoked; WU9 builds it). -- [ ] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe. -- [ ] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path. -- [ ] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager - screen. -- [ ] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and - `estado_alarmas_snooze_test.dart` pass unmodified. -- [ ] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirm +- [x] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe. + **Corrected at apply time**: proposal.md's own WU8 row says "giant time + station + switch" (not "name") — + the card shows the STATION (or `noStationUseInternalSound`), not the alarm's custom name; the name stays + editable inside the editor sheet, unchanged. Tap-to-edit and swipe-to-delete-with-confirmation implemented via + `Dismissible` (`confirmDismiss` shows an `AlertDialog`, `onDismissed` calls the existing `eliminarAlarma`) plus + an inner `Material(type: MaterialType.transparency) > InkWell` (the known `PluriGlassSurface`-ink-splash hazard + from WU7, pre-empted here rather than rediscovered). All 4 old per-card chips (repeat/vacation/volume/fade) and + notice lines (next-execution/skipped/vacation-paused) are dropped from the collapsed card per "minimal cards" + — still reachable via the editor once opened; only the hard-guarded trio (edit/skip/delete) is a capability + that must never be lost, and it isn't. +- [x] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path. +- [x] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager + screen. **Design decision, not specified by any ADR (WU8 has none)**: the summary row's "range count + + next-range countdown" is computed directly over the existing `estado.vacaciones` (no new `EstadoAlarmas` query + method — ADR-6's pure query additions are WU9's job). The push target is `_PantallaVacacionesTemporal`, a + private placeholder holding the OLD inline panel's body verbatim (hint text, "Add" action, per-range delete) — + same "keep the real action, drop only the header" rule WU3a/WU3b established — so add/delete-range capability + is never dropped, not even for one commit, and WU9 replaces this whole widget with the real `PantallaVacaciones`. +- [x] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and + `estado_alarmas_snooze_test.dart` pass unmodified. Also removed now-dead code the simplification orphaned: + the `_InfoChip` widget and the `_programacion` helper (both were only reachable from the old chips row). +- [x] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirmed `pantalla_alarma_sonando_dismiss_guard_test.dart` was not touched by this WU (it only edits the root list - screen). + screen) — re-ran it alongside the scoped suite, byte-identical, all 8 cases green. ## WU9 — Vacaciones manager (new screen) diff --git a/test/pantallas/pantalla_alarmas_editor_test.dart b/test/pantallas/pantalla_alarmas_editor_test.dart index 4f617d5..8b7f977 100644 --- a/test/pantallas/pantalla_alarmas_editor_test.dart +++ b/test/pantallas/pantalla_alarmas_editor_test.dart @@ -6,6 +6,7 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/pantallas/pantalla_alarmas.dart'; import 'package:pluriwave/servicios/servicio_alarmas.dart'; +import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,6 +19,80 @@ class _Entorno { final EstadoAlarmas estadoAlarmas; } +class _EntornoDosAlarmas { + _EntornoDosAlarmas({required this.estadoAlarmas, required this.android}); + + final EstadoAlarmas estadoAlarmas; + final FakePuertoAlarmasAndroid android; +} + +/// WU8 fixture: two DAILY alarms ('r1' 07:00, 'r2' 08:00) under a FIXED clock +/// (06:00, before both) so 'r1' is deterministically the featured/soonest +/// alarm regardless of the wall-clock time the suite happens to run at. +Future<_EntornoDosAlarmas> _montarDosAlarmas(WidgetTester tester) async { + tester.view.physicalSize = const Size(1440, 3200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final radio = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + servicioGrabacion: FakeServicioGrabacionRadioInactiva(), + iniciarAutomaticamente: false, + ); + addTearDown(radio.dispose); + + final android = FakePuertoAlarmasAndroid(); + final estadoAlarmas = EstadoAlarmas( + servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)), + android: android, + iniciarAutomaticamente: false, + ); + addTearDown(estadoAlarmas.dispose); + addTearDown(android.dispose); + await estadoAlarmas.guardarAlarma( + const AlarmaMusical( + id: 'r1', + nombre: 'Primera', + hora: 7, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + ), + ); + await estadoAlarmas.guardarAlarma( + const AlarmaMusical( + id: 'r2', + nombre: 'Segunda', + hora: 8, + minuto: 0, + tipoProgramacion: TipoProgramacionAlarma.diaria, + diasSemana: [], + ), + ); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: radio), + ChangeNotifierProvider.value(value: estadoAlarmas), + ], + child: MaterialApp( + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: PantallaAlarmas()), + ), + ), + ); + await tester.pumpAndSettle(); + + return _EntornoDosAlarmas(estadoAlarmas: estadoAlarmas, android: android); +} + Future<_Entorno> _abrirEditor(WidgetTester tester) async { tester.view.physicalSize = const Size(1440, 3200); tester.view.devicePixelRatio = 1.0; @@ -73,10 +148,12 @@ Future<_Entorno> _abrirEditor(WidgetTester tester) async { ); await tester.pumpAndSettle(); - final l10n = lookupAppLocalizations(const Locale('es')); - await tester.ensureVisible(find.text(l10n.editAction).first); + // WU8: the always-visible "Edit" button is gone — tapping the simplified + // card itself now opens the editor. + final tarjeta = find.byKey(const ValueKey('tarjeta-alarma-ed1')); + await tester.ensureVisible(tarjeta); await tester.pumpAndSettle(); - await tester.tap(find.text(l10n.editAction).first); + await tester.tap(tarjeta); await tester.pumpAndSettle(); return _Entorno(estadoAlarmas: estadoAlarmas); } @@ -207,4 +284,110 @@ void main() { final volumen = sliders.firstWhere((slider) => slider.max == 1.0); expect(volumen.min, 0.0); }); + + group('WU8 — tarjeta de alarma simplificada', () { + testWidgets( + 'tocar la tarjeta abre el editor precargado con los datos de esa ' + 'alarma', + (tester) async { + await _montarDosAlarmas(tester); + + await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2'))); + await tester.pumpAndSettle(); + + expect(find.text(l10n.editAlarmTitle), findsOneWidget); + expect(find.text('Segunda'), findsOneWidget); + }, + ); + + testWidgets('deslizar la tarjeta pide confirmacion; cancelar la conserva y ' + 'confirmar la elimina', (tester) async { + final entorno = await _montarDosAlarmas(tester); + + // Cancelar: la tarjeta se conserva. + await tester.drag( + find.byKey(const ValueKey('tarjeta-alarma-r2')), + const Offset(-600, 0), + ); + await tester.pumpAndSettle(); + expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget); + await tester.tap(find.text(l10n.cancelAction)); + await tester.pumpAndSettle(); + expect(entorno.estadoAlarmas.alarmas.length, 2); + expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsOneWidget); + + // Confirmar: la tarjeta se elimina. + await tester.drag( + find.byKey(const ValueKey('tarjeta-alarma-r2')), + const Offset(-600, 0), + ); + await tester.pumpAndSettle(); + expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget); + await tester.tap(find.text(l10n.deleteAction)); + await tester.pumpAndSettle(); + + expect(entorno.estadoAlarmas.alarmas.length, 1); + expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsNothing); + }); + + testWidgets( + 'el banner destacado permite saltar la alarma mas proxima (accion ' + '"Saltar")', + (tester) async { + final entorno = await _montarDosAlarmas(tester); + final proximaAntes = entorno.estadoAlarmas.proximaAlarma; + expect(proximaAntes?.id, 'r1'); + + await tester.tap(find.byKey(const ValueKey('hero-skip-next'))); + await tester.pumpAndSettle(); + + expect(find.byType(SnackBar), findsOneWidget); + final r1 = entorno.estadoAlarmas.alarmas.firstWhere( + (a) => a.id == 'r1', + ); + expect(r1.proximaEjecucion, isNot(proximaAntes!.proximaEjecucion)); + }, + ); + + testWidgets('la fila resumen de vacaciones empuja una pantalla al tocarla ' + '(destino provisorio; WU9 lo reemplaza)', (tester) async { + await _montarDosAlarmas(tester); + + await tester.tap(find.byKey(const ValueKey('vacaciones-resumen'))); + await tester.pumpAndSettle(); + + expect(find.byType(PluriPushScaffold), findsOneWidget); + }); + + testWidgets( + 'guardia de capacidad: editar (tap), saltar (hero) y eliminar (swipe) ' + 'siguen siendo alcanzables tras la simplificacion de la tarjeta', + (tester) async { + final entorno = await _montarDosAlarmas(tester); + + // 1) Saltar, desde el banner destacado. + await tester.tap(find.byKey(const ValueKey('hero-skip-next'))); + await tester.pumpAndSettle(); + expect(find.byType(SnackBar), findsOneWidget); + + // 2) Editar, tocando la tarjeta. + await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2'))); + await tester.pumpAndSettle(); + expect(find.text(l10n.editAlarmTitle), findsOneWidget); + await tester.tap(find.byIcon(Icons.close_rounded)); + await tester.pumpAndSettle(); + + // 3) Eliminar, deslizando y confirmando. + await tester.drag( + find.byKey(const ValueKey('tarjeta-alarma-r2')), + const Offset(-600, 0), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text(l10n.deleteAction)); + await tester.pumpAndSettle(); + + expect(entorno.estadoAlarmas.alarmas.length, 1); + }, + ); + }); }