From 2423c1a75e0aaee71e304b294bfcda4804f0fd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 20 Sep 2026 16:12:45 +0200 Subject: [PATCH] added guardian letters with chat and multiple answer functionalities --- CLAUDE.md | 8 +- lib/access/access_requirement.dart | 4 +- lib/api/demo/data/demo_parent_letters.dart | 273 ++ .../errors/marianumconnect_error.dart | 16 + .../marianumconnect_query.dart | 19 +- .../guardian_login_exception.dart | 16 +- .../get_newsletter_file.dart | 11 +- .../get_ticker_page_file.dart | 11 +- .../parent_letters/get_parent_letter.dart | 9 + .../get_parent_letter_attachment.dart | 14 + .../parent_letters/get_parent_letters.dart | 16 + .../mark_parent_letter_read.dart | 11 + .../parent_letter_exception.dart | 90 + .../parent_letters/parent_letter_models.dart | 226 + .../parent_letter_models.freezed.dart | 3988 +++++++++++++++++ .../parent_letter_models.g.dart | 331 ++ .../parent_letters/parent_letter_query.dart | 18 + .../post_parent_letter_thread_message.dart | 20 + .../submit_parent_letter_response.dart | 34 + lib/app.dart | 29 +- lib/main.dart | 25 +- lib/notification/notification_controller.dart | 28 +- lib/notification/notification_tasks.dart | 38 + lib/push/notification_permission_prompt.dart | 190 + lib/push/push_renderer.dart | 33 +- lib/push/push_tap_router.dart | 46 +- lib/push/push_target.dart | 45 + lib/routing/app_routes.dart | 7 + lib/state/app/modules/app_modules.dart | 56 +- .../bloc/parent_letter_bloc.dart | 103 + .../bloc/parent_letter_event.dart | 5 + .../bloc/parent_letter_state.dart | 15 + .../bloc/parent_letter_state.freezed.dart | 285 ++ .../bloc/parent_letter_state.g.dart | 17 + .../bloc/parent_letters_bloc.dart | 107 + .../bloc/parent_letters_event.dart | 5 + .../bloc/parent_letters_state.dart | 21 + .../bloc/parent_letters_state.freezed.dart | 300 ++ .../bloc/parent_letters_state.g.dart | 29 + .../parent_letters/parent_letters_logic.dart | 67 + .../repository/parent_letter_repository.dart | 53 + .../repository/parent_letters_repository.dart | 22 + lib/storage/modules_settings.g.dart | 1 + lib/storage/notification_settings.dart | 11 + lib/storage/notification_settings.g.dart | 4 + lib/utils/random_id.dart | 9 + .../parent_letter_form_policy.dart | 94 + .../parent_letters/parent_letter_view.dart | 161 + .../parent_letters/parent_letters_view.dart | 132 + .../widgets/parent_letter_attachments.dart | 87 + .../widgets/parent_letter_response_card.dart | 225 + .../widgets/parent_letter_status_chip.dart | 48 + .../widgets/parent_letter_thread.dart | 138 + .../widgets/parent_letter_tile.dart | 91 + .../widgets/signature_sheet.dart | 125 + .../pages/settings/data/default_settings.dart | 1 + lib/view/pages/talk/chat_list.dart | 2 +- .../talk/notification_permission_prompt.dart | 89 - lib/widget/module_badge_icon.dart | 31 + pubspec.yaml | 1 + test/access/access_requirement_test.dart | 31 + .../parent_letter_models_test.dart | 241 + test/demo/demo_parent_letters_test.dart | 41 + test/push/push_tap_router_test.dart | 66 + test/state/app_modules_order_test.dart | 17 +- test/state/parent_letters_logic_test.dart | 104 + test/utils/random_id_test.dart | 15 + .../parent_letter_form_policy_test.dart | 168 + 68 files changed, 8348 insertions(+), 226 deletions(-) create mode 100644 lib/api/demo/data/demo_parent_letters.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/get_parent_letter.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/get_parent_letters.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/parent_letter_models.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/parent_letter_models.freezed.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/parent_letter_models.g.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/parent_letter_query.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart create mode 100644 lib/api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart create mode 100644 lib/push/notification_permission_prompt.dart create mode 100644 lib/push/push_target.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letter_bloc.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letter_event.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letter_state.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letter_state.freezed.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letter_state.g.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letters_bloc.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letters_event.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letters_state.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letters_state.freezed.dart create mode 100644 lib/state/app/modules/parent_letters/bloc/parent_letters_state.g.dart create mode 100644 lib/state/app/modules/parent_letters/parent_letters_logic.dart create mode 100644 lib/state/app/modules/parent_letters/repository/parent_letter_repository.dart create mode 100644 lib/state/app/modules/parent_letters/repository/parent_letters_repository.dart create mode 100644 lib/view/pages/parent_letters/parent_letter_form_policy.dart create mode 100644 lib/view/pages/parent_letters/parent_letter_view.dart create mode 100644 lib/view/pages/parent_letters/parent_letters_view.dart create mode 100644 lib/view/pages/parent_letters/widgets/parent_letter_attachments.dart create mode 100644 lib/view/pages/parent_letters/widgets/parent_letter_response_card.dart create mode 100644 lib/view/pages/parent_letters/widgets/parent_letter_status_chip.dart create mode 100644 lib/view/pages/parent_letters/widgets/parent_letter_thread.dart create mode 100644 lib/view/pages/parent_letters/widgets/parent_letter_tile.dart create mode 100644 lib/view/pages/parent_letters/widgets/signature_sheet.dart delete mode 100644 lib/view/pages/talk/notification_permission_prompt.dart create mode 100644 lib/widget/module_badge_icon.dart create mode 100644 test/access/access_requirement_test.dart create mode 100644 test/api/marianumconnect/parent_letter_models_test.dart create mode 100644 test/demo/demo_parent_letters_test.dart create mode 100644 test/push/push_tap_router_test.dart create mode 100644 test/state/parent_letters_logic_test.dart create mode 100644 test/utils/random_id_test.dart create mode 100644 test/view/parent_letters/parent_letter_form_policy_test.dart diff --git a/CLAUDE.md b/CLAUDE.md index 4d5ac32..e69a821 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # MarianumMobile Client -Flutter-App für die Schul-Community: Stundenplan, Ticker, Newsletter & Co. über das MarianumConnect-Backend, Nextcloud Talk + Files. Zwei Kontoarten: Schul-Konto (Schüler/Lehrer, Benutzername + Passwort) und Eltern-Konto (passwortlos per E-Mail-Code/App-Link, sieht die Stundenpläne der zugeordneten Kinder, kein Talk/Files). +Flutter-App für die Schul-Community: Stundenplan, Ticker, Newsletter & Co. über das MarianumConnect-Backend, Nextcloud Talk + Files. Zwei Kontoarten: Schul-Konto (Schüler/Lehrer, Benutzername + Passwort) und Eltern-Konto (passwortlos per E-Mail-Code/App-Link, sieht die Stundenpläne der zugeordneten Kinder und erhält Elternbriefe, kein Talk/Files). ## Stack @@ -56,10 +56,12 @@ lib/ **Session:** Die aktive Sitzung liegt in `SessionManager().current` (`lib/session/`). Nextcloud-Zugriffe nur über `SessionManager().requireNextcloud()` – Eltern-Sessions haben keine Nextcloud-Identität. Abmelden ausschließlich über `SessionLifecycle.signOut()`. Die Keychain-Keys in `SessionKeys` sind eingefroren (Bestandsinstallationen, iOS-NSE). -**Rollen & Zuschnitt:** Views verzweigen nie auf Rollen. Module und Settings-Sections deklarieren `AccessRequirement`s (`AppModule.requirements`, `Settings._sections`); Views bekommen ein Subjekt + eine Policy, die an genau einer Stelle als pure Funktion aufgelöst wird (Vorbild: `TimetableSubject` + `TimetablePolicy.resolve`, `AbsenceFormPolicy`). `UserRole` nur für Anzeige/Policy-Ableitung. +**Rollen & Zuschnitt:** Views verzweigen nie auf Rollen. Module und Settings-Sections deklarieren `AccessRequirement`s (`nextcloud`, `guardian`; `AppModule.requirements`, `Settings._sections`); Views bekommen ein Subjekt + eine Policy, die an genau einer Stelle als pure Funktion aufgelöst wird (Vorbild: `TimetableSubject` + `TimetablePolicy.resolve`, `AbsenceFormPolicy`). `UserRole` nur für Anzeige/Policy-Ableitung. **Stundenplan:** Ein `TimetableBloc(subject: …)` für alle Fälle (eigener Plan, Fremdplan, Kind). Den globalen Bloc stellt `PrimaryTimetableScope` bereit und tauscht ihn beim Kindwechsel aus; page-scoped Fremdpläne nutzen `ScopedTimetableBloc`. Die Kinderauswahl (`ChildSelectionCubit`) ist modulübergreifend. +**Elternbriefe:** Modul `parentLetters` (nur Eltern-Sessions, `AccessRequirement.guardian`): Lehrer-Mitteilungen aus MarianumConnect mit Gelesen-Status, Kenntnisnahme/Auswahl/Unterschrift pro Kind, Thread und Anhängen. Der Posteingang (`ParentLettersBloc`) ist global (Modul-Badge, Push-Refresh), ein Brief ist page-scoped (`ParentLetterBloc`). Ob und wie geantwortet werden darf, entscheidet der Server (`editable`); `ParentLetterFormPolicy.resolve` macht daraus das Formular. Push: Connect-Direct-Push `type: parent-letter`, Tap-Routing allein über `parentLetterId`. Alle Benachrichtigungs-Taps (lokal gerendert wie FCM) werden von `resolvePushTarget` (`lib/push/push_target.dart`) aufgelöst und in `NotificationTasks.openPushTarget` navigiert – neue Push-Ziele nur dort ergänzen. + ## Build / Run ```bash @@ -74,7 +76,7 @@ flutter test # Tests (siehe test | Backend | Pfad | Zweck | |---------------------------|-----------------------|----------------------------------------| -| MarianumConnect (Bearer) | `lib/api/marianumconnect/` | Auth, Stundenplan (Webuntis-Proxy), Ticker, Newsletter, Ferien, Abwesenheit, Capabilities, Push | +| MarianumConnect (Bearer) | `lib/api/marianumconnect/` | Auth, Stundenplan (Webuntis-Proxy), Ticker, Newsletter, Ferien, Abwesenheit, Elternbriefe, Capabilities, Push | | Nextcloud (Talk + WebDAV) | `lib/api/marianumcloud/` | Chats, Datei-Verwaltung | | MHSL (Legacy) | `lib/api/mhsl/` | nur noch Einmal-Migration der Custom Events | diff --git a/lib/access/access_requirement.dart b/lib/access/access_requirement.dart index 8e0e1b5..26ad182 100644 --- a/lib/access/access_requirement.dart +++ b/lib/access/access_requirement.dart @@ -3,10 +3,12 @@ import '../session/session.dart'; /// Backend identity a feature needs. Modules and settings sections declare /// these; anything whose requirements the session does not meet is hidden. enum AccessRequirement { - nextcloud; + nextcloud, + guardian; bool isMetBy(Session? session) => switch (this) { AccessRequirement.nextcloud => session?.nextcloud != null, + AccessRequirement.guardian => session is GuardianSession, }; } diff --git a/lib/api/demo/data/demo_parent_letters.dart b/lib/api/demo/data/demo_parent_letters.dart new file mode 100644 index 0000000..ab805d4 --- /dev/null +++ b/lib/api/demo/data/demo_parent_letters.dart @@ -0,0 +1,273 @@ +import '../../marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../demo_persona.dart'; + +/// Demo inbox of the guardian persona: one letter per variant (information, +/// acknowledgement, choice, signature, answered, expired, thread, attachment). +/// Child ids match [DemoCapabilities.guardianState]. +class DemoParentLetters { + const DemoParentLetters._(); + + static const _lena = 'demo-child-1'; + static const _jonas = 'demo-child-2'; + + static final Set _readInSession = {}; + + static void markRead(String letterId) => _readInSession.add(letterId); + + static ParentLetterListResponse list() { + final items = [for (final letter in _letters()) letter.summary]; + return ParentLetterListResponse( + items: items, + unreadCount: items.where((letter) => !letter.read).length, + openCount: items + .where((letter) => letter.status == ParentLetterStatus.open) + .length, + ); + } + + static ParentLetterDetail detail(String letterId) { + final letters = _letters(); + return letters.firstWhere( + (letter) => letter.summary.id == letterId, + orElse: () => letters.first, + ); + } + + static ParentLetterPerson _teacher(int index) => + ParentLetterPerson(displayName: DemoPersona.teachers[index].name); + + static const _participation = ParentLetterField( + id: 'participation', + type: ParentLetterFieldType.singleChoice, + label: 'Nimmt Ihr Kind teil?', + isRequired: true, + options: [ + ParentLetterOption(id: 'yes', label: 'Ja'), + ParentLetterOption(id: 'no', label: 'Nein'), + ], + ); + + static List _letters() { + final now = DateTime.now(); + ParentLetterDetail letter({ + required String id, + required String subject, + required ParentLetterPerson sender, + required Duration age, + required String body, + required List childIds, + bool read = true, + ParentLetterStatus status = ParentLetterStatus.info, + ParentLetterRequest? request, + List children = const [], + List attachments = const [], + ParentLetterThread thread = const ParentLetterThread(enabled: true), + }) => ParentLetterDetail( + summary: ParentLetterSummary( + id: id, + subject: subject, + preview: body.replaceAll('\n', ' ').trim(), + sender: sender, + sentAt: now.subtract(age), + read: read || _readInSession.contains(id), + childIds: childIds, + attachmentCount: attachments.length, + status: status, + deadline: request?.deadline, + ), + content: ParentLetterContent( + body: body, + attachments: attachments, + request: request, + children: children, + thread: thread, + ), + ); + + return [ + letter( + id: 'demo-letter-hike', + subject: 'Wandertag der ${DemoPersona.className}', + sender: _teacher(0), + age: const Duration(hours: 2), + read: false, + childIds: const [_lena], + status: ParentLetterStatus.open, + body: + 'Liebe Eltern,\n\nam Freitag in zwei Wochen findet unser Wandertag ' + 'statt. Wir fahren mit dem Bus in die Rhön und sind gegen 16 Uhr ' + 'zurück. Alle Einzelheiten finden Sie im angehängten Schreiben.\n\n' + 'Bitte geben Sie uns eine verbindliche Rückmeldung.\n\n' + 'Viele Grüße\n${DemoPersona.teachers[0].name}', + attachments: const [ + ParentLetterAttachment( + id: 'demo-attachment-hike', + fileName: 'Wandertag.pdf', + mimeType: 'application/pdf', + size: 48211, + ), + ], + request: ParentLetterRequest( + fields: const [_participation], + signatureRequired: true, + deadline: now.add(const Duration(days: 10)), + ), + children: const [ + ParentLetterChildState( + childId: _lena, + status: ParentLetterStatus.open, + editable: true, + ), + ], + ), + letter( + id: 'demo-letter-parents-evening', + subject: 'Einladung zum Elternabend der 6a', + sender: _teacher(1), + age: const Duration(days: 1), + read: false, + childIds: const [_jonas], + status: ParentLetterStatus.open, + body: + 'Liebe Eltern,\n\nhiermit lade ich Sie herzlich zum ersten ' + 'Elternabend des Schuljahres ein. Wir treffen uns am kommenden ' + 'Dienstag um 19 Uhr in Raum ${DemoPersona.rooms[0]}.\n\n' + 'Bitte bestätigen Sie kurz den Erhalt dieser Einladung.', + request: const ParentLetterRequest(), + children: const [ + ParentLetterChildState( + childId: _jonas, + status: ParentLetterStatus.open, + editable: true, + ), + ], + ), + letter( + id: 'demo-letter-school-festival', + subject: 'Schulfest: Wer kommt mit?', + sender: _teacher(5), + age: const Duration(days: 3), + childIds: const [_lena, _jonas], + status: ParentLetterStatus.open, + body: + 'Liebe Eltern,\n\nfür die Planung unseres Schulfestes möchten wir ' + 'wissen, welche Kinder teilnehmen. Die Angabe können Sie bis zum ' + 'Ablauf der Frist noch ändern.\n\nWeitere Informationen: ' + 'https://www.marianum-fulda.de', + request: ParentLetterRequest( + fields: const [_participation], + deadline: now.add(const Duration(days: 5)), + ), + children: [ + ParentLetterChildState( + childId: _lena, + status: ParentLetterStatus.done, + editable: true, + response: ParentLetterResponse( + respondedAt: now.subtract(const Duration(days: 2)), + respondedBy: const ParentLetterPerson(self: true), + answers: const [ + ParentLetterAnswer( + fieldId: 'participation', + optionIds: ['yes'], + ), + ], + ), + ), + const ParentLetterChildState( + childId: _jonas, + status: ParentLetterStatus.open, + editable: true, + ), + ], + ), + letter( + id: 'demo-letter-reports', + subject: 'Zeugnisausgabe und Unterrichtsschluss', + sender: _teacher(3), + age: const Duration(days: 7), + childIds: const [_lena, _jonas], + body: + 'Liebe Eltern,\n\nam letzten Schultag vor den Ferien endet der ' + 'Unterricht nach der dritten Stunde mit der Zeugnisausgabe. Die ' + 'Busse fahren entsprechend früher.', + thread: ParentLetterThread( + enabled: true, + messages: [ + ParentLetterThreadMessage( + id: 'demo-thread-1', + author: const ParentLetterPerson(self: true), + body: 'Gilt das auch für die Nachmittagsbetreuung?', + sentAt: now.subtract(const Duration(days: 6, hours: 20)), + ), + ParentLetterThreadMessage( + id: 'demo-thread-2', + author: _teacher(3), + body: + 'Ja, die Betreuung entfällt an diesem Tag ebenfalls. Bei ' + 'Bedarf melden Sie sich bitte im Sekretariat.', + sentAt: now.subtract(const Duration(days: 6, hours: 2)), + ), + ], + ), + ), + letter( + id: 'demo-letter-swimming', + subject: 'Einverständnis Schwimmunterricht', + sender: _teacher(6), + age: const Duration(days: 21), + childIds: const [_jonas], + status: ParentLetterStatus.done, + body: + 'Liebe Eltern,\n\nim zweiten Halbjahr findet der Sportunterricht ' + 'der 6a im Hallenbad statt. Dafür benötigen wir Ihr ' + 'Einverständnis.', + request: const ParentLetterRequest( + fields: [_participation], + signatureRequired: true, + ), + children: [ + ParentLetterChildState( + childId: _jonas, + status: ParentLetterStatus.done, + response: ParentLetterResponse( + respondedAt: now.subtract(const Duration(days: 20)), + respondedBy: const ParentLetterPerson(displayName: 'M. Hoffmann'), + answers: const [ + ParentLetterAnswer( + fieldId: 'participation', + optionIds: ['yes'], + ), + ], + signed: true, + ), + ), + ], + thread: const ParentLetterThread(), + ), + letter( + id: 'demo-letter-ski-trip', + subject: 'Anmeldung zur Skifreizeit', + sender: _teacher(6), + age: const Duration(days: 35), + childIds: const [_lena], + status: ParentLetterStatus.expired, + body: + 'Liebe Eltern,\n\ndie Anmeldung zur Skifreizeit der Jahrgangsstufe ' + '10 ist ab sofort möglich. Die Plätze sind begrenzt.', + request: ParentLetterRequest( + fields: const [_participation], + signatureRequired: true, + deadline: now.subtract(const Duration(days: 14)), + ), + children: const [ + ParentLetterChildState( + childId: _lena, + status: ParentLetterStatus.expired, + ), + ], + thread: const ParentLetterThread(), + ), + ]; + } +} diff --git a/lib/api/marianumconnect/errors/marianumconnect_error.dart b/lib/api/marianumconnect/errors/marianumconnect_error.dart index 18fa4b7..58e9ff9 100644 --- a/lib/api/marianumconnect/errors/marianumconnect_error.dart +++ b/lib/api/marianumconnect/errors/marianumconnect_error.dart @@ -6,6 +6,22 @@ import '../../errors/network_exception.dart'; import '../../errors/parse_exception.dart'; import '../../errors/server_exception.dart'; +/// The error code of a rejected call: the server answers either with JSON +/// `{"error": ""}` or with the plain text `Fehler: ` of its +/// generic error handler. +String? marianumConnectErrorCode(Object? body) { + if (body is Map) { + final code = body['error']; + return code is String ? code : null; + } + if (body is String) { + const prefix = 'Fehler: '; + return (body.startsWith(prefix) ? body.substring(prefix.length) : body) + .trim(); + } + return null; +} + /// Converts a DioException raised against the Marianum-Connect API into one of /// the app's typed AppExceptions. Keeps the dio dependency out of call sites /// that just want to render an error message. diff --git a/lib/api/marianumconnect/marianumconnect_query.dart b/lib/api/marianumconnect/marianumconnect_query.dart index a69e373..8800dd6 100644 --- a/lib/api/marianumconnect/marianumconnect_query.dart +++ b/lib/api/marianumconnect/marianumconnect_query.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:dio/dio.dart'; +import '../errors/app_exception.dart'; import 'errors/marianumconnect_error.dart'; import 'marianumconnect_api.dart'; import 'marianumconnect_endpoint.dart'; @@ -23,10 +26,14 @@ abstract class MarianumConnectQuery { try { return await body(); } on DioException catch (e) { - throw mapMarianumConnectError(e); + throw mapError(e); } } + /// The AppException a failed call surfaces as. Query families with domain + /// error codes override this instead of re-implementing [guard]. + AppException mapError(DioException error) => mapMarianumConnectError(error); + /// GETs [path] and parses the JSON object body with [fromJson]. Future getObject( String path, @@ -55,6 +62,16 @@ abstract class MarianumConnectQuery { .toList(); }); + /// GETs the raw bytes of [path] (files that need the bearer token). + Future getBytes(String path) => guard(() async { + final response = await dio.get>( + endpoint(path), + options: Options(responseType: ResponseType.bytes), + ); + final bytes = response.data!; + return bytes is Uint8List ? bytes : Uint8List.fromList(bytes); + }); + /// Formats [d] as an ISO `yyyy-MM-dd` day for MarianumConnect query params. String isoDate(DateTime d) => '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; diff --git a/lib/api/marianumconnect/queries/auth_guardian/guardian_login_exception.dart b/lib/api/marianumconnect/queries/auth_guardian/guardian_login_exception.dart index 672741d..bd8c3b5 100644 --- a/lib/api/marianumconnect/queries/auth_guardian/guardian_login_exception.dart +++ b/lib/api/marianumconnect/queries/auth_guardian/guardian_login_exception.dart @@ -84,21 +84,9 @@ class GuardianLoginException extends AppException { 'Bitte versuche es später erneut.', }; - /// Accepts the documented JSON body (`{"error": …, "attemptsLeft": …}`) as - /// well as the plain-text `Fehler: ` the server's generic error - /// handler produces. static (String?, int?) _parseBody(Object? data) { - if (data is Map) { - final attempts = data['attemptsLeft']; - return (data['error'] as String?, attempts is int ? attempts : null); - } - if (data is String) { - final code = data.startsWith('Fehler: ') - ? data.substring('Fehler: '.length) - : data; - return (code.trim(), null); - } - return (null, null); + final attempts = data is Map ? data['attemptsLeft'] : null; + return (marianumConnectErrorCode(data), attempts is int ? attempts : null); } static GuardianLoginError? _errorFor(String? code, int status) => diff --git a/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart index ba530ff..0095053 100644 --- a/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart +++ b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart @@ -1,7 +1,5 @@ import 'dart:typed_data'; -import 'package:dio/dio.dart'; - import '../../marianumconnect_query.dart'; /// Downloads the raw PDF bytes of a Marianum Message from @@ -15,11 +13,6 @@ class GetNewsletterFile extends MarianumConnectQuery { GetNewsletterFile(this.id, {super.dio}); - Future run() => guard(() async { - final response = await dio.get>( - endpoint('newsletter/${Uri.encodeComponent(id)}/file'), - options: Options(responseType: ResponseType.bytes), - ); - return Uint8List.fromList(response.data!); - }); + Future run() => + getBytes('newsletter/${Uri.encodeComponent(id)}/file'); } diff --git a/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart b/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart index 011db55..f95b67f 100644 --- a/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart +++ b/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart @@ -1,7 +1,5 @@ import 'dart:typed_data'; -import 'package:dio/dio.dart'; - import '../../marianumconnect_query.dart'; /// Downloads the raw bytes of a PROXIED_FILE ticker page from @@ -15,11 +13,6 @@ class GetTickerPageFile extends MarianumConnectQuery { GetTickerPageFile(this.slug, {super.dio}); - Future run() => guard(() async { - final response = await dio.get>( - endpoint('ticker/pages/${Uri.encodeComponent(slug)}/file'), - options: Options(responseType: ResponseType.bytes), - ); - return Uint8List.fromList(response.data!); - }); + Future run() => + getBytes('ticker/pages/${Uri.encodeComponent(slug)}/file'); } diff --git a/lib/api/marianumconnect/queries/parent_letters/get_parent_letter.dart b/lib/api/marianumconnect/queries/parent_letters/get_parent_letter.dart new file mode 100644 index 0000000..c4cbe9b --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/get_parent_letter.dart @@ -0,0 +1,9 @@ +import 'parent_letter_models.dart'; +import 'parent_letter_query.dart'; + +class GetParentLetter extends ParentLetterQuery { + GetParentLetter({super.dio}); + + Future run(String letterId) => + getObject(letterPath(letterId), ParentLetterDetail.fromJson); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart b/lib/api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart new file mode 100644 index 0000000..4dc7bb9 --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart @@ -0,0 +1,14 @@ +import 'dart:typed_data'; + +import 'parent_letter_query.dart'; + +class GetParentLetterAttachment extends ParentLetterQuery { + GetParentLetterAttachment({super.dio}); + + Future run({ + required String letterId, + required String attachmentId, + }) => getBytes( + letterPath(letterId, '/attachments/${Uri.encodeComponent(attachmentId)}'), + ); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/get_parent_letters.dart b/lib/api/marianumconnect/queries/parent_letters/get_parent_letters.dart new file mode 100644 index 0000000..41e162a --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/get_parent_letters.dart @@ -0,0 +1,16 @@ +import 'parent_letter_models.dart'; +import 'parent_letter_query.dart'; + +/// `GET parent-letters`: the guardian's inbox across all children, newest +/// first. [before] continues after the given letter (keyset paging). +class GetParentLetters extends ParentLetterQuery { + static const int pageSize = 50; + + GetParentLetters({super.dio}); + + Future run({String? before}) => getObject( + 'parent-letters', + ParentLetterListResponse.fromJson, + queryParameters: {'limit': pageSize, 'before': ?before}, + ); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart b/lib/api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart new file mode 100644 index 0000000..c3c5bdc --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart @@ -0,0 +1,11 @@ +import 'parent_letter_query.dart'; + +/// `POST parent-letters/{id}/read`: marks the letter and its thread as read +/// for this guardian. Idempotent. +class MarkParentLetterRead extends ParentLetterQuery { + MarkParentLetterRead({super.dio}); + + Future run(String letterId) => guard(() async { + await dio.post(endpoint(letterPath(letterId, '/read'))); + }); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart b/lib/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart new file mode 100644 index 0000000..0aa8a3e --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart @@ -0,0 +1,90 @@ +import 'package:dio/dio.dart'; + +import '../../../errors/app_exception.dart'; +import '../../errors/marianumconnect_error.dart'; + +enum ParentLetterError { + guardianRequired, + letterNotFound, + childNotFound, + attachmentNotFound, + invalidAnswers, + signatureRequired, + responseFinal, + deadlinePassed, + threadDisabled, + invalidRequest, +} + +/// A parent-letter call the server rejected with a domain reason. +class ParentLetterException extends AppException { + final ParentLetterError error; + + const ParentLetterException( + this.error, { + required super.userMessage, + super.technicalDetails, + }) : super(allowRetry: false); + + /// Only 4xx answers with a parent-letter reason become a + /// [ParentLetterException]; everything else (network, 401, 5xx) keeps the + /// generic MarianumConnect mapping. + static AppException fromDio(DioException e) { + final response = e.response; + final status = response?.statusCode; + if (status == null || status < 400 || status >= 500 || status == 401) { + return mapMarianumConnectError(e); + } + final error = _errorFor(marianumConnectErrorCode(response!.data), status); + if (error == null) return mapMarianumConnectError(e); + return ParentLetterException( + error, + userMessage: messageFor(error), + technicalDetails: 'MC $status: ${response.data}', + ); + } + + static String messageFor(ParentLetterError error) => switch (error) { + ParentLetterError.guardianRequired => + 'Elternbriefe sind nur mit einem Eltern-Konto verfügbar.', + ParentLetterError.letterNotFound => + 'Dieser Elternbrief ist nicht mehr verfügbar.', + ParentLetterError.childNotFound => + 'Dieser Elternbrief betrifft das ausgewählte Kind nicht.', + ParentLetterError.attachmentNotFound => + 'Der Anhang ist nicht mehr verfügbar.', + ParentLetterError.invalidAnswers => + 'Die Rückmeldung ist unvollständig. Bitte prüfe deine Angaben.', + ParentLetterError.signatureRequired => + 'Für diese Rückmeldung wird eine Unterschrift benötigt.', + ParentLetterError.responseFinal => + 'Die Rückmeldung wurde bereits endgültig abgegeben und kann nicht mehr ' + 'geändert werden.', + ParentLetterError.deadlinePassed => + 'Die Frist für diese Rückmeldung ist abgelaufen.', + ParentLetterError.threadDisabled => + 'Auf diesen Elternbrief kann nicht geantwortet werden.', + ParentLetterError.invalidRequest => + 'Die Anfrage konnte nicht verarbeitet werden.', + }; + + static ParentLetterError? _errorFor(String? code, int status) => + switch (code) { + 'guardian_required' => ParentLetterError.guardianRequired, + 'letter_not_found' => ParentLetterError.letterNotFound, + 'child_not_found' => ParentLetterError.childNotFound, + 'attachment_not_found' => ParentLetterError.attachmentNotFound, + 'invalid_answers' => ParentLetterError.invalidAnswers, + 'signature_required' => ParentLetterError.signatureRequired, + 'response_final' => ParentLetterError.responseFinal, + 'deadline_passed' => ParentLetterError.deadlinePassed, + 'thread_disabled' => ParentLetterError.threadDisabled, + 'invalid_request' => ParentLetterError.invalidRequest, + _ => switch (status) { + 404 => ParentLetterError.letterNotFound, + 409 => ParentLetterError.responseFinal, + 410 => ParentLetterError.deadlinePassed, + _ => null, + }, + }; +} diff --git a/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.dart b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.dart new file mode 100644 index 0000000..7b1a1fd --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.dart @@ -0,0 +1,226 @@ +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'parent_letter_models.freezed.dart'; +part 'parent_letter_models.g.dart'; + +enum ParentLetterStatus { info, open, done, expired } + +enum ParentLetterFieldType { + @JsonValue('single_choice') + singleChoice, + unknown, +} + +/// Sender, thread author or responding guardian. [self] marks the signed-in +/// guardian. +@freezed +abstract class ParentLetterPerson with _$ParentLetterPerson { + const factory ParentLetterPerson({ + @Default('') String displayName, + @Default(false) bool self, + }) = _ParentLetterPerson; + + factory ParentLetterPerson.fromJson(Map json) => + _$ParentLetterPersonFromJson(json); +} + +/// Inbox entry from `GET parent-letters`. [childIds] are the opaque guardian +/// child ids from `me/capabilities`. +@freezed +abstract class ParentLetterSummary with _$ParentLetterSummary { + const factory ParentLetterSummary({ + required String id, + @Default('') String subject, + @Default('') String preview, + @Default(ParentLetterPerson()) ParentLetterPerson sender, + required DateTime sentAt, + DateTime? editedAt, + @Default(true) bool read, + @Default([]) List childIds, + @Default(0) int attachmentCount, + @JsonKey(unknownEnumValue: ParentLetterStatus.info) + @Default(ParentLetterStatus.info) + ParentLetterStatus status, + DateTime? deadline, + }) = _ParentLetterSummary; + + factory ParentLetterSummary.fromJson(Map json) => + _$ParentLetterSummaryFromJson(json); +} + +@freezed +abstract class ParentLetterListResponse with _$ParentLetterListResponse { + const factory ParentLetterListResponse({ + @Default([]) List items, + @Default(false) bool hasMore, + @Default(0) int unreadCount, + @Default(0) int openCount, + }) = _ParentLetterListResponse; + + factory ParentLetterListResponse.fromJson(Map json) => + _$ParentLetterListResponseFromJson(json); +} + +@freezed +abstract class ParentLetterAttachment with _$ParentLetterAttachment { + const factory ParentLetterAttachment({ + required String id, + @Default('') String fileName, + @Default('') String mimeType, + @Default(0) int size, + }) = _ParentLetterAttachment; + + factory ParentLetterAttachment.fromJson(Map json) => + _$ParentLetterAttachmentFromJson(json); +} + +@freezed +abstract class ParentLetterOption with _$ParentLetterOption { + const factory ParentLetterOption({ + required String id, + @Default('') String label, + }) = _ParentLetterOption; + + factory ParentLetterOption.fromJson(Map json) => + _$ParentLetterOptionFromJson(json); +} + +@freezed +abstract class ParentLetterField with _$ParentLetterField { + const factory ParentLetterField({ + required String id, + @JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) + @Default(ParentLetterFieldType.unknown) + ParentLetterFieldType type, + @Default('') String label, + @JsonKey(name: 'required') @Default(false) bool isRequired, + @Default([]) List options, + }) = _ParentLetterField; + + factory ParentLetterField.fromJson(Map json) => + _$ParentLetterFieldFromJson(json); +} + +/// What the sender asks for. No fields and no signature means a plain +/// acknowledgement. +@freezed +abstract class ParentLetterRequest with _$ParentLetterRequest { + const factory ParentLetterRequest({ + @Default([]) List fields, + @Default(false) bool signatureRequired, + DateTime? deadline, + }) = _ParentLetterRequest; + + factory ParentLetterRequest.fromJson(Map json) => + _$ParentLetterRequestFromJson(json); +} + +@freezed +abstract class ParentLetterAnswer with _$ParentLetterAnswer { + const factory ParentLetterAnswer({ + required String fieldId, + @Default([]) List optionIds, + String? text, + }) = _ParentLetterAnswer; + + factory ParentLetterAnswer.fromJson(Map json) => + _$ParentLetterAnswerFromJson(json); +} + +@freezed +abstract class ParentLetterResponse with _$ParentLetterResponse { + const factory ParentLetterResponse({ + DateTime? respondedAt, + @Default(ParentLetterPerson()) ParentLetterPerson respondedBy, + @Default([]) List answers, + @Default(false) bool signed, + }) = _ParentLetterResponse; + + factory ParentLetterResponse.fromJson(Map json) => + _$ParentLetterResponseFromJson(json); +} + +/// Per-child state of a letter. [editable] is the server's verdict on whether +/// this guardian may (re)submit right now. +@freezed +abstract class ParentLetterChildState with _$ParentLetterChildState { + const factory ParentLetterChildState({ + required String childId, + @JsonKey(unknownEnumValue: ParentLetterStatus.info) + @Default(ParentLetterStatus.info) + ParentLetterStatus status, + @Default(false) bool editable, + ParentLetterResponse? response, + }) = _ParentLetterChildState; + + factory ParentLetterChildState.fromJson(Map json) => + _$ParentLetterChildStateFromJson(json); +} + +@freezed +abstract class ParentLetterThreadMessage with _$ParentLetterThreadMessage { + const factory ParentLetterThreadMessage({ + required String id, + @Default(ParentLetterPerson()) ParentLetterPerson author, + @Default('') String body, + required DateTime sentAt, + }) = _ParentLetterThreadMessage; + + factory ParentLetterThreadMessage.fromJson(Map json) => + _$ParentLetterThreadMessageFromJson(json); +} + +@freezed +abstract class ParentLetterThread with _$ParentLetterThread { + const factory ParentLetterThread({ + @Default(false) bool enabled, + @Default([]) List messages, + }) = _ParentLetterThread; + + factory ParentLetterThread.fromJson(Map json) => + _$ParentLetterThreadFromJson(json); +} + +/// The detail-only part of `GET parent-letters/{id}`. +@freezed +abstract class ParentLetterContent with _$ParentLetterContent { + const factory ParentLetterContent({ + @Default('') String body, + @Default([]) List attachments, + ParentLetterRequest? request, + @Default([]) List children, + @Default(ParentLetterThread()) ParentLetterThread thread, + }) = _ParentLetterContent; + + factory ParentLetterContent.fromJson(Map json) => + _$ParentLetterContentFromJson(json); +} + +/// A full letter. The server sends the inbox fields and the detail fields in +/// one flat object; they are parsed into [summary] and [content] so the inbox +/// entry can be replaced from a detail answer without copying fields. +@immutable +class ParentLetterDetail { + final ParentLetterSummary summary; + final ParentLetterContent content; + + const ParentLetterDetail({required this.summary, required this.content}); + + factory ParentLetterDetail.fromJson(Map json) => + ParentLetterDetail( + summary: ParentLetterSummary.fromJson(json), + content: ParentLetterContent.fromJson(json), + ); + + Map toJson() => {...summary.toJson(), ...content.toJson()}; + + @override + bool operator ==(Object other) => + other is ParentLetterDetail && + other.summary == summary && + other.content == content; + + @override + int get hashCode => Object.hash(summary, content); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.freezed.dart b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.freezed.dart new file mode 100644 index 0000000..e8fbdbe --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.freezed.dart @@ -0,0 +1,3988 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parent_letter_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ParentLetterPerson implements DiagnosticableTreeMixin { + + String get displayName; bool get self; +/// Create a copy of ParentLetterPerson +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith get copyWith => _$ParentLetterPersonCopyWithImpl(this as ParentLetterPerson, _$identity); + + /// Serializes this ParentLetterPerson to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterPerson; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterPerson')) + ..add(DiagnosticsProperty('displayName', _this.displayName))..add(DiagnosticsProperty('self', _this.self)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterPerson; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterPerson&&(identical(other.displayName, _this.displayName) || other.displayName == _this.displayName)&&(identical(other.self, _this.self) || other.self == _this.self)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterPerson; + return Object.hash(runtimeType,_this.displayName,_this.self); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterPerson; + return 'ParentLetterPerson(displayName: ${_this.displayName}, self: ${_this.self})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterPersonCopyWith<$Res> { + factory $ParentLetterPersonCopyWith(ParentLetterPerson value, $Res Function(ParentLetterPerson) _then) = _$ParentLetterPersonCopyWithImpl; +@useResult +$Res call({ + String displayName, bool self +}); + + + + +} +/// @nodoc +class _$ParentLetterPersonCopyWithImpl<$Res> + implements $ParentLetterPersonCopyWith<$Res> { + _$ParentLetterPersonCopyWithImpl(this._self, this._then); + + final ParentLetterPerson _self; + final $Res Function(ParentLetterPerson) _then; + +/// Create a copy of ParentLetterPerson +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? displayName = null,Object? self = null,}) { + return _then(ParentLetterPerson( +displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String,self: null == self ? _self.self : self // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterPerson]. +extension ParentLetterPersonPatterns on ParentLetterPerson { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterPerson value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterPerson() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterPerson value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterPerson(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterPerson value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterPerson() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String displayName, bool self)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterPerson() when $default != null: +return $default(_that.displayName,_that.self);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String displayName, bool self) $default,) {final _that = this; +switch (_that) { +case _ParentLetterPerson(): +return $default(_that.displayName,_that.self);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String displayName, bool self)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterPerson() when $default != null: +return $default(_that.displayName,_that.self);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterPerson with DiagnosticableTreeMixin implements ParentLetterPerson { + const _ParentLetterPerson({this.displayName = '', this.self = false}); + factory _ParentLetterPerson.fromJson(Map json) => _$ParentLetterPersonFromJson(json); + +@override@JsonKey() final String displayName; +@override@JsonKey() final bool self; + +/// Create a copy of ParentLetterPerson +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterPersonCopyWith<_ParentLetterPerson> get copyWith => __$ParentLetterPersonCopyWithImpl<_ParentLetterPerson>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterPersonToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterPerson')) + ..add(DiagnosticsProperty('displayName', displayName))..add(DiagnosticsProperty('self', self)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterPerson&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.self, self) || other.self == self)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,displayName,self); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterPerson(displayName: $displayName, self: $self)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterPersonCopyWith<$Res> implements $ParentLetterPersonCopyWith<$Res> { + factory _$ParentLetterPersonCopyWith(_ParentLetterPerson value, $Res Function(_ParentLetterPerson) _then) = __$ParentLetterPersonCopyWithImpl; +@override @useResult +$Res call({ + String displayName, bool self +}); + + + + +} +/// @nodoc +class __$ParentLetterPersonCopyWithImpl<$Res> + implements _$ParentLetterPersonCopyWith<$Res> { + __$ParentLetterPersonCopyWithImpl(this._self, this._then); + + final _ParentLetterPerson _self; + final $Res Function(_ParentLetterPerson) _then; + +/// Create a copy of ParentLetterPerson +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? displayName = null,Object? self = null,}) { + return _then(_ParentLetterPerson( +displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String,self: null == self ? _self.self : self // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterSummary implements DiagnosticableTreeMixin { + + String get id; String get subject; String get preview; ParentLetterPerson get sender; DateTime get sentAt; DateTime? get editedAt; bool get read; List get childIds; int get attachmentCount;@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus get status; DateTime? get deadline; +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterSummaryCopyWith get copyWith => _$ParentLetterSummaryCopyWithImpl(this as ParentLetterSummary, _$identity); + + /// Serializes this ParentLetterSummary to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterSummary; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterSummary')) + ..add(DiagnosticsProperty('id', _this.id))..add(DiagnosticsProperty('subject', _this.subject))..add(DiagnosticsProperty('preview', _this.preview))..add(DiagnosticsProperty('sender', _this.sender))..add(DiagnosticsProperty('sentAt', _this.sentAt))..add(DiagnosticsProperty('editedAt', _this.editedAt))..add(DiagnosticsProperty('read', _this.read))..add(DiagnosticsProperty('childIds', _this.childIds))..add(DiagnosticsProperty('attachmentCount', _this.attachmentCount))..add(DiagnosticsProperty('status', _this.status))..add(DiagnosticsProperty('deadline', _this.deadline)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterSummary; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterSummary&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.subject, _this.subject) || other.subject == _this.subject)&&(identical(other.preview, _this.preview) || other.preview == _this.preview)&&(identical(other.sender, _this.sender) || other.sender == _this.sender)&&(identical(other.sentAt, _this.sentAt) || other.sentAt == _this.sentAt)&&(identical(other.editedAt, _this.editedAt) || other.editedAt == _this.editedAt)&&(identical(other.read, _this.read) || other.read == _this.read)&&const DeepCollectionEquality().equals(other.childIds, _this.childIds)&&(identical(other.attachmentCount, _this.attachmentCount) || other.attachmentCount == _this.attachmentCount)&&(identical(other.status, _this.status) || other.status == _this.status)&&(identical(other.deadline, _this.deadline) || other.deadline == _this.deadline)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterSummary; + return Object.hash(runtimeType,_this.id,_this.subject,_this.preview,_this.sender,_this.sentAt,_this.editedAt,_this.read,const DeepCollectionEquality().hash(_this.childIds),_this.attachmentCount,_this.status,_this.deadline); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterSummary; + return 'ParentLetterSummary(id: ${_this.id}, subject: ${_this.subject}, preview: ${_this.preview}, sender: ${_this.sender}, sentAt: ${_this.sentAt}, editedAt: ${_this.editedAt}, read: ${_this.read}, childIds: ${_this.childIds}, attachmentCount: ${_this.attachmentCount}, status: ${_this.status}, deadline: ${_this.deadline})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterSummaryCopyWith<$Res> { + factory $ParentLetterSummaryCopyWith(ParentLetterSummary value, $Res Function(ParentLetterSummary) _then) = _$ParentLetterSummaryCopyWithImpl; +@useResult +$Res call({ + String id, String subject, String preview, ParentLetterPerson sender, DateTime sentAt, DateTime? editedAt, bool read, List childIds, int attachmentCount,@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, DateTime? deadline +}); + + +$ParentLetterPersonCopyWith<$Res> get sender; + +} +/// @nodoc +class _$ParentLetterSummaryCopyWithImpl<$Res> + implements $ParentLetterSummaryCopyWith<$Res> { + _$ParentLetterSummaryCopyWithImpl(this._self, this._then); + + final ParentLetterSummary _self; + final $Res Function(ParentLetterSummary) _then; + +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? subject = null,Object? preview = null,Object? sender = null,Object? sentAt = null,Object? editedAt = freezed,Object? read = null,Object? childIds = null,Object? attachmentCount = null,Object? status = null,Object? deadline = freezed,}) { + return _then(ParentLetterSummary( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,preview: null == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable +as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,sentAt: null == sentAt ? _self.sentAt : sentAt // ignore: cast_nullable_to_non_nullable +as DateTime,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,read: null == read ? _self.read : read // ignore: cast_nullable_to_non_nullable +as bool,childIds: null == childIds ? _self.childIds : childIds // ignore: cast_nullable_to_non_nullable +as List,attachmentCount: null == attachmentCount ? _self.attachmentCount : attachmentCount // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ParentLetterStatus,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get sender { + + return $ParentLetterPersonCopyWith<$Res>(_self.sender, (value) { + return _then(_self.copyWith(sender: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ParentLetterSummary]. +extension ParentLetterSummaryPatterns on ParentLetterSummary { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterSummary value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterSummary() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterSummary value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterSummary(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterSummary value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterSummary() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String subject, String preview, ParentLetterPerson sender, DateTime sentAt, DateTime? editedAt, bool read, List childIds, int attachmentCount, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, DateTime? deadline)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterSummary() when $default != null: +return $default(_that.id,_that.subject,_that.preview,_that.sender,_that.sentAt,_that.editedAt,_that.read,_that.childIds,_that.attachmentCount,_that.status,_that.deadline);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String subject, String preview, ParentLetterPerson sender, DateTime sentAt, DateTime? editedAt, bool read, List childIds, int attachmentCount, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, DateTime? deadline) $default,) {final _that = this; +switch (_that) { +case _ParentLetterSummary(): +return $default(_that.id,_that.subject,_that.preview,_that.sender,_that.sentAt,_that.editedAt,_that.read,_that.childIds,_that.attachmentCount,_that.status,_that.deadline);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String subject, String preview, ParentLetterPerson sender, DateTime sentAt, DateTime? editedAt, bool read, List childIds, int attachmentCount, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, DateTime? deadline)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterSummary() when $default != null: +return $default(_that.id,_that.subject,_that.preview,_that.sender,_that.sentAt,_that.editedAt,_that.read,_that.childIds,_that.attachmentCount,_that.status,_that.deadline);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterSummary with DiagnosticableTreeMixin implements ParentLetterSummary { + const _ParentLetterSummary({required this.id, this.subject = '', this.preview = '', this.sender = const ParentLetterPerson(), required this.sentAt, this.editedAt, this.read = true, List childIds = const [], this.attachmentCount = 0, @JsonKey(unknownEnumValue: ParentLetterStatus.info) this.status = ParentLetterStatus.info, this.deadline}): _childIds = childIds; + factory _ParentLetterSummary.fromJson(Map json) => _$ParentLetterSummaryFromJson(json); + +@override final String id; +@override@JsonKey() final String subject; +@override@JsonKey() final String preview; +@override@JsonKey() final ParentLetterPerson sender; +@override final DateTime sentAt; +@override final DateTime? editedAt; +@override@JsonKey() final bool read; + final List _childIds; +@override@JsonKey() List get childIds { + if (_childIds is EqualUnmodifiableListView) return _childIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_childIds); +} + +@override@JsonKey() final int attachmentCount; +@override@JsonKey(unknownEnumValue: ParentLetterStatus.info) final ParentLetterStatus status; +@override final DateTime? deadline; + +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterSummaryCopyWith<_ParentLetterSummary> get copyWith => __$ParentLetterSummaryCopyWithImpl<_ParentLetterSummary>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterSummaryToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterSummary')) + ..add(DiagnosticsProperty('id', id))..add(DiagnosticsProperty('subject', subject))..add(DiagnosticsProperty('preview', preview))..add(DiagnosticsProperty('sender', sender))..add(DiagnosticsProperty('sentAt', sentAt))..add(DiagnosticsProperty('editedAt', editedAt))..add(DiagnosticsProperty('read', read))..add(DiagnosticsProperty('childIds', childIds))..add(DiagnosticsProperty('attachmentCount', attachmentCount))..add(DiagnosticsProperty('status', status))..add(DiagnosticsProperty('deadline', deadline)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterSummary&&(identical(other.id, id) || other.id == id)&&(identical(other.subject, subject) || other.subject == subject)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.sentAt, sentAt) || other.sentAt == sentAt)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.read, read) || other.read == read)&&const DeepCollectionEquality().equals(other.childIds, _childIds)&&(identical(other.attachmentCount, attachmentCount) || other.attachmentCount == attachmentCount)&&(identical(other.status, status) || other.status == status)&&(identical(other.deadline, deadline) || other.deadline == deadline)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,id,subject,preview,sender,sentAt,editedAt,read,const DeepCollectionEquality().hash(_childIds),attachmentCount,status,deadline); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterSummary(id: $id, subject: $subject, preview: $preview, sender: $sender, sentAt: $sentAt, editedAt: $editedAt, read: $read, childIds: $childIds, attachmentCount: $attachmentCount, status: $status, deadline: $deadline)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterSummaryCopyWith<$Res> implements $ParentLetterSummaryCopyWith<$Res> { + factory _$ParentLetterSummaryCopyWith(_ParentLetterSummary value, $Res Function(_ParentLetterSummary) _then) = __$ParentLetterSummaryCopyWithImpl; +@override @useResult +$Res call({ + String id, String subject, String preview, ParentLetterPerson sender, DateTime sentAt, DateTime? editedAt, bool read, List childIds, int attachmentCount,@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, DateTime? deadline +}); + + +@override $ParentLetterPersonCopyWith<$Res> get sender; + +} +/// @nodoc +class __$ParentLetterSummaryCopyWithImpl<$Res> + implements _$ParentLetterSummaryCopyWith<$Res> { + __$ParentLetterSummaryCopyWithImpl(this._self, this._then); + + final _ParentLetterSummary _self; + final $Res Function(_ParentLetterSummary) _then; + +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? subject = null,Object? preview = null,Object? sender = null,Object? sentAt = null,Object? editedAt = freezed,Object? read = null,Object? childIds = null,Object? attachmentCount = null,Object? status = null,Object? deadline = freezed,}) { + return _then(_ParentLetterSummary( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,subject: null == subject ? _self.subject : subject // ignore: cast_nullable_to_non_nullable +as String,preview: null == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable +as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,sentAt: null == sentAt ? _self.sentAt : sentAt // ignore: cast_nullable_to_non_nullable +as DateTime,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,read: null == read ? _self.read : read // ignore: cast_nullable_to_non_nullable +as bool,childIds: null == childIds ? _self._childIds : childIds // ignore: cast_nullable_to_non_nullable +as List,attachmentCount: null == attachmentCount ? _self.attachmentCount : attachmentCount // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ParentLetterStatus,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +/// Create a copy of ParentLetterSummary +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get sender { + + return $ParentLetterPersonCopyWith<$Res>(_self.sender, (value) { + return _then(_self.copyWith(sender: value)); + }); +} +} + + +/// @nodoc +mixin _$ParentLetterListResponse implements DiagnosticableTreeMixin { + + List get items; bool get hasMore; int get unreadCount; int get openCount; +/// Create a copy of ParentLetterListResponse +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterListResponseCopyWith get copyWith => _$ParentLetterListResponseCopyWithImpl(this as ParentLetterListResponse, _$identity); + + /// Serializes this ParentLetterListResponse to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterListResponse; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterListResponse')) + ..add(DiagnosticsProperty('items', _this.items))..add(DiagnosticsProperty('hasMore', _this.hasMore))..add(DiagnosticsProperty('unreadCount', _this.unreadCount))..add(DiagnosticsProperty('openCount', _this.openCount)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterListResponse; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterListResponse&&const DeepCollectionEquality().equals(other.items, _this.items)&&(identical(other.hasMore, _this.hasMore) || other.hasMore == _this.hasMore)&&(identical(other.unreadCount, _this.unreadCount) || other.unreadCount == _this.unreadCount)&&(identical(other.openCount, _this.openCount) || other.openCount == _this.openCount)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterListResponse; + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.items),_this.hasMore,_this.unreadCount,_this.openCount); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterListResponse; + return 'ParentLetterListResponse(items: ${_this.items}, hasMore: ${_this.hasMore}, unreadCount: ${_this.unreadCount}, openCount: ${_this.openCount})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterListResponseCopyWith<$Res> { + factory $ParentLetterListResponseCopyWith(ParentLetterListResponse value, $Res Function(ParentLetterListResponse) _then) = _$ParentLetterListResponseCopyWithImpl; +@useResult +$Res call({ + List items, bool hasMore, int unreadCount, int openCount +}); + + + + +} +/// @nodoc +class _$ParentLetterListResponseCopyWithImpl<$Res> + implements $ParentLetterListResponseCopyWith<$Res> { + _$ParentLetterListResponseCopyWithImpl(this._self, this._then); + + final ParentLetterListResponse _self; + final $Res Function(ParentLetterListResponse) _then; + +/// Create a copy of ParentLetterListResponse +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? items = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) { + return _then(ParentLetterListResponse( +items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable +as List,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterListResponse]. +extension ParentLetterListResponsePatterns on ParentLetterListResponse { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterListResponse value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterListResponse() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterListResponse value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterListResponse(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterListResponse value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterListResponse() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List items, bool hasMore, int unreadCount, int openCount)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterListResponse() when $default != null: +return $default(_that.items,_that.hasMore,_that.unreadCount,_that.openCount);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List items, bool hasMore, int unreadCount, int openCount) $default,) {final _that = this; +switch (_that) { +case _ParentLetterListResponse(): +return $default(_that.items,_that.hasMore,_that.unreadCount,_that.openCount);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List items, bool hasMore, int unreadCount, int openCount)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterListResponse() when $default != null: +return $default(_that.items,_that.hasMore,_that.unreadCount,_that.openCount);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterListResponse with DiagnosticableTreeMixin implements ParentLetterListResponse { + const _ParentLetterListResponse({ List items = const [], this.hasMore = false, this.unreadCount = 0, this.openCount = 0}): _items = items; + factory _ParentLetterListResponse.fromJson(Map json) => _$ParentLetterListResponseFromJson(json); + + final List _items; +@override@JsonKey() List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); +} + +@override@JsonKey() final bool hasMore; +@override@JsonKey() final int unreadCount; +@override@JsonKey() final int openCount; + +/// Create a copy of ParentLetterListResponse +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterListResponseCopyWith<_ParentLetterListResponse> get copyWith => __$ParentLetterListResponseCopyWithImpl<_ParentLetterListResponse>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterListResponseToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterListResponse')) + ..add(DiagnosticsProperty('items', items))..add(DiagnosticsProperty('hasMore', hasMore))..add(DiagnosticsProperty('unreadCount', unreadCount))..add(DiagnosticsProperty('openCount', openCount)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterListResponse&&const DeepCollectionEquality().equals(other.items, _items)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)&&(identical(other.openCount, openCount) || other.openCount == openCount)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),hasMore,unreadCount,openCount); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterListResponse(items: $items, hasMore: $hasMore, unreadCount: $unreadCount, openCount: $openCount)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterListResponseCopyWith<$Res> implements $ParentLetterListResponseCopyWith<$Res> { + factory _$ParentLetterListResponseCopyWith(_ParentLetterListResponse value, $Res Function(_ParentLetterListResponse) _then) = __$ParentLetterListResponseCopyWithImpl; +@override @useResult +$Res call({ + List items, bool hasMore, int unreadCount, int openCount +}); + + + + +} +/// @nodoc +class __$ParentLetterListResponseCopyWithImpl<$Res> + implements _$ParentLetterListResponseCopyWith<$Res> { + __$ParentLetterListResponseCopyWithImpl(this._self, this._then); + + final _ParentLetterListResponse _self; + final $Res Function(_ParentLetterListResponse) _then; + +/// Create a copy of ParentLetterListResponse +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? items = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) { + return _then(_ParentLetterListResponse( +items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable +as List,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterAttachment implements DiagnosticableTreeMixin { + + String get id; String get fileName; String get mimeType; int get size; +/// Create a copy of ParentLetterAttachment +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterAttachmentCopyWith get copyWith => _$ParentLetterAttachmentCopyWithImpl(this as ParentLetterAttachment, _$identity); + + /// Serializes this ParentLetterAttachment to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterAttachment; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterAttachment')) + ..add(DiagnosticsProperty('id', _this.id))..add(DiagnosticsProperty('fileName', _this.fileName))..add(DiagnosticsProperty('mimeType', _this.mimeType))..add(DiagnosticsProperty('size', _this.size)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterAttachment; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterAttachment&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.fileName, _this.fileName) || other.fileName == _this.fileName)&&(identical(other.mimeType, _this.mimeType) || other.mimeType == _this.mimeType)&&(identical(other.size, _this.size) || other.size == _this.size)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterAttachment; + return Object.hash(runtimeType,_this.id,_this.fileName,_this.mimeType,_this.size); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterAttachment; + return 'ParentLetterAttachment(id: ${_this.id}, fileName: ${_this.fileName}, mimeType: ${_this.mimeType}, size: ${_this.size})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterAttachmentCopyWith<$Res> { + factory $ParentLetterAttachmentCopyWith(ParentLetterAttachment value, $Res Function(ParentLetterAttachment) _then) = _$ParentLetterAttachmentCopyWithImpl; +@useResult +$Res call({ + String id, String fileName, String mimeType, int size +}); + + + + +} +/// @nodoc +class _$ParentLetterAttachmentCopyWithImpl<$Res> + implements $ParentLetterAttachmentCopyWith<$Res> { + _$ParentLetterAttachmentCopyWithImpl(this._self, this._then); + + final ParentLetterAttachment _self; + final $Res Function(ParentLetterAttachment) _then; + +/// Create a copy of ParentLetterAttachment +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fileName = null,Object? mimeType = null,Object? size = null,}) { + return _then(ParentLetterAttachment( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable +as String,mimeType: null == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable +as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterAttachment]. +extension ParentLetterAttachmentPatterns on ParentLetterAttachment { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterAttachment value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterAttachment() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterAttachment value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterAttachment(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterAttachment value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterAttachment() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String fileName, String mimeType, int size)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterAttachment() when $default != null: +return $default(_that.id,_that.fileName,_that.mimeType,_that.size);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String fileName, String mimeType, int size) $default,) {final _that = this; +switch (_that) { +case _ParentLetterAttachment(): +return $default(_that.id,_that.fileName,_that.mimeType,_that.size);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String fileName, String mimeType, int size)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterAttachment() when $default != null: +return $default(_that.id,_that.fileName,_that.mimeType,_that.size);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterAttachment with DiagnosticableTreeMixin implements ParentLetterAttachment { + const _ParentLetterAttachment({required this.id, this.fileName = '', this.mimeType = '', this.size = 0}); + factory _ParentLetterAttachment.fromJson(Map json) => _$ParentLetterAttachmentFromJson(json); + +@override final String id; +@override@JsonKey() final String fileName; +@override@JsonKey() final String mimeType; +@override@JsonKey() final int size; + +/// Create a copy of ParentLetterAttachment +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterAttachmentCopyWith<_ParentLetterAttachment> get copyWith => __$ParentLetterAttachmentCopyWithImpl<_ParentLetterAttachment>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterAttachmentToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterAttachment')) + ..add(DiagnosticsProperty('id', id))..add(DiagnosticsProperty('fileName', fileName))..add(DiagnosticsProperty('mimeType', mimeType))..add(DiagnosticsProperty('size', size)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterAttachment&&(identical(other.id, id) || other.id == id)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.size, size) || other.size == size)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,id,fileName,mimeType,size); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterAttachment(id: $id, fileName: $fileName, mimeType: $mimeType, size: $size)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterAttachmentCopyWith<$Res> implements $ParentLetterAttachmentCopyWith<$Res> { + factory _$ParentLetterAttachmentCopyWith(_ParentLetterAttachment value, $Res Function(_ParentLetterAttachment) _then) = __$ParentLetterAttachmentCopyWithImpl; +@override @useResult +$Res call({ + String id, String fileName, String mimeType, int size +}); + + + + +} +/// @nodoc +class __$ParentLetterAttachmentCopyWithImpl<$Res> + implements _$ParentLetterAttachmentCopyWith<$Res> { + __$ParentLetterAttachmentCopyWithImpl(this._self, this._then); + + final _ParentLetterAttachment _self; + final $Res Function(_ParentLetterAttachment) _then; + +/// Create a copy of ParentLetterAttachment +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fileName = null,Object? mimeType = null,Object? size = null,}) { + return _then(_ParentLetterAttachment( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable +as String,mimeType: null == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable +as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterOption implements DiagnosticableTreeMixin { + + String get id; String get label; +/// Create a copy of ParentLetterOption +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterOptionCopyWith get copyWith => _$ParentLetterOptionCopyWithImpl(this as ParentLetterOption, _$identity); + + /// Serializes this ParentLetterOption to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterOption; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterOption')) + ..add(DiagnosticsProperty('id', _this.id))..add(DiagnosticsProperty('label', _this.label)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterOption; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterOption&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.label, _this.label) || other.label == _this.label)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterOption; + return Object.hash(runtimeType,_this.id,_this.label); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterOption; + return 'ParentLetterOption(id: ${_this.id}, label: ${_this.label})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterOptionCopyWith<$Res> { + factory $ParentLetterOptionCopyWith(ParentLetterOption value, $Res Function(ParentLetterOption) _then) = _$ParentLetterOptionCopyWithImpl; +@useResult +$Res call({ + String id, String label +}); + + + + +} +/// @nodoc +class _$ParentLetterOptionCopyWithImpl<$Res> + implements $ParentLetterOptionCopyWith<$Res> { + _$ParentLetterOptionCopyWithImpl(this._self, this._then); + + final ParentLetterOption _self; + final $Res Function(ParentLetterOption) _then; + +/// Create a copy of ParentLetterOption +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = null,}) { + return _then(ParentLetterOption( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterOption]. +extension ParentLetterOptionPatterns on ParentLetterOption { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterOption value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterOption() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterOption value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterOption(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterOption value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterOption() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String label)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterOption() when $default != null: +return $default(_that.id,_that.label);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String label) $default,) {final _that = this; +switch (_that) { +case _ParentLetterOption(): +return $default(_that.id,_that.label);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String label)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterOption() when $default != null: +return $default(_that.id,_that.label);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterOption with DiagnosticableTreeMixin implements ParentLetterOption { + const _ParentLetterOption({required this.id, this.label = ''}); + factory _ParentLetterOption.fromJson(Map json) => _$ParentLetterOptionFromJson(json); + +@override final String id; +@override@JsonKey() final String label; + +/// Create a copy of ParentLetterOption +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterOptionCopyWith<_ParentLetterOption> get copyWith => __$ParentLetterOptionCopyWithImpl<_ParentLetterOption>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterOptionToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterOption')) + ..add(DiagnosticsProperty('id', id))..add(DiagnosticsProperty('label', label)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterOption&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,id,label); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterOption(id: $id, label: $label)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterOptionCopyWith<$Res> implements $ParentLetterOptionCopyWith<$Res> { + factory _$ParentLetterOptionCopyWith(_ParentLetterOption value, $Res Function(_ParentLetterOption) _then) = __$ParentLetterOptionCopyWithImpl; +@override @useResult +$Res call({ + String id, String label +}); + + + + +} +/// @nodoc +class __$ParentLetterOptionCopyWithImpl<$Res> + implements _$ParentLetterOptionCopyWith<$Res> { + __$ParentLetterOptionCopyWithImpl(this._self, this._then); + + final _ParentLetterOption _self; + final $Res Function(_ParentLetterOption) _then; + +/// Create a copy of ParentLetterOption +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = null,}) { + return _then(_ParentLetterOption( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterField implements DiagnosticableTreeMixin { + + String get id;@JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType get type; String get label;@JsonKey(name: 'required') bool get isRequired; List get options; +/// Create a copy of ParentLetterField +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterFieldCopyWith get copyWith => _$ParentLetterFieldCopyWithImpl(this as ParentLetterField, _$identity); + + /// Serializes this ParentLetterField to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterField; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterField')) + ..add(DiagnosticsProperty('id', _this.id))..add(DiagnosticsProperty('type', _this.type))..add(DiagnosticsProperty('label', _this.label))..add(DiagnosticsProperty('isRequired', _this.isRequired))..add(DiagnosticsProperty('options', _this.options)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterField; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterField&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.type, _this.type) || other.type == _this.type)&&(identical(other.label, _this.label) || other.label == _this.label)&&(identical(other.isRequired, _this.isRequired) || other.isRequired == _this.isRequired)&&const DeepCollectionEquality().equals(other.options, _this.options)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterField; + return Object.hash(runtimeType,_this.id,_this.type,_this.label,_this.isRequired,const DeepCollectionEquality().hash(_this.options)); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterField; + return 'ParentLetterField(id: ${_this.id}, type: ${_this.type}, label: ${_this.label}, isRequired: ${_this.isRequired}, options: ${_this.options})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterFieldCopyWith<$Res> { + factory $ParentLetterFieldCopyWith(ParentLetterField value, $Res Function(ParentLetterField) _then) = _$ParentLetterFieldCopyWithImpl; +@useResult +$Res call({ + String id,@JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType type, String label,@JsonKey(name: 'required') bool isRequired, List options +}); + + + + +} +/// @nodoc +class _$ParentLetterFieldCopyWithImpl<$Res> + implements $ParentLetterFieldCopyWith<$Res> { + _$ParentLetterFieldCopyWithImpl(this._self, this._then); + + final ParentLetterField _self; + final $Res Function(ParentLetterField) _then; + +/// Create a copy of ParentLetterField +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = null,Object? isRequired = null,Object? options = null,}) { + return _then(ParentLetterField( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ParentLetterFieldType,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable +as String,isRequired: null == isRequired ? _self.isRequired : isRequired // ignore: cast_nullable_to_non_nullable +as bool,options: null == options ? _self.options : options // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterField]. +extension ParentLetterFieldPatterns on ParentLetterField { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterField value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterField() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterField value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterField(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterField value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterField() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, @JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType type, String label, @JsonKey(name: 'required') bool isRequired, List options)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterField() when $default != null: +return $default(_that.id,_that.type,_that.label,_that.isRequired,_that.options);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, @JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType type, String label, @JsonKey(name: 'required') bool isRequired, List options) $default,) {final _that = this; +switch (_that) { +case _ParentLetterField(): +return $default(_that.id,_that.type,_that.label,_that.isRequired,_that.options);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, @JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType type, String label, @JsonKey(name: 'required') bool isRequired, List options)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterField() when $default != null: +return $default(_that.id,_that.type,_that.label,_that.isRequired,_that.options);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterField with DiagnosticableTreeMixin implements ParentLetterField { + const _ParentLetterField({required this.id, @JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) this.type = ParentLetterFieldType.unknown, this.label = '', @JsonKey(name: 'required') this.isRequired = false, List options = const []}): _options = options; + factory _ParentLetterField.fromJson(Map json) => _$ParentLetterFieldFromJson(json); + +@override final String id; +@override@JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) final ParentLetterFieldType type; +@override@JsonKey() final String label; +@override@JsonKey(name: 'required') final bool isRequired; + final List _options; +@override@JsonKey() List get options { + if (_options is EqualUnmodifiableListView) return _options; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_options); +} + + +/// Create a copy of ParentLetterField +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterFieldCopyWith<_ParentLetterField> get copyWith => __$ParentLetterFieldCopyWithImpl<_ParentLetterField>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterFieldToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterField')) + ..add(DiagnosticsProperty('id', id))..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('label', label))..add(DiagnosticsProperty('isRequired', isRequired))..add(DiagnosticsProperty('options', options)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterField&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.isRequired, isRequired) || other.isRequired == isRequired)&&const DeepCollectionEquality().equals(other.options, _options)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,id,type,label,isRequired,const DeepCollectionEquality().hash(_options)); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterField(id: $id, type: $type, label: $label, isRequired: $isRequired, options: $options)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterFieldCopyWith<$Res> implements $ParentLetterFieldCopyWith<$Res> { + factory _$ParentLetterFieldCopyWith(_ParentLetterField value, $Res Function(_ParentLetterField) _then) = __$ParentLetterFieldCopyWithImpl; +@override @useResult +$Res call({ + String id,@JsonKey(unknownEnumValue: ParentLetterFieldType.unknown) ParentLetterFieldType type, String label,@JsonKey(name: 'required') bool isRequired, List options +}); + + + + +} +/// @nodoc +class __$ParentLetterFieldCopyWithImpl<$Res> + implements _$ParentLetterFieldCopyWith<$Res> { + __$ParentLetterFieldCopyWithImpl(this._self, this._then); + + final _ParentLetterField _self; + final $Res Function(_ParentLetterField) _then; + +/// Create a copy of ParentLetterField +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = null,Object? isRequired = null,Object? options = null,}) { + return _then(_ParentLetterField( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ParentLetterFieldType,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable +as String,isRequired: null == isRequired ? _self.isRequired : isRequired // ignore: cast_nullable_to_non_nullable +as bool,options: null == options ? _self._options : options // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterRequest implements DiagnosticableTreeMixin { + + List get fields; bool get signatureRequired; DateTime? get deadline; +/// Create a copy of ParentLetterRequest +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterRequestCopyWith get copyWith => _$ParentLetterRequestCopyWithImpl(this as ParentLetterRequest, _$identity); + + /// Serializes this ParentLetterRequest to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterRequest; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterRequest')) + ..add(DiagnosticsProperty('fields', _this.fields))..add(DiagnosticsProperty('signatureRequired', _this.signatureRequired))..add(DiagnosticsProperty('deadline', _this.deadline)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterRequest; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterRequest&&const DeepCollectionEquality().equals(other.fields, _this.fields)&&(identical(other.signatureRequired, _this.signatureRequired) || other.signatureRequired == _this.signatureRequired)&&(identical(other.deadline, _this.deadline) || other.deadline == _this.deadline)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterRequest; + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.fields),_this.signatureRequired,_this.deadline); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterRequest; + return 'ParentLetterRequest(fields: ${_this.fields}, signatureRequired: ${_this.signatureRequired}, deadline: ${_this.deadline})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterRequestCopyWith<$Res> { + factory $ParentLetterRequestCopyWith(ParentLetterRequest value, $Res Function(ParentLetterRequest) _then) = _$ParentLetterRequestCopyWithImpl; +@useResult +$Res call({ + List fields, bool signatureRequired, DateTime? deadline +}); + + + + +} +/// @nodoc +class _$ParentLetterRequestCopyWithImpl<$Res> + implements $ParentLetterRequestCopyWith<$Res> { + _$ParentLetterRequestCopyWithImpl(this._self, this._then); + + final ParentLetterRequest _self; + final $Res Function(ParentLetterRequest) _then; + +/// Create a copy of ParentLetterRequest +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? fields = null,Object? signatureRequired = null,Object? deadline = freezed,}) { + return _then(ParentLetterRequest( +fields: null == fields ? _self.fields : fields // ignore: cast_nullable_to_non_nullable +as List,signatureRequired: null == signatureRequired ? _self.signatureRequired : signatureRequired // ignore: cast_nullable_to_non_nullable +as bool,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterRequest]. +extension ParentLetterRequestPatterns on ParentLetterRequest { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterRequest value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterRequest() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterRequest value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterRequest(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterRequest value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterRequest() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List fields, bool signatureRequired, DateTime? deadline)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterRequest() when $default != null: +return $default(_that.fields,_that.signatureRequired,_that.deadline);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List fields, bool signatureRequired, DateTime? deadline) $default,) {final _that = this; +switch (_that) { +case _ParentLetterRequest(): +return $default(_that.fields,_that.signatureRequired,_that.deadline);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List fields, bool signatureRequired, DateTime? deadline)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterRequest() when $default != null: +return $default(_that.fields,_that.signatureRequired,_that.deadline);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterRequest with DiagnosticableTreeMixin implements ParentLetterRequest { + const _ParentLetterRequest({ List fields = const [], this.signatureRequired = false, this.deadline}): _fields = fields; + factory _ParentLetterRequest.fromJson(Map json) => _$ParentLetterRequestFromJson(json); + + final List _fields; +@override@JsonKey() List get fields { + if (_fields is EqualUnmodifiableListView) return _fields; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_fields); +} + +@override@JsonKey() final bool signatureRequired; +@override final DateTime? deadline; + +/// Create a copy of ParentLetterRequest +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterRequestCopyWith<_ParentLetterRequest> get copyWith => __$ParentLetterRequestCopyWithImpl<_ParentLetterRequest>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterRequestToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterRequest')) + ..add(DiagnosticsProperty('fields', fields))..add(DiagnosticsProperty('signatureRequired', signatureRequired))..add(DiagnosticsProperty('deadline', deadline)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterRequest&&const DeepCollectionEquality().equals(other.fields, _fields)&&(identical(other.signatureRequired, signatureRequired) || other.signatureRequired == signatureRequired)&&(identical(other.deadline, deadline) || other.deadline == deadline)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_fields),signatureRequired,deadline); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterRequest(fields: $fields, signatureRequired: $signatureRequired, deadline: $deadline)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterRequestCopyWith<$Res> implements $ParentLetterRequestCopyWith<$Res> { + factory _$ParentLetterRequestCopyWith(_ParentLetterRequest value, $Res Function(_ParentLetterRequest) _then) = __$ParentLetterRequestCopyWithImpl; +@override @useResult +$Res call({ + List fields, bool signatureRequired, DateTime? deadline +}); + + + + +} +/// @nodoc +class __$ParentLetterRequestCopyWithImpl<$Res> + implements _$ParentLetterRequestCopyWith<$Res> { + __$ParentLetterRequestCopyWithImpl(this._self, this._then); + + final _ParentLetterRequest _self; + final $Res Function(_ParentLetterRequest) _then; + +/// Create a copy of ParentLetterRequest +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? fields = null,Object? signatureRequired = null,Object? deadline = freezed,}) { + return _then(_ParentLetterRequest( +fields: null == fields ? _self._fields : fields // ignore: cast_nullable_to_non_nullable +as List,signatureRequired: null == signatureRequired ? _self.signatureRequired : signatureRequired // ignore: cast_nullable_to_non_nullable +as bool,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterAnswer implements DiagnosticableTreeMixin { + + String get fieldId; List get optionIds; String? get text; +/// Create a copy of ParentLetterAnswer +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterAnswerCopyWith get copyWith => _$ParentLetterAnswerCopyWithImpl(this as ParentLetterAnswer, _$identity); + + /// Serializes this ParentLetterAnswer to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterAnswer; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterAnswer')) + ..add(DiagnosticsProperty('fieldId', _this.fieldId))..add(DiagnosticsProperty('optionIds', _this.optionIds))..add(DiagnosticsProperty('text', _this.text)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterAnswer; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterAnswer&&(identical(other.fieldId, _this.fieldId) || other.fieldId == _this.fieldId)&&const DeepCollectionEquality().equals(other.optionIds, _this.optionIds)&&(identical(other.text, _this.text) || other.text == _this.text)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterAnswer; + return Object.hash(runtimeType,_this.fieldId,const DeepCollectionEquality().hash(_this.optionIds),_this.text); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterAnswer; + return 'ParentLetterAnswer(fieldId: ${_this.fieldId}, optionIds: ${_this.optionIds}, text: ${_this.text})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterAnswerCopyWith<$Res> { + factory $ParentLetterAnswerCopyWith(ParentLetterAnswer value, $Res Function(ParentLetterAnswer) _then) = _$ParentLetterAnswerCopyWithImpl; +@useResult +$Res call({ + String fieldId, List optionIds, String? text +}); + + + + +} +/// @nodoc +class _$ParentLetterAnswerCopyWithImpl<$Res> + implements $ParentLetterAnswerCopyWith<$Res> { + _$ParentLetterAnswerCopyWithImpl(this._self, this._then); + + final ParentLetterAnswer _self; + final $Res Function(ParentLetterAnswer) _then; + +/// Create a copy of ParentLetterAnswer +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? fieldId = null,Object? optionIds = null,Object? text = freezed,}) { + return _then(ParentLetterAnswer( +fieldId: null == fieldId ? _self.fieldId : fieldId // ignore: cast_nullable_to_non_nullable +as String,optionIds: null == optionIds ? _self.optionIds : optionIds // ignore: cast_nullable_to_non_nullable +as List,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterAnswer]. +extension ParentLetterAnswerPatterns on ParentLetterAnswer { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterAnswer value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterAnswer() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterAnswer value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterAnswer(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterAnswer value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterAnswer() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String fieldId, List optionIds, String? text)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterAnswer() when $default != null: +return $default(_that.fieldId,_that.optionIds,_that.text);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String fieldId, List optionIds, String? text) $default,) {final _that = this; +switch (_that) { +case _ParentLetterAnswer(): +return $default(_that.fieldId,_that.optionIds,_that.text);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String fieldId, List optionIds, String? text)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterAnswer() when $default != null: +return $default(_that.fieldId,_that.optionIds,_that.text);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterAnswer with DiagnosticableTreeMixin implements ParentLetterAnswer { + const _ParentLetterAnswer({required this.fieldId, List optionIds = const [], this.text}): _optionIds = optionIds; + factory _ParentLetterAnswer.fromJson(Map json) => _$ParentLetterAnswerFromJson(json); + +@override final String fieldId; + final List _optionIds; +@override@JsonKey() List get optionIds { + if (_optionIds is EqualUnmodifiableListView) return _optionIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_optionIds); +} + +@override final String? text; + +/// Create a copy of ParentLetterAnswer +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterAnswerCopyWith<_ParentLetterAnswer> get copyWith => __$ParentLetterAnswerCopyWithImpl<_ParentLetterAnswer>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterAnswerToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterAnswer')) + ..add(DiagnosticsProperty('fieldId', fieldId))..add(DiagnosticsProperty('optionIds', optionIds))..add(DiagnosticsProperty('text', text)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterAnswer&&(identical(other.fieldId, fieldId) || other.fieldId == fieldId)&&const DeepCollectionEquality().equals(other.optionIds, _optionIds)&&(identical(other.text, text) || other.text == text)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,fieldId,const DeepCollectionEquality().hash(_optionIds),text); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterAnswer(fieldId: $fieldId, optionIds: $optionIds, text: $text)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterAnswerCopyWith<$Res> implements $ParentLetterAnswerCopyWith<$Res> { + factory _$ParentLetterAnswerCopyWith(_ParentLetterAnswer value, $Res Function(_ParentLetterAnswer) _then) = __$ParentLetterAnswerCopyWithImpl; +@override @useResult +$Res call({ + String fieldId, List optionIds, String? text +}); + + + + +} +/// @nodoc +class __$ParentLetterAnswerCopyWithImpl<$Res> + implements _$ParentLetterAnswerCopyWith<$Res> { + __$ParentLetterAnswerCopyWithImpl(this._self, this._then); + + final _ParentLetterAnswer _self; + final $Res Function(_ParentLetterAnswer) _then; + +/// Create a copy of ParentLetterAnswer +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? fieldId = null,Object? optionIds = null,Object? text = freezed,}) { + return _then(_ParentLetterAnswer( +fieldId: null == fieldId ? _self.fieldId : fieldId // ignore: cast_nullable_to_non_nullable +as String,optionIds: null == optionIds ? _self._optionIds : optionIds // ignore: cast_nullable_to_non_nullable +as List,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterResponse implements DiagnosticableTreeMixin { + + DateTime? get respondedAt; ParentLetterPerson get respondedBy; List get answers; bool get signed; +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterResponseCopyWith get copyWith => _$ParentLetterResponseCopyWithImpl(this as ParentLetterResponse, _$identity); + + /// Serializes this ParentLetterResponse to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterResponse; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterResponse')) + ..add(DiagnosticsProperty('respondedAt', _this.respondedAt))..add(DiagnosticsProperty('respondedBy', _this.respondedBy))..add(DiagnosticsProperty('answers', _this.answers))..add(DiagnosticsProperty('signed', _this.signed)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterResponse; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterResponse&&(identical(other.respondedAt, _this.respondedAt) || other.respondedAt == _this.respondedAt)&&(identical(other.respondedBy, _this.respondedBy) || other.respondedBy == _this.respondedBy)&&const DeepCollectionEquality().equals(other.answers, _this.answers)&&(identical(other.signed, _this.signed) || other.signed == _this.signed)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterResponse; + return Object.hash(runtimeType,_this.respondedAt,_this.respondedBy,const DeepCollectionEquality().hash(_this.answers),_this.signed); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterResponse; + return 'ParentLetterResponse(respondedAt: ${_this.respondedAt}, respondedBy: ${_this.respondedBy}, answers: ${_this.answers}, signed: ${_this.signed})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterResponseCopyWith<$Res> { + factory $ParentLetterResponseCopyWith(ParentLetterResponse value, $Res Function(ParentLetterResponse) _then) = _$ParentLetterResponseCopyWithImpl; +@useResult +$Res call({ + DateTime? respondedAt, ParentLetterPerson respondedBy, List answers, bool signed +}); + + +$ParentLetterPersonCopyWith<$Res> get respondedBy; + +} +/// @nodoc +class _$ParentLetterResponseCopyWithImpl<$Res> + implements $ParentLetterResponseCopyWith<$Res> { + _$ParentLetterResponseCopyWithImpl(this._self, this._then); + + final ParentLetterResponse _self; + final $Res Function(ParentLetterResponse) _then; + +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? respondedAt = freezed,Object? respondedBy = null,Object? answers = null,Object? signed = null,}) { + return _then(ParentLetterResponse( +respondedAt: freezed == respondedAt ? _self.respondedAt : respondedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,respondedBy: null == respondedBy ? _self.respondedBy : respondedBy // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,answers: null == answers ? _self.answers : answers // ignore: cast_nullable_to_non_nullable +as List,signed: null == signed ? _self.signed : signed // ignore: cast_nullable_to_non_nullable +as bool, + )); +} +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get respondedBy { + + return $ParentLetterPersonCopyWith<$Res>(_self.respondedBy, (value) { + return _then(_self.copyWith(respondedBy: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ParentLetterResponse]. +extension ParentLetterResponsePatterns on ParentLetterResponse { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterResponse value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterResponse() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterResponse value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterResponse(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterResponse value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterResponse() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( DateTime? respondedAt, ParentLetterPerson respondedBy, List answers, bool signed)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterResponse() when $default != null: +return $default(_that.respondedAt,_that.respondedBy,_that.answers,_that.signed);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( DateTime? respondedAt, ParentLetterPerson respondedBy, List answers, bool signed) $default,) {final _that = this; +switch (_that) { +case _ParentLetterResponse(): +return $default(_that.respondedAt,_that.respondedBy,_that.answers,_that.signed);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( DateTime? respondedAt, ParentLetterPerson respondedBy, List answers, bool signed)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterResponse() when $default != null: +return $default(_that.respondedAt,_that.respondedBy,_that.answers,_that.signed);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterResponse with DiagnosticableTreeMixin implements ParentLetterResponse { + const _ParentLetterResponse({this.respondedAt, this.respondedBy = const ParentLetterPerson(), List answers = const [], this.signed = false}): _answers = answers; + factory _ParentLetterResponse.fromJson(Map json) => _$ParentLetterResponseFromJson(json); + +@override final DateTime? respondedAt; +@override@JsonKey() final ParentLetterPerson respondedBy; + final List _answers; +@override@JsonKey() List get answers { + if (_answers is EqualUnmodifiableListView) return _answers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_answers); +} + +@override@JsonKey() final bool signed; + +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterResponseCopyWith<_ParentLetterResponse> get copyWith => __$ParentLetterResponseCopyWithImpl<_ParentLetterResponse>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterResponseToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterResponse')) + ..add(DiagnosticsProperty('respondedAt', respondedAt))..add(DiagnosticsProperty('respondedBy', respondedBy))..add(DiagnosticsProperty('answers', answers))..add(DiagnosticsProperty('signed', signed)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterResponse&&(identical(other.respondedAt, respondedAt) || other.respondedAt == respondedAt)&&(identical(other.respondedBy, respondedBy) || other.respondedBy == respondedBy)&&const DeepCollectionEquality().equals(other.answers, _answers)&&(identical(other.signed, signed) || other.signed == signed)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,respondedAt,respondedBy,const DeepCollectionEquality().hash(_answers),signed); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterResponse(respondedAt: $respondedAt, respondedBy: $respondedBy, answers: $answers, signed: $signed)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterResponseCopyWith<$Res> implements $ParentLetterResponseCopyWith<$Res> { + factory _$ParentLetterResponseCopyWith(_ParentLetterResponse value, $Res Function(_ParentLetterResponse) _then) = __$ParentLetterResponseCopyWithImpl; +@override @useResult +$Res call({ + DateTime? respondedAt, ParentLetterPerson respondedBy, List answers, bool signed +}); + + +@override $ParentLetterPersonCopyWith<$Res> get respondedBy; + +} +/// @nodoc +class __$ParentLetterResponseCopyWithImpl<$Res> + implements _$ParentLetterResponseCopyWith<$Res> { + __$ParentLetterResponseCopyWithImpl(this._self, this._then); + + final _ParentLetterResponse _self; + final $Res Function(_ParentLetterResponse) _then; + +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? respondedAt = freezed,Object? respondedBy = null,Object? answers = null,Object? signed = null,}) { + return _then(_ParentLetterResponse( +respondedAt: freezed == respondedAt ? _self.respondedAt : respondedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,respondedBy: null == respondedBy ? _self.respondedBy : respondedBy // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,answers: null == answers ? _self._answers : answers // ignore: cast_nullable_to_non_nullable +as List,signed: null == signed ? _self.signed : signed // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +/// Create a copy of ParentLetterResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get respondedBy { + + return $ParentLetterPersonCopyWith<$Res>(_self.respondedBy, (value) { + return _then(_self.copyWith(respondedBy: value)); + }); +} +} + + +/// @nodoc +mixin _$ParentLetterChildState implements DiagnosticableTreeMixin { + + String get childId;@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus get status; bool get editable; ParentLetterResponse? get response; +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterChildStateCopyWith get copyWith => _$ParentLetterChildStateCopyWithImpl(this as ParentLetterChildState, _$identity); + + /// Serializes this ParentLetterChildState to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterChildState; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterChildState')) + ..add(DiagnosticsProperty('childId', _this.childId))..add(DiagnosticsProperty('status', _this.status))..add(DiagnosticsProperty('editable', _this.editable))..add(DiagnosticsProperty('response', _this.response)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterChildState; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterChildState&&(identical(other.childId, _this.childId) || other.childId == _this.childId)&&(identical(other.status, _this.status) || other.status == _this.status)&&(identical(other.editable, _this.editable) || other.editable == _this.editable)&&(identical(other.response, _this.response) || other.response == _this.response)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterChildState; + return Object.hash(runtimeType,_this.childId,_this.status,_this.editable,_this.response); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterChildState; + return 'ParentLetterChildState(childId: ${_this.childId}, status: ${_this.status}, editable: ${_this.editable}, response: ${_this.response})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterChildStateCopyWith<$Res> { + factory $ParentLetterChildStateCopyWith(ParentLetterChildState value, $Res Function(ParentLetterChildState) _then) = _$ParentLetterChildStateCopyWithImpl; +@useResult +$Res call({ + String childId,@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, bool editable, ParentLetterResponse? response +}); + + +$ParentLetterResponseCopyWith<$Res>? get response; + +} +/// @nodoc +class _$ParentLetterChildStateCopyWithImpl<$Res> + implements $ParentLetterChildStateCopyWith<$Res> { + _$ParentLetterChildStateCopyWithImpl(this._self, this._then); + + final ParentLetterChildState _self; + final $Res Function(ParentLetterChildState) _then; + +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? childId = null,Object? status = null,Object? editable = null,Object? response = freezed,}) { + return _then(ParentLetterChildState( +childId: null == childId ? _self.childId : childId // ignore: cast_nullable_to_non_nullable +as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ParentLetterStatus,editable: null == editable ? _self.editable : editable // ignore: cast_nullable_to_non_nullable +as bool,response: freezed == response ? _self.response : response // ignore: cast_nullable_to_non_nullable +as ParentLetterResponse?, + )); +} +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterResponseCopyWith<$Res>? get response { + if (_self.response == null) { + return null; + } + + return $ParentLetterResponseCopyWith<$Res>(_self.response!, (value) { + return _then(_self.copyWith(response: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ParentLetterChildState]. +extension ParentLetterChildStatePatterns on ParentLetterChildState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterChildState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterChildState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterChildState value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterChildState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterChildState value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterChildState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String childId, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, bool editable, ParentLetterResponse? response)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterChildState() when $default != null: +return $default(_that.childId,_that.status,_that.editable,_that.response);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String childId, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, bool editable, ParentLetterResponse? response) $default,) {final _that = this; +switch (_that) { +case _ParentLetterChildState(): +return $default(_that.childId,_that.status,_that.editable,_that.response);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String childId, @JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, bool editable, ParentLetterResponse? response)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterChildState() when $default != null: +return $default(_that.childId,_that.status,_that.editable,_that.response);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterChildState with DiagnosticableTreeMixin implements ParentLetterChildState { + const _ParentLetterChildState({required this.childId, @JsonKey(unknownEnumValue: ParentLetterStatus.info) this.status = ParentLetterStatus.info, this.editable = false, this.response}); + factory _ParentLetterChildState.fromJson(Map json) => _$ParentLetterChildStateFromJson(json); + +@override final String childId; +@override@JsonKey(unknownEnumValue: ParentLetterStatus.info) final ParentLetterStatus status; +@override@JsonKey() final bool editable; +@override final ParentLetterResponse? response; + +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterChildStateCopyWith<_ParentLetterChildState> get copyWith => __$ParentLetterChildStateCopyWithImpl<_ParentLetterChildState>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterChildStateToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterChildState')) + ..add(DiagnosticsProperty('childId', childId))..add(DiagnosticsProperty('status', status))..add(DiagnosticsProperty('editable', editable))..add(DiagnosticsProperty('response', response)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterChildState&&(identical(other.childId, childId) || other.childId == childId)&&(identical(other.status, status) || other.status == status)&&(identical(other.editable, editable) || other.editable == editable)&&(identical(other.response, response) || other.response == response)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,childId,status,editable,response); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterChildState(childId: $childId, status: $status, editable: $editable, response: $response)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterChildStateCopyWith<$Res> implements $ParentLetterChildStateCopyWith<$Res> { + factory _$ParentLetterChildStateCopyWith(_ParentLetterChildState value, $Res Function(_ParentLetterChildState) _then) = __$ParentLetterChildStateCopyWithImpl; +@override @useResult +$Res call({ + String childId,@JsonKey(unknownEnumValue: ParentLetterStatus.info) ParentLetterStatus status, bool editable, ParentLetterResponse? response +}); + + +@override $ParentLetterResponseCopyWith<$Res>? get response; + +} +/// @nodoc +class __$ParentLetterChildStateCopyWithImpl<$Res> + implements _$ParentLetterChildStateCopyWith<$Res> { + __$ParentLetterChildStateCopyWithImpl(this._self, this._then); + + final _ParentLetterChildState _self; + final $Res Function(_ParentLetterChildState) _then; + +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? childId = null,Object? status = null,Object? editable = null,Object? response = freezed,}) { + return _then(_ParentLetterChildState( +childId: null == childId ? _self.childId : childId // ignore: cast_nullable_to_non_nullable +as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ParentLetterStatus,editable: null == editable ? _self.editable : editable // ignore: cast_nullable_to_non_nullable +as bool,response: freezed == response ? _self.response : response // ignore: cast_nullable_to_non_nullable +as ParentLetterResponse?, + )); +} + +/// Create a copy of ParentLetterChildState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterResponseCopyWith<$Res>? get response { + if (_self.response == null) { + return null; + } + + return $ParentLetterResponseCopyWith<$Res>(_self.response!, (value) { + return _then(_self.copyWith(response: value)); + }); +} +} + + +/// @nodoc +mixin _$ParentLetterThreadMessage implements DiagnosticableTreeMixin { + + String get id; ParentLetterPerson get author; String get body; DateTime get sentAt; +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterThreadMessageCopyWith get copyWith => _$ParentLetterThreadMessageCopyWithImpl(this as ParentLetterThreadMessage, _$identity); + + /// Serializes this ParentLetterThreadMessage to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterThreadMessage; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterThreadMessage')) + ..add(DiagnosticsProperty('id', _this.id))..add(DiagnosticsProperty('author', _this.author))..add(DiagnosticsProperty('body', _this.body))..add(DiagnosticsProperty('sentAt', _this.sentAt)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterThreadMessage; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterThreadMessage&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.author, _this.author) || other.author == _this.author)&&(identical(other.body, _this.body) || other.body == _this.body)&&(identical(other.sentAt, _this.sentAt) || other.sentAt == _this.sentAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterThreadMessage; + return Object.hash(runtimeType,_this.id,_this.author,_this.body,_this.sentAt); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterThreadMessage; + return 'ParentLetterThreadMessage(id: ${_this.id}, author: ${_this.author}, body: ${_this.body}, sentAt: ${_this.sentAt})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterThreadMessageCopyWith<$Res> { + factory $ParentLetterThreadMessageCopyWith(ParentLetterThreadMessage value, $Res Function(ParentLetterThreadMessage) _then) = _$ParentLetterThreadMessageCopyWithImpl; +@useResult +$Res call({ + String id, ParentLetterPerson author, String body, DateTime sentAt +}); + + +$ParentLetterPersonCopyWith<$Res> get author; + +} +/// @nodoc +class _$ParentLetterThreadMessageCopyWithImpl<$Res> + implements $ParentLetterThreadMessageCopyWith<$Res> { + _$ParentLetterThreadMessageCopyWithImpl(this._self, this._then); + + final ParentLetterThreadMessage _self; + final $Res Function(ParentLetterThreadMessage) _then; + +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? author = null,Object? body = null,Object? sentAt = null,}) { + return _then(ParentLetterThreadMessage( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,author: null == author ? _self.author : author // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,sentAt: null == sentAt ? _self.sentAt : sentAt // ignore: cast_nullable_to_non_nullable +as DateTime, + )); +} +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get author { + + return $ParentLetterPersonCopyWith<$Res>(_self.author, (value) { + return _then(_self.copyWith(author: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ParentLetterThreadMessage]. +extension ParentLetterThreadMessagePatterns on ParentLetterThreadMessage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterThreadMessage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterThreadMessage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterThreadMessage value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterThreadMessage(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterThreadMessage value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterThreadMessage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, ParentLetterPerson author, String body, DateTime sentAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterThreadMessage() when $default != null: +return $default(_that.id,_that.author,_that.body,_that.sentAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, ParentLetterPerson author, String body, DateTime sentAt) $default,) {final _that = this; +switch (_that) { +case _ParentLetterThreadMessage(): +return $default(_that.id,_that.author,_that.body,_that.sentAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, ParentLetterPerson author, String body, DateTime sentAt)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterThreadMessage() when $default != null: +return $default(_that.id,_that.author,_that.body,_that.sentAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterThreadMessage with DiagnosticableTreeMixin implements ParentLetterThreadMessage { + const _ParentLetterThreadMessage({required this.id, this.author = const ParentLetterPerson(), this.body = '', required this.sentAt}); + factory _ParentLetterThreadMessage.fromJson(Map json) => _$ParentLetterThreadMessageFromJson(json); + +@override final String id; +@override@JsonKey() final ParentLetterPerson author; +@override@JsonKey() final String body; +@override final DateTime sentAt; + +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterThreadMessageCopyWith<_ParentLetterThreadMessage> get copyWith => __$ParentLetterThreadMessageCopyWithImpl<_ParentLetterThreadMessage>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterThreadMessageToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterThreadMessage')) + ..add(DiagnosticsProperty('id', id))..add(DiagnosticsProperty('author', author))..add(DiagnosticsProperty('body', body))..add(DiagnosticsProperty('sentAt', sentAt)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterThreadMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.author, author) || other.author == author)&&(identical(other.body, body) || other.body == body)&&(identical(other.sentAt, sentAt) || other.sentAt == sentAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,id,author,body,sentAt); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterThreadMessage(id: $id, author: $author, body: $body, sentAt: $sentAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterThreadMessageCopyWith<$Res> implements $ParentLetterThreadMessageCopyWith<$Res> { + factory _$ParentLetterThreadMessageCopyWith(_ParentLetterThreadMessage value, $Res Function(_ParentLetterThreadMessage) _then) = __$ParentLetterThreadMessageCopyWithImpl; +@override @useResult +$Res call({ + String id, ParentLetterPerson author, String body, DateTime sentAt +}); + + +@override $ParentLetterPersonCopyWith<$Res> get author; + +} +/// @nodoc +class __$ParentLetterThreadMessageCopyWithImpl<$Res> + implements _$ParentLetterThreadMessageCopyWith<$Res> { + __$ParentLetterThreadMessageCopyWithImpl(this._self, this._then); + + final _ParentLetterThreadMessage _self; + final $Res Function(_ParentLetterThreadMessage) _then; + +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? author = null,Object? body = null,Object? sentAt = null,}) { + return _then(_ParentLetterThreadMessage( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,author: null == author ? _self.author : author // ignore: cast_nullable_to_non_nullable +as ParentLetterPerson,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,sentAt: null == sentAt ? _self.sentAt : sentAt // ignore: cast_nullable_to_non_nullable +as DateTime, + )); +} + +/// Create a copy of ParentLetterThreadMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterPersonCopyWith<$Res> get author { + + return $ParentLetterPersonCopyWith<$Res>(_self.author, (value) { + return _then(_self.copyWith(author: value)); + }); +} +} + + +/// @nodoc +mixin _$ParentLetterThread implements DiagnosticableTreeMixin { + + bool get enabled; List get messages; +/// Create a copy of ParentLetterThread +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterThreadCopyWith get copyWith => _$ParentLetterThreadCopyWithImpl(this as ParentLetterThread, _$identity); + + /// Serializes this ParentLetterThread to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterThread; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterThread')) + ..add(DiagnosticsProperty('enabled', _this.enabled))..add(DiagnosticsProperty('messages', _this.messages)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterThread; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterThread&&(identical(other.enabled, _this.enabled) || other.enabled == _this.enabled)&&const DeepCollectionEquality().equals(other.messages, _this.messages)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterThread; + return Object.hash(runtimeType,_this.enabled,const DeepCollectionEquality().hash(_this.messages)); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterThread; + return 'ParentLetterThread(enabled: ${_this.enabled}, messages: ${_this.messages})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterThreadCopyWith<$Res> { + factory $ParentLetterThreadCopyWith(ParentLetterThread value, $Res Function(ParentLetterThread) _then) = _$ParentLetterThreadCopyWithImpl; +@useResult +$Res call({ + bool enabled, List messages +}); + + + + +} +/// @nodoc +class _$ParentLetterThreadCopyWithImpl<$Res> + implements $ParentLetterThreadCopyWith<$Res> { + _$ParentLetterThreadCopyWithImpl(this._self, this._then); + + final ParentLetterThread _self; + final $Res Function(ParentLetterThread) _then; + +/// Create a copy of ParentLetterThread +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? enabled = null,Object? messages = null,}) { + return _then(ParentLetterThread( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterThread]. +extension ParentLetterThreadPatterns on ParentLetterThread { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterThread value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterThread() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterThread value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterThread(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterThread value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterThread() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool enabled, List messages)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterThread() when $default != null: +return $default(_that.enabled,_that.messages);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool enabled, List messages) $default,) {final _that = this; +switch (_that) { +case _ParentLetterThread(): +return $default(_that.enabled,_that.messages);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool enabled, List messages)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterThread() when $default != null: +return $default(_that.enabled,_that.messages);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterThread with DiagnosticableTreeMixin implements ParentLetterThread { + const _ParentLetterThread({this.enabled = false, List messages = const []}): _messages = messages; + factory _ParentLetterThread.fromJson(Map json) => _$ParentLetterThreadFromJson(json); + +@override@JsonKey() final bool enabled; + final List _messages; +@override@JsonKey() List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); +} + + +/// Create a copy of ParentLetterThread +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterThreadCopyWith<_ParentLetterThread> get copyWith => __$ParentLetterThreadCopyWithImpl<_ParentLetterThread>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterThreadToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterThread')) + ..add(DiagnosticsProperty('enabled', enabled))..add(DiagnosticsProperty('messages', messages)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterThread&&(identical(other.enabled, enabled) || other.enabled == enabled)&&const DeepCollectionEquality().equals(other.messages, _messages)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,enabled,const DeepCollectionEquality().hash(_messages)); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterThread(enabled: $enabled, messages: $messages)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterThreadCopyWith<$Res> implements $ParentLetterThreadCopyWith<$Res> { + factory _$ParentLetterThreadCopyWith(_ParentLetterThread value, $Res Function(_ParentLetterThread) _then) = __$ParentLetterThreadCopyWithImpl; +@override @useResult +$Res call({ + bool enabled, List messages +}); + + + + +} +/// @nodoc +class __$ParentLetterThreadCopyWithImpl<$Res> + implements _$ParentLetterThreadCopyWith<$Res> { + __$ParentLetterThreadCopyWithImpl(this._self, this._then); + + final _ParentLetterThread _self; + final $Res Function(_ParentLetterThread) _then; + +/// Create a copy of ParentLetterThread +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? enabled = null,Object? messages = null,}) { + return _then(_ParentLetterThread( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,messages: null == messages ? _self._messages : messages // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$ParentLetterContent implements DiagnosticableTreeMixin { + + String get body; List get attachments; ParentLetterRequest? get request; List get children; ParentLetterThread get thread; +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterContentCopyWith get copyWith => _$ParentLetterContentCopyWithImpl(this as ParentLetterContent, _$identity); + + /// Serializes this ParentLetterContent to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + final _this = this as ParentLetterContent; + properties + ..add(DiagnosticsProperty('type', 'ParentLetterContent')) + ..add(DiagnosticsProperty('body', _this.body))..add(DiagnosticsProperty('attachments', _this.attachments))..add(DiagnosticsProperty('request', _this.request))..add(DiagnosticsProperty('children', _this.children))..add(DiagnosticsProperty('thread', _this.thread)); +} + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterContent; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterContent&&(identical(other.body, _this.body) || other.body == _this.body)&&const DeepCollectionEquality().equals(other.attachments, _this.attachments)&&(identical(other.request, _this.request) || other.request == _this.request)&&const DeepCollectionEquality().equals(other.children, _this.children)&&(identical(other.thread, _this.thread) || other.thread == _this.thread)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterContent; + return Object.hash(runtimeType,_this.body,const DeepCollectionEquality().hash(_this.attachments),_this.request,const DeepCollectionEquality().hash(_this.children),_this.thread); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + final _this = this as ParentLetterContent; + return 'ParentLetterContent(body: ${_this.body}, attachments: ${_this.attachments}, request: ${_this.request}, children: ${_this.children}, thread: ${_this.thread})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterContentCopyWith<$Res> { + factory $ParentLetterContentCopyWith(ParentLetterContent value, $Res Function(ParentLetterContent) _then) = _$ParentLetterContentCopyWithImpl; +@useResult +$Res call({ + String body, List attachments, ParentLetterRequest? request, List children, ParentLetterThread thread +}); + + +$ParentLetterRequestCopyWith<$Res>? get request;$ParentLetterThreadCopyWith<$Res> get thread; + +} +/// @nodoc +class _$ParentLetterContentCopyWithImpl<$Res> + implements $ParentLetterContentCopyWith<$Res> { + _$ParentLetterContentCopyWithImpl(this._self, this._then); + + final ParentLetterContent _self; + final $Res Function(ParentLetterContent) _then; + +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? body = null,Object? attachments = null,Object? request = freezed,Object? children = null,Object? thread = null,}) { + return _then(ParentLetterContent( +body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable +as List,request: freezed == request ? _self.request : request // ignore: cast_nullable_to_non_nullable +as ParentLetterRequest?,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable +as List,thread: null == thread ? _self.thread : thread // ignore: cast_nullable_to_non_nullable +as ParentLetterThread, + )); +} +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterRequestCopyWith<$Res>? get request { + if (_self.request == null) { + return null; + } + + return $ParentLetterRequestCopyWith<$Res>(_self.request!, (value) { + return _then(_self.copyWith(request: value)); + }); +}/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterThreadCopyWith<$Res> get thread { + + return $ParentLetterThreadCopyWith<$Res>(_self.thread, (value) { + return _then(_self.copyWith(thread: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ParentLetterContent]. +extension ParentLetterContentPatterns on ParentLetterContent { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterContent value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterContent() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterContent value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterContent(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterContent value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterContent() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String body, List attachments, ParentLetterRequest? request, List children, ParentLetterThread thread)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterContent() when $default != null: +return $default(_that.body,_that.attachments,_that.request,_that.children,_that.thread);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String body, List attachments, ParentLetterRequest? request, List children, ParentLetterThread thread) $default,) {final _that = this; +switch (_that) { +case _ParentLetterContent(): +return $default(_that.body,_that.attachments,_that.request,_that.children,_that.thread);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String body, List attachments, ParentLetterRequest? request, List children, ParentLetterThread thread)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterContent() when $default != null: +return $default(_that.body,_that.attachments,_that.request,_that.children,_that.thread);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterContent with DiagnosticableTreeMixin implements ParentLetterContent { + const _ParentLetterContent({this.body = '', List attachments = const [], this.request, List children = const [], this.thread = const ParentLetterThread()}): _attachments = attachments,_children = children; + factory _ParentLetterContent.fromJson(Map json) => _$ParentLetterContentFromJson(json); + +@override@JsonKey() final String body; + final List _attachments; +@override@JsonKey() List get attachments { + if (_attachments is EqualUnmodifiableListView) return _attachments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_attachments); +} + +@override final ParentLetterRequest? request; + final List _children; +@override@JsonKey() List get children { + if (_children is EqualUnmodifiableListView) return _children; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_children); +} + +@override@JsonKey() final ParentLetterThread thread; + +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterContentCopyWith<_ParentLetterContent> get copyWith => __$ParentLetterContentCopyWithImpl<_ParentLetterContent>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterContentToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ParentLetterContent')) + ..add(DiagnosticsProperty('body', body))..add(DiagnosticsProperty('attachments', attachments))..add(DiagnosticsProperty('request', request))..add(DiagnosticsProperty('children', children))..add(DiagnosticsProperty('thread', thread)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterContent&&(identical(other.body, body) || other.body == body)&&const DeepCollectionEquality().equals(other.attachments, _attachments)&&(identical(other.request, request) || other.request == request)&&const DeepCollectionEquality().equals(other.children, _children)&&(identical(other.thread, thread) || other.thread == thread)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,body,const DeepCollectionEquality().hash(_attachments),request,const DeepCollectionEquality().hash(_children),thread); +} + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ParentLetterContent(body: $body, attachments: $attachments, request: $request, children: $children, thread: $thread)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterContentCopyWith<$Res> implements $ParentLetterContentCopyWith<$Res> { + factory _$ParentLetterContentCopyWith(_ParentLetterContent value, $Res Function(_ParentLetterContent) _then) = __$ParentLetterContentCopyWithImpl; +@override @useResult +$Res call({ + String body, List attachments, ParentLetterRequest? request, List children, ParentLetterThread thread +}); + + +@override $ParentLetterRequestCopyWith<$Res>? get request;@override $ParentLetterThreadCopyWith<$Res> get thread; + +} +/// @nodoc +class __$ParentLetterContentCopyWithImpl<$Res> + implements _$ParentLetterContentCopyWith<$Res> { + __$ParentLetterContentCopyWithImpl(this._self, this._then); + + final _ParentLetterContent _self; + final $Res Function(_ParentLetterContent) _then; + +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? body = null,Object? attachments = null,Object? request = freezed,Object? children = null,Object? thread = null,}) { + return _then(_ParentLetterContent( +body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable +as String,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable +as List,request: freezed == request ? _self.request : request // ignore: cast_nullable_to_non_nullable +as ParentLetterRequest?,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable +as List,thread: null == thread ? _self.thread : thread // ignore: cast_nullable_to_non_nullable +as ParentLetterThread, + )); +} + +/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterRequestCopyWith<$Res>? get request { + if (_self.request == null) { + return null; + } + + return $ParentLetterRequestCopyWith<$Res>(_self.request!, (value) { + return _then(_self.copyWith(request: value)); + }); +}/// Create a copy of ParentLetterContent +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ParentLetterThreadCopyWith<$Res> get thread { + + return $ParentLetterThreadCopyWith<$Res>(_self.thread, (value) { + return _then(_self.copyWith(thread: value)); + }); +} +} + +// dart format on diff --git a/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.g.dart b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.g.dart new file mode 100644 index 0000000..3536ac8 --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/parent_letter_models.g.dart @@ -0,0 +1,331 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parent_letter_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ParentLetterPerson _$ParentLetterPersonFromJson(Map json) => + _ParentLetterPerson( + displayName: json['displayName'] as String? ?? '', + self: json['self'] as bool? ?? false, + ); + +Map _$ParentLetterPersonToJson(_ParentLetterPerson instance) => + { + 'displayName': instance.displayName, + 'self': instance.self, + }; + +_ParentLetterSummary _$ParentLetterSummaryFromJson(Map json) => + _ParentLetterSummary( + id: json['id'] as String, + subject: json['subject'] as String? ?? '', + preview: json['preview'] as String? ?? '', + sender: json['sender'] == null + ? const ParentLetterPerson() + : ParentLetterPerson.fromJson(json['sender'] as Map), + sentAt: DateTime.parse(json['sentAt'] as String), + editedAt: json['editedAt'] == null + ? null + : DateTime.parse(json['editedAt'] as String), + read: json['read'] as bool? ?? true, + childIds: + (json['childIds'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + attachmentCount: (json['attachmentCount'] as num?)?.toInt() ?? 0, + status: + $enumDecodeNullable( + _$ParentLetterStatusEnumMap, + json['status'], + unknownValue: ParentLetterStatus.info, + ) ?? + ParentLetterStatus.info, + deadline: json['deadline'] == null + ? null + : DateTime.parse(json['deadline'] as String), + ); + +Map _$ParentLetterSummaryToJson( + _ParentLetterSummary instance, +) => { + 'id': instance.id, + 'subject': instance.subject, + 'preview': instance.preview, + 'sender': instance.sender, + 'sentAt': instance.sentAt.toIso8601String(), + 'editedAt': instance.editedAt?.toIso8601String(), + 'read': instance.read, + 'childIds': instance.childIds, + 'attachmentCount': instance.attachmentCount, + 'status': _$ParentLetterStatusEnumMap[instance.status]!, + 'deadline': instance.deadline?.toIso8601String(), +}; + +const _$ParentLetterStatusEnumMap = { + ParentLetterStatus.info: 'info', + ParentLetterStatus.open: 'open', + ParentLetterStatus.done: 'done', + ParentLetterStatus.expired: 'expired', +}; + +_ParentLetterListResponse _$ParentLetterListResponseFromJson( + Map json, +) => _ParentLetterListResponse( + items: + (json['items'] as List?) + ?.map((e) => ParentLetterSummary.fromJson(e as Map)) + .toList() ?? + const [], + hasMore: json['hasMore'] as bool? ?? false, + unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0, + openCount: (json['openCount'] as num?)?.toInt() ?? 0, +); + +Map _$ParentLetterListResponseToJson( + _ParentLetterListResponse instance, +) => { + 'items': instance.items, + 'hasMore': instance.hasMore, + 'unreadCount': instance.unreadCount, + 'openCount': instance.openCount, +}; + +_ParentLetterAttachment _$ParentLetterAttachmentFromJson( + Map json, +) => _ParentLetterAttachment( + id: json['id'] as String, + fileName: json['fileName'] as String? ?? '', + mimeType: json['mimeType'] as String? ?? '', + size: (json['size'] as num?)?.toInt() ?? 0, +); + +Map _$ParentLetterAttachmentToJson( + _ParentLetterAttachment instance, +) => { + 'id': instance.id, + 'fileName': instance.fileName, + 'mimeType': instance.mimeType, + 'size': instance.size, +}; + +_ParentLetterOption _$ParentLetterOptionFromJson(Map json) => + _ParentLetterOption( + id: json['id'] as String, + label: json['label'] as String? ?? '', + ); + +Map _$ParentLetterOptionToJson(_ParentLetterOption instance) => + {'id': instance.id, 'label': instance.label}; + +_ParentLetterField _$ParentLetterFieldFromJson(Map json) => + _ParentLetterField( + id: json['id'] as String, + type: + $enumDecodeNullable( + _$ParentLetterFieldTypeEnumMap, + json['type'], + unknownValue: ParentLetterFieldType.unknown, + ) ?? + ParentLetterFieldType.unknown, + label: json['label'] as String? ?? '', + isRequired: json['required'] as bool? ?? false, + options: + (json['options'] as List?) + ?.map( + (e) => ParentLetterOption.fromJson(e as Map), + ) + .toList() ?? + const [], + ); + +Map _$ParentLetterFieldToJson(_ParentLetterField instance) => + { + 'id': instance.id, + 'type': _$ParentLetterFieldTypeEnumMap[instance.type]!, + 'label': instance.label, + 'required': instance.isRequired, + 'options': instance.options, + }; + +const _$ParentLetterFieldTypeEnumMap = { + ParentLetterFieldType.singleChoice: 'single_choice', + ParentLetterFieldType.unknown: 'unknown', +}; + +_ParentLetterRequest _$ParentLetterRequestFromJson(Map json) => + _ParentLetterRequest( + fields: + (json['fields'] as List?) + ?.map( + (e) => ParentLetterField.fromJson(e as Map), + ) + .toList() ?? + const [], + signatureRequired: json['signatureRequired'] as bool? ?? false, + deadline: json['deadline'] == null + ? null + : DateTime.parse(json['deadline'] as String), + ); + +Map _$ParentLetterRequestToJson( + _ParentLetterRequest instance, +) => { + 'fields': instance.fields, + 'signatureRequired': instance.signatureRequired, + 'deadline': instance.deadline?.toIso8601String(), +}; + +_ParentLetterAnswer _$ParentLetterAnswerFromJson(Map json) => + _ParentLetterAnswer( + fieldId: json['fieldId'] as String, + optionIds: + (json['optionIds'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + text: json['text'] as String?, + ); + +Map _$ParentLetterAnswerToJson(_ParentLetterAnswer instance) => + { + 'fieldId': instance.fieldId, + 'optionIds': instance.optionIds, + 'text': instance.text, + }; + +_ParentLetterResponse _$ParentLetterResponseFromJson( + Map json, +) => _ParentLetterResponse( + respondedAt: json['respondedAt'] == null + ? null + : DateTime.parse(json['respondedAt'] as String), + respondedBy: json['respondedBy'] == null + ? const ParentLetterPerson() + : ParentLetterPerson.fromJson( + json['respondedBy'] as Map, + ), + answers: + (json['answers'] as List?) + ?.map((e) => ParentLetterAnswer.fromJson(e as Map)) + .toList() ?? + const [], + signed: json['signed'] as bool? ?? false, +); + +Map _$ParentLetterResponseToJson( + _ParentLetterResponse instance, +) => { + 'respondedAt': instance.respondedAt?.toIso8601String(), + 'respondedBy': instance.respondedBy, + 'answers': instance.answers, + 'signed': instance.signed, +}; + +_ParentLetterChildState _$ParentLetterChildStateFromJson( + Map json, +) => _ParentLetterChildState( + childId: json['childId'] as String, + status: + $enumDecodeNullable( + _$ParentLetterStatusEnumMap, + json['status'], + unknownValue: ParentLetterStatus.info, + ) ?? + ParentLetterStatus.info, + editable: json['editable'] as bool? ?? false, + response: json['response'] == null + ? null + : ParentLetterResponse.fromJson(json['response'] as Map), +); + +Map _$ParentLetterChildStateToJson( + _ParentLetterChildState instance, +) => { + 'childId': instance.childId, + 'status': _$ParentLetterStatusEnumMap[instance.status]!, + 'editable': instance.editable, + 'response': instance.response, +}; + +_ParentLetterThreadMessage _$ParentLetterThreadMessageFromJson( + Map json, +) => _ParentLetterThreadMessage( + id: json['id'] as String, + author: json['author'] == null + ? const ParentLetterPerson() + : ParentLetterPerson.fromJson(json['author'] as Map), + body: json['body'] as String? ?? '', + sentAt: DateTime.parse(json['sentAt'] as String), +); + +Map _$ParentLetterThreadMessageToJson( + _ParentLetterThreadMessage instance, +) => { + 'id': instance.id, + 'author': instance.author, + 'body': instance.body, + 'sentAt': instance.sentAt.toIso8601String(), +}; + +_ParentLetterThread _$ParentLetterThreadFromJson(Map json) => + _ParentLetterThread( + enabled: json['enabled'] as bool? ?? false, + messages: + (json['messages'] as List?) + ?.map( + (e) => ParentLetterThreadMessage.fromJson( + e as Map, + ), + ) + .toList() ?? + const [], + ); + +Map _$ParentLetterThreadToJson(_ParentLetterThread instance) => + { + 'enabled': instance.enabled, + 'messages': instance.messages, + }; + +_ParentLetterContent _$ParentLetterContentFromJson(Map json) => + _ParentLetterContent( + body: json['body'] as String? ?? '', + attachments: + (json['attachments'] as List?) + ?.map( + (e) => + ParentLetterAttachment.fromJson(e as Map), + ) + .toList() ?? + const [], + request: json['request'] == null + ? null + : ParentLetterRequest.fromJson( + json['request'] as Map, + ), + children: + (json['children'] as List?) + ?.map( + (e) => + ParentLetterChildState.fromJson(e as Map), + ) + .toList() ?? + const [], + thread: json['thread'] == null + ? const ParentLetterThread() + : ParentLetterThread.fromJson(json['thread'] as Map), + ); + +Map _$ParentLetterContentToJson( + _ParentLetterContent instance, +) => { + 'body': instance.body, + 'attachments': instance.attachments, + 'request': instance.request, + 'children': instance.children, + 'thread': instance.thread, +}; diff --git a/lib/api/marianumconnect/queries/parent_letters/parent_letter_query.dart b/lib/api/marianumconnect/queries/parent_letters/parent_letter_query.dart new file mode 100644 index 0000000..d131576 --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/parent_letter_query.dart @@ -0,0 +1,18 @@ +import 'package:dio/dio.dart'; + +import '../../../errors/app_exception.dart'; +import '../../marianumconnect_query.dart'; +import 'parent_letter_exception.dart'; + +/// Base for the parent-letter calls: domain rejections surface as +/// [ParentLetterException]. +abstract class ParentLetterQuery extends MarianumConnectQuery { + ParentLetterQuery({super.dio}); + + String letterPath(String letterId, [String suffix = '']) => + 'parent-letters/${Uri.encodeComponent(letterId)}$suffix'; + + @override + AppException mapError(DioException error) => + ParentLetterException.fromDio(error); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart b/lib/api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart new file mode 100644 index 0000000..c408f02 --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart @@ -0,0 +1,20 @@ +import 'parent_letter_models.dart'; +import 'parent_letter_query.dart'; + +/// `POST parent-letters/{id}/thread`. [clientMessageId] makes a retry +/// idempotent on the server. +class PostParentLetterThreadMessage extends ParentLetterQuery { + PostParentLetterThreadMessage({super.dio}); + + Future run({ + required String letterId, + required String body, + required String clientMessageId, + }) => guard(() async { + final response = await dio.post>( + endpoint(letterPath(letterId, '/thread')), + data: {'body': body, 'clientMessageId': clientMessageId}, + ); + return ParentLetterThreadMessage.fromJson(response.data!); + }); +} diff --git a/lib/api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart b/lib/api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart new file mode 100644 index 0000000..874504d --- /dev/null +++ b/lib/api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'parent_letter_models.dart'; +import 'parent_letter_query.dart'; + +/// `PUT parent-letters/{id}/responses/{childId}`: creates or (while editable) +/// replaces the response for one child. Answers with the updated letter. +class SubmitParentLetterResponse extends ParentLetterQuery { + SubmitParentLetterResponse({super.dio}); + + Future run({ + required String letterId, + required String childId, + required List answers, + Uint8List? signaturePng, + }) => guard(() async { + final response = await dio.put>( + endpoint( + letterPath(letterId, '/responses/${Uri.encodeComponent(childId)}'), + ), + data: { + 'answers': [ + for (final answer in answers) + {'fieldId': answer.fieldId, 'optionIds': answer.optionIds}, + ], + 'signaturePng': signaturePng == null + ? null + : base64Encode(signaturePng), + }, + ); + return ParentLetterDetail.fromJson(response.data!); + }); +} diff --git a/lib/app.dart b/lib/app.dart index e2c4e1b..d31b2c6 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,6 +11,7 @@ import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart import 'main.dart'; import 'model/data_cleaner.dart'; import 'notification/notification_controller.dart'; +import 'notification/notification_service.dart'; import 'notification/notification_tasks.dart'; import 'push/push_registration.dart'; import 'push/push_tap_router.dart'; @@ -119,18 +120,11 @@ class _AppState extends State with WidgetsBindingObserver { } } - void _onPushTapPending() { - final token = PushTapRouter.pendingChatToken.value; - if (token == null || !mounted) return; - PushTapRouter.pendingChatToken.value = null; - NotificationTasks.navigateToTalk(context, chatToken: token); - } - - void _onNewsletterTapPending() { - final id = PushTapRouter.pendingNewsletterId.value; - if (id == null || !mounted) return; - PushTapRouter.pendingNewsletterId.value = null; - AppRoutes.openNewsletterById(context, id: id); + void _onPushTargetPending() { + final target = PushTapRouter.pendingTarget.value; + if (target == null || !mounted) return; + PushTapRouter.pendingTarget.value = null; + NotificationTasks.openPushTarget(context, target); } Future _handlePendingWidgetNavigation() async { @@ -234,8 +228,12 @@ class _AppState extends State with WidgetsBindingObserver { // Android renders pushes locally, so a tap arrives via the local // notifications callback (PushTapRouter) rather than onMessageOpenedApp. - PushTapRouter.pendingChatToken.addListener(_onPushTapPending); - PushTapRouter.pendingNewsletterId.addListener(_onNewsletterTapPending); + PushTapRouter.pendingTarget.addListener(_onPushTargetPending); + unawaited( + PushTapRouter.handleAppLaunch( + NotificationService().flutterLocalNotificationsPlugin, + ), + ); _onMessageSub = FirebaseMessaging.onMessage.listen((message) { if (!mounted) return; @@ -264,8 +262,7 @@ class _AppState extends State with WidgetsBindingObserver { _onMessageSub?.cancel(); _onMessageOpenedAppSub?.cancel(); _fcmTokenRefreshSub?.cancel(); - PushTapRouter.pendingChatToken.removeListener(_onPushTapPending); - PushTapRouter.pendingNewsletterId.removeListener(_onNewsletterTapPending); + PushTapRouter.pendingTarget.removeListener(_onPushTargetPending); ShareIntentListener.pending.removeListener(_handlePendingShare); ShareIntentListener.instance.detach(); Main.bottomNavigator.removeListener(_onTabControllerChanged); diff --git a/lib/main.dart b/lib/main.dart index e102655..571aa9d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,7 @@ import 'auth_link/guardian_link_listener.dart'; import 'background/widget_background_task.dart'; import 'firebase_options.dart'; import 'notification/notification_service.dart'; +import 'push/notification_permission_prompt.dart'; import 'push/push_message_handler.dart'; import 'push/push_registration.dart'; import 'push/push_registration_store.dart'; @@ -43,6 +44,7 @@ import 'state/app/modules/chat/bloc/chat_bloc.dart'; import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart'; import 'state/app/modules/children/child_selection_cubit.dart'; import 'state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart'; +import 'state/app/modules/parent_letters/bloc/parent_letters_bloc.dart'; import 'state/app/modules/settings/bloc/settings_cubit.dart'; import 'state/app/modules/timetable/primary/primary_timetable_scope.dart'; import 'storage/hydrated_storage_bootstrap.dart'; @@ -256,6 +258,7 @@ Future main() async { create: (ctx) => ChatBloc(chatListBloc: ctx.read()), ), BlocProvider(create: (_) => ChildSelectionCubit()), + BlocProvider(create: (_) => ParentLettersBloc()), ], child: const PrimaryTimetableScope(child: Main()), ), @@ -327,16 +330,27 @@ class _MainState extends State
{ capabilitiesCubit.load().then((_) { if (!mounted) return; _syncPush(settingsCubit, capabilitiesCubit); + _promptGuardianNotifications(); }), ); unawaited(context.read().load()); } + /// Waits for the post-login splash so the dialog never covers it; the + /// splash's completion calls this again. + void _promptGuardianNotifications() { + if (_showPostLoginSplash) return; + final overlayContext = AppRoutes.overlayContext; + if (overlayContext == null) return; + unawaited(maybePromptGuardianLoginNotifications(overlayContext)); + } + /// Warms the chat list and files root in the background so the first screen /// render hits populated data. The timetable needs no warm-up: /// PrimaryTimetableScope creates a freshly loading bloc per account. void _prefetchBaseData(BuildContext context) { unawaited(context.read().refresh(silent: true)); + unawaited(context.read().refresh(silent: true)); if (SessionManager().hasNextcloud) { unawaited(ListFilesCache.prefetchRootListing()); } @@ -482,6 +496,7 @@ class _MainState extends State
{ final childSelectionCubit = context .read(); final chatListBloc = context.read(); + final parentLettersBloc = context.read(); final chatBloc = context.read(); final nextcloudCapabilitiesCubit = context .read(); @@ -495,6 +510,7 @@ class _MainState extends State
{ settingsCubit: settingsCubit, childSelectionCubit: childSelectionCubit, chatListBloc: chatListBloc, + parentLettersBloc: parentLettersBloc, chatBloc: chatBloc, breakerBloc: breakerBloc, capabilitiesCubit: capabilitiesCubit, @@ -515,9 +531,10 @@ class _MainState extends State
{ if (_showPostLoginSplash) PostLoginSplash( key: const ValueKey('post-login-splash'), - onComplete: () => setState( - () => _showPostLoginSplash = false, - ), + onComplete: () { + setState(() => _showPostLoginSplash = false); + _promptGuardianNotifications(); + }, ), ], ); @@ -541,6 +558,7 @@ Future _wipeUserState({ required SettingsCubit settingsCubit, required ChildSelectionCubit childSelectionCubit, required ChatListBloc chatListBloc, + required ParentLettersBloc parentLettersBloc, required ChatBloc chatBloc, required BreakerBloc breakerBloc, required CapabilitiesCubit capabilitiesCubit, @@ -558,6 +576,7 @@ Future _wipeUserState({ await Future.wait([ childSelectionCubit.reset(), chatListBloc.reset(), + parentLettersBloc.reset(), chatBloc.reset(), breakerBloc.reset(), capabilitiesCubit.reset(), diff --git a/lib/notification/notification_controller.dart b/lib/notification/notification_controller.dart index 57f4d8a..e393883 100644 --- a/lib/notification/notification_controller.dart +++ b/lib/notification/notification_controller.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../push/push_message_handler.dart'; -import '../routing/app_routes.dart'; +import '../push/push_target.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../widget/debug/debug_tile.dart'; import '../widget/debug/json_viewer.dart'; @@ -42,18 +42,15 @@ class NotificationController { RemoteMessage message, BuildContext context, ) async { - final newsletterId = _extractNewsletterId(message); - if (newsletterId != null) { - AppRoutes.openNewsletterById( + final target = resolvePushTarget(message.data); + if (target != null) { + NotificationTasks.openPushTarget( context, - id: newsletterId, + target, title: message.notification?.title, ); } else { - NotificationTasks.navigateToTalk( - context, - chatToken: _extractChatToken(message), - ); + NotificationTasks.navigateToTalk(context); } NotificationTasks.updateProviders(context); unawaited(NotificationTasks.refreshBadge()); @@ -68,17 +65,4 @@ class NotificationController { ); }); } - - static String? _extractChatToken(RemoteMessage message) { - for (final key in const ['chatToken', 'token', 'roomToken']) { - final value = message.data[key]; - if (value is String && value.isNotEmpty) return value; - } - return null; - } - - static String? _extractNewsletterId(RemoteMessage message) { - final value = message.data['newsletterId']; - return value is String && value.isNotEmpty ? value : null; - } } diff --git a/lib/notification/notification_tasks.dart b/lib/notification/notification_tasks.dart index fd5b92b..21d18ae 100644 --- a/lib/notification/notification_tasks.dart +++ b/lib/notification/notification_tasks.dart @@ -7,10 +7,13 @@ import 'package:flutter_app_badge/flutter_app_badge.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../push/chat_thread_store.dart'; +import '../push/push_renderer.dart'; +import '../push/push_target.dart'; import '../routing/app_routes.dart'; import '../session/session_manager.dart'; import '../state/app/modules/app_modules.dart'; import '../state/app/modules/chat_list/bloc/chat_list_bloc.dart'; +import '../state/app/modules/parent_letters/bloc/parent_letters_bloc.dart'; import 'notification_service.dart'; class NotificationTasks { @@ -72,6 +75,41 @@ class NotificationTasks { /// even if the user has already left. static void updateProviders(BuildContext context) { context.read().refresh(); + context.read().refresh(silent: true); + } + + /// [title] is the notification title, when the tap came with one. + static void openPushTarget( + BuildContext context, + PushTarget target, { + String? title, + }) { + switch (target) { + case ParentLetterTarget(:final letterId): + if (AppModule.isAvailableFor( + Modules.parentLetters, + SessionManager().current, + )) { + AppRoutes.openParentLetter(context, id: letterId); + } + case NewsletterTarget(:final newsletterId): + AppRoutes.openNewsletterById(context, id: newsletterId, title: title); + case ChatTarget(:final chatToken): + navigateToTalk(context, chatToken: chatToken); + } + } + + /// Removes the letter's tray notification (Android; iOS alerts are posted by + /// the system and cannot be addressed by id). + static Future clearParentLetterNotification(String letterId) async { + try { + await NotificationService().flutterLocalNotificationsPlugin.cancel( + id: PushRenderer.parentLetterNotificationId(letterId), + ); + await refreshBadge(); + } on Object catch (e) { + log('Parent letter notification cleanup failed: $e'); + } } /// Switches to the Talk tab. If [chatToken] is provided, also schedules diff --git a/lib/push/notification_permission_prompt.dart b/lib/push/notification_permission_prompt.dart new file mode 100644 index 0000000..ec36275 --- /dev/null +++ b/lib/push/notification_permission_prompt.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:app_settings/app_settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../session/session_manager.dart'; +import '../state/app/modules/app_modules.dart'; +import '../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; +import '../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../storage/notification_settings.dart'; +import '../widget/confirm_dialog.dart'; +import 'push_registration.dart'; + +const _talkExplanation = + 'Damit du keine neuen Nachrichten im Talk verpasst, fragen wir dich ' + 'gleich nach der Erlaubnis für Benachrichtigungen. Bitte akzeptiere ' + 'sie, um Push-Nachrichten zu erhalten.'; +const _talkDeclinedNote = + 'Ohne die Berechtigung erhältst du keine Benachrichtigungen bei neuen ' + 'Talk-Nachrichten. Du kannst sie jederzeit in den Systemeinstellungen ' + 'deines Geräts nachträglich aktivieren.'; +const _parentLetterExplanation = + 'Damit du keine Elternbriefe der Schule verpasst, fragen wir dich gleich ' + 'nach der Erlaubnis für Benachrichtigungen. Bitte akzeptiere sie, um ' + 'Push-Nachrichten zu erhalten.'; +const _parentLetterDeclinedNote = + 'Ohne die Berechtigung erhältst du keine Benachrichtigungen bei neuen ' + 'Elternbriefen. Du kannst sie jederzeit in den Systemeinstellungen deines ' + 'Geräts nachträglich aktivieren.'; + +/// The occasions with a guided notification-permission flow. Each runs at +/// most once per install, guarded by its own flag in [NotificationSettings]. +enum _PermissionPrompt { + talkVisit(_talkExplanation, _talkDeclinedNote), + guardianLogin( + _parentLetterExplanation, + _parentLetterDeclinedNote, + module: Modules.parentLetters, + spentOnDisplay: true, + ), + parentLettersVisit( + _parentLetterExplanation, + _parentLetterDeclinedNote, + module: Modules.parentLetters, + ); + + const _PermissionPrompt( + this.explanation, + this.declinedNote, { + this.module, + this.spentOnDisplay = false, + }); + + final String explanation; + final String declinedNote; + + /// Only sessions that have this module are asked. + final Modules? module; + + /// Spends the one-shot as soon as the explanation is displayed, so a + /// dismissed dialog does not come back on every app start. + final bool spentOnDisplay; + + bool wasShown(NotificationSettings settings) => switch (this) { + talkVisit => settings.talkPermissionPromptShown, + guardianLogin => settings.guardianLoginPromptShown, + parentLettersVisit => settings.parentLettersPromptShown, + }; + + void markShown(NotificationSettings settings) { + switch (this) { + case talkVisit: + settings.talkPermissionPromptShown = true; + case guardianLogin: + settings.guardianLoginPromptShown = true; + case parentLettersVisit: + settings.parentLettersPromptShown = true; + } + } +} + +/// Shows the one-time notification-permission flow on the first Talk visit. +/// +/// The OS prompt is deliberately kept out of the cold-start path (younger users +/// decline it reflexively before ever seeing why they'd want it). Instead, the +/// first time Talk is opened we explain the request, then trigger the OS prompt, +/// and — if declined — offer a shortcut to the system settings. +/// +/// Runs at most once per install (guarded by `talkPermissionPromptShown`). +Future maybePromptTalkNotifications(BuildContext context) => + _maybePrompt(context, _PermissionPrompt.talkVisit); + +/// Accounts that receive parent letters never reach the Talk flow, so they +/// are asked once right after signing in. +Future maybePromptGuardianLoginNotifications(BuildContext context) => + _maybePrompt(context, _PermissionPrompt.guardianLogin); + +/// Second chance with context: explains the request once more on the first +/// visit of the parent letters when the permission is still missing. +Future maybePromptParentLetterNotifications(BuildContext context) => + _maybePrompt(context, _PermissionPrompt.parentLettersVisit); + +bool _promptInFlight = false; + +Future _maybePrompt( + BuildContext context, + _PermissionPrompt prompt, +) async { + final module = prompt.module; + if (module != null && + !AppModule.isAvailableFor(module, SessionManager().current)) { + return; + } + + final settings = context.read(); + final notificationSettings = settings.val().notificationSettings; + + // Already handled once, or the user opted out of push entirely. + if (prompt.wasShown(notificationSettings)) return; + if (!notificationSettings.enabled) return; + + // Capabilities may still be loading on a fresh cold start; retry on the next + // occasion instead of burning the one-shot flag. + if (!context.read().canReceivePushNotifications) return; + + if (_promptInFlight) return; + _promptInFlight = true; + try { + // Users who already granted the permission: register silently and mark the + // prompt as handled without showing any dialog. + if (await PushRegistration.isOsPermissionGranted()) { + prompt.markShown(settings.val(write: true).notificationSettings); + unawaited(PushRegistration().register()); + return; + } + + if (!context.mounted) return; + + if (prompt.spentOnDisplay) { + prompt.markShown(settings.val(write: true).notificationSettings); + } + await showDialog( + context: context, + builder: ConfirmDialog( + icon: Icons.notifications_active_outlined, + title: 'Benachrichtigungen aktivieren', + content: prompt.explanation, + confirmButton: 'Weiter', + cancelButton: null, + onConfirm: () => + unawaited(_requestPermission(context, settings, prompt)), + ).build, + ); + } finally { + _promptInFlight = false; + } +} + +Future _requestPermission( + BuildContext context, + SettingsCubit settings, + _PermissionPrompt prompt, +) async { + final granted = await PushRegistration.requestOsPermission(); + + // Mark handled regardless of the outcome — the user can re-enable later via + // the system settings; we don't want to prompt again on the next occasion. + prompt.markShown(settings.val(write: true).notificationSettings); + + if (granted) { + unawaited(PushRegistration().register()); + return; + } + + log('Push: notification permission declined on ${prompt.name}'); + + if (!context.mounted) return; + + ConfirmDialog( + icon: Icons.notifications_off_outlined, + title: 'Benachrichtigungen deaktiviert', + content: prompt.declinedNote, + confirmButton: 'Einstellungen öffnen', + cancelButton: 'Später', + onConfirm: () => + AppSettings.openAppSettings(type: AppSettingsType.notification), + ).asDialog(context); +} diff --git a/lib/push/push_renderer.dart b/lib/push/push_renderer.dart index 678c90c..afbaad2 100644 --- a/lib/push/push_renderer.dart +++ b/lib/push/push_renderer.dart @@ -12,6 +12,7 @@ import 'nid_store.dart'; import 'push_actions.dart'; import 'push_avatar.dart'; import 'push_subject.dart'; +import 'push_target.dart'; /// Renders decrypted push subjects (and plaintext Connect pushes) as local /// notifications. Talk messages of one chat stack into a SINGLE @@ -23,6 +24,8 @@ class PushRenderer { static const talkChannelName = 'Talk-Nachrichten'; static const generalChannelId = 'nextcloud_general'; static const generalChannelName = 'Benachrichtigungen'; + static const parentLettersChannelId = 'parent_letters'; + static const parentLettersChannelName = 'Elternbriefe'; static const String iosTalkCategory = 'TALK_MESSAGE'; @@ -67,6 +70,14 @@ class PushRenderer { description: 'Allgemeine Benachrichtigungen', ), ); + await android.createNotificationChannel( + const AndroidNotificationChannel( + parentLettersChannelId, + parentLettersChannelName, + description: 'Neue Elternbriefe und Antworten der Schule', + importance: Importance.high, + ), + ); } /// Renders a decrypted Nextcloud push subject. @@ -350,23 +361,35 @@ class PushRenderer { required String body, Map? data, }) async { - final id = _fallbackId('$title$body'); - const androidDetails = AndroidNotificationDetails( - generalChannelId, - generalChannelName, + final parentLetterId = data?[parentLetterIdKey]; + final isParentLetter = + data?['type'] == parentLetterPushType && + parentLetterId != null && + parentLetterId.isNotEmpty; + // One notification per letter: a reply replaces the letter's earlier one. + final id = isParentLetter + ? parentLetterNotificationId(parentLetterId) + : _fallbackId('$title$body'); + final androidDetails = AndroidNotificationDetails( + isParentLetter ? parentLettersChannelId : generalChannelId, + isParentLetter ? parentLettersChannelName : generalChannelName, importance: Importance.high, priority: Priority.high, color: _accentColor, + styleInformation: isParentLetter ? BigTextStyleInformation(body) : null, ); await _plugin.show( id: id, title: title, body: body, - notificationDetails: const NotificationDetails(android: androidDetails), + notificationDetails: NotificationDetails(android: androidDetails), payload: data == null ? null : jsonEncode(data), ); } + static int parentLetterNotificationId(String letterId) => + stableChatNotificationId('parent-letter:$letterId'); + String _payload({required String? chatToken, required int nid}) => jsonEncode({'chatToken': ?chatToken, 'nid': nid}); diff --git a/lib/push/push_tap_router.dart b/lib/push/push_tap_router.dart index b2ff32e..f77102a 100644 --- a/lib/push/push_tap_router.dart +++ b/lib/push/push_tap_router.dart @@ -1,24 +1,22 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'push_actions.dart'; +import 'push_target.dart'; /// Routes foreground notification interactions from the single /// flutter_local_notifications response callback. Action responses (reply / /// mark-read) are dispatched straight to [PushActions]; a plain tap publishes -/// the target chat token via [pendingChatToken] for [App] to navigate to. +/// its target via [pendingTarget] for [App] to navigate to. class PushTapRouter { PushTapRouter._(); - /// Chat token of the most recently tapped Talk notification, or null. [App] - /// listens to this and opens the chat, then resets it to null. - static final ValueNotifier pendingChatToken = ValueNotifier(null); - - /// Newsletter id of the most recently tapped Marianum-Message notification, - /// or null. [App] listens to this and opens the message, then resets it. - static final ValueNotifier pendingNewsletterId = ValueNotifier(null); + /// Target of the most recently tapped notification, or null. [App] listens + /// to this and navigates, then resets it to null. + static final ValueNotifier pendingTarget = ValueNotifier(null); static void handleResponse(NotificationResponse response) { final actionId = response.actionId; @@ -29,13 +27,28 @@ class PushTapRouter { } final map = _payloadMap(response.payload); if (map == null) return; - final newsletterId = _stringValue(map, 'newsletterId'); - if (newsletterId != null) { - pendingNewsletterId.value = newsletterId; - return; + final target = resolvePushTarget(map); + if (target != null) pendingTarget.value = target; + } + + static bool _launchHandled = false; + + /// Routes the tap that cold-started the app. The plugin reports such a tap + /// only through its launch details, never through the response callback; + /// the details stay set for the whole process, hence the one-shot guard. + static Future handleAppLaunch( + FlutterLocalNotificationsPlugin plugin, + ) async { + if (_launchHandled) return; + _launchHandled = true; + try { + final details = await plugin.getNotificationAppLaunchDetails(); + final response = details?.notificationResponse; + if (details?.didNotificationLaunchApp != true || response == null) return; + handleResponse(response); + } on Object catch (e) { + log('Reading the notification launch details failed: $e'); } - final token = _stringValue(map, 'chatToken'); - if (token != null) pendingChatToken.value = token; } static Map? _payloadMap(String? payload) { @@ -46,9 +59,4 @@ class PushTapRouter { return null; } } - - static String? _stringValue(Map map, String key) { - final value = map[key]; - return value is String && value.isNotEmpty ? value : null; - } } diff --git a/lib/push/push_target.dart b/lib/push/push_target.dart new file mode 100644 index 0000000..5f86fdd --- /dev/null +++ b/lib/push/push_target.dart @@ -0,0 +1,45 @@ +/// `type` of the visible Connect push for a parent letter. Its data carries +/// the letter under [parentLetterIdKey]. +const String parentLetterPushType = 'parent-letter'; +const String parentLetterIdKey = 'parentLetterId'; + +/// Where a tapped notification leads. +sealed class PushTarget { + const PushTarget(); +} + +class ParentLetterTarget extends PushTarget { + final String letterId; + const ParentLetterTarget(this.letterId); +} + +class NewsletterTarget extends PushTarget { + final String newsletterId; + const NewsletterTarget(this.newsletterId); +} + +class ChatTarget extends PushTarget { + final String chatToken; + const ChatTarget(this.chatToken); +} + +/// Resolves the data of a tapped notification — the payload of a locally +/// rendered one as well as the data of an FCM message. Null when it names no +/// known target. +PushTarget? resolvePushTarget(Map data) { + String? value(String key) { + final value = data[key]; + return value is String && value.isNotEmpty ? value : null; + } + + if (value(parentLetterIdKey) case final letterId?) { + return ParentLetterTarget(letterId); + } + if (value('newsletterId') case final newsletterId?) { + return NewsletterTarget(newsletterId); + } + for (final key in const ['chatToken', 'token', 'roomToken']) { + if (value(key) case final chatToken?) return ChatTarget(chatToken); + } + return null; +} diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index ad41593..62f2746 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -26,6 +26,7 @@ import '../view/pages/marianum_message/marianum_message_view.dart'; import '../view/pages/more/feedback/feedback_dialog.dart'; import '../view/pages/more/roomplan/roomplan.dart'; import '../view/pages/more/share/qr_share_view.dart'; +import '../view/pages/parent_letters/parent_letter_view.dart'; import '../view/pages/settings/chat_background_settings_page.dart'; import '../view/pages/settings/modules_settings_page.dart'; import '../view/pages/settings/settings.dart'; @@ -203,6 +204,12 @@ class AppRoutes { ); } + /// Opens a parent letter by id — from the inbox as well as from push deep + /// links, where only the id is known. + static void openParentLetter(BuildContext context, {required String id}) { + pushScreen(context, withNavBar: false, screen: ParentLetterView(id: id)); + } + /// Opens a ticker page (CONTENT or PROXIED_FILE) as a standalone detail /// screen. Used for deep links from outside the ticker module — inside the /// module pages open in-place instead. diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index b4422e6..3d0f34c 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -1,4 +1,3 @@ -import 'package:badges/badges.dart' as badges; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; @@ -16,14 +15,18 @@ import '../../../view/pages/holidays/holidays_view.dart'; import '../../../view/pages/marianum_dates/marianum_dates_view.dart'; import '../../../view/pages/marianum_message/marianum_message_list_view.dart'; import '../../../view/pages/more/roomplan/roomplan.dart'; +import '../../../view/pages/parent_letters/parent_letters_view.dart'; import '../../../view/pages/talk/chat_list.dart'; import '../../../view/pages/ticker/ticker_view.dart'; import '../../../view/pages/timetable/timetable.dart'; import '../../../widget/breaker/breaker.dart'; import '../../../widget/centered_leading.dart'; +import '../../../widget/module_badge_icon.dart'; import '../infrastructure/loadable_state/loadable_state.dart'; import 'chat_list/bloc/chat_list_bloc.dart'; import 'chat_list/bloc/chat_list_state.dart'; +import 'parent_letters/bloc/parent_letters_bloc.dart'; +import 'parent_letters/bloc/parent_letters_state.dart'; import 'settings/bloc/settings_cubit.dart'; class AppModule { @@ -46,6 +49,7 @@ class AppModule { static const Map> requirements = { Modules.talk: {AccessRequirement.nextcloud}, Modules.files: {AccessRequirement.nextcloud}, + Modules.parentLetters: {AccessRequirement.guardian}, }; static bool isAvailableFor(Modules module, Session? session) => @@ -64,6 +68,18 @@ class AppModule { breakerArea: BreakerArea.timetable, create: Timetable.new, ), + Modules.parentLetters: AppModule( + Modules.parentLetters, + name: 'Elternbriefe', + icon: () => + BlocBuilder>( + builder: (context, state) => ModuleBadgeIcon( + icon: Icons.mail_outline, + count: state.data?.unreadCount ?? 0, + ), + ), + create: ParentLettersView.new, + ), Modules.ticker: AppModule( Modules.ticker, name: 'Ticker', @@ -77,34 +93,15 @@ class AppModule { Modules.talk, name: 'Talk', icon: () => BlocBuilder>( - builder: (context, state) { - final rooms = state.data?.rooms; - if (rooms == null || rooms.data.isEmpty) { - return const Icon(Icons.chat); - } - final messages = rooms.data - .map((e) => e.unreadMessages) - .reduce((a, b) => a + b); - return badges.Badge( - showBadge: messages > 0, - position: badges.BadgePosition.topEnd(top: -3, end: -3), - stackFit: StackFit.loose, - badgeStyle: badges.BadgeStyle( - padding: const EdgeInsets.all(3), - badgeColor: Theme.of(context).primaryColor, - elevation: 1, - ), - badgeContent: Text( - '$messages', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - child: const Icon(Icons.chat), - ); - }, + builder: (context, state) => ModuleBadgeIcon( + icon: Icons.chat, + count: + state.data?.rooms?.data.fold( + 0, + (sum, room) => sum + room.unreadMessages, + ) ?? + 0, + ), ), breakerArea: BreakerArea.talk, create: ChatList.new, @@ -299,6 +296,7 @@ class AppModule { enum Modules { timetable, + parentLetters, ticker, talk, files, diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letter_bloc.dart b/lib/state/app/modules/parent_letters/bloc/parent_letter_bloc.dart new file mode 100644 index 0000000..cbeb28b --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letter_bloc.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_exception.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../../utils/random_id.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import '../repository/parent_letter_repository.dart'; +import 'parent_letter_event.dart'; +import 'parent_letter_state.dart'; +import 'parent_letters_bloc.dart'; + +/// One letter; [id] is the letter id so each letter keeps its own hydrated +/// cache entry. Changes are mirrored into the app-wide [inbox]. +class ParentLetterBloc + extends + LoadableHydratedBloc< + ParentLetterEvent, + ParentLetterState, + ParentLetterRepository + > { + final String letterId; + final ParentLettersBloc inbox; + + ParentLetterBloc(this.letterId, {required this.inbox}); + + @override + String get id => letterId; + + @override + Future gatherData() async { + final ParentLetterDetail letter; + try { + letter = await repo.getLetter(letterId); + } on ParentLetterException catch (e) { + if (e.error == ParentLetterError.letterNotFound) { + // Withdrawn: a hydrated copy must not stay readable. + add(Emit((state) => state.copyWith(letter: null))); + unawaited(inbox.refresh(silent: true)); + } + rethrow; + } + add(DataGathered((state) => state.copyWith(letter: letter))); + if (!letter.summary.read) inbox.markRead(letterId); + } + + Future submitResponse({ + required String childId, + required List answers, + Uint8List? signaturePng, + }) async { + final letter = await repo.submitResponse( + letterId: letterId, + childId: childId, + answers: answers, + signaturePng: signaturePng, + ); + if (isClosed) return; + add(Emit((state) => state.copyWith(letter: letter))); + inbox.applyDetail(letter); + } + + Future sendThreadMessage(String body) async { + final message = await repo.sendThreadMessage( + letterId: letterId, + body: body, + clientMessageId: randomUuidV4(), + ); + if (isClosed) return; + add( + Emit((state) { + final letter = state.letter; + if (letter == null) return state; + final thread = letter.content.thread; + return state.copyWith( + letter: ParentLetterDetail( + summary: letter.summary, + content: letter.content.copyWith( + thread: thread.copyWith(messages: [...thread.messages, message]), + ), + ), + ); + }), + ); + } + + Future loadAttachment(String attachmentId) => + repo.getAttachment(letterId: letterId, attachmentId: attachmentId); + + @override + ParentLetterRepository repository() => ParentLetterRepository(); + + @override + ParentLetterState fromNothing() => const ParentLetterState(); + + @override + ParentLetterState fromStorage(Map json) => + ParentLetterState.fromJson(json); + + @override + Map? toStorage(ParentLetterState state) => state.toJson(); +} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letter_event.dart b/lib/state/app/modules/parent_letters/bloc/parent_letter_event.dart new file mode 100644 index 0000000..39e2f4b --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letter_event.dart @@ -0,0 +1,5 @@ +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import 'parent_letter_state.dart'; + +sealed class ParentLetterEvent + extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letter_state.dart b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.dart new file mode 100644 index 0000000..3e8a744 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +part 'parent_letter_state.freezed.dart'; +part 'parent_letter_state.g.dart'; + +@freezed +abstract class ParentLetterState with _$ParentLetterState { + const factory ParentLetterState({ParentLetterDetail? letter}) = + _ParentLetterState; + + factory ParentLetterState.fromJson(Map json) => + _$ParentLetterStateFromJson(json); +} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letter_state.freezed.dart b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.freezed.dart new file mode 100644 index 0000000..3bb6495 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.freezed.dart @@ -0,0 +1,285 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parent_letter_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ParentLetterState { + + ParentLetterDetail? get letter; +/// Create a copy of ParentLetterState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLetterStateCopyWith get copyWith => _$ParentLetterStateCopyWithImpl(this as ParentLetterState, _$identity); + + /// Serializes this ParentLetterState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + final _this = this as ParentLetterState; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLetterState&&(identical(other.letter, _this.letter) || other.letter == _this.letter)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLetterState; + return Object.hash(runtimeType,_this.letter); +} + +@override +String toString() { + final _this = this as ParentLetterState; + return 'ParentLetterState(letter: ${_this.letter})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLetterStateCopyWith<$Res> { + factory $ParentLetterStateCopyWith(ParentLetterState value, $Res Function(ParentLetterState) _then) = _$ParentLetterStateCopyWithImpl; +@useResult +$Res call({ + ParentLetterDetail? letter +}); + + + + +} +/// @nodoc +class _$ParentLetterStateCopyWithImpl<$Res> + implements $ParentLetterStateCopyWith<$Res> { + _$ParentLetterStateCopyWithImpl(this._self, this._then); + + final ParentLetterState _self; + final $Res Function(ParentLetterState) _then; + +/// Create a copy of ParentLetterState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? letter = freezed,}) { + return _then(ParentLetterState( +letter: freezed == letter ? _self.letter : letter // ignore: cast_nullable_to_non_nullable +as ParentLetterDetail?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLetterState]. +extension ParentLetterStatePatterns on ParentLetterState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLetterState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLetterState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLetterState value) $default,){ +final _that = this; +switch (_that) { +case _ParentLetterState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLetterState value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLetterState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( ParentLetterDetail? letter)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLetterState() when $default != null: +return $default(_that.letter);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( ParentLetterDetail? letter) $default,) {final _that = this; +switch (_that) { +case _ParentLetterState(): +return $default(_that.letter);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( ParentLetterDetail? letter)? $default,) {final _that = this; +switch (_that) { +case _ParentLetterState() when $default != null: +return $default(_that.letter);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLetterState implements ParentLetterState { + const _ParentLetterState({this.letter}); + factory _ParentLetterState.fromJson(Map json) => _$ParentLetterStateFromJson(json); + +@override final ParentLetterDetail? letter; + +/// Create a copy of ParentLetterState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLetterStateCopyWith<_ParentLetterState> get copyWith => __$ParentLetterStateCopyWithImpl<_ParentLetterState>(this, _$identity); + +@override +Map toJson() { + return _$ParentLetterStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLetterState&&(identical(other.letter, letter) || other.letter == letter)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,letter); +} + +@override +String toString() { + return 'ParentLetterState(letter: $letter)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLetterStateCopyWith<$Res> implements $ParentLetterStateCopyWith<$Res> { + factory _$ParentLetterStateCopyWith(_ParentLetterState value, $Res Function(_ParentLetterState) _then) = __$ParentLetterStateCopyWithImpl; +@override @useResult +$Res call({ + ParentLetterDetail? letter +}); + + + + +} +/// @nodoc +class __$ParentLetterStateCopyWithImpl<$Res> + implements _$ParentLetterStateCopyWith<$Res> { + __$ParentLetterStateCopyWithImpl(this._self, this._then); + + final _ParentLetterState _self; + final $Res Function(_ParentLetterState) _then; + +/// Create a copy of ParentLetterState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? letter = freezed,}) { + return _then(_ParentLetterState( +letter: freezed == letter ? _self.letter : letter // ignore: cast_nullable_to_non_nullable +as ParentLetterDetail?, + )); +} + + +} + +// dart format on diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letter_state.g.dart b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.g.dart new file mode 100644 index 0000000..9c55102 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letter_state.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parent_letter_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ParentLetterState _$ParentLetterStateFromJson(Map json) => + _ParentLetterState( + letter: json['letter'] == null + ? null + : ParentLetterDetail.fromJson(json['letter'] as Map), + ); + +Map _$ParentLetterStateToJson(_ParentLetterState instance) => + {'letter': instance.letter}; diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letters_bloc.dart b/lib/state/app/modules/parent_letters/bloc/parent_letters_bloc.dart new file mode 100644 index 0000000..6786124 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letters_bloc.dart @@ -0,0 +1,107 @@ +import 'dart:developer'; + +import '../../../../../access/access_requirement.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import '../parent_letters_logic.dart'; +import '../repository/parent_letters_repository.dart'; +import 'parent_letters_event.dart'; +import 'parent_letters_state.dart'; + +/// The guardian's letter inbox. Lives app-wide because the module badge and +/// push handling need it outside the page. +class ParentLettersBloc + extends + LoadableHydratedBloc< + ParentLettersEvent, + ParentLettersState, + ParentLettersRepository + > { + @override + Set get requirements => const {AccessRequirement.guardian}; + + Future? _loading; + + /// App start, prefetch, page visit and push handling all ask for the first + /// page within moments of each other; they share one request. + @override + Future gatherData() => + _loading ??= _loadFirstPage().whenComplete(() => _loading = null); + + Future _loadFirstPage() async { + final page = await repo.getLetters(); + if (isClosed) return; + add(DataGathered((_) => firstPageState(page))); + } + + /// Reloads the first page; older pages loaded via [loadOlder] are dropped. + Future refresh({bool silent = false}) async { + if (!requirementsMet) return; + if (!silent) add(RefetchStarted()); + try { + await gatherData(); + } catch (e) { + if (isClosed) return; + if (silent) { + log('Silent parent letters refresh failed: $e'); + } else { + addLoadingError(e); + } + } + } + + Future loadOlder() async { + final letters = innerState?.letters; + if (letters == null || letters.isEmpty) return; + final page = await repo.getLetters(before: letters.last.id); + if (isClosed) return; + add(Emit((state) => appendOlderPage(state, page))); + } + + /// Optimistic locally. When nothing could be applied locally (a letter + /// opened from a push is not in the inbox yet), the follow-up refresh picks + /// it up. + void markRead(String letterId) { + final current = innerState; + final updated = current == null ? null : withLetterRead(current, letterId); + final appliedLocally = updated != null && !identical(updated, current); + if (appliedLocally) add(Emit((_) => updated)); + repo + .markRead(letterId) + .then((_) { + if (!appliedLocally) _reloadAfterWrite(); + }) + .catchError((Object e) { + log('Marking parent letter $letterId as read failed: $e'); + _reloadAfterWrite(); + }); + } + + /// A request already in flight may predate the write, so it is not joined. + Future _reloadAfterWrite() async { + try { + await _loading; + } on Object { + // Its own caller reports the failure. + } + await refresh(silent: true); + } + + /// Takes over a freshly loaded letter. + void applyDetail(ParentLetterDetail detail) => + add(Emit((state) => withSummary(state, detail.summary))); + + @override + ParentLettersRepository repository() => ParentLettersRepository(); + + @override + ParentLettersState fromNothing() => const ParentLettersState(); + + @override + ParentLettersState fromStorage(Map json) => + ParentLettersState.fromJson(json); + + @override + Map? toStorage(ParentLettersState state) => state.toJson(); +} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letters_event.dart b/lib/state/app/modules/parent_letters/bloc/parent_letters_event.dart new file mode 100644 index 0000000..81c310d --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letters_event.dart @@ -0,0 +1,5 @@ +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import 'parent_letters_state.dart'; + +sealed class ParentLettersEvent + extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letters_state.dart b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.dart new file mode 100644 index 0000000..a121b18 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +part 'parent_letters_state.freezed.dart'; +part 'parent_letters_state.g.dart'; + +/// Hydrated inbox. [unreadCount] and [openCount] cover all letters on the +/// server, not just the loaded [letters]. +@freezed +abstract class ParentLettersState with _$ParentLettersState { + const factory ParentLettersState({ + @Default([]) List letters, + @Default(false) bool hasMore, + @Default(0) int unreadCount, + @Default(0) int openCount, + }) = _ParentLettersState; + + factory ParentLettersState.fromJson(Map json) => + _$ParentLettersStateFromJson(json); +} diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letters_state.freezed.dart b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.freezed.dart new file mode 100644 index 0000000..ecfa6b8 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.freezed.dart @@ -0,0 +1,300 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'parent_letters_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ParentLettersState { + + List get letters; bool get hasMore; int get unreadCount; int get openCount; +/// Create a copy of ParentLettersState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ParentLettersStateCopyWith get copyWith => _$ParentLettersStateCopyWithImpl(this as ParentLettersState, _$identity); + + /// Serializes this ParentLettersState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + final _this = this as ParentLettersState; + return identical(this, other) || (other.runtimeType == runtimeType&&other is ParentLettersState&&const DeepCollectionEquality().equals(other.letters, _this.letters)&&(identical(other.hasMore, _this.hasMore) || other.hasMore == _this.hasMore)&&(identical(other.unreadCount, _this.unreadCount) || other.unreadCount == _this.unreadCount)&&(identical(other.openCount, _this.openCount) || other.openCount == _this.openCount)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + final _this = this as ParentLettersState; + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.letters),_this.hasMore,_this.unreadCount,_this.openCount); +} + +@override +String toString() { + final _this = this as ParentLettersState; + return 'ParentLettersState(letters: ${_this.letters}, hasMore: ${_this.hasMore}, unreadCount: ${_this.unreadCount}, openCount: ${_this.openCount})'; +} + + +} + +/// @nodoc +abstract mixin class $ParentLettersStateCopyWith<$Res> { + factory $ParentLettersStateCopyWith(ParentLettersState value, $Res Function(ParentLettersState) _then) = _$ParentLettersStateCopyWithImpl; +@useResult +$Res call({ + List letters, bool hasMore, int unreadCount, int openCount +}); + + + + +} +/// @nodoc +class _$ParentLettersStateCopyWithImpl<$Res> + implements $ParentLettersStateCopyWith<$Res> { + _$ParentLettersStateCopyWithImpl(this._self, this._then); + + final ParentLettersState _self; + final $Res Function(ParentLettersState) _then; + +/// Create a copy of ParentLettersState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? letters = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) { + return _then(ParentLettersState( +letters: null == letters ? _self.letters : letters // ignore: cast_nullable_to_non_nullable +as List,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ParentLettersState]. +extension ParentLettersStatePatterns on ParentLettersState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ParentLettersState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ParentLettersState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ParentLettersState value) $default,){ +final _that = this; +switch (_that) { +case _ParentLettersState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ParentLettersState value)? $default,){ +final _that = this; +switch (_that) { +case _ParentLettersState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List letters, bool hasMore, int unreadCount, int openCount)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ParentLettersState() when $default != null: +return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List letters, bool hasMore, int unreadCount, int openCount) $default,) {final _that = this; +switch (_that) { +case _ParentLettersState(): +return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List letters, bool hasMore, int unreadCount, int openCount)? $default,) {final _that = this; +switch (_that) { +case _ParentLettersState() when $default != null: +return $default(_that.letters,_that.hasMore,_that.unreadCount,_that.openCount);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ParentLettersState implements ParentLettersState { + const _ParentLettersState({ List letters = const [], this.hasMore = false, this.unreadCount = 0, this.openCount = 0}): _letters = letters; + factory _ParentLettersState.fromJson(Map json) => _$ParentLettersStateFromJson(json); + + final List _letters; +@override@JsonKey() List get letters { + if (_letters is EqualUnmodifiableListView) return _letters; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_letters); +} + +@override@JsonKey() final bool hasMore; +@override@JsonKey() final int unreadCount; +@override@JsonKey() final int openCount; + +/// Create a copy of ParentLettersState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ParentLettersStateCopyWith<_ParentLettersState> get copyWith => __$ParentLettersStateCopyWithImpl<_ParentLettersState>(this, _$identity); + +@override +Map toJson() { + return _$ParentLettersStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ParentLettersState&&const DeepCollectionEquality().equals(other.letters, _letters)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)&&(identical(other.openCount, openCount) || other.openCount == openCount)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode { + return Object.hash(runtimeType,const DeepCollectionEquality().hash(_letters),hasMore,unreadCount,openCount); +} + +@override +String toString() { + return 'ParentLettersState(letters: $letters, hasMore: $hasMore, unreadCount: $unreadCount, openCount: $openCount)'; +} + + +} + +/// @nodoc +abstract mixin class _$ParentLettersStateCopyWith<$Res> implements $ParentLettersStateCopyWith<$Res> { + factory _$ParentLettersStateCopyWith(_ParentLettersState value, $Res Function(_ParentLettersState) _then) = __$ParentLettersStateCopyWithImpl; +@override @useResult +$Res call({ + List letters, bool hasMore, int unreadCount, int openCount +}); + + + + +} +/// @nodoc +class __$ParentLettersStateCopyWithImpl<$Res> + implements _$ParentLettersStateCopyWith<$Res> { + __$ParentLettersStateCopyWithImpl(this._self, this._then); + + final _ParentLettersState _self; + final $Res Function(_ParentLettersState) _then; + +/// Create a copy of ParentLettersState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? letters = null,Object? hasMore = null,Object? unreadCount = null,Object? openCount = null,}) { + return _then(_ParentLettersState( +letters: null == letters ? _self._letters : letters // ignore: cast_nullable_to_non_nullable +as List,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable +as bool,unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable +as int,openCount: null == openCount ? _self.openCount : openCount // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + +// dart format on diff --git a/lib/state/app/modules/parent_letters/bloc/parent_letters_state.g.dart b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.g.dart new file mode 100644 index 0000000..460c734 --- /dev/null +++ b/lib/state/app/modules/parent_letters/bloc/parent_letters_state.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'parent_letters_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ParentLettersState _$ParentLettersStateFromJson(Map json) => + _ParentLettersState( + letters: + (json['letters'] as List?) + ?.map( + (e) => ParentLetterSummary.fromJson(e as Map), + ) + .toList() ?? + const [], + hasMore: json['hasMore'] as bool? ?? false, + unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0, + openCount: (json['openCount'] as num?)?.toInt() ?? 0, + ); + +Map _$ParentLettersStateToJson(_ParentLettersState instance) => + { + 'letters': instance.letters, + 'hasMore': instance.hasMore, + 'unreadCount': instance.unreadCount, + 'openCount': instance.openCount, + }; diff --git a/lib/state/app/modules/parent_letters/parent_letters_logic.dart b/lib/state/app/modules/parent_letters/parent_letters_logic.dart new file mode 100644 index 0000000..1f4d6b3 --- /dev/null +++ b/lib/state/app/modules/parent_letters/parent_letters_logic.dart @@ -0,0 +1,67 @@ +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import 'bloc/parent_letters_state.dart'; + +ParentLettersState firstPageState(ParentLetterListResponse page) => + ParentLettersState( + letters: page.items, + hasMore: page.hasMore, + unreadCount: page.unreadCount, + openCount: page.openCount, + ); + +ParentLettersState appendOlderPage( + ParentLettersState state, + ParentLetterListResponse page, +) { + final known = {for (final letter in state.letters) letter.id}; + return state.copyWith( + letters: [ + ...state.letters, + ...page.items.where((letter) => !known.contains(letter.id)), + ], + hasMore: page.hasMore, + unreadCount: page.unreadCount, + openCount: page.openCount, + ); +} + +/// Marks [letterId] as read and lowers the unread counter. Returns [state] +/// itself when the letter is unknown or already read. +ParentLettersState withLetterRead(ParentLettersState state, String letterId) { + final index = state.letters.indexWhere((letter) => letter.id == letterId); + if (index < 0 || state.letters[index].read) return state; + final letters = [...state.letters]; + letters[index] = letters[index].copyWith(read: true); + return state.copyWith( + letters: letters, + unreadCount: state.unreadCount > 0 ? state.unreadCount - 1 : 0, + ); +} + +/// Replaces the letter and moves [ParentLettersState.openCount] along with +/// its status, so answering does not need a reload to fix the counter. +ParentLettersState withSummary( + ParentLettersState state, + ParentLetterSummary summary, +) { + final index = state.letters.indexWhere((letter) => letter.id == summary.id); + if (index < 0) return state; + int open(ParentLetterSummary letter) => + letter.status == ParentLetterStatus.open ? 1 : 0; + final openCount = + state.openCount - open(state.letters[index]) + open(summary); + final letters = [...state.letters]; + letters[index] = summary; + return state.copyWith( + letters: letters, + openCount: openCount < 0 ? 0 : openCount, + ); +} + +/// [childId] null means all children. +List filterLettersByChild( + List letters, + String? childId, +) => childId == null + ? letters + : letters.where((letter) => letter.childIds.contains(childId)).toList(); diff --git a/lib/state/app/modules/parent_letters/repository/parent_letter_repository.dart b/lib/state/app/modules/parent_letters/repository/parent_letter_repository.dart new file mode 100644 index 0000000..295c97c --- /dev/null +++ b/lib/state/app/modules/parent_letters/repository/parent_letter_repository.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; + +import '../../../../../api/demo/data/demo_parent_letters.dart'; +import '../../../../../api/demo/demo_mode.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letter.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letter_attachment.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/post_parent_letter_thread_message.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/submit_parent_letter_response.dart'; +import '../../../infrastructure/repository/repository.dart'; +import '../bloc/parent_letter_state.dart'; + +/// Split from [ParentLettersRepository] because the loadable base binds a +/// repository to its state type. Writes never reach this class in demo mode +/// (the views guard them). +class ParentLetterRepository extends Repository { + Future getLetter(String letterId) { + if (DemoMode.active) { + return Future.value(DemoParentLetters.detail(letterId)); + } + return GetParentLetter().run(letterId); + } + + Future submitResponse({ + required String letterId, + required String childId, + required List answers, + Uint8List? signaturePng, + }) => SubmitParentLetterResponse().run( + letterId: letterId, + childId: childId, + answers: answers, + signaturePng: signaturePng, + ); + + Future sendThreadMessage({ + required String letterId, + required String body, + required String clientMessageId, + }) => PostParentLetterThreadMessage().run( + letterId: letterId, + body: body, + clientMessageId: clientMessageId, + ); + + Future getAttachment({ + required String letterId, + required String attachmentId, + }) => GetParentLetterAttachment().run( + letterId: letterId, + attachmentId: attachmentId, + ); +} diff --git a/lib/state/app/modules/parent_letters/repository/parent_letters_repository.dart b/lib/state/app/modules/parent_letters/repository/parent_letters_repository.dart new file mode 100644 index 0000000..5a9841c --- /dev/null +++ b/lib/state/app/modules/parent_letters/repository/parent_letters_repository.dart @@ -0,0 +1,22 @@ +import '../../../../../api/demo/data/demo_parent_letters.dart'; +import '../../../../../api/demo/demo_mode.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/get_parent_letters.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/mark_parent_letter_read.dart'; +import '../../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../infrastructure/repository/repository.dart'; +import '../bloc/parent_letters_state.dart'; + +class ParentLettersRepository extends Repository { + Future getLetters({String? before}) { + if (DemoMode.active) return Future.value(DemoParentLetters.list()); + return GetParentLetters().run(before: before); + } + + Future markRead(String letterId) { + if (DemoMode.active) { + DemoParentLetters.markRead(letterId); + return Future.value(); + } + return MarkParentLetterRead().run(letterId); + } +} diff --git a/lib/storage/modules_settings.g.dart b/lib/storage/modules_settings.g.dart index 20ea811..d792866 100644 --- a/lib/storage/modules_settings.g.dart +++ b/lib/storage/modules_settings.g.dart @@ -31,6 +31,7 @@ Map _$ModulesSettingsToJson( const _$ModulesEnumMap = { Modules.timetable: 'timetable', + Modules.parentLetters: 'parentLetters', Modules.ticker: 'ticker', Modules.talk: 'talk', Modules.files: 'files', diff --git a/lib/storage/notification_settings.dart b/lib/storage/notification_settings.dart index 89817b4..35e7057 100644 --- a/lib/storage/notification_settings.dart +++ b/lib/storage/notification_settings.dart @@ -16,9 +16,20 @@ class NotificationSettings { @JsonKey(defaultValue: false) bool talkPermissionPromptShown; + /// Same one-shot guards for the two occasions on which accounts with + /// parent letters are asked: right after signing in and, if the permission + /// is still missing, on the first visit of the parent letters. + @JsonKey(defaultValue: false) + bool guardianLoginPromptShown; + + @JsonKey(defaultValue: false) + bool parentLettersPromptShown; + NotificationSettings({ this.enabled = true, this.talkPermissionPromptShown = false, + this.guardianLoginPromptShown = false, + this.parentLettersPromptShown = false, }); factory NotificationSettings.fromJson(Map json) => diff --git a/lib/storage/notification_settings.g.dart b/lib/storage/notification_settings.g.dart index e468d36..fc8d4a9 100644 --- a/lib/storage/notification_settings.g.dart +++ b/lib/storage/notification_settings.g.dart @@ -12,6 +12,8 @@ NotificationSettings _$NotificationSettingsFromJson( enabled: json['enabled'] as bool? ?? true, talkPermissionPromptShown: json['talkPermissionPromptShown'] as bool? ?? false, + guardianLoginPromptShown: json['guardianLoginPromptShown'] as bool? ?? false, + parentLettersPromptShown: json['parentLettersPromptShown'] as bool? ?? false, ); Map _$NotificationSettingsToJson( @@ -19,4 +21,6 @@ Map _$NotificationSettingsToJson( ) => { 'enabled': instance.enabled, 'talkPermissionPromptShown': instance.talkPermissionPromptShown, + 'guardianLoginPromptShown': instance.guardianLoginPromptShown, + 'parentLettersPromptShown': instance.parentLettersPromptShown, }; diff --git a/lib/utils/random_id.dart b/lib/utils/random_id.dart index bfb3a92..ffbb21a 100644 --- a/lib/utils/random_id.dart +++ b/lib/utils/random_id.dart @@ -8,3 +8,12 @@ String randomHexId({int bytes = 16}) { (_) => random.nextInt(256), ).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } + +/// Random RFC 4122 version-4 UUID, for ids a server expects in UUID form. +String randomUuidV4() { + final hex = randomHexId(); + final variant = (int.parse(hex[16], radix: 16) & 0x3 | 0x8).toRadixString(16); + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-' + '4${hex.substring(13, 16)}-$variant${hex.substring(17, 20)}-' + '${hex.substring(20)}'; +} diff --git a/lib/view/pages/parent_letters/parent_letter_form_policy.dart b/lib/view/pages/parent_letters/parent_letter_form_policy.dart new file mode 100644 index 0000000..e6eec0c --- /dev/null +++ b/lib/view/pages/parent_letters/parent_letter_form_policy.dart @@ -0,0 +1,94 @@ +import '../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +enum ParentLetterFormMode { + /// Nothing answered yet and the server accepts a response. + open, + + /// Answered, but the server still accepts a change. + change, + + /// Answered and locked. + done, + + /// Unanswered and locked (deadline passed). + closed, + + /// The letter asks for an input this app version cannot render. + unsupported, +} + +/// What the response form of one child shows and allows. Whether a response +/// may be sent at all is the server's call ([ParentLetterChildState.editable]); +/// this only turns that verdict into a form. +class ParentLetterFormPolicy { + final ParentLetterFormMode mode; + final List fields; + final bool signatureRequired; + + const ParentLetterFormPolicy._( + this.mode, + this.fields, + this.signatureRequired, + ); + + bool get canSubmit => + mode == ParentLetterFormMode.open || mode == ParentLetterFormMode.change; + + bool get isAcknowledgement => fields.isEmpty && !signatureRequired; + + /// Signed responses and acknowledgements cannot be changed afterwards, so + /// the form asks for confirmation first. + bool get submitIsFinal => isAcknowledgement || signatureRequired; + + String get submitLabel { + if (isAcknowledgement) return 'Zur Kenntnis genommen'; + if (signatureRequired) return 'Unterschreiben und absenden'; + return mode == ParentLetterFormMode.change + ? 'Rückmeldung ändern' + : 'Rückmeldung absenden'; + } + + /// [selection] maps field id to the chosen option id. + bool isComplete(Map selection) => fields + .where((field) => field.isRequired) + .every((field) => selection.containsKey(field.id)); + + List answersFor(Map selection) => [ + for (final field in fields) + if (selection[field.id] case final optionId?) + ParentLetterAnswer(fieldId: field.id, optionIds: [optionId]), + ]; + + /// The selection a previous [response] stands for. + static Map selectionOf(ParentLetterResponse? response) => { + for (final answer in response?.answers ?? const []) + if (answer.optionIds.isNotEmpty) answer.fieldId: answer.optionIds.first, + }; + + /// Null when the letter asks for nothing. + static ParentLetterFormPolicy? resolve({ + required ParentLetterRequest? request, + required ParentLetterChildState child, + }) { + if (request == null) return null; + final supported = request.fields + .where((field) => field.type != ParentLetterFieldType.unknown) + .toList(); + final ParentLetterFormMode mode; + if (!child.editable) { + mode = child.response != null + ? ParentLetterFormMode.done + : ParentLetterFormMode.closed; + } else if (request.fields.any( + (field) => + field.isRequired && field.type == ParentLetterFieldType.unknown, + )) { + mode = ParentLetterFormMode.unsupported; + } else { + mode = child.response != null + ? ParentLetterFormMode.change + : ParentLetterFormMode.open; + } + return ParentLetterFormPolicy._(mode, supported, request.signatureRequired); + } +} diff --git a/lib/view/pages/parent_letters/parent_letter_view.dart b/lib/view/pages/parent_letters/parent_letter_view.dart new file mode 100644 index 0000000..42f26be --- /dev/null +++ b/lib/view/pages/parent_letters/parent_letter_view.dart @@ -0,0 +1,161 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; + +import '../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart'; +import '../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../extensions/date_time.dart'; +import '../../../notification/notification_tasks.dart'; +import '../../../state/app/infrastructure/loadable_state/loadable_state.dart'; +import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart'; +import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart'; +import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; +import '../../../state/app/modules/parent_letters/bloc/parent_letter_bloc.dart'; +import '../../../state/app/modules/parent_letters/bloc/parent_letter_state.dart'; +import '../../../state/app/modules/parent_letters/bloc/parent_letters_bloc.dart'; +import '../../../utils/url_opener.dart'; +import 'parent_letter_form_policy.dart'; +import 'widgets/parent_letter_attachments.dart'; +import 'widgets/parent_letter_response_card.dart'; +import 'widgets/parent_letter_thread.dart'; + +class ParentLetterView extends StatelessWidget { + final String id; + + const ParentLetterView({required this.id, super.key}); + + @override + Widget build( + BuildContext context, + ) => BlocModule>( + create: (context) => + ParentLetterBloc(id, inbox: context.read()), + autoRebuild: true, + onInitialisation: (_, _) => + NotificationTasks.clearParentLetterNotification(id), + child: (context, bloc, state) { + final letter = state.data?.letter; + return Scaffold( + appBar: AppBar(title: const Text('Elternbrief')), + body: Column( + children: [ + Expanded( + child: LoadableStateConsumer( + isReady: (state) => state.letter != null, + child: (state, loading) => _LetterBody( + letter: state.letter!, + children: context.watch().state.children, + bloc: bloc, + ), + ), + ), + if (letter != null && letter.content.thread.enabled) + ParentLetterThreadInput( + recipientName: letter.summary.sender.displayName, + onSend: bloc.sendThreadMessage, + ), + ], + ), + ); + }, + ); +} + +class _LetterBody extends StatelessWidget { + final ParentLetterDetail letter; + final List children; + final ParentLetterBloc bloc; + + const _LetterBody({ + required this.letter, + required this.children, + required this.bloc, + }); + + String _childName(String childId) => + children.firstWhereOrNull((child) => child.id == childId)?.displayName ?? + 'dein Kind'; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final summary = letter.summary; + final content = letter.content; + final editedAt = summary.editedAt; + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 16), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(summary.subject, style: theme.textTheme.titleLarge), + const SizedBox(height: 4), + Text( + [ + summary.sender.displayName, + summary.sentAt.formatDateTime(), + if (editedAt != null) + 'bearbeitet am ${editedAt.formatDateTime()}', + ].join(' · '), + style: theme.textTheme.bodySmall, + ), + if (children.length > 1) + Text( + 'Betrifft: ${summary.childIds.map(_childName).join(', ')}', + style: theme.textTheme.bodySmall, + ), + const SizedBox(height: 16), + SelectableLinkify( + text: content.body, + onOpen: UrlOpener.onOpen, + options: const LinkifyOptions(humanize: false), + style: theme.textTheme.bodyLarge, + ), + ], + ), + ), + if (content.attachments.isNotEmpty) ...[ + const SizedBox(height: 8), + ParentLetterAttachments( + letterId: summary.id, + attachments: content.attachments, + load: bloc.loadAttachment, + ), + ], + const SizedBox(height: 8), + for (final child in content.children) + if (ParentLetterFormPolicy.resolve( + request: content.request, + child: child, + ) + case final policy?) + ParentLetterResponseCard( + key: ValueKey((child.childId, child.response)), + childName: _childName(child.childId), + child: child, + policy: policy, + deadline: content.request?.deadline, + onSubmit: (answers, signaturePng) => bloc.submitResponse( + childId: child.childId, + answers: answers, + signaturePng: signaturePng, + ), + ), + if (content.thread.messages.isNotEmpty) ...[ + const Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + 'Nachrichten', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ParentLetterThreadMessages(content.thread.messages), + ], + ], + ); + } +} diff --git a/lib/view/pages/parent_letters/parent_letters_view.dart b/lib/view/pages/parent_letters/parent_letters_view.dart new file mode 100644 index 0000000..b783824 --- /dev/null +++ b/lib/view/pages/parent_letters/parent_letters_view.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart'; +import '../../../push/notification_permission_prompt.dart'; +import '../../../routing/app_routes.dart'; +import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart'; +import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; +import '../../../state/app/modules/parent_letters/bloc/parent_letters_bloc.dart'; +import '../../../state/app/modules/parent_letters/bloc/parent_letters_state.dart'; +import '../../../state/app/modules/parent_letters/parent_letters_logic.dart'; +import '../../../widget/async_action_button.dart'; +import '../../../widget/child_switcher.dart'; +import '../../../widget/placeholder_view.dart'; +import 'widgets/parent_letter_tile.dart'; + +class ParentLettersView extends StatefulWidget { + const ParentLettersView({super.key}); + + @override + State createState() => _ParentLettersViewState(); +} + +class _ParentLettersViewState extends State { + /// Null shows the letters of all children. Deliberately independent of the + /// app-wide child selection: the inbox must not hide a sibling's letters. + String? _childFilter; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + context.read().refresh(silent: true); + maybePromptParentLetterNotifications(context); + }); + } + + @override + Widget build(BuildContext context) { + final children = context.watch().state.children; + final filter = children.any((child) => child.id == _childFilter) + ? _childFilter + : null; + return Scaffold( + appBar: AppBar(title: const Text('Elternbriefe')), + body: Column( + children: [ + if (children.length > 1) + _ChildFilterBar( + children: children, + selected: filter, + onSelected: (id) => setState(() => _childFilter = id), + ), + Expanded( + child: LoadableStateConsumer( + child: (state, loading) { + if (children.isEmpty) return const NoChildrenPlaceholder(); + final letters = filterLettersByChild(state.letters, filter); + if (letters.isEmpty && !state.hasMore) { + return const PlaceholderView( + icon: Icons.mark_email_read_outlined, + text: 'Keine Elternbriefe vorhanden.', + ); + } + return ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: letters.length + (state.hasMore ? 1 : 0), + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + if (index == letters.length) { + return Padding( + padding: const EdgeInsets.all(8), + child: AsyncTextButton( + onPressed: context + .read() + .loadOlder, + child: const Text('Ältere Elternbriefe laden'), + ), + ); + } + final letter = letters[index]; + return ParentLetterTile( + letter: letter, + children: children, + onTap: () => + AppRoutes.openParentLetter(context, id: letter.id), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _ChildFilterBar extends StatelessWidget { + final List children; + final String? selected; + final ValueChanged onSelected; + + const _ChildFilterBar({ + required this.children, + required this.selected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) => SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + spacing: 8, + children: [ + ChoiceChip( + label: const Text('Alle'), + selected: selected == null, + onSelected: (_) => onSelected(null), + ), + for (final child in children) + ChoiceChip( + label: Text(child.firstName), + selected: selected == child.id, + onSelected: (_) => onSelected(child.id), + ), + ], + ), + ); +} diff --git a/lib/view/pages/parent_letters/widgets/parent_letter_attachments.dart b/lib/view/pages/parent_letters/widgets/parent_letter_attachments.dart new file mode 100644 index 0000000..171700e --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/parent_letter_attachments.dart @@ -0,0 +1,87 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../widget/async_action_button.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/demo_restricted.dart'; + +/// File name safe to use inside the cache directory: no path parts, never +/// empty. +String safeAttachmentFileName(ParentLetterAttachment attachment) { + final name = attachment.fileName + .split(RegExp(r'[/\\]')) + .last + .replaceAll(RegExp(r'[\x00-\x1f:*?"<>|]'), '_') + .trim(); + return name.isEmpty || name == '.' || name == '..' ? 'Anhang' : name; +} + +class ParentLetterAttachments extends StatelessWidget { + final String letterId; + final List attachments; + final Future Function(String attachmentId) load; + + const ParentLetterAttachments({ + required this.letterId, + required this.attachments, + required this.load, + super.key, + }); + + Future _open( + BuildContext context, + ParentLetterAttachment attachment, + ) async { + if (guardDemoAction(context)) return; + final cache = await getTemporaryDirectory(); + final directory = Directory( + [ + cache.path, + 'parent_letters', + Uri.encodeComponent(letterId), + Uri.encodeComponent(attachment.id), + ].join(Platform.pathSeparator), + ); + await directory.create(recursive: true); + final file = File( + '${directory.path}${Platform.pathSeparator}' + '${safeAttachmentFileName(attachment)}', + ); + // An attachment never changes under its id, so a complete earlier download + // is reused. + final cached = + file.existsSync() && + (attachment.size <= 0 || file.lengthSync() == attachment.size); + if (!cached) { + await file.writeAsBytes(await load(attachment.id), flush: true); + } + if (!context.mounted) return; + AppRoutes.openFileViewer(context, file.path); + } + + @override + Widget build(BuildContext context) => Column( + children: [ + for (final attachment in attachments) + AsyncListTile( + closeOnSuccess: false, + leading: const CenteredLeading(Icon(Icons.attach_file)), + title: Text( + safeAttachmentFileName(attachment), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: attachment.size > 0 + ? Text(filesize(attachment.size)) + : null, + onPressed: () => _open(context, attachment), + ), + ], + ); +} diff --git a/lib/view/pages/parent_letters/widgets/parent_letter_response_card.dart b/lib/view/pages/parent_letters/widgets/parent_letter_response_card.dart new file mode 100644 index 0000000..1822990 --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/parent_letter_response_card.dart @@ -0,0 +1,225 @@ +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../widget/async_action_button.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../../../../widget/demo_restricted.dart'; +import '../parent_letter_form_policy.dart'; +import 'signature_sheet.dart'; + +typedef ParentLetterSubmit = + Future Function( + List answers, + Uint8List? signaturePng, + ); + +/// Response form (or its result) of one child. The form state starts from +/// the child's response; key the card by it to restart on a new one. +class ParentLetterResponseCard extends StatefulWidget { + final String childName; + final ParentLetterChildState child; + final ParentLetterFormPolicy policy; + final DateTime? deadline; + final ParentLetterSubmit onSubmit; + + const ParentLetterResponseCard({ + required this.childName, + required this.child, + required this.policy, + required this.deadline, + required this.onSubmit, + super.key, + }); + + @override + State createState() => + _ParentLetterResponseCardState(); +} + +class _ParentLetterResponseCardState extends State { + late Map _selection = ParentLetterFormPolicy.selectionOf( + widget.child.response, + ); + + Future _submit() async { + if (guardDemoAction(context)) return; + final policy = widget.policy; + Uint8List? signature; + if (policy.signatureRequired) { + signature = await showSignatureSheet( + context, + signerHint: 'Rückmeldung für ${widget.childName}', + ); + if (signature == null || !mounted) return; + } + final answers = policy.answersFor(_selection); + if (!policy.submitIsFinal) return widget.onSubmit(answers, signature); + ConfirmDialog( + icon: Icons.task_alt, + title: 'Verbindlich absenden?', + content: + 'Die Rückmeldung für ${widget.childName} kann danach nicht mehr ' + 'geändert werden.', + confirmButton: 'Absenden', + onConfirmAsync: () => widget.onSubmit(answers, signature), + ).asDialog(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final policy = widget.policy; + final deadline = widget.deadline; + return Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Rückmeldung für ${widget.childName}', + style: theme.textTheme.titleMedium, + ), + ), + if (deadline != null && policy.canSubmit) + Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 16, 0), + child: Text( + 'Frist: ${deadline.formatDateTime()}', + style: theme.textTheme.bodySmall, + ), + ), + const SizedBox(height: 8), + ...switch (policy.mode) { + ParentLetterFormMode.open || + ParentLetterFormMode.change => _form(policy), + ParentLetterFormMode.done => _result(theme, policy), + ParentLetterFormMode.closed => [ + _note( + 'Die Frist ist abgelaufen. Eine Rückmeldung ist nicht mehr ' + 'möglich.', + ), + ], + ParentLetterFormMode.unsupported => [ + _note( + 'Für diese Rückmeldung wird eine neuere Version der App ' + 'benötigt. Bitte aktualisiere die App.', + ), + ], + }, + ], + ), + ), + ); + } + + Widget _note(String text) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(text), + ); + + List _form(ParentLetterFormPolicy policy) => [ + if (policy.mode == ParentLetterFormMode.change) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _note(_respondedLine(widget.child.response!)), + ), + for (final field in policy.fields) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + field.isRequired ? '${field.label} *' : field.label, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + RadioGroup( + groupValue: _selection[field.id], + onChanged: (optionId) { + if (optionId == null) return; + setState(() => _selection = {..._selection, field.id: optionId}); + }, + child: Column( + children: [ + for (final option in field.options) + RadioListTile( + dense: true, + title: Text(option.label), + value: option.id, + ), + ], + ), + ), + ], + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), + child: AsyncActionButton( + icon: policy.signatureRequired ? Icons.draw_outlined : Icons.check, + onPressed: policy.isComplete(_selection) ? _submit : null, + child: Text(policy.submitLabel), + ), + ), + ]; + + List _result(ThemeData theme, ParentLetterFormPolicy policy) { + final response = widget.child.response!; + final selection = ParentLetterFormPolicy.selectionOf(response); + return [ + for (final field in policy.fields) + if (selection[field.id] case final optionId?) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 6), + child: Text( + '${field.label}\n${_optionLabel(field, optionId)}', + style: theme.textTheme.bodyMedium, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + response.signed ? Icons.draw_outlined : Icons.task_alt, + size: 18, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded(child: Text(_respondedLine(response))), + ], + ), + ), + ]; + } + + String _optionLabel(ParentLetterField field, String optionId) => + field.options + .firstWhereOrNull((option) => option.id == optionId) + ?.label ?? + '–'; + + String _respondedLine(ParentLetterResponse response) { + final verb = response.signed + ? 'Unterschrieben' + : widget.policy.isAcknowledgement + ? 'Zur Kenntnis genommen' + : 'Beantwortet'; + final by = response.respondedBy.self + ? 'von dir' + : response.respondedBy.displayName.isEmpty + ? '' + : 'von ${response.respondedBy.displayName}'; + final at = response.respondedAt; + return [ + verb, + if (by.isNotEmpty) by, + if (at != null) 'am ${at.formatDateTime()}', + ].join(' '); + } +} diff --git a/lib/view/pages/parent_letters/widgets/parent_letter_status_chip.dart b/lib/view/pages/parent_letters/widgets/parent_letter_status_chip.dart new file mode 100644 index 0000000..4025bea --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/parent_letter_status_chip.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +/// Small pill for the response state of a letter; nothing for plain +/// information letters. +class ParentLetterStatusChip extends StatelessWidget { + final ParentLetterStatus status; + + const ParentLetterStatusChip(this.status, {super.key}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final (label, background, foreground) = switch (status) { + ParentLetterStatus.info => (null, null, null), + ParentLetterStatus.open => ( + 'Rückmeldung offen', + scheme.primary, + scheme.onPrimary, + ), + ParentLetterStatus.done => ( + 'Erledigt', + scheme.surfaceContainerHighest, + scheme.onSurfaceVariant, + ), + ParentLetterStatus.expired => ( + 'Frist abgelaufen', + scheme.errorContainer, + scheme.onErrorContainer, + ), + }; + if (label == null) return const SizedBox.shrink(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + label, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: foreground), + ), + ); + } +} diff --git a/lib/view/pages/parent_letters/widgets/parent_letter_thread.dart b/lib/view/pages/parent_letters/widgets/parent_letter_thread.dart new file mode 100644 index 0000000..2448ce8 --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/parent_letter_thread.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; + +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../utils/url_opener.dart'; +import '../../../../widget/async_action_button.dart'; +import '../../../../widget/demo_restricted.dart'; + +/// The private conversation between this guardian and the sender. +class ParentLetterThreadMessages extends StatelessWidget { + final List messages; + + const ParentLetterThreadMessages(this.messages, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final message in messages) + Align( + alignment: message.author.self + ? Alignment.centerRight + : Alignment.centerLeft, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.8, + ), + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: message.author.self + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + [ + if (message.author.self) + 'Du' + else + message.author.displayName, + message.sentAt.formatDateShortHm(), + ].join(' · '), + style: theme.textTheme.labelSmall, + ), + const SizedBox(height: 2), + SelectableLinkify( + text: message.body, + onOpen: UrlOpener.onOpen, + options: const LinkifyOptions(humanize: false), + ), + ], + ), + ), + ), + ], + ); + } +} + +class ParentLetterThreadInput extends StatefulWidget { + static const int maxLength = 4000; + + final String recipientName; + final Future Function(String body) onSend; + + const ParentLetterThreadInput({ + required this.recipientName, + required this.onSend, + super.key, + }); + + @override + State createState() => + _ParentLetterThreadInputState(); +} + +class _ParentLetterThreadInputState extends State { + final TextEditingController _text = TextEditingController(); + + @override + void dispose() { + _text.dispose(); + super.dispose(); + } + + Future _send() async { + if (guardDemoAction(context)) return; + await widget.onSend(_text.text.trim()); + _text.clear(); + } + + @override + Widget build(BuildContext context) => Material( + elevation: 8, + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 4, 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextField( + controller: _text, + minLines: 1, + maxLines: 5, + maxLength: ParentLetterThreadInput.maxLength, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: widget.recipientName.isEmpty + ? 'Antworten' + : 'Antwort an ${widget.recipientName}', + border: InputBorder.none, + counterText: '', + ), + ), + ), + ValueListenableBuilder( + valueListenable: _text, + builder: (context, value, _) => AsyncIconButton( + icon: Icons.send, + tooltip: 'Senden', + onPressed: value.text.trim().isEmpty ? null : _send, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/view/pages/parent_letters/widgets/parent_letter_tile.dart b/lib/view/pages/parent_letters/widgets/parent_letter_tile.dart new file mode 100644 index 0000000..4603891 --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/parent_letter_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart'; +import '../../../../api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../widget/a11y/a11y_labels.dart'; +import 'parent_letter_status_chip.dart'; + +class ParentLetterTile extends StatelessWidget { + final ParentLetterSummary letter; + + /// The guardian's children; names are only shown when there is more than + /// one to tell apart. + final List children; + final VoidCallback onTap; + + const ParentLetterTile({ + required this.letter, + required this.children, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final unread = !letter.read; + final childNames = children.length < 2 + ? '' + : children + .where((child) => letter.childIds.contains(child.id)) + .map((child) => child.firstName) + .join(', '); + final deadline = letter.deadline; + return ListTile( + leading: Semantics( + label: unread ? A11yLabels.unread : null, + child: Badge( + isLabelVisible: unread, + smallSize: 10, + backgroundColor: theme.primaryColor, + child: Icon(unread ? Icons.mail : Icons.drafts_outlined), + ), + ), + title: Text( + letter.subject, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: unread ? const TextStyle(fontWeight: FontWeight.bold) : null, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + [ + letter.sender.displayName, + letter.sentAt.formatDateRelativeShort(), + if (childNames.isNotEmpty) childNames, + ].join(' · '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (letter.preview.isNotEmpty) + Text(letter.preview, maxLines: 2, overflow: TextOverflow.ellipsis), + if (letter.status != ParentLetterStatus.info) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + ParentLetterStatusChip(letter.status), + if (deadline != null && + letter.status == ParentLetterStatus.open) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + 'bis ${deadline.formatDate()}', + style: theme.textTheme.labelSmall, + ), + ), + ], + ), + ), + ], + ), + trailing: letter.attachmentCount > 0 + ? const Icon(Icons.attach_file, size: 18) + : null, + onTap: onTap, + ); + } +} diff --git a/lib/view/pages/parent_letters/widgets/signature_sheet.dart b/lib/view/pages/parent_letters/widgets/signature_sheet.dart new file mode 100644 index 0000000..57b7c12 --- /dev/null +++ b/lib/view/pages/parent_letters/widgets/signature_sheet.dart @@ -0,0 +1,125 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:signature/signature.dart'; + +import '../../../../widget/info_dialog.dart'; + +/// Upper bound the server accepts for a signature image. +const int maxSignatureBytes = 256 * 1024; + +/// Lets the guardian draw a signature. Resolves to the PNG (transparent +/// background, cropped to the strokes) or null when dismissed. +Future showSignatureSheet( + BuildContext context, { + required String signerHint, +}) => showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: false, + showDragHandle: false, + builder: (_) => _SignatureSheet(signerHint: signerHint), +); + +class _SignatureSheet extends StatefulWidget { + final String signerHint; + + const _SignatureSheet({required this.signerHint}); + + @override + State<_SignatureSheet> createState() => _SignatureSheetState(); +} + +class _SignatureSheetState extends State<_SignatureSheet> { + // Paper-like pad in both themes: what is drawn is what gets exported. + final SignatureController _controller = SignatureController( + penStrokeWidth: 2.5, + penColor: Colors.black, + strokeCap: StrokeCap.round, + strokeJoin: StrokeJoin.round, + ); + + @override + void initState() { + super.initState(); + _controller.addListener(_onChanged); + } + + bool _isEmpty = true; + + // The controller notifies per drawn point; only the buttons depend on it. + void _onChanged() { + if (_controller.isEmpty == _isEmpty) return; + setState(() => _isEmpty = _controller.isEmpty); + } + + @override + void dispose() { + _controller.removeListener(_onChanged); + _controller.dispose(); + super.dispose(); + } + + Future _accept() async { + var png = await _controller.toPngBytes(); + if (png != null && png.lengthInBytes > maxSignatureBytes) { + png = await _controller.toPngBytes(width: 600); + } + if (!mounted) return; + if (png == null || png.lengthInBytes > maxSignatureBytes) { + InfoDialog.show( + context, + 'Die Unterschrift konnte nicht übernommen werden. Bitte versuche es ' + 'erneut.', + title: 'Unterschrift', + ); + return; + } + Navigator.pop(context, png); + } + + @override + Widget build(BuildContext context) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Unterschrift', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 4), + Text(widget.signerHint), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Signature( + controller: _controller, + height: 220, + backgroundColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: _isEmpty ? null : _controller.clear, + icon: const Icon(Icons.undo), + label: const Text('Löschen'), + ), + const Spacer(), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Abbrechen'), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _isEmpty ? null : _accept, + child: const Text('Übernehmen'), + ), + ], + ), + ], + ), + ), + ); +} diff --git a/lib/view/pages/settings/data/default_settings.dart b/lib/view/pages/settings/data/default_settings.dart index 4a54706..40b8465 100644 --- a/lib/view/pages/settings/data/default_settings.dart +++ b/lib/view/pages/settings/data/default_settings.dart @@ -25,6 +25,7 @@ class DefaultSettings { modulesSettings: ModulesSettings( moduleOrder: [ Modules.timetable, + Modules.parentLetters, Modules.ticker, Modules.talk, Modules.files, diff --git a/lib/view/pages/talk/chat_list.dart b/lib/view/pages/talk/chat_list.dart index 18051e4..4c1567c 100644 --- a/lib/view/pages/talk/chat_list.dart +++ b/lib/view/pages/talk/chat_list.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_split_view/flutter_split_view.dart'; +import '../../../push/notification_permission_prompt.dart'; import '../../../routing/app_routes.dart'; import '../../../state/app/infrastructure/loadable_state/loadable_state.dart'; import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart'; @@ -14,7 +15,6 @@ import '../../../widget/demo_restricted.dart'; import '../../../widget/placeholder_view.dart'; import 'data/open_direct_chat.dart'; import 'join_chat.dart'; -import 'notification_permission_prompt.dart'; import 'search_chat.dart'; import 'widgets/chat_tile.dart'; import 'widgets/split_view_placeholder.dart'; diff --git a/lib/view/pages/talk/notification_permission_prompt.dart b/lib/view/pages/talk/notification_permission_prompt.dart deleted file mode 100644 index e197e5d..0000000 --- a/lib/view/pages/talk/notification_permission_prompt.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:app_settings/app_settings.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../push/push_registration.dart'; -import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; -import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../widget/confirm_dialog.dart'; - -/// Shows the one-time notification-permission flow on the first Talk visit. -/// -/// The OS prompt is deliberately kept out of the cold-start path (younger users -/// decline it reflexively before ever seeing why they'd want it). Instead, the -/// first time Talk is opened we explain the request, then trigger the OS prompt, -/// and — if declined — offer a shortcut to the system settings. -/// -/// Runs at most once per install (guarded by `talkPermissionPromptShown`). -Future maybePromptTalkNotifications(BuildContext context) async { - final settings = context.read(); - final notificationSettings = settings.val().notificationSettings; - - // Already handled once, or the user opted out of push entirely. - if (notificationSettings.talkPermissionPromptShown) return; - if (!notificationSettings.enabled) return; - - // Capabilities may still be loading on a fresh cold start; retry on the next - // Talk visit instead of burning the one-shot flag. - if (!context.read().canReceivePushNotifications) return; - - // Existing users who already granted the permission: register silently and - // mark the prompt as handled without showing any dialog. - if (await PushRegistration.isOsPermissionGranted()) { - settings.val(write: true).notificationSettings.talkPermissionPromptShown = - true; - unawaited(PushRegistration().register()); - return; - } - - if (!context.mounted) return; - - ConfirmDialog( - icon: Icons.notifications_active_outlined, - title: 'Benachrichtigungen aktivieren', - content: - 'Damit du keine neuen Nachrichten im Talk verpasst, fragen wir dich ' - 'gleich nach der Erlaubnis für Benachrichtigungen. Bitte akzeptiere ' - 'sie, um Push-Nachrichten zu erhalten.', - confirmButton: 'Weiter', - cancelButton: null, - onConfirm: () => unawaited(_requestPermission(context, settings)), - ).asDialog(context); -} - -Future _requestPermission( - BuildContext context, - SettingsCubit settings, -) async { - final granted = await PushRegistration.requestOsPermission(); - - // Mark handled regardless of the outcome — the user can re-enable later via - // the system settings; we don't want to prompt again on the next Talk visit. - settings.val(write: true).notificationSettings.talkPermissionPromptShown = - true; - - if (granted) { - unawaited(PushRegistration().register()); - return; - } - - log('Push: notification permission declined on first Talk visit'); - - if (!context.mounted) return; - - ConfirmDialog( - icon: Icons.notifications_off_outlined, - title: 'Benachrichtigungen deaktiviert', - content: - 'Ohne die Berechtigung erhältst du keine Benachrichtigungen bei neuen ' - 'Talk-Nachrichten. Du kannst sie jederzeit in den Systemeinstellungen ' - 'deines Geräts nachträglich aktivieren.', - confirmButton: 'Einstellungen öffnen', - cancelButton: 'Später', - onConfirm: () => - AppSettings.openAppSettings(type: AppSettingsType.notification), - ).asDialog(context); -} diff --git a/lib/widget/module_badge_icon.dart b/lib/widget/module_badge_icon.dart new file mode 100644 index 0000000..08802f7 --- /dev/null +++ b/lib/widget/module_badge_icon.dart @@ -0,0 +1,31 @@ +import 'package:badges/badges.dart' as badges; +import 'package:flutter/material.dart'; + +/// Module icon with an unread counter; the plain icon while [count] is zero. +class ModuleBadgeIcon extends StatelessWidget { + final IconData icon; + final int count; + + const ModuleBadgeIcon({required this.icon, required this.count, super.key}); + + @override + Widget build(BuildContext context) => badges.Badge( + showBadge: count > 0, + position: badges.BadgePosition.topEnd(top: -3, end: -3), + stackFit: StackFit.loose, + badgeStyle: badges.BadgeStyle( + padding: const EdgeInsets.all(3), + badgeColor: Theme.of(context).primaryColor, + elevation: 1, + ), + badgeContent: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + child: Icon(icon), + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 1ccb61f..df42855 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,7 @@ dependencies: app_links: ^7.2.1 integration_test: sdk: flutter + signature: ^6.4.0 dev_dependencies: flutter_test: diff --git a/test/access/access_requirement_test.dart b/test/access/access_requirement_test.dart new file mode 100644 index 0000000..10932a0 --- /dev/null +++ b/test/access/access_requirement_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/access/access_requirement.dart'; +import 'package:marianum_mobile/session/session.dart'; + +void main() { + final student = CredentialSession(username: 'max', password: 'pw'); + const guardian = GuardianSession(email: 'e@x.de'); + + test('nextcloud is only met by sessions with a Nextcloud identity', () { + expect(AccessRequirement.nextcloud.isMetBy(student), isTrue); + expect(AccessRequirement.nextcloud.isMetBy(guardian), isFalse); + expect(AccessRequirement.nextcloud.isMetBy(null), isFalse); + }); + + test('guardian is only met by guardian sessions', () { + expect(AccessRequirement.guardian.isMetBy(guardian), isTrue); + expect(AccessRequirement.guardian.isMetBy(student), isFalse); + expect(AccessRequirement.guardian.isMetBy(null), isFalse); + }); + + test('a set is met only when every requirement is met', () { + expect({}.areMetBy(null), isTrue); + expect( + { + AccessRequirement.guardian, + AccessRequirement.nextcloud, + }.areMetBy(guardian), + isFalse, + ); + }); +} diff --git a/test/api/marianumconnect/parent_letter_models_test.dart b/test/api/marianumconnect/parent_letter_models_test.dart new file mode 100644 index 0000000..a5e70b4 --- /dev/null +++ b/test/api/marianumconnect/parent_letter_models_test.dart @@ -0,0 +1,241 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/errors/auth_exception.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/get_parent_letter.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_exception.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +const _detailJson = ''' +{ + "id": "l1", "subject": "Wandertag 10b", "preview": "Liebe Eltern", + "sender": {"displayName": "Hr. Müller"}, + "sentAt": "2026-09-20T08:15:00", "editedAt": null, "read": false, + "childIds": ["c1"], "attachmentCount": 1, "status": "open", + "deadline": "2026-10-01T23:59:00", + "body": "Liebe Eltern,\\n…", + "attachments": [{"id": "a1", "fileName": "Brief.pdf", "mimeType": "application/pdf", "size": 48211}], + "request": { + "fields": [ + {"id": "f1", "type": "single_choice", "label": "Teilnahme?", "required": true, + "options": [{"id": "o1", "label": "Ja"}, {"id": "o2", "label": "Nein"}]}, + {"id": "f2", "type": "date_range", "label": "Zeitraum", "required": false, "options": null} + ], + "signatureRequired": true, "deadline": "2026-10-01T23:59:00" + }, + "children": [ + {"childId": "c1", "status": "done", "editable": false, + "response": {"respondedAt": "2026-09-21T19:02:11", + "respondedBy": {"displayName": "A. Hoffmann", "self": true}, + "answers": [{"fieldId": "f1", "optionIds": ["o1"], "text": null}], "signed": true}} + ], + "thread": {"enabled": true, "messages": [ + {"id": "m1", "author": {"displayName": "Hr. Müller", "self": false}, "body": "Danke", "sentAt": "2026-09-21T20:10:00"} + ]} +} +'''; + +void main() { + group('ParentLetterSummary', () { + test('parses the documented inbox entry', () { + final response = ParentLetterListResponse.fromJson({ + 'items': [ + { + 'id': 'l1', + 'subject': 'Wandertag', + 'preview': 'Liebe Eltern', + 'sender': {'displayName': 'Hr. Müller'}, + 'sentAt': '2026-09-20T08:15:00', + 'editedAt': null, + 'read': false, + 'childIds': ['c1', 'c2'], + 'attachmentCount': 2, + 'status': 'expired', + 'deadline': '2026-10-01T23:59:00', + }, + ], + 'hasMore': true, + 'unreadCount': 3, + 'openCount': 1, + }); + final letter = response.items.single; + expect(letter.sender.displayName, 'Hr. Müller'); + expect(letter.sentAt, DateTime(2026, 9, 20, 8, 15)); + expect(letter.read, isFalse); + expect(letter.childIds, ['c1', 'c2']); + expect(letter.status, ParentLetterStatus.expired); + expect(letter.deadline, DateTime(2026, 10, 1, 23, 59)); + expect(response.hasMore, isTrue); + expect(response.unreadCount, 3); + expect(response.openCount, 1); + }); + + test('explicit nulls and unknown values fall back to safe defaults', () { + final letter = ParentLetterSummary.fromJson({ + 'id': 'l1', + 'subject': null, + 'sender': null, + 'sentAt': '2026-09-20T08:15:00', + 'read': null, + 'childIds': null, + 'status': 'archived', + 'deadline': null, + }); + expect(letter.subject, ''); + expect(letter.sender.displayName, ''); + expect(letter.read, isTrue); + expect(letter.childIds, isEmpty); + expect(letter.status, ParentLetterStatus.info); + expect(letter.deadline, isNull); + }); + + test('an empty list response is an empty inbox', () { + final response = ParentLetterListResponse.fromJson(const {}); + expect(response.items, isEmpty); + expect(response.hasMore, isFalse); + expect(response.unreadCount, 0); + }); + }); + + group('ParentLetterDetail', () { + final detail = ParentLetterDetail.fromJson( + jsonDecode(_detailJson) as Map, + ); + + test('splits the flat object into summary and content', () { + expect(detail.summary.id, 'l1'); + expect(detail.summary.status, ParentLetterStatus.open); + expect(detail.content.body, startsWith('Liebe Eltern')); + expect(detail.content.attachments.single.size, 48211); + expect(detail.content.thread.messages.single.author.self, isFalse); + }); + + test('keeps unknown field types and tolerates null options', () { + final fields = detail.content.request!.fields; + expect(fields[0].type, ParentLetterFieldType.singleChoice); + expect(fields[0].isRequired, isTrue); + expect(fields[0].options.map((o) => o.id), ['o1', 'o2']); + expect(fields[1].type, ParentLetterFieldType.unknown); + expect(fields[1].options, isEmpty); + }); + + test('parses the per-child response', () { + final child = detail.content.children.single; + expect(child.editable, isFalse); + expect(child.response!.respondedBy.self, isTrue); + expect(child.response!.signed, isTrue); + expect(child.response!.answers.single.optionIds, ['o1']); + }); + + test('a letter without request, children and thread is pure information', () { + final info = ParentLetterDetail.fromJson({ + 'id': 'l2', + 'sentAt': '2026-09-20T08:15:00', + 'request': null, + 'thread': null, + }); + expect(info.content.request, isNull); + expect(info.content.children, isEmpty); + expect(info.content.thread.enabled, isFalse); + }); + + test('survives the storage round trip', () { + final restored = ParentLetterDetail.fromJson( + jsonDecode(jsonEncode(detail.toJson())) as Map, + ); + expect(restored, detail); + }); + }); + + group('ParentLetterException mapping', () { + Future errorOf(int status, Object? body) async { + final options = RequestOptions(path: 'parent-letters/l1'); + final dio = _ThrowingDio( + DioException( + requestOptions: options, + type: DioExceptionType.badResponse, + response: Response( + requestOptions: options, + statusCode: status, + data: body, + ), + ), + ); + try { + await GetParentLetter(dio: dio).run('l1'); + } catch (e) { + return e; + } + fail('expected an error'); + } + + test('reads the JSON error code', () async { + final error = await errorOf(409, {'error': 'response_final'}); + expect( + error, + isA() + .having((e) => e.error, 'error', ParentLetterError.responseFinal) + .having((e) => e.allowRetry, 'allowRetry', isFalse), + ); + }); + + test('reads the plain-text "Fehler: " body', () async { + final error = await errorOf(410, 'Fehler: deadline_passed'); + expect( + (error as ParentLetterException).error, + ParentLetterError.deadlinePassed, + ); + }); + + test('the code wins over the status', () async { + final error = await errorOf(404, {'error': 'child_not_found'}); + expect( + (error as ParentLetterException).error, + ParentLetterError.childNotFound, + ); + }); + + test('falls back to the status without a known code', () async { + expect( + ((await errorOf(404, null)) as ParentLetterException).error, + ParentLetterError.letterNotFound, + ); + expect( + ((await errorOf(409, 'Fehler: whatever')) as ParentLetterException) + .error, + ParentLetterError.responseFinal, + ); + }); + + test('401 and 5xx keep the generic mapping', () async { + expect(await errorOf(401, 'Fehler: x'), isA()); + expect( + await errorOf(500, {'error': 'letter_not_found'}), + isNot(isA()), + ); + }); + }); +} + +/// Minimal fake Dio whose `get` always fails with the given exception; every +/// other member is unused. +class _ThrowingDio implements Dio { + final DioException error; + + _ThrowingDio(this.error); + + @override + Future> get( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) => Future>.error(error); + + @override + dynamic noSuchMethod(Invocation invocation) => + super.noSuchMethod(invocation); +} diff --git a/test/demo/demo_parent_letters_test.dart b/test/demo/demo_parent_letters_test.dart new file mode 100644 index 0000000..b3e9ebb --- /dev/null +++ b/test/demo/demo_parent_letters_test.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/demo/data/demo_parent_letters.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; + +void main() { + test('every demo inbox letter resolves to a consistent detail', () { + final inbox = DemoParentLetters.list(); + expect(inbox.items, isNotEmpty); + expect( + inbox.unreadCount, + inbox.items.where((letter) => !letter.read).length, + ); + for (final letter in inbox.items) { + final detail = DemoParentLetters.detail(letter.id); + expect(detail.summary.id, letter.id); + expect(detail.summary.status, letter.status); + expect( + detail.content.children.map((child) => child.childId), + detail.content.request == null ? isEmpty : letter.childIds, + ); + // The inbox is hydrated, so the fixtures must survive storage. + expect( + ParentLetterDetail.fromJson( + jsonDecode(jsonEncode(detail.toJson())) as Map, + ).content, + detail.content, + ); + } + }); + + test('a letter read in the session stays read', () { + final unread = DemoParentLetters.list().items.firstWhere((l) => !l.read); + DemoParentLetters.markRead(unread.id); + final after = DemoParentLetters.list().items.firstWhere( + (l) => l.id == unread.id, + ); + expect(after.read, isTrue); + }); +} diff --git a/test/push/push_tap_router_test.dart b/test/push/push_tap_router_test.dart new file mode 100644 index 0000000..4cdd46b --- /dev/null +++ b/test/push/push_tap_router_test.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/push/push_tap_router.dart'; +import 'package:marianum_mobile/push/push_target.dart'; + +void main() { + group('resolvePushTarget', () { + test('a parent letter push routes by its letter id', () { + final target = resolvePushTarget({ + 'source': 'connect', + 'type': 'parent-letter', + 'parentLetterId': 'l1', + 'event': 'reply', + 'title': 'Hr. Müller: Wandertag', + }); + expect( + target, + isA().having((t) => t.letterId, 'letterId', 'l1'), + ); + }); + + test('newsletter pushes route by newsletterId', () { + expect( + resolvePushTarget({'source': 'connect', 'newsletterId': 'n1'}), + isA().having((t) => t.newsletterId, 'id', 'n1'), + ); + }); + + test('chat pushes accept every token key in use', () { + for (final key in ['chatToken', 'token', 'roomToken']) { + expect( + resolvePushTarget({key: 'abc', 'nid': 5}), + isA().having((t) => t.chatToken, 'chatToken', 'abc'), + reason: key, + ); + } + }); + + test('empty ids and unrelated data name no target', () { + expect(resolvePushTarget({'parentLetterId': ''}), isNull); + expect(resolvePushTarget({'type': 'widget-refresh'}), isNull); + expect(resolvePushTarget({'chatToken': 7}), isNull); + }); + }); + + group('PushTapRouter.handleResponse', () { + setUp(() => PushTapRouter.pendingTarget.value = null); + + NotificationResponse tap(String payload) => NotificationResponse( + notificationResponseType: NotificationResponseType.selectedNotification, + payload: payload, + ); + + test('publishes the target of a tapped notification', () { + PushTapRouter.handleResponse(tap(jsonEncode({'parentLetterId': 'l1'}))); + expect(PushTapRouter.pendingTarget.value, isA()); + }); + + test('ignores broken payloads', () { + PushTapRouter.handleResponse(tap('not json')); + expect(PushTapRouter.pendingTarget.value, isNull); + }); + }); +} diff --git a/test/state/app_modules_order_test.dart b/test/state/app_modules_order_test.dart index 7628966..1e0f2f7 100644 --- a/test/state/app_modules_order_test.dart +++ b/test/state/app_modules_order_test.dart @@ -29,9 +29,15 @@ void main() { final effective = AppModule.effectiveModuleOrder(settingsWith(custom)); + // parentLetters and ticker are both missing and follow timetable in the + // declared order, so they line up behind it. + expect( + effective.indexOf(Modules.parentLetters), + effective.indexOf(Modules.timetable) + 1, + ); expect( effective.indexOf(Modules.ticker), - effective.indexOf(Modules.timetable) + 1, + effective.indexOf(Modules.parentLetters) + 1, ); expect(effective.toSet(), Modules.values.toSet()); }); @@ -94,10 +100,11 @@ void main() { final student = CredentialSession(username: 'max', password: 'pw'); const guardian = GuardianSession(email: 'e@x.de'); - test('password accounts see every module', () { - for (final m in Modules.values) { - expect(AppModule.isAvailableFor(m, student), isTrue, reason: m.name); - } + test('password accounts see every module but the guardian ones', () { + final hidden = Modules.values + .where((m) => !AppModule.isAvailableFor(m, student)) + .toSet(); + expect(hidden, {Modules.parentLetters}); }); test('guardians lose exactly the Nextcloud modules', () { diff --git a/test/state/parent_letters_logic_test.dart b/test/state/parent_letters_logic_test.dart new file mode 100644 index 0000000..17f6422 --- /dev/null +++ b/test/state/parent_letters_logic_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import 'package:marianum_mobile/state/app/modules/parent_letters/bloc/parent_letters_state.dart'; +import 'package:marianum_mobile/state/app/modules/parent_letters/parent_letters_logic.dart'; + +ParentLetterSummary _letter( + String id, { + bool read = true, + List childIds = const ['c1'], +}) => ParentLetterSummary( + id: id, + sentAt: DateTime(2026, 9, 20), + read: read, + childIds: childIds, +); + +void main() { + test('firstPageState takes over list and counters', () { + final state = firstPageState( + ParentLetterListResponse( + items: [_letter('a')], + hasMore: true, + unreadCount: 2, + openCount: 1, + ), + ); + expect(state.letters.single.id, 'a'); + expect(state.hasMore, isTrue); + expect(state.unreadCount, 2); + expect(state.openCount, 1); + }); + + test('appendOlderPage appends without duplicating known letters', () { + final state = ParentLettersState( + letters: [_letter('a'), _letter('b')], + hasMore: true, + ); + final next = appendOlderPage( + state, + ParentLetterListResponse(items: [_letter('b'), _letter('c')]), + ); + expect(next.letters.map((l) => l.id), ['a', 'b', 'c']); + expect(next.hasMore, isFalse); + }); + + group('withLetterRead', () { + final state = ParentLettersState( + letters: [_letter('a', read: false), _letter('b')], + unreadCount: 1, + ); + + test('marks the letter and lowers the counter', () { + final next = withLetterRead(state, 'a'); + expect(next.letters.first.read, isTrue); + expect(next.unreadCount, 0); + }); + + test('returns the same state for read or unknown letters', () { + expect(identical(withLetterRead(state, 'b'), state), isTrue); + expect(identical(withLetterRead(state, 'x'), state), isTrue); + }); + + test('never drops the counter below zero', () { + final drifted = state.copyWith(unreadCount: 0); + expect(withLetterRead(drifted, 'a').unreadCount, 0); + }); + }); + + group('withSummary', () { + final open = _letter('b').copyWith(status: ParentLetterStatus.open); + final state = ParentLettersState( + letters: [_letter('a'), open], + openCount: 3, + ); + + test('replaces the letter in place and lowers the open counter', () { + final next = withSummary( + state, + open.copyWith(status: ParentLetterStatus.done), + ); + expect(next.letters.map((l) => l.id), ['a', 'b']); + expect(next.letters.last.status, ParentLetterStatus.done); + expect(next.openCount, 2); + }); + + test('keeps the counter while the letter stays open', () { + expect(withSummary(state, open.copyWith(read: false)).openCount, 3); + }); + + test('ignores letters the inbox does not hold', () { + expect(identical(withSummary(state, _letter('x')), state), isTrue); + }); + }); + + test('filterLettersByChild keeps letters that concern the child', () { + final letters = [ + _letter('a', childIds: ['c1']), + _letter('b', childIds: ['c2']), + _letter('c', childIds: ['c1', 'c2']), + ]; + expect(filterLettersByChild(letters, null), letters); + expect(filterLettersByChild(letters, 'c2').map((l) => l.id), ['b', 'c']); + }); +} diff --git a/test/utils/random_id_test.dart b/test/utils/random_id_test.dart new file mode 100644 index 0000000..9f23fb5 --- /dev/null +++ b/test/utils/random_id_test.dart @@ -0,0 +1,15 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/utils/random_id.dart'; + +void main() { + test('randomUuidV4 is a well-formed version-4 UUID', () { + final pattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', + ); + final ids = {for (var i = 0; i < 200; i++) randomUuidV4()}; + expect(ids, hasLength(200)); + for (final id in ids) { + expect(id, matches(pattern)); + } + }); +} diff --git a/test/view/parent_letters/parent_letter_form_policy_test.dart b/test/view/parent_letters/parent_letter_form_policy_test.dart new file mode 100644 index 0000000..cef4306 --- /dev/null +++ b/test/view/parent_letters/parent_letter_form_policy_test.dart @@ -0,0 +1,168 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/marianumconnect/queries/parent_letters/parent_letter_models.dart'; +import 'package:marianum_mobile/view/pages/parent_letters/parent_letter_form_policy.dart'; +import 'package:marianum_mobile/view/pages/parent_letters/widgets/parent_letter_attachments.dart'; + +const _choice = ParentLetterField( + id: 'f1', + type: ParentLetterFieldType.singleChoice, + label: 'Teilnahme?', + isRequired: true, + options: [ + ParentLetterOption(id: 'yes', label: 'Ja'), + ParentLetterOption(id: 'no', label: 'Nein'), + ], +); +const _optionalChoice = ParentLetterField( + id: 'f2', + type: ParentLetterFieldType.singleChoice, +); +const _unknownRequired = ParentLetterField(id: 'f3', isRequired: true); +const _unknownOptional = ParentLetterField(id: 'f4'); + +const _answered = ParentLetterResponse( + answers: [ + ParentLetterAnswer(fieldId: 'f1', optionIds: ['no']), + ], +); + +ParentLetterChildState _child({ + bool editable = true, + ParentLetterResponse? response, +}) => ParentLetterChildState( + childId: 'c1', + editable: editable, + response: response, +); + +void main() { + group('ParentLetterFormPolicy.resolve', () { + test('letters without a request have no form', () { + expect( + ParentLetterFormPolicy.resolve(request: null, child: _child()), + isNull, + ); + }); + + test('mode follows the server verdict and the existing response', () { + const request = ParentLetterRequest(fields: [_choice]); + ParentLetterFormMode mode(ParentLetterChildState child) => + ParentLetterFormPolicy.resolve(request: request, child: child)!.mode; + + expect(mode(_child()), ParentLetterFormMode.open); + expect(mode(_child(response: _answered)), ParentLetterFormMode.change); + expect( + mode(_child(editable: false, response: _answered)), + ParentLetterFormMode.done, + ); + expect(mode(_child(editable: false)), ParentLetterFormMode.closed); + }); + + test('an unknown required field blocks the form', () { + final policy = ParentLetterFormPolicy.resolve( + request: const ParentLetterRequest(fields: [_choice, _unknownRequired]), + child: _child(), + )!; + expect(policy.mode, ParentLetterFormMode.unsupported); + expect(policy.canSubmit, isFalse); + }); + + test('an answered letter stays readable despite unknown fields', () { + final policy = ParentLetterFormPolicy.resolve( + request: const ParentLetterRequest(fields: [_choice, _unknownRequired]), + child: _child(editable: false, response: _answered), + )!; + expect(policy.mode, ParentLetterFormMode.done); + }); + + test('unknown optional fields are skipped', () { + final policy = ParentLetterFormPolicy.resolve( + request: const ParentLetterRequest(fields: [_choice, _unknownOptional]), + child: _child(), + )!; + expect(policy.mode, ParentLetterFormMode.open); + expect(policy.fields, [_choice]); + }); + }); + + group('submit semantics', () { + ParentLetterFormPolicy policy( + ParentLetterRequest request, { + ParentLetterResponse? response, + }) => ParentLetterFormPolicy.resolve( + request: request, + child: _child(response: response), + )!; + + test('acknowledgement', () { + final p = policy(const ParentLetterRequest()); + expect(p.isAcknowledgement, isTrue); + expect(p.submitIsFinal, isTrue); + expect(p.submitLabel, 'Zur Kenntnis genommen'); + expect(p.isComplete(const {}), isTrue); + expect(p.answersFor(const {}), isEmpty); + }); + + test('plain choice can be changed later', () { + const request = ParentLetterRequest(fields: [_choice]); + expect(policy(request).submitIsFinal, isFalse); + expect(policy(request).submitLabel, 'Rückmeldung absenden'); + expect( + policy(request, response: _answered).submitLabel, + 'Rückmeldung ändern', + ); + }); + + test('signature makes the response final', () { + final p = policy( + const ParentLetterRequest(fields: [_choice], signatureRequired: true), + ); + expect(p.submitIsFinal, isTrue); + expect(p.isAcknowledgement, isFalse); + expect(p.submitLabel, 'Unterschreiben und absenden'); + }); + + test('only required fields must be answered', () { + final p = policy( + const ParentLetterRequest(fields: [_choice, _optionalChoice]), + ); + expect(p.isComplete(const {}), isFalse); + expect(p.isComplete(const {'f2': 'x'}), isFalse); + expect(p.isComplete(const {'f1': 'yes'}), isTrue); + }); + + test('answers contain exactly the selected fields', () { + final p = policy( + const ParentLetterRequest(fields: [_choice, _optionalChoice]), + ); + expect(p.answersFor(const {'f1': 'yes', 'stale': 'x'}), const [ + ParentLetterAnswer(fieldId: 'f1', optionIds: ['yes']), + ]); + }); + + test('selectionOf restores a previous response', () { + expect(ParentLetterFormPolicy.selectionOf(_answered), {'f1': 'no'}); + expect(ParentLetterFormPolicy.selectionOf(null), isEmpty); + }); + }); + + group('safeAttachmentFileName', () { + String name(String fileName) => safeAttachmentFileName( + ParentLetterAttachment(id: 'a', fileName: fileName), + ); + + test('keeps ordinary names', () { + expect(name('Elternbrief 10b.pdf'), 'Elternbrief 10b.pdf'); + }); + + test('strips path parts and reserved characters', () { + expect(name('../../etc/passwd'), 'passwd'); + expect(name(r'C:\Users\x\brief?.pdf'), 'brief_.pdf'); + }); + + test('never returns an empty or relative name', () { + expect(name(''), 'Anhang'); + expect(name('foo/..'), 'Anhang'); + }); + }); +}