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

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

36
.metadata Normal file
View File

@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: android
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: web
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

425
CLAUDE.md Normal file
View File

@ -0,0 +1,425 @@
# CLAUDE.md - Plan B Logistics Flutter App
This file provides guidance to Claude Code when working with this Flutter/Dart project.
## Project Overview
Plan B Logistics Flutter is a complete refactor of the Ionic Angular delivery management app into Flutter/Dart, maintaining all functionality while applying Svrnty design system (colors, typography, and Material Design 3).
**Key Features:**
- OAuth2/OIDC authentication with Keycloak
- CQRS pattern for API integration with Result<T> error handling
- Delivery route and delivery management
- Photo upload for delivery proof
- i18n support (French/English)
- Native features: Camera, Phone calls, Maps
## Essential Commands
### Setup & Dependencies
```bash
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
```
### Development
```bash
flutter run -d chrome # Web
flutter run -d macos # macOS
flutter run -d ios # iOS simulator
flutter run -d android # Android emulator
```
### Testing & Analysis
```bash
flutter test
flutter analyze
flutter test --coverage
```
### Build
```bash
flutter build web
flutter build ios
flutter build android
```
## Code Architecture
### Core Structure
```
lib/
├── api/ # CQRS API client and types
│ ├── types.dart # Result<T>, Serializable, ApiError
│ ├── client.dart # CqrsApiClient implementation
│ └── openapi_config.dart
├── models/ # Data models (strict typing)
│ ├── delivery.dart
│ ├── delivery_route.dart
│ ├── user_profile.dart
│ └── ...
├── services/ # Business logic
│ └── auth_service.dart
├── providers/ # Riverpod state management
│ └── providers.dart
├── pages/ # Screen widgets
│ ├── login_page.dart
│ ├── routes_page.dart
│ ├── deliveries_page.dart
│ └── settings_page.dart
├── components/ # Reusable UI components
├── l10n/ # i18n translations (*.arb files)
├── utils/ # Utility functions
├── theme.dart # Svrnty theme configuration
└── main.dart # App entry point
```
### Design System (Svrnty)
**Primary Colors:**
- Primary (Crimson): #C44D58
- Secondary (Slate Blue): #475C6C
- Error: #BA1A1A
**Typography:**
- Primary Font: Montserrat (all weights 300-700)
- Monospace Font: IBMPlexMono
- Material Design 3 text styles
**Theme Files:**
- `lib/theme.dart` - Complete Material 3 theme configuration
- Light and dark themes with high-contrast variants
- All colors defined in ColorScheme
## Core Patterns & Standards
### 1. Strict Typing (MANDATORY)
**NO `dynamic`, NO untyped `var`**
```dart
// FORBIDDEN:
var data = fetchData();
dynamic result = api.call();
// REQUIRED:
DeliveryRoute data = fetchData();
Result<DeliveryRoute> result = api.call();
```
### 2. Serializable Interface
All models must implement `Serializable`:
```dart
class Delivery implements Serializable {
final int id;
final String name;
const Delivery({required this.id, required this.name});
factory Delivery.fromJson(Map<String, dynamic> json) {
return Delivery(
id: json['id'] as int,
name: json['name'] as String,
);
}
@override
Map<String, Object?> toJson() => {
'id': id,
'name': name,
};
}
```
### 3. Error Handling with Result<T>
**NEVER use try-catch for API calls. Use Result<T> pattern:**
```dart
final result = await apiClient.executeQuery<DeliveryRoute>(
endpoint: 'deliveryRoutes',
query: GetRoutesQuery(),
fromJson: DeliveryRoute.fromJson,
);
result.when(
success: (route) => showRoute(route),
error: (error) {
switch (error.type) {
case ApiErrorType.network:
showSnackbar('No connection');
case ApiErrorType.timeout:
showSnackbar('Request timeout');
case ApiErrorType.validation:
showValidationErrors(error.details);
case ApiErrorType.http when error.statusCode == 401:
navigateToLogin();
default:
showSnackbar('Error: ${error.message}');
}
},
);
```
### 4. CQRS API Integration
**Query (Read Operations):**
```dart
final result = await client.executeQuery<Delivery>(
endpoint: 'simpleDeliveriesQueryItems',
query: GetDeliveriesQuery(routeFragmentId: 123),
fromJson: Delivery.fromJson,
);
```
**Command (Write Operations):**
```dart
final result = await client.executeCommand(
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(deliveryId: 123),
);
```
### 5. Riverpod State Management
**Providers Pattern:**
```dart
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService();
});
final userProfileProvider = FutureProvider<UserProfile?>((ref) async {
final authService = ref.watch(authServiceProvider);
final token = await authService.getToken();
return token != null ? authService.decodeToken(token) : null;
});
// Usage in widget:
final userProfile = ref.watch(userProfileProvider);
userProfile.when(
data: (profile) => Text(profile?.fullName ?? ''),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error: $error'),
);
```
### 6. Authentication Flow
1. **Login**: `AuthService.login()` triggers OAuth2/OIDC flow with Keycloak
2. **Token Storage**: Secure storage with `flutter_secure_storage`
3. **Token Validation**: Check expiration with `JwtDecoder.isExpired()`
4. **Auto Refresh**: Implement token refresh on 401 responses
5. **Logout**: Clear tokens from secure storage
**Keycloak Configuration:**
- Realm: planb-internal
- Client ID: delivery-mobile-app
- Discovery URL: https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration
- Scopes: openid, profile, offline_access
### 7. No Emojis Rule
**MANDATORY: NO emojis in code, comments, or commit messages**
```dart
// FORBIDDEN:
// Bug fix for delivery issues
void completeDelivery(int id) { ... } // Done
// REQUIRED:
// Bug fix for delivery completion logic
void completeDelivery(int id) { ... }
```
## API Integration
### Base URLs
```dart
const String queryBaseUrl = 'https://api-route.goutezplanb.com/api/query';
const String commandBaseUrl = 'https://api-route.goutezplanb.com/api/command';
```
### Key Endpoints
- Query: `/api/query/simpleDeliveriesQueryItems`
- Query: `/api/query/simpleDeliveryRouteQueryItems`
- Command: `/api/command/completeDelivery`
- Command: `/api/command/markDeliveryAsUncompleted`
- Upload: `/api/delivery/uploadDeliveryPicture`
### Authorization
All requests to API base URL must include Bearer token:
```dart
final authClient = CqrsApiClient(
config: ApiClientConfig(
baseUrl: 'https://api-route.goutezplanb.com',
defaultHeaders: {'Authorization': 'Bearer $token'},
),
);
```
## Internationalization (i18n)
### File Structure
```
lib/l10n/
├── app_en.arb # English translations
└── app_fr.arb # French translations
```
### ARB Format
```json
{
"appTitle": "Plan B Logistics",
"loginButton": "Login with Keycloak",
"deliveryStatus": "Delivery #{id} is {status}",
"@deliveryStatus": {
"placeholders": {
"id": {"type": "int"},
"status": {"type": "String"}
}
}
}
```
### Usage in Code
```dart
AppLocalizations.of(context)!.appTitle
AppLocalizations.of(context)!.deliveryStatus(id: 123, status: 'completed')
```
## Native Features
### Camera Integration
- Package: `image_picker`
- Use: Photo capture for delivery proof
- Platforms: iOS, Android, Web
```dart
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
// Upload to server
}
```
### Phone Calls
- Package: `url_launcher`
- Use: Call customer from delivery details
```dart
final Uri phoneUri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
```
### Maps Integration
- Package: `url_launcher`
- Use: Open maps app to show delivery address
```dart
final Uri mapUri = Uri(
scheme: 'https',
host: 'maps.google.com',
queryParameters: {'q': '${address.latitude},${address.longitude}'},
);
if (await canLaunchUrl(mapUri)) {
await launchUrl(mapUri);
}
```
## File Naming Conventions
- **Files**: snake_case (e.g., `delivery_route.dart`)
- **Classes**: PascalCase (e.g., `DeliveryRoute`)
- **Variables/Functions**: camelCase (e.g., `deliveryId`, `completeDelivery()`)
- **Constants**: camelCase or UPPER_SNAKE_CASE (e.g., `kPrimaryColor` or `MAX_RETRIES`)
- **Private members**: Prefix with underscore (e.g., `_secureStorage`)
## Git Conventions
- **Author**: Svrnty
- **Co-Author**: Jean-Philippe Brule <jp@svrnty.io>
- **Commits**: Clear, concise messages describing the "why"
- **NO emojis in commits**
Example:
```
Implement OAuth2/OIDC authentication with Keycloak
Adds AuthService with flutter_appauth integration, JWT token
management with secure storage, and automatic token refresh.
Co-Authored-By: Claude <noreply@anthropic.com>
```
## Common Development Tasks
### Adding a New Page
1. Create widget file in `lib/pages/[name]_page.dart`
2. Extend `ConsumerWidget` for Riverpod access
3. Use strict typing for all parameters and variables
4. Apply Svrnty colors from theme
5. Handle loading/error states with `.when()`
### Adding a New Data Model
1. Create in `lib/models/[name].dart`
2. Implement `Serializable` interface
3. Add `fromJson` factory constructor
4. Implement `toJson()` method
5. Use explicit types (no `dynamic`)
### Implementing API Call
1. Create Query/Command class implementing `Serializable`
2. Use `CqrsApiClient.executeQuery()` or `.executeCommand()`
3. Handle Result<T> with `.when()` pattern
4. Never use try-catch for API calls
5. Provide proper error messages to user
### Adding i18n Support
1. Add key to `app_en.arb` and `app_fr.arb`
2. Use `AppLocalizations.of(context)!.keyName` in widgets
3. For parameterized strings, define placeholders in ARB
4. Test both English and French text
## Testing
- Unit tests: `test/` directory
- Widget tests: `test/` directory with widget_test suffix
- Use Riverpod's testing utilities for provider testing
- Mock API client for service tests
- Maintain >80% code coverage for business logic
## Deployment Checklist
- [ ] All strict typing rules followed
- [ ] No `dynamic` or untyped `var`
- [ ] All API calls use Result<T> pattern
- [ ] i18n translations complete for both languages
- [ ] Theme colors correctly applied
- [ ] No emojis in code or commits
- [ ] Tests passing (flutter test)
- [ ] Static analysis clean (flutter analyze)
- [ ] No secrets in code (tokens, keys, credentials)
- [ ] APK/IPA builds successful
## Key Dependencies
- `flutter_riverpod`: State management
- `flutter_appauth`: OAuth2/OIDC
- `flutter_secure_storage`: Token storage
- `jwt_decoder`: JWT token parsing
- `http`: HTTP client
- `image_picker`: Camera/photo access
- `url_launcher`: Phone calls and maps
- `animate_do`: Animations (from Svrnty)
- `lottie`: Loading animations
- `iconsax`: Icon set
- `intl`: Internationalization
## Support & Documentation
- **Theme**: See `lib/theme.dart` for complete Svrnty design system
- **API Types**: See `lib/api/types.dart` for Result<T> and error handling
- **Models**: See `lib/models/` for data structure examples
- **Providers**: See `lib/providers/providers.dart` for state management setup
- **Auth**: See `lib/services/auth_service.dart` for OAuth2/OIDC flow

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# Plan B Logistics - Flutter Mobile App
A complete Flutter/Dart refactoring of the Plan B Logistics delivery management system. Built with Material Design 3, Svrnty brand colors, and CQRS architecture for type-safe API integration.
## Overview
This is a mobile delivery management application for logistics personnel to:
- View assigned delivery routes with progress tracking
- Manage individual deliveries (complete, uncomplete, skip)
- Capture photos as delivery proof
- Call customers and navigate to delivery addresses
- Manage app settings and language preferences
- Secure authentication via OAuth2/OIDC with Keycloak
**Built with:**
- Flutter 3.9+ / Dart 3.9.2+
- Material Design 3 with Svrnty theming (Crimson & Slate Blue)
- Riverpod for state management
- CQRS pattern with Result<T> error handling
- Strict typing (no `dynamic`)
## Quick Start
### Prerequisites
- Flutter SDK 3.9.2+: [Install Flutter](https://flutter.dev/docs/get-started/install)
- Dart SDK 3.9.2+ (included with Flutter)
### Setup
```bash
cd ionic-planb-logistic-app-flutter
flutter pub get
```
### Run
```bash
flutter run # Android/iOS default device
flutter run -d chrome # Web
flutter run -d ios # iOS simulator
flutter run -d android # Android emulator
```
## Project Structure
```
lib/
├── api/ # CQRS client & types
├── models/ # Data models
├── services/ # Auth service
├── providers/ # Riverpod state
├── pages/ # Login, Routes, Deliveries, Settings
├── l10n/ # Translations (EN/FR)
├── theme.dart # Svrnty Material Design 3
└── main.dart # Entry point
```
## Key Features
- **OAuth2/OIDC Authentication** with Keycloak
- **CQRS API Integration** with Result<T> error handling
- **Riverpod State Management** for reactive UI
- **Internationalization** (English & French)
- **Material Design 3** with Svrnty brand colors
- **Native Features**: Camera, Phone calls, Maps
- **Strict Typing**: No `dynamic` type allowed
## Development
See **[CLAUDE.md](CLAUDE.md)** for:
- Detailed architecture & patterns
- Code standards & conventions
- API integration examples
- Development workflow
## Build Commands
```bash
flutter build web --release # Web
flutter build ios --release # iOS
flutter build appbundle --release # Android (Play Store)
```
## Documentation
- **CLAUDE.md** - Complete development guidelines
- **pubspec.yaml** - Dependencies and configuration
- **[Flutter Docs](https://flutter.dev/docs)** - Official documentation
## Version
1.0.0+1
---
Svrnty Edition

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.goutezplanb.planb_logistic"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.goutezplanb.planb_logistic"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="planb_logistic"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.goutezplanb.planb_logistic
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Planb Logistic</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>planb_logistic</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

5
l10n.yaml Normal file
View File

@ -0,0 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false

300
lib/api/client.dart Normal file
View File

@ -0,0 +1,300 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'types.dart';
import 'openapi_config.dart';
class CqrsApiClient {
final ApiClientConfig config;
late final http.Client _httpClient;
CqrsApiClient({
required this.config,
http.Client? httpClient,
}) {
_httpClient = httpClient ?? http.Client();
}
String get baseUrl => config.baseUrl;
Future<Result<T>> executeQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) fromJson,
}) async {
try {
final url = Uri.parse('$baseUrl/api/query/$endpoint');
final headers = _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(query.toJson()),
)
.timeout(config.timeout);
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<PaginatedResult<T>>> executePaginatedQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) itemFromJson,
required int page,
required int pageSize,
List<FilterCriteria>? filters,
}) async {
try {
final url = Uri.parse(
'$baseUrl/api/query/$endpoint?page=$page&pageSize=$pageSize',
);
final headers = _buildHeaders();
final queryData = {
...query.toJson(),
if (filters != null && filters.isNotEmpty)
'filters': filters.map((f) => f.toJson()).toList(),
};
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(queryData),
)
.timeout(config.timeout);
return _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute paginated query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<void>> executeCommand({
required String endpoint,
required Serializable command,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<T>> executeCommandWithResult<T>({
required String endpoint,
required Serializable command,
required T Function(Map<String, dynamic>) fromJson,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command with result: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<String>> uploadFile({
required String endpoint,
required String filePath,
required String fieldName,
Map<String, String>? additionalFields,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final request = http.MultipartRequest('POST', url)
..headers.addAll(_buildHeaders())
..files.add(await http.MultipartFile.fromPath(fieldName, filePath));
if (additionalFields != null) {
request.fields.addAll(additionalFields);
}
final response = await request.send().timeout(config.timeout);
final responseBody = await response.stream.bytesToString();
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to upload file: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Map<String, String> _buildHeaders() {
final headers = <String, String>{
'Content-Type': 'application/json',
'Accept': 'application/json',
...config.defaultHeaders,
};
return headers;
}
Result<T> _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.isEmpty) {
return Result.success(null as T);
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
return Result.success(fromJson(data));
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse response: ${e.toString()}'),
);
}
}
Result<PaginatedResult<T>> _handlePaginatedResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) itemFromJson,
int page,
int pageSize,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final items = (data['items'] as List?)
?.map((item) => itemFromJson(item as Map<String, dynamic>))
.toList() ?? [];
final totalCount = data['totalCount'] as int? ?? items.length;
return Result.success(
PaginatedResult(
items: items,
page: page,
pageSize: pageSize,
totalCount: totalCount,
),
);
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse paginated response: ${e.toString()}'),
);
}
}
Result<Never> _handleErrorResponse(http.Response response) {
try {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
if (response.statusCode == 422) {
final errors = data['errors'] as Map<String, dynamic>?;
final details = errors?.map(
(key, value) => MapEntry(
key,
(value as List?)?.map((e) => e.toString()).toList() ?? [],
),
);
return Result.error(
ApiError.validation(message, details),
);
}
return Result.error(
ApiError.http(statusCode: response.statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(
statusCode: response.statusCode,
message: response.reasonPhrase ?? 'Unknown error',
),
);
}
}
Result<Never> _parseErrorFromString(String body, int statusCode) {
try {
final data = jsonDecode(body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
return Result.error(
ApiError.http(statusCode: statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(statusCode: statusCode, message: 'Unknown error'),
);
}
}
void close() {
_httpClient.close();
}
}

View File

@ -0,0 +1,21 @@
class ApiClientConfig {
final String baseUrl;
final Duration timeout;
final Map<String, String> defaultHeaders;
const ApiClientConfig({
required this.baseUrl,
this.timeout = const Duration(seconds: 30),
this.defaultHeaders = const {},
});
static const ApiClientConfig development = ApiClientConfig(
baseUrl: 'https://api-route.goutezplanb.com',
timeout: Duration(seconds: 30),
);
static const ApiClientConfig production = ApiClientConfig(
baseUrl: 'https://api-route.goutezplanb.com',
timeout: Duration(seconds: 30),
);
}

160
lib/api/types.dart Normal file
View File

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

69
lib/l10n/app_en.arb Normal file
View File

@ -0,0 +1,69 @@
{
"@@locale": "en",
"appTitle": "Plan B Logistics",
"appDescription": "Delivery Management System",
"loginWithKeycloak": "Login with Keycloak",
"deliveryRoutes": "Delivery Routes",
"routes": "Routes",
"deliveries": "Deliveries",
"settings": "Settings",
"profile": "Profile",
"logout": "Logout",
"completed": "Completed",
"pending": "Pending",
"todo": "To Do",
"delivered": "Delivered",
"newCustomer": "New Customer",
"items": "{count} items",
"@items": {
"placeholders": {
"count": {"type": "int"}
}
},
"moneyCurrency": "{amount} MAD",
"@moneyCurrency": {
"placeholders": {
"amount": {"type": "double"}
}
},
"call": "Call",
"map": "Map",
"more": "More",
"markAsCompleted": "Mark as Completed",
"markAsUncompleted": "Mark as Uncompleted",
"uploadPhoto": "Upload Photo",
"viewDetails": "View Details",
"deliverySuccessful": "Delivery marked as completed",
"deliveryFailed": "Failed to mark delivery",
"noDeliveries": "No deliveries",
"noRoutes": "No routes available",
"error": "Error: {message}",
"@error": {
"placeholders": {
"message": {"type": "String"}
}
},
"retry": "Retry",
"authenticationRequired": "Authentication required",
"phoneCall": "Call customer",
"navigateToAddress": "Show on map",
"language": "Language",
"english": "English",
"french": "French",
"appVersion": "App Version",
"about": "About",
"fullName": "{firstName} {lastName}",
"@fullName": {
"placeholders": {
"firstName": {"type": "String"},
"lastName": {"type": "String"}
}
},
"completedDeliveries": "{completed}/{total} completed",
"@completedDeliveries": {
"placeholders": {
"completed": {"type": "int"},
"total": {"type": "int"}
}
}
}

69
lib/l10n/app_fr.arb Normal file
View File

@ -0,0 +1,69 @@
{
"@@locale": "fr",
"appTitle": "Plan B Logistique",
"appDescription": "Systme de Gestion des Livraisons",
"loginWithKeycloak": "Connexion avec Keycloak",
"deliveryRoutes": "Itinraires de Livraison",
"routes": "Itinraires",
"deliveries": "Livraisons",
"settings": "Paramtres",
"profile": "Profil",
"logout": "Dconnexion",
"completed": "Livr",
"pending": "En attente",
"todo": "livrer",
"delivered": "Livr",
"newCustomer": "Nouveau Client",
"items": "{count} articles",
"@items": {
"placeholders": {
"count": {"type": "int"}
}
},
"moneyCurrency": "{amount} MAD",
"@moneyCurrency": {
"placeholders": {
"amount": {"type": "double"}
}
},
"call": "Appeler",
"map": "Carte",
"more": "Plus",
"markAsCompleted": "Marquer comme livr",
"markAsUncompleted": "Marquer comme livrer",
"uploadPhoto": "Tlcharger une photo",
"viewDetails": "Voir les dtails",
"deliverySuccessful": "Livraison marque comme complte",
"deliveryFailed": "chec du marquage de la livraison",
"noDeliveries": "Aucune livraison",
"noRoutes": "Aucun itinraire disponible",
"error": "Erreur: {message}",
"@error": {
"placeholders": {
"message": {"type": "String"}
}
},
"retry": "Ressayer",
"authenticationRequired": "Authentification requise",
"phoneCall": "Appeler le client",
"navigateToAddress": "Afficher sur la carte",
"language": "Langue",
"english": "English",
"french": "Franais",
"appVersion": "Version de l'application",
"about": " propos",
"fullName": "{firstName} {lastName}",
"@fullName": {
"placeholders": {
"firstName": {"type": "String"},
"lastName": {"type": "String"}
}
},
"completedDeliveries": "{completed}/{total} livrs",
"@completedDeliveries": {
"placeholders": {
"completed": {"type": "int"},
"total": {"type": "int"}
}
}
}

View File

@ -0,0 +1,368 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_fr.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('fr'),
];
/// No description provided for @appTitle.
///
/// In en, this message translates to:
/// **'Plan B Logistics'**
String get appTitle;
/// No description provided for @appDescription.
///
/// In en, this message translates to:
/// **'Delivery Management System'**
String get appDescription;
/// No description provided for @loginWithKeycloak.
///
/// In en, this message translates to:
/// **'Login with Keycloak'**
String get loginWithKeycloak;
/// No description provided for @deliveryRoutes.
///
/// In en, this message translates to:
/// **'Delivery Routes'**
String get deliveryRoutes;
/// No description provided for @routes.
///
/// In en, this message translates to:
/// **'Routes'**
String get routes;
/// No description provided for @deliveries.
///
/// In en, this message translates to:
/// **'Deliveries'**
String get deliveries;
/// No description provided for @settings.
///
/// In en, this message translates to:
/// **'Settings'**
String get settings;
/// No description provided for @profile.
///
/// In en, this message translates to:
/// **'Profile'**
String get profile;
/// No description provided for @logout.
///
/// In en, this message translates to:
/// **'Logout'**
String get logout;
/// No description provided for @completed.
///
/// In en, this message translates to:
/// **'Completed'**
String get completed;
/// No description provided for @pending.
///
/// In en, this message translates to:
/// **'Pending'**
String get pending;
/// No description provided for @todo.
///
/// In en, this message translates to:
/// **'To Do'**
String get todo;
/// No description provided for @delivered.
///
/// In en, this message translates to:
/// **'Delivered'**
String get delivered;
/// No description provided for @newCustomer.
///
/// In en, this message translates to:
/// **'New Customer'**
String get newCustomer;
/// No description provided for @items.
///
/// In en, this message translates to:
/// **'{count} items'**
String items(int count);
/// No description provided for @moneyCurrency.
///
/// In en, this message translates to:
/// **'{amount} MAD'**
String moneyCurrency(double amount);
/// No description provided for @call.
///
/// In en, this message translates to:
/// **'Call'**
String get call;
/// No description provided for @map.
///
/// In en, this message translates to:
/// **'Map'**
String get map;
/// No description provided for @more.
///
/// In en, this message translates to:
/// **'More'**
String get more;
/// No description provided for @markAsCompleted.
///
/// In en, this message translates to:
/// **'Mark as Completed'**
String get markAsCompleted;
/// No description provided for @markAsUncompleted.
///
/// In en, this message translates to:
/// **'Mark as Uncompleted'**
String get markAsUncompleted;
/// No description provided for @uploadPhoto.
///
/// In en, this message translates to:
/// **'Upload Photo'**
String get uploadPhoto;
/// No description provided for @viewDetails.
///
/// In en, this message translates to:
/// **'View Details'**
String get viewDetails;
/// No description provided for @deliverySuccessful.
///
/// In en, this message translates to:
/// **'Delivery marked as completed'**
String get deliverySuccessful;
/// No description provided for @deliveryFailed.
///
/// In en, this message translates to:
/// **'Failed to mark delivery'**
String get deliveryFailed;
/// No description provided for @noDeliveries.
///
/// In en, this message translates to:
/// **'No deliveries'**
String get noDeliveries;
/// No description provided for @noRoutes.
///
/// In en, this message translates to:
/// **'No routes available'**
String get noRoutes;
/// No description provided for @error.
///
/// In en, this message translates to:
/// **'Error: {message}'**
String error(String message);
/// No description provided for @retry.
///
/// In en, this message translates to:
/// **'Retry'**
String get retry;
/// No description provided for @authenticationRequired.
///
/// In en, this message translates to:
/// **'Authentication required'**
String get authenticationRequired;
/// No description provided for @phoneCall.
///
/// In en, this message translates to:
/// **'Call customer'**
String get phoneCall;
/// No description provided for @navigateToAddress.
///
/// In en, this message translates to:
/// **'Show on map'**
String get navigateToAddress;
/// No description provided for @language.
///
/// In en, this message translates to:
/// **'Language'**
String get language;
/// No description provided for @english.
///
/// In en, this message translates to:
/// **'English'**
String get english;
/// No description provided for @french.
///
/// In en, this message translates to:
/// **'French'**
String get french;
/// No description provided for @appVersion.
///
/// In en, this message translates to:
/// **'App Version'**
String get appVersion;
/// No description provided for @about.
///
/// In en, this message translates to:
/// **'About'**
String get about;
/// No description provided for @fullName.
///
/// In en, this message translates to:
/// **'{firstName} {lastName}'**
String fullName(String firstName, String lastName);
/// No description provided for @completedDeliveries.
///
/// In en, this message translates to:
/// **'{completed}/{total} completed'**
String completedDeliveries(int completed, int total);
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'fr'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'fr':
return AppLocalizationsFr();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@ -0,0 +1,137 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'Plan B Logistics';
@override
String get appDescription => 'Delivery Management System';
@override
String get loginWithKeycloak => 'Login with Keycloak';
@override
String get deliveryRoutes => 'Delivery Routes';
@override
String get routes => 'Routes';
@override
String get deliveries => 'Deliveries';
@override
String get settings => 'Settings';
@override
String get profile => 'Profile';
@override
String get logout => 'Logout';
@override
String get completed => 'Completed';
@override
String get pending => 'Pending';
@override
String get todo => 'To Do';
@override
String get delivered => 'Delivered';
@override
String get newCustomer => 'New Customer';
@override
String items(int count) {
return '$count items';
}
@override
String moneyCurrency(double amount) {
return '$amount MAD';
}
@override
String get call => 'Call';
@override
String get map => 'Map';
@override
String get more => 'More';
@override
String get markAsCompleted => 'Mark as Completed';
@override
String get markAsUncompleted => 'Mark as Uncompleted';
@override
String get uploadPhoto => 'Upload Photo';
@override
String get viewDetails => 'View Details';
@override
String get deliverySuccessful => 'Delivery marked as completed';
@override
String get deliveryFailed => 'Failed to mark delivery';
@override
String get noDeliveries => 'No deliveries';
@override
String get noRoutes => 'No routes available';
@override
String error(String message) {
return 'Error: $message';
}
@override
String get retry => 'Retry';
@override
String get authenticationRequired => 'Authentication required';
@override
String get phoneCall => 'Call customer';
@override
String get navigateToAddress => 'Show on map';
@override
String get language => 'Language';
@override
String get english => 'English';
@override
String get french => 'French';
@override
String get appVersion => 'App Version';
@override
String get about => 'About';
@override
String fullName(String firstName, String lastName) {
return '$firstName $lastName';
}
@override
String completedDeliveries(int completed, int total) {
return '$completed/$total completed';
}
}

View File

@ -0,0 +1,137 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'Plan B Logistique';
@override
String get appDescription => 'Systme de Gestion des Livraisons';
@override
String get loginWithKeycloak => 'Connexion avec Keycloak';
@override
String get deliveryRoutes => 'Itinraires de Livraison';
@override
String get routes => 'Itinraires';
@override
String get deliveries => 'Livraisons';
@override
String get settings => 'Paramtres';
@override
String get profile => 'Profil';
@override
String get logout => 'Dconnexion';
@override
String get completed => 'Livr';
@override
String get pending => 'En attente';
@override
String get todo => 'livrer';
@override
String get delivered => 'Livr';
@override
String get newCustomer => 'Nouveau Client';
@override
String items(int count) {
return '$count articles';
}
@override
String moneyCurrency(double amount) {
return '$amount MAD';
}
@override
String get call => 'Appeler';
@override
String get map => 'Carte';
@override
String get more => 'Plus';
@override
String get markAsCompleted => 'Marquer comme livr';
@override
String get markAsUncompleted => 'Marquer comme livrer';
@override
String get uploadPhoto => 'Tlcharger une photo';
@override
String get viewDetails => 'Voir les dtails';
@override
String get deliverySuccessful => 'Livraison marque comme complte';
@override
String get deliveryFailed => 'chec du marquage de la livraison';
@override
String get noDeliveries => 'Aucune livraison';
@override
String get noRoutes => 'Aucun itinraire disponible';
@override
String error(String message) {
return 'Erreur: $message';
}
@override
String get retry => 'Ressayer';
@override
String get authenticationRequired => 'Authentification requise';
@override
String get phoneCall => 'Appeler le client';
@override
String get navigateToAddress => 'Afficher sur la carte';
@override
String get language => 'Langue';
@override
String get english => 'English';
@override
String get french => 'Franais';
@override
String get appVersion => 'Version de l\'application';
@override
String get about => ' propos';
@override
String fullName(String firstName, String lastName) {
return '$firstName $lastName';
}
@override
String completedDeliveries(int completed, int total) {
return '$completed/$total livrs';
}
}

53
lib/main.dart Normal file
View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'theme.dart';
import 'providers/providers.dart';
import 'pages/login_page.dart';
import 'pages/routes_page.dart';
void main() {
runApp(
const ProviderScope(
child: PlanBLogisticApp(),
),
);
}
class PlanBLogisticApp extends ConsumerWidget {
const PlanBLogisticApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final language = ref.watch(languageProvider);
return MaterialApp(
title: 'Plan B Logistics',
theme: MaterialTheme(const TextTheme()).light(),
darkTheme: MaterialTheme(const TextTheme()).dark(),
themeMode: ThemeMode.dark,
locale: Locale(language),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''),
Locale('fr', ''),
],
home: const AppHome(),
);
}
}
class AppHome extends ConsumerWidget {
const AppHome({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Re-enable authentication when Keycloak is configured
// For now, bypass auth and go directly to RoutesPage
return const RoutesPage();
}
}

81
lib/models/delivery.dart Normal file
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,
};
}

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,
};
}

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,
};
}

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,
};
}

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(),
};
}

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
lib/models/user_info.dart Normal file
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,
};
}

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)';
}

View File

@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/delivery.dart';
import '../providers/providers.dart';
import '../api/client.dart';
import '../api/openapi_config.dart';
import '../models/delivery_commands.dart';
import '../utils/breakpoints.dart';
import '../utils/responsive.dart';
class DeliveriesPage extends ConsumerStatefulWidget {
final int routeFragmentId;
final String routeName;
const DeliveriesPage({
super.key,
required this.routeFragmentId,
required this.routeName,
});
@override
ConsumerState<DeliveriesPage> createState() => _DeliveriesPageState();
}
class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
late PageController _pageController;
int _currentSegment = 0;
@override
void initState() {
super.initState();
_pageController = PageController();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
final token = ref.watch(authTokenProvider).valueOrNull;
return Scaffold(
appBar: AppBar(
title: Text(widget.routeName),
elevation: 0,
),
body: deliveriesData.when(
data: (deliveries) {
final todoDeliveries = deliveries
.where((d) => !d.delivered && !d.isSkipped)
.toList();
final completedDeliveries = deliveries
.where((d) => d.delivered)
.toList();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('To Do'),
),
ButtonSegment(
value: 1,
label: Text('Delivered'),
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
),
);
}
Future<void> _handleDeliveryAction(
BuildContext context,
Delivery delivery,
String action,
String? token,
) async {
if (token == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Authentication required')),
);
return;
}
final authClient = CqrsApiClient(
config: ApiClientConfig(
baseUrl: ApiClientConfig.production.baseUrl,
defaultHeaders: {'Authorization': 'Bearer $token'},
),
);
switch (action) {
case 'complete':
final result = await authClient.executeCommand(
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(
deliveryId: delivery.id,
deliveredAt: DateTime.now().toIso8601String(),
),
);
result.when(
success: (_) {
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Delivery marked as completed')),
);
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
);
break;
case 'uncomplete':
final result = await authClient.executeCommand(
endpoint: 'markDeliveryAsUncompleted',
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
);
result.when(
success: (_) {
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Delivery marked as uncompleted')),
);
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
);
break;
case 'call':
final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null
? delivery.orders.first.contact
: null;
if (contact?.phoneNumber != null) {
final Uri phoneUri = Uri(scheme: 'tel', path: contact!.phoneNumber);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
}
break;
case 'map':
if (delivery.deliveryAddress != null) {
final address = delivery.deliveryAddress!;
final Uri mapUri = Uri(
scheme: 'https',
host: 'maps.google.com',
queryParameters: {
'q': '${address.latitude},${address.longitude}',
},
);
if (await canLaunchUrl(mapUri)) {
await launchUrl(mapUri);
}
}
break;
}
}
}
class DeliveryListView extends StatelessWidget {
final List<Delivery> deliveries;
final Function(Delivery, String) onAction;
const DeliveryListView({
super.key,
required this.deliveries,
required this.onAction,
});
@override
Widget build(BuildContext context) {
if (deliveries.isEmpty) {
return const Center(
child: Text('No deliveries'),
);
}
return RefreshIndicator(
onRefresh: () async {
// Trigger refresh via provider
},
child: ListView.builder(
itemCount: deliveries.length,
itemBuilder: (context, index) {
final delivery = deliveries[index];
return DeliveryCard(
delivery: delivery,
onAction: onAction,
);
},
),
);
}
}
class DeliveryCard extends StatelessWidget {
final Delivery delivery;
final Function(Delivery, String) onAction;
const DeliveryCard({
super.key,
required this.delivery,
required this.onAction,
});
@override
Widget build(BuildContext context) {
final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null
? delivery.orders.first.contact
: null;
final order = delivery.orders.isNotEmpty ? delivery.orders.first : null;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
delivery.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (contact != null)
Text(
contact.fullName,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (delivery.delivered)
Chip(
label: const Text('Delivered'),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
)
else if (order?.isNewCustomer ?? false)
Chip(
label: const Text('New Customer'),
backgroundColor: Colors.orange.shade100,
),
],
),
const SizedBox(height: 12),
if (delivery.deliveryAddress != null)
Text(
delivery.deliveryAddress!.formattedAddress,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (order != null) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (order.totalItems != null)
Text(
'${order.totalItems} items',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
'${order.totalAmount} MAD',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
if (contact?.phoneNumber != null)
OutlinedButton.icon(
onPressed: () => onAction(delivery, 'call'),
icon: const Icon(Icons.phone),
label: const Text('Call'),
),
if (delivery.deliveryAddress != null)
OutlinedButton.icon(
onPressed: () => onAction(delivery, 'map'),
icon: const Icon(Icons.map),
label: const Text('Map'),
),
OutlinedButton.icon(
onPressed: () => _showDeliveryActions(context),
icon: const Icon(Icons.more_vert),
label: const Text('More'),
),
],
),
],
),
),
);
}
void _showDeliveryActions(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!delivery.delivered)
ListTile(
leading: const Icon(Icons.check_circle),
title: const Text('Mark as Completed'),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'complete');
},
)
else
ListTile(
leading: const Icon(Icons.undo),
title: const Text('Mark as Uncompleted'),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'uncomplete');
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Upload Photo'),
onTap: () {
Navigator.pop(context);
// TODO: Implement photo upload
},
),
ListTile(
leading: const Icon(Icons.description),
title: const Text('View Details'),
onTap: () {
Navigator.pop(context);
// TODO: Navigate to delivery details
},
),
],
),
),
);
}
}

58
lib/pages/login_page.dart Normal file
View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/providers.dart';
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Plan B Logistics',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
Text(
'Delivery Management System',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () async {
final authService = ref.read(authServiceProvider);
final result = await authService.login();
result.when(
success: (token) {
if (context.mounted) {
// ignore: unused_result
ref.refresh(isAuthenticatedProvider);
}
},
onError: (error) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login failed: $error')),
);
}
},
cancelled: () {},
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: const Text('Login with Keycloak'),
),
],
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More