leafittome/lib/features/household/data/household_providers.dart
cschlaefke edc4ecf405 Fix: Besitzer-Anzeige in Alt-Haushalten – eigene E-Mail wird nachgetragen
Haushalte aus der V1-Zeit haben keinen members-Eintrag für ihren Ersteller,
die Besitzer-Anzeige blieb dadurch leer. Die App trägt die eigene E-Mail
jetzt beim Start in allen eigenen Haushalten nach; die Rules erlauben dafür
nur den eigenen members-Eintrag ohne Rollenwechsel. Bis dahin zeigt die UI
„?" statt einer leeren Stelle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:50:22 +02:00

220 lines
8 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/foundation.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;
});
});
/// Trägt die eigene E-Mail in der members-Map aller eigenen Haushalte nach.
///
/// Haushalte aus der V1-Zeit haben für ihren Ersteller keinen members-Eintrag
/// (die Map kam erst mit V2) dadurch blieb die Besitzer-Anzeige leer.
/// Läuft beim App-Start mit, sobald die Haushalte geladen sind; die Rules
/// erlauben jedem genau die Pflege des eigenen Eintrags (ohne Rollenwechsel).
/// Wird in app.dart per ref.watch aktiv gehalten.
final memberEntrySyncProvider = Provider<void>((ref) {
final user = ref.watch(authStateProvider).value;
final email = user?.email;
if (user == null || email == null || email.isEmpty) return;
final households = ref.watch(myHouseholdsProvider).value ?? const [];
for (final household in households) {
final me = household.memberByUid(user.uid);
if (me != null && me.email.isNotEmpty) continue;
ref
.read(firestoreProvider)
.collection('households')
.doc(household.id)
.update({
'members.${user.uid}.email': email,
'members.${user.uid}.role': (me?.role ?? HouseholdRole.member).name,
}).catchError((Object error) {
// Nur loggen eine fehlende Anzeige darf die App nicht stören.
debugPrint('E-Mail-Nachtrag für ${household.id} fehlgeschlagen: $error');
});
}
});
/// 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,
);
});