feat(client): LT Wahl admin, Teamer admin, Verantwortlichen self-registration
New screens (client/app/lib/screens/): - wahl_admin_screen.dart — per KC: list/create Wahlen; per Wahl: add workshops, run the assignment (POST /wahl/:id/zuteilung/run), view the result table. - teamer_admin_screen.dart — per Gemeinde: list/create local Teamer accounts, create group-link or per-email invites (shows the token). - verantwortliche_register_screen.dart — enter a KC invite code (GET /onboarding/kc/:code), pick a Gemeinde, submit (POST /onboarding/verantwortliche); shown on the home screen to a logged-in Authentik user who has no membership yet. - ui.dart — shared toast / ErrorText / SectionHeader / promptText. KcDetailScreen now links to Wahl admin and each Gemeinde row opens Teamer admin. Backend: widen the wahl + files LT routes to AuthGuard(['authentik','team']) for consistency with the other LT controllers. Rebrand web/index.html + manifest from "kc_app" to "KC-App". Verified against local Postgres with an isLeitungsteam team token: create KC/Gemeinde/Wahl/Workshop, run Zuteilung, create Teamer + invite, resolve an invite code. flutter analyze/test/build web green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import '../main.dart';
|
||||
import 'teamer_admin_screen.dart';
|
||||
import 'ui.dart';
|
||||
import 'wahl_admin_screen.dart';
|
||||
|
||||
/// Leitungsteam admin: KCs, their Gemeinden, and pending self-registrations.
|
||||
class AdminScreen extends StatefulWidget {
|
||||
@@ -24,13 +27,13 @@ class _AdminScreenState extends State<AdminScreen> {
|
||||
|
||||
Future<void> _createKc() async {
|
||||
final api = AppScope.of(context).api;
|
||||
final name = await _promptText(context, 'Neues KC', 'Name');
|
||||
final name = await promptText(context, 'Neues KC', 'Name');
|
||||
if (name == null || name.isEmpty || !mounted) return;
|
||||
try {
|
||||
await api.createKc(name);
|
||||
if (mounted) _reload();
|
||||
} catch (e) {
|
||||
if (mounted) _toast(context, '$e');
|
||||
if (mounted) toast(context, '$e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,13 +112,13 @@ class _KcDetailScreenState extends State<KcDetailScreen> {
|
||||
|
||||
Future<void> _addGemeinde() async {
|
||||
final api = _api;
|
||||
final name = await _promptText(context, 'Neue Gemeinde', 'Name');
|
||||
final name = await promptText(context, 'Neue Gemeinde', 'Name');
|
||||
if (name == null || name.isEmpty || !mounted) return;
|
||||
try {
|
||||
await api.createGemeinde(widget.kc.id, name);
|
||||
if (mounted) _reloadGemeinden();
|
||||
} catch (e) {
|
||||
if (mounted) _toast(context, '$e');
|
||||
if (mounted) toast(context, '$e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +136,22 @@ class _KcDetailScreenState extends State<KcDetailScreen> {
|
||||
trailing: const Icon(Icons.qr_code_2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('Gemeinden',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.how_to_vote),
|
||||
title: const Text('Workshop-Wahlen'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => WahlAdminScreen(kcId: widget.kc.id)),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _addGemeinde,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Hinzufügen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SectionHeader('Gemeinden', action: TextButton.icon(
|
||||
onPressed: _addGemeinde,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Hinzufügen'),
|
||||
)),
|
||||
_GemeindeList(future: _gemeinden!, onRetry: _reloadGemeinden),
|
||||
const Divider(height: 40),
|
||||
Text('Offene Verantwortlichen-Anfragen',
|
||||
@@ -161,7 +166,7 @@ class _KcDetailScreenState extends State<KcDetailScreen> {
|
||||
: await _api.rejectOnboarding(id);
|
||||
_reloadRequests();
|
||||
} catch (e) {
|
||||
if (context.mounted) _toast(context, '$e');
|
||||
if (context.mounted) toast(context, '$e');
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -199,6 +204,12 @@ class _GemeindeList extends StatelessWidget {
|
||||
dense: true,
|
||||
leading: const Icon(Icons.church),
|
||||
title: Text(g.name),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TeamerAdminScreen(gemeindeId: g.id, gemeindeName: g.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -260,32 +271,3 @@ class _RequestList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _promptText(BuildContext context, String title, String label) {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(labelText: label),
|
||||
onSubmitted: (v) => Navigator.of(context).pop(v.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toast(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user