diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
index 32393e5..ada706b 100644
--- a/app/android/app/src/main/AndroidManifest.xml
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
completeJobJson = {
- 'job': {
- 'id': {'timestamp': 1705312200, '\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'},
- 'jobNumber': 'JOB-2024-001',
- 'status': 'ASSIGNED',
- 'createdAt': '2024-01-15T10:30:00.000Z',
- 'updatedAt': '2024-01-15T14:45:00.000Z',
- 'createdBy': 'admin@example.com',
- 'customerSelection': 'Kunde ABC GmbH',
- 'pickupCompany': 'Absender GmbH',
- 'pickupSalutation': 'Herr',
- 'pickupFirstName': 'Max',
- 'pickupLastName': 'Mustermann',
- 'pickupPhone': '+49 123 456789',
- 'pickupStreet': 'Hauptstraße',
- 'pickupHouseNumber': '42',
- 'pickupAddressAddition': 'Hinterhaus',
- 'pickupZip': '10115',
- 'pickupCity': 'Berlin',
- 'deliveryCompany': 'Empfänger AG',
- 'deliverySalutation': 'Frau',
- 'deliveryFirstName': 'Erika',
- 'deliveryLastName': 'Musterfrau',
- 'deliveryPhone': '+49 987 654321',
- 'deliveryStreet': 'Nebenstraße',
- 'deliveryHouseNumber': '7a',
- 'deliveryAddressAddition': null,
- 'deliveryZip': '80331',
- 'deliveryCity': 'München',
- 'digitalProcessing': true,
- 'appUser': 'driver@example.com',
- 'pickupDate': '2024-01-16',
- 'deliveryDate': '2024-01-17',
- 'remark': 'Bitte vorsichtig behandeln',
- 'price': 149.99,
- 'draft': false,
- },
- 'cargoItems': [
- {
- 'id': {'timestamp': 1705312201},
- 'jobId': {'timestamp': 1705312200},
- 'description': 'Palette mit Elektronik',
- 'quantity': 2,
- 'weightKg': 150.5,
- 'lengthMm': 1200.0,
- 'widthMm': 800.0,
- 'heightMm': 1000.0,
- },
- {
- 'id': {'timestamp': 1705312202},
- 'jobId': {'timestamp': 1705312200},
- 'description': 'Karton mit Dokumenten',
- 'quantity': 5,
- 'weightKg': 12.0,
- 'lengthMm': 400.0,
- 'widthMm': 300.0,
- 'heightMm': 200.0,
- },
- ],
- 'tasks': [
- {
- 'id': {'timestamp': 1705312210},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'completedAt': null,
- 'completedBy': null,
- 'taskOrder': 1,
- 'taskSpecificData': {
- 'taskType': 'CONFIRMATION',
- 'buttonText': 'Abholung bestätigen',
- },
- },
- {
- 'id': {'timestamp': 1705312211},
- 'jobId': {'timestamp': 1705312200},
- 'completed': true,
- 'completedAt': '2024-01-16T09:15:00.000Z',
- 'completedBy': 'driver@example.com',
- 'taskOrder': 2,
- 'taskSpecificData': {'taskType': 'SIGNATURE'},
- },
- {
- 'id': {'timestamp': 1705312212},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'completedAt': null,
- 'completedBy': null,
- 'taskOrder': 3,
- 'taskSpecificData': {
- 'taskType': 'PHOTO',
- 'minPhotoCount': 2,
- 'maxPhotoCount': 10,
- },
- },
- {
- 'id': {'timestamp': 1705312213},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'completedAt': null,
- 'completedBy': null,
- 'taskOrder': 4,
- 'taskSpecificData': {
- 'taskType': 'BARCODE',
- 'minBarcodeCount': 1,
- 'maxBarcodeCount': 5,
- },
- },
- {
- 'id': {'timestamp': 1705312214},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'completedAt': null,
- 'completedBy': null,
- 'taskOrder': 5,
- 'taskSpecificData': {
- 'taskType': 'TODOLIST',
- 'todoItems': [
- 'Ladung sichern',
- 'Dokumente prüfen',
- 'Unterschrift einholen',
- ],
- },
- },
- {
- 'id': {'timestamp': 1705312215},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'completedAt': null,
- 'completedBy': null,
- 'taskOrder': 6,
- 'taskSpecificData': {
- 'taskType': 'COMMENT',
- 'commentText': '',
- 'required': true,
- },
- },
- ],
- };
-
- group('Job Parsing', () {
- late Job job;
-
- setUp(() {
- job = Job.fromJson(completeJobJson);
- });
-
- test('parses job basic fields correctly', () {
- expect(job.id, '1705312200');
- expect(job.jobNumber, 'JOB-2024-001');
- expect(job.status, 'ASSIGNED');
- expect(job.createdBy, 'admin@example.com');
- expect(job.customerSelection, 'Kunde ABC GmbH');
- expect(job.appUser, 'driver@example.com');
- expect(job.remark, 'Bitte vorsichtig behandeln');
- });
-
- test('parses pickup address correctly', () {
- expect(job.pickupCompany, 'Absender GmbH');
- expect(job.pickupSalutation, 'Herr');
- expect(job.pickupFirstName, 'Max');
- expect(job.pickupLastName, 'Mustermann');
- expect(job.pickupPhone, '+49 123 456789');
- expect(job.pickupStreet, 'Hauptstraße');
- expect(job.pickupHouseNumber, '42');
- expect(job.pickupAddressAddition, 'Hinterhaus');
- expect(job.pickupZip, '10115');
- expect(job.pickupCity, 'Berlin');
- });
-
- test('parses delivery address correctly', () {
- expect(job.deliveryCompany, 'Empfänger AG');
- expect(job.deliverySalutation, 'Frau');
- expect(job.deliveryFirstName, 'Erika');
- expect(job.deliveryLastName, 'Musterfrau');
- expect(job.deliveryPhone, '+49 987 654321');
- expect(job.deliveryStreet, 'Nebenstraße');
- expect(job.deliveryHouseNumber, '7a');
- expect(job.deliveryAddressAddition, '');
- expect(job.deliveryZip, '80331');
- expect(job.deliveryCity, 'München');
- });
-
- test('parses date strings correctly', () {
- expect(job.pickupDate, '2024-01-16');
- expect(job.deliveryDate, '2024-01-17');
- });
-
- test('parses DateTime from ISO string', () {
- expect(job.createdAt, DateTime.utc(2024, 1, 15, 10, 30, 0));
- expect(job.updatedAt, DateTime.utc(2024, 1, 15, 14, 45, 0));
- });
-
- test('parses DateTime from array format', () {
- final jsonWithArrayDate = Map.from(completeJobJson);
- final jobData = Map.from(jsonWithArrayDate['job']);
- jobData['createdAt'] = [2024, 1, 15, 10, 30, 0, 0];
- jobData['updatedAt'] = [2024, 1, 15, 14, 45, 0, 500000000];
- jsonWithArrayDate['job'] = jobData;
-
- final jobWithArrayDate = Job.fromJson(jsonWithArrayDate);
-
- expect(jobWithArrayDate.createdAt, DateTime(2024, 1, 15, 10, 30, 0, 0));
- expect(jobWithArrayDate.updatedAt.year, 2024);
- expect(jobWithArrayDate.updatedAt.month, 1);
- expect(jobWithArrayDate.updatedAt.day, 15);
- });
-
- test('parses price as double', () {
- expect(job.price, 149.99);
- });
-
- test('parses boolean fields correctly', () {
- expect(job.digitalProcessing, true);
- expect(job.draft, false);
- });
-
- test('parses cargoItems array', () {
- expect(job.cargoItems.length, 2);
- });
-
- test('parses tasks array', () {
- expect(job.tasks.length, 6);
- });
-
- test('parses delivery stations and flattens station tasks', () {
- final jsonWithStations = {
- 'job': {
- 'id': 'station-job-1',
- 'jobNumber': 'JOB-STATION-001',
- 'status': 'CREATED',
- 'deliveryCompany': 'Legacy Delivery',
- 'deliveryStreet': 'Legacy Street',
- 'deliveryHouseNumber': '1',
- 'deliveryZip': '12345',
- 'deliveryCity': 'Legacy City',
- 'deliveryCitiesDisplay': 'Boostedt -> Geesthacht',
- 'firstDeliveryCity': 'Boostedt',
- 'lastDeliveryCity': 'Geesthacht',
- 'deliveryStations': [
- {
- 'stationOrder': 0,
- 'company': 'Volker Hinst',
- 'street': 'Vossbarg',
- 'houseNumber': '24',
- 'zip': '24893',
- 'city': 'Boostedt',
- 'tasks': [
- {
- 'id': 'station-task-1',
- 'jobId': 'station-job-1',
- 'taskOrder': 0,
- 'description': 'Erste Station',
- 'displayName': 'Bestätigung',
- 'taskSpecificData': {
- 'taskType': 'CONFIRMATION',
- 'buttonText': 'Blubb',
- },
- },
- ],
- },
- {
- 'stationOrder': 1,
- 'company': 'Timm GmbH',
- 'street': 'Gerhart-Hauptmann-Weg',
- 'houseNumber': '14',
- 'zip': '21502',
- 'city': 'Geesthacht',
- 'tasks': [
- {
- 'id': 'station-task-2',
- 'jobId': 'station-job-1',
- 'taskOrder': 0,
- 'description': 'Zweite Station',
- 'displayName': 'Bestätigung',
- 'taskSpecificData': {
- 'taskType': 'CONFIRMATION',
- 'buttonText': 'Blubb',
- },
- },
- ],
- },
- ],
- },
- 'cargoItems': [],
- 'tasks': [
- {
- 'id': 'legacy-task',
- 'jobId': 'station-job-1',
- 'taskOrder': 0,
- 'description': 'Legacy',
- 'taskSpecificData': {
- 'taskType': 'CONFIRMATION',
- 'buttonText': 'Legacy',
- },
- },
- ],
- };
-
- final jobWithStations = Job.fromJson(jsonWithStations);
-
- expect(jobWithStations.deliveryStations.length, 2);
- expect(jobWithStations.tasks.length, 2);
- expect(jobWithStations.tasks[0].stationOrder, 0);
- expect(jobWithStations.tasks[1].stationOrder, 1);
- expect(
- jobWithStations.deliveryStations[0].tasks.first.id,
- 'station-task-1',
- );
- expect(jobWithStations.deliveryCitiesDisplay, 'Boostedt -> Geesthacht');
- });
-
- test('extracts ID from Map object with timestamp', () {
- expect(job.id, '1705312200');
- });
-
- test('extracts ID from Map object with \$oid fallback', () {
- final jsonWithOidOnly = {
- 'job': {
- 'id': {'\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'},
- 'jobNumber': 'JOB-TEST',
- 'status': 'CREATED',
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final jobWithOid = Job.fromJson(jsonWithOidOnly);
- expect(jobWithOid.id, '65a4b5c8d4e5f6a7b8c9d0e1');
- });
-
- test('handles flat JSON structure (without nested job object)', () {
- final flatJson = {
- 'id': 'flat-job-id-123',
- 'jobNumber': 'JOB-FLAT-001',
- 'status': 'CREATED',
- 'createdAt': '2024-01-15T10:30:00.000Z',
- 'updatedAt': '2024-01-15T10:30:00.000Z',
- 'createdBy': 'test@test.de',
- 'customerSelection': 'Test Kunde',
- 'pickupCompany': 'Test Firma',
- 'pickupFirstName': 'Test',
- 'pickupLastName': 'User',
- 'pickupPhone': '12345',
- 'pickupStreet': 'Teststr',
- 'pickupHouseNumber': '1',
- 'pickupAddressAddition': '',
- 'pickupZip': '12345',
- 'pickupCity': 'Teststadt',
- 'deliveryCompany': 'Ziel Firma',
- 'deliveryFirstName': 'Ziel',
- 'deliveryLastName': 'Person',
- 'deliveryPhone': '54321',
- 'deliveryStreet': 'Zielstr',
- 'deliveryHouseNumber': '2',
- 'deliveryAddressAddition': '',
- 'deliveryZip': '54321',
- 'deliveryCity': 'Zielstadt',
- 'digitalProcessing': false,
- 'appUser': 'user@test.de',
- 'pickupDate': '2024-01-20',
- 'deliveryDate': '2024-01-21',
- 'remark': '',
- 'price': 0.0,
- 'draft': true,
- };
-
- final flatJob = Job.fromJson(flatJson);
-
- expect(flatJob.id, 'flat-job-id-123');
- expect(flatJob.jobNumber, 'JOB-FLAT-001');
- expect(flatJob.draft, true);
- });
- });
-
- group('CargoItem Parsing', () {
- late Job job;
-
- setUp(() {
- job = Job.fromJson(completeJobJson);
- });
-
- test('parses CargoItem fields correctly', () {
- final cargoItem = job.cargoItems[0];
-
- expect(cargoItem.id, '1705312201');
- expect(cargoItem.jobId, '1705312200');
- expect(cargoItem.description, 'Palette mit Elektronik');
- expect(cargoItem.quantity, 2);
- expect(cargoItem.weightKg, 150.5);
- expect(cargoItem.lengthCm, 1200.0);
- expect(cargoItem.widthCm, 800.0);
- expect(cargoItem.heightCm, 1000.0);
- });
-
- test('parses multiple CargoItems', () {
- expect(job.cargoItems.length, 2);
- expect(job.cargoItems[0].description, 'Palette mit Elektronik');
- expect(job.cargoItems[1].description, 'Karton mit Dokumenten');
- });
-
- test('extracts CargoItem ID from Map object', () {
- expect(job.cargoItems[0].id, '1705312201');
- expect(job.cargoItems[1].id, '1705312202');
- });
-
- test('handles CargoItem with simple string ID', () {
- final cargoJson = {
- 'id': 'simple-string-id',
- 'jobId': 'simple-job-id',
- 'description': 'Test Item',
- 'quantity': 1,
- 'weightKg': 10.0,
- 'lengthMm': 100.0,
- 'widthMm': 100.0,
- 'heightMm': 100.0,
- };
-
- final cargoItem = CargoItem.fromJson(cargoJson);
-
- expect(cargoItem.id, 'simple-string-id');
- expect(cargoItem.jobId, 'simple-job-id');
- });
- });
-
- group('Task Parsing', () {
- late Job job;
-
- setUp(() {
- job = Job.fromJson(completeJobJson);
- });
-
- test('creates ConfirmationTask with buttonText', () {
- final task = job.tasks[0];
-
- expect(task, isA());
- final confirmationTask = task as ConfirmationTask;
- expect(confirmationTask.taskOrder, 1);
- expect(confirmationTask.buttonText, 'Abholung bestätigen');
- expect(confirmationTask.completed, false);
- });
-
- test('creates SignatureTask', () {
- final task = job.tasks[1];
-
- expect(task, isA());
- expect(task.taskOrder, 2);
- expect(task.completed, true);
- expect(task.completedBy, 'driver@example.com');
- });
-
- test('creates PhotoTask with min/maxPhotoCount', () {
- final task = job.tasks[2];
-
- expect(task, isA());
- final photoTask = task as PhotoTask;
- expect(photoTask.taskOrder, 3);
- expect(photoTask.minPhotoCount, 2);
- expect(photoTask.maxPhotoCount, 10);
- });
-
- test('creates BarcodeTask with min/maxBarcodeCount', () {
- final task = job.tasks[3];
-
- expect(task, isA());
- final barcodeTask = task as BarcodeTask;
- expect(barcodeTask.taskOrder, 4);
- expect(barcodeTask.minBarcodeCount, 1);
- expect(barcodeTask.maxBarcodeCount, 5);
- });
-
- test('creates TodoListTask with todoItems', () {
- final task = job.tasks[4];
-
- expect(task, isA());
- final todoListTask = task as TodoListTask;
- expect(todoListTask.taskOrder, 5);
- expect(todoListTask.todoItems.length, 3);
- expect(todoListTask.todoItems[0], 'Ladung sichern');
- expect(todoListTask.todoItems[1], 'Dokumente prüfen');
- expect(todoListTask.todoItems[2], 'Unterschrift einholen');
- });
-
- test('creates CommentTask with commentText and required', () {
- final task = job.tasks[5];
-
- expect(task, isA());
- final commentTask = task as CommentTask;
- expect(commentTask.taskOrder, 6);
- expect(commentTask.commentText, '');
- expect(commentTask.required, true);
- });
-
- test('falls back to GenericTask for unknown task type', () {
- final unknownTaskJson = {
- 'id': {'timestamp': 1705312299},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'taskOrder': 99,
- 'taskSpecificData': {'taskType': 'UNKNOWN_TYPE'},
- };
-
- final task = Task.fromJson(unknownTaskJson);
-
- expect(task, isA());
- });
-
- test('falls back to GenericTask when taskType is missing', () {
- final noTypeTaskJson = {
- 'id': {'timestamp': 1705312298},
- 'jobId': {'timestamp': 1705312200},
- 'completed': false,
- 'taskOrder': 98,
- 'taskSpecificData': {},
- };
-
- final task = Task.fromJson(noTypeTaskJson);
-
- expect(task, isA());
- });
-
- test('parses completedAt from ISO string', () {
- final task = job.tasks[1];
-
- expect(task.completedAt, DateTime.utc(2024, 1, 16, 9, 15, 0));
- });
-
- test('parses completedAt from array format', () {
- final taskJsonWithArrayDate = {
- 'id': {'timestamp': 1705312220},
- 'jobId': {'timestamp': 1705312200},
- 'completed': true,
- 'completedAt': [2024, 1, 16, 9, 15, 0, 0],
- 'completedBy': 'driver@example.com',
- 'taskOrder': 10,
- 'taskSpecificData': {'taskType': 'SIGNATURE'},
- };
-
- final task = Task.fromJson(taskJsonWithArrayDate);
-
- expect(task.completedAt, DateTime(2024, 1, 16, 9, 15, 0, 0));
- });
-
- test('extracts task ID from Map object', () {
- final task = job.tasks[0];
- expect(task.id, '1705312210');
- });
-
- test('extracts task jobId from Map object', () {
- final task = job.tasks[0];
- expect(task.jobId, '1705312200');
- });
- });
-
- group('Task Defaults', () {
- test('ConfirmationTask uses default buttonText', () {
- final taskJson = {
- 'id': 'task-1',
- 'jobId': 'job-1',
- 'taskOrder': 1,
- 'taskSpecificData': {'taskType': 'CONFIRMATION'},
- };
-
- final task = Task.fromJson(taskJson) as ConfirmationTask;
- expect(task.buttonText, 'Bestätigen');
- });
-
- test('PhotoTask uses default min/max counts', () {
- final taskJson = {
- 'id': 'task-2',
- 'jobId': 'job-1',
- 'taskOrder': 2,
- 'taskSpecificData': {'taskType': 'PHOTO'},
- };
-
- final task = Task.fromJson(taskJson) as PhotoTask;
- expect(task.minPhotoCount, 1);
- expect(task.maxPhotoCount, 5);
- });
-
- test('BarcodeTask uses default min/max counts', () {
- final taskJson = {
- 'id': 'task-3',
- 'jobId': 'job-1',
- 'taskOrder': 3,
- 'taskSpecificData': {'taskType': 'BARCODE'},
- };
-
- final task = Task.fromJson(taskJson) as BarcodeTask;
- expect(task.minBarcodeCount, 1);
- expect(task.maxBarcodeCount, 10);
- });
-
- test('CommentTask uses default values', () {
- final taskJson = {
- 'id': 'task-4',
- 'jobId': 'job-1',
- 'taskOrder': 4,
- 'taskSpecificData': {'taskType': 'COMMENT'},
- };
-
- final task = Task.fromJson(taskJson) as CommentTask;
- expect(task.commentText, '');
- expect(task.required, false);
- });
-
- test('TodoListTask handles empty todoItems', () {
- final taskJson = {
- 'id': 'task-5',
- 'jobId': 'job-1',
- 'taskOrder': 5,
- 'taskSpecificData': {'taskType': 'TODOLIST'},
- };
-
- final task = Task.fromJson(taskJson) as TodoListTask;
- expect(task.todoItems, isEmpty);
- });
- });
-
- group('Edge Cases', () {
- test('handles empty cargoItems array', () {
- final jsonWithEmptyCargoItems = Map.from(
- completeJobJson,
- );
- jsonWithEmptyCargoItems['cargoItems'] = [];
-
- final job = Job.fromJson(jsonWithEmptyCargoItems);
-
- expect(job.cargoItems, isEmpty);
- });
-
- test('handles empty tasks array', () {
- final jsonWithEmptyTasks = Map.from(completeJobJson);
- jsonWithEmptyTasks['tasks'] = [];
-
- final job = Job.fromJson(jsonWithEmptyTasks);
-
- expect(job.tasks, isEmpty);
- });
-
- test('handles missing optional fields with defaults', () {
- final minimalJson = {
- 'job': {'jobNumber': 'JOB-MIN-001'},
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(minimalJson);
-
- expect(job.jobNumber, 'JOB-MIN-001');
- expect(job.status, 'UNKNOWN');
- expect(job.pickupCompany, '');
- expect(job.deliveryCompany, '');
- expect(job.digitalProcessing, false);
- expect(job.price, 0.0);
- expect(job.draft, false);
- });
-
- test('handles null values in optional fields', () {
- final jsonWithNulls = {
- 'job': {
- 'id': 'null-test-id',
- 'jobNumber': 'JOB-NULL-001',
- 'status': 'CREATED',
- 'pickupSalutation': null,
- 'deliverySalutation': null,
- 'pickupAddressAddition': null,
- 'deliveryAddressAddition': null,
- 'remark': null,
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(jsonWithNulls);
-
- expect(job.pickupSalutation, isNull);
- expect(job.deliverySalutation, isNull);
- expect(job.pickupAddressAddition, '');
- expect(job.deliveryAddressAddition, '');
- expect(job.remark, '');
- });
-
- test('generates ID from jobNumber when ID is missing', () {
- final jsonWithoutId = {
- 'job': {'jobNumber': 'JOB-NOID-001'},
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(jsonWithoutId);
-
- expect(job.id, 'jobnum:JOB-NOID-001');
- });
- });
-
- group('Roundtrip (fromJson -> toJson -> fromJson)', () {
- test('Job preserves data through serialization', () {
- final original = Job.fromJson(completeJobJson);
- final json = original.toJson();
- final restored = Job.fromJson(json);
-
- expect(restored.id, original.id);
- expect(restored.jobNumber, original.jobNumber);
- expect(restored.status, original.status);
- expect(restored.pickupCompany, original.pickupCompany);
- expect(restored.pickupCity, original.pickupCity);
- expect(restored.deliveryCompany, original.deliveryCompany);
- expect(restored.deliveryCity, original.deliveryCity);
- expect(restored.price, original.price);
- expect(restored.digitalProcessing, original.digitalProcessing);
- });
-
- test('CargoItem preserves data through serialization', () {
- final original = Job.fromJson(completeJobJson).cargoItems[0];
- final json = original.toJson();
- final restored = CargoItem.fromJson(json);
-
- expect(restored.id, original.id);
- expect(restored.jobId, original.jobId);
- expect(restored.description, original.description);
- expect(restored.quantity, original.quantity);
- expect(restored.weightKg, original.weightKg);
- expect(restored.lengthCm, original.lengthCm);
- });
-
- test('ConfirmationTask preserves data through serialization', () {
- final original =
- Job.fromJson(completeJobJson).tasks[0] as ConfirmationTask;
- final json = original.toJson();
- final restored = Task.fromJson(json) as ConfirmationTask;
-
- expect(restored.id, original.id);
- expect(restored.jobId, original.jobId);
- expect(restored.buttonText, original.buttonText);
- expect(restored.taskOrder, original.taskOrder);
- });
-
- test('PhotoTask preserves data through serialization', () {
- final original = Job.fromJson(completeJobJson).tasks[2] as PhotoTask;
- final json = original.toJson();
- final restored = Task.fromJson(json) as PhotoTask;
-
- expect(restored.minPhotoCount, original.minPhotoCount);
- expect(restored.maxPhotoCount, original.maxPhotoCount);
- });
-
- test('TodoListTask preserves data through serialization', () {
- final original = Job.fromJson(completeJobJson).tasks[4] as TodoListTask;
- final json = original.toJson();
- final restored = Task.fromJson(json) as TodoListTask;
-
- expect(restored.todoItems, original.todoItems);
- });
-
- test('CommentTask preserves data through serialization', () {
- final original = Job.fromJson(completeJobJson).tasks[5] as CommentTask;
- final json = original.toJson();
- final restored = Task.fromJson(json) as CommentTask;
-
- expect(restored.commentText, original.commentText);
- expect(restored.required, original.required);
- });
- });
-
- group('Job Status', () {
- test('statusDisplayText returns German text for known statuses', () {
- final statuses = {
- 'CREATED': 'Erstellt',
- 'PENDING': 'Wartend',
- 'ASSIGNED': 'Zugewiesen',
- 'IN_PROGRESS': 'In Bearbeitung',
- 'STARTED': 'In Bearbeitung',
- 'COMPLETED': 'Abgeschlossen',
- 'DONE': 'Abgeschlossen',
- 'CANCELLED': 'Abgebrochen',
- 'FAILED': 'Fehlgeschlagen',
- };
-
- for (final entry in statuses.entries) {
- final json = {
- 'job': {
- 'id': 'status-test',
- 'jobNumber': 'JOB-STATUS',
- 'status': entry.key,
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(json);
- expect(
- job.statusDisplayText,
- entry.value,
- reason: 'Status ${entry.key} should display as ${entry.value}',
- );
- }
- });
-
- test('statusColor returns correct color for statuses', () {
- final statusColors = {
- 'CREATED': 'orange',
- 'ASSIGNED': 'orange',
- 'IN_PROGRESS': 'blue',
- 'COMPLETED': 'green',
- 'CANCELLED': 'red',
- };
-
- for (final entry in statusColors.entries) {
- final json = {
- 'job': {
- 'id': 'color-test',
- 'jobNumber': 'JOB-COLOR',
- 'status': entry.key,
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(json);
- expect(
- job.statusColor,
- entry.value,
- reason: 'Status ${entry.key} should have color ${entry.value}',
- );
- }
- });
- });
-
- group('Job normalized()', () {
- test('trims string fields', () {
- final json = {
- 'job': {
- 'id': 'normalize-test',
- 'jobNumber': ' JOB-TRIM ',
- 'status': ' ASSIGNED ',
- 'pickupCompany': ' Test Company ',
- 'pickupCity': ' Berlin ',
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(json).normalized();
-
- expect(job.jobNumber, 'JOB-TRIM');
- expect(job.status, 'ASSIGNED');
- expect(job.pickupCompany, 'Test Company');
- expect(job.pickupCity, 'Berlin');
- });
-
- test('converts null strings to empty strings', () {
- final json = {
- 'job': {
- 'id': 'null-normalize-test',
- 'jobNumber': 'JOB-NULL',
- 'pickupSalutation': null,
- },
- 'cargoItems': [],
- 'tasks': [],
- };
-
- final job = Job.fromJson(json).normalized();
-
- expect(job.pickupSalutation, '');
- });
- });
-}
diff --git a/app/test/models/message_envelope_test.dart b/app/test/models/message_envelope_test.dart
deleted file mode 100644
index aed4915..0000000
--- a/app/test/models/message_envelope_test.dart
+++ /dev/null
@@ -1,190 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:votianlt_app/models/message_envelope.dart';
-
-void main() {
- group('MessageEnvelope', () {
- group('fromJson', () {
- test('parses all required fields correctly', () {
- final json = {
- 'messageId': 'test-uuid-123',
- 'timestamp': '2024-01-15T10:30:00.000Z',
- 'topic': '/server/user123/message',
- 'payload': {'key': 'value'},
- };
-
- final envelope = MessageEnvelope.fromJson(json);
-
- expect(envelope.messageId, 'test-uuid-123');
- expect(envelope.timestamp, DateTime.utc(2024, 1, 15, 10, 30, 0));
- expect(envelope.topic, '/server/user123/message');
- expect(envelope.payload, {'key': 'value'});
- expect(envelope.requiresAck, true); // default
- expect(envelope.retryCount, 0); // default
- expect(envelope.expiresAt, isNull);
- });
-
- test('parses optional fields when present', () {
- final json = {
- 'messageId': 'test-uuid-456',
- 'timestamp': '2024-01-15T10:30:00.000Z',
- 'topic': '/server/user123/message',
- 'payload': {'data': 123},
- 'requiresAck': false,
- 'retryCount': 3,
- 'expiresAt': '2024-01-15T11:30:00.000Z',
- };
-
- final envelope = MessageEnvelope.fromJson(json);
-
- expect(envelope.requiresAck, false);
- expect(envelope.retryCount, 3);
- expect(envelope.expiresAt, DateTime.utc(2024, 1, 15, 11, 30, 0));
- });
-
- test('handles list payload', () {
- final json = {
- 'messageId': 'test-uuid-789',
- 'timestamp': '2024-01-15T10:30:00.000Z',
- 'topic': '/server/user123/jobs',
- 'payload': [
- {'id': '1'},
- {'id': '2'}
- ],
- };
-
- final envelope = MessageEnvelope.fromJson(json);
-
- expect(envelope.payload, isList);
- expect(envelope.payload.length, 2);
- });
-
- test('handles null payload', () {
- final json = {
- 'messageId': 'test-uuid-null',
- 'timestamp': '2024-01-15T10:30:00.000Z',
- 'topic': '/server/user123/ping',
- 'payload': null,
- };
-
- final envelope = MessageEnvelope.fromJson(json);
-
- expect(envelope.payload, isNull);
- });
- });
-
- group('toJson', () {
- test('serializes all fields correctly', () {
- final envelope = MessageEnvelope(
- messageId: 'test-uuid-123',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'key': 'value'},
- requiresAck: true,
- retryCount: 2,
- expiresAt: DateTime.utc(2024, 1, 15, 11, 30, 0),
- );
-
- final json = envelope.toJson();
-
- expect(json['messageId'], 'test-uuid-123');
- expect(json['timestamp'], '2024-01-15T10:30:00.000Z');
- expect(json['topic'], '/server/user123/message');
- expect(json['payload'], {'key': 'value'});
- expect(json['requiresAck'], true);
- expect(json['retryCount'], 2);
- expect(json['expiresAt'], '2024-01-15T11:30:00.000Z');
- });
-
- test('omits expiresAt when null', () {
- final envelope = MessageEnvelope(
- messageId: 'test-uuid-123',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'key': 'value'},
- );
-
- final json = envelope.toJson();
-
- expect(json.containsKey('expiresAt'), false);
- });
- });
-
- group('fromJson/toJson roundtrip', () {
- test('preserves all data through serialization', () {
- final original = MessageEnvelope(
- messageId: 'roundtrip-uuid',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'nested': {'key': 'value'}, 'list': [1, 2, 3]},
- requiresAck: false,
- retryCount: 5,
- expiresAt: DateTime.utc(2024, 1, 16, 10, 30, 0),
- );
-
- final json = original.toJson();
- final restored = MessageEnvelope.fromJson(json);
-
- expect(restored.messageId, original.messageId);
- expect(restored.timestamp, original.timestamp);
- expect(restored.topic, original.topic);
- expect(restored.payload, original.payload);
- expect(restored.requiresAck, original.requiresAck);
- expect(restored.retryCount, original.retryCount);
- expect(restored.expiresAt, original.expiresAt);
- });
- });
-
- group('copyWith', () {
- test('creates copy with updated messageId', () {
- final original = MessageEnvelope(
- messageId: 'original-id',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'key': 'value'},
- );
-
- final copy = original.copyWith(messageId: 'new-id');
-
- expect(copy.messageId, 'new-id');
- expect(copy.timestamp, original.timestamp);
- expect(copy.topic, original.topic);
- expect(copy.payload, original.payload);
- });
-
- test('creates copy with updated retryCount', () {
- final original = MessageEnvelope(
- messageId: 'test-id',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'key': 'value'},
- retryCount: 0,
- );
-
- final copy = original.copyWith(retryCount: 3);
-
- expect(copy.retryCount, 3);
- expect(copy.messageId, original.messageId);
- });
- });
-
- group('toString', () {
- test('returns readable representation', () {
- final envelope = MessageEnvelope(
- messageId: 'test-id',
- timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
- topic: '/server/user123/message',
- payload: {'key': 'value'},
- requiresAck: true,
- retryCount: 2,
- );
-
- final str = envelope.toString();
-
- expect(str, contains('test-id'));
- expect(str, contains('/server/user123/message'));
- expect(str, contains('requiresAck: true'));
- expect(str, contains('retryCount: 2'));
- });
- });
- });
-}
diff --git a/app/test/services/ack_tracker_test.dart b/app/test/services/ack_tracker_test.dart
deleted file mode 100644
index f9852cd..0000000
--- a/app/test/services/ack_tracker_test.dart
+++ /dev/null
@@ -1,312 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:votianlt_app/services/ack_tracker.dart';
-
-void main() {
- group('AckTracker', () {
- late AckTracker tracker;
- late List