leafittome/lib/features/household/data/household_providers.dart
cschlaefke 1ca02f224b V2.1: Mehrere Haushalte je Nutzer – Mitgliedschaft bleibt beim Beitritt erhalten
- memberUids ist Quelle der Wahrheit, users.householdId nur noch aktiver Zeiger
- Haushalts-Screen: Liste „Meine Haushalte" mit Rolle, Aktiv-Markierung und Wechsel per Tipp
- Beitritts-Dialog warnt nicht mehr vor Datenverlust, sondern erklärt das Zurückwechseln
- sendDailyReminders sammelt fällige Aufgaben aus allen Haushalten des Nutzers
- FirebaseFunctions im HouseholdRepository lazy erzeugt (Widget-Tests ohne Firebase-Init)
- Drawer-Titel mit Ellipsis gegen Overflow auf schmalen Breiten
- Widget-Test für den Haushaltswechsel

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

156 lines
5.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 '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',
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';
}
}
final householdRepositoryProvider = Provider<HouseholdRepository>((ref) {
return HouseholdRepository(
ref.watch(firestoreProvider),
() => FirebaseFunctions.instanceFor(region: 'europe-west3'),
() => ref.read(firebaseAuthProvider).currentUser?.uid,
);
});