checkpoint

This commit is contained in:
Mathias Beaulieu-Duncan 2025-11-26 17:41:37 -05:00
parent ef5c0c1a95
commit 2ecd1c5b4e
15 changed files with 794 additions and 152 deletions

276
DEVELOPMENT.md Normal file
View 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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../models/delivery_route.dart'; import '../models/delivery_route.dart';
import '../theme/spacing_system.dart'; import '../theme/spacing_system.dart';
import '../theme/size_system.dart'; import '../theme/size_system.dart';
@ -72,6 +73,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
final isMobile = context.isMobile; final isMobile = context.isMobile;
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isExpanded = ref.watch(collapseStateProvider); final isExpanded = ref.watch(collapseStateProvider);
final l10n = AppLocalizations.of(context)!;
// On mobile, always show as collapsible // On mobile, always show as collapsible
if (isMobile) { if (isMobile) {
@ -95,7 +97,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
children: [ children: [
if (isExpanded) if (isExpanded)
Text( Text(
'Routes', l10n.routes,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
@ -146,7 +148,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
child: Padding( child: Padding(
padding: EdgeInsets.only(left: AppSpacing.md), padding: EdgeInsets.only(left: AppSpacing.md),
child: Text( child: Text(
'Routes', l10n.routes,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../models/delivery.dart'; import '../models/delivery.dart';
import '../theme/animation_system.dart'; import '../theme/animation_system.dart';
import '../theme/color_system.dart'; import '../theme/color_system.dart';
import '../l10n/app_localizations.dart';
class DeliveryListItem extends StatefulWidget { class DeliveryListItem extends StatefulWidget {
final Delivery delivery; final Delivery delivery;
@ -94,6 +95,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.delivery); final statusColor = _getStatusColor(widget.delivery);
final l10n = AppLocalizations.of(context)!;
// Collapsed view: Show only the badge // Collapsed view: Show only the badge
if (widget.isCollapsed) { if (widget.isCollapsed) {
@ -296,7 +298,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
Text( Text(
widget.delivery.deliveryAddress widget.delivery.deliveryAddress
?.formattedAddress ?? ?.formattedAddress ??
'No address', l10n.noAddress,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyMedium .bodyMedium

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../models/delivery_route.dart'; import '../models/delivery_route.dart';
import '../theme/animation_system.dart'; import '../theme/animation_system.dart';
import '../theme/color_system.dart'; import '../theme/color_system.dart';
import '../l10n/app_localizations.dart';
class RouteListItem extends StatefulWidget { class RouteListItem extends StatefulWidget {
final DeliveryRoute route; final DeliveryRoute route;
@ -79,6 +80,7 @@ class _RouteListItemState extends State<RouteListItem>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.route); final statusColor = _getStatusColor(widget.route);
final l10n = AppLocalizations.of(context)!;
// Collapsed view: Show only the badge // Collapsed view: Show only the badge
if (widget.isCollapsed) { if (widget.isCollapsed) {
@ -229,7 +231,7 @@ class _RouteListItemState extends State<RouteListItem>
const SizedBox(height: 4), const SizedBox(height: 4),
// Route details // Route details
Text( Text(
'${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries', l10n.routeDeliveries(widget.route.deliveredCount, widget.route.deliveriesCount),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyMedium .bodyMedium

View File

@ -84,5 +84,30 @@
"requestPermission": "Request Permission", "requestPermission": "Request Permission",
"navigationArrived": "You have arrived at the destination", "navigationArrived": "You have arrived at the destination",
"navigatingTo": "Navigating to", "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"
} }

View File

@ -1,18 +1,18 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"appTitle": "Plan B Logistique", "appTitle": "Plan B Logistique",
"appDescription": "Systme de Gestion des Livraisons", "appDescription": "Système de Gestion des Livraisons",
"loginWithKeycloak": "Connexion avec Keycloak", "loginWithKeycloak": "Connexion avec Keycloak",
"deliveryRoutes": "Itinraires de Livraison", "deliveryRoutes": "Itinéraires de Livraison",
"routes": "Itinraires", "routes": "Itinéraires",
"deliveries": "Livraisons", "deliveries": "Livraisons",
"settings": "Paramtres", "settings": "Paramètres",
"profile": "Profil", "profile": "Profil",
"logout": "Dconnexion", "logout": "Déconnexion",
"completed": "Livr", "completed": "Livré",
"pending": "En attente", "pending": "En attente",
"todo": "livrer", "todo": "À livrer",
"delivered": "Livr", "delivered": "Livré",
"newCustomer": "Nouveau Client", "newCustomer": "Nouveau Client",
"items": "{count} articles", "items": "{count} articles",
"@items": { "@items": {
@ -29,29 +29,29 @@
"call": "Appeler", "call": "Appeler",
"map": "Carte", "map": "Carte",
"more": "Plus", "more": "Plus",
"markAsCompleted": "Marquer comme livr", "markAsCompleted": "Marquer comme livré",
"markAsUncompleted": "Marquer comme livrer", "markAsUncompleted": "Marquer comme à livrer",
"uploadPhoto": "Tlcharger une photo", "uploadPhoto": "Télécharger une photo",
"viewDetails": "Voir les dtails", "viewDetails": "Voir les détails",
"deliverySuccessful": "Livraison marque comme complte", "deliverySuccessful": "Livraison marquée comme complète",
"deliveryFailed": "chec du marquage de la livraison", "deliveryFailed": "Échec du marquage de la livraison",
"noDeliveries": "Aucune livraison", "noDeliveries": "Aucune livraison",
"noRoutes": "Aucun itinraire disponible", "noRoutes": "Aucun itinéraire disponible",
"error": "Erreur: {message}", "error": "Erreur: {message}",
"@error": { "@error": {
"placeholders": { "placeholders": {
"message": {"type": "String"} "message": {"type": "String"}
} }
}, },
"retry": "Ressayer", "retry": "Réessayer",
"authenticationRequired": "Authentification requise", "authenticationRequired": "Authentification requise",
"phoneCall": "Appeler le client", "phoneCall": "Appeler le client",
"navigateToAddress": "Afficher sur la carte", "navigateToAddress": "Afficher sur la carte",
"language": "Langue", "language": "Langue",
"english": "English", "english": "English",
"french": "Franais", "french": "Français",
"appVersion": "Version de l'application", "appVersion": "Version de l'application",
"about": " propos", "about": "À propos",
"fullName": "{firstName} {lastName}", "fullName": "{firstName} {lastName}",
"@fullName": { "@fullName": {
"placeholders": { "placeholders": {
@ -59,7 +59,7 @@
"lastName": {"type": "String"} "lastName": {"type": "String"}
} }
}, },
"completedDeliveries": "{completed}/{total} livrs", "completedDeliveries": "{completed}/{total} livrés",
"@completedDeliveries": { "@completedDeliveries": {
"placeholders": { "placeholders": {
"completed": {"type": "int"}, "completed": {"type": "int"},
@ -69,20 +69,45 @@
"navigationTcTitle": "Service de Navigation", "navigationTcTitle": "Service de Navigation",
"navigationTcDescription": "Cette application utilise Google Navigation pour fournir une navigation virage par virage pour les livraisons.", "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.", "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", "accept": "Accepter",
"decline": "Refuser", "decline": "Refuser",
"locationPermissionRequired": "Permission de localisation", "locationPermissionRequired": "Permission de localisation",
"locationPermissionMessage": "Cette application ncessite la permission de localisation pour naviguer vers les livraisons.", "locationPermissionMessage": "Cette application nécessite la permission de localisation pour naviguer vers les livraisons.",
"locationPermissionDenied": "Permission de localisation refuse. La navigation ne peut pas continuer.", "locationPermissionDenied": "Permission de localisation refusée. La navigation ne peut pas continuer.",
"permissionPermanentlyDenied": "Permission requise", "permissionPermanentlyDenied": "Permission requise",
"openSettingsMessage": "La permission de localisation est dfinitivement refuse. Veuillez l'activer dans les paramtres de l'application.", "openSettingsMessage": "La permission de localisation est définitivement refusée. Veuillez l'activer dans les paramètres de l'application.",
"openSettings": "Ouvrir les paramtres", "openSettings": "Ouvrir les paramètres",
"cancel": "Annuler", "cancel": "Annuler",
"ok": "OK", "ok": "OK",
"errorTitle": "Erreur", "errorTitle": "Erreur",
"requestPermission": "Demander la permission", "requestPermission": "Demander la permission",
"navigationArrived": "Vous tes arriv la destination", "navigationArrived": "Vous êtes arrivé à la destination",
"navigatingTo": "Navigation vers", "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"
} }

View File

@ -445,6 +445,120 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Initializing navigation...'** /// **'Initializing navigation...'**
String get initializingNavigation; 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 class _AppLocalizationsDelegate

View File

@ -197,4 +197,63 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get initializingNavigation => 'Initializing navigation...'; 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';
} }

View File

@ -12,40 +12,40 @@ class AppLocalizationsFr extends AppLocalizations {
String get appTitle => 'Plan B Logistique'; String get appTitle => 'Plan B Logistique';
@override @override
String get appDescription => 'Systme de Gestion des Livraisons'; String get appDescription => 'Système de Gestion des Livraisons';
@override @override
String get loginWithKeycloak => 'Connexion avec Keycloak'; String get loginWithKeycloak => 'Connexion avec Keycloak';
@override @override
String get deliveryRoutes => 'Itinraires de Livraison'; String get deliveryRoutes => 'Itinéraires de Livraison';
@override @override
String get routes => 'Itinraires'; String get routes => 'Itinéraires';
@override @override
String get deliveries => 'Livraisons'; String get deliveries => 'Livraisons';
@override @override
String get settings => 'Paramtres'; String get settings => 'Paramètres';
@override @override
String get profile => 'Profil'; String get profile => 'Profil';
@override @override
String get logout => 'Dconnexion'; String get logout => 'Déconnexion';
@override @override
String get completed => 'Livr'; String get completed => 'Livré';
@override @override
String get pending => 'En attente'; String get pending => 'En attente';
@override @override
String get todo => 'livrer'; String get todo => 'À livrer';
@override @override
String get delivered => 'Livr'; String get delivered => 'Livré';
@override @override
String get newCustomer => 'Nouveau Client'; String get newCustomer => 'Nouveau Client';
@ -70,28 +70,28 @@ class AppLocalizationsFr extends AppLocalizations {
String get more => 'Plus'; String get more => 'Plus';
@override @override
String get markAsCompleted => 'Marquer comme livr'; String get markAsCompleted => 'Marquer comme livré';
@override @override
String get markAsUncompleted => 'Marquer comme livrer'; String get markAsUncompleted => 'Marquer comme à livrer';
@override @override
String get uploadPhoto => 'Tlcharger une photo'; String get uploadPhoto => 'Télécharger une photo';
@override @override
String get viewDetails => 'Voir les dtails'; String get viewDetails => 'Voir les détails';
@override @override
String get deliverySuccessful => 'Livraison marque comme complte'; String get deliverySuccessful => 'Livraison marquée comme complète';
@override @override
String get deliveryFailed => 'chec du marquage de la livraison'; String get deliveryFailed => 'Échec du marquage de la livraison';
@override @override
String get noDeliveries => 'Aucune livraison'; String get noDeliveries => 'Aucune livraison';
@override @override
String get noRoutes => 'Aucun itinraire disponible'; String get noRoutes => 'Aucun itinéraire disponible';
@override @override
String error(String message) { String error(String message) {
@ -99,7 +99,7 @@ class AppLocalizationsFr extends AppLocalizations {
} }
@override @override
String get retry => 'Ressayer'; String get retry => 'Réessayer';
@override @override
String get authenticationRequired => 'Authentification requise'; String get authenticationRequired => 'Authentification requise';
@ -117,13 +117,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get english => 'English'; String get english => 'English';
@override @override
String get french => 'Franais'; String get french => 'Français';
@override @override
String get appVersion => 'Version de l\'application'; String get appVersion => 'Version de l\'application';
@override @override
String get about => ' propos'; String get about => 'À propos';
@override @override
String fullName(String firstName, String lastName) { String fullName(String firstName, String lastName) {
@ -132,7 +132,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String completedDeliveries(int completed, int total) { String completedDeliveries(int completed, int total) {
return '$completed/$total livrs'; return '$completed/$total livrés';
} }
@override @override
@ -148,7 +148,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get navigationTcTerms => 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 @override
String get accept => 'Accepter'; String get accept => 'Accepter';
@ -161,21 +161,21 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get locationPermissionMessage => 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 @override
String get locationPermissionDenied => 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 @override
String get permissionPermanentlyDenied => 'Permission requise'; String get permissionPermanentlyDenied => 'Permission requise';
@override @override
String get openSettingsMessage => 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 @override
String get openSettings => 'Ouvrir les paramtres'; String get openSettings => 'Ouvrir les paramètres';
@override @override
String get cancel => 'Annuler'; String get cancel => 'Annuler';
@ -190,11 +190,70 @@ class AppLocalizationsFr extends AppLocalizations {
String get requestPermission => 'Demander la permission'; String get requestPermission => 'Demander la permission';
@override @override
String get navigationArrived => 'Vous tes arriv la destination'; String get navigationArrived => 'Vous êtes arrivé à la destination';
@override @override
String get navigatingTo => 'Navigation vers'; String get navigatingTo => 'Navigation vers';
@override @override
String get initializingNavigation => 'Initialisation de la navigation...'; 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';
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';
import 'theme.dart'; import 'theme.dart';
import 'providers/providers.dart'; import 'providers/providers.dart';
import 'pages/login_page.dart'; import 'pages/login_page.dart';
@ -27,26 +28,67 @@ class PlanBLogisticApp extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final language = ref.watch(languageProvider); final languageAsync = ref.watch(languageProvider);
final themeMode = ref.watch(themeModeProvider); final themeMode = ref.watch(themeModeProvider);
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( return MaterialApp(
title: 'Plan B Logistics', title: 'Plan B Logistics',
theme: MaterialTheme(const TextTheme()).light(), theme: MaterialTheme(const TextTheme()).light(),
darkTheme: MaterialTheme(const TextTheme()).dark(), darkTheme: MaterialTheme(const TextTheme()).dark(),
themeMode: themeMode, themeMode: themeMode,
locale: Locale(language), locale: locale,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: const [ supportedLocales: const [
Locale('en', 'CA'), Locale('en'),
Locale('fr', 'CA'), Locale('fr'),
], ],
home: const AppHome(), 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(),
),
);
} }
} }

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -85,6 +86,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId)); final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
final tokenAsync = ref.watch(authTokenProvider); final tokenAsync = ref.watch(authTokenProvider);
final token = tokenAsync.hasValue ? tokenAsync.value : null; final token = tokenAsync.hasValue ? tokenAsync.value : null;
final l10n = AppLocalizations.of(context)!;
// When embedded in sidebar, show only the delivery list with back button // When embedded in sidebar, show only the delivery list with back button
// This is a responsive sidebar that collapses like routes // This is a responsive sidebar that collapses like routes
@ -183,7 +185,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
error: (error, stackTrace) => Center( 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(), child: CircularProgressIndicator(),
), ),
error: (error, stackTrace) => Center( 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, String? token,
) async { ) async {
if (token == null) { if (token == null) {
ToastHelper.showError(context, 'Authentication required'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
return; return;
} }
@ -312,12 +315,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
); );
result.when( result.when(
success: (_) { success: (_) {
final l10n = AppLocalizations.of(context)!;
// ignore: unused_result // ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId)); ref.refresh(deliveriesProvider(widget.routeFragmentId));
ToastHelper.showSuccess(context, 'Delivery marked as completed'); ToastHelper.showSuccess(context, l10n.deliverySuccessful);
}, },
onError: (error) { onError: (error) {
ToastHelper.showError(context, 'Error: ${error.message}'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.error(error.message));
}, },
); );
break; break;
@ -329,12 +334,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
); );
result.when( result.when(
success: (_) { success: (_) {
final l10n = AppLocalizations.of(context)!;
// ignore: unused_result // ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId)); ref.refresh(deliveriesProvider(widget.routeFragmentId));
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
}, },
onError: (error) { onError: (error) {
ToastHelper.showError(context, 'Error: ${error.message}'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.error(error.message));
}, },
); );
break; break;
@ -368,7 +375,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
String? token, String? token,
) async { ) async {
if (token == null) { if (token == null) {
ToastHelper.showError(context, 'Authentication required'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
return; return;
} }
@ -416,11 +424,11 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false), onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(true), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (deliveries.isEmpty) { if (deliveries.isEmpty) {
return const Center( return Center(
child: Text('No deliveries'), child: Text(l10n.noDeliveries),
); );
} }
@ -599,12 +608,12 @@ class DeliveryCard extends StatelessWidget {
), ),
if (delivery.delivered) if (delivery.delivered)
Chip( Chip(
label: const Text('Delivered'), label: Text(AppLocalizations.of(context)!.delivered),
backgroundColor: Theme.of(context).colorScheme.primaryContainer, backgroundColor: Theme.of(context).colorScheme.primaryContainer,
) )
else if (order?.isNewCustomer ?? false) else if (order?.isNewCustomer ?? false)
Chip( Chip(
label: const Text('New Customer'), label: Text(AppLocalizations.of(context)!.newCustomer),
backgroundColor: const Color(0xFFFFFBEB), backgroundColor: const Color(0xFFFFFBEB),
), ),
], ],
@ -624,11 +633,11 @@ class DeliveryCard extends StatelessWidget {
children: [ children: [
if (order.totalItems != null) if (order.totalItems != null)
Text( Text(
'${order.totalItems} items', AppLocalizations.of(context)!.items(order.totalItems!),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
Text( Text(
'${order.totalAmount} MAD', AppLocalizations.of(context)!.moneyCurrency(order.totalAmount),
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
@ -644,7 +653,7 @@ class DeliveryCard extends StatelessWidget {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => onAction(delivery, 'call'), onPressed: () => onAction(delivery, 'call'),
icon: const Icon(Icons.phone), icon: const Icon(Icons.phone),
label: const Text('Call'), label: Text(AppLocalizations.of(context)!.call),
), ),
if (delivery.deliveryAddress != null) if (delivery.deliveryAddress != null)
OutlinedButton.icon( OutlinedButton.icon(
@ -653,12 +662,12 @@ class DeliveryCard extends StatelessWidget {
onAction(delivery, 'map'); onAction(delivery, 'map');
}, },
icon: const Icon(Icons.map), icon: const Icon(Icons.map),
label: const Text('Navigate'), label: Text(AppLocalizations.of(context)!.navigate),
), ),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => _showDeliveryActions(context), onPressed: () => _showDeliveryActions(context),
icon: const Icon(Icons.more_vert), 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) { void _showDeliveryActions(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => SafeArea( builder: (context) => SafeArea(
@ -679,7 +689,7 @@ class DeliveryCard extends StatelessWidget {
if (!delivery.delivered) if (!delivery.delivered)
ListTile( ListTile(
leading: const Icon(Icons.check_circle), leading: const Icon(Icons.check_circle),
title: const Text('Mark as Completed'), title: Text(l10n.markAsCompleted),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onAction(delivery, 'complete'); onAction(delivery, 'complete');
@ -688,7 +698,7 @@ class DeliveryCard extends StatelessWidget {
else else
ListTile( ListTile(
leading: const Icon(Icons.undo), leading: const Icon(Icons.undo),
title: const Text('Mark as Uncompleted'), title: Text(l10n.markAsUncompleted),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onAction(delivery, 'uncomplete'); onAction(delivery, 'uncomplete');
@ -696,7 +706,7 @@ class DeliveryCard extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.camera_alt), leading: const Icon(Icons.camera_alt),
title: const Text('Upload Photo'), title: Text(l10n.uploadPhoto),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: Implement photo upload // TODO: Implement photo upload
@ -704,7 +714,7 @@ class DeliveryCard extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.description), leading: const Icon(Icons.description),
title: const Text('View Details'), title: Text(l10n.viewDetails),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: Navigate to delivery details // TODO: Navigate to delivery details

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../providers/providers.dart'; import '../providers/providers.dart';
import '../utils/toast_helper.dart'; import '../utils/toast_helper.dart';
@ -78,7 +79,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Plan B Logistics', AppLocalizations.of(context)!.appTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayMedium?.copyWith( style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -87,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Delivery Management System', AppLocalizations.of(context)!.appDescription,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
@ -96,17 +97,17 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox(height: 48), const SizedBox(height: 48),
TextFormField( TextFormField(
controller: _usernameController, controller: _usernameController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Username', labelText: AppLocalizations.of(context)!.username,
hintText: 'Enter your username', hintText: AppLocalizations.of(context)!.usernameHint,
prefixIcon: Icon(Icons.person), prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
enabled: !_isLoading, enabled: !_isLoading,
validator: (value) { validator: (value) {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
return 'Please enter your username'; return AppLocalizations.of(context)!.usernameRequired;
} }
return null; return null;
}, },
@ -115,8 +116,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField( TextFormField(
controller: _passwordController, controller: _passwordController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: AppLocalizations.of(context)!.password,
hintText: 'Enter your password', hintText: AppLocalizations.of(context)!.passwordHint,
prefixIcon: const Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: IconButton( suffixIcon: IconButton(
@ -136,7 +137,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
onFieldSubmitted: (_) => _handleLogin(), onFieldSubmitted: (_) => _handleLogin(),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter your password'; return AppLocalizations.of(context)!.passwordRequired;
} }
return null; return null;
}, },
@ -158,7 +159,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
), ),
), ),
) )
: const Text('Login'), : Text(AppLocalizations.of(context)!.loginButton),
), ),
], ],
), ),

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/delivery.dart'; import '../models/delivery.dart';
@ -86,7 +87,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
final token = await authService.ensureValidToken(); final token = await authService.ensureValidToken();
if (token == null) { if (token == null) {
if (mounted) { if (mounted) {
ToastHelper.showError(context, 'Authentication required'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
} }
return; return;
} }
@ -173,7 +175,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
} }
if (mounted) { 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('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
debugPrint('Error details: ${error.details}'); debugPrint('Error details: ${error.details}');
if (mounted) { if (mounted) {
String errorMessage = 'Error: ${error.message}'; final l10n = AppLocalizations.of(context)!;
String errorMessage = l10n.error(error.message);
if (error.statusCode == 500) { if (error.statusCode == 500) {
errorMessage = 'Server error - Please contact support'; errorMessage = 'Server error - Please contact support';
} }
@ -251,6 +255,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
} }
if (mounted) { if (mounted) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
} }
} }
@ -261,7 +266,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
} }
if (mounted) { 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(); final token = await authService.ensureValidToken();
if (token == null) { if (token == null) {
if (mounted) { if (mounted) {
ToastHelper.showError(context, 'Authentication required'); final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
} }
return; return;
} }
@ -507,10 +514,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
final routesData = ref.watch(deliveryRoutesProvider); final routesData = ref.watch(deliveryRoutesProvider);
final allDeliveriesData = ref.watch(allDeliveriesProvider); final allDeliveriesData = ref.watch(allDeliveriesProvider);
final userProfile = ref.watch(userProfileProvider); final userProfile = ref.watch(userProfileProvider);
final l10n = AppLocalizations.of(context)!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Delivery Routes'), title: Text(l10n.deliveryRoutes),
elevation: 0, elevation: 0,
actions: [ actions: [
IconButton( IconButton(
@ -580,8 +588,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
body: routesData.when( body: routesData.when(
data: (routes) { data: (routes) {
if (routes.isEmpty) { if (routes.isEmpty) {
return const Center( return Center(
child: Text('No routes available'), child: Text(l10n.noRoutes),
); );
} }
return allDeliveriesData.when( return allDeliveriesData.when(
@ -642,11 +650,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Error loading deliveries: $error'), Text(l10n.error(error.toString())),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () => ref.refresh(allDeliveriesProvider), onPressed: () => ref.refresh(allDeliveriesProvider),
child: const Text('Retry'), child: Text(l10n.retry),
), ),
], ],
), ),
@ -660,11 +668,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Error: $error'), Text(l10n.error(error.toString())),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () => ref.refresh(deliveryRoutesProvider), onPressed: () => ref.refresh(deliveryRoutesProvider),
child: const Text('Retry'), child: Text(l10n.retry),
), ),
], ],
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.dart';
import '../providers/providers.dart'; import '../providers/providers.dart';
class SettingsPage extends ConsumerWidget { class SettingsPage extends ConsumerWidget {
@ -8,12 +9,18 @@ class SettingsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final userProfile = ref.watch(userProfileProvider); final userProfile = ref.watch(userProfileProvider);
final language = ref.watch(languageProvider); final languageAsync = ref.watch(languageProvider);
final themeMode = ref.watch(themeModeProvider); final themeMode = ref.watch(themeModeProvider);
final l10n = AppLocalizations.of(context)!;
final language = languageAsync.maybeWhen(
data: (value) => value,
orElse: () => 'system',
);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Settings'), title: Text(l10n.settings),
), ),
body: ListView( body: ListView(
children: [ children: [
@ -23,14 +30,14 @@ class SettingsPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Profile', l10n.profile,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
userProfile.when( userProfile.when(
data: (profile) { data: (profile) {
if (profile == null) { if (profile == null) {
return const Text('No profile information'); return Text(l10n.noProfileInfo);
} }
return Card( return Card(
child: Padding( child: Padding(
@ -78,7 +85,7 @@ class SettingsPage extends ConsumerWidget {
} }
}, },
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
tooltip: 'Logout', tooltip: l10n.logout,
), ),
], ],
), ),
@ -88,7 +95,7 @@ class SettingsPage extends ConsumerWidget {
); );
}, },
loading: () => const CircularProgressIndicator(), 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Preferences', l10n.preferences,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( ListTile(
title: const Text('Language'), title: Text(l10n.language),
subtitle: Text( subtitle: Text(
language == 'system' language == 'system'
? 'System' ? l10n.systemLanguage
: language == 'fr' : language == 'fr'
? 'Français' ? l10n.french
: 'English' : l10n.english
), ),
trailing: DropdownButton<String>( trailing: DropdownButton<String>(
value: language, value: language,
@ -120,18 +127,18 @@ class SettingsPage extends ConsumerWidget {
ref.read(languageProvider.notifier).setLanguage(newValue); ref.read(languageProvider.notifier).setLanguage(newValue);
} }
}, },
items: const [ items: [
DropdownMenuItem( DropdownMenuItem(
value: 'system', value: 'system',
child: Text('System'), child: Text(l10n.systemLanguage),
), ),
DropdownMenuItem( DropdownMenuItem(
value: 'en', value: 'en',
child: Text('English'), child: Text(l10n.english),
), ),
DropdownMenuItem( DropdownMenuItem(
value: 'fr', value: 'fr',
child: Text('Français'), child: Text(l10n.french),
), ),
], ],
), ),
@ -141,7 +148,7 @@ class SettingsPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Theme', l10n.theme,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -150,21 +157,21 @@ class SettingsPage extends ConsumerWidget {
onSelectionChanged: (Set<ThemeMode> newSelection) { onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first); ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first);
}, },
segments: const [ segments: [
ButtonSegment<ThemeMode>( ButtonSegment<ThemeMode>(
value: ThemeMode.light, value: ThemeMode.light,
label: Text('Light'), label: Text(l10n.themeLight),
icon: Icon(Icons.light_mode), icon: const Icon(Icons.light_mode),
), ),
ButtonSegment<ThemeMode>( ButtonSegment<ThemeMode>(
value: ThemeMode.dark, value: ThemeMode.dark,
label: Text('Dark'), label: Text(l10n.themeDark),
icon: Icon(Icons.dark_mode), icon: const Icon(Icons.dark_mode),
), ),
ButtonSegment<ThemeMode>( ButtonSegment<ThemeMode>(
value: ThemeMode.system, value: ThemeMode.system,
label: Text('Auto'), label: Text(l10n.themeSystem),
icon: Icon(Icons.brightness_auto), icon: const Icon(Icons.brightness_auto),
), ),
], ],
), ),
@ -180,17 +187,17 @@ class SettingsPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'About', l10n.about,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( ListTile(
title: const Text('App Version'), title: Text(l10n.appVersion),
subtitle: const Text('1.0.0'), subtitle: const Text('1.0.0'),
), ),
ListTile( ListTile(
title: const Text('Built with Flutter'), title: Text(l10n.builtWithFlutter),
subtitle: const Text('Plan B Logistics Management System'), subtitle: Text(l10n.appDescription),
), ),
], ],
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api/types.dart'; import '../api/types.dart';
import '../api/client.dart'; import '../api/client.dart';
import '../api/openapi_config.dart'; import '../api/openapi_config.dart';
@ -125,15 +126,24 @@ final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
return allDeliveries; return allDeliveries;
}); });
// Language notifier for state management // Language notifier for state management with SharedPreferences persistence
class LanguageNotifier extends Notifier<String> { class LanguageNotifier extends AsyncNotifier<String> {
@override static const String _languageKey = 'app_language';
String build() => 'system';
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(); return LanguageNotifier();
}); });