- Einladen per einmaligem 6-stelligem Code (7 Tage gültig), wahlweise als Mitglied oder Pflanzen-Sitter - Beitritt über Cloud Function joinHousehold (Transaktion, Admin-Rechte) - Security Rules: Sitter dürfen an Pflanzen nur Bestätigungs-Felder ändern; invites nur erstellen, nie lesen - Haushalts-Screen: Mitgliederliste mit Rollen, Einladen, Beitreten mit Wechsel-Warnung - UI-Gating: Sitter sehen keine Anlegen-/Bearbeiten-/Löschen-Aktionen - Bestätigungen speichern den Namen (lastWateredBy/lastFertilizedBy), Anzeige im Profil - Tests: Members-Map im Seed, neuer Sitter-Test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
4.4 KiB
Dart
128 lines
4.4 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?,
|
||
);
|
||
}
|
||
|
||
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,
|
||
};
|
||
}
|
||
|
||
/// Alle Pflanzen des Haushalts, live aus Firestore.
|
||
/// Leere Liste, solange kein Haushalt geladen ist (z. B. direkt nach Login).
|
||
final plantsProvider = StreamProvider<List<Plant>>((ref) {
|
||
final householdId = ref.watch(householdIdProvider).value;
|
||
if (householdId == null) return Stream.value(const []);
|
||
|
||
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()),
|
||
]);
|
||
});
|
||
|
||
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).
|
||
Future<void> confirmTask(String plantId, CareTaskType type) {
|
||
final field = switch (type) {
|
||
CareTaskType.watering => 'lastWatered',
|
||
CareTaskType.fertilizing => 'lastFertilized',
|
||
};
|
||
return _plants.doc(plantId).update({
|
||
field: Timestamp.now(),
|
||
'${field}By': _userLabelGetter(),
|
||
});
|
||
}
|
||
}
|
||
|
||
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;
|
||
},
|
||
);
|
||
});
|