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:
Claude Code
2025-10-31 04:58:10 -04:00
commit 4b03e9aba5
117 changed files with 7045 additions and 0 deletions
+81
View File
@@ -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,
};
}
+56
View File
@@ -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,
};
}
+59
View File
@@ -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,
};
}
+32
View File
@@ -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,
};
}
+53
View File
@@ -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(),
};
}
+59
View File
@@ -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,
};
}
+32
View File
@@ -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,
};
}
+24
View File
@@ -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)';
}