import 'package:cloud_functions/cloud_functions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/firebase/firebase_providers.dart'; import '../../../core/widgets/app_drawer.dart'; import '../../../l10n/generated/app_localizations.dart'; import '../data/household_providers.dart'; import '../domain/household.dart'; /// Haushalt: Mitglieder mit Rollen, Einladen per Code, Beitreten per Code. class HouseholdScreen extends ConsumerStatefulWidget { const HouseholdScreen({super.key}); @override ConsumerState createState() => _HouseholdScreenState(); } class _HouseholdScreenState extends ConsumerState { final _codeController = TextEditingController(); bool _joining = false; @override void dispose() { _codeController.dispose(); super.dispose(); } Future _createInvite(HouseholdRole role) async { final l10n = AppLocalizations.of(context); final householdId = ref.read(householdIdProvider).value; if (householdId == null) return; final code = await ref .read(householdRepositoryProvider) .createInvite(householdId: householdId, role: role); if (!mounted) return; await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(l10n.inviteCodeTitle), content: Column( mainAxisSize: MainAxisSize.min, children: [ SelectableText( code, style: Theme.of(dialogContext) .textTheme .displaySmall ?.copyWith(letterSpacing: 6, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 12), Text(l10n.inviteCodeHint), ], ), actions: [ TextButton.icon( icon: const Icon(Icons.copy), label: Text(l10n.copyCode), onPressed: () async { final messenger = ScaffoldMessenger.of(context); await Clipboard.setData(ClipboardData(text: code)); messenger.showSnackBar( SnackBar(content: Text(l10n.codeCopied)), ); }, ), FilledButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('OK'), ), ], ), ); } /// E-Mail mit Fallback: Alt-Einträge ohne E-Mail zeigen „?“, bis die App /// des Betroffenen sie nachgetragen hat (memberEntrySyncProvider). String _labelFor(String? email) => (email == null || email.isEmpty) ? '?' : email; Future _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 _rename(Household household) async { final newName = await showDialog( context: context, builder: (_) => _RenameDialog(initialName: household.name), ); if (newName == null || newName.isEmpty || newName == household.name) { return; } await ref .read(householdRepositoryProvider) .renameHousehold(household.id, newName); } Future _leave(Household household) async { final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(l10n.leaveHouseholdConfirmTitle), content: Text(l10n.leaveHouseholdConfirmBody(household.name)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.leaveHousehold), ), ], ), ); if (confirmed != true || !mounted) return; final messenger = ScaffoldMessenger.of(context); try { await ref .read(householdRepositoryProvider) .leaveHousehold(household.id); messenger ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(l10n.leftHousehold(household.name)))); } on FirebaseFunctionsException { messenger ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.authErrorGeneric))); } } Future _removeMember( Household household, HouseholdMember member) async { final l10n = AppLocalizations.of(context); final label = member.email.isEmpty ? l10n.roleMember : member.email; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(l10n.removeMemberConfirmTitle), content: Text(l10n.removeMemberConfirmBody(label)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.removeMemberAction), ), ], ), ); if (confirmed != true || !mounted) return; final messenger = ScaffoldMessenger.of(context); try { await ref.read(householdRepositoryProvider).removeMember( householdId: household.id, memberUid: member.uid, ); messenger ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.memberRemoved(label)))); } on FirebaseFunctionsException { messenger ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.authErrorGeneric))); } } Future _join() async { final l10n = AppLocalizations.of(context); final code = _codeController.text.trim(); if (code.isEmpty) return; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(l10n.joinWarningTitle), content: Text(l10n.joinWarningBody), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.joinButton), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _joining = true); try { final name = await ref.read(householdRepositoryProvider).joinHousehold(code); if (!mounted) return; _codeController.clear(); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.joinSuccess(name)))); } on FirebaseFunctionsException catch (e) { if (!mounted) return; final message = switch (e.code) { 'not-found' => l10n.joinErrorInvalid, 'failed-precondition' => l10n.joinErrorUsed, _ => l10n.authErrorGeneric, }; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(message))); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.authErrorGeneric))); } finally { if (mounted) setState(() => _joining = false); } } @override Widget build(BuildContext context) { 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; final iAmOwner = household != null && myUid != null && household.isOwner(myUid); return Scaffold( appBar: AppBar( title: Text(household?.name ?? l10n.householdTitle), actions: [ if (household != null && myRole == HouseholdRole.member) IconButton( icon: const Icon(Icons.edit), tooltip: l10n.renameHousehold, onPressed: () => _rename(household), ), ], ), drawer: const AppDrawer(), 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.isOwner(myUid) ? l10n.ownerIsMe : l10n.ownerIs(_labelFor( entry.memberByUid(entry.ownerUid)?.email)), ), 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) for (final member in household.members) ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( child: Icon(member.role == HouseholdRole.sitter ? Icons.volunteer_activism : Icons.person), ), title: Text( member.uid == myUid && member.email.isEmpty ? l10n.meLabel : _labelFor(member.email), ), subtitle: Text([ member.role == HouseholdRole.sitter ? l10n.roleSitter : l10n.roleMember, if (household.isOwner(member.uid)) l10n.ownerLabel, ].join(' · ')), trailing: member.uid == myUid ? Chip(label: Text(l10n.meLabel)) : iAmOwner ? IconButton( icon: Icon(Icons.person_remove, color: theme.colorScheme.error), tooltip: l10n.removeMemberAction, onPressed: () => _removeMember(household, member), ) : null, ), if (household != null && myUid != null && !iAmOwner) ...[ const SizedBox(height: 8), OutlinedButton.icon( onPressed: () => _leave(household), icon: Icon(Icons.logout, color: theme.colorScheme.error), label: Text( l10n.leaveHousehold, style: TextStyle(color: theme.colorScheme.error), ), ), ], if (myRole == HouseholdRole.member) ...[ const Divider(height: 32), Text(l10n.inviteTitle, style: theme.textTheme.titleMedium), const SizedBox(height: 4), Text( l10n.inviteExplanation, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 12), FilledButton.tonalIcon( onPressed: () => _createInvite(HouseholdRole.member), icon: const Icon(Icons.person_add), label: Text(l10n.inviteMember), ), const SizedBox(height: 8), FilledButton.tonalIcon( onPressed: () => _createInvite(HouseholdRole.sitter), icon: const Icon(Icons.volunteer_activism), label: Text(l10n.inviteSitter), ), ], const Divider(height: 32), Text(l10n.joinTitle, style: theme.textTheme.titleMedium), const SizedBox(height: 12), TextField( controller: _codeController, decoration: InputDecoration( labelText: l10n.joinCodeLabel, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.key), ), textCapitalization: TextCapitalization.characters, autocorrect: false, ), const SizedBox(height: 12), FilledButton.icon( onPressed: _joining ? null : _join, icon: _joining ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.group_add), label: Text(l10n.joinButton), ), ], ), ); } } /// Eigener Dialog fürs Umbenennen: Der TextEditingController gehört dem /// Dialog-State und lebt damit bis zum Ende der Zuklapp-Animation /// (sofortiges dispose nach showDialog crasht während der Animation). class _RenameDialog extends StatefulWidget { const _RenameDialog({required this.initialName}); final String initialName; @override State<_RenameDialog> createState() => _RenameDialogState(); } class _RenameDialogState extends State<_RenameDialog> { late final TextEditingController _controller = TextEditingController(text: widget.initialName); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return AlertDialog( title: Text(l10n.renameHousehold), content: TextField( controller: _controller, autofocus: true, decoration: InputDecoration( labelText: l10n.householdNameLabel, border: const OutlineInputBorder(), ), textCapitalization: TextCapitalization.sentences, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.cancel), ), FilledButton( onPressed: () => Navigator.pop(context, _controller.text.trim()), child: Text(l10n.saveButton), ), ], ); } }