leafittome/lib/features/plants/data/plants_provider.dart
cschlaefke f0b420b656 V3: Stellplatz-Bewertung (Standort-Analyse per Foto + Eignungs-Sterne auf Abruf)
Letzter Roadmap-Punkt von V3. Foto-basierte Lichtverhältnis-Analyse pro
Stellplatz (Cloud Function analyzeLocation) und darauf aufbauende, rein
textbasierte Eignungs-Bewertung einer Pflanze (assessPlantFit) — beides
ausschließlich auf Abruf, nicht automatisch (Kostenkontrolle, eigener
Anthropic-Key). Neue Stellplatz-Detailseite, Verlinkung im Pflanzen-Detail,
Erkennung veralteter Bewertungen bei Umzug/Neuanalyse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:17:18 +02:00

172 lines
6 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:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/firebase/firebase_providers.dart';
import '../../household/data/household_providers.dart';
import '../../today/domain/due_task.dart';
import '../domain/plant.dart';
/// Firestore-Anbindung der Pflanzen:
/// households/{householdId}/plants/{plantId}
Plant _plantFromDoc(String id, Map<String, dynamic> data) {
return Plant(
id: id,
nickname: data['nickname'] as String? ?? '',
species: data['species'] as String? ?? '',
locationId: data['locationId'] as String?,
description: data['description'] as String? ?? '',
careNotes: data['careNotes'] as String? ?? '',
photoUrl: data['photoUrl'] as String?,
wateringIntervalDays: (data['wateringIntervalDays'] as num?)?.toInt() ?? 7,
fertilizingIntervalDays:
(data['fertilizingIntervalDays'] as num?)?.toInt() ?? 28,
lastWatered: (data['lastWatered'] as Timestamp?)?.toDate(),
lastFertilized: (data['lastFertilized'] as Timestamp?)?.toDate(),
lastWateredBy: data['lastWateredBy'] as String?,
lastFertilizedBy: data['lastFertilizedBy'] as String?,
repottingIntervalMonths:
(data['repottingIntervalMonths'] as num?)?.toInt(),
lastRepotted: (data['lastRepotted'] as Timestamp?)?.toDate(),
lastRepottedBy: data['lastRepottedBy'] as String?,
fitStars: (data['fitStars'] as num?)?.toInt(),
fitReasoning: data['fitReasoning'] as String?,
fitLocationId: data['fitLocationId'] as String?,
fitAssessedAt: (data['fitAssessedAt'] as Timestamp?)?.toDate(),
);
}
Map<String, dynamic> _plantToMap(Plant plant) {
return {
'nickname': plant.nickname,
'species': plant.species,
'locationId': plant.locationId,
'description': plant.description,
'careNotes': plant.careNotes,
'photoUrl': plant.photoUrl,
'wateringIntervalDays': plant.wateringIntervalDays,
'fertilizingIntervalDays': plant.fertilizingIntervalDays,
'lastWatered':
plant.lastWatered != null ? Timestamp.fromDate(plant.lastWatered!) : null,
'lastFertilized': plant.lastFertilized != null
? Timestamp.fromDate(plant.lastFertilized!)
: null,
'lastWateredBy': plant.lastWateredBy,
'lastFertilizedBy': plant.lastFertilizedBy,
'repottingIntervalMonths': plant.repottingIntervalMonths,
'lastRepotted': plant.lastRepotted != null
? Timestamp.fromDate(plant.lastRepotted!)
: null,
'lastRepottedBy': plant.lastRepottedBy,
};
}
/// Alle Pflanzen eines bestimmten Haushalts, live aus Firestore.
/// Die Tages-Checkliste nutzt das für jeden Haushalt des Nutzers.
final householdPlantsProvider =
StreamProvider.family<List<Plant>, String>((ref, householdId) {
return ref
.watch(firestoreProvider)
.collection('households')
.doc(householdId)
.collection('plants')
.orderBy('nickname')
.snapshots()
.map((snapshot) => [
for (final doc in snapshot.docs) _plantFromDoc(doc.id, doc.data()),
]);
});
/// Die Pflanzen des gerade aktiven Haushalts.
/// Leere Liste, solange kein Haushalt geladen ist (z. B. direkt nach Login).
final plantsProvider = Provider<AsyncValue<List<Plant>>>((ref) {
final householdId = ref.watch(householdIdProvider).value;
if (householdId == null) return const AsyncValue.data([]);
return ref.watch(householdPlantsProvider(householdId));
});
final plantByIdProvider = Provider.family<Plant?, String>((ref, id) {
final plants = ref.watch(plantsProvider).value ?? const [];
for (final plant in plants) {
if (plant.id == id) return plant;
}
return null;
});
class PlantRepository {
PlantRepository(this._firestore, this._householdIdGetter, this._userLabelGetter);
final FirebaseFirestore _firestore;
final String? Function() _householdIdGetter;
/// Anzeigename des angemeldeten Nutzers für "bestätigt von".
final String? Function() _userLabelGetter;
CollectionReference<Map<String, dynamic>> get _plants {
final householdId = _householdIdGetter();
if (householdId == null) {
throw StateError('Kein Haushalt geladen Aktion nicht möglich.');
}
return _firestore
.collection('households')
.doc(householdId)
.collection('plants');
}
Future<void> addPlant(Plant plant) => _plants.add(_plantToMap(plant));
Future<void> updatePlant(Plant plant) =>
_plants.doc(plant.id).update(_plantToMap(plant));
Future<void> removePlant(String plantId) => _plants.doc(plantId).delete();
/// Bestätigt eine Aufgabe: setzt das Erledigungsdatum, wodurch sich die
/// nächste Fälligkeit automatisch neu berechnet (Intervall-Modell).
/// [householdId] kommt aus der Aufgabe die Checkliste zeigt Aufgaben
/// aus allen Haushalten, nicht nur dem aktiven.
Future<void> confirmTask(
String householdId, String plantId, CareTaskType type) {
final field = switch (type) {
CareTaskType.watering => 'lastWatered',
CareTaskType.fertilizing => 'lastFertilized',
CareTaskType.repotting => 'lastRepotted',
};
return _firestore
.collection('households')
.doc(householdId)
.collection('plants')
.doc(plantId)
.update({
field: Timestamp.now(),
'${field}By': _userLabelGetter(),
});
}
/// Speichert das Ergebnis einer Eignungs-Bewertung (V3, auf Abruf) für
/// den aktuell zugeordneten Stellplatz.
Future<void> saveFitAssessment(
String plantId, {
required int stars,
required String reasoning,
required String locationId,
}) {
return _plants.doc(plantId).update({
'fitStars': stars,
'fitReasoning': reasoning,
'fitLocationId': locationId,
'fitAssessedAt': Timestamp.now(),
});
}
}
final plantRepositoryProvider = Provider<PlantRepository>((ref) {
return PlantRepository(
ref.watch(firestoreProvider),
() => ref.read(householdIdProvider).value,
() {
final email = ref.read(firebaseAuthProvider).currentUser?.email;
// "chris@example.com" → "chris" als Anzeigename.
return email?.split('@').first;
},
);
});