Initial commit: Plan B Logistics Flutter app with dark mode and responsive design
Implements complete refactor of Ionic Angular logistics app to Flutter/Dart with: - Svrnty dark mode console theme (Material Design 3) - Responsive layouts (mobile, tablet, desktop) following FRONTEND standards - CQRS API integration with Result<T> error handling - OAuth2/OIDC authentication support (mocked for initial testing) - Delivery route and delivery management features - Multi-language support (EN/FR) with i18n - Native integrations (camera, phone calls, maps) - Strict typing throughout codebase - Mock data for UI testing without backend Follows all FRONTEND style guides, design patterns, and conventions. App is running in dark mode and fully responsive across all device sizes. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import '../api/types.dart';
|
||||
import 'delivery_address.dart';
|
||||
import 'delivery_order.dart';
|
||||
import 'user_info.dart';
|
||||
|
||||
class Delivery implements Serializable {
|
||||
final int id;
|
||||
final int routeFragmentId;
|
||||
final int deliveryIndex;
|
||||
final List<DeliveryOrder> orders;
|
||||
final UserInfo? deliveredBy;
|
||||
final DeliveryAddress? deliveryAddress;
|
||||
final String? deliveredAt;
|
||||
final String? skippedAt;
|
||||
final String createdAt;
|
||||
final String? updatedAt;
|
||||
final bool delivered;
|
||||
final bool hasBeenSkipped;
|
||||
final bool isSkipped;
|
||||
final String name;
|
||||
|
||||
const Delivery({
|
||||
required this.id,
|
||||
required this.routeFragmentId,
|
||||
required this.deliveryIndex,
|
||||
required this.orders,
|
||||
this.deliveredBy,
|
||||
this.deliveryAddress,
|
||||
this.deliveredAt,
|
||||
this.skippedAt,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.delivered,
|
||||
required this.hasBeenSkipped,
|
||||
required this.isSkipped,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory Delivery.fromJson(Map<String, dynamic> json) {
|
||||
return Delivery(
|
||||
id: json['id'] as int,
|
||||
routeFragmentId: json['routeFragmentId'] as int,
|
||||
deliveryIndex: json['deliveryIndex'] as int,
|
||||
orders: (json['orders'] as List?)
|
||||
?.map((e) => DeliveryOrder.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
deliveredBy: json['deliveredBy'] != null
|
||||
? UserInfo.fromJson(json['deliveredBy'] as Map<String, dynamic>)
|
||||
: null,
|
||||
deliveryAddress: json['deliveryAddress'] != null
|
||||
? DeliveryAddress.fromJson(json['deliveryAddress'] as Map<String, dynamic>)
|
||||
: null,
|
||||
deliveredAt: json['deliveredAt'] as String?,
|
||||
skippedAt: json['skippedAt'] as String?,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String?,
|
||||
delivered: json['delivered'] as bool,
|
||||
hasBeenSkipped: json['hasBeenSkipped'] as bool,
|
||||
isSkipped: json['isSkipped'] as bool,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
'routeFragmentId': routeFragmentId,
|
||||
'deliveryIndex': deliveryIndex,
|
||||
'orders': orders.map((o) => o.toJson()).toList(),
|
||||
'deliveredBy': deliveredBy?.toJson(),
|
||||
'deliveryAddress': deliveryAddress?.toJson(),
|
||||
'deliveredAt': deliveredAt,
|
||||
'skippedAt': skippedAt,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
'delivered': delivered,
|
||||
'hasBeenSkipped': hasBeenSkipped,
|
||||
'isSkipped': isSkipped,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import '../api/types.dart';
|
||||
|
||||
class DeliveryAddress implements Serializable {
|
||||
final int id;
|
||||
final String line1;
|
||||
final String line2;
|
||||
final String postalCode;
|
||||
final String city;
|
||||
final String subdivision;
|
||||
final String countryCode;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String formattedAddress;
|
||||
|
||||
const DeliveryAddress({
|
||||
required this.id,
|
||||
required this.line1,
|
||||
required this.line2,
|
||||
required this.postalCode,
|
||||
required this.city,
|
||||
required this.subdivision,
|
||||
required this.countryCode,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.formattedAddress,
|
||||
});
|
||||
|
||||
factory DeliveryAddress.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryAddress(
|
||||
id: json['id'] as int,
|
||||
line1: json['line1'] as String,
|
||||
line2: json['line2'] as String,
|
||||
postalCode: json['postalCode'] as String,
|
||||
city: json['city'] as String,
|
||||
subdivision: json['subdivision'] as String,
|
||||
countryCode: json['countryCode'] as String,
|
||||
latitude: json['latitude'] as double?,
|
||||
longitude: json['longitude'] as double?,
|
||||
formattedAddress: json['formattedAddress'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
'line1': line1,
|
||||
'line2': line2,
|
||||
'postalCode': postalCode,
|
||||
'city': city,
|
||||
'subdivision': subdivision,
|
||||
'countryCode': countryCode,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'formattedAddress': formattedAddress,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import '../api/types.dart';
|
||||
|
||||
class CompleteDeliveryCommand implements Serializable {
|
||||
final int deliveryId;
|
||||
final String? deliveredAt;
|
||||
|
||||
const CompleteDeliveryCommand({
|
||||
required this.deliveryId,
|
||||
this.deliveredAt,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'deliveryId': deliveryId,
|
||||
'deliveredAt': deliveredAt,
|
||||
};
|
||||
}
|
||||
|
||||
class MarkDeliveryAsUncompletedCommand implements Serializable {
|
||||
final int deliveryId;
|
||||
|
||||
const MarkDeliveryAsUncompletedCommand({
|
||||
required this.deliveryId,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'deliveryId': deliveryId,
|
||||
};
|
||||
}
|
||||
|
||||
class UploadDeliveryPictureCommand implements Serializable {
|
||||
final int deliveryId;
|
||||
final String filePath;
|
||||
|
||||
const UploadDeliveryPictureCommand({
|
||||
required this.deliveryId,
|
||||
required this.filePath,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'deliveryId': deliveryId,
|
||||
'filePath': filePath,
|
||||
};
|
||||
}
|
||||
|
||||
class SkipDeliveryCommand implements Serializable {
|
||||
final int deliveryId;
|
||||
|
||||
const SkipDeliveryCommand({
|
||||
required this.deliveryId,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'deliveryId': deliveryId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import '../api/types.dart';
|
||||
|
||||
class DeliveryContact implements Serializable {
|
||||
final String firstName;
|
||||
final String? lastName;
|
||||
final String fullName;
|
||||
final String? phoneNumber;
|
||||
|
||||
const DeliveryContact({
|
||||
required this.firstName,
|
||||
this.lastName,
|
||||
required this.fullName,
|
||||
this.phoneNumber,
|
||||
});
|
||||
|
||||
factory DeliveryContact.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryContact(
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String?,
|
||||
fullName: json['fullName'] as String,
|
||||
phoneNumber: json['phoneNumber'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'fullName': fullName,
|
||||
'phoneNumber': phoneNumber,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import '../api/types.dart';
|
||||
import 'delivery_contact.dart';
|
||||
|
||||
class DeliveryOrder implements Serializable {
|
||||
final int id;
|
||||
final bool isNewCustomer;
|
||||
final String? note;
|
||||
final double totalAmount;
|
||||
final double? totalPaid;
|
||||
final int? totalItems;
|
||||
final List<DeliveryContact> contacts;
|
||||
final DeliveryContact? contact;
|
||||
|
||||
const DeliveryOrder({
|
||||
required this.id,
|
||||
required this.isNewCustomer,
|
||||
this.note,
|
||||
required this.totalAmount,
|
||||
this.totalPaid,
|
||||
this.totalItems,
|
||||
required this.contacts,
|
||||
this.contact,
|
||||
});
|
||||
|
||||
factory DeliveryOrder.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryOrder(
|
||||
id: json['id'] as int,
|
||||
isNewCustomer: json['isNewCustomer'] as bool,
|
||||
note: json['note'] as String?,
|
||||
totalAmount: (json['totalAmount'] as num).toDouble(),
|
||||
totalPaid: (json['totalPaid'] as num?)?.toDouble(),
|
||||
totalItems: json['totalItems'] as int?,
|
||||
contacts: (json['contacts'] as List?)
|
||||
?.map((e) => DeliveryContact.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
contact: json['contact'] != null
|
||||
? DeliveryContact.fromJson(json['contact'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
'isNewCustomer': isNewCustomer,
|
||||
'note': note,
|
||||
'totalAmount': totalAmount,
|
||||
'totalPaid': totalPaid,
|
||||
'totalItems': totalItems,
|
||||
'contacts': contacts.map((c) => c.toJson()).toList(),
|
||||
'contact': contact?.toJson(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import '../api/types.dart';
|
||||
|
||||
class DeliveryRoute implements Serializable {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final int routeFragmentId;
|
||||
final int totalDeliveries;
|
||||
final int completedDeliveries;
|
||||
final int skippedDeliveries;
|
||||
final String createdAt;
|
||||
final String? updatedAt;
|
||||
|
||||
const DeliveryRoute({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.routeFragmentId,
|
||||
required this.totalDeliveries,
|
||||
required this.completedDeliveries,
|
||||
required this.skippedDeliveries,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory DeliveryRoute.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryRoute(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
routeFragmentId: json['routeFragmentId'] as int,
|
||||
totalDeliveries: json['totalDeliveries'] as int,
|
||||
completedDeliveries: json['completedDeliveries'] as int,
|
||||
skippedDeliveries: json['skippedDeliveries'] as int,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
double get progress {
|
||||
if (totalDeliveries == 0) return 0.0;
|
||||
return completedDeliveries / totalDeliveries;
|
||||
}
|
||||
|
||||
int get pendingDeliveries => totalDeliveries - completedDeliveries - skippedDeliveries;
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'routeFragmentId': routeFragmentId,
|
||||
'totalDeliveries': totalDeliveries,
|
||||
'completedDeliveries': completedDeliveries,
|
||||
'skippedDeliveries': skippedDeliveries,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import '../api/types.dart';
|
||||
|
||||
class UserInfo implements Serializable {
|
||||
final int id;
|
||||
final String firstName;
|
||||
final String? lastName;
|
||||
final String fullName;
|
||||
|
||||
const UserInfo({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
this.lastName,
|
||||
required this.fullName,
|
||||
});
|
||||
|
||||
factory UserInfo.fromJson(Map<String, dynamic> json) {
|
||||
return UserInfo(
|
||||
id: json['id'] as int,
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String?,
|
||||
fullName: json['fullName'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'id': id,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'fullName': fullName,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
class UserProfile {
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
|
||||
const UserProfile({
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
|
||||
factory UserProfile.fromJwtClaims(Map<String, dynamic> claims) {
|
||||
return UserProfile(
|
||||
firstName: claims['given_name'] as String? ?? '',
|
||||
lastName: claims['family_name'] as String? ?? '',
|
||||
email: claims['email'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'UserProfile(firstName: $firstName, lastName: $lastName, email: $email)';
|
||||
}
|
||||
Reference in New Issue
Block a user