fix(alarmas): una alarma de un solo uso a una hora ya pasada suena manana
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:
@@ -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(
|
||||
|
||||
@@ -53,6 +53,36 @@ class ServicioProgramacionAlarmas {
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolves the wall-clock date a ONE-TIME alarm should actually land on,
|
||||
/// given the time the user just typed. Alarm clocks everywhere read "05:30"
|
||||
/// at 18:30 as "05:30 tomorrow"; without this, `calcularProxima` correctly
|
||||
/// refuses a candidate that is already in the past and the alarm silently
|
||||
/// never fires. The rollover belongs here and NOT inside `calcularProxima`:
|
||||
/// that one must stay literal, because the post-execution path and the
|
||||
/// native scheduler both rely on a past one-shot resolving to null instead
|
||||
/// of resurrecting itself on the next day.
|
||||
///
|
||||
/// A date the user picked deliberately in the future is never overridden.
|
||||
/// A stale date (an old draft reopened days later) snaps to today/tomorrow
|
||||
/// rather than to `fecha + 1`, which would still be in the past.
|
||||
///
|
||||
/// Day arithmetic goes through the DateTime constructor, never
|
||||
/// `add(Duration(days: 1))` — see `_siguienteDia` for why.
|
||||
DateTime normalizarFechaUnica({
|
||||
required DateTime fecha,
|
||||
required int hora,
|
||||
required int minuto,
|
||||
required DateTime ahora,
|
||||
}) {
|
||||
final elegido = DateTime(fecha.year, fecha.month, fecha.day, hora, minuto);
|
||||
if (_sigueSiendoInminente(elegido, ahora)) return elegido;
|
||||
|
||||
final hoy = DateTime(ahora.year, ahora.month, ahora.day, hora, minuto);
|
||||
return _sigueSiendoInminente(hoy, ahora)
|
||||
? hoy
|
||||
: DateTime(ahora.year, ahora.month, ahora.day + 1, hora, minuto);
|
||||
}
|
||||
|
||||
DateTime calcularSnooze(DateTime desde, int minutos) {
|
||||
final seguro = minutos == 3 || minutos == 5 || minutos == 10 ? minutos : 5;
|
||||
return desde.add(Duration(minutes: seguro));
|
||||
|
||||
Reference in New Issue
Block a user