leafittome/lib/features/today/application/due_tasks_provider.dart
cschlaefke e81ce48543 V3: Nächstes Fälligkeitsdatum bei Gießen/Düngen/Umtopfen in Pflanzen-Detail
Nutzer vermisste beim Umtopf-Test eine Info, wann das nächste Umtopfen
ansteht (Formular/Checkliste zeigten nur "alle X Monate"/"zuletzt am").
Berechnung aus due_tasks_provider public gemacht statt dupliziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 15:13:04 +02:00

159 lines
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../household/data/household_providers.dart';
import '../../household/domain/household.dart';
import '../../plants/data/plants_provider.dart';
import '../../plants/domain/plant.dart';
import '../domain/due_task.dart';
DateTime _dateOnly(DateTime dt) => DateTime(dt.year, dt.month, dt.day);
DateTime? _nextDue(DateTime? lastDone, int intervalDays, DateTime today) {
// Noch nie erledigt → sofort fällig.
if (lastDone == null) return today;
return _dateOnly(lastDone).add(Duration(days: intervalDays));
}
/// Kalendermonate addieren; ein 31. wird notfalls auf den Monatsletzten
/// gekürzt (31. Jan + 1 Monat → 28./29. Feb).
DateTime _addMonths(DateTime date, int months) {
final totalMonths = date.month - 1 + months;
final year = date.year + totalMonths ~/ 12;
final month = totalMonths % 12 + 1;
final lastDayOfMonth = DateTime(year, month + 1, 0).day;
return DateTime(
year, month, date.day > lastDayOfMonth ? lastDayOfMonth : date.day);
}
/// Fälligkeit fürs Gießen.
DateTime? nextWateringDue(Plant plant, DateTime today) =>
_nextDue(plant.lastWatered, plant.wateringIntervalDays, today);
/// Fälligkeit fürs Düngen.
DateTime? nextFertilizingDue(Plant plant, DateTime today) =>
_nextDue(plant.lastFertilized, plant.fertilizingIntervalDays, today);
/// Fälligkeit fürs Umtopfen — nur wenn ein Intervall gesetzt ist (opt-in).
DateTime? nextRepottingDue(Plant plant, DateTime today) {
final months = plant.repottingIntervalMonths;
if (months == null) return null;
if (plant.lastRepotted == null) return today;
return _addMonths(_dateOnly(plant.lastRepotted!), months);
}
Iterable<DueTask> _tasksForPlant(
Plant plant, Household household, DateTime today) sync* {
final watering = nextWateringDue(plant, today);
if (watering != null && !watering.isAfter(today)) {
yield DueTask(
plant: plant,
type: CareTaskType.watering,
dueDate: watering,
householdId: household.id,
householdName: household.name,
);
}
final fertilizing = nextFertilizingDue(plant, today);
if (fertilizing != null && !fertilizing.isAfter(today)) {
yield DueTask(
plant: plant,
type: CareTaskType.fertilizing,
dueDate: fertilizing,
householdId: household.id,
householdName: household.name,
);
}
final repotting = nextRepottingDue(plant, today);
if (repotting != null && !repotting.isAfter(today)) {
yield DueTask(
plant: plant,
type: CareTaskType.repotting,
dueDate: repotting,
householdId: household.id,
householdName: household.name,
);
}
}
/// Alle heute fälligen und überfälligen Aufgaben aus **allen** Haushalten
/// des Nutzers. Sortiert nach Haushalt (aktiver zuerst), darin Überfälliges
/// zuerst so gruppiert die Checkliste ohne weitere Logik.
final dueTasksProvider = Provider<List<DueTask>>((ref) {
final households = ref.watch(myHouseholdsProvider).value ?? const [];
final activeId = ref.watch(householdIdProvider).value;
final today = _dateOnly(DateTime.now());
final ordered = [...households]..sort((a, b) {
if (a.id == activeId) return -1;
if (b.id == activeId) return 1;
return 0; // myHouseholdsProvider liefert bereits nach Name sortiert.
});
final tasks = <DueTask>[];
for (final household in ordered) {
final plants =
ref.watch(householdPlantsProvider(household.id)).value ?? const [];
final householdTasks = [
for (final plant in plants) ..._tasksForPlant(plant, household, today),
]..sort((a, b) {
final byDate = a.dueDate.compareTo(b.dueDate);
if (byDate != 0) return byDate;
return a.plant.nickname.compareTo(b.plant.nickname);
});
tasks.addAll(householdTasks);
}
return tasks;
});
/// True, solange die Haushalte oder deren Pflanzen noch erstmalig laden
/// damit die Checkliste nicht kurz „Alles versorgt“ zeigt, bevor Daten da sind.
final dueTasksLoadingProvider = Provider<bool>((ref) {
final householdsAsync = ref.watch(myHouseholdsProvider);
if (householdsAsync.isLoading && !householdsAsync.hasValue) return true;
final households = householdsAsync.value ?? const [];
for (final household in households) {
if (!ref.watch(householdPlantsProvider(household.id)).hasValue) {
return true;
}
}
return false;
});
/// Hält die Provider-Kette der Checkliste dauerhaft aktiv (via app.dart).
///
/// Riverpod 3 pausiert Provider ohne aktive Zuhörer. Wechselt der Router
/// von Login auf die Heute-Ansicht, würde deren Einhängen die pausierte,
/// inzwischen veraltete Kette mitten im Widget-Build wiederbeleben — das
/// löst „setState() or markNeedsBuild() called during build“ aus. Mit einem
/// dauerhaften Zuhörer läuft die Kette stattdessen sofort mit und ist beim
/// Einhängen der Screens bereits frisch.
final checklistWarmupProvider = Provider<void>((ref) {
ref.watch(dueTasksProvider);
ref.watch(dueTasksLoadingProvider);
ref.watch(nextDueDateProvider);
ref.watch(myRoleProvider);
});
/// Das nächste zukünftige Fälligkeitsdatum über alle Haushalte für den
/// "Alles versorgt"-Screen ("Nächste Aufgabe: Freitag").
final nextDueDateProvider = Provider<DateTime?>((ref) {
final households = ref.watch(myHouseholdsProvider).value ?? const [];
final today = _dateOnly(DateTime.now());
DateTime? next;
for (final household in households) {
final plants =
ref.watch(householdPlantsProvider(household.id)).value ?? const [];
for (final plant in plants) {
for (final candidate in [
nextWateringDue(plant, today),
nextFertilizingDue(plant, today),
nextRepottingDue(plant, today),
]) {
if (candidate == null || !candidate.isAfter(today)) continue;
if (next == null || candidate.isBefore(next)) next = candidate;
}
}
}
return next;
});