543 lines
18 KiB
Dart
543 lines
18 KiB
Dart
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<CargoItem> cargoItems;
|
|
final List<DeliveryStation> deliveryStations;
|
|
final List<Task> 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<String, dynamic>? 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<String, dynamic> json) {
|
|
// Support both flat structure and { job: {...}, cargoItems: [...], tasks: [...] }
|
|
final jobJson =
|
|
(json['job'] is Map)
|
|
? Map<String, dynamic>.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<String, dynamic>) {
|
|
// 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<String, dynamic>) {
|
|
idValue =
|
|
outerId['timestamp']?.toString() ??
|
|
outerId[r'$oid']?.toString() ??
|
|
'';
|
|
} else if (outerId != null) {
|
|
idValue = outerId.toString();
|
|
}
|
|
}
|
|
|
|
// Parse cargoItems array
|
|
List<CargoItem> cargoItems = [];
|
|
if (json['cargoItems'] is List) {
|
|
cargoItems =
|
|
(json['cargoItems'] as List)
|
|
.map(
|
|
(item) =>
|
|
CargoItem.fromJson(Map<String, dynamic>.from(item as Map)),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
// Parse delivery stations and prefer their tasks over the legacy top-level tasks.
|
|
List<DeliveryStation> deliveryStations = [];
|
|
final deliveryStationsRaw =
|
|
jobJson['deliveryStations'] ?? json['deliveryStations'];
|
|
if (deliveryStationsRaw is List) {
|
|
deliveryStations =
|
|
deliveryStationsRaw
|
|
.map(
|
|
(station) => DeliveryStation.fromJson(
|
|
Map<String, dynamic>.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<Task> 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<String, dynamic>.from(task as Map)),
|
|
)
|
|
.toList()
|
|
..sort(compareTasks);
|
|
} else if (jobJson['tasks'] is List) {
|
|
tasks =
|
|
(jobJson['tasks'] as List)
|
|
.map(
|
|
(task) => Task.fromJson(Map<String, dynamic>.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<String, dynamic>?,
|
|
);
|
|
}
|
|
|
|
/// 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<String, dynamic> 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';
|
|
}
|
|
}
|
|
}
|