fix(alarmas): una alarma de un solo uso a una hora ya pasada suena manana
Build & Deploy PluriWave / Análisis de código (push) Successful in 32s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 3m11s

El editor dejaba la fecha clavada en hoy, asi que al escribir 05:30 siendo
las 18:30 el candidato quedaba en el pasado, `calcularProxima` lo rechazaba
con razon y la alarma se guardaba sin proxima ejecucion: nunca sonaba. En la
practica solo se podian poner alarmas unicas para lo que quedaba de dia.

`normalizarFechaUnica` resuelve el dia que el usuario realmente quiere, con
la convencion de cualquier despertador: hora ya pasada -> manana, hora por
llegar -> hoy. Una fecha elegida a proposito en el futuro no se toca nunca,
y una fecha rancia salta a hoy/manana en vez de a `fecha + 1`, que seguiria
en el pasado.

La regla vive en el editor y NO dentro de `calcularProxima`: esa tiene que
seguir siendo literal, porque el recalculo posterior al disparo y el motor
nativo dependen de que una alarma unica vencida resuelva a null en vez de
resucitar al dia siguiente.

De paso el editor pasa a leer el reloj inyectado del servicio en vez de
`DateTime.now()`, para que la vista previa, los limites del selector de
fecha y el ajuste lean el mismo instante y se puedan fijar en las pruebas.
This commit is contained in:
2026-09-22 13:14:32 +02:00
parent 2891a5703e
commit 44af98eac3
4 changed files with 272 additions and 41 deletions
+45 -6
View File
@@ -656,7 +656,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
void initState() {
super.initState();
final alarma = widget.alarma;
final ahora = DateTime.now().add(const Duration(minutes: 5));
final ahora = _ahora().add(const Duration(minutes: 5));
_hora = TimeOfDay(
hour: alarma?.hora ?? ahora.hour,
minute: alarma?.minuto ?? ahora.minute,
@@ -800,7 +800,11 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
child: Center(
child: EditorHoraInline(
value: _hora,
onChanged: (nuevo) => setState(() => _hora = nuevo),
onChanged:
(nuevo) => setState(() {
_hora = nuevo;
_reajustarFechaUnica();
}),
),
),
),
@@ -823,7 +827,10 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
],
selected: {_tipo},
onSelectionChanged:
(value) => setState(() => _tipo = value.first),
(value) => setState(() {
_tipo = value.first;
_reajustarFechaUnica();
}),
),
const SizedBox(height: 10),
// Audit 8.5 (t4 line 381): the "REPETIR" eyebrow above the
@@ -1133,7 +1140,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
);
final proxima = _programacion.calcularProxima(
alarma: borrador,
desde: DateTime.now(),
desde: _ahora(),
vacaciones: estado.vacaciones,
excepciones: estado.excepciones,
);
@@ -1163,16 +1170,48 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
}
Future<void> _elegirFecha() async {
final ahora = DateTime.now();
final ahora = _ahora();
final nueva = await showDatePicker(
context: context,
initialDate: _fecha.isBefore(ahora) ? ahora : _fecha,
firstDate: DateTime(ahora.year, ahora.month, ahora.day),
lastDate: ahora.add(const Duration(days: 730)),
);
if (nueva != null) setState(() => _fecha = nueva);
if (nueva != null) {
setState(() {
_fecha = nueva;
_reajustarFechaUnica();
});
}
}
/// Keeps `_fecha` on the day the user actually MEANS for a one-time alarm.
/// Every alarm clock reads "05:30" typed at 18:30 as "05:30 tomorrow"; the
/// editor used to keep the date pinned to today, so `calcularProxima`
/// rightly refused the past candidate and the alarm was saved with no next
/// execution at all — silently never ringing. Runs after every edit that
/// can invalidate the (time, date, recurrence) triple, so the visible date
/// field, the preview line and the saved alarm never disagree.
///
/// A date the user deliberately picked in the future is left untouched;
/// the rollover rule itself lives in the domain service.
void _reajustarFechaUnica() {
if (_tipo != TipoProgramacionAlarma.unica) return;
_fecha = _programacion.normalizarFechaUnica(
fecha: _fecha,
hora: _hora.hour,
minuto: _hora.minute,
ahora: _ahora(),
);
}
/// Single clock for the whole editor. Goes through the alarm service's
/// injected clock instead of `DateTime.now()` so the draft preview, the
/// date picker's bounds and the one-time rollover all read the SAME
/// instant — and so they can be tested at a fixed wall-clock time instead
/// of depending on when the suite happens to run.
DateTime _ahora() => context.read<EstadoAlarmas>().servicio.ahora();
Future<void> _guardar() async {
if (_tipo == TipoProgramacionAlarma.diasSemana && _diasSemana.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(