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>
426 lines
11 KiB
Markdown
426 lines
11 KiB
Markdown
# 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
|