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>
This commit is contained in:
parent
8b74a5b3df
commit
1ca02f224b
11 changed files with 233 additions and 53 deletions
|
|
@ -18,7 +18,7 @@ lib/
|
|||
│ ├── today/ # Tages-Checkliste (Start-Screen)
|
||||
│ ├── plants/ # Pflanzen: Liste, Profil, Anlegen/Bearbeiten
|
||||
│ ├── locations/ # Stellplätze
|
||||
│ ├── household/ # Haushalt (V2-Platzhalter)
|
||||
│ ├── household/ # Haushalt: Mitglieder, Einladen, Wechseln
|
||||
│ └── settings/ # Einstellungs-Screen
|
||||
└── l10n/
|
||||
├── app_de.arb # ALLE deutschen Texte (Quelle)
|
||||
|
|
@ -46,6 +46,7 @@ Jedes Feature folgt demselben Muster (nicht jedes braucht alle Schichten):
|
|||
|
||||
- **Aufgaben werden nicht gespeichert, sondern berechnet.** Es gibt keine „Task-Tabelle": Aus `lastWatered + wateringIntervalDays` ergibt sich die Fälligkeit. Eine Bestätigung setzt nur `lastWatered` neu — dadurch kann nichts inkonsistent werden, und Überfälliges „wandert" automatisch mit.
|
||||
- **Haushaltszentriert gedacht:** Pflanzen, Stellplätze und Aufgaben gehören dem *Haushalt*, nicht einem Nutzer. In Firestore wird das `households/{id}/plants/{id}` usw. — so ist V2 (Sharing) nur „weiteres Mitglied hinzufügen", keine Datenmigration.
|
||||
- **Mehrere Haushalte pro Nutzer:** Quelle der Wahrheit für die Mitgliedschaft ist das Feld `memberUids` der Haushalte (`myHouseholdsProvider` fragt sie per `arrayContains` ab). `users/{uid}.householdId` ist nur noch der Zeiger auf den gerade **aktiven** Haushalt, den die App anzeigt — Wechseln heißt: diesen Zeiger umstellen (Haushalts-Screen, „Meine Haushalte“). Beim Beitritt per Einladungscode bleibt man Mitglied aller bisherigen Haushalte; die tägliche Sammel-Push berücksichtigt Aufgaben aus allen Haushalten.
|
||||
- **`null` bei `lastWatered` heißt „noch nie"** → Aufgabe ist sofort fällig. So braucht eine neu angelegte Pflanze keine Sonderbehandlung.
|
||||
|
||||
## Übersetzungen (i18n)
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ firebase deploy --only firestore --project leaf-it-to-me-app
|
|||
## Das Datenmodell und die Sicherheitsregeln (zum Verständnis)
|
||||
|
||||
```
|
||||
users/{uid} → E-Mail, householdId, fcmTokens, reminderTime (nur der Nutzer selbst)
|
||||
users/{uid} → E-Mail, householdId (= aktiver Haushalt), fcmTokens, reminderTime
|
||||
(nur der Nutzer selbst)
|
||||
households/{id} → Name, memberUids, members {uid → Rolle+E-Mail}
|
||||
households/{id}/plants/{id} → Pflanze: Art, Intervalle, lastWatered(+By), ...
|
||||
households/{id}/locations/{id} → Stellplatz: Name
|
||||
|
|
@ -85,6 +86,8 @@ invites/{code} → Einladung: householdId, Rolle; nur Erstelle
|
|||
|
||||
**Rollen (V2):** `member` = volle Rechte, `sitter` = Pflanzen-Sitter (sieht alles, darf aber nur Aufgaben bestätigen — die Rules erlauben Sittern an Pflanzen ausschließlich Änderungen der Bestätigungs-Felder). Der Beitritt per Einladungscode läuft über die Cloud Function `joinHousehold`, weil der Beitretende laut Rules noch kein Mitglied ist.
|
||||
|
||||
**Mehrere Haushalte (V2.1):** Ein Nutzer kann Mitglied beliebig vieler Haushalte sein — maßgeblich ist, in welchen `memberUids` er steht. `users/{uid}.householdId` zeigt nur, welcher Haushalt in der App gerade **aktiv** ist; gewechselt wird im Haushalts-Screen unter „Meine Haushalte“ (die App fragt die eigenen Haushalte per `where('memberUids', arrayContains: uid)` ab — das erlauben die bestehenden Rules bereits, es waren keine Regel-Änderungen nötig). Beim Beitritt per Code wechselt die App in den neuen Haushalt, alle bisherigen Mitgliedschaften bleiben bestehen. Die tägliche Sammel-Push (`sendDailyReminders`) sammelt fällige Aufgaben aus **allen** Haushalten des Nutzers, nicht nur aus dem aktiven.
|
||||
|
||||
Die Regeln in `firestore.rules` setzen das Haushalts-Prinzip durch:
|
||||
|
||||
- Dein **Nutzerprofil** (`users/{uid}`) kannst nur du selbst lesen/schreiben.
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
## Verifizierter Stand
|
||||
|
||||
- **V1 komplett** und auf Android Ende-zu-Ende getestet: Foto → Erkennung → vorbefülltes Profil; Tages-Checkliste; Sammel-Push kam im regulären Scheduler-Lauf auf dem Sperrbildschirm an. iOS-App läuft ebenfalls (Login, Pflanzen, Fotos) — **nur iOS-Push fehlt** (braucht Apple Developer Account, Beantragung scheiterte bisher an Apples Webseite; Schritte stehen in `docs/firebase-einrichtung.md` Schritt 6).
|
||||
- **V2 implementiert und deployt** (Commit `1b2c880`), vom Nutzer **noch nicht getestet**: Einladungscodes (member/sitter), `joinHousehold`-Function, Rules-Enforcement für Sitter, „bestätigt von"-Anzeige. Testplan wurde dem Nutzer genannt (iPhone-Konto lädt Android-Konto ein — er hat versehentlich zwei getrennte Konten/Haushalte).
|
||||
- **V2 implementiert, deployt und vom Nutzer erfolgreich getestet** (Einladungscodes member/sitter, `joinHousehold`-Function, Rules-Enforcement für Sitter, „bestätigt von"-Anzeige).
|
||||
- **V2.1 Multi-Haushalt implementiert**: Nutzer bleiben Mitglied aller Haushalte (`memberUids` ist Quelle der Wahrheit, `users.householdId` nur noch aktiver Zeiger), Wechsel-Liste „Meine Haushalte" im Haushalts-Screen, `sendDailyReminders` sammelt Aufgaben aus allen Haushalten. Rules unverändert (arrayContains-Query war schon erlaubt).
|
||||
|
||||
## Offen / Nächstes
|
||||
|
||||
1. Rückmeldung zum V2-Test abwarten, Bugs fixen.
|
||||
1. V2.1 (Haushalts-Wechsel) vom Nutzer auf den Geräten testen lassen.
|
||||
2. iOS-Push, sobald Apple Developer Account existiert.
|
||||
3. **V3:** Krankheits-Diagnose per Foto, Stellplatz-Bewertung (Standort-Analyse pro Stellplatz + Eignungs-Sterne pro Pflanze — Zwei-Ebenen-Konzept siehe Memory), Umtopf-Erinnerungen.
|
||||
4. **Backlog:** Button „KI-Zweitmeinung" bei unbefriedigendem PlantNet-Ergebnis; Push-Anzeige auch bei App im Vordergrund; App Check vor App-Store-Release; Englisch (`app_en.arb`).
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import * as admin from "firebase-admin";
|
|||
*/
|
||||
|
||||
interface UserDoc {
|
||||
householdId?: string;
|
||||
householdId?: string; // aktiver Haushalt (nur Anzeige-Zeiger der App)
|
||||
fcmTokens?: string[];
|
||||
reminderTime?: string; // "HH:mm"
|
||||
timezone?: string; // IANA, z. B. "Europe/Berlin"
|
||||
|
|
@ -150,12 +150,8 @@ export const sendDailyReminders = onSchedule(
|
|||
for (const userDoc of users.docs) {
|
||||
const user = userDoc.data() as UserDoc;
|
||||
const tokens = user.fcmTokens ?? [];
|
||||
if (tokens.length === 0 || !user.householdId) {
|
||||
logger.info("Übersprungen: kein Token oder kein Haushalt", {
|
||||
uid: userDoc.id,
|
||||
tokens: tokens.length,
|
||||
householdId: user.householdId ?? null,
|
||||
});
|
||||
if (tokens.length === 0) {
|
||||
logger.info("Übersprungen: kein Token", {uid: userDoc.id});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -178,12 +174,29 @@ export const sendDailyReminders = onSchedule(
|
|||
continue;
|
||||
}
|
||||
|
||||
const summary = await collectDueTasks(
|
||||
db,
|
||||
user.householdId,
|
||||
local,
|
||||
timezone
|
||||
);
|
||||
// Aufgaben aus ALLEN Haushalten sammeln, in denen der Nutzer Mitglied
|
||||
// ist – nicht nur aus dem gerade aktiven (users.householdId ist seit
|
||||
// Multi-Haushalt nur noch der Anzeige-Zeiger der App).
|
||||
const memberships = await db
|
||||
.collection("households")
|
||||
.where("memberUids", "array-contains", userDoc.id)
|
||||
.get();
|
||||
const summary: DueSummary = {
|
||||
watering: [],
|
||||
fertilizing: [],
|
||||
overdueCount: 0,
|
||||
};
|
||||
for (const householdDoc of memberships.docs) {
|
||||
const part = await collectDueTasks(
|
||||
db,
|
||||
householdDoc.id,
|
||||
local,
|
||||
timezone
|
||||
);
|
||||
summary.watering.push(...part.watering);
|
||||
summary.fertilizing.push(...part.fertilizing);
|
||||
summary.overdueCount += part.overdueCount;
|
||||
}
|
||||
const taskCount = summary.watering.length + summary.fertilizing.length;
|
||||
logger.info("Erinnerungs-Slot erreicht", {
|
||||
uid: userDoc.id,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,12 @@ class AppDrawer extends ConsumerWidget {
|
|||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import '../../../core/firebase/firebase_providers.dart';
|
||||
import '../domain/household.dart';
|
||||
|
||||
/// Die Haushalts-ID des angemeldeten Nutzers (aus users/{uid}.householdId).
|
||||
/// 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) {
|
||||
|
|
@ -23,7 +25,30 @@ final householdIdProvider = StreamProvider<String?>((ref) {
|
|||
.map((snapshot) => snapshot.data()?['householdId'] as String?);
|
||||
});
|
||||
|
||||
/// Der komplette Haushalt inkl. Mitgliedern und Rollen, live aus Firestore.
|
||||
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);
|
||||
|
|
@ -36,26 +61,29 @@ final householdProvider = StreamProvider<Household?>((ref) {
|
|||
.map((snapshot) {
|
||||
final data = snapshot.data();
|
||||
if (data == null) return null;
|
||||
final membersMap = (data['members'] as Map<String, dynamic>?) ?? {};
|
||||
final memberUids = (data['memberUids'] as List<dynamic>?) ?? [];
|
||||
return Household(
|
||||
id: snapshot.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? ??
|
||||
'',
|
||||
),
|
||||
],
|
||||
);
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -68,10 +96,12 @@ final myRoleProvider = Provider<HouseholdRole>((ref) {
|
|||
});
|
||||
|
||||
class HouseholdRepository {
|
||||
HouseholdRepository(this._firestore, this._functions, this._uidGetter);
|
||||
HouseholdRepository(this._firestore, this._functionsGetter, this._uidGetter);
|
||||
|
||||
final FirebaseFirestore _firestore;
|
||||
final FirebaseFunctions _functions;
|
||||
// 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';
|
||||
|
|
@ -96,10 +126,21 @@ class HouseholdRepository {
|
|||
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 = _functions.httpsCallable('joinHousehold');
|
||||
final callable = _functionsGetter().httpsCallable('joinHousehold');
|
||||
final response =
|
||||
await callable.call<Map<String, dynamic>>({'code': code.trim()});
|
||||
return response.data['householdName'] as String? ?? 'Haushalt';
|
||||
|
|
@ -109,7 +150,7 @@ class HouseholdRepository {
|
|||
final householdRepositoryProvider = Provider<HouseholdRepository>((ref) {
|
||||
return HouseholdRepository(
|
||||
ref.watch(firestoreProvider),
|
||||
FirebaseFunctions.instanceFor(region: 'europe-west3'),
|
||||
() => FirebaseFunctions.instanceFor(region: 'europe-west3'),
|
||||
() => ref.read(firebaseAuthProvider).currentUser?.uid,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,6 +77,18 @@ class _HouseholdScreenState extends ConsumerState<HouseholdScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _switchTo(Household target) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await ref.read(householdRepositoryProvider).switchHousehold(target.id);
|
||||
if (!mounted) return;
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(content: Text(l10n.switchHouseholdSuccess(target.name))),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _join() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final code = _codeController.text.trim();
|
||||
|
|
@ -135,6 +147,7 @@ class _HouseholdScreenState extends ConsumerState<HouseholdScreen> {
|
|||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final household = ref.watch(householdProvider).value;
|
||||
final myHouseholds = ref.watch(myHouseholdsProvider).value ?? const [];
|
||||
final myRole = ref.watch(myRoleProvider);
|
||||
final myUid = ref.watch(authStateProvider).value?.uid;
|
||||
|
||||
|
|
@ -144,6 +157,35 @@ class _HouseholdScreenState extends ConsumerState<HouseholdScreen> {
|
|||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (myHouseholds.length > 1) ...[
|
||||
Text(l10n.myHouseholds, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
for (final entry in myHouseholds)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
entry.id == household?.id
|
||||
? Icons.home
|
||||
: Icons.home_outlined,
|
||||
color: entry.id == household?.id
|
||||
? theme.colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
title: Text(entry.name),
|
||||
subtitle: Text(
|
||||
myUid != null && entry.roleOf(myUid) == HouseholdRole.sitter
|
||||
? l10n.roleSitter
|
||||
: l10n.roleMember,
|
||||
),
|
||||
trailing: entry.id == household?.id
|
||||
? Chip(label: Text(l10n.activeHouseholdLabel))
|
||||
: null,
|
||||
onTap: entry.id == household?.id
|
||||
? null
|
||||
: () => _switchTo(entry),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
Text(l10n.householdMembers, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
if (household != null)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,14 @@
|
|||
}
|
||||
},
|
||||
"householdTitle": "Haushalt",
|
||||
"myHouseholds": "Meine Haushalte",
|
||||
"activeHouseholdLabel": "Aktiv",
|
||||
"switchHouseholdSuccess": "Du bist jetzt im Haushalt „{name}“.",
|
||||
"@switchHouseholdSuccess": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"householdMembers": "Mitglieder",
|
||||
"roleMember": "Mitglied",
|
||||
"roleSitter": "Pflanzen-Sitter",
|
||||
|
|
@ -120,8 +128,8 @@
|
|||
"joinTitle": "Haushalt beitreten",
|
||||
"joinCodeLabel": "Einladungscode eingeben",
|
||||
"joinButton": "Beitreten",
|
||||
"joinWarningTitle": "Haushalt wechseln?",
|
||||
"joinWarningBody": "Du trittst einem anderen Haushalt bei. Die Pflanzen deines bisherigen Haushalts siehst du danach nicht mehr.",
|
||||
"joinWarningTitle": "Haushalt beitreten?",
|
||||
"joinWarningBody": "Du trittst dem Haushalt bei und wechselst direkt dorthin. Deine bisherigen Haushalte bleiben erhalten – unter „Meine Haushalte“ kannst du jederzeit zurückwechseln.",
|
||||
"joinSuccess": "Willkommen im Haushalt „{name}“!",
|
||||
"@joinSuccess": {
|
||||
"placeholders": {
|
||||
|
|
|
|||
|
|
@ -364,6 +364,24 @@ abstract class AppLocalizations {
|
|||
/// **'Haushalt'**
|
||||
String get householdTitle;
|
||||
|
||||
/// No description provided for @myHouseholds.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Meine Haushalte'**
|
||||
String get myHouseholds;
|
||||
|
||||
/// No description provided for @activeHouseholdLabel.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Aktiv'**
|
||||
String get activeHouseholdLabel;
|
||||
|
||||
/// No description provided for @switchHouseholdSuccess.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Du bist jetzt im Haushalt „{name}“.'**
|
||||
String switchHouseholdSuccess(String name);
|
||||
|
||||
/// No description provided for @householdMembers.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
|
|
@ -457,13 +475,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @joinWarningTitle.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Haushalt wechseln?'**
|
||||
/// **'Haushalt beitreten?'**
|
||||
String get joinWarningTitle;
|
||||
|
||||
/// No description provided for @joinWarningBody.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Du trittst einem anderen Haushalt bei. Die Pflanzen deines bisherigen Haushalts siehst du danach nicht mehr.'**
|
||||
/// **'Du trittst dem Haushalt bei und wechselst direkt dorthin. Deine bisherigen Haushalte bleiben erhalten – unter „Meine Haushalte“ kannst du jederzeit zurückwechseln.'**
|
||||
String get joinWarningBody;
|
||||
|
||||
/// No description provided for @joinSuccess.
|
||||
|
|
|
|||
|
|
@ -187,6 +187,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get householdTitle => 'Haushalt';
|
||||
|
||||
@override
|
||||
String get myHouseholds => 'Meine Haushalte';
|
||||
|
||||
@override
|
||||
String get activeHouseholdLabel => 'Aktiv';
|
||||
|
||||
@override
|
||||
String switchHouseholdSuccess(String name) {
|
||||
return 'Du bist jetzt im Haushalt „$name“.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get householdMembers => 'Mitglieder';
|
||||
|
||||
|
|
@ -235,11 +246,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get joinButton => 'Beitreten';
|
||||
|
||||
@override
|
||||
String get joinWarningTitle => 'Haushalt wechseln?';
|
||||
String get joinWarningTitle => 'Haushalt beitreten?';
|
||||
|
||||
@override
|
||||
String get joinWarningBody =>
|
||||
'Du trittst einem anderen Haushalt bei. Die Pflanzen deines bisherigen Haushalts siehst du danach nicht mehr.';
|
||||
'Du trittst dem Haushalt bei und wechselst direkt dorthin. Deine bisherigen Haushalte bleiben erhalten – unter „Meine Haushalte“ kannst du jederzeit zurückwechseln.';
|
||||
|
||||
@override
|
||||
String joinSuccess(String name) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ class _FakePushRegistrationService implements PushRegistrationService {
|
|||
|
||||
/// Baut die App mit gemocktem Firebase: angemeldeter Nutzer 'u1' im Haushalt
|
||||
/// 'h1' mit einer Monstera, deren Gießen seit einem Tag überfällig ist.
|
||||
Future<Widget> buildTestApp({String role = 'member'}) async {
|
||||
/// Mit [secondHousehold] ist u1 zusätzlich Mitglied im Haushalt 'h2'.
|
||||
Future<Widget> buildTestApp({
|
||||
String role = 'member',
|
||||
bool secondHousehold = false,
|
||||
}) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
|
|
@ -43,6 +47,16 @@ Future<Widget> buildTestApp({String role = 'member'}) async {
|
|||
'u1': {'role': role, 'email': 'test@example.com'},
|
||||
},
|
||||
});
|
||||
if (secondHousehold) {
|
||||
await firestore.collection('households').doc('h2').set({
|
||||
'name': 'Zweiter Haushalt',
|
||||
'memberUids': ['u1', 'u2'],
|
||||
'members': {
|
||||
'u1': {'role': 'sitter', 'email': 'test@example.com'},
|
||||
'u2': {'role': 'member', 'email': 'other@example.com'},
|
||||
},
|
||||
});
|
||||
}
|
||||
final now = DateTime.now();
|
||||
await firestore
|
||||
.collection('households')
|
||||
|
|
@ -107,4 +121,29 @@ void main() {
|
|||
// … aber Pflanzen anlegen nicht.
|
||||
expect(find.text('Pflanze hinzufügen'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Mitglied mehrerer Haushalte kann den aktiven Haushalt wechseln',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(await buildTestApp(secondHousehold: true));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Über das Menü zur Haushalts-Ansicht.
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Haushalt'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Beide Haushalte sind gelistet, h1 ist aktiv.
|
||||
expect(find.text('Meine Haushalte'), findsOneWidget);
|
||||
expect(find.text('Aktiv'), findsOneWidget);
|
||||
expect(find.text('Zweiter Haushalt'), findsOneWidget);
|
||||
|
||||
// Wechsel nach h2: Zeiger in users/u1 wird umgestellt …
|
||||
await tester.tap(find.text('Zweiter Haushalt'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// … und die Ansicht zeigt den neuen Haushalt (AppBar + Listeneintrag).
|
||||
expect(find.text('Zweiter Haushalt'), findsNWidgets(2));
|
||||
expect(find.text('other@example.com'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue