checkpoint
This commit is contained in:
parent
ef5c0c1a95
commit
2ecd1c5b4e
276
DEVELOPMENT.md
Normal file
276
DEVELOPMENT.md
Normal file
@ -0,0 +1,276 @@
|
||||
# Development Guide - Plan B Logistics Flutter App
|
||||
|
||||
This guide covers building and running the Plan B Logistics Flutter app on Android devices (KM10) with local backend communication.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Flutter SDK installed and configured
|
||||
- Android SDK installed (with platform-tools for adb)
|
||||
- Android device (KM10) connected via USB with USB debugging enabled
|
||||
- Backend API running on localhost (Mac)
|
||||
|
||||
## Device Setup
|
||||
|
||||
### 1. Verify Device Connection
|
||||
|
||||
Check that your Android device is connected and recognized:
|
||||
|
||||
```bash
|
||||
flutter devices
|
||||
```
|
||||
|
||||
You should see output similar to:
|
||||
```
|
||||
KM10 (mobile) • 24117ad4 • android-arm64 • Android 13 (API 33)
|
||||
```
|
||||
|
||||
Alternatively, use adb directly:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l
|
||||
```
|
||||
|
||||
### 2. Configure ADB Reverse Proxy
|
||||
|
||||
The app needs to communicate with your local backend API running on `localhost:7182`. Since the Android device cannot access your Mac's localhost directly, you need to set up a reverse proxy using adb.
|
||||
|
||||
#### Get Device Serial Number
|
||||
|
||||
First, identify your device's serial number:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb devices
|
||||
```
|
||||
|
||||
Example output:
|
||||
```
|
||||
24117ad4 device
|
||||
```
|
||||
|
||||
#### Set Up Reverse Proxy
|
||||
|
||||
Forward the device's localhost:7182 to your Mac's localhost:7182:
|
||||
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
|
||||
```
|
||||
|
||||
Replace `24117ad4` with your actual device serial number.
|
||||
|
||||
#### Verify Reverse Proxy
|
||||
|
||||
Check that the reverse proxy is active:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
tcp:7182 tcp:7182
|
||||
```
|
||||
|
||||
#### Test Backend Connectivity (Optional)
|
||||
|
||||
From the device shell, test if the backend is accessible:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell "curl -k https://localhost:7182/api/query/simpleDeliveryRouteQueryItems -X POST -H 'Content-Type: application/json' -d '{}' -m 5 2>&1 | head -10"
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### 2. Run on Connected Device
|
||||
|
||||
Run the app on the KM10 device in debug mode:
|
||||
|
||||
```bash
|
||||
flutter run -d KM10
|
||||
```
|
||||
|
||||
Or using the device serial number:
|
||||
```bash
|
||||
flutter run -d 24117ad4
|
||||
```
|
||||
|
||||
### 3. Hot Reload and Restart
|
||||
|
||||
While the app is running:
|
||||
- **Hot Reload** (r): Reload changed code without restarting
|
||||
- **Hot Restart** (R): Restart the entire app
|
||||
- **Quit** (q): Stop the app
|
||||
|
||||
### 4. Build Release APK
|
||||
|
||||
To build a release APK:
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
The APK will be located at:
|
||||
```
|
||||
build/app/outputs/flutter-apk/app-release.apk
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Full Development Session
|
||||
|
||||
```bash
|
||||
# 1. Check device connection
|
||||
flutter devices
|
||||
|
||||
# 2. Set up ADB reverse proxy (do this once per device connection)
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
|
||||
|
||||
# 3. Verify reverse proxy
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
|
||||
|
||||
# 4. Run the app
|
||||
flutter run -d KM10
|
||||
```
|
||||
|
||||
### If Backend Connection Fails
|
||||
|
||||
If the app cannot connect to the backend API:
|
||||
|
||||
1. Verify backend is running on Mac:
|
||||
```bash
|
||||
curl https://localhost:7182/api/query/simpleDeliveryRouteQueryItems \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
2. Check ADB reverse proxy is active:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list
|
||||
```
|
||||
|
||||
3. Re-establish reverse proxy if needed:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --remove-all
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182
|
||||
```
|
||||
|
||||
4. Check device logs for errors:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100
|
||||
```
|
||||
|
||||
## API Configuration
|
||||
|
||||
The app is configured to use the following API endpoints:
|
||||
|
||||
- **Query Base URL**: `https://localhost:7182/api/query`
|
||||
- **Command Base URL**: `https://localhost:7182/api/command`
|
||||
|
||||
These are configured in `lib/api/openapi_config.dart`.
|
||||
|
||||
## Common Commands Reference
|
||||
|
||||
### Device Management
|
||||
|
||||
```bash
|
||||
# List all connected devices
|
||||
flutter devices
|
||||
|
||||
# List devices with adb
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l
|
||||
|
||||
# Get device info
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell getprop
|
||||
```
|
||||
|
||||
### ADB Reverse Proxy
|
||||
|
||||
```bash
|
||||
# Set up reverse proxy for port 7182
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse tcp:7182 tcp:7182
|
||||
|
||||
# List all reverse proxies
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --list
|
||||
|
||||
# Remove specific reverse proxy
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --remove tcp:7182
|
||||
|
||||
# Remove all reverse proxies
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s <DEVICE_ID> reverse --remove-all
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```bash
|
||||
# View Flutter logs (last 100 lines)
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100
|
||||
|
||||
# View Flutter logs in real-time
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -v time
|
||||
|
||||
# View all logs (last 300 lines)
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d -t 300
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Debug build (default)
|
||||
flutter build apk --debug
|
||||
|
||||
# Release build
|
||||
flutter build apk --release
|
||||
|
||||
# Profile build (for performance testing)
|
||||
flutter build apk --profile
|
||||
|
||||
# Install APK directly
|
||||
flutter install -d KM10
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Device Not Found
|
||||
|
||||
If `flutter devices` doesn't show your device:
|
||||
|
||||
1. Check USB debugging is enabled on the device
|
||||
2. Check device is authorized (check device screen for prompt)
|
||||
3. Restart adb server:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb kill-server
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb start-server
|
||||
```
|
||||
|
||||
### App Crashes on Startup
|
||||
|
||||
1. Check logs:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d | grep -i error
|
||||
```
|
||||
|
||||
2. Clear app data:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell pm clear com.goutezplanb.planb_logistic
|
||||
```
|
||||
|
||||
3. Uninstall and reinstall:
|
||||
```bash
|
||||
/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 uninstall com.goutezplanb.planb_logistic
|
||||
flutter run -d KM10
|
||||
```
|
||||
|
||||
### Backend Connection Issues
|
||||
|
||||
1. Verify reverse proxy is active
|
||||
2. Check backend API is running
|
||||
3. Check SSL certificate issues (app uses `https://localhost:7182`)
|
||||
4. Review network logs in the Flutter output
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Flutter Documentation: https://docs.flutter.dev
|
||||
- Android Debug Bridge (adb): https://developer.android.com/studio/command-line/adb
|
||||
- Project-specific guidelines: See CLAUDE.md for code standards and architecture
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/delivery_route.dart';
|
||||
import '../theme/spacing_system.dart';
|
||||
import '../theme/size_system.dart';
|
||||
@ -72,6 +73,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
|
||||
final isMobile = context.isMobile;
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final isExpanded = ref.watch(collapseStateProvider);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// On mobile, always show as collapsible
|
||||
if (isMobile) {
|
||||
@ -95,7 +97,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
|
||||
children: [
|
||||
if (isExpanded)
|
||||
Text(
|
||||
'Routes',
|
||||
l10n.routes,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@ -146,7 +148,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: AppSpacing.md),
|
||||
child: Text(
|
||||
'Routes',
|
||||
l10n.routes,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import '../models/delivery.dart';
|
||||
import '../theme/animation_system.dart';
|
||||
import '../theme/color_system.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class DeliveryListItem extends StatefulWidget {
|
||||
final Delivery delivery;
|
||||
@ -94,6 +95,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final statusColor = _getStatusColor(widget.delivery);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Collapsed view: Show only the badge
|
||||
if (widget.isCollapsed) {
|
||||
@ -296,7 +298,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
|
||||
Text(
|
||||
widget.delivery.deliveryAddress
|
||||
?.formattedAddress ??
|
||||
'No address',
|
||||
l10n.noAddress,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import '../models/delivery_route.dart';
|
||||
import '../theme/animation_system.dart';
|
||||
import '../theme/color_system.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RouteListItem extends StatefulWidget {
|
||||
final DeliveryRoute route;
|
||||
@ -79,6 +80,7 @@ class _RouteListItemState extends State<RouteListItem>
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final statusColor = _getStatusColor(widget.route);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Collapsed view: Show only the badge
|
||||
if (widget.isCollapsed) {
|
||||
@ -229,7 +231,7 @@ class _RouteListItemState extends State<RouteListItem>
|
||||
const SizedBox(height: 4),
|
||||
// Route details
|
||||
Text(
|
||||
'${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries',
|
||||
l10n.routeDeliveries(widget.route.deliveredCount, widget.route.deliveriesCount),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
|
||||
@ -84,5 +84,30 @@
|
||||
"requestPermission": "Request Permission",
|
||||
"navigationArrived": "You have arrived at the destination",
|
||||
"navigatingTo": "Navigating to",
|
||||
"initializingNavigation": "Initializing navigation..."
|
||||
"initializingNavigation": "Initializing navigation...",
|
||||
"noProfileInfo": "No profile information",
|
||||
"preferences": "Preferences",
|
||||
"systemLanguage": "System",
|
||||
"theme": "Theme",
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"themeSystem": "Auto",
|
||||
"builtWithFlutter": "Built with Flutter",
|
||||
"noAddress": "No address",
|
||||
"routeDeliveries": "{delivered}/{total} deliveries",
|
||||
"@routeDeliveries": {
|
||||
"placeholders": {
|
||||
"delivered": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"username": "Username",
|
||||
"usernameHint": "Enter your username",
|
||||
"usernameRequired": "Username is required",
|
||||
"password": "Password",
|
||||
"passwordHint": "Enter your password",
|
||||
"passwordRequired": "Password is required",
|
||||
"loginButton": "Login",
|
||||
"navigate": "Navigate",
|
||||
"upload": "Upload"
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
{
|
||||
"@@locale": "fr",
|
||||
"appTitle": "Plan B Logistique",
|
||||
"appDescription": "Systme de Gestion des Livraisons",
|
||||
"appDescription": "Système de Gestion des Livraisons",
|
||||
"loginWithKeycloak": "Connexion avec Keycloak",
|
||||
"deliveryRoutes": "Itinraires de Livraison",
|
||||
"routes": "Itinraires",
|
||||
"deliveryRoutes": "Itinéraires de Livraison",
|
||||
"routes": "Itinéraires",
|
||||
"deliveries": "Livraisons",
|
||||
"settings": "Paramtres",
|
||||
"settings": "Paramètres",
|
||||
"profile": "Profil",
|
||||
"logout": "Dconnexion",
|
||||
"completed": "Livr",
|
||||
"logout": "Déconnexion",
|
||||
"completed": "Livré",
|
||||
"pending": "En attente",
|
||||
"todo": "livrer",
|
||||
"delivered": "Livr",
|
||||
"todo": "À livrer",
|
||||
"delivered": "Livré",
|
||||
"newCustomer": "Nouveau Client",
|
||||
"items": "{count} articles",
|
||||
"@items": {
|
||||
@ -29,29 +29,29 @@
|
||||
"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",
|
||||
"markAsCompleted": "Marquer comme livré",
|
||||
"markAsUncompleted": "Marquer comme à livrer",
|
||||
"uploadPhoto": "Télécharger une photo",
|
||||
"viewDetails": "Voir les détails",
|
||||
"deliverySuccessful": "Livraison marquée comme complète",
|
||||
"deliveryFailed": "Échec du marquage de la livraison",
|
||||
"noDeliveries": "Aucune livraison",
|
||||
"noRoutes": "Aucun itinraire disponible",
|
||||
"noRoutes": "Aucun itinéraire disponible",
|
||||
"error": "Erreur: {message}",
|
||||
"@error": {
|
||||
"placeholders": {
|
||||
"message": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"retry": "Ressayer",
|
||||
"retry": "Réessayer",
|
||||
"authenticationRequired": "Authentification requise",
|
||||
"phoneCall": "Appeler le client",
|
||||
"navigateToAddress": "Afficher sur la carte",
|
||||
"language": "Langue",
|
||||
"english": "English",
|
||||
"french": "Franais",
|
||||
"french": "Français",
|
||||
"appVersion": "Version de l'application",
|
||||
"about": " propos",
|
||||
"about": "À propos",
|
||||
"fullName": "{firstName} {lastName}",
|
||||
"@fullName": {
|
||||
"placeholders": {
|
||||
@ -59,7 +59,7 @@
|
||||
"lastName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"completedDeliveries": "{completed}/{total} livrs",
|
||||
"completedDeliveries": "{completed}/{total} livrés",
|
||||
"@completedDeliveries": {
|
||||
"placeholders": {
|
||||
"completed": {"type": "int"},
|
||||
@ -69,20 +69,45 @@
|
||||
"navigationTcTitle": "Service de Navigation",
|
||||
"navigationTcDescription": "Cette application utilise Google Navigation pour fournir une navigation virage par virage pour les livraisons.",
|
||||
"navigationTcAttribution": "Attribution: Services de cartes et de navigation fournis par Google Maps.",
|
||||
"navigationTcTerms": "En acceptant, vous acceptez les conditions d'utilisation et la politique de confidentialit de Google pour les services de navigation.",
|
||||
"navigationTcTerms": "En acceptant, vous acceptez les conditions d'utilisation et la politique de confidentialité de Google pour les services de navigation.",
|
||||
"accept": "Accepter",
|
||||
"decline": "Refuser",
|
||||
"locationPermissionRequired": "Permission de localisation",
|
||||
"locationPermissionMessage": "Cette application ncessite la permission de localisation pour naviguer vers les livraisons.",
|
||||
"locationPermissionDenied": "Permission de localisation refuse. La navigation ne peut pas continuer.",
|
||||
"locationPermissionMessage": "Cette application nécessite la permission de localisation pour naviguer vers les livraisons.",
|
||||
"locationPermissionDenied": "Permission de localisation refusée. La navigation ne peut pas continuer.",
|
||||
"permissionPermanentlyDenied": "Permission requise",
|
||||
"openSettingsMessage": "La permission de localisation est dfinitivement refuse. Veuillez l'activer dans les paramtres de l'application.",
|
||||
"openSettings": "Ouvrir les paramtres",
|
||||
"openSettingsMessage": "La permission de localisation est définitivement refusée. Veuillez l'activer dans les paramètres de l'application.",
|
||||
"openSettings": "Ouvrir les paramètres",
|
||||
"cancel": "Annuler",
|
||||
"ok": "OK",
|
||||
"errorTitle": "Erreur",
|
||||
"requestPermission": "Demander la permission",
|
||||
"navigationArrived": "Vous tes arriv la destination",
|
||||
"navigationArrived": "Vous êtes arrivé à la destination",
|
||||
"navigatingTo": "Navigation vers",
|
||||
"initializingNavigation": "Initialisation de la navigation..."
|
||||
"initializingNavigation": "Initialisation de la navigation...",
|
||||
"noProfileInfo": "Aucune information de profil",
|
||||
"preferences": "Préférences",
|
||||
"systemLanguage": "Système",
|
||||
"theme": "Thème",
|
||||
"themeLight": "Clair",
|
||||
"themeDark": "Sombre",
|
||||
"themeSystem": "Auto",
|
||||
"builtWithFlutter": "Développé avec Flutter",
|
||||
"noAddress": "Aucune adresse",
|
||||
"routeDeliveries": "{delivered}/{total} livraisons",
|
||||
"@routeDeliveries": {
|
||||
"placeholders": {
|
||||
"delivered": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"username": "Nom d'utilisateur",
|
||||
"usernameHint": "Entrez votre nom d'utilisateur",
|
||||
"usernameRequired": "Le nom d'utilisateur est requis",
|
||||
"password": "Mot de passe",
|
||||
"passwordHint": "Entrez votre mot de passe",
|
||||
"passwordRequired": "Le mot de passe est requis",
|
||||
"loginButton": "Connexion",
|
||||
"navigate": "Naviguer",
|
||||
"upload": "Téléverser"
|
||||
}
|
||||
|
||||
@ -445,6 +445,120 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Initializing navigation...'**
|
||||
String get initializingNavigation;
|
||||
|
||||
/// No description provided for @noProfileInfo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No profile information'**
|
||||
String get noProfileInfo;
|
||||
|
||||
/// No description provided for @preferences.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preferences'**
|
||||
String get preferences;
|
||||
|
||||
/// No description provided for @systemLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'System'**
|
||||
String get systemLanguage;
|
||||
|
||||
/// No description provided for @theme.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Theme'**
|
||||
String get theme;
|
||||
|
||||
/// No description provided for @themeLight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Light'**
|
||||
String get themeLight;
|
||||
|
||||
/// No description provided for @themeDark.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Dark'**
|
||||
String get themeDark;
|
||||
|
||||
/// No description provided for @themeSystem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto'**
|
||||
String get themeSystem;
|
||||
|
||||
/// No description provided for @builtWithFlutter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Built with Flutter'**
|
||||
String get builtWithFlutter;
|
||||
|
||||
/// No description provided for @noAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No address'**
|
||||
String get noAddress;
|
||||
|
||||
/// No description provided for @routeDeliveries.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{delivered}/{total} deliveries'**
|
||||
String routeDeliveries(int delivered, int total);
|
||||
|
||||
/// No description provided for @username.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Username'**
|
||||
String get username;
|
||||
|
||||
/// No description provided for @usernameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your username'**
|
||||
String get usernameHint;
|
||||
|
||||
/// No description provided for @usernameRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Username is required'**
|
||||
String get usernameRequired;
|
||||
|
||||
/// No description provided for @password.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password'**
|
||||
String get password;
|
||||
|
||||
/// No description provided for @passwordHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your password'**
|
||||
String get passwordHint;
|
||||
|
||||
/// No description provided for @passwordRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password is required'**
|
||||
String get passwordRequired;
|
||||
|
||||
/// No description provided for @loginButton.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Login'**
|
||||
String get loginButton;
|
||||
|
||||
/// No description provided for @navigate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Navigate'**
|
||||
String get navigate;
|
||||
|
||||
/// No description provided for @upload.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Upload'**
|
||||
String get upload;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@ -197,4 +197,63 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get initializingNavigation => 'Initializing navigation...';
|
||||
|
||||
@override
|
||||
String get noProfileInfo => 'No profile information';
|
||||
|
||||
@override
|
||||
String get preferences => 'Preferences';
|
||||
|
||||
@override
|
||||
String get systemLanguage => 'System';
|
||||
|
||||
@override
|
||||
String get theme => 'Theme';
|
||||
|
||||
@override
|
||||
String get themeLight => 'Light';
|
||||
|
||||
@override
|
||||
String get themeDark => 'Dark';
|
||||
|
||||
@override
|
||||
String get themeSystem => 'Auto';
|
||||
|
||||
@override
|
||||
String get builtWithFlutter => 'Built with Flutter';
|
||||
|
||||
@override
|
||||
String get noAddress => 'No address';
|
||||
|
||||
@override
|
||||
String routeDeliveries(int delivered, int total) {
|
||||
return '$delivered/$total deliveries';
|
||||
}
|
||||
|
||||
@override
|
||||
String get username => 'Username';
|
||||
|
||||
@override
|
||||
String get usernameHint => 'Enter your username';
|
||||
|
||||
@override
|
||||
String get usernameRequired => 'Username is required';
|
||||
|
||||
@override
|
||||
String get password => 'Password';
|
||||
|
||||
@override
|
||||
String get passwordHint => 'Enter your password';
|
||||
|
||||
@override
|
||||
String get passwordRequired => 'Password is required';
|
||||
|
||||
@override
|
||||
String get loginButton => 'Login';
|
||||
|
||||
@override
|
||||
String get navigate => 'Navigate';
|
||||
|
||||
@override
|
||||
String get upload => 'Upload';
|
||||
}
|
||||
|
||||
@ -12,40 +12,40 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get appTitle => 'Plan B Logistique';
|
||||
|
||||
@override
|
||||
String get appDescription => 'Systme de Gestion des Livraisons';
|
||||
String get appDescription => 'Système de Gestion des Livraisons';
|
||||
|
||||
@override
|
||||
String get loginWithKeycloak => 'Connexion avec Keycloak';
|
||||
|
||||
@override
|
||||
String get deliveryRoutes => 'Itinraires de Livraison';
|
||||
String get deliveryRoutes => 'Itinéraires de Livraison';
|
||||
|
||||
@override
|
||||
String get routes => 'Itinraires';
|
||||
String get routes => 'Itinéraires';
|
||||
|
||||
@override
|
||||
String get deliveries => 'Livraisons';
|
||||
|
||||
@override
|
||||
String get settings => 'Paramtres';
|
||||
String get settings => 'Paramètres';
|
||||
|
||||
@override
|
||||
String get profile => 'Profil';
|
||||
|
||||
@override
|
||||
String get logout => 'Dconnexion';
|
||||
String get logout => 'Déconnexion';
|
||||
|
||||
@override
|
||||
String get completed => 'Livr';
|
||||
String get completed => 'Livré';
|
||||
|
||||
@override
|
||||
String get pending => 'En attente';
|
||||
|
||||
@override
|
||||
String get todo => 'livrer';
|
||||
String get todo => 'À livrer';
|
||||
|
||||
@override
|
||||
String get delivered => 'Livr';
|
||||
String get delivered => 'Livré';
|
||||
|
||||
@override
|
||||
String get newCustomer => 'Nouveau Client';
|
||||
@ -70,28 +70,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get more => 'Plus';
|
||||
|
||||
@override
|
||||
String get markAsCompleted => 'Marquer comme livr';
|
||||
String get markAsCompleted => 'Marquer comme livré';
|
||||
|
||||
@override
|
||||
String get markAsUncompleted => 'Marquer comme livrer';
|
||||
String get markAsUncompleted => 'Marquer comme à livrer';
|
||||
|
||||
@override
|
||||
String get uploadPhoto => 'Tlcharger une photo';
|
||||
String get uploadPhoto => 'Télécharger une photo';
|
||||
|
||||
@override
|
||||
String get viewDetails => 'Voir les dtails';
|
||||
String get viewDetails => 'Voir les détails';
|
||||
|
||||
@override
|
||||
String get deliverySuccessful => 'Livraison marque comme complte';
|
||||
String get deliverySuccessful => 'Livraison marquée comme complète';
|
||||
|
||||
@override
|
||||
String get deliveryFailed => 'chec du marquage de la livraison';
|
||||
String get deliveryFailed => 'Échec du marquage de la livraison';
|
||||
|
||||
@override
|
||||
String get noDeliveries => 'Aucune livraison';
|
||||
|
||||
@override
|
||||
String get noRoutes => 'Aucun itinraire disponible';
|
||||
String get noRoutes => 'Aucun itinéraire disponible';
|
||||
|
||||
@override
|
||||
String error(String message) {
|
||||
@ -99,7 +99,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get retry => 'Ressayer';
|
||||
String get retry => 'Réessayer';
|
||||
|
||||
@override
|
||||
String get authenticationRequired => 'Authentification requise';
|
||||
@ -117,13 +117,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get english => 'English';
|
||||
|
||||
@override
|
||||
String get french => 'Franais';
|
||||
String get french => 'Français';
|
||||
|
||||
@override
|
||||
String get appVersion => 'Version de l\'application';
|
||||
|
||||
@override
|
||||
String get about => ' propos';
|
||||
String get about => 'À propos';
|
||||
|
||||
@override
|
||||
String fullName(String firstName, String lastName) {
|
||||
@ -132,7 +132,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String completedDeliveries(int completed, int total) {
|
||||
return '$completed/$total livrs';
|
||||
return '$completed/$total livrés';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -148,7 +148,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get navigationTcTerms =>
|
||||
'En acceptant, vous acceptez les conditions d\'utilisation et la politique de confidentialit de Google pour les services de navigation.';
|
||||
'En acceptant, vous acceptez les conditions d\'utilisation et la politique de confidentialité de Google pour les services de navigation.';
|
||||
|
||||
@override
|
||||
String get accept => 'Accepter';
|
||||
@ -161,21 +161,21 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get locationPermissionMessage =>
|
||||
'Cette application ncessite la permission de localisation pour naviguer vers les livraisons.';
|
||||
'Cette application nécessite la permission de localisation pour naviguer vers les livraisons.';
|
||||
|
||||
@override
|
||||
String get locationPermissionDenied =>
|
||||
'Permission de localisation refuse. La navigation ne peut pas continuer.';
|
||||
'Permission de localisation refusée. La navigation ne peut pas continuer.';
|
||||
|
||||
@override
|
||||
String get permissionPermanentlyDenied => 'Permission requise';
|
||||
|
||||
@override
|
||||
String get openSettingsMessage =>
|
||||
'La permission de localisation est dfinitivement refuse. Veuillez l\'activer dans les paramtres de l\'application.';
|
||||
'La permission de localisation est définitivement refusée. Veuillez l\'activer dans les paramètres de l\'application.';
|
||||
|
||||
@override
|
||||
String get openSettings => 'Ouvrir les paramtres';
|
||||
String get openSettings => 'Ouvrir les paramètres';
|
||||
|
||||
@override
|
||||
String get cancel => 'Annuler';
|
||||
@ -190,11 +190,70 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get requestPermission => 'Demander la permission';
|
||||
|
||||
@override
|
||||
String get navigationArrived => 'Vous tes arriv la destination';
|
||||
String get navigationArrived => 'Vous êtes arrivé à la destination';
|
||||
|
||||
@override
|
||||
String get navigatingTo => 'Navigation vers';
|
||||
|
||||
@override
|
||||
String get initializingNavigation => 'Initialisation de la navigation...';
|
||||
|
||||
@override
|
||||
String get noProfileInfo => 'Aucune information de profil';
|
||||
|
||||
@override
|
||||
String get preferences => 'Préférences';
|
||||
|
||||
@override
|
||||
String get systemLanguage => 'Système';
|
||||
|
||||
@override
|
||||
String get theme => 'Thème';
|
||||
|
||||
@override
|
||||
String get themeLight => 'Clair';
|
||||
|
||||
@override
|
||||
String get themeDark => 'Sombre';
|
||||
|
||||
@override
|
||||
String get themeSystem => 'Auto';
|
||||
|
||||
@override
|
||||
String get builtWithFlutter => 'Développé avec Flutter';
|
||||
|
||||
@override
|
||||
String get noAddress => 'Aucune adresse';
|
||||
|
||||
@override
|
||||
String routeDeliveries(int delivered, int total) {
|
||||
return '$delivered/$total livraisons';
|
||||
}
|
||||
|
||||
@override
|
||||
String get username => 'Nom d\'utilisateur';
|
||||
|
||||
@override
|
||||
String get usernameHint => 'Entrez votre nom d\'utilisateur';
|
||||
|
||||
@override
|
||||
String get usernameRequired => 'Le nom d\'utilisateur est requis';
|
||||
|
||||
@override
|
||||
String get password => 'Mot de passe';
|
||||
|
||||
@override
|
||||
String get passwordHint => 'Entrez votre mot de passe';
|
||||
|
||||
@override
|
||||
String get passwordRequired => 'Le mot de passe est requis';
|
||||
|
||||
@override
|
||||
String get loginButton => 'Connexion';
|
||||
|
||||
@override
|
||||
String get navigate => 'Naviguer';
|
||||
|
||||
@override
|
||||
String get upload => 'Téléverser';
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'theme.dart';
|
||||
import 'providers/providers.dart';
|
||||
import 'pages/login_page.dart';
|
||||
@ -27,25 +28,66 @@ class PlanBLogisticApp extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final language = ref.watch(languageProvider);
|
||||
final languageAsync = ref.watch(languageProvider);
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Plan B Logistics',
|
||||
theme: MaterialTheme(const TextTheme()).light(),
|
||||
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
||||
themeMode: themeMode,
|
||||
locale: Locale(language),
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en', 'CA'),
|
||||
Locale('fr', 'CA'),
|
||||
],
|
||||
home: const AppHome(),
|
||||
return languageAsync.when(
|
||||
data: (language) {
|
||||
Locale? locale;
|
||||
if (language == 'en') {
|
||||
locale = const Locale('en');
|
||||
} else if (language == 'fr') {
|
||||
locale = const Locale('fr');
|
||||
}
|
||||
// If language is 'system', locale remains null and will use device locale
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Plan B Logistics',
|
||||
theme: MaterialTheme(const TextTheme()).light(),
|
||||
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
||||
themeMode: themeMode,
|
||||
locale: locale,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('fr'),
|
||||
],
|
||||
home: const AppHome(),
|
||||
);
|
||||
},
|
||||
loading: () => MaterialApp(
|
||||
title: 'Plan B Logistics',
|
||||
theme: MaterialTheme(const TextTheme()).light(),
|
||||
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
||||
themeMode: themeMode,
|
||||
home: const Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (_, __) => MaterialApp(
|
||||
title: 'Plan B Logistics',
|
||||
theme: MaterialTheme(const TextTheme()).light(),
|
||||
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
||||
themeMode: themeMode,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('fr'),
|
||||
],
|
||||
home: const AppHome(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@ -85,6 +86,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
||||
final tokenAsync = ref.watch(authTokenProvider);
|
||||
final token = tokenAsync.hasValue ? tokenAsync.value : null;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// When embedded in sidebar, show only the delivery list with back button
|
||||
// This is a responsive sidebar that collapses like routes
|
||||
@ -183,7 +185,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Text('Error: $error'),
|
||||
child: Text(l10n.error(error.toString())),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -277,7 +279,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Text('Error: $error'),
|
||||
child: Text(l10n.error(error.toString())),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -291,7 +293,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
String? token,
|
||||
) async {
|
||||
if (token == null) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -312,12 +315,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
);
|
||||
result.when(
|
||||
success: (_) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
// ignore: unused_result
|
||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as completed');
|
||||
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
||||
},
|
||||
onError: (error) {
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.error(error.message));
|
||||
},
|
||||
);
|
||||
break;
|
||||
@ -329,12 +334,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
);
|
||||
result.when(
|
||||
success: (_) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
// ignore: unused_result
|
||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
||||
},
|
||||
onError: (error) {
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.error(error.message));
|
||||
},
|
||||
);
|
||||
break;
|
||||
@ -368,7 +375,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
String? token,
|
||||
) async {
|
||||
if (token == null) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -416,11 +424,11 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Upload'),
|
||||
child: Text(AppLocalizations.of(context)!.upload),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -511,9 +519,10 @@ class UnifiedDeliveryListView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (deliveries.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No deliveries'),
|
||||
return Center(
|
||||
child: Text(l10n.noDeliveries),
|
||||
);
|
||||
}
|
||||
|
||||
@ -599,12 +608,12 @@ class DeliveryCard extends StatelessWidget {
|
||||
),
|
||||
if (delivery.delivered)
|
||||
Chip(
|
||||
label: const Text('Delivered'),
|
||||
label: Text(AppLocalizations.of(context)!.delivered),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
)
|
||||
else if (order?.isNewCustomer ?? false)
|
||||
Chip(
|
||||
label: const Text('New Customer'),
|
||||
label: Text(AppLocalizations.of(context)!.newCustomer),
|
||||
backgroundColor: const Color(0xFFFFFBEB),
|
||||
),
|
||||
],
|
||||
@ -624,11 +633,11 @@ class DeliveryCard extends StatelessWidget {
|
||||
children: [
|
||||
if (order.totalItems != null)
|
||||
Text(
|
||||
'${order.totalItems} items',
|
||||
AppLocalizations.of(context)!.items(order.totalItems!),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'${order.totalAmount} MAD',
|
||||
AppLocalizations.of(context)!.moneyCurrency(order.totalAmount),
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
@ -644,7 +653,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => onAction(delivery, 'call'),
|
||||
icon: const Icon(Icons.phone),
|
||||
label: const Text('Call'),
|
||||
label: Text(AppLocalizations.of(context)!.call),
|
||||
),
|
||||
if (delivery.deliveryAddress != null)
|
||||
OutlinedButton.icon(
|
||||
@ -653,12 +662,12 @@ class DeliveryCard extends StatelessWidget {
|
||||
onAction(delivery, 'map');
|
||||
},
|
||||
icon: const Icon(Icons.map),
|
||||
label: const Text('Navigate'),
|
||||
label: Text(AppLocalizations.of(context)!.navigate),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showDeliveryActions(context),
|
||||
icon: const Icon(Icons.more_vert),
|
||||
label: const Text('More'),
|
||||
label: Text(AppLocalizations.of(context)!.more),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -670,6 +679,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _showDeliveryActions(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
@ -679,7 +689,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
if (!delivery.delivered)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.check_circle),
|
||||
title: const Text('Mark as Completed'),
|
||||
title: Text(l10n.markAsCompleted),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onAction(delivery, 'complete');
|
||||
@ -688,7 +698,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
else
|
||||
ListTile(
|
||||
leading: const Icon(Icons.undo),
|
||||
title: const Text('Mark as Uncompleted'),
|
||||
title: Text(l10n.markAsUncompleted),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onAction(delivery, 'uncomplete');
|
||||
@ -696,7 +706,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Upload Photo'),
|
||||
title: Text(l10n.uploadPhoto),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implement photo upload
|
||||
@ -704,7 +714,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.description),
|
||||
title: const Text('View Details'),
|
||||
title: Text(l10n.viewDetails),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Navigate to delivery details
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
|
||||
@ -78,7 +79,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Plan B Logistics',
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@ -87,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Delivery Management System',
|
||||
AppLocalizations.of(context)!.appDescription,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@ -96,17 +97,17 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
const SizedBox(height: 48),
|
||||
TextFormField(
|
||||
controller: _usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Username',
|
||||
hintText: 'Enter your username',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.username,
|
||||
hintText: AppLocalizations.of(context)!.usernameHint,
|
||||
prefixIcon: const Icon(Icons.person),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
enabled: !_isLoading,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter your username';
|
||||
return AppLocalizations.of(context)!.usernameRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@ -115,8 +116,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
labelText: AppLocalizations.of(context)!.password,
|
||||
hintText: AppLocalizations.of(context)!.passwordHint,
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@ -136,7 +137,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
onFieldSubmitted: (_) => _handleLogin(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
return AppLocalizations.of(context)!.passwordRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@ -158,7 +159,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text('Login'),
|
||||
: Text(AppLocalizations.of(context)!.loginButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/delivery.dart';
|
||||
@ -86,7 +87,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
final token = await authService.ensureValidToken();
|
||||
if (token == null) {
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -173,7 +175,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as completed');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -185,7 +188,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
|
||||
debugPrint('Error details: ${error.details}');
|
||||
if (mounted) {
|
||||
String errorMessage = 'Error: ${error.message}';
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
String errorMessage = l10n.error(error.message);
|
||||
if (error.statusCode == 500) {
|
||||
errorMessage = 'Server error - Please contact support';
|
||||
}
|
||||
@ -251,6 +255,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
||||
}
|
||||
}
|
||||
@ -261,7 +266,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.error(error.message));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -286,7 +292,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
final token = await authService.ensureValidToken();
|
||||
if (token == null) {
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -507,10 +514,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
final routesData = ref.watch(deliveryRoutesProvider);
|
||||
final allDeliveriesData = ref.watch(allDeliveriesProvider);
|
||||
final userProfile = ref.watch(userProfileProvider);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Delivery Routes'),
|
||||
title: Text(l10n.deliveryRoutes),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
@ -580,8 +588,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
body: routesData.when(
|
||||
data: (routes) {
|
||||
if (routes.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No routes available'),
|
||||
return Center(
|
||||
child: Text(l10n.noRoutes),
|
||||
);
|
||||
}
|
||||
return allDeliveriesData.when(
|
||||
@ -642,11 +650,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Error loading deliveries: $error'),
|
||||
Text(l10n.error(error.toString())),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(allDeliveriesProvider),
|
||||
child: const Text('Retry'),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -660,11 +668,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Error: $error'),
|
||||
Text(l10n.error(error.toString())),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(deliveryRoutesProvider),
|
||||
child: const Text('Retry'),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/providers.dart';
|
||||
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
@ -8,12 +9,18 @@ class SettingsPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userProfile = ref.watch(userProfileProvider);
|
||||
final language = ref.watch(languageProvider);
|
||||
final languageAsync = ref.watch(languageProvider);
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final language = languageAsync.maybeWhen(
|
||||
data: (value) => value,
|
||||
orElse: () => 'system',
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
title: Text(l10n.settings),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
@ -23,14 +30,14 @@ class SettingsPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Profile',
|
||||
l10n.profile,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
userProfile.when(
|
||||
data: (profile) {
|
||||
if (profile == null) {
|
||||
return const Text('No profile information');
|
||||
return Text(l10n.noProfileInfo);
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
@ -78,7 +85,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
tooltip: 'Logout',
|
||||
tooltip: l10n.logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -88,7 +95,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, stackTrace) => Text('Error: $error'),
|
||||
error: (error, stackTrace) => Text(l10n.error(error.toString())),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -100,18 +107,18 @@ class SettingsPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Preferences',
|
||||
l10n.preferences,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: const Text('Language'),
|
||||
title: Text(l10n.language),
|
||||
subtitle: Text(
|
||||
language == 'system'
|
||||
? 'System'
|
||||
? l10n.systemLanguage
|
||||
: language == 'fr'
|
||||
? 'Français'
|
||||
: 'English'
|
||||
? l10n.french
|
||||
: l10n.english
|
||||
),
|
||||
trailing: DropdownButton<String>(
|
||||
value: language,
|
||||
@ -120,18 +127,18 @@ class SettingsPage extends ConsumerWidget {
|
||||
ref.read(languageProvider.notifier).setLanguage(newValue);
|
||||
}
|
||||
},
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'system',
|
||||
child: Text('System'),
|
||||
child: Text(l10n.systemLanguage),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text('English'),
|
||||
child: Text(l10n.english),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'fr',
|
||||
child: Text('Français'),
|
||||
child: Text(l10n.french),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -141,7 +148,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Theme',
|
||||
l10n.theme,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@ -150,21 +157,21 @@ class SettingsPage extends ConsumerWidget {
|
||||
onSelectionChanged: (Set<ThemeMode> newSelection) {
|
||||
ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first);
|
||||
},
|
||||
segments: const [
|
||||
segments: [
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.light,
|
||||
label: Text('Light'),
|
||||
icon: Icon(Icons.light_mode),
|
||||
label: Text(l10n.themeLight),
|
||||
icon: const Icon(Icons.light_mode),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.dark,
|
||||
label: Text('Dark'),
|
||||
icon: Icon(Icons.dark_mode),
|
||||
label: Text(l10n.themeDark),
|
||||
icon: const Icon(Icons.dark_mode),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.system,
|
||||
label: Text('Auto'),
|
||||
icon: Icon(Icons.brightness_auto),
|
||||
label: Text(l10n.themeSystem),
|
||||
icon: const Icon(Icons.brightness_auto),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -180,17 +187,17 @@ class SettingsPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'About',
|
||||
l10n.about,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: const Text('App Version'),
|
||||
title: Text(l10n.appVersion),
|
||||
subtitle: const Text('1.0.0'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Built with Flutter'),
|
||||
subtitle: const Text('Plan B Logistics Management System'),
|
||||
title: Text(l10n.builtWithFlutter),
|
||||
subtitle: Text(l10n.appDescription),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../api/types.dart';
|
||||
import '../api/client.dart';
|
||||
import '../api/openapi_config.dart';
|
||||
@ -125,15 +126,24 @@ final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
|
||||
return allDeliveries;
|
||||
});
|
||||
|
||||
// Language notifier for state management
|
||||
class LanguageNotifier extends Notifier<String> {
|
||||
@override
|
||||
String build() => 'system';
|
||||
// Language notifier for state management with SharedPreferences persistence
|
||||
class LanguageNotifier extends AsyncNotifier<String> {
|
||||
static const String _languageKey = 'app_language';
|
||||
|
||||
void setLanguage(String lang) => state = lang;
|
||||
@override
|
||||
Future<String> build() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_languageKey) ?? 'system';
|
||||
}
|
||||
|
||||
Future<void> setLanguage(String lang) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_languageKey, lang);
|
||||
state = AsyncValue.data(lang);
|
||||
}
|
||||
}
|
||||
|
||||
final languageProvider = NotifierProvider<LanguageNotifier, String>(() {
|
||||
final languageProvider = AsyncNotifierProvider<LanguageNotifier, String>(() {
|
||||
return LanguageNotifier();
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user