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>
161 lines
3.3 KiB
Dart
161 lines
3.3 KiB
Dart
abstract interface class Serializable {
|
|
Map<String, Object?> toJson();
|
|
}
|
|
|
|
enum ApiErrorType {
|
|
network,
|
|
timeout,
|
|
validation,
|
|
http,
|
|
unknown,
|
|
}
|
|
|
|
class ApiError {
|
|
final ApiErrorType type;
|
|
final String message;
|
|
final int? statusCode;
|
|
final Map<String, List<String>>? details;
|
|
final Exception? originalException;
|
|
|
|
const ApiError({
|
|
required this.type,
|
|
required this.message,
|
|
this.statusCode,
|
|
this.details,
|
|
this.originalException,
|
|
});
|
|
|
|
factory ApiError.network(String message) => ApiError(
|
|
type: ApiErrorType.network,
|
|
message: message,
|
|
);
|
|
|
|
factory ApiError.timeout() => const ApiError(
|
|
type: ApiErrorType.timeout,
|
|
message: 'Request timeout',
|
|
);
|
|
|
|
factory ApiError.validation(String message, Map<String, List<String>>? details) => ApiError(
|
|
type: ApiErrorType.validation,
|
|
message: message,
|
|
details: details,
|
|
);
|
|
|
|
factory ApiError.http({
|
|
required int statusCode,
|
|
required String message,
|
|
}) => ApiError(
|
|
type: ApiErrorType.http,
|
|
message: message,
|
|
statusCode: statusCode,
|
|
);
|
|
|
|
factory ApiError.unknown(String message, {Exception? exception}) => ApiError(
|
|
type: ApiErrorType.unknown,
|
|
message: message,
|
|
originalException: exception,
|
|
);
|
|
}
|
|
|
|
sealed class Result<T> {
|
|
const Result();
|
|
|
|
factory Result.success(T data) => Success<T>(data);
|
|
|
|
factory Result.error(ApiError error) => Error<T>(error);
|
|
|
|
R when<R>({
|
|
required R Function(T data) success,
|
|
required R Function(ApiError error) onError,
|
|
}) {
|
|
return switch (this) {
|
|
Success<T>(:final data) => success(data),
|
|
Error<T>(:final error) => onError(error),
|
|
};
|
|
}
|
|
|
|
R? whenSuccess<R>(R Function(T data) fn) {
|
|
return switch (this) {
|
|
Success<T>(:final data) => fn(data),
|
|
Error<T>() => null,
|
|
};
|
|
}
|
|
|
|
R? whenError<R>(R Function(ApiError error) fn) {
|
|
return switch (this) {
|
|
Success<T>() => null,
|
|
Error<T>(:final error) => fn(error),
|
|
};
|
|
}
|
|
|
|
bool get isSuccess => this is Success<T>;
|
|
bool get isError => this is Error<T>;
|
|
|
|
T? getOrNull() => whenSuccess((data) => data);
|
|
ApiError? getErrorOrNull() => whenError((error) => error);
|
|
}
|
|
|
|
final class Success<T> extends Result<T> {
|
|
final T data;
|
|
|
|
const Success(this.data);
|
|
}
|
|
|
|
final class Error<T> extends Result<T> {
|
|
final ApiError error;
|
|
|
|
const Error(this.error);
|
|
}
|
|
|
|
class PaginatedResult<T> {
|
|
final List<T> items;
|
|
final int page;
|
|
final int pageSize;
|
|
final int totalCount;
|
|
|
|
const PaginatedResult({
|
|
required this.items,
|
|
required this.page,
|
|
required this.pageSize,
|
|
required this.totalCount,
|
|
});
|
|
|
|
int get totalPages => (totalCount / pageSize).ceil();
|
|
bool get hasNextPage => page < totalPages;
|
|
}
|
|
|
|
enum FilterOperator {
|
|
equals('eq'),
|
|
notEquals('neq'),
|
|
greaterThan('gt'),
|
|
greaterThanOrEqual('gte'),
|
|
lessThan('lt'),
|
|
lessThanOrEqual('lte'),
|
|
contains('contains'),
|
|
startsWith('startsWith'),
|
|
endsWith('endsWith'),
|
|
in_('in');
|
|
|
|
final String operator;
|
|
const FilterOperator(this.operator);
|
|
}
|
|
|
|
class FilterCriteria implements Serializable {
|
|
final String field;
|
|
final FilterOperator operator;
|
|
final Object? value;
|
|
|
|
FilterCriteria({
|
|
required this.field,
|
|
required this.operator,
|
|
required this.value,
|
|
});
|
|
|
|
@override
|
|
Map<String, Object?> toJson() => {
|
|
'field': field,
|
|
'operator': operator.operator,
|
|
'value': value,
|
|
};
|
|
}
|