feat(vacaciones): add vacation range manager screen
Add the Vacaciones manager screen per design ADR-6: an active-range
hero (name, days-remaining countdown, determinate progress bar, and a
per-alarm pause-impact line), a "PROGRAMADOS" upcoming-ranges list, an
"Add range" CTA, and a "Rangos pasados" history section. This is the
real destination WU8's Alarmas-root summary row pushes to, replacing
WU8's own temporary placeholder (_PantallaVacacionesTemporal, now
deleted).
EstadoAlarmas gains 4 pure query methods (rangoVacacionesActivo,
vacacionesProximas, vacacionesPasadas, impactoDeRango) -- read-only
over _alarmas/_vacaciones, no writes, no rescheduling, no native
bridge calls. impactoDeRango mirrors ServicioProgramacionAlarmas's own
pause predicate exactly, so the screen never disagrees with the
scheduler about which alarms are paused. ImpactoVacaciones joins
RangoVacaciones in alarma_musical.dart.
The add-range form (_EditorVacacionesSheet, _PickerButton) moved
verbatim from pantalla_alarmas.dart to its one remaining consumer.
estado_alarmas.dart's scheduling/snooze paths and the ringing screen's
dismiss guard are untouched; both test files pass unmodified.
New ARB keys (en/es only; other 11 locales are WU18's job):
vacationImpact{Paused,Continues}Label, vacationUpcomingSectionTitle,
vacationPastSectionTitle, addVacationRangeCta,
vacationNoActiveRangeHint.
This commit is contained in:
@@ -354,6 +354,69 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
await guardarVacaciones(nuevos);
|
||||
}
|
||||
|
||||
// ── Vacation queries (design ADR-6, WU9) ──────────────────────────────
|
||||
// Four PURE queries: none writes, none reschedules, none touches the
|
||||
// native bridge. Read-only over `_alarmas`/`_vacaciones`. Every method
|
||||
// takes `{DateTime? ahora}` (clock injection) defaulting to
|
||||
// `DateTime.now()` so tests can pass a fixed instant.
|
||||
|
||||
/// Currently-active vacation range, if today falls within one.
|
||||
/// Delegates to the existing `RangoVacaciones.contiene(fecha)` — which
|
||||
/// already handles the `activo` flag and day granularity — rather than
|
||||
/// reimplementing date math (a second implementation is a second set of
|
||||
/// off-by-one bugs). Callers derive "days remaining" themselves from the
|
||||
/// returned range's `finDia`, the same way WU8's summary row already
|
||||
/// does.
|
||||
RangoVacaciones? rangoVacacionesActivo({DateTime? ahora}) {
|
||||
final fecha = ahora ?? DateTime.now();
|
||||
for (final rango in _vacaciones) {
|
||||
if (rango.contiene(fecha)) return rango;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Ranges that have not yet started (`inicio > hoy`), soonest-first.
|
||||
List<RangoVacaciones> vacacionesProximas({DateTime? ahora}) {
|
||||
final fecha = ahora ?? DateTime.now();
|
||||
final hoy = DateTime(fecha.year, fecha.month, fecha.day);
|
||||
return _vacaciones.where((rango) => rango.inicioDia.isAfter(hoy)).toList()
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
}
|
||||
|
||||
/// Ranges whose end date has already passed (`fin < hoy`), most-recently-
|
||||
/// ended first.
|
||||
List<RangoVacaciones> vacacionesPasadas({DateTime? ahora}) {
|
||||
final fecha = ahora ?? DateTime.now();
|
||||
final hoy = DateTime(fecha.year, fecha.month, fecha.day);
|
||||
return _vacaciones.where((rango) => rango.finDia.isBefore(hoy)).toList()
|
||||
..sort((a, b) => b.finDia.compareTo(a.finDia));
|
||||
}
|
||||
|
||||
/// Per-alarm pause impact for [rango]. Mirrors
|
||||
/// `ServicioProgramacionAlarmas`'s own pause predicate EXACTLY
|
||||
/// (`servicio_programacion_alarmas.dart`:
|
||||
/// `!alarma.sonarEnVacaciones && estaEnVacaciones(candidato, vacaciones)`)
|
||||
/// — if these two ever diverge, the Vacaciones screen lies about which
|
||||
/// alarms are paused. [rango] is accepted for API symmetry with the
|
||||
/// other 3 queries above; the predicate itself needs no dates because it
|
||||
/// only makes sense to call this for a range that IS currently active —
|
||||
/// any alarm actually paused by it already has `sonarEnVacaciones ==
|
||||
/// false`, which is exactly what the scheduler itself would have used to
|
||||
/// skip that alarm's candidate occurrence.
|
||||
ImpactoVacaciones impactoDeRango(RangoVacaciones rango) {
|
||||
final pausadas = <AlarmaMusical>[];
|
||||
final noAfectadas = <AlarmaMusical>[];
|
||||
for (final alarma in _alarmas) {
|
||||
if (!alarma.activa) continue;
|
||||
if (alarma.sonarEnVacaciones) {
|
||||
noAfectadas.add(alarma);
|
||||
} else {
|
||||
pausadas.add(alarma);
|
||||
}
|
||||
}
|
||||
return ImpactoVacaciones(pausadas: pausadas, noAfectadas: noAfectadas);
|
||||
}
|
||||
|
||||
ExcepcionAlarma? ultimaExcepcionPara(String alarmaId) {
|
||||
final candidatas =
|
||||
_excepciones.where((e) => e.alarmaId == alarmaId).toList()
|
||||
|
||||
Reference in New Issue
Block a user