added guardian letters with chat and multiple answer functionalities

This commit is contained in:
2026-09-20 16:12:45 +02:00
parent 67c935c05b
commit 2423c1a75e
68 changed files with 8348 additions and 226 deletions
@@ -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');
});
});
}