refactor: Projektstruktur in app/ und backend/ aufgeteilt

This commit is contained in:
2026-03-24 15:06:44 +01:00
parent 5f5d5995c5
commit 2673ef658d
449 changed files with 28551 additions and 167 deletions

View File

@@ -0,0 +1,85 @@
/// Acknowledgment message sent by client to confirm message receipt
class AcknowledgmentMessage {
/// ID of the message being acknowledged
final String messageId;
/// Status of the acknowledgment
final AcknowledgmentStatus status;
/// Timestamp when the acknowledgment was created
final DateTime timestamp;
/// Optional error message if status is FAILED
final String? errorMessage;
AcknowledgmentMessage({
required this.messageId,
required this.status,
required this.timestamp,
this.errorMessage,
});
/// Create AcknowledgmentMessage from JSON
factory AcknowledgmentMessage.fromJson(Map<String, dynamic> json) {
return AcknowledgmentMessage(
messageId: json['messageId'] as String,
status: AcknowledgmentStatus.fromString(json['status'] as String),
timestamp: DateTime.parse(json['timestamp'] as String),
errorMessage: json['errorMessage'] as String?,
);
}
/// Convert AcknowledgmentMessage to JSON
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'status': status.toString(),
'timestamp': timestamp.toIso8601String(),
if (errorMessage != null) 'errorMessage': errorMessage,
};
}
@override
String toString() {
return 'AcknowledgmentMessage(messageId: $messageId, status: $status)';
}
}
/// Status of an acknowledgment
enum AcknowledgmentStatus {
/// Message was received by the client
received,
/// Message was processed successfully
processed,
/// Message processing failed
failed;
/// Convert string to AcknowledgmentStatus
static AcknowledgmentStatus fromString(String value) {
switch (value.toUpperCase()) {
case 'RECEIVED':
return AcknowledgmentStatus.received;
case 'PROCESSED':
return AcknowledgmentStatus.processed;
case 'FAILED':
return AcknowledgmentStatus.failed;
default:
return AcknowledgmentStatus.received;
}
}
@override
String toString() {
switch (this) {
case AcknowledgmentStatus.received:
return 'RECEIVED';
case AcknowledgmentStatus.processed:
return 'PROCESSED';
case AcknowledgmentStatus.failed:
return 'FAILED';
}
}
}

View File

@@ -0,0 +1,99 @@
class CargoItem {
final String id; // Will store the timestamp as string
final String jobId; // Will store the timestamp as string
final String description;
final int quantity;
final double weightKg;
final double lengthCm;
final double widthCm;
final double heightCm;
CargoItem({
required this.id,
required this.jobId,
required this.description,
required this.quantity,
required this.weightKg,
required this.lengthCm,
required this.widthCm,
required this.heightCm,
});
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 CargoItem.fromJson(Map<String, dynamic> json) {
// Parse the complex id object - can be either a Map or a simple string
String idValue = '';
if (json['id'] is Map) {
final idMap = json['id'] as Map<String, dynamic>;
idValue = idMap['timestamp']?.toString() ?? '';
} else {
idValue = json['id']?.toString() ?? '';
}
// Parse the complex jobId object - can be either a Map or a simple string
String jobIdValue = '';
if (json['jobId'] is Map) {
final jobIdMap = json['jobId'] as Map<String, dynamic>;
jobIdValue = jobIdMap['timestamp']?.toString() ?? '';
} else {
jobIdValue = json['jobId']?.toString() ?? '';
}
return CargoItem(
id: idValue,
jobId: jobIdValue,
description: _readString(json['description']),
quantity: json['quantity'] is num ? json['quantity'].toInt() : 0,
weightKg: json['weightKg'] is num ? json['weightKg'].toDouble() : 0.0,
lengthCm: json['lengthMm'] is num ? json['lengthMm'].toDouble() : 0.0,
widthCm: json['widthMm'] is num ? json['widthMm'].toDouble() : 0.0,
heightCm: json['heightMm'] is num ? json['heightMm'].toDouble() : 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'description': description,
'quantity': quantity,
'weightKg': weightKg,
'lengthMm': lengthCm,
'widthMm': widthCm,
'heightMm': heightCm,
};
}
@override
String toString() {
return 'CargoItem(id: $id, description: $description, quantity: $quantity)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CargoItem && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// Get formatted dimensions string for display
String get formattedDimensions {
return '${lengthCm.toInt()} × ${widthCm.toInt()} × ${heightCm.toInt()} cm';
}
/// Get formatted weight string for display
String get formattedWeight {
return '${weightKg.toStringAsFixed(1)} kg';
}
}

162
app/lib/models/chat.dart Normal file
View File

@@ -0,0 +1,162 @@
import 'chat_message.dart';
enum ChatType { general, jobSpecific }
class Chat {
final String id;
final String title;
final String? receiver;
final ChatType type;
final String? jobId; // only for job-specific chats
final String? jobNumber; // only for job-specific chats
final List<ChatMessage> messages;
final DateTime lastMessageTime;
final String lastMessagePreview;
Chat({
required this.id,
required this.title,
this.receiver,
required this.type,
this.jobId,
this.jobNumber,
required this.messages,
required this.lastMessageTime,
required this.lastMessagePreview,
});
factory Chat.fromJson(Map<String, dynamic> json) {
final messagesList = json['messages'] as List? ?? [];
final messages =
messagesList
.map(
(messageJson) =>
ChatMessage.fromJson(messageJson as Map<String, dynamic>),
)
.toList();
return Chat(
id: json['id']?.toString() ?? '',
title: json['title']?.toString() ?? '',
receiver: json['receiver']?.toString(),
type:
json['type'] == 'jobSpecific'
? ChatType.jobSpecific
: ChatType.general,
jobId: json['jobId']?.toString(),
jobNumber: json['jobNumber']?.toString(),
messages: messages,
lastMessageTime: _resolveLastMessageTime(json, messages),
lastMessagePreview: _resolveLastMessagePreview(json, messages),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'receiver': receiver,
'type': type == ChatType.jobSpecific ? 'jobSpecific' : 'general',
'jobId': jobId,
'jobNumber': jobNumber,
'messages': messages.map((message) => message.toJson()).toList(),
'lastMessageTime': lastMessageTime.toIso8601String(),
'lastMessagePreview': lastMessagePreview,
};
}
// Factory constructor for general chat
factory Chat.general({
required String id,
required String title,
String? receiver,
required List<ChatMessage> messages,
}) {
final lastMessage = messages.isNotEmpty ? messages.last : null;
return Chat(
id: id,
title: title,
receiver: receiver,
type: ChatType.general,
messages: messages,
lastMessageTime: lastMessage?.createdAt ?? DateTime.now(),
lastMessagePreview:
lastMessage != null
? _previewForMessage(lastMessage)
: 'Noch keine Nachrichten',
);
}
// Factory constructor for job-specific chat
factory Chat.jobSpecific({
required String id,
required String jobId,
required String jobNumber,
String? receiver,
required List<ChatMessage> messages,
}) {
final lastMessage = messages.isNotEmpty ? messages.last : null;
return Chat(
id: id,
title: 'Job $jobNumber',
receiver: receiver,
type: ChatType.jobSpecific,
jobId: jobId,
jobNumber: jobNumber,
messages: messages,
lastMessageTime: lastMessage?.createdAt ?? DateTime.now(),
lastMessagePreview:
lastMessage != null
? _previewForMessage(lastMessage)
: 'Noch keine Nachrichten',
);
}
static DateTime _resolveLastMessageTime(
Map<String, dynamic> json,
List<ChatMessage> messages,
) {
if (messages.isNotEmpty) {
return messages.last.createdAt;
}
final raw = json['lastMessageTime']?.toString();
if (raw != null) {
final parsed = DateTime.tryParse(raw);
if (parsed != null) {
return parsed;
}
}
return DateTime.now();
}
static String _resolveLastMessagePreview(
Map<String, dynamic> json,
List<ChatMessage> messages,
) {
if (messages.isNotEmpty) {
return _previewForMessage(messages.last);
}
return json['lastMessagePreview']?.toString() ?? 'Noch keine Nachrichten';
}
static String _previewForMessage(ChatMessage message) {
if (message.contentType == ChatContentType.image) {
return '[Bild]';
}
return message.content;
}
@override
String toString() {
return 'Chat(id: $id, title: $title, type: $type, jobId: $jobId, messagesCount: ${messages.length})';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Chat && other.id == id;
}
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,211 @@
import 'package:votianlt_app/services/developer.dart' as developer;
enum ChatDirection { incoming, outgoing }
enum ChatMessageType { general, jobRelated }
enum ChatContentType { text, image }
ChatDirection chatDirectionFromString(
String? value, {
ChatDirection fallback = ChatDirection.incoming,
}) {
switch (value?.toUpperCase()) {
case 'CLIENT':
case 'OUTGOING':
return ChatDirection.outgoing;
case 'SERVER':
case 'INCOMING':
return ChatDirection.incoming;
default:
return fallback;
}
}
String chatDirectionToString(ChatDirection direction) {
return direction == ChatDirection.outgoing ? 'CLIENT' : 'SERVER';
}
ChatMessageType chatMessageTypeFromString(
String? value, {
ChatMessageType fallback = ChatMessageType.general,
}) {
switch (value?.toUpperCase()) {
case 'JOB_RELATED':
return ChatMessageType.jobRelated;
case 'GENERAL':
return ChatMessageType.general;
default:
return fallback;
}
}
String chatMessageTypeToString(ChatMessageType type) {
return type == ChatMessageType.jobRelated ? 'JOB_RELATED' : 'GENERAL';
}
ChatContentType chatContentTypeFromString(
String? value, {
ChatContentType fallback = ChatContentType.text,
}) {
switch (value?.toUpperCase()) {
case 'IMAGE':
return ChatContentType.image;
case 'TEXT':
return ChatContentType.text;
default:
return fallback;
}
}
String chatContentTypeToString(ChatContentType type) {
return type == ChatContentType.image ? 'IMAGE' : 'TEXT';
}
class ChatMessage {
final String id;
final String content;
final DateTime createdAt;
final ChatDirection direction;
final ChatMessageType messageType;
final ChatContentType contentType;
final String? jobId;
final String? jobNumber;
final bool read;
final bool pendingSync;
const ChatMessage({
required this.id,
required this.content,
required this.createdAt,
required this.direction,
required this.messageType,
this.contentType = ChatContentType.text,
this.jobId,
this.jobNumber,
this.read = false,
this.pendingSync = false,
});
factory ChatMessage.fromJson(Map<String, dynamic> json) {
final rawId = (json['messageId'] ?? json['id'] ?? '').toString();
final rawContent = (json['content'] ?? json['text'] ?? '').toString();
final rawContentType = json['contentType']?.toString();
DateTime createdAt;
final createdAtRaw = json['createdAt'] ?? json['timestamp'];
if (createdAtRaw is DateTime) {
createdAt = createdAtRaw;
} else {
createdAt =
DateTime.tryParse(createdAtRaw?.toString() ?? '') ?? DateTime.now();
}
var direction = chatDirectionFromString(
json['origin']?.toString() ?? json['direction']?.toString(),
fallback:
json['isOwn'] == true
? ChatDirection.outgoing
: ChatDirection.incoming,
);
var messageType = chatMessageTypeFromString(
json['messageType']?.toString(),
fallback:
((json['jobId']?.toString().isNotEmpty ?? false) ||
(json['jobNumber']?.toString().isNotEmpty ?? false))
? ChatMessageType.jobRelated
: ChatMessageType.general,
);
final jobIdRaw = json['jobId']?.toString();
final jobNumberRaw = json['jobNumber']?.toString();
final jobId = jobIdRaw?.trim();
final jobNumber = jobNumberRaw?.trim();
if (rawId.isEmpty || rawContent.isEmpty) {
developer.log(
'Invalid ChatMessage payload (one or more required fields missing): $json',
);
}
return ChatMessage(
id: rawId,
content: rawContent,
createdAt: createdAt,
direction: direction,
messageType: messageType,
contentType: chatContentTypeFromString(rawContentType),
jobId: jobId?.isEmpty ?? true ? null : jobId,
jobNumber: jobNumber?.isEmpty ?? true ? null : jobNumber,
read: json['read'] == true,
pendingSync: json['pendingSync'] == true,
);
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{
'messageId': id,
'content': content,
'origin': chatDirectionToString(direction),
'messageType': chatMessageTypeToString(messageType),
'contentType': chatContentTypeToString(contentType),
'createdAt': createdAt.toIso8601String(),
'read': read,
};
if (jobId != null && jobId!.isNotEmpty) {
map['jobId'] = jobId;
}
if (jobNumber != null && jobNumber!.isNotEmpty) {
map['jobNumber'] = jobNumber;
}
if (pendingSync) {
map['pendingSync'] = true;
}
return map;
}
bool get isOwn => direction == ChatDirection.outgoing;
ChatMessage copyWith({
String? id,
String? content,
DateTime? createdAt,
ChatDirection? direction,
ChatMessageType? messageType,
ChatContentType? contentType,
String? jobId,
String? jobNumber,
bool? read,
bool? pendingSync,
}) {
return ChatMessage(
id: id ?? this.id,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
direction: direction ?? this.direction,
messageType: messageType ?? this.messageType,
contentType: contentType ?? this.contentType,
jobId: jobId ?? this.jobId,
jobNumber: jobNumber ?? this.jobNumber,
read: read ?? this.read,
pendingSync: pendingSync ?? this.pendingSync,
);
}
@override
String toString() {
return 'ChatMessage(id: $id, direction: $direction, messageType: $messageType, contentType: $contentType, createdAt: $createdAt)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatMessage && other.id == id;
}
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,136 @@
import 'task.dart';
class DeliveryStation {
final int stationOrder;
final String company;
final String? salutation;
final String firstName;
final String lastName;
final String phone;
final String street;
final String houseNumber;
final String addressAddition;
final String zip;
final String city;
final String deliveryDate;
final String deliveryTime;
final List<Task> tasks;
DeliveryStation({
required this.stationOrder,
required this.company,
this.salutation,
required this.firstName,
required this.lastName,
required this.phone,
required this.street,
required this.houseNumber,
required this.addressAddition,
required this.zip,
required this.city,
required this.deliveryDate,
required this.deliveryTime,
required this.tasks,
});
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 DeliveryStation.fromJson(Map<String, dynamic> json) {
final stationOrder =
json['stationOrder'] is num
? (json['stationOrder'] as num).toInt()
: int.tryParse(json['stationOrder']?.toString() ?? '') ?? 0;
final tasks =
(json['tasks'] as List? ?? const []).map((rawTask) {
final taskJson = Map<String, dynamic>.from(rawTask as Map);
taskJson['stationOrder'] ??= stationOrder;
return Task.fromJson(taskJson);
}).toList()
..sort((a, b) => (a.taskOrder ?? 0).compareTo(b.taskOrder ?? 0));
return DeliveryStation(
stationOrder: stationOrder,
company: _readString(json['company']),
salutation: json['salutation']?.toString(),
firstName: _readString(json['firstName']),
lastName: _readString(json['lastName']),
phone: _readString(json['phone']),
street: _readString(json['street']),
houseNumber: _readString(json['houseNumber']),
addressAddition: _readString(json['addressAddition']),
zip: _readString(json['zip']),
city: _readString(json['city']),
deliveryDate: _readString(json['deliveryDate']),
deliveryTime: _readString(json['deliveryTime']),
tasks: tasks,
);
}
Map<String, dynamic> toJson() {
return {
'stationOrder': stationOrder,
'company': company,
'salutation': salutation,
'firstName': firstName,
'lastName': lastName,
'phone': phone,
'street': street,
'houseNumber': houseNumber,
'addressAddition': addressAddition,
'zip': zip,
'city': city,
'deliveryDate': deliveryDate,
'deliveryTime': deliveryTime,
'tasks': tasks.map((task) => task.toJson()).toList(),
};
}
DeliveryStation normalized() {
String t(String? value) => (value ?? '').trim();
return DeliveryStation(
stationOrder: stationOrder,
company: t(company),
salutation: t(salutation),
firstName: t(firstName),
lastName: t(lastName),
phone: t(phone),
street: t(street),
houseNumber: t(houseNumber),
addressAddition: t(addressAddition),
zip: t(zip),
city: t(city),
deliveryDate: t(deliveryDate),
deliveryTime: t(deliveryTime),
tasks: tasks,
);
}
String get displayName {
final name = [
firstName.trim(),
lastName.trim(),
].where((part) => part.isNotEmpty).join(' ');
return name.isNotEmpty ? name : company;
}
String get formattedAddress {
final streetPart = [
street.trim(),
houseNumber.trim(),
].where((part) => part.isNotEmpty).join(' ');
final cityPart = [
zip.trim(),
city.trim(),
].where((part) => part.isNotEmpty).join(' ');
return [streetPart, cityPart].where((part) => part.isNotEmpty).join(', ');
}
}

542
app/lib/models/job.dart Normal file
View File

@@ -0,0 +1,542 @@
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';
}
}
}

View File

@@ -0,0 +1,90 @@
/// Message Envelope for wrapping all WebSocket messages with metadata
///
/// This provides reliable message delivery with acknowledgment mechanism
class MessageEnvelope {
/// Unique message identifier (UUID)
final String messageId;
/// Timestamp when the message was created
final DateTime timestamp;
/// Target topic
final String topic;
/// Original message payload (can be Map or List)
final dynamic payload;
/// Whether this message requires acknowledgment
final bool requiresAck;
/// Number of retry attempts (for tracking)
final int retryCount;
/// Optional expiration timestamp
final DateTime? expiresAt;
MessageEnvelope({
required this.messageId,
required this.timestamp,
required this.topic,
required this.payload,
this.requiresAck = true,
this.retryCount = 0,
this.expiresAt,
});
/// Create MessageEnvelope from JSON
factory MessageEnvelope.fromJson(Map<String, dynamic> json) {
return MessageEnvelope(
messageId: json['messageId'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
topic: json['topic'] as String,
payload: json['payload'],
requiresAck: json['requiresAck'] as bool? ?? true,
retryCount: json['retryCount'] as int? ?? 0,
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'] as String)
: null,
);
}
/// Convert MessageEnvelope to JSON
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'timestamp': timestamp.toIso8601String(),
'topic': topic,
'payload': payload,
'requiresAck': requiresAck,
'retryCount': retryCount,
if (expiresAt != null) 'expiresAt': expiresAt!.toIso8601String(),
};
}
/// Create a copy with updated fields
MessageEnvelope copyWith({
String? messageId,
DateTime? timestamp,
String? topic,
dynamic payload,
bool? requiresAck,
int? retryCount,
DateTime? expiresAt,
}) {
return MessageEnvelope(
messageId: messageId ?? this.messageId,
timestamp: timestamp ?? this.timestamp,
topic: topic ?? this.topic,
payload: payload ?? this.payload,
requiresAck: requiresAck ?? this.requiresAck,
retryCount: retryCount ?? this.retryCount,
expiresAt: expiresAt ?? this.expiresAt,
);
}
@override
String toString() {
return 'MessageEnvelope(messageId: $messageId, topic: $topic, requiresAck: $requiresAck, retryCount: $retryCount)';
}
}

View File

@@ -0,0 +1,51 @@
class QueuedMessage {
final String id;
final String topic;
final Map<String, dynamic> payload;
final DateTime createdAt;
final int retryCount;
QueuedMessage({
required this.id,
required this.topic,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
factory QueuedMessage.fromJson(Map<String, dynamic> json) {
return QueuedMessage(
id: json['id'],
topic: json['topic'],
payload: Map<String, dynamic>.from(json['payload']),
createdAt: DateTime.parse(json['createdAt']),
retryCount: json['retryCount'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'topic': topic,
'payload': payload,
'createdAt': createdAt.toIso8601String(),
'retryCount': retryCount,
};
}
QueuedMessage copyWith({
String? id,
String? topic,
Map<String, dynamic>? payload,
DateTime? createdAt,
int? retryCount,
}) {
return QueuedMessage(
id: id ?? this.id,
topic: topic ?? this.topic,
payload: payload ?? this.payload,
createdAt: createdAt ?? this.createdAt,
retryCount: retryCount ?? this.retryCount,
);
}
}

View File

@@ -0,0 +1,20 @@
/// Represents a translated remark in a specific language
class RemarkTranslation {
final String language;
final String text;
RemarkTranslation({required this.language, required this.text});
factory RemarkTranslation.fromJson(Map<String, dynamic> json) {
return RemarkTranslation(language: json['language']?.toString() ?? '', text: json['text']?.toString() ?? '');
}
Map<String, dynamic> toJson() {
return {'language': language, 'text': text};
}
@override
String toString() {
return 'RemarkTranslation(language: $language, text: $text)';
}
}

198
app/lib/models/task.dart Normal file
View File

@@ -0,0 +1,198 @@
// Import all task types
import 'tasks/generic_task.dart';
import 'tasks/confirmation_task.dart';
import 'tasks/photo_task.dart';
import 'tasks/todolist_task.dart';
import 'tasks/signature_task.dart';
import 'tasks/barcode_task.dart';
import 'tasks/comment_task.dart';
abstract class Task {
final String id;
final String jobId;
final int? stationOrder;
final bool completed;
final bool optional;
final DateTime? completedAt;
final String? completedBy;
final int? taskOrder;
final String? title;
final String? description;
final String? displayName;
Task({
required this.id,
required this.jobId,
this.stationOrder,
this.completed = false,
this.optional = false,
this.completedAt,
this.completedBy,
this.taskOrder,
this.title,
this.description,
this.displayName,
});
factory Task.fromJson(Map<String, dynamic> json) {
// Get task specific data to determine task type
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>?;
final taskType =
(taskSpecificData?['taskType'] ?? json['taskType'])?.toString();
// Create specific task type based on taskType
switch (taskType) {
case 'CONFIRMATION':
return ConfirmationTask.fromJson(json);
case 'PHOTO':
return PhotoTask.fromJson(json);
case 'TODOLIST':
return TodoListTask.fromJson(json);
case 'SIGNATURE':
return SignatureTask.fromJson(json);
case 'BARCODE':
return BarcodeTask.fromJson(json);
case 'COMMENT':
return CommentTask.fromJson(json);
case 'GENERIC':
return GenericTask.fromJson(json);
default:
// Fallback to a generic task if no specific type is found
return GenericTask.fromJson(json);
}
}
Map<String, dynamic> toJson();
Task copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
});
@override
String toString() {
return 'Task(id: $id, jobId: $jobId, stationOrder: $stationOrder, completed: $completed, taskOrder: $taskOrder)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Task && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// 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, microsecond]
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 microsecond = value.length > 6 ? value[6] as int : 0;
return DateTime(year, month, day, hour, minute, second, microsecond);
} catch (e) {
return null;
}
}
return null;
}
static String? _readOptionalString(dynamic value) {
if (value == null) {
return null;
}
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return null;
}
static int? _readOptionalInt(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value);
}
return null;
}
// Helper method to parse common properties
static Map<String, dynamic> parseCommonProperties(Map<String, dynamic> json) {
// Parse the complex id object - can be either a Map or a simple string
String idValue = '';
if (json['id'] is Map) {
final idMap = json['id'] as Map<String, dynamic>;
idValue = idMap['timestamp']?.toString() ?? '';
} else {
idValue = json['id']?.toString() ?? '';
}
// Parse the complex jobId object - can be either a Map or a simple string
String jobIdValue = '';
if (json['jobId'] is Map) {
final jobIdMap = json['jobId'] as Map<String, dynamic>;
jobIdValue = jobIdMap['timestamp']?.toString() ?? '';
} else {
jobIdValue = json['jobId']?.toString() ?? '';
}
// Parse completedAt using the helper method to handle both string and array formats
final completedAt = _parseDateTime(json['completedAt']);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>?;
final stationOrder = _readOptionalInt(json['stationOrder']);
final title = _readOptionalString(
json['title'] ?? taskSpecificData?['title'],
);
final description = _readOptionalString(
json['description'] ?? taskSpecificData?['description'],
);
final displayName = _readOptionalString(
json['displayName'] ?? taskSpecificData?['displayName'],
);
return {
'id': idValue,
'jobId': jobIdValue,
'stationOrder': stationOrder,
'completed': json['completed'] ?? false,
'optional': json['optional'] ?? false,
'completedAt': completedAt,
'completedBy': json['completedBy'],
'taskOrder': json['taskOrder'],
'title': title,
'description': description,
'displayName': displayName,
};
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Barcode Task
class BarcodeTask extends Task {
final int minBarcodeCount;
final int maxBarcodeCount;
BarcodeTask({
required super.id,
required super.jobId,
required this.minBarcodeCount,
required this.maxBarcodeCount,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory BarcodeTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return BarcodeTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
minBarcodeCount: taskSpecificData['minBarcodeCount'] ?? 1,
maxBarcodeCount: taskSpecificData['maxBarcodeCount'] ?? 10,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'BARCODE',
'title': title,
'minBarcodeCount': minBarcodeCount,
'maxBarcodeCount': maxBarcodeCount,
},
};
}
@override
BarcodeTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
int? minBarcodeCount,
int? maxBarcodeCount,
}) {
return BarcodeTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
minBarcodeCount: minBarcodeCount ?? this.minBarcodeCount,
maxBarcodeCount: maxBarcodeCount ?? this.maxBarcodeCount,
);
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Comment Task
class CommentTask extends Task {
final String commentText;
final bool required;
CommentTask({
required super.id,
required super.jobId,
required this.commentText,
this.required = false,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory CommentTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return CommentTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
commentText: taskSpecificData['commentText'] ?? '',
required: taskSpecificData['required'] ?? false,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'COMMENT',
'title': title,
'commentText': commentText,
'required': required,
},
};
}
@override
CommentTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
String? commentText,
bool? required,
}) {
return CommentTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
commentText: commentText ?? this.commentText,
required: required ?? this.required,
);
}
}

View File

@@ -0,0 +1,95 @@
import '../task.dart';
// Confirmation Task
class ConfirmationTask extends Task {
final String buttonText;
ConfirmationTask({
required super.id,
required super.jobId,
required this.buttonText,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory ConfirmationTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
final buttonText =
taskSpecificData['buttonText']?.toString() ?? 'Bestätigen';
return ConfirmationTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
buttonText: buttonText,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'CONFIRMATION',
'title': title,
'buttonText': buttonText,
},
};
}
@override
ConfirmationTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
String? buttonText,
}) {
return ConfirmationTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
buttonText: buttonText ?? this.buttonText,
);
}
}

View File

@@ -0,0 +1,81 @@
import '../task.dart';
// Generic Task implementation for fallback
class GenericTask extends Task {
GenericTask({
required super.id,
required super.jobId,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory GenericTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
return GenericTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {'taskType': 'GENERIC', 'title': title},
};
}
@override
GenericTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
}) {
return GenericTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
);
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Photo Task
class PhotoTask extends Task {
final int minPhotoCount;
final int maxPhotoCount;
PhotoTask({
required super.id,
required super.jobId,
required this.minPhotoCount,
required this.maxPhotoCount,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory PhotoTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return PhotoTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
minPhotoCount: taskSpecificData['minPhotoCount'] ?? 1,
maxPhotoCount: taskSpecificData['maxPhotoCount'] ?? 5,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'PHOTO',
'title': title,
'minPhotoCount': minPhotoCount,
'maxPhotoCount': maxPhotoCount,
},
};
}
@override
PhotoTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
int? minPhotoCount,
int? maxPhotoCount,
}) {
return PhotoTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
minPhotoCount: minPhotoCount ?? this.minPhotoCount,
maxPhotoCount: maxPhotoCount ?? this.maxPhotoCount,
);
}
}

View File

@@ -0,0 +1,82 @@
import '../task.dart';
// Signature Task
class SignatureTask extends Task {
SignatureTask({
required super.id,
required super.jobId,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory SignatureTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
return SignatureTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {'taskType': 'SIGNATURE', 'title': title},
};
}
@override
SignatureTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
}) {
return SignatureTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
);
}
}

View File

@@ -0,0 +1,96 @@
import '../task.dart';
// TodoList Task
class TodoListTask extends Task {
final List<String> todoItems;
TodoListTask({
required super.id,
required super.jobId,
required this.todoItems,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory TodoListTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
final rawItems = taskSpecificData['todoItems'] as List? ?? [];
final todoItems = rawItems.map((item) => item?.toString() ?? '').toList();
return TodoListTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
todoItems: todoItems,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'TODOLIST',
'title': title,
'todoItems': todoItems,
},
};
}
@override
TodoListTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
List<String>? todoItems,
}) {
return TodoListTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
todoItems: todoItems ?? this.todoItems,
);
}
}