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>
172 lines
6 KiB
Dart
172 lines
6 KiB
Dart
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;
|
||
},
|
||
);
|
||
});
|