leafittome/lib/features/plants/data/plants_provider.dart
cschlaefke 1b2c880c89 V2: Haushalt teilen – Einladungscodes, Mitglieder- und Sitter-Rolle, sichtbare Bestätigungen
- 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>
2026-07-19 19:14:34 +02:00

128 lines
4.4 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?,
);
}
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;
},
);
});