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>
78 lines
2.1 KiB
Dart
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'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|