fix(alarm): recalculate every alarm on each mutation, not just the touched one
guardarAlarma only recomputed proximaEjecucion for the alarm being saved; every other alarm kept whatever snapshot the last periodic recalculation left, which can be stale or already past-due. Since EstadoAlarmas.proximaAlarma just sorts by proximaProgramable, a stale sibling could wrongly outrank a freshly activated/created/edited alarm in the "Próxima alarma" panel until the next 1-minute tick. Extended the same full-list recalculation guardarVacaciones already did to guardarAlarma, eliminarAlarma, completarEjecucion, sincronizarEjecucionesNativas, saltarProxima and posponerEjecucionHasta, via a shared _recalcularLista helper.
This commit is contained in:
@@ -116,23 +116,32 @@ class ServicioAlarmas {
|
||||
}) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final ahora = _reloj();
|
||||
final vacacionesActuales = vacaciones ?? config.vacaciones;
|
||||
final excepcionesActuales = excepciones ?? config.excepciones;
|
||||
final alarmas = List<AlarmaMusical>.from(config.alarmas);
|
||||
final index = alarmas.indexWhere((a) => a.id == alarma.id);
|
||||
final normalizada = _recalcular(
|
||||
alarma.copyWith(creadaEn: alarma.creadaEn ?? ahora, actualizadaEn: ahora),
|
||||
vacaciones ?? config.vacaciones,
|
||||
excepciones ?? config.excepciones,
|
||||
final actualizada = alarma.copyWith(
|
||||
creadaEn: alarma.creadaEn ?? ahora,
|
||||
actualizadaEn: ahora,
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
alarmas[index] = normalizada;
|
||||
alarmas[index] = actualizada;
|
||||
} else {
|
||||
alarmas.add(normalizada);
|
||||
alarmas.add(actualizada);
|
||||
}
|
||||
// Recalculate every alarm, not just the one just saved: siblings can
|
||||
// hold a proximaEjecucion snapshot from the last periodic tick that is
|
||||
// stale (even past-due), which would otherwise outrank a freshly
|
||||
// created/activated/edited alarm when EstadoAlarmas.proximaAlarma sorts
|
||||
// by proximaProgramable.
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: vacaciones ?? config.vacaciones,
|
||||
excepciones: excepciones ?? config.excepciones,
|
||||
alarmas: _recalcularLista(
|
||||
alarmas,
|
||||
vacacionesActuales,
|
||||
excepcionesActuales,
|
||||
),
|
||||
vacaciones: vacacionesActuales,
|
||||
excepciones: excepcionesActuales,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
@@ -140,10 +149,13 @@ class ServicioAlarmas {
|
||||
|
||||
Future<ConfiguracionAlarmas> eliminarAlarma(String id) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final restantes = config.alarmas.where((a) => a.id != id).toList();
|
||||
final excepciones =
|
||||
config.excepciones.where((e) => e.alarmaId != id).toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: config.alarmas.where((a) => a.id != id).toList(),
|
||||
alarmas: _recalcularLista(restantes, config.vacaciones, excepciones),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones.where((e) => e.alarmaId != id).toList(),
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
@@ -156,12 +168,12 @@ class ServicioAlarmas {
|
||||
final normalizadas =
|
||||
vacaciones.map((v) => v.normalizado()).toList()
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, normalizadas, config.excepciones))
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
alarmas: _recalcularLista(
|
||||
config.alarmas,
|
||||
normalizadas,
|
||||
config.excepciones,
|
||||
),
|
||||
vacaciones: normalizadas,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
@@ -188,10 +200,11 @@ class ServicioAlarmas {
|
||||
|
||||
Future<ConfiguracionAlarmas> recalcularTodas() => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, config.vacaciones, config.excepciones))
|
||||
.toList();
|
||||
final alarmas = _recalcularLista(
|
||||
config.alarmas,
|
||||
config.vacaciones,
|
||||
config.excepciones,
|
||||
);
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
@@ -258,7 +271,7 @@ class ServicioAlarmas {
|
||||
|
||||
if (!huboCambios) return config;
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
@@ -266,38 +279,26 @@ class ServicioAlarmas {
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) =>
|
||||
_enCola(() async {
|
||||
final config = await _configActual();
|
||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null) return config;
|
||||
Future<ConfiguracionAlarmas> saltarProxima(
|
||||
String alarmaId,
|
||||
) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null) return config;
|
||||
|
||||
final excepciones = [
|
||||
...config.excepciones,
|
||||
ExcepcionAlarma(
|
||||
alarmaId: alarmaId,
|
||||
ejecucion: proxima,
|
||||
tipo: 'skipNext',
|
||||
),
|
||||
];
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
a.id == alarmaId
|
||||
? _recalcular(a, config.vacaciones, excepciones)
|
||||
: a,
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
});
|
||||
final excepciones = [
|
||||
...config.excepciones,
|
||||
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: proxima, tipo: 'skipNext'),
|
||||
];
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: _recalcularLista(config.alarmas, config.vacaciones, excepciones),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> posponerEjecucion(
|
||||
String alarmaId,
|
||||
@@ -340,7 +341,7 @@ class ServicioAlarmas {
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
@@ -376,7 +377,7 @@ class ServicioAlarmas {
|
||||
);
|
||||
}).toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
@@ -459,6 +460,18 @@ class ServicioAlarmas {
|
||||
);
|
||||
}
|
||||
|
||||
// Every mutation that can shift "which alarm fires next" (create, edit,
|
||||
// activate/deactivate, delete, fire, snooze, skip) recalculates the whole
|
||||
// list, not just the alarm it touched directly. Siblings otherwise keep a
|
||||
// proximaEjecucion snapshot from the last periodic tick, which can be
|
||||
// stale (or already past-due) and wrongly outrank a freshly updated alarm
|
||||
// wherever proximaProgramable values are compared (EstadoAlarmas.proximaAlarma).
|
||||
List<AlarmaMusical> _recalcularLista(
|
||||
List<AlarmaMusical> alarmas,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) => alarmas.map((a) => _recalcular(a, vacaciones, excepciones)).toList();
|
||||
|
||||
Future<SharedPreferences> _resolverPrefs() async =>
|
||||
_prefs ?? SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// SharedPreferences spy: only the members ServicioAlarmas touches are
|
||||
/// implemented; everything else throws via noSuchMethod.
|
||||
class _PrefsEspia implements SharedPreferences {
|
||||
final Map<String, Object> _datos = {};
|
||||
|
||||
@override
|
||||
String? getString(String key) => _datos[key] as String?;
|
||||
|
||||
@override
|
||||
Future<bool> setString(String key, String value) async {
|
||||
_datos[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Regression for: activating/creating/deleting/firing an alarm must never
|
||||
// leave a sibling stuck with a stale (already past-due) proximaEjecucion
|
||||
// snapshot, since EstadoAlarmas.proximaAlarma just sorts by
|
||||
// proximaProgramable and a stale past value would incorrectly outrank a
|
||||
// freshly computed future one.
|
||||
test(
|
||||
'guardarAlarma recalcula la proximaEjecucion de TODAS las alarmas, no solo la guardada',
|
||||
() async {
|
||||
final prefs = _PrefsEspia();
|
||||
var reloj = DateTime(2026, 1, 1, 6, 0);
|
||||
final servicio = ServicioAlarmas(prefs: prefs, reloj: () => reloj);
|
||||
|
||||
final a = servicio.crearAlarma(
|
||||
nombre: 'A',
|
||||
hora: 7,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
);
|
||||
final configTrasA = await servicio.guardarAlarma(a);
|
||||
final aGuardada = configTrasA.alarmas.single;
|
||||
expect(aGuardada.proximaEjecucion, DateTime(2026, 1, 1, 7, 0));
|
||||
|
||||
// Time moves forward well past A's stored occurrence, simulating the
|
||||
// periodic 1-minute refresh not having ticked yet.
|
||||
reloj = DateTime(2026, 1, 2, 8, 0);
|
||||
|
||||
final b = servicio.crearAlarma(
|
||||
nombre: 'B',
|
||||
hora: 9,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
);
|
||||
final configTrasB = await servicio.guardarAlarma(b);
|
||||
|
||||
final aTrasGuardarB = configTrasB.alarmas.firstWhere(
|
||||
(alarma) => alarma.id == a.id,
|
||||
);
|
||||
expect(
|
||||
aTrasGuardarB.proximaEjecucion!.isAfter(reloj),
|
||||
isTrue,
|
||||
reason:
|
||||
'A debe recalcularse al guardar B, no quedar con el snapshot '
|
||||
'viejo (ya vencido) que la haria ganar la comparacion de '
|
||||
'"proxima alarma" por error',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user