added guardian login with views for their assigned childs

This commit is contained in:
2026-09-20 11:11:58 +02:00
parent e4e2b1a4fb
commit 67c935c05b
117 changed files with 4784 additions and 1514 deletions
@@ -4,15 +4,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/cloud_users/cloud_users_actions.dart';
import '../../../../api/marianumconnect/queries/auth_logout/auth_logout.dart';
import '../../../../model/account_data.dart';
import '../../../../push/push_registration.dart';
import '../../../../routing/app_routes.dart';
import '../../../../session/session.dart';
import '../../../../session/session_lifecycle.dart';
import '../../../../session/session_manager.dart';
import '../../../../state/app/modules/account/bloc/account_bloc.dart';
import '../../../../state/app/modules/account/bloc/account_state.dart';
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import '../../../../widget/app_progress_indicator.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/avatar_actions_sheet.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/child_switcher.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/demo_restricted.dart';
import '../../../../widget/user_avatar.dart';
@@ -21,14 +25,53 @@ import '../../../../widget/user_avatar.dart';
// every Settings rebuild doesn't re-issue the OCS request.
String? _cachedDisplayName;
class AccountSection extends StatefulWidget {
class AccountSection extends StatelessWidget {
const AccountSection({super.key});
@override
State<AccountSection> createState() => _AccountSectionState();
Widget build(BuildContext context) => switch (SessionManager().current) {
GuardianSession(:final email) => _GuardianAccount(email: email),
_ => const _SchoolAccount(),
};
}
class _AccountSectionState extends State<AccountSection> {
/// Guardians have no Nextcloud profile: show the e-mail and linked children.
class _GuardianAccount extends StatelessWidget {
final String email;
const _GuardianAccount({required this.email});
@override
Widget build(BuildContext context) {
final children = context.watch<CapabilitiesCubit>().state.children;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ListTile(
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
leading: const CenteredLeading(Icon(Icons.family_restroom_outlined)),
title: const Text('Elternkonto'),
subtitle: Text(email),
trailing: TextButton.icon(
icon: const Icon(Icons.logout_outlined, size: 18),
label: const Text('Abmelden'),
onPressed: () => _confirmLogout(context),
),
),
for (final child in children) ChildTile(child: child),
],
);
}
}
class _SchoolAccount extends StatefulWidget {
const _SchoolAccount();
@override
State<_SchoolAccount> createState() => _SchoolAccountState();
}
class _SchoolAccountState extends State<_SchoolAccount> {
int _avatarVersion = 0;
bool _avatarBusy = false;
String? _displayName = _cachedDisplayName;
@@ -42,9 +85,7 @@ class _AccountSectionState extends State<AccountSection> {
Future<void> _loadDisplayName() async {
try {
final info = await GetUserInfo().run();
_cachedDisplayName = info.displayName.isEmpty
? null
: info.displayName;
_cachedDisplayName = info.displayName.isEmpty ? null : info.displayName;
if (!mounted) return;
setState(() => _displayName = _cachedDisplayName);
} catch (_) {
@@ -84,13 +125,17 @@ class _AccountSectionState extends State<AccountSection> {
setState(() => _avatarBusy = false);
if (!ok) return;
invalidateAvatarCache(id: AccountData().getUsername(), isGroup: false);
invalidateAvatarCache(
id: SessionManager().requireNextcloud().username,
isGroup: false,
);
setState(() => _avatarVersion++);
}
@override
Widget build(BuildContext context) {
final username = AccountData().getUsername();
final nextcloud = SessionManager().requireNextcloud();
final username = nextcloud.username;
final displayName = _displayName;
final theme = Theme.of(context);
@@ -109,8 +154,10 @@ class _AccountSectionState extends State<AccountSection> {
children: [
Center(
child: GestureDetector(
onTap: () =>
AppRoutes.openLargeProfilePicture(context, username),
onTap: () => AppRoutes.openLargeProfilePicture(
context,
username,
),
child: UserAvatar(
key: ValueKey(_avatarVersion),
id: username,
@@ -164,7 +211,7 @@ class _AccountSectionState extends State<AccountSection> {
TextButton.icon(
icon: const Icon(Icons.logout_outlined, size: 18),
label: const Text('Abmelden'),
onPressed: () => _showLogoutDialog(context),
onPressed: () => _confirmLogout(context),
),
],
),
@@ -172,13 +219,11 @@ class _AccountSectionState extends State<AccountSection> {
// Nur für Login-Flow-Konten (2FA) sichtbar — Passwort-Konten heilen
// sich still über das App-Passwort-Minting und sollen von dem ganzen
// Flow-Mechanismus nichts mitbekommen.
if (!AccountData().isDemo && AccountData().usesLoginFlow)
if (!SessionManager().isDemo && nextcloud.usesLoginFlow)
AsyncListTile(
leading: const Icon(Icons.cloud_sync_outlined),
title: const Text('Nextcloud neu verbinden'),
subtitle: const Text(
'Bei Anmeldeproblemen in Talk oder Dateien',
),
subtitle: const Text('Bei Anmeldeproblemen in Talk oder Dateien'),
closeOnSuccess: false,
onPressed: _reconnectNextcloud,
),
@@ -197,35 +242,29 @@ class _AccountSectionState extends State<AccountSection> {
const SnackBar(content: Text('Nextcloud-Verbindung erneuert.')),
);
}
}
Future<void> _showLogoutDialog(BuildContext context) async {
// Flip AccountBloc state only after the dialog fully closes: doing it from
// inside removeData (the previous approach) raced AsyncDialogAction's
// pop(true) against the listener's popUntil(isFirst) and could leave the
// navigator in an inconsistent state.
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => ConfirmDialog(
title: 'Abmelden?',
content: 'Möchtest du dich wirklich abmelden?',
confirmButton: 'Abmelden',
onConfirmAsync: _performLogout,
),
);
if (confirmed != true || !context.mounted) return;
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
}
Future<void> _confirmLogout(BuildContext context) async {
// Flip AccountBloc state only after the dialog fully closes: doing it from
// inside the sign-out (the previous approach) raced AsyncDialogAction's
// pop(true) against the listener's popUntil(isFirst) and could leave the
// navigator in an inconsistent state.
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => ConfirmDialog(
title: 'Abmelden?',
content: 'Möchtest du dich wirklich abmelden?',
confirmButton: 'Abmelden',
onConfirmAsync: _performLogout,
),
);
if (confirmed != true || !context.mounted) return;
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
}
// Ordered teardown: unregister push at Nextcloud + proxy and revoke the app
// password (while Nextcloud credentials are still available), THEN revoke the
// MC bearer token, and finally wipe local credentials. Each step is
// best-effort so an offline logout still reaches a clean local state.
Future<void> _performLogout() async {
await PushRegistration().logoutCleanup();
await AuthLogout().run();
await AccountData().removeData();
_cachedDisplayName = null;
}
Future<void> _performLogout() async {
await SessionLifecycle.signOut();
_cachedDisplayName = null;
}
class _AvatarEditBadge extends StatelessWidget {
@@ -253,11 +292,7 @@ class _AvatarEditBadge extends StatelessWidget {
color: theme.colorScheme.onPrimary,
),
)
: Icon(
Icons.edit,
size: 14,
color: theme.colorScheme.onPrimary,
),
: Icon(Icons.edit, size: 14, color: theme.colorScheme.onPrimary),
),
),
);