fix: Stationsabschluss absichern und Kundenformulare anpassen

Sperrt nachträgliche Aufgabenabschlüsse in App und Backend und zeigt erledigte Aufträge trotz offener optionaler Aufgaben grün an. Ergänzt Regressionstests, die Anredeauswahl und optionale Steuerangaben für Kunden. Enthält App-Version 0.9.25, Backend-Version 0.9.26 und das aktualisierte Frontend-Bundle.
This commit is contained in:
Sven committed 2026-10-06 15:09:51 +02:00
1 parent 8758c53e5b
commit 5e6044e4b1
16 files changed
+638 -69

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:votianlt_app/models/job.dart';
import 'package:votianlt_app/models/job_progress.dart';
Map<String, dynamic> task(
String id,
int order, {
bool completed = false,
bool optional = false,
}) => {
'id': id,
'jobId': 'job-1',
'stationOrder': order,
'completed': completed,
'optional': optional,
'taskSpecificData': {'taskType': 'CONFIRMATION', 'buttonText': 'Bestätigen'},
};
Job exampleJob({
String status = 'IN_PROGRESS',
List<Object> completedStations = const [],
bool mandatoryDone = true,
}) => Job.fromJson({
'id': 'job-1',
'status': status,
'completedStationOrders': completedStations,
'deliveryStations': [
{
'stationOrder': 0,
'tasks': [
task('required', 0, completed: mandatoryDone),
task('optional', 0, optional: true),
],
},
{
'stationOrder': 2,
'tasks': [task('final', 2, completed: true)],
},
],
});
void main() {
test(
'closed intermediate station locks optional tasks but leaves the next station open',
() {
final job = exampleJob(completedStations: [0]);
final progress = JobProgress(job);
expect(progress.isCompleted, isFalse);
expect(progress.canCompleteTask(job.tasks[1]), isFalse);
expect(progress.isStationClosed(0), isTrue);
expect(progress.isStationClosed(2), isFalse);
},
);
test(
'server completion is authoritative despite unfinished optional tasks',
() {
final job = exampleJob(status: 'COMPLETED');
final progress = JobProgress(job);
expect(progress.isCompleted, isTrue);
expect(progress.completedTaskCount, 2);
expect(progress.canCompleteTask(job.tasks[1]), isFalse);
expect(job.tasks[1].completed, isFalse);
},
);
test(
'all mandatory tasks alone do not complete the job without station confirmation',
() {
expect(JobProgress(exampleJob()).isCompleted, isFalse);
},
);
test(
'final station confirmation completes the job even with optional tasks open',
() {
expect(
JobProgress(exampleJob(completedStations: [0, 2])).isCompleted,
isTrue,
);
},
);
test(
'offline final station confirmation uses locally completed mandatory tasks',
() {
final progress = JobProgress(
exampleJob(mandatoryDone: false),
localCompletedStations: {0, 2},
taskStatuses: {'required': true},
);
expect(progress.isCompleted, isTrue);
expect(progress.completedTaskCount, 2);
},
);
test(
'final confirmation cannot bypass unfinished mandatory work at an earlier station',
() {
expect(
JobProgress(
exampleJob(mandatoryDone: false),
localCompletedStations: {2},
).isCompleted,
isFalse,
);
},
);
test(
'completed station confirmations survive normalization and JSON storage',
() {
final saved =
exampleJob(
completedStations: [0, '2', 2, 'invalid'],
).normalized().toJson();
final restored = Job.fromJson(saved);
expect(restored.completedStationOrders, [0, 2]);
expect(JobProgress(restored).isStationClosed(0), isTrue);
},
);
test(
'legacy job with no stations completes only after explicit station zero confirmation',
() {
final job = Job.fromJson({
'id': 'legacy',
'status': 'IN_PROGRESS',
'tasks': [task('optional', 0, optional: true)],
});
expect(JobProgress(job).isCompleted, isFalse);
expect(JobProgress(job, localCompletedStations: {0}).isCompleted, isTrue);
},
);
test('cancelled jobs remain locked and are not reported as completed', () {
final progress = JobProgress(
exampleJob(status: 'CANCELLED'),
localCompletedStations: {0, 2},
);
expect(progress.isCompleted, isFalse);
expect(progress.isStationClosed(2), isTrue);
});
test('embedded station order takes precedence over a stale task order', () {
final job = Job.fromJson({
'id': 'job-1',
'completedStationOrders': [0],
'deliveryStations': [
{
'stationOrder': 0,
'tasks': [task('optional', 8, optional: true)],
},
{'stationOrder': 1, 'tasks': []},
],
});
expect(job.tasks.single.stationOrder, 0);
expect(JobProgress(job).canCompleteTask(job.tasks.single), isFalse);
});
}