Files
KC-APP/client/app/lib/screens/ui.dart
linusandClaude Sonnet 5 2e3e62b896 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>
2026-09-10 10:09:35 +02:00

78 lines
2.1 KiB
Dart

import 'package:flutter/material.dart';
/// Small shared widgets/helpers used across the admin screens.
void toast(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
class ErrorText extends StatelessWidget {
const ErrorText(this.message, {super.key, this.onRetry});
final String message;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(message, textAlign: TextAlign.center),
if (onRetry != null) ...[
const SizedBox(height: 12),
OutlinedButton(onPressed: onRetry, child: const Text('Erneut versuchen')),
],
],
),
),
);
}
}
class SectionHeader extends StatelessWidget {
const SectionHeader(this.title, {super.key, this.action});
final String title;
final Widget? action;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
),
?action,
],
);
}
}
/// Single-line text prompt dialog. Returns the trimmed value or null.
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'),
),
],
),
);
}