added guardian letters with chat and multiple answer functionalities
This commit is contained in:
@@ -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<String> _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<ParentLetterDetail> _letters() {
|
||||
final now = DateTime.now();
|
||||
ParentLetterDetail letter({
|
||||
required String id,
|
||||
required String subject,
|
||||
required ParentLetterPerson sender,
|
||||
required Duration age,
|
||||
required String body,
|
||||
required List<String> childIds,
|
||||
bool read = true,
|
||||
ParentLetterStatus status = ParentLetterStatus.info,
|
||||
ParentLetterRequest? request,
|
||||
List<ParentLetterChildState> children = const [],
|
||||
List<ParentLetterAttachment> 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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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": "<code>"}` or with the plain text `Fehler: <code>` 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.
|
||||
|
||||
@@ -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<T> getObject<T>(
|
||||
String path,
|
||||
@@ -55,6 +62,16 @@ abstract class MarianumConnectQuery {
|
||||
.toList();
|
||||
});
|
||||
|
||||
/// GETs the raw bytes of [path] (files that need the bearer token).
|
||||
Future<Uint8List> getBytes(String path) => guard(() async {
|
||||
final response = await dio.get<List<int>>(
|
||||
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')}';
|
||||
|
||||
@@ -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: <code>` 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) =>
|
||||
|
||||
@@ -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<Uint8List> run() => guard(() async {
|
||||
final response = await dio.get<List<int>>(
|
||||
endpoint('newsletter/${Uri.encodeComponent(id)}/file'),
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return Uint8List.fromList(response.data!);
|
||||
});
|
||||
Future<Uint8List> run() =>
|
||||
getBytes('newsletter/${Uri.encodeComponent(id)}/file');
|
||||
}
|
||||
|
||||
@@ -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<Uint8List> run() => guard(() async {
|
||||
final response = await dio.get<List<int>>(
|
||||
endpoint('ticker/pages/${Uri.encodeComponent(slug)}/file'),
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return Uint8List.fromList(response.data!);
|
||||
});
|
||||
Future<Uint8List> run() =>
|
||||
getBytes('ticker/pages/${Uri.encodeComponent(slug)}/file');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'parent_letter_models.dart';
|
||||
import 'parent_letter_query.dart';
|
||||
|
||||
class GetParentLetter extends ParentLetterQuery {
|
||||
GetParentLetter({super.dio});
|
||||
|
||||
Future<ParentLetterDetail> run(String letterId) =>
|
||||
getObject(letterPath(letterId), ParentLetterDetail.fromJson);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'parent_letter_query.dart';
|
||||
|
||||
class GetParentLetterAttachment extends ParentLetterQuery {
|
||||
GetParentLetterAttachment({super.dio});
|
||||
|
||||
Future<Uint8List> run({
|
||||
required String letterId,
|
||||
required String attachmentId,
|
||||
}) => getBytes(
|
||||
letterPath(letterId, '/attachments/${Uri.encodeComponent(attachmentId)}'),
|
||||
);
|
||||
}
|
||||
@@ -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<ParentLetterListResponse> run({String? before}) => getObject(
|
||||
'parent-letters',
|
||||
ParentLetterListResponse.fromJson,
|
||||
queryParameters: {'limit': pageSize, 'before': ?before},
|
||||
);
|
||||
}
|
||||
@@ -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<void> run(String letterId) => guard(() async {
|
||||
await dio.post<void>(endpoint(letterPath(letterId, '/read')));
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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<String, dynamic> 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<String> childIds,
|
||||
@Default(0) int attachmentCount,
|
||||
@JsonKey(unknownEnumValue: ParentLetterStatus.info)
|
||||
@Default(ParentLetterStatus.info)
|
||||
ParentLetterStatus status,
|
||||
DateTime? deadline,
|
||||
}) = _ParentLetterSummary;
|
||||
|
||||
factory ParentLetterSummary.fromJson(Map<String, dynamic> json) =>
|
||||
_$ParentLetterSummaryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterListResponse with _$ParentLetterListResponse {
|
||||
const factory ParentLetterListResponse({
|
||||
@Default([]) List<ParentLetterSummary> items,
|
||||
@Default(false) bool hasMore,
|
||||
@Default(0) int unreadCount,
|
||||
@Default(0) int openCount,
|
||||
}) = _ParentLetterListResponse;
|
||||
|
||||
factory ParentLetterListResponse.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$ParentLetterAttachmentFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterOption with _$ParentLetterOption {
|
||||
const factory ParentLetterOption({
|
||||
required String id,
|
||||
@Default('') String label,
|
||||
}) = _ParentLetterOption;
|
||||
|
||||
factory ParentLetterOption.fromJson(Map<String, dynamic> 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<ParentLetterOption> options,
|
||||
}) = _ParentLetterField;
|
||||
|
||||
factory ParentLetterField.fromJson(Map<String, dynamic> 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<ParentLetterField> fields,
|
||||
@Default(false) bool signatureRequired,
|
||||
DateTime? deadline,
|
||||
}) = _ParentLetterRequest;
|
||||
|
||||
factory ParentLetterRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$ParentLetterRequestFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterAnswer with _$ParentLetterAnswer {
|
||||
const factory ParentLetterAnswer({
|
||||
required String fieldId,
|
||||
@Default([]) List<String> optionIds,
|
||||
String? text,
|
||||
}) = _ParentLetterAnswer;
|
||||
|
||||
factory ParentLetterAnswer.fromJson(Map<String, dynamic> json) =>
|
||||
_$ParentLetterAnswerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterResponse with _$ParentLetterResponse {
|
||||
const factory ParentLetterResponse({
|
||||
DateTime? respondedAt,
|
||||
@Default(ParentLetterPerson()) ParentLetterPerson respondedBy,
|
||||
@Default([]) List<ParentLetterAnswer> answers,
|
||||
@Default(false) bool signed,
|
||||
}) = _ParentLetterResponse;
|
||||
|
||||
factory ParentLetterResponse.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$ParentLetterThreadMessageFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ParentLetterThread with _$ParentLetterThread {
|
||||
const factory ParentLetterThread({
|
||||
@Default(false) bool enabled,
|
||||
@Default([]) List<ParentLetterThreadMessage> messages,
|
||||
}) = _ParentLetterThread;
|
||||
|
||||
factory ParentLetterThread.fromJson(Map<String, dynamic> 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<ParentLetterAttachment> attachments,
|
||||
ParentLetterRequest? request,
|
||||
@Default([]) List<ParentLetterChildState> children,
|
||||
@Default(ParentLetterThread()) ParentLetterThread thread,
|
||||
}) = _ParentLetterContent;
|
||||
|
||||
factory ParentLetterContent.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
ParentLetterDetail(
|
||||
summary: ParentLetterSummary.fromJson(json),
|
||||
content: ParentLetterContent.fromJson(json),
|
||||
);
|
||||
|
||||
Map<String, dynamic> 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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,331 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'parent_letter_models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ParentLetterPerson _$ParentLetterPersonFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterPerson(
|
||||
displayName: json['displayName'] as String? ?? '',
|
||||
self: json['self'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterPersonToJson(_ParentLetterPerson instance) =>
|
||||
<String, dynamic>{
|
||||
'displayName': instance.displayName,
|
||||
'self': instance.self,
|
||||
};
|
||||
|
||||
_ParentLetterSummary _$ParentLetterSummaryFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
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<dynamic>?)
|
||||
?.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<String, dynamic> _$ParentLetterSummaryToJson(
|
||||
_ParentLetterSummary instance,
|
||||
) => <String, dynamic>{
|
||||
'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<String, dynamic> json,
|
||||
) => _ParentLetterListResponse(
|
||||
items:
|
||||
(json['items'] as List<dynamic>?)
|
||||
?.map((e) => ParentLetterSummary.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
hasMore: json['hasMore'] as bool? ?? false,
|
||||
unreadCount: (json['unreadCount'] as num?)?.toInt() ?? 0,
|
||||
openCount: (json['openCount'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterListResponseToJson(
|
||||
_ParentLetterListResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'items': instance.items,
|
||||
'hasMore': instance.hasMore,
|
||||
'unreadCount': instance.unreadCount,
|
||||
'openCount': instance.openCount,
|
||||
};
|
||||
|
||||
_ParentLetterAttachment _$ParentLetterAttachmentFromJson(
|
||||
Map<String, dynamic> 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<String, dynamic> _$ParentLetterAttachmentToJson(
|
||||
_ParentLetterAttachment instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'fileName': instance.fileName,
|
||||
'mimeType': instance.mimeType,
|
||||
'size': instance.size,
|
||||
};
|
||||
|
||||
_ParentLetterOption _$ParentLetterOptionFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterOption(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterOptionToJson(_ParentLetterOption instance) =>
|
||||
<String, dynamic>{'id': instance.id, 'label': instance.label};
|
||||
|
||||
_ParentLetterField _$ParentLetterFieldFromJson(Map<String, dynamic> 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<dynamic>?)
|
||||
?.map(
|
||||
(e) => ParentLetterOption.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterFieldToJson(_ParentLetterField instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) =>
|
||||
_ParentLetterRequest(
|
||||
fields:
|
||||
(json['fields'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ParentLetterField.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
signatureRequired: json['signatureRequired'] as bool? ?? false,
|
||||
deadline: json['deadline'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deadline'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterRequestToJson(
|
||||
_ParentLetterRequest instance,
|
||||
) => <String, dynamic>{
|
||||
'fields': instance.fields,
|
||||
'signatureRequired': instance.signatureRequired,
|
||||
'deadline': instance.deadline?.toIso8601String(),
|
||||
};
|
||||
|
||||
_ParentLetterAnswer _$ParentLetterAnswerFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterAnswer(
|
||||
fieldId: json['fieldId'] as String,
|
||||
optionIds:
|
||||
(json['optionIds'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
text: json['text'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterAnswerToJson(_ParentLetterAnswer instance) =>
|
||||
<String, dynamic>{
|
||||
'fieldId': instance.fieldId,
|
||||
'optionIds': instance.optionIds,
|
||||
'text': instance.text,
|
||||
};
|
||||
|
||||
_ParentLetterResponse _$ParentLetterResponseFromJson(
|
||||
Map<String, dynamic> 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<String, dynamic>,
|
||||
),
|
||||
answers:
|
||||
(json['answers'] as List<dynamic>?)
|
||||
?.map((e) => ParentLetterAnswer.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
signed: json['signed'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterResponseToJson(
|
||||
_ParentLetterResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'respondedAt': instance.respondedAt?.toIso8601String(),
|
||||
'respondedBy': instance.respondedBy,
|
||||
'answers': instance.answers,
|
||||
'signed': instance.signed,
|
||||
};
|
||||
|
||||
_ParentLetterChildState _$ParentLetterChildStateFromJson(
|
||||
Map<String, dynamic> 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<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterChildStateToJson(
|
||||
_ParentLetterChildState instance,
|
||||
) => <String, dynamic>{
|
||||
'childId': instance.childId,
|
||||
'status': _$ParentLetterStatusEnumMap[instance.status]!,
|
||||
'editable': instance.editable,
|
||||
'response': instance.response,
|
||||
};
|
||||
|
||||
_ParentLetterThreadMessage _$ParentLetterThreadMessageFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ParentLetterThreadMessage(
|
||||
id: json['id'] as String,
|
||||
author: json['author'] == null
|
||||
? const ParentLetterPerson()
|
||||
: ParentLetterPerson.fromJson(json['author'] as Map<String, dynamic>),
|
||||
body: json['body'] as String? ?? '',
|
||||
sentAt: DateTime.parse(json['sentAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterThreadMessageToJson(
|
||||
_ParentLetterThreadMessage instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'author': instance.author,
|
||||
'body': instance.body,
|
||||
'sentAt': instance.sentAt.toIso8601String(),
|
||||
};
|
||||
|
||||
_ParentLetterThread _$ParentLetterThreadFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterThread(
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
messages:
|
||||
(json['messages'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ParentLetterThreadMessage.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterThreadToJson(_ParentLetterThread instance) =>
|
||||
<String, dynamic>{
|
||||
'enabled': instance.enabled,
|
||||
'messages': instance.messages,
|
||||
};
|
||||
|
||||
_ParentLetterContent _$ParentLetterContentFromJson(Map<String, dynamic> json) =>
|
||||
_ParentLetterContent(
|
||||
body: json['body'] as String? ?? '',
|
||||
attachments:
|
||||
(json['attachments'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) =>
|
||||
ParentLetterAttachment.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
request: json['request'] == null
|
||||
? null
|
||||
: ParentLetterRequest.fromJson(
|
||||
json['request'] as Map<String, dynamic>,
|
||||
),
|
||||
children:
|
||||
(json['children'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) =>
|
||||
ParentLetterChildState.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
thread: json['thread'] == null
|
||||
? const ParentLetterThread()
|
||||
: ParentLetterThread.fromJson(json['thread'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ParentLetterContentToJson(
|
||||
_ParentLetterContent instance,
|
||||
) => <String, dynamic>{
|
||||
'body': instance.body,
|
||||
'attachments': instance.attachments,
|
||||
'request': instance.request,
|
||||
'children': instance.children,
|
||||
'thread': instance.thread,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<ParentLetterThreadMessage> run({
|
||||
required String letterId,
|
||||
required String body,
|
||||
required String clientMessageId,
|
||||
}) => guard(() async {
|
||||
final response = await dio.post<Map<String, dynamic>>(
|
||||
endpoint(letterPath(letterId, '/thread')),
|
||||
data: {'body': body, 'clientMessageId': clientMessageId},
|
||||
);
|
||||
return ParentLetterThreadMessage.fromJson(response.data!);
|
||||
});
|
||||
}
|
||||
@@ -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<ParentLetterDetail> run({
|
||||
required String letterId,
|
||||
required String childId,
|
||||
required List<ParentLetterAnswer> answers,
|
||||
Uint8List? signaturePng,
|
||||
}) => guard(() async {
|
||||
final response = await dio.put<Map<String, dynamic>>(
|
||||
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!);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user