leafittome/lib/features/household/data/household_providers.dart
cschlaefke 778b3bd34d V2.2: Besitzer-Konzept, Haushalte umbenennen, Austreten/Entfernen, Checkliste über alle Haushalte
- ownerUid je Haushalt (Alt-Haushalte: erster memberUids-Eintrag = Ersteller);
  Besitzer-Anzeige in Mitglieder- und Haushaltsliste
- Haushalt umbenennen (Dialog im Haushalts-Screen); Rules erlauben Clients am
  Haushalts-Dokument nur noch das Namensfeld
- Cloud Functions leaveHousehold (selbst austreten, auch als Sitter) und
  removeMember (nur Besitzer); beide biegen den aktiven Zeiger des Betroffenen
  um und legen notfalls einen frischen eigenen Haushalt an
- Haushalts-Wechsler im Drawer (PopupMenu, ab zwei Haushalten)
- Tages-Checkliste und nächste Fälligkeit aggregieren über alle Haushalte,
  mit Zwischenüberschrift je Haushalt; Bestätigen schreibt in den
  Herkunfts-Haushalt der Aufgabe
- Widget-Tests: Gruppierung, Besitzer-Rechte, Umbenennen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:39:10 +02:00

189 lines
6.7 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:math';
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 '../domain/household.dart';
/// Die ID des **aktiven** Haushalts (aus users/{uid}.householdId).
///
/// Man kann Mitglied mehrerer Haushalte sein ([myHouseholdsProvider]);
/// dieser Zeiger bestimmt, welcher davon gerade in der App angezeigt wird.
/// null, solange niemand angemeldet ist oder das Profil noch lädt.
/// Alle Daten-Provider (Pflanzen, Stellplätze) hängen hieran.
final householdIdProvider = StreamProvider<String?>((ref) {
final user = ref.watch(authStateProvider).value;
if (user == null) return Stream.value(null);
return ref
.watch(firestoreProvider)
.collection('users')
.doc(user.uid)
.snapshots()
.map((snapshot) => snapshot.data()?['householdId'] as String?);
});
Household _householdFromData(String id, Map<String, dynamic> data) {
final membersMap = (data['members'] as Map<String, dynamic>?) ?? {};
final memberUids = (data['memberUids'] as List<dynamic>?) ?? [];
return Household(
id: id,
name: data['name'] as String? ?? 'Haushalt',
// Alt-Haushalte ohne ownerUid: Der Ersteller steht immer an erster
// Stelle von memberUids (Beitretende werden hinten angehängt).
ownerUid: data['ownerUid'] as String? ??
(memberUids.isNotEmpty ? memberUids.first as String : ''),
members: [
for (final uid in memberUids.cast<String>())
HouseholdMember(
uid: uid,
role: ((membersMap[uid] as Map<String, dynamic>?)?['role']
as String?) ==
'sitter'
? HouseholdRole.sitter
: HouseholdRole.member,
email: (membersMap[uid] as Map<String, dynamic>?)?['email']
as String? ??
'',
),
],
);
}
/// Der aktive Haushalt inkl. Mitgliedern und Rollen, live aus Firestore.
final householdProvider = StreamProvider<Household?>((ref) {
final householdId = ref.watch(householdIdProvider).value;
if (householdId == null) return Stream.value(null);
return ref
.watch(firestoreProvider)
.collection('households')
.doc(householdId)
.snapshots()
.map((snapshot) {
final data = snapshot.data();
if (data == null) return null;
return _householdFromData(snapshot.id, data);
});
});
/// Alle Haushalte, in denen der angemeldete Nutzer Mitglied ist.
///
/// Quelle der Wahrheit ist das Feld `memberUids` der Haushalte selbst
/// wer per Einladung beitritt, bleibt in seinen bisherigen Haushalten
/// Mitglied und kann jederzeit zurückwechseln.
final myHouseholdsProvider = StreamProvider<List<Household>>((ref) {
final user = ref.watch(authStateProvider).value;
if (user == null) return Stream.value(const []);
return ref
.watch(firestoreProvider)
.collection('households')
.where('memberUids', arrayContains: user.uid)
.snapshots()
.map((snapshot) {
final households = [
for (final doc in snapshot.docs) _householdFromData(doc.id, doc.data()),
]..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return households;
});
});
/// Rolle des angemeldeten Nutzers im aktuellen Haushalt.
final myRoleProvider = Provider<HouseholdRole>((ref) {
final household = ref.watch(householdProvider).value;
final uid = ref.watch(authStateProvider).value?.uid;
if (household == null || uid == null) return HouseholdRole.member;
return household.roleOf(uid);
});
class HouseholdRepository {
HouseholdRepository(this._firestore, this._functionsGetter, this._uidGetter);
final FirebaseFirestore _firestore;
// Lazy, damit Widget-Tests ohne initialisiertes Firebase auskommen
// FirebaseFunctions.instanceFor wirft sonst schon beim Erzeugen.
final FirebaseFunctions Function() _functionsGetter;
final String? Function() _uidGetter;
static const _codeAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
/// Erzeugt einen einmaligen Einladungscode (7 Tage gültig).
Future<String> createInvite({
required String householdId,
required HouseholdRole role,
}) async {
final random = Random.secure();
final code = List.generate(
6,
(_) => _codeAlphabet[random.nextInt(_codeAlphabet.length)],
).join();
await _firestore.collection('invites').doc(code).set({
'householdId': householdId,
'role': role.name,
'createdBy': _uidGetter(),
'createdAt': FieldValue.serverTimestamp(),
});
return code;
}
/// Macht einen anderen Haushalt zum aktiven (users/{uid}.householdId).
/// Die Mitgliedschaft in allen anderen Haushalten bleibt bestehen.
Future<void> switchHousehold(String householdId) async {
final uid = _uidGetter();
if (uid == null) return;
await _firestore
.collection('users')
.doc(uid)
.set({'householdId': householdId}, SetOptions(merge: true));
}
/// Löst einen Code ein und wechselt in den zugehörigen Haushalt.
/// Gibt den Namen des neuen Haushalts zurück.
Future<String> joinHousehold(String code) async {
final callable = _functionsGetter().httpsCallable('joinHousehold');
final response =
await callable.call<Map<String, dynamic>>({'code': code.trim()});
return response.data['householdName'] as String? ?? 'Haushalt';
}
/// Benennt einen Haushalt um (Rules: nur volle Mitglieder, nur das
/// Namensfeld).
Future<void> renameHousehold(String householdId, String name) {
return _firestore
.collection('households')
.doc(householdId)
.update({'name': name});
}
/// Tritt aus einem Haushalt aus, dem man per Einladung beigetreten ist.
/// Läuft als Cloud Function, weil auch Sitter austreten dürfen, die laut
/// Rules das Haushalts-Dokument nicht ändern können.
Future<void> leaveHousehold(String householdId) async {
final callable = _functionsGetter().httpsCallable('leaveHousehold');
await callable.call<Map<String, dynamic>>({'householdId': householdId});
}
/// Entfernt ein Mitglied aus dem Haushalt (nur der Besitzer).
Future<void> removeMember({
required String householdId,
required String memberUid,
}) async {
final callable = _functionsGetter().httpsCallable('removeMember');
await callable.call<Map<String, dynamic>>({
'householdId': householdId,
'memberUid': memberUid,
});
}
}
final householdRepositoryProvider = Provider<HouseholdRepository>((ref) {
return HouseholdRepository(
ref.watch(firestoreProvider),
() => FirebaseFunctions.instanceFor(region: 'europe-west3'),
() => ref.read(firebaseAuthProvider).currentUser?.uid,
);
});