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
@@ -0,0 +1,28 @@
import '../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart';
import '../../../session/session.dart';
import '../../../state/app/modules/children/child_selection_cubit.dart';
/// Who an absence report is filed for, and what the form lets the user edit.
class AbsenceFormPolicy {
/// The child the report is for; null when users report for themselves.
final GuardianChild? child;
const AbsenceFormPolicy._(this.child);
/// Guardians report for a linked child whose identity the server knows,
/// so name and class are fixed.
bool get identityEditable => child == null;
/// Null when the session cannot file a report (guardian without children).
static AbsenceFormPolicy? resolve({
required Session? session,
required List<GuardianChild> children,
required String? selectedChildId,
}) => switch (session) {
GuardianSession() => switch (effectiveChild(children, selectedChildId)) {
null => null,
final child => AbsenceFormPolicy._(child),
},
_ => const AbsenceFormPolicy._(null),
};
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/errors/error_mapper.dart';
import '../../../api/marianumconnect/queries/absence/absence_classes.dart';
@@ -6,24 +7,55 @@ import '../../../api/marianumconnect/queries/absence/absence_prefill.dart';
import '../../../api/marianumconnect/queries/absence/absence_prefill_response.dart';
import '../../../api/marianumconnect/queries/absence/absence_submit.dart';
import '../../../extensions/date_time.dart';
import '../../../session/session_manager.dart';
import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import '../../../state/app/modules/children/child_selection_cubit.dart';
import '../../../widget/app_progress_indicator.dart';
import '../../../widget/async_action_button.dart';
import '../../../widget/child_switcher.dart';
import '../../../widget/demo_restricted.dart';
import '../../../widget/focus_behaviour.dart';
import '../../../widget/placeholder_view.dart';
import 'absence_form_policy.dart';
/// Mobile mirror of the public absence-report form: submit-only (no history —
/// that lives on the web). Identity/class/phone are prefilled from the backend
/// but stay editable; the class list matches the submit validation source.
/// Guardians report for the selected child, whose identity is fixed.
/// User-facing texts mirror the public web form (`AbsencePublicForm.svelte`).
class AbsenceReportView extends StatefulWidget {
class AbsenceReportView extends StatelessWidget {
const AbsenceReportView({super.key});
@override
State<AbsenceReportView> createState() => _AbsenceReportViewState();
Widget build(BuildContext context) {
final policy = AbsenceFormPolicy.resolve(
session: SessionManager().current,
children: context.watch<CapabilitiesCubit>().state.children,
selectedChildId: context.watch<ChildSelectionCubit>().state,
);
return Scaffold(
appBar: AppBar(
title: const Text('Abwesenheitsmeldung'),
actions: const [ChildSwitcher()],
),
body: policy == null
? const NoChildrenPlaceholder()
// Re-created per child so no input leaks into another child's report.
: _AbsenceForm(key: ValueKey(policy.child?.id), policy: policy),
);
}
}
class _AbsenceReportViewState extends State<AbsenceReportView> {
class _AbsenceForm extends StatefulWidget {
final AbsenceFormPolicy policy;
const _AbsenceForm({required this.policy, super.key});
@override
State<_AbsenceForm> createState() => _AbsenceFormState();
}
class _AbsenceFormState extends State<_AbsenceForm> {
static const String _required = 'Dieses Feld ist erforderlich.';
final TextEditingController _firstName = TextEditingController();
@@ -67,7 +99,17 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
if (_submitted) setState(() {});
}
String? get _childId => widget.policy.child?.id;
Future<void> _load() async {
if (!widget.policy.identityEditable) {
// The child's identity is the whole point of the form, so a failed
// prefill is an error here, not a degraded start.
final prefill = await AbsencePrefill().run(childId: _childId);
_classes = [prefill.className];
_applyPrefill(prefill, _classes);
return;
}
// Both GETs are independent — fire them together. Prefill is best-effort
// (mapped to null on failure), so a classes error still propagates while a
// prefill failure never surfaces as an unhandled async error.
@@ -148,6 +190,7 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
absentUntil: _absentUntil,
phone: _phone.text.trim(),
note: _note.text.trim(),
childId: _childId,
);
if (!mounted) return;
// Replace the whole form with a terminal success screen. There is
@@ -157,10 +200,8 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Abwesenheitsmeldung')),
body: _done ? const _SubmittedView() : _buildBody(context),
);
Widget build(BuildContext context) =>
_done ? const _SubmittedView() : _buildBody(context);
Widget _buildBody(BuildContext context) => FutureBuilder<void>(
future: _init,
@@ -205,6 +246,7 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
const SizedBox(height: 20),
TextField(
controller: _firstName,
readOnly: !widget.policy.identityEditable,
textCapitalization: TextCapitalization.words,
decoration: _decoration(
'Vorname',
@@ -215,6 +257,7 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
const SizedBox(height: 16),
TextField(
controller: _lastName,
readOnly: !widget.policy.identityEditable,
textCapitalization: TextCapitalization.words,
decoration: _decoration(
'Nachname',
@@ -234,7 +277,9 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
items: _classes
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
.toList(),
onChanged: (value) => setState(() => _selectedClass = value),
onChanged: widget.policy.identityEditable
? (value) => setState(() => _selectedClass = value)
: null,
),
const SizedBox(height: 16),
_DateField(