Files
votianlt/app/lib/models/job.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';
}
}
}