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>
159 lines
5.8 KiB
Dart
159 lines
5.8 KiB
Dart
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;
|
||
});
|