import 'cargo_item.dart'; import 'delivery_station.dart'; import 'task.dart'; class Job { final String id; // Will store the timestamp as string final String jobNumber; final String status; final DateTime createdAt; final DateTime updatedAt; final String createdBy; final String customerSelection; final String pickupCompany; final String? pickupSalutation; final String pickupFirstName; final String pickupLastName; final String pickupPhone; final String pickupStreet; final String pickupHouseNumber; final String pickupAddressAddition; final String pickupZip; final String pickupCity; final String deliveryCompany; final String? deliverySalutation; final String deliveryFirstName; final String deliveryLastName; final String deliveryPhone; final String deliveryStreet; final String deliveryHouseNumber; final String deliveryAddressAddition; final String deliveryZip; final String deliveryCity; final bool digitalProcessing; final String appUser; final String pickupDate; final String pickupTime; final String deliveryDate; final String deliveryTime; final String remark; final double price; final bool draft; // New fields for cargoItems and tasks final List cargoItems; final List deliveryStations; final List tasks; final String deliveryCitiesDisplay; final String firstDeliveryCity; final String lastDeliveryCity; // Legacy fields for backward compatibility final String title; final String description; final String priority; final DateTime? dueDate; final String? assignedTo; final String? location; final Map? additionalData; Job({ required this.id, required this.jobNumber, required this.status, required this.createdAt, required this.updatedAt, required this.createdBy, required this.customerSelection, required this.pickupCompany, this.pickupSalutation, required this.pickupFirstName, required this.pickupLastName, required this.pickupPhone, required this.pickupStreet, required this.pickupHouseNumber, required this.pickupAddressAddition, required this.pickupZip, required this.pickupCity, required this.deliveryCompany, this.deliverySalutation, required this.deliveryFirstName, required this.deliveryLastName, required this.deliveryPhone, required this.deliveryStreet, required this.deliveryHouseNumber, required this.deliveryAddressAddition, required this.deliveryZip, required this.deliveryCity, required this.digitalProcessing, required this.appUser, required this.pickupDate, required this.pickupTime, required this.deliveryDate, required this.deliveryTime, required this.remark, required this.price, required this.draft, // New fields for cargoItems and tasks required this.cargoItems, required this.tasks, this.deliveryStations = const [], this.deliveryCitiesDisplay = '', this.firstDeliveryCity = '', this.lastDeliveryCity = '', // Legacy fields String? title, String? description, this.priority = 'normal', this.dueDate, this.assignedTo, this.location, this.additionalData, }) : title = title ?? 'Job $jobNumber', description = description ?? 'Transport von $pickupCity nach $deliveryCity'; /// Parse DateTime from either string or array format static DateTime? _parseDateTime(dynamic value) { if (value == null) return null; if (value is String) { return DateTime.tryParse(value); } if (value is List && value.isNotEmpty) { try { // Array format: [year, month, day, hour, minute, second, nanosecond] final year = value[0] as int; final month = value.length > 1 ? value[1] as int : 1; final day = value.length > 2 ? value[2] as int : 1; final hour = value.length > 3 ? value[3] as int : 0; final minute = value.length > 4 ? value[4] as int : 0; final second = value.length > 5 ? value[5] as int : 0; final nanosecond = value.length > 6 ? value[6] as int : 0; // Convert nanoseconds to microseconds (divide by 1000) final microsecond = nanosecond ~/ 1000; return DateTime(year, month, day, hour, minute, second, microsecond); } catch (e) { return null; } } return null; } /// Parse time string (for pickupTime/deliveryTime) static String? _parseTimeString(dynamic value) { if (value == null) return null; if (value is String && value.isNotEmpty && value != 'null') { return value; } return null; } /// Parse date string from either string or array format (for pickupDate/deliveryDate) static String? _parseDateString(dynamic value) { if (value == null) return null; if (value is String) { return value; } if (value is List && value.isNotEmpty) { try { // Array format: [year, month, day] final year = value[0] as int; final month = value.length > 1 ? value[1] as int : 1; final day = value.length > 2 ? value[2] as int : 1; // Format as ISO date string return '${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; } catch (e) { return null; } } return value.toString(); } static String _readString(dynamic value, {String fallback = ''}) { if (value is String) { return value; } if (value is num || value is bool) { return value.toString(); } return fallback; } factory Job.fromJson(Map json) { // Support both flat structure and { job: {...}, cargoItems: [...], tasks: [...] } final jobJson = (json['job'] is Map) ? Map.from(json['job'] as Map) : json; // Determine the id robustly. Prefer the inner job.id if present. String idValue = ''; final dynamic innerId = jobJson['id']; if (innerId is Map) { // Some backends send an object; try common fields idValue = innerId['timestamp']?.toString() ?? innerId[r'$oid']?.toString() ?? ''; } else if (innerId != null) { idValue = innerId.toString(); } if (idValue.isEmpty) { // Fallback to outer json['id'] if provided final dynamic outerId = json['id']; if (outerId is Map) { idValue = outerId['timestamp']?.toString() ?? outerId[r'$oid']?.toString() ?? ''; } else if (outerId != null) { idValue = outerId.toString(); } } // Parse cargoItems array List cargoItems = []; if (json['cargoItems'] is List) { cargoItems = (json['cargoItems'] as List) .map( (item) => CargoItem.fromJson(Map.from(item as Map)), ) .toList(); } // Parse delivery stations and prefer their tasks over the legacy top-level tasks. List deliveryStations = []; final deliveryStationsRaw = jobJson['deliveryStations'] ?? json['deliveryStations']; if (deliveryStationsRaw is List) { deliveryStations = deliveryStationsRaw .map( (station) => DeliveryStation.fromJson( Map.from(station as Map), ), ) .toList() ..sort((a, b) => a.stationOrder.compareTo(b.stationOrder)); } int compareTasks(Task a, Task b) { final stationCompare = (a.stationOrder ?? -1).compareTo( b.stationOrder ?? -1, ); if (stationCompare != 0) { return stationCompare; } return (a.taskOrder ?? 0).compareTo(b.taskOrder ?? 0); } // Parse tasks array List tasks = []; if (deliveryStations.isNotEmpty) { tasks = deliveryStations.expand((station) => station.tasks).toList() ..sort(compareTasks); } else if (json['tasks'] is List) { tasks = (json['tasks'] as List) .map( (task) => Task.fromJson(Map.from(task as Map)), ) .toList() ..sort(compareTasks); } else if (jobJson['tasks'] is List) { tasks = (jobJson['tasks'] as List) .map( (task) => Task.fromJson(Map.from(task as Map)), ) .toList() ..sort(compareTasks); } // As a last resort, derive a deterministic id if still empty (avoid UNIQUE '' collisions) if (idValue.isEmpty) { final jobNumber = jobJson['jobNumber']?.toString(); final createdAt = jobJson['createdAt']?.toString(); idValue = (jobNumber?.isNotEmpty == true) ? 'jobnum:$jobNumber' : (createdAt?.isNotEmpty == true ? 'ts:${createdAt!}' : DateTime.now().millisecondsSinceEpoch.toString()); } return Job( id: idValue, jobNumber: jobJson['jobNumber']?.toString() ?? '', status: jobJson['status']?.toString() ?? 'UNKNOWN', createdAt: _parseDateTime(jobJson['createdAt']) ?? DateTime.now(), updatedAt: _parseDateTime(jobJson['updatedAt']) ?? DateTime.now(), createdBy: jobJson['createdBy']?.toString() ?? '', customerSelection: jobJson['customerSelection']?.toString() ?? '', pickupCompany: jobJson['pickupCompany']?.toString() ?? '', pickupSalutation: jobJson['pickupSalutation']?.toString(), pickupFirstName: jobJson['pickupFirstName']?.toString() ?? '', pickupLastName: jobJson['pickupLastName']?.toString() ?? '', pickupPhone: jobJson['pickupPhone']?.toString() ?? '', pickupStreet: jobJson['pickupStreet']?.toString() ?? '', pickupHouseNumber: jobJson['pickupHouseNumber']?.toString() ?? '', pickupAddressAddition: jobJson['pickupAddressAddition']?.toString() ?? '', pickupZip: jobJson['pickupZip']?.toString() ?? '', pickupCity: jobJson['pickupCity']?.toString() ?? '', deliveryCompany: jobJson['deliveryCompany']?.toString() ?? '', deliverySalutation: jobJson['deliverySalutation']?.toString(), deliveryFirstName: jobJson['deliveryFirstName']?.toString() ?? '', deliveryLastName: jobJson['deliveryLastName']?.toString() ?? '', deliveryPhone: jobJson['deliveryPhone']?.toString() ?? '', deliveryStreet: jobJson['deliveryStreet']?.toString() ?? '', deliveryHouseNumber: jobJson['deliveryHouseNumber']?.toString() ?? '', deliveryAddressAddition: jobJson['deliveryAddressAddition']?.toString() ?? '', deliveryZip: jobJson['deliveryZip']?.toString() ?? '', deliveryCity: jobJson['deliveryCity']?.toString() ?? '', digitalProcessing: jobJson['digitalProcessing'] == true, appUser: jobJson['appUser']?.toString() ?? '', pickupDate: _parseDateString(jobJson['pickupDate']) ?? '', pickupTime: _parseTimeString(jobJson['pickupTime']) ?? '', deliveryDate: _parseDateString(jobJson['deliveryDate']) ?? '', deliveryTime: _parseTimeString(jobJson['deliveryTime']) ?? '', remark: _readString(jobJson['remark']), price: (jobJson['price'] is num) ? jobJson['price'].toDouble() : 0.0, draft: jobJson['draft'] == true, // New fields for cargoItems and tasks cargoItems: cargoItems, deliveryStations: deliveryStations, tasks: tasks, deliveryCitiesDisplay: _readString(jobJson['deliveryCitiesDisplay']), firstDeliveryCity: _readString(jobJson['firstDeliveryCity']), lastDeliveryCity: _readString(jobJson['lastDeliveryCity']), // Legacy fields for backward compatibility title: jobJson['title']?.toString(), description: jobJson['description']?.toString(), priority: jobJson['priority']?.toString() ?? 'normal', dueDate: _parseDateTime(jobJson['dueDate']), assignedTo: jobJson['assignedTo']?.toString(), location: jobJson['location']?.toString(), additionalData: jobJson['additionalData'] as Map?, ); } /// Return a normalized copy (trim strings, null→'') Job normalized() { String t(String? s) => (s ?? '').trim(); return Job( id: id, jobNumber: t(jobNumber), status: t(status), createdAt: createdAt, updatedAt: updatedAt, createdBy: t(createdBy), customerSelection: t(customerSelection), pickupCompany: t(pickupCompany), pickupSalutation: t(pickupSalutation), pickupFirstName: t(pickupFirstName), pickupLastName: t(pickupLastName), pickupPhone: t(pickupPhone), pickupStreet: t(pickupStreet), pickupHouseNumber: t(pickupHouseNumber), pickupAddressAddition: t(pickupAddressAddition), pickupZip: t(pickupZip), pickupCity: t(pickupCity), deliveryCompany: t(deliveryCompany), deliverySalutation: t(deliverySalutation), deliveryFirstName: t(deliveryFirstName), deliveryLastName: t(deliveryLastName), deliveryPhone: t(deliveryPhone), deliveryStreet: t(deliveryStreet), deliveryHouseNumber: t(deliveryHouseNumber), deliveryAddressAddition: t(deliveryAddressAddition), deliveryZip: t(deliveryZip), deliveryCity: t(deliveryCity), digitalProcessing: digitalProcessing, appUser: t(appUser), pickupDate: t(pickupDate), pickupTime: t(pickupTime), deliveryDate: t(deliveryDate), deliveryTime: t(deliveryTime), remark: t(remark), price: price, draft: draft, cargoItems: cargoItems, deliveryStations: deliveryStations.map((station) => station.normalized()).toList(), tasks: tasks, deliveryCitiesDisplay: t(deliveryCitiesDisplay), firstDeliveryCity: t(firstDeliveryCity), lastDeliveryCity: t(lastDeliveryCity), title: t(title), description: t(description), priority: t(priority), dueDate: dueDate, assignedTo: t(assignedTo), location: t(location), additionalData: additionalData, ); } Map toJson() { return { 'id': id, 'jobNumber': jobNumber, 'status': status, 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), 'createdBy': createdBy, 'customerSelection': customerSelection, 'pickupCompany': pickupCompany, 'pickupSalutation': pickupSalutation, 'pickupFirstName': pickupFirstName, 'pickupLastName': pickupLastName, 'pickupPhone': pickupPhone, 'pickupStreet': pickupStreet, 'pickupHouseNumber': pickupHouseNumber, 'pickupAddressAddition': pickupAddressAddition, 'pickupZip': pickupZip, 'pickupCity': pickupCity, 'deliveryCompany': deliveryCompany, 'deliverySalutation': deliverySalutation, 'deliveryFirstName': deliveryFirstName, 'deliveryLastName': deliveryLastName, 'deliveryPhone': deliveryPhone, 'deliveryStreet': deliveryStreet, 'deliveryHouseNumber': deliveryHouseNumber, 'deliveryAddressAddition': deliveryAddressAddition, 'deliveryZip': deliveryZip, 'deliveryCity': deliveryCity, 'digitalProcessing': digitalProcessing, 'appUser': appUser, 'pickupDate': pickupDate, 'pickupTime': pickupTime, 'deliveryDate': deliveryDate, 'deliveryTime': deliveryTime, 'remark': remark, 'price': price, 'draft': draft, // New fields for cargoItems and tasks 'cargoItems': cargoItems.map((item) => item.toJson()).toList(), 'deliveryStations': deliveryStations.map((station) => station.toJson()).toList(), 'tasks': tasks.map((task) => task.toJson()).toList(), 'deliveryCitiesDisplay': deliveryCitiesDisplay, 'firstDeliveryCity': firstDeliveryCity, 'lastDeliveryCity': lastDeliveryCity, // Legacy fields 'title': title, 'description': description, 'priority': priority, 'dueDate': dueDate?.toIso8601String(), 'assignedTo': assignedTo, 'location': location, 'additionalData': additionalData, }; } @override String toString() { return 'Job(id: $id, title: $title, status: $status, priority: $priority)'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is Job && other.id == id; } @override int get hashCode => id.hashCode; /// Get status color for UI display String get statusColor { switch (status.toLowerCase()) { case 'created': return 'orange'; case 'pending': case 'assigned': return 'orange'; case 'in_progress': case 'started': return 'blue'; case 'completed': case 'done': return 'green'; case 'cancelled': case 'failed': return 'red'; default: return 'grey'; } } /// Get status display text in German String get statusDisplayText { switch (status.toLowerCase()) { case 'created': return 'Erstellt'; case 'pending': return 'Wartend'; case 'assigned': return 'Zugewiesen'; case 'in_progress': case 'started': return 'In Bearbeitung'; case 'completed': case 'done': return 'Abgeschlossen'; case 'cancelled': return 'Abgebrochen'; case 'failed': return 'Fehlgeschlagen'; default: return status; // Show the original status if not mapped } } /// Get priority display text in German String get priorityDisplayText { switch (priority.toLowerCase()) { case 'low': return 'Niedrig'; case 'normal': return 'Normal'; case 'high': return 'Hoch'; case 'urgent': return 'Dringend'; default: return 'Normal'; } } }