- New VerantwortlicheInvite model: LT-issued invites so a person can register as Gemeinde Verantwortliche(r) for a specific Gemeinde, skipping the self-registration approval step. - Wahl/Workshop/Teilnehmer gain phase support (phasenAnzahl, beschreibung), mirroring the WP plugin's multi-phase elections. Teilnehmer unique constraint now scoped per phase. - Auth: team login + guest auth adjustments, spec coverage. - sync.service.ts: register VerantwortlicheInvite as a synced model. - wahl.service.ts: submitTeilnehmer updated for the new phase-scoped unique key. - client: login/home screen rework, new theme.dart, FCM web tweaks. - .gitignore: ignore .DS_Store. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
208 lines
6.1 KiB
Dart
208 lines
6.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../api.dart';
|
|
import '../main.dart';
|
|
import '../theme.dart';
|
|
import 'admin_screen.dart';
|
|
import 'chat_screen.dart';
|
|
import 'files_screen.dart';
|
|
import 'verantwortliche_register_screen.dart';
|
|
import 'wahl_screen.dart';
|
|
|
|
class HomeScreen extends StatelessWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = AppScope.of(context);
|
|
final id = state.identity!;
|
|
final kcId = id.kcId;
|
|
|
|
final needsVerantwRegistration = id.kind == SessionKind.user &&
|
|
!id.isLeitungsteam &&
|
|
id.memberships.isEmpty;
|
|
|
|
final tiles = <Widget>[
|
|
if (id.isLeitungsteam)
|
|
_NavTile(
|
|
icon: Icons.admin_panel_settings,
|
|
title: 'Verwaltung',
|
|
subtitle: 'KCs, Gemeinden, Onboarding-Freigaben',
|
|
onTap: () => _open(context, const AdminScreen()),
|
|
),
|
|
if (needsVerantwRegistration)
|
|
_NavTile(
|
|
icon: Icons.how_to_reg,
|
|
title: 'Als Verantwortliche/r registrieren',
|
|
subtitle: 'KC-Code eingeben, Gemeinde wählen, Freigabe abwarten',
|
|
onTap: () => _open(context, const VerantwortlicheRegisterScreen()),
|
|
),
|
|
if (id.kind == SessionKind.guest)
|
|
_NavTile(
|
|
icon: Icons.how_to_vote,
|
|
title: 'Workshop-Wahl',
|
|
subtitle: 'Deine Wünsche abgeben',
|
|
onTap: () => _open(context, const WahlScreen()),
|
|
),
|
|
if (kcId != null)
|
|
_NavTile(
|
|
icon: Icons.folder_shared,
|
|
title: 'Dateien',
|
|
subtitle: 'Freigegebene Dateien ansehen',
|
|
onTap: () => _open(context, FilesScreen(kcId: kcId)),
|
|
),
|
|
if (kcId != null)
|
|
_NavTile(
|
|
icon: Icons.forum,
|
|
title: 'Chat',
|
|
subtitle: 'Kanäle, Verlauf & Live-Nachrichten',
|
|
onTap: () => _open(context, ChatScreen(kcId: kcId)),
|
|
),
|
|
];
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('KC-App'),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: 'Abmelden',
|
|
onPressed: state.logout,
|
|
icon: const Icon(Icons.logout),
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 560),
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
_IdentityCard(id: id),
|
|
const SizedBox(height: 16),
|
|
...tiles,
|
|
if (tiles.isEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: 24),
|
|
child: Text(
|
|
'Für diesen Account gibt es hier noch keine Ansichten. '
|
|
'Sobald dir eine Gemeinde/ein KC zugeordnet ist, erscheinen '
|
|
'Dateien und Chat.',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _open(BuildContext context, Widget screen) {
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => screen));
|
|
}
|
|
}
|
|
|
|
class _IdentityCard extends StatelessWidget {
|
|
const _IdentityCard({required this.id});
|
|
final Identity id;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final lines = <String>[
|
|
if (id.email != null) id.email!,
|
|
if (id.isLeitungsteam)
|
|
'Leitungsteam-Rechte gelten KC-übergreifend.'
|
|
else if (id.memberships.length > 1)
|
|
'${id.memberships.length} Zuordnungen',
|
|
];
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [KcColors.blue, KcColors.teal],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.person, color: Colors.white),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(id.roleLabel, style: Theme.of(context).textTheme.titleMedium),
|
|
for (final l in lines) ...[
|
|
const SizedBox(height: 2),
|
|
Text(l, style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavTile extends StatelessWidget {
|
|
const _NavTile({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
});
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(20),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: KcColors.blue.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: KcColors.blue),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 2),
|
|
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: KcColors.slate.withValues(alpha: 0.6)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|