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>
This commit is contained in:
parent
5e39ec8118
commit
e81ce48543
7 changed files with 107 additions and 24 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Handoff: LeafItToMe (Pflanzenpflege-App)
|
||||
|
||||
Stand: 2026-07-24 (Session-Ende). Sprache mit dem Nutzer (Chris): **Deutsch**.
|
||||
Stand: 2026-08-19 (Session-Ende). Sprache mit dem Nutzer (Chris): **Deutsch**.
|
||||
|
||||
## Projekt in einem Satz
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
- **Apple-Login: implementiert, Bug behoben, vom Nutzer bestätigt** (2026-07-23). Der Bug (neuer Apple-Account konnte nichts speichern) hatte zwei Schichten: Bootstrap hing am Login-Aufruf und wurde vom Router-Redirect überholt; UND die Push-Registrierung hatte das users-Doc ohne `householdId` vorab angelegt, weshalb ein Existenz-Check nicht reichte. Fix: reaktiver Bootstrap + `householdId`-Maßstab + merge-Writes; selbstheilend für kaputte Accounts. Zusätzlich beseitigt: Riverpod-3-Warnung „markNeedsBuild during build" (BootstrapGate als Stack-Overlay statt Baum-Austausch + `checklistWarmupProvider` in `app.dart` gegen das Pausieren unbeobachteter Provider).
|
||||
- **Google-Login: implementiert, deployt und auf Gerät getestet** (2026-07-23 implementiert, Gerätetest 2026-07-24 bestätigt; `firebase-einrichtung.md` Schritt 8; Debug-SHA-1/SHA-256 per `firebase apps:android:sha:create` hinterlegt — der SHA-Konsolen-Hinweis betrifft nur den nativen Flow und ist für unseren Browser-Flow irrelevant).
|
||||
- **V3 Baustein 1 — Krankheits-Diagnose per Foto: komplett, deployt, vom Nutzer bestätigt** („sieht richtig gut aus"). Function `diagnosePlant` + „Pflanze untersuchen" im Pflanzen-Detail (Kamera/Galerie → Ergebnis-Sheet mit Ampel-Icon, Behandlung, Vorbeugung, KI-Disclaimer; `diagnosis_sheet.dart` gemeinsam genutzt). Nachbesserungen aus Live-Tests, alle bestätigt: Art-Angabe ist unbestätigter Hinweis (`matchesSpecies=false` → Warnkasten „Foto zeigt andere Pflanze"), kein HTML/Markdown im Text, konsequentes Duzen. **Untersuchungshistorie** unter `households/{id}/plants/{id}/diagnoses` (Rules: read/create alle im Haushalt, delete nur member, update nie), auf eigener Seite `/plants/:id/diagnoses`; Button „Frühere Untersuchungen" im Detail ist bei leerer Historie deaktiviert. **Historie-Seite auf Gerät getestet und bestätigt** (2026-07-24).
|
||||
- **V3 Baustein 2 — Umtopf-Erinnerungen: implementiert und deployt** (2026-07-23, Commit `81d990d`). Dritter Aufgabentyp `repotting`, als Einziger opt-in (`repottingIntervalMonths` optional) und in Kalender-Monaten (31. → Monatsletzter; Logik gespiegelt in `due_tasks_provider` und `reminders.ts`). Formular mit Intervall-Feld + „Zuletzt umgetopft"-Datum, Checkliste + Push („Umtopfen: …"), Sitter dürfen bestätigen. Rules + `sendDailyReminders` sind deployt. **Gerätetest steht aus.**
|
||||
- **V3 Baustein 2 — Umtopf-Erinnerungen: implementiert und deployt** (2026-07-23, Commit `81d990d`). Dritter Aufgabentyp `repotting`, als Einziger opt-in (`repottingIntervalMonths` optional) und in Kalender-Monaten (31. → Monatsletzter; Logik gespiegelt in `due_tasks_provider` und `reminders.ts`). Formular mit Intervall-Feld + „Zuletzt umgetopft"-Datum, Checkliste + Push („Umtopfen: …"), Sitter dürfen bestätigen. Rules + `sendDailyReminders` sind deployt. **Gerätetest bestätigt** (2026-08-19): Stellplatz-Bewertung „super", Umtopf-Flow selbst funktionierte, aber Chris vermisste eine Anzeige, *wann* das nächste Umtopfen ansteht (das Formularfeld war nicht das Problem). Nachgebessert: Pflanzen-Detailseite zeigt jetzt bei Gießen/Düngen/Umtopfen zusätzlich zum letzten Datum das **nächste Fälligkeitsdatum** an (bzw. „heute fällig"). Berechnung dafür aus `due_tasks_provider.dart` public gemacht (`nextWateringDue`/`nextFertilizingDue`/`nextRepottingDue`) statt dupliziert. Neue l10n-Keys `nextWatering`/`nextFertilizing`/`nextRepotting`/`dueToday`. Zwei Tests ergänzt (Stellplatz- und Umtopf-Test prüfen jetzt auch die neue Anzeige). **Gerätetest für diese Nachbesserung steht noch aus.**
|
||||
- **V3 Baustein 3 — Stellplatz-Bewertung: implementiert und deployt** (2026-07-24, Functions `analyzeLocation`/`assessPlantFit` + Firestore-Rules live). Konzept mit dem Nutzer abgestimmt: Standort-Daten nur per Foto (keine Zusatzfragen zu Himmelsrichtung), Eignungs-Sterne ausschließlich auf Abruf (Kostenkontrolle, eigener Anthropic-Key). Zwei Functions `analyzeLocation` (Foto → Lichtkategorie + Freitext, gespeichert auf `PlantLocation`) und `assessPlantFit` (textbasiert, Pflegeprofil + Standort-Analyse → 1-5 Sterne + Begründung, gespeichert auf `Plant`). Neue Seite `LocationDetailScreen` (`/locations/:id`, erreichbar per Tap in der Stellplätze-Liste): Foto/Analyse-Bereich (nur Mitglieder) + Liste zugeordneter Pflanzen mit Sternen oder „Eignung prüfen"-Button (auch für Sitter, informativ). Pflanzen-Detail zeigt den Stellplatz jetzt kompakt mit Sternen und verlinkt dorthin. Veraltete Bewertungen (Pflanze verschoben oder Stellplatz neu analysiert) werden erkannt und als solche markiert statt falscher Sterne. Firestore-Rules erweitert (Sitter dürfen die vier Fit-Felder schreiben). Details/Begründung der Entscheidungen in `docs/architektur.md`. **Gerätetest steht aus** (frisch angelegte Functions können beim allerersten Aufruf HTTP 401 liefern, siehe Stolperfallen).
|
||||
- Testabdeckung: 16 Widget-Tests grün (u. a. Bootstrap-Heilung, Untersuchungshistorie bis ins Sheet, Umtopf-Flow Ende-zu-Ende, Stellplatz-Bewertung inkl. Veraltet-Erkennung), Analyzer sauber, `tsc` kompiliert, Android-Debug-Build ok.
|
||||
|
||||
|
|
@ -31,8 +31,8 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
|
||||
### 1. Ausstehende Gerätetests (Nutzer, bei Rückmeldung ggf. nachbessern)
|
||||
|
||||
- **Stellplatz-Bewertung:** Stellplatz öffnen → Foto aufnehmen → Licht-Analyse erscheint (Kategorie-Chip + Text); bei einer zugeordneten Pflanze „Eignung prüfen" → Sterne + Begründung erscheinen; Pflanze auf anderen Stellplatz verschieben → alte Sterne werden als veraltet markiert; Pflanzen-Detail zeigt den Stellplatz samt Sternen kompakt an und verlinkt zur Stellplatz-Seite.
|
||||
- **Umtopf-Flow:** Pflanze bearbeiten → Intervall 12 Monate + „Zuletzt umgetopft" vor >1 Jahr → Aufgabe „… umtopfen" in Checkliste → Erledigt; Push-Text „Umtopfen: …" beim nächsten regulären Slot.
|
||||
- **Nächstes-Fälligkeitsdatum auf der Pflanzen-Detailseite** (neu, 2026-08-19): Pflanze öffnen → bei Gießen/Düngen/Umtopfen erscheint jetzt zusätzlich zum letzten Datum eine zweite Zeile „Nächstes Gießen/Düngen/Umtopfen: …" (bzw. „heute fällig").
|
||||
- **Umtopf-Flow — Push-Text:** „Umtopfen: …" beim nächsten regulären Push-Slot (Checkliste + Bestätigen sind bereits bestätigt).
|
||||
|
||||
### 2. Backlog
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import '../../household/data/household_providers.dart';
|
|||
import '../../household/domain/household.dart';
|
||||
import '../../locations/data/locations_provider.dart';
|
||||
import '../../locations/domain/plant_location.dart';
|
||||
import '../../today/application/due_tasks_provider.dart';
|
||||
import '../data/plant_diagnosis_service.dart';
|
||||
import '../data/plant_fit_service.dart';
|
||||
import '../data/plants_provider.dart';
|
||||
|
|
@ -45,6 +46,11 @@ class PlantDetailScreen extends ConsumerWidget {
|
|||
String formatDate(DateTime? date) =>
|
||||
date != null ? dateFormat.format(date) : l10n.never;
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
String formatNextDue(DateTime date) =>
|
||||
date.isAfter(today) ? dateFormat.format(date) : l10n.dueToday;
|
||||
|
||||
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
|
||||
|
||||
return Scaffold(
|
||||
|
|
@ -119,27 +125,24 @@ class PlantDetailScreen extends ConsumerWidget {
|
|||
_InfoTile(
|
||||
icon: Icons.water_drop,
|
||||
label: l10n.wateringEvery(plant.wateringIntervalDays),
|
||||
value: l10n.lastWatered(formatDate(plant.lastWatered)) +
|
||||
(plant.lastWateredBy != null
|
||||
? l10n.doneBy(plant.lastWateredBy!)
|
||||
: ''),
|
||||
value: '${l10n.lastWatered(formatDate(plant.lastWatered))}'
|
||||
'${plant.lastWateredBy != null ? l10n.doneBy(plant.lastWateredBy!) : ''}'
|
||||
'\n${l10n.nextWatering(formatNextDue(nextWateringDue(plant, today)!))}',
|
||||
),
|
||||
_InfoTile(
|
||||
icon: Icons.compost,
|
||||
label: l10n.fertilizingEvery(plant.fertilizingIntervalDays),
|
||||
value: l10n.lastFertilized(formatDate(plant.lastFertilized)) +
|
||||
(plant.lastFertilizedBy != null
|
||||
? l10n.doneBy(plant.lastFertilizedBy!)
|
||||
: ''),
|
||||
value: '${l10n.lastFertilized(formatDate(plant.lastFertilized))}'
|
||||
'${plant.lastFertilizedBy != null ? l10n.doneBy(plant.lastFertilizedBy!) : ''}'
|
||||
'\n${l10n.nextFertilizing(formatNextDue(nextFertilizingDue(plant, today)!))}',
|
||||
),
|
||||
if (plant.repottingIntervalMonths != null)
|
||||
_InfoTile(
|
||||
icon: Icons.yard,
|
||||
label: l10n.repottingEvery(plant.repottingIntervalMonths!),
|
||||
value: l10n.lastRepotted(formatDate(plant.lastRepotted)) +
|
||||
(plant.lastRepottedBy != null
|
||||
? l10n.doneBy(plant.lastRepottedBy!)
|
||||
: ''),
|
||||
value: '${l10n.lastRepotted(formatDate(plant.lastRepotted))}'
|
||||
'${plant.lastRepottedBy != null ? l10n.doneBy(plant.lastRepottedBy!) : ''}'
|
||||
'\n${l10n.nextRepotting(formatNextDue(nextRepottingDue(plant, today)!))}',
|
||||
),
|
||||
if (plant.description.isNotEmpty) ...[
|
||||
const Divider(height: 32),
|
||||
|
|
|
|||
|
|
@ -25,8 +25,16 @@ DateTime _addMonths(DateTime date, int months) {
|
|||
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? _nextRepotDue(Plant plant, DateTime today) {
|
||||
DateTime? nextRepottingDue(Plant plant, DateTime today) {
|
||||
final months = plant.repottingIntervalMonths;
|
||||
if (months == null) return null;
|
||||
if (plant.lastRepotted == null) return today;
|
||||
|
|
@ -35,7 +43,7 @@ DateTime? _nextRepotDue(Plant plant, DateTime today) {
|
|||
|
||||
Iterable<DueTask> _tasksForPlant(
|
||||
Plant plant, Household household, DateTime today) sync* {
|
||||
final watering = _nextDue(plant.lastWatered, plant.wateringIntervalDays, today);
|
||||
final watering = nextWateringDue(plant, today);
|
||||
if (watering != null && !watering.isAfter(today)) {
|
||||
yield DueTask(
|
||||
plant: plant,
|
||||
|
|
@ -45,8 +53,7 @@ Iterable<DueTask> _tasksForPlant(
|
|||
householdName: household.name,
|
||||
);
|
||||
}
|
||||
final fertilizing =
|
||||
_nextDue(plant.lastFertilized, plant.fertilizingIntervalDays, today);
|
||||
final fertilizing = nextFertilizingDue(plant, today);
|
||||
if (fertilizing != null && !fertilizing.isAfter(today)) {
|
||||
yield DueTask(
|
||||
plant: plant,
|
||||
|
|
@ -56,7 +63,7 @@ Iterable<DueTask> _tasksForPlant(
|
|||
householdName: household.name,
|
||||
);
|
||||
}
|
||||
final repotting = _nextRepotDue(plant, today);
|
||||
final repotting = nextRepottingDue(plant, today);
|
||||
if (repotting != null && !repotting.isAfter(today)) {
|
||||
yield DueTask(
|
||||
plant: plant,
|
||||
|
|
@ -139,9 +146,9 @@ final nextDueDateProvider = Provider<DateTime?>((ref) {
|
|||
ref.watch(householdPlantsProvider(household.id)).value ?? const [];
|
||||
for (final plant in plants) {
|
||||
for (final candidate in [
|
||||
_nextDue(plant.lastWatered, plant.wateringIntervalDays, today),
|
||||
_nextDue(plant.lastFertilized, plant.fertilizingIntervalDays, today),
|
||||
_nextRepotDue(plant, today),
|
||||
nextWateringDue(plant, today),
|
||||
nextFertilizingDue(plant, today),
|
||||
nextRepottingDue(plant, today),
|
||||
]) {
|
||||
if (candidate == null || !candidate.isAfter(today)) continue;
|
||||
if (next == null || candidate.isBefore(next)) next = candidate;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,25 @@
|
|||
"date": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nextWatering": "Nächstes Gießen: {date}",
|
||||
"@nextWatering": {
|
||||
"placeholders": {
|
||||
"date": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nextFertilizing": "Nächstes Düngen: {date}",
|
||||
"@nextFertilizing": {
|
||||
"placeholders": {
|
||||
"date": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nextRepotting": "Nächstes Umtopfen: {date}",
|
||||
"@nextRepotting": {
|
||||
"placeholders": {
|
||||
"date": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"dueToday": "heute fällig",
|
||||
"never": "noch nie",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
|
|
|
|||
|
|
@ -328,6 +328,30 @@ abstract class AppLocalizations {
|
|||
/// **'Zuletzt gedüngt: {date}'**
|
||||
String lastFertilized(String date);
|
||||
|
||||
/// No description provided for @nextWatering.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nächstes Gießen: {date}'**
|
||||
String nextWatering(String date);
|
||||
|
||||
/// No description provided for @nextFertilizing.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nächstes Düngen: {date}'**
|
||||
String nextFertilizing(String date);
|
||||
|
||||
/// No description provided for @nextRepotting.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nächstes Umtopfen: {date}'**
|
||||
String nextRepotting(String date);
|
||||
|
||||
/// No description provided for @dueToday.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'heute fällig'**
|
||||
String get dueToday;
|
||||
|
||||
/// No description provided for @never.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
|
|
|
|||
|
|
@ -165,6 +165,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return 'Zuletzt gedüngt: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String nextWatering(String date) {
|
||||
return 'Nächstes Gießen: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String nextFertilizing(String date) {
|
||||
return 'Nächstes Düngen: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String nextRepotting(String date) {
|
||||
return 'Nächstes Umtopfen: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String get dueToday => 'heute fällig';
|
||||
|
||||
@override
|
||||
String get never => 'noch nie';
|
||||
|
||||
|
|
|
|||
|
|
@ -521,6 +521,9 @@ void main() {
|
|||
await tester.tap(find.text('Monstera'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Fensterbank · ★★★★☆'), findsOneWidget);
|
||||
// Nächste Fälligkeit wird zusätzlich zum letzten Datum angezeigt.
|
||||
expect(find.textContaining('Nächstes Gießen:'), findsOneWidget);
|
||||
expect(find.textContaining('Nächstes Düngen:'), findsOneWidget);
|
||||
|
||||
// Antippen navigiert zur Stellplatz-Detailseite mit voller Analyse.
|
||||
await tester.tap(find.textContaining('Fensterbank · ★★★★☆'));
|
||||
|
|
@ -763,6 +766,15 @@ void main() {
|
|||
final lastRepotted = (doc.data()?['lastRepotted'] as Timestamp).toDate();
|
||||
expect(now.difference(lastRepotted).inDays, 0);
|
||||
expect(doc.data()?['lastRepottedBy'], 'test');
|
||||
|
||||
// Detailseite zeigt jetzt zusätzlich die nächste Fälligkeit an.
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Meine Pflanzen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Monstera'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Nächstes Umtopfen:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue