leafittome/lib/features/plants/data/plant_diagnosis_service.dart
cschlaefke 9caaa2ef79 V3: Untersuchungshistorie je Pflanze
Jede Diagnose landet automatisch in households/{id}/plants/{id}/diagnoses;
das Pflanzen-Detail zeigt die Liste (Ampel-Icon, Kurzbefund, Datum),
Antippen öffnet das Ergebnis-Sheet mit Untersuchungsdatum, volle
Mitglieder können Einträge löschen. Rules: read/create für alle im
Haushalt (auch Sitter), delete nur member, update nie. Widget-Test
navigiert bis ins Sheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:55:07 +02:00

161 lines
5.3 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 'dart:convert';
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/firebase/firebase_providers.dart';
import '../../household/data/household_providers.dart';
/// Ergebnis der KI-Krankheits-Diagnose (Cloud Function diagnosePlant).
class DiagnosisResult {
const DiagnosisResult({
required this.healthy,
required this.matchesSpecies,
required this.summary,
required this.details,
required this.treatment,
required this.prevention,
});
final bool healthy;
/// false, wenn das Foto laut KI eindeutig eine andere Pflanzenart zeigt
/// als die im Profil hinterlegte (z. B. versehentlich falsches Foto).
final bool matchesSpecies;
final String summary;
final String details;
final String treatment;
final String prevention;
}
/// Ein gespeicherter Eintrag der Untersuchungshistorie
/// (households/{id}/plants/{id}/diagnoses).
class PlantDiagnosisEntry {
const PlantDiagnosisEntry({
required this.id,
required this.createdAt,
required this.result,
});
final String id;
/// null, solange der Server-Zeitstempel noch nicht bestätigt ist.
final DateTime? createdAt;
final DiagnosisResult result;
}
class PlantDiagnosisService {
PlantDiagnosisService(
this._functionsGetter,
this._firestore,
this._householdIdGetter,
);
// Lazy, damit Widget-Tests ohne initialisiertes Firebase auskommen —
// FirebaseFunctions.instanceFor wirft sonst schon beim Erzeugen.
final FirebaseFunctions Function() _functionsGetter;
final FirebaseFirestore _firestore;
final String? Function() _householdIdGetter;
CollectionReference<Map<String, dynamic>> _diagnosesRef(
String householdId,
String plantId,
) {
return _firestore
.collection('households')
.doc(householdId)
.collection('plants')
.doc(plantId)
.collection('diagnoses');
}
/// Schickt das Foto an die Cloud Function (Claude) dauert einige Sekunden.
/// [speciesHint] ist die bekannte Art der Pflanze und hilft der Diagnose.
Future<DiagnosisResult> diagnose(
Uint8List imageBytes, {
String? speciesHint,
}) async {
final callable = _functionsGetter().httpsCallable('diagnosePlant');
final response = await callable.call<Map<String, dynamic>>({
'imageBase64': base64Encode(imageBytes),
if (speciesHint != null && speciesHint.trim().isNotEmpty)
'speciesHint': speciesHint.trim(),
});
final data = response.data;
return DiagnosisResult(
healthy: data['healthy'] as bool? ?? false,
matchesSpecies: data['matchesSpecies'] as bool? ?? true,
summary: data['summary'] as String? ?? '',
details: data['details'] as String? ?? '',
treatment: data['treatment'] as String? ?? '',
prevention: data['prevention'] as String? ?? '',
);
}
/// Legt einen Eintrag in der Untersuchungshistorie der Pflanze an.
Future<void> saveDiagnosis(String plantId, DiagnosisResult result) async {
final householdId = _householdIdGetter();
if (householdId == null) return;
await _diagnosesRef(householdId, plantId).add({
'healthy': result.healthy,
'matchesSpecies': result.matchesSpecies,
'summary': result.summary,
'details': result.details,
'treatment': result.treatment,
'prevention': result.prevention,
'createdAt': FieldValue.serverTimestamp(),
});
}
/// Entfernt einen Eintrag aus der Historie (nur volle Mitglieder).
Future<void> deleteDiagnosis(String plantId, String diagnosisId) async {
final householdId = _householdIdGetter();
if (householdId == null) return;
await _diagnosesRef(householdId, plantId).doc(diagnosisId).delete();
}
}
final plantDiagnosisServiceProvider = Provider<PlantDiagnosisService>((ref) {
return PlantDiagnosisService(
() => FirebaseFunctions.instanceFor(region: 'europe-west3'),
ref.watch(firestoreProvider),
() => ref.read(householdIdProvider).value,
);
});
/// Untersuchungshistorie einer Pflanze, neueste zuerst, live aus Firestore.
final plantDiagnosesProvider =
StreamProvider.family<List<PlantDiagnosisEntry>, String>((ref, plantId) {
final householdId = ref.watch(householdIdProvider).value;
if (householdId == null) return Stream.value(const []);
return ref
.watch(firestoreProvider)
.collection('households')
.doc(householdId)
.collection('plants')
.doc(plantId)
.collection('diagnoses')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) => [
for (final doc in snapshot.docs)
PlantDiagnosisEntry(
id: doc.id,
createdAt: (doc.data()['createdAt'] as Timestamp?)?.toDate(),
result: DiagnosisResult(
healthy: doc.data()['healthy'] as bool? ?? false,
matchesSpecies:
doc.data()['matchesSpecies'] as bool? ?? true,
summary: doc.data()['summary'] as String? ?? '',
details: doc.data()['details'] as String? ?? '',
treatment: doc.data()['treatment'] as String? ?? '',
prevention: doc.data()['prevention'] as String? ?? '',
),
),
]);
});