Compare commits
15 Commits
v3.6.3
...
35e9ddbed5
| Author | SHA1 | Date | |
|---|---|---|---|
| 35e9ddbed5 | |||
| 3daf5a204a | |||
| 1cdb4ec58c | |||
| 7345cf805e | |||
| eef1fc8d32 | |||
| 097335193e | |||
| cf1e54d8d0 | |||
| d6c4c6d228 | |||
| b3e03ef6e6 | |||
| 9c837f8adb | |||
| 16b30b0e9e | |||
| c1c6c55cbe | |||
| 3c3a9b90aa | |||
| 6952417147 | |||
| a392305820 |
@@ -15,7 +15,8 @@ set -euo pipefail
|
|||||||
START_TIME=$(($(date +%s%N)/1000000))
|
START_TIME=$(($(date +%s%N)/1000000))
|
||||||
echo "[$(date '+%H:%M:%S.%3N')] Début du script deploy-app.sh"
|
echo "[$(date '+%H:%M:%S.%3N')] Début du script deploy-app.sh"
|
||||||
|
|
||||||
cd /home/pierre/dev/geosector/app
|
# Note: Le code source est sur IN1:/home/pierre/dev/geosector/app
|
||||||
|
# Ce script peut être lancé depuis n'importe où (desktop/laptop)
|
||||||
|
|
||||||
# =====================================
|
# =====================================
|
||||||
# Configuration générale
|
# Configuration générale
|
||||||
@@ -62,6 +63,10 @@ echo_info() {
|
|||||||
echo -e "${BLUE}Info:${NC} $1"
|
echo -e "${BLUE}Info:${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo_success() {
|
||||||
|
echo -e "${GREEN}✓${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
echo_warning() {
|
echo_warning() {
|
||||||
echo -e "${YELLOW}Warning:${NC} $1"
|
echo -e "${YELLOW}Warning:${NC} $1"
|
||||||
}
|
}
|
||||||
@@ -144,50 +149,120 @@ ARCHIVE_NAME="app-deploy-${TIMESTAMP}.tar.gz"
|
|||||||
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||||
|
|
||||||
if [ "$SOURCE_TYPE" = "local_build" ]; then
|
if [ "$SOURCE_TYPE" = "local_build" ]; then
|
||||||
# DEV: Build Flutter et créer une archive
|
# DEV: Build Flutter sur IN1 et créer une archive
|
||||||
echo_step "Building Flutter app for DEV..."
|
echo_step "Building Flutter app on IN1 for DEV..."
|
||||||
|
|
||||||
# Configuration du cache local (partagé web + iOS/Android)
|
# Variables pour IN1
|
||||||
echo_info "📦 Configuration du cache local pour builds web et mobile..."
|
IN1_HOST="IN1"
|
||||||
export PUB_CACHE="$PWD/.pub-cache-local"
|
IN1_PROJECT_PATH="/home/pierre/dev/geosector/app"
|
||||||
export GRADLE_USER_HOME="$PWD/.gradle-local"
|
|
||||||
mkdir -p "$PUB_CACHE" "$GRADLE_USER_HOME"
|
|
||||||
|
|
||||||
echo_info " Cache Pub: $PUB_CACHE"
|
echo_info "🖥️ Compilation distante sur IN1:${IN1_PROJECT_PATH}"
|
||||||
echo_info " Cache Gradle: $GRADLE_USER_HOME"
|
|
||||||
echo_info " → Packages patchés seront prêts pour transfert iOS/Android"
|
|
||||||
|
|
||||||
# Charger les variables d'environnement
|
# Charger les variables d'environnement localement pour la version
|
||||||
if [ ! -f .env-deploy-dev ]; then
|
if [ ! -f .env-deploy-dev ]; then
|
||||||
echo_error "Missing .env-deploy-dev file"
|
echo_error "Missing .env-deploy-dev file"
|
||||||
fi
|
fi
|
||||||
source .env-deploy-dev
|
source .env-deploy-dev
|
||||||
|
|
||||||
# Mise à jour de la version
|
# Système automatique de versioning YY.MM.DDNN
|
||||||
echo_info "Managing version..."
|
echo_info "Managing version (automatic YY.MM.DDNN)..."
|
||||||
|
|
||||||
|
# Date du jour
|
||||||
|
TODAY=$(date +%y.%m.%d)
|
||||||
|
TODAY_YY=$(date +%y)
|
||||||
|
TODAY_MM=$(date +%m)
|
||||||
|
TODAY_DD=$(date +%d)
|
||||||
|
|
||||||
if [ -f ../VERSION ]; then
|
if [ -f ../VERSION ]; then
|
||||||
VERSION=$(cat ../VERSION | tr -d '\n\r' | tr -d ' ')
|
CURRENT_VERSION=$(cat ../VERSION | tr -d '\n\r' | tr -d ' ')
|
||||||
echo_info "Version found: $VERSION"
|
echo_info "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Extraire la partie date et le build number (format: YY.MM.DDNN)
|
||||||
|
if [[ $CURRENT_VERSION =~ ^([0-9]{2})\.([0-9]{2})\.([0-9]{2})([0-9]{2})$ ]]; then
|
||||||
|
PREV_YY="${BASH_REMATCH[1]}"
|
||||||
|
PREV_MM="${BASH_REMATCH[2]}"
|
||||||
|
PREV_DD="${BASH_REMATCH[3]}"
|
||||||
|
BUILD_NUM="${BASH_REMATCH[4]}"
|
||||||
|
|
||||||
|
# Si la date a changé, reset à 01
|
||||||
|
if [ "$PREV_YY" != "$TODAY_YY" ] || [ "$PREV_MM" != "$TODAY_MM" ] || [ "$PREV_DD" != "$TODAY_DD" ]; then
|
||||||
|
NEW_BUILD="01"
|
||||||
|
echo_info "Date changed: $PREV_YY.$PREV_MM.$PREV_DD → $TODAY_YY.$TODAY_MM.$TODAY_DD, resetting build to 01"
|
||||||
else
|
else
|
||||||
echo_warning "VERSION file not found"
|
# Incrémenter le build number
|
||||||
read -p "Enter version number (x.x.x format): " VERSION
|
NEW_BUILD=$(printf "%02d" $((10#$BUILD_NUM + 1)))
|
||||||
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
echo_info "Same date, incrementing build: $BUILD_NUM → $NEW_BUILD"
|
||||||
echo "$VERSION" > ../VERSION
|
|
||||||
echo_info "VERSION file created with $VERSION"
|
|
||||||
else
|
|
||||||
echo_error "Invalid version format"
|
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
# Format ancien ou invalide, créer nouveau format
|
||||||
|
NEW_BUILD="01"
|
||||||
|
echo_warning "Old version format detected, creating new format"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Pas de fichier VERSION, créer avec .01
|
||||||
|
NEW_BUILD="01"
|
||||||
|
echo_warning "VERSION file not found, creating new one"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Génération du build number et mise à jour du pubspec.yaml
|
# Construire la version semver: YY.MM.DDNN (3 parties)
|
||||||
BUILD_NUMBER=$(echo $VERSION | tr -d '.')
|
VERSION="${TODAY_YY}.${TODAY_MM}.${TODAY_DD}${NEW_BUILD}"
|
||||||
|
echo "$VERSION" > ../VERSION
|
||||||
|
echo_success "✅ New version: $VERSION"
|
||||||
|
|
||||||
|
# Build number: YYMMDDNN (tous les chiffres)
|
||||||
|
BUILD_NUMBER="${TODAY_YY}${TODAY_MM}${TODAY_DD}${NEW_BUILD}"
|
||||||
|
|
||||||
|
# Version complète pour pubspec.yaml
|
||||||
FULL_VERSION="${VERSION}+${BUILD_NUMBER}"
|
FULL_VERSION="${VERSION}+${BUILD_NUMBER}"
|
||||||
echo_info "Full version: $FULL_VERSION"
|
echo_info "Semver version for pubspec.yaml: $FULL_VERSION"
|
||||||
|
|
||||||
sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml || echo_error "Failed to update pubspec.yaml"
|
sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml || echo_error "Failed to update pubspec.yaml"
|
||||||
|
|
||||||
# Génération automatique du fichier AppInfoService (remplace package_info_plus)
|
# Lancer la compilation sur IN1 via SSH
|
||||||
echo_info "Auto-generating app_info_service.dart with version $VERSION+$BUILD_NUMBER..."
|
echo_info "🚀 Lancement de la compilation sur IN1..."
|
||||||
|
BUILD_START=$(($(date +%s%N)/1000000))
|
||||||
|
|
||||||
|
ssh ${IN1_HOST} "bash -l -s" <<'REMOTE_SCRIPT' || echo_error "Remote build failed on IN1"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Charger le profil utilisateur pour avoir Flutter dans le PATH
|
||||||
|
if [ -f ~/.bashrc ]; then
|
||||||
|
source ~/.bashrc
|
||||||
|
fi
|
||||||
|
if [ -f ~/.profile ]; then
|
||||||
|
source ~/.profile
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /home/pierre/dev/geosector/app
|
||||||
|
|
||||||
|
echo "[IN1] Starting Flutter build process..."
|
||||||
|
|
||||||
|
# Charger les variables d'environnement
|
||||||
|
if [ ! -f .env-deploy-dev ]; then
|
||||||
|
echo "ERROR: Missing .env-deploy-dev file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source .env-deploy-dev
|
||||||
|
|
||||||
|
# Lire la version
|
||||||
|
if [ -f ../VERSION ]; then
|
||||||
|
VERSION=$(cat ../VERSION | tr -d '\n\r' | tr -d ' ')
|
||||||
|
echo "[IN1] Version: $VERSION"
|
||||||
|
else
|
||||||
|
echo "ERROR: VERSION file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Génération du build number
|
||||||
|
BUILD_NUMBER=$(echo $VERSION | tr -d '.')
|
||||||
|
FULL_VERSION="${VERSION}+${BUILD_NUMBER}"
|
||||||
|
echo "[IN1] Full version: $FULL_VERSION"
|
||||||
|
|
||||||
|
# Mise à jour du pubspec.yaml
|
||||||
|
sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml
|
||||||
|
|
||||||
|
# Génération automatique du fichier AppInfoService
|
||||||
|
echo "[IN1] Generating app_info_service.dart..."
|
||||||
cat > lib/core/services/app_info_service.dart <<EOF
|
cat > lib/core/services/app_info_service.dart <<EOF
|
||||||
// ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
// ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
||||||
// This file is automatically generated by deploy-app.sh script
|
// This file is automatically generated by deploy-app.sh script
|
||||||
@@ -198,13 +273,13 @@ if [ "$SOURCE_TYPE" = "local_build" ]; then
|
|||||||
// Provides application version and build information without external dependencies
|
// Provides application version and build information without external dependencies
|
||||||
|
|
||||||
class AppInfoService {
|
class AppInfoService {
|
||||||
// Version number (format: x.x.x)
|
// Version number (format: YY.MM.DDNN - auto-incremented on each DEV deploy)
|
||||||
static const String version = '$VERSION';
|
static const String version = '$VERSION';
|
||||||
|
|
||||||
// Build number (version without dots: xxx)
|
// Build number (version without dots: YYMMDDNN)
|
||||||
static const String buildNumber = '$BUILD_NUMBER';
|
static const String buildNumber = '$BUILD_NUMBER';
|
||||||
|
|
||||||
// Full version string (format: vx.x.x+xxx)
|
// Full version string (format: vYY.MM.DDNN+YYMMDDNN)
|
||||||
static String get fullVersion => 'v\$version+\$buildNumber';
|
static String get fullVersion => 'v\$version+\$buildNumber';
|
||||||
|
|
||||||
// Application name
|
// Application name
|
||||||
@@ -214,53 +289,60 @@ class AppInfoService {
|
|||||||
static const String packageName = 'fr.geosector.app3';
|
static const String packageName = 'fr.geosector.app3';
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
echo_info "✓ app_info_service.dart updated successfully"
|
|
||||||
|
|
||||||
# Mode de compilation en RELEASE (production)
|
# Configuration du cache local
|
||||||
echo_info "🏁 Mode RELEASE - Compilation optimisée pour production"
|
echo "[IN1] Configuring local cache..."
|
||||||
BUILD_FLAGS="--release"
|
export PUB_CACHE="$PWD/.pub-cache-local"
|
||||||
|
export GRADLE_USER_HOME="$PWD/.gradle-local"
|
||||||
|
mkdir -p "$PUB_CACHE" "$GRADLE_USER_HOME"
|
||||||
|
|
||||||
# Nettoyage
|
# Nettoyage
|
||||||
echo_info "Cleaning previous builds..."
|
echo "[IN1] Cleaning previous builds..."
|
||||||
rm -rf .dart_tool build .packages pubspec.lock 2>/dev/null || true
|
rm -rf .dart_tool build .packages pubspec.lock 2>/dev/null || true
|
||||||
flutter clean || echo_warning "Flutter clean partially failed"
|
flutter clean
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
echo_info "Getting dependencies..."
|
echo "[IN1] Getting dependencies..."
|
||||||
flutter pub get || echo_error "Flutter pub get failed"
|
flutter pub get
|
||||||
|
|
||||||
# Patch nfc_manager 3.3.0 (AndroidManifest namespace)
|
# Patch nfc_manager 3.3.0
|
||||||
echo_info "Applying nfc_manager 3.3.0 patch..."
|
echo "[IN1] Applying nfc_manager patch..."
|
||||||
./fastlane/scripts/commun/fix-nfc-manager.sh || echo_error "nfc_manager patch failed"
|
./fastlane/scripts/commun/fix-nfc-manager.sh
|
||||||
|
|
||||||
echo_info "Cleaning generated files..."
|
echo "[IN1] Cleaning generated files..."
|
||||||
dart run build_runner clean || echo_error "Build runner clean failed"
|
dart run build_runner clean
|
||||||
|
|
||||||
echo_info "Generating code files..."
|
echo "[IN1] Generating code files..."
|
||||||
dart run build_runner build --delete-conflicting-outputs || echo_error "Code generation failed"
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
echo_info "Building Flutter web application..."
|
# Mode de compilation en RELEASE
|
||||||
# Mesure du temps de compilation Flutter
|
echo "[IN1] 🏁 Building Flutter web (RELEASE mode)..."
|
||||||
BUILD_START=$(($(date +%s%N)/1000000))
|
flutter build web --release
|
||||||
echo_info "[$(date '+%H:%M:%S.%3N')] Début de la compilation Flutter (Mode: RELEASE)"
|
|
||||||
|
|
||||||
flutter build web $BUILD_FLAGS || echo_error "Flutter build failed"
|
echo "[IN1] Fixing web assets structure..."
|
||||||
|
./copy-web-images.sh
|
||||||
|
|
||||||
BUILD_END=$(($(date +%s%N)/1000000))
|
# Créer l'archive sur IN1
|
||||||
BUILD_TIME=$((BUILD_END - BUILD_START))
|
echo "[IN1] Creating deployment archive..."
|
||||||
echo_info "[$(date '+%H:%M:%S.%3N')] Fin de la compilation Flutter"
|
tar -czf /tmp/app-deploy-build.tar.gz -C build/web \
|
||||||
echo_info "⏱️ Temps de compilation Flutter: ${BUILD_TIME} ms ($((BUILD_TIME/1000)) secondes)"
|
|
||||||
|
|
||||||
echo_info "Fixing web assets structure..."
|
|
||||||
./copy-web-images.sh || echo_error "Failed to fix web assets"
|
|
||||||
|
|
||||||
# Créer l'archive depuis le build (avec exclusions pour réduire la taille)
|
|
||||||
echo_info "Creating archive from build..."
|
|
||||||
tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} \
|
|
||||||
--exclude='*.symbols' \
|
--exclude='*.symbols' \
|
||||||
--exclude='*.kra' \
|
--exclude='*.kra' \
|
||||||
--exclude='.DS_Store' \
|
--exclude='.DS_Store' \
|
||||||
. || echo_error "Failed to create archive"
|
.
|
||||||
|
|
||||||
|
echo "[IN1] Build completed successfully!"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
|
||||||
|
BUILD_END=$(($(date +%s%N)/1000000))
|
||||||
|
BUILD_TIME=$((BUILD_END - BUILD_START))
|
||||||
|
echo_info "⏱️ Temps de compilation sur IN1: ${BUILD_TIME} ms ($((BUILD_TIME/1000)) secondes)"
|
||||||
|
|
||||||
|
# Récupérer l'archive depuis IN1
|
||||||
|
echo_info "📥 Downloading archive from IN1..."
|
||||||
|
scp ${IN1_HOST}:/tmp/app-deploy-build.tar.gz ${TEMP_ARCHIVE} || echo_error "Failed to download archive from IN1"
|
||||||
|
|
||||||
|
# Nettoyer sur IN1
|
||||||
|
ssh ${IN1_HOST} "rm -f /tmp/app-deploy-build.tar.gz"
|
||||||
|
|
||||||
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
||||||
|
|
||||||
|
|||||||
259
app/lib/core/config/navigation_config.dart
Executable file
259
app/lib/core/config/navigation_config.dart
Executable file
@@ -0,0 +1,259 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
||||||
|
|
||||||
|
/// Configuration centralisée de la navigation pour toute l'application
|
||||||
|
/// Gère les destinations, routes et logique de navigation pour admin et user
|
||||||
|
class NavigationConfig {
|
||||||
|
// ========================================
|
||||||
|
// DESTINATIONS DE NAVIGATION
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Destinations communes à tous les rôles
|
||||||
|
static const List<NavigationDestination> _commonDestinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: 'Tableau de bord',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.history_outlined),
|
||||||
|
selectedIcon: Icon(Icons.history),
|
||||||
|
label: 'Historique',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.map_outlined),
|
||||||
|
selectedIcon: Icon(Icons.map),
|
||||||
|
label: 'Carte',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Destination Messages avec badge (commune à tous)
|
||||||
|
static NavigationDestination get _messagesDestination {
|
||||||
|
return createBadgedNavigationDestination(
|
||||||
|
icon: const Icon(Icons.chat_outlined),
|
||||||
|
selectedIcon: const Icon(Icons.chat),
|
||||||
|
label: 'Messages',
|
||||||
|
showBadge: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destination Terrain (user uniquement)
|
||||||
|
static const NavigationDestination _fieldModeDestination = NavigationDestination(
|
||||||
|
icon: Icon(Icons.explore_outlined),
|
||||||
|
selectedIcon: Icon(Icons.explore),
|
||||||
|
label: 'Terrain',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Destinations admin desktop uniquement
|
||||||
|
static const List<NavigationDestination> _adminDesktopDestinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.business_outlined),
|
||||||
|
selectedIcon: Icon(Icons.business),
|
||||||
|
label: 'Amicale & membres',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.calendar_today_outlined),
|
||||||
|
selectedIcon: Icon(Icons.calendar_today),
|
||||||
|
label: 'Opérations',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.analytics_outlined),
|
||||||
|
selectedIcon: Icon(Icons.analytics),
|
||||||
|
label: 'Connexions',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// GETTERS DE DESTINATIONS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir la liste des destinations selon le rôle et le device
|
||||||
|
static List<NavigationDestination> getDestinations({
|
||||||
|
required bool isAdmin,
|
||||||
|
required bool isMobile,
|
||||||
|
}) {
|
||||||
|
final destinations = <NavigationDestination>[];
|
||||||
|
|
||||||
|
// Ajouter les destinations communes
|
||||||
|
destinations.addAll(_commonDestinations);
|
||||||
|
|
||||||
|
// Ajouter Messages (avec badge)
|
||||||
|
destinations.add(_messagesDestination);
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
// Admin : ajouter les pages supplémentaires sur desktop uniquement
|
||||||
|
if (!isMobile) {
|
||||||
|
destinations.addAll(_adminDesktopDestinations);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User : ajouter la page Terrain
|
||||||
|
destinations.add(_fieldModeDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// NAVIGATION (INDEX → ROUTE)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Naviguer vers une page selon l'index et le rôle
|
||||||
|
static void navigateToIndex(BuildContext context, int index, bool isAdmin) {
|
||||||
|
if (isAdmin) {
|
||||||
|
_navigateAdminIndex(context, index);
|
||||||
|
} else {
|
||||||
|
_navigateUserIndex(context, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigation pour les admins
|
||||||
|
static void _navigateAdminIndex(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
context.go('/admin');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
context.go('/admin/history');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/admin/map');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
context.go('/admin/messages');
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
context.go('/admin/amicale');
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
context.go('/admin/operations');
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
context.go('/admin/connexions');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.go('/admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigation pour les utilisateurs standards
|
||||||
|
static void _navigateUserIndex(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
context.go('/user/dashboard');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
context.go('/user/history');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/user/map');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
context.go('/user/messages');
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
context.go('/user/field-mode');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.go('/user/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// RÉSOLUTION (ROUTE → INDEX)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir l'index selon la route actuelle et le rôle
|
||||||
|
static int getIndexFromRoute(String route, bool isAdmin) {
|
||||||
|
// Enlever les paramètres de query si présents
|
||||||
|
final cleanRoute = route.split('?').first;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
return _getAdminIndexFromRoute(cleanRoute);
|
||||||
|
} else {
|
||||||
|
return _getUserIndexFromRoute(cleanRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir l'index admin depuis la route
|
||||||
|
static int _getAdminIndexFromRoute(String route) {
|
||||||
|
if (route.contains('/admin/history')) return 1;
|
||||||
|
if (route.contains('/admin/map')) return 2;
|
||||||
|
if (route.contains('/admin/messages')) return 3;
|
||||||
|
if (route.contains('/admin/amicale')) return 4;
|
||||||
|
if (route.contains('/admin/operations')) return 5;
|
||||||
|
if (route.contains('/admin/connexions')) return 6;
|
||||||
|
return 0; // Dashboard par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir l'index user depuis la route
|
||||||
|
static int _getUserIndexFromRoute(String route) {
|
||||||
|
if (route.contains('/user/history')) return 1;
|
||||||
|
if (route.contains('/user/map')) return 2;
|
||||||
|
if (route.contains('/user/messages')) return 3;
|
||||||
|
if (route.contains('/user/field-mode')) return 4;
|
||||||
|
return 0; // Dashboard par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// UTILITAIRES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir le nom de la page selon l'index et le rôle
|
||||||
|
static String getPageNameFromIndex(int index, bool isAdmin) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return _getAdminPageName(index);
|
||||||
|
} else {
|
||||||
|
return _getUserPageName(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir le nom de page admin
|
||||||
|
static String _getAdminPageName(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return 'dashboard';
|
||||||
|
case 1:
|
||||||
|
return 'history';
|
||||||
|
case 2:
|
||||||
|
return 'map';
|
||||||
|
case 3:
|
||||||
|
return 'messages';
|
||||||
|
case 4:
|
||||||
|
return 'amicale';
|
||||||
|
case 5:
|
||||||
|
return 'operations';
|
||||||
|
case 6:
|
||||||
|
return 'connexions';
|
||||||
|
default:
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir le nom de page user
|
||||||
|
static String _getUserPageName(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return 'dashboard';
|
||||||
|
case 1:
|
||||||
|
return 'history';
|
||||||
|
case 2:
|
||||||
|
return 'map';
|
||||||
|
case 3:
|
||||||
|
return 'messages';
|
||||||
|
case 4:
|
||||||
|
return 'field-mode';
|
||||||
|
default:
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TITRES ET LABELS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir le titre du dashboard selon le rôle
|
||||||
|
static String getDashboardTitle(bool isAdmin) {
|
||||||
|
return isAdmin ? 'Tableau de bord Administration' : 'GEOSECTOR';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,7 @@ class AppTheme {
|
|||||||
static const Color warningColor = Color(0xFFF7A278); // Orange
|
static const Color warningColor = Color(0xFFF7A278); // Orange
|
||||||
static const Color backgroundLightColor =
|
static const Color backgroundLightColor =
|
||||||
Color(0xFFF4F5F6); // Gris très clair
|
Color(0xFFF4F5F6); // Gris très clair
|
||||||
static const Color backgroundDarkColor = Color(0xFF111827);
|
|
||||||
static const Color textLightColor = Color(0xFF000000); // Noir
|
static const Color textLightColor = Color(0xFF000000); // Noir
|
||||||
static const Color textDarkColor = Color(0xFFF9FAFB);
|
|
||||||
|
|
||||||
// Couleurs de texte supplémentaires
|
// Couleurs de texte supplémentaires
|
||||||
static const Color textSecondaryColor = Color(0xFF7F8C8D);
|
static const Color textSecondaryColor = Color(0xFF7F8C8D);
|
||||||
@@ -191,103 +189,6 @@ class AppTheme {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thème sombre
|
|
||||||
static ThemeData get darkTheme {
|
|
||||||
return ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
fontFamily: 'Inter',
|
|
||||||
colorScheme: const ColorScheme.dark(
|
|
||||||
primary: primaryColor,
|
|
||||||
secondary: secondaryColor,
|
|
||||||
tertiary: accentColor,
|
|
||||||
surface: Color(0xFF1F2937),
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
onSecondary: Colors.white,
|
|
||||||
onSurface: textDarkColor,
|
|
||||||
error: errorColor,
|
|
||||||
),
|
|
||||||
scaffoldBackgroundColor: backgroundDarkColor,
|
|
||||||
textTheme: _getTextTheme(textDarkColor),
|
|
||||||
appBarTheme: const AppBarTheme(
|
|
||||||
backgroundColor: Color(0xFF1F2937),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: spacingL, vertical: spacingM),
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusRounded),
|
|
||||||
),
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontFamily: 'Inter',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: primaryColor,
|
|
||||||
side: const BorderSide(color: primaryColor),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: spacingL, vertical: spacingM),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textButtonTheme: TextButtonThemeData(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: primaryColor,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: spacingM, vertical: spacingS),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
|
||||||
filled: true,
|
|
||||||
fillColor: const Color(0xFF374151),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: textDarkColor.withOpacity(0.1),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: textDarkColor.withOpacity(0.1),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
|
||||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: spacingM, vertical: spacingM),
|
|
||||||
),
|
|
||||||
cardTheme: CardTheme(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadiusXL),
|
|
||||||
),
|
|
||||||
color: const Color(0xFF1F2937),
|
|
||||||
),
|
|
||||||
dividerTheme: DividerThemeData(
|
|
||||||
color: textDarkColor.withOpacity(0.1),
|
|
||||||
thickness: 1,
|
|
||||||
space: spacingM,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode helper pour générer le TextTheme responsive
|
// Méthode helper pour générer le TextTheme responsive
|
||||||
static TextTheme getResponsiveTextTheme(BuildContext context, Color textColor) {
|
static TextTheme getResponsiveTextTheme(BuildContext context, Color textColor) {
|
||||||
final scaleFactor = getFontScaleFactor(MediaQuery.of(context).size.width);
|
final scaleFactor = getFontScaleFactor(MediaQuery.of(context).size.width);
|
||||||
@@ -351,7 +252,7 @@ class AppTheme {
|
|||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
color: textColor,
|
color: textColor,
|
||||||
fontSize: 16 * scaleFactor,
|
fontSize: 16 * scaleFactor,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
bodyMedium: TextStyle(
|
bodyMedium: TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
@@ -363,6 +264,7 @@ class AppTheme {
|
|||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
color: textColor.withOpacity(0.7),
|
color: textColor.withOpacity(0.7),
|
||||||
fontSize: 12 * scaleFactor,
|
fontSize: 12 * scaleFactor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Label styles (petits textes, boutons)
|
// Label styles (petits textes, boutons)
|
||||||
@@ -376,11 +278,13 @@ class AppTheme {
|
|||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
color: textColor.withOpacity(0.7),
|
color: textColor.withOpacity(0.7),
|
||||||
fontSize: 12 * scaleFactor,
|
fontSize: 12 * scaleFactor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
labelSmall: TextStyle(
|
labelSmall: TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
color: textColor.withOpacity(0.7),
|
color: textColor.withOpacity(0.7),
|
||||||
fontSize: 11 * scaleFactor,
|
fontSize: 11 * scaleFactor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -397,12 +301,12 @@ class AppTheme {
|
|||||||
titleLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 22),
|
titleLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 22),
|
||||||
titleMedium: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 16, fontWeight: FontWeight.w600),
|
titleMedium: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
titleSmall: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
titleSmall: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
bodyLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 16, fontWeight: FontWeight.w500),
|
bodyLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
bodyMedium: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w500),
|
bodyMedium: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
bodySmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12),
|
bodySmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
labelLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
labelLarge: TextStyle(fontFamily: 'Inter', color: textColor, fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
labelMedium: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12),
|
labelMedium: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
labelSmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 11),
|
labelSmall: TextStyle(fontFamily: 'Inter', color: textColor.withOpacity(0.7), fontSize: 11, fontWeight: FontWeight.w500),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
186
app/lib/presentation/pages/history_page.dart
Normal file → Executable file
186
app/lib/presentation/pages/history_page.dart
Normal file → Executable file
@@ -5,6 +5,7 @@ import 'package:geosector_app/core/constants/app_keys.dart';
|
|||||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||||
|
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
||||||
@@ -55,6 +56,8 @@ class _HistoryContentState extends State<HistoryContent> {
|
|||||||
String _selectedTypeFilter = 'Tous les types';
|
String _selectedTypeFilter = 'Tous les types';
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
int? selectedTypeId;
|
int? selectedTypeId;
|
||||||
|
int? _selectedMemberId; // null = "Tous" (admin uniquement)
|
||||||
|
int? _selectedSectorId; // null = "Tous" (admin uniquement)
|
||||||
|
|
||||||
// Contrôleur de recherche
|
// Contrôleur de recherche
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@@ -221,6 +224,20 @@ class _HistoryContentState extends State<HistoryContent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filtre par membre (admin uniquement)
|
||||||
|
if (isAdmin && _selectedMemberId != null) {
|
||||||
|
if (passage.fkUser != _selectedMemberId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par secteur (admin uniquement)
|
||||||
|
if (isAdmin && _selectedSectorId != null) {
|
||||||
|
if (passage.fkSector != _selectedSectorId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filtre par recherche textuelle
|
// Filtre par recherche textuelle
|
||||||
if (_searchQuery.isNotEmpty) {
|
if (_searchQuery.isNotEmpty) {
|
||||||
final query = _searchQuery.toLowerCase();
|
final query = _searchQuery.toLowerCase();
|
||||||
@@ -302,6 +319,7 @@ class _HistoryContentState extends State<HistoryContent> {
|
|||||||
children: [
|
children: [
|
||||||
// Barre de recherche
|
// Barre de recherche
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
@@ -319,6 +337,42 @@ class _HistoryContentState extends State<HistoryContent> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Filtres admin uniquement
|
||||||
|
if (isAdmin) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Filtre par membre
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: _buildMemberDropdown(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Filtre par secteur
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: _buildSectorDropdown(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Bouton réinitialiser les filtres
|
||||||
|
if (isAdmin && (_searchQuery.isNotEmpty || _selectedMemberId != null || _selectedSectorId != null)) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
|
tooltip: 'Réinitialiser les filtres',
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = '';
|
||||||
|
_searchController.clear();
|
||||||
|
_selectedMemberId = null;
|
||||||
|
_selectedSectorId = null;
|
||||||
|
});
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -563,4 +617,136 @@ class _HistoryContentState extends State<HistoryContent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construit le dropdown de sélection de membre (admin uniquement)
|
||||||
|
Widget _buildMemberDropdown() {
|
||||||
|
// Récupérer tous les membres depuis la box Hive
|
||||||
|
final membresBox = membreRepository.getMembresBox();
|
||||||
|
var membres = membresBox.values.where((membre) {
|
||||||
|
// Ne garder que les membres ayant un opeUserId (membres actifs dans une opération)
|
||||||
|
return membre.opeUserId != null;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Filtrage dynamique : si un secteur est sélectionné, ne montrer que les membres de ce secteur
|
||||||
|
if (_selectedSectorId != null) {
|
||||||
|
final userSectorsBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||||
|
final memberIdsInSector = userSectorsBox.values
|
||||||
|
.where((us) => us.fkSector == _selectedSectorId)
|
||||||
|
.map((us) => us.opeUserId)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
membres = membres.where((membre) => memberIdsInSector.contains(membre.opeUserId)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trier par nom
|
||||||
|
membres.sort((a, b) {
|
||||||
|
final nameA = a.name ?? a.firstName ?? '';
|
||||||
|
final nameB = b.name ?? b.firstName ?? '';
|
||||||
|
return nameA.compareTo(nameB);
|
||||||
|
});
|
||||||
|
|
||||||
|
return DropdownButtonFormField<int?>(
|
||||||
|
value: _selectedMemberId,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
isDense: true,
|
||||||
|
labelText: 'Membre',
|
||||||
|
prefixIcon: Icon(Icons.person, size: 20),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem<int?>(
|
||||||
|
value: null,
|
||||||
|
child: Text('Tous'),
|
||||||
|
),
|
||||||
|
...membres.map((membre) {
|
||||||
|
// Format: FirstName name (sectName)
|
||||||
|
final firstName = membre.firstName ?? '';
|
||||||
|
final name = membre.name ?? '';
|
||||||
|
final sectName = membre.sectName ?? '';
|
||||||
|
|
||||||
|
String displayName = '';
|
||||||
|
if (firstName.isNotEmpty && name.isNotEmpty) {
|
||||||
|
displayName = '$firstName $name';
|
||||||
|
} else if (firstName.isNotEmpty) {
|
||||||
|
displayName = firstName;
|
||||||
|
} else if (name.isNotEmpty) {
|
||||||
|
displayName = name;
|
||||||
|
} else {
|
||||||
|
displayName = 'Membre #${membre.opeUserId}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectName.isNotEmpty) {
|
||||||
|
displayName = '$displayName ($sectName)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return DropdownMenuItem<int?>(
|
||||||
|
value: membre.opeUserId,
|
||||||
|
child: Text(
|
||||||
|
displayName,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChanged: (int? newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedMemberId = newValue;
|
||||||
|
});
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le dropdown de sélection de secteur (admin uniquement)
|
||||||
|
Widget _buildSectorDropdown() {
|
||||||
|
// Récupérer tous les secteurs depuis le repository
|
||||||
|
var allSectors = sectorRepository.getAllSectors();
|
||||||
|
|
||||||
|
// Filtrage dynamique : si un membre est sélectionné, ne montrer que ses secteurs
|
||||||
|
if (_selectedMemberId != null) {
|
||||||
|
final userSectorsBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||||
|
final sectorIdsForMember = userSectorsBox.values
|
||||||
|
.where((us) => us.opeUserId == _selectedMemberId)
|
||||||
|
.map((us) => us.fkSector)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
allSectors = allSectors.where((sector) => sectorIdsForMember.contains(sector.id)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trier par nom
|
||||||
|
allSectors.sort((a, b) => a.libelle.compareTo(b.libelle));
|
||||||
|
|
||||||
|
return DropdownButtonFormField<int?>(
|
||||||
|
value: _selectedSectorId,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
isDense: true,
|
||||||
|
labelText: 'Secteur',
|
||||||
|
prefixIcon: Icon(Icons.location_on, size: 20),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem<int?>(
|
||||||
|
value: null,
|
||||||
|
child: Text('Tous'),
|
||||||
|
),
|
||||||
|
...allSectors.map((sector) {
|
||||||
|
return DropdownMenuItem<int?>(
|
||||||
|
value: sector.id,
|
||||||
|
child: Text(
|
||||||
|
sector.libelle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChanged: (int? newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedSectorId = newValue;
|
||||||
|
});
|
||||||
|
_applyFilters();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
app/lib/presentation/pages/map_page.dart
Normal file → Executable file
3
app/lib/presentation/pages/map_page.dart
Normal file → Executable file
@@ -8,6 +8,7 @@ import 'dart:math' as math;
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
|
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
import 'package:geosector_app/presentation/widgets/app_scaffold.dart';
|
||||||
|
import 'package:geosector_app/core/config/navigation_config.dart';
|
||||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||||
import 'package:geosector_app/core/services/location_service.dart';
|
import 'package:geosector_app/core/services/location_service.dart';
|
||||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||||
@@ -36,7 +37,7 @@ class MapPage extends StatelessWidget {
|
|||||||
|
|
||||||
// Obtenir l'index de navigation selon la route actuelle
|
// Obtenir l'index de navigation selon la route actuelle
|
||||||
final currentRoute = GoRouterState.of(context).uri.toString();
|
final currentRoute = GoRouterState.of(context).uri.toString();
|
||||||
final selectedIndex = NavigationHelper.getIndexFromRoute(currentRoute, isAdmin);
|
final selectedIndex = NavigationConfig.getIndexFromRoute(currentRoute, isAdmin);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
|
||||||
import 'package:geosector_app/app.dart';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Class pour dessiner les petits points blancs sur le fond
|
|
||||||
class DotsPainter extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = Colors.white.withOpacity(0.5)
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final random = math.Random(42); // Seed fixe pour consistance
|
|
||||||
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
||||||
|
|
||||||
for (int i = 0; i < numberOfDots; i++) {
|
|
||||||
final x = random.nextDouble() * size.width;
|
|
||||||
final y = random.nextDouble() * size.height;
|
|
||||||
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
||||||
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scaffold partagé pour toutes les pages d'administration
|
|
||||||
/// Fournit le fond dégradé et la navigation commune
|
|
||||||
class AdminScaffold extends StatelessWidget {
|
|
||||||
/// Le contenu de la page
|
|
||||||
final Widget body;
|
|
||||||
|
|
||||||
/// L'index de navigation sélectionné
|
|
||||||
final int selectedIndex;
|
|
||||||
|
|
||||||
/// Le titre de la page
|
|
||||||
final String pageTitle;
|
|
||||||
|
|
||||||
/// Callback optionnel pour gérer la navigation personnalisée
|
|
||||||
final Function(int)? onDestinationSelected;
|
|
||||||
|
|
||||||
const AdminScaffold({
|
|
||||||
super.key,
|
|
||||||
required this.body,
|
|
||||||
required this.selectedIndex,
|
|
||||||
required this.pageTitle,
|
|
||||||
this.onDestinationSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final currentUser = userRepository.getCurrentUser();
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
final isMobile = size.width <= 900;
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
// Fond dégradé avec petits points blancs
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Colors.white, Colors.red.shade300],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Contenu de la page avec navigation
|
|
||||||
DashboardLayout(
|
|
||||||
key: ValueKey('dashboard_layout_$selectedIndex'),
|
|
||||||
title: 'Tableau de bord Administration',
|
|
||||||
selectedIndex: selectedIndex,
|
|
||||||
onDestinationSelected: onDestinationSelected ?? (index) {
|
|
||||||
// Navigation par défaut si pas de callback personnalisé
|
|
||||||
AdminNavigationHelper.navigateToIndex(context, index);
|
|
||||||
},
|
|
||||||
destinations: AdminNavigationHelper.getDestinations(
|
|
||||||
currentUser: currentUser,
|
|
||||||
isMobile: isMobile,
|
|
||||||
),
|
|
||||||
isAdmin: true,
|
|
||||||
body: body,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper pour centraliser la logique de navigation admin
|
|
||||||
class AdminNavigationHelper {
|
|
||||||
/// Obtenir la liste des destinations de navigation selon le rôle et le device
|
|
||||||
static List<NavigationDestination> getDestinations({
|
|
||||||
required dynamic currentUser,
|
|
||||||
required bool isMobile,
|
|
||||||
}) {
|
|
||||||
final destinations = <NavigationDestination>[
|
|
||||||
// Pages de base toujours visibles
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
|
||||||
selectedIcon: Icon(Icons.dashboard),
|
|
||||||
label: 'Tableau de bord',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.bar_chart_outlined),
|
|
||||||
selectedIcon: Icon(Icons.bar_chart),
|
|
||||||
label: 'Statistiques',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.history_outlined),
|
|
||||||
selectedIcon: Icon(Icons.history),
|
|
||||||
label: 'Historique',
|
|
||||||
),
|
|
||||||
createBadgedNavigationDestination(
|
|
||||||
icon: const Icon(Icons.chat_outlined),
|
|
||||||
selectedIcon: const Icon(Icons.chat),
|
|
||||||
label: 'Messages',
|
|
||||||
showBadge: true,
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.map_outlined),
|
|
||||||
selectedIcon: Icon(Icons.map),
|
|
||||||
label: 'Carte',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ajouter les pages admin (role 2) seulement sur desktop
|
|
||||||
if (currentUser?.role == 2 && !isMobile) {
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.business_outlined),
|
|
||||||
selectedIcon: Icon(Icons.business),
|
|
||||||
label: 'Amicale & membres',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.calendar_today_outlined),
|
|
||||||
selectedIcon: Icon(Icons.calendar_today),
|
|
||||||
label: 'Opérations',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Naviguer vers une page selon l'index
|
|
||||||
static void navigateToIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/admin');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/admin/statistics');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/admin/history');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/admin/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/admin/map');
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
context.go('/admin/amicale');
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
context.go('/admin/operations');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir l'index selon la route actuelle
|
|
||||||
static int getIndexFromRoute(String route) {
|
|
||||||
if (route.contains('/statistics')) return 1;
|
|
||||||
if (route.contains('/history')) return 2;
|
|
||||||
if (route.contains('/messages')) return 3;
|
|
||||||
if (route.contains('/map')) return 4;
|
|
||||||
if (route.contains('/amicale')) return 5;
|
|
||||||
if (route.contains('/operations')) return 6;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir le nom de la page selon l'index
|
|
||||||
static String getPageNameFromIndex(int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'statistics';
|
|
||||||
case 2: return 'history';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'map';
|
|
||||||
case 5: return 'amicale';
|
|
||||||
case 6: return 'operations';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
263
app/lib/presentation/widgets/app_scaffold.dart
Normal file → Executable file
263
app/lib/presentation/widgets/app_scaffold.dart
Normal file → Executable file
@@ -1,33 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
import 'package:geosector_app/presentation/widgets/backgrounds/gradient_background.dart';
|
||||||
|
import 'package:geosector_app/core/config/navigation_config.dart';
|
||||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||||
import 'package:geosector_app/app.dart';
|
import 'package:geosector_app/app.dart';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Classe pour dessiner les petits points blancs sur le fond
|
|
||||||
class DotsPainter extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = Colors.white.withOpacity(0.5)
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final random = math.Random(42); // Seed fixe pour consistance
|
|
||||||
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
||||||
|
|
||||||
for (int i = 0; i < numberOfDots; i++) {
|
|
||||||
final x = random.nextDouble() * size.width;
|
|
||||||
final y = random.nextDouble() * size.height;
|
|
||||||
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
||||||
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scaffold unifié pour toutes les pages (admin et user)
|
/// Scaffold unifié pour toutes les pages (admin et user)
|
||||||
/// Adapte automatiquement son apparence selon le rôle de l'utilisateur
|
/// Adapte automatiquement son apparence selon le rôle de l'utilisateur
|
||||||
@@ -102,43 +78,26 @@ class AppScaffold extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Couleurs de fond selon le rôle
|
|
||||||
final gradientColors = isAdmin
|
|
||||||
? [Colors.white, Colors.red.shade300] // Admin: dégradé rouge
|
|
||||||
: [Colors.white, Colors.green.shade300]; // User: dégradé vert
|
|
||||||
|
|
||||||
// Titre avec suffixe selon le rôle
|
|
||||||
final dashboardTitle = isAdmin
|
|
||||||
? 'Tableau de bord Administration'
|
|
||||||
: 'GEOSECTOR';
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond dégradé avec petits points blancs (optionnel)
|
// Fond dégradé avec petits points blancs (optionnel)
|
||||||
if (showBackground)
|
if (showBackground)
|
||||||
Container(
|
GradientBackground(
|
||||||
decoration: BoxDecoration(
|
isAdmin: isAdmin,
|
||||||
gradient: LinearGradient(
|
showDots: true,
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: gradientColors,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Contenu de la page avec navigation
|
// Contenu de la page avec navigation
|
||||||
DashboardLayout(
|
DashboardLayout(
|
||||||
key: ValueKey('dashboard_layout_${isAdmin ? 'admin' : 'user'}_$selectedIndex'),
|
key: ValueKey(
|
||||||
title: dashboardTitle,
|
'dashboard_layout_${isAdmin ? 'admin' : 'user'}_$selectedIndex'),
|
||||||
|
title: NavigationConfig.getDashboardTitle(isAdmin),
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: onDestinationSelected ?? (index) {
|
onDestinationSelected: onDestinationSelected ??
|
||||||
NavigationHelper.navigateToIndex(context, index, isAdmin);
|
(index) {
|
||||||
|
NavigationConfig.navigateToIndex(context, index, isAdmin);
|
||||||
},
|
},
|
||||||
destinations: NavigationHelper.getDestinations(
|
destinations: NavigationConfig.getDestinations(
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
isMobile: isMobile,
|
isMobile: isMobile,
|
||||||
),
|
),
|
||||||
@@ -159,27 +118,13 @@ class AppScaffold extends StatelessWidget {
|
|||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
// Utiliser le même fond que pour un utilisateur normal (vert)
|
|
||||||
final gradientColors = isAdmin
|
|
||||||
? [Colors.white, Colors.red.shade300]
|
|
||||||
: [Colors.white, Colors.green.shade300];
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond dégradé (optionnel)
|
// Fond dégradé (optionnel)
|
||||||
if (showBackground)
|
if (showBackground)
|
||||||
Container(
|
GradientBackground(
|
||||||
decoration: BoxDecoration(
|
isAdmin: isAdmin,
|
||||||
gradient: LinearGradient(
|
showDots: true,
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: gradientColors,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Message d'accès restreint
|
// Message d'accès restreint
|
||||||
@@ -246,181 +191,3 @@ class AppScaffold extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper centralisé pour la navigation
|
|
||||||
class NavigationHelper {
|
|
||||||
/// Obtenir la liste des destinations selon le mode d'affichage et le device
|
|
||||||
static List<NavigationDestination> getDestinations({
|
|
||||||
required bool isAdmin,
|
|
||||||
required bool isMobile,
|
|
||||||
}) {
|
|
||||||
final destinations = <NavigationDestination>[];
|
|
||||||
|
|
||||||
// Pages communes à tous les rôles
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
|
||||||
selectedIcon: Icon(Icons.dashboard),
|
|
||||||
label: 'Tableau de bord',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.history_outlined),
|
|
||||||
selectedIcon: Icon(Icons.history),
|
|
||||||
label: 'Historique',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.map_outlined),
|
|
||||||
selectedIcon: Icon(Icons.map),
|
|
||||||
label: 'Carte',
|
|
||||||
),
|
|
||||||
createBadgedNavigationDestination(
|
|
||||||
icon: const Icon(Icons.chat_outlined),
|
|
||||||
selectedIcon: const Icon(Icons.chat),
|
|
||||||
label: 'Messages',
|
|
||||||
showBadge: true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Pages spécifiques aux utilisateurs standards
|
|
||||||
if (!isAdmin) {
|
|
||||||
destinations.add(
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.explore_outlined),
|
|
||||||
selectedIcon: Icon(Icons.explore),
|
|
||||||
label: 'Terrain',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages spécifiques aux admins (seulement sur desktop)
|
|
||||||
if (isAdmin && !isMobile) {
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.business_outlined),
|
|
||||||
selectedIcon: Icon(Icons.business),
|
|
||||||
label: 'Amicale & membres',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.calendar_today_outlined),
|
|
||||||
selectedIcon: Icon(Icons.calendar_today),
|
|
||||||
label: 'Opérations',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.analytics_outlined),
|
|
||||||
selectedIcon: Icon(Icons.analytics),
|
|
||||||
label: 'Connexions',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Naviguer vers une page selon l'index et le rôle
|
|
||||||
static void navigateToIndex(BuildContext context, int index, bool isAdmin) {
|
|
||||||
if (isAdmin) {
|
|
||||||
_navigateAdminIndex(context, index);
|
|
||||||
} else {
|
|
||||||
_navigateUserIndex(context, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation pour les admins
|
|
||||||
static void _navigateAdminIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/admin');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/admin/history');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/admin/map');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/admin/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/admin/amicale');
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
context.go('/admin/operations');
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
context.go('/admin/connexions');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation pour les utilisateurs standards
|
|
||||||
static void _navigateUserIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/user/dashboard');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/user/history');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/user/map');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/user/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/user/field-mode');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/user/dashboard');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir l'index selon la route actuelle et le rôle
|
|
||||||
static int getIndexFromRoute(String route, bool isAdmin) {
|
|
||||||
// Enlever les paramètres de query si présents
|
|
||||||
final cleanRoute = route.split('?').first;
|
|
||||||
|
|
||||||
if (isAdmin) {
|
|
||||||
if (cleanRoute.contains('/admin/history')) return 1;
|
|
||||||
if (cleanRoute.contains('/admin/map')) return 2;
|
|
||||||
if (cleanRoute.contains('/admin/messages')) return 3;
|
|
||||||
if (cleanRoute.contains('/admin/amicale')) return 4;
|
|
||||||
if (cleanRoute.contains('/admin/operations')) return 5;
|
|
||||||
if (cleanRoute.contains('/admin/connexions')) return 6;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
} else {
|
|
||||||
if (cleanRoute.contains('/user/history')) return 1;
|
|
||||||
if (cleanRoute.contains('/user/map')) return 2;
|
|
||||||
if (cleanRoute.contains('/user/messages')) return 3;
|
|
||||||
if (cleanRoute.contains('/user/field-mode')) return 4;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir le nom de la page selon l'index et le rôle
|
|
||||||
static String getPageNameFromIndex(int index, bool isAdmin) {
|
|
||||||
if (isAdmin) {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'history';
|
|
||||||
case 2: return 'map';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'amicale';
|
|
||||||
case 5: return 'operations';
|
|
||||||
case 6: return 'connexions';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'history';
|
|
||||||
case 2: return 'map';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'field-mode';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
app/lib/presentation/widgets/backgrounds/dots_painter.dart
Executable file
46
app/lib/presentation/widgets/backgrounds/dots_painter.dart
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
/// Painter pour dessiner des petits points blancs semi-transparents sur un fond
|
||||||
|
/// Utilisé pour créer un effet visuel subtil sur les fonds dégradés
|
||||||
|
class DotsPainter extends CustomPainter {
|
||||||
|
/// Opacité des points (0.0 à 1.0)
|
||||||
|
final double opacity;
|
||||||
|
|
||||||
|
/// Seed pour le générateur aléatoire (pour consistance du pattern)
|
||||||
|
final int seed;
|
||||||
|
|
||||||
|
/// Densité des points (nombre de pixels par point)
|
||||||
|
/// Plus la valeur est élevée, moins il y a de points
|
||||||
|
final int density;
|
||||||
|
|
||||||
|
const DotsPainter({
|
||||||
|
this.opacity = 0.5,
|
||||||
|
this.seed = 42,
|
||||||
|
this.density = 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.white.withOpacity(opacity)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final random = math.Random(seed);
|
||||||
|
final numberOfDots = (size.width * size.height) ~/ density;
|
||||||
|
|
||||||
|
for (int i = 0; i < numberOfDots; i++) {
|
||||||
|
final x = random.nextDouble() * size.width;
|
||||||
|
final y = random.nextDouble() * size.height;
|
||||||
|
final radius = 1.0 + random.nextDouble() * 2.0;
|
||||||
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant DotsPainter oldDelegate) {
|
||||||
|
return oldDelegate.opacity != opacity ||
|
||||||
|
oldDelegate.seed != seed ||
|
||||||
|
oldDelegate.density != density;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/lib/presentation/widgets/backgrounds/gradient_background.dart
Executable file
49
app/lib/presentation/widgets/backgrounds/gradient_background.dart
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geosector_app/presentation/widgets/backgrounds/dots_painter.dart';
|
||||||
|
|
||||||
|
/// Widget de fond dégradé avec points décoratifs
|
||||||
|
/// Utilisé comme fond pour les pages admin et user avec des couleurs différentes
|
||||||
|
class GradientBackground extends StatelessWidget {
|
||||||
|
/// Indique si le fond est pour un admin (rouge) ou user (vert)
|
||||||
|
final bool isAdmin;
|
||||||
|
|
||||||
|
/// Afficher ou non les points décoratifs
|
||||||
|
final bool showDots;
|
||||||
|
|
||||||
|
/// Opacité des points (0.0 à 1.0)
|
||||||
|
final double dotsOpacity;
|
||||||
|
|
||||||
|
const GradientBackground({
|
||||||
|
super.key,
|
||||||
|
required this.isAdmin,
|
||||||
|
this.showDots = true,
|
||||||
|
this.dotsOpacity = 0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Couleurs de fond selon le rôle
|
||||||
|
final gradientColors = isAdmin
|
||||||
|
? [Colors.white, Colors.red.shade300] // Admin: dégradé rouge
|
||||||
|
: [Colors.white, Colors.green.shade300]; // User: dégradé vert
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: gradientColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: showDots
|
||||||
|
? CustomPaint(
|
||||||
|
painter: DotsPainter(opacity: dotsOpacity),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,30 +48,6 @@ class DashboardLayout extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
try {
|
|
||||||
debugPrint('Building DashboardLayout');
|
|
||||||
|
|
||||||
// Vérifier que les destinations ne sont pas vides
|
|
||||||
if (destinations.isEmpty) {
|
|
||||||
debugPrint('ERREUR: destinations est vide dans DashboardLayout');
|
|
||||||
return const Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text('Erreur: Aucune destination de navigation disponible'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que selectedIndex est valide
|
|
||||||
if (selectedIndex < 0 || selectedIndex >= destinations.length) {
|
|
||||||
debugPrint('ERREUR: selectedIndex invalide dans DashboardLayout');
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child:
|
|
||||||
Text('Erreur: Index de navigation invalide ($selectedIndex)'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scaffold avec fond transparent (le fond est géré par AppScaffold)
|
// Scaffold avec fond transparent (le fond est géré par AppScaffold)
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: ValueKey('dashboard_scaffold_$selectedIndex'),
|
key: ValueKey('dashboard_scaffold_$selectedIndex'),
|
||||||
@@ -85,55 +61,17 @@ class DashboardLayout extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
body: ResponsiveNavigation(
|
body: ResponsiveNavigation(
|
||||||
key: ValueKey('responsive_nav_$selectedIndex'),
|
key: ValueKey('responsive_nav_$selectedIndex'),
|
||||||
title:
|
title: title,
|
||||||
title, // Même si le titre n'est pas affiché dans la navigation, il est utilisé pour la cohérence
|
|
||||||
body: body,
|
body: body,
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: onDestinationSelected,
|
onDestinationSelected: onDestinationSelected,
|
||||||
destinations: destinations,
|
destinations: destinations,
|
||||||
// Ne pas afficher le bouton "Nouveau passage" dans la navigation
|
|
||||||
showNewPassageButton: false,
|
showNewPassageButton: false,
|
||||||
onNewPassagePressed: null,
|
onNewPassagePressed: null,
|
||||||
sidebarBottomItems: sidebarBottomItems,
|
sidebarBottomItems: sidebarBottomItems,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
// Ne pas afficher l'AppBar dans la navigation car nous utilisons DashboardAppBar
|
|
||||||
showAppBar: false,
|
showAppBar: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
debugPrint('ERREUR CRITIQUE dans DashboardLayout.build: $e');
|
|
||||||
// Afficher une interface de secours en cas d'erreur
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Erreur - $title'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error_outline, color: Colors.red, size: 64),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Une erreur est survenue',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppTheme.r(context, 20),
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text('Détails: $e'),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context)
|
|
||||||
.pushNamedAndRemoveUntil('/', (route) => false);
|
|
||||||
},
|
|
||||||
child: const Text('Retour à l\'accueil'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
docs/PLANNING-2026-Q1.md
Normal file → Executable file
27
docs/PLANNING-2026-Q1.md
Normal file → Executable file
@@ -31,12 +31,10 @@
|
|||||||
|-------|------------|--------------------------------------------|--------|
|
|-------|------------|--------------------------------------------|--------|
|
||||||
| 19/01 | `#13` Jour 1 | ✅ `#204` Design couleurs flashy | à livrer v3.6.3 |
|
| 19/01 | `#13` Jour 1 | ✅ `#204` Design couleurs flashy | à livrer v3.6.3 |
|
||||||
| 19/01 | | ✅ `#205` Écrans utilisateurs simplifiés | à livrer v3.6.3 |
|
| 19/01 | | ✅ `#205` Écrans utilisateurs simplifiés | à livrer v3.6.3 |
|
||||||
| 20/01 | `#13` Jour 2 | `#113` Couleur repasses orange | |
|
| 20/01 | `#13` Jour 2 | ✅ `#113` Couleur repasses orange | ✅ Validé 26/01 |
|
||||||
| 20/01 | | `#72`Épaisseur police lisibilité | |
|
| 20/01 | | ✅ `#72` Épaisseur police lisibilité (theme + BtnPassages) | ✅ Livré 26/01 |
|
||||||
| 21/01 | `#13` Jour 3 | `#71`Visibilité bouton "Envoyer message" | |
|
| 21/01 | `#13` Jour 3 | ✅ `#42` Filtres membre/secteur history (admin) | ✅ Livré 26/01 |
|
||||||
| 21/01 | | `#59`Listing rues invisible (clavier) | |
|
| 23/01 | `#13` Jour 5 | ✅ `#74`Simplifier DashboardLayout/AppScaffold | ✅ Livré 26/01 |
|
||||||
| 22/01 | `#13` Jour 4 | `#42`Historique adresses cliquables | |
|
|
||||||
| 23/01 | `#13` Jour 5 | `#74`Simplifier DashboardLayout/AppScaffold | |
|
|
||||||
| 24/01 | | `#28`Gestion reçus Flutter nouveaux champs | |
|
| 24/01 | | `#28`Gestion reçus Flutter nouveaux champs | |
|
||||||
| 25/01 | | `#50`Modifier secteur au clic | |
|
| 25/01 | | `#50`Modifier secteur au clic | |
|
||||||
|
|
||||||
@@ -143,17 +141,28 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## PHASE 7 : DIVERS (sans date)
|
||||||
|
### Tâches diverses - À planifier ultérieurement
|
||||||
|
|
||||||
|
| ID | Tâche | Cat | Statut |
|
||||||
|
|------|----------------------------------------|------|--------|
|
||||||
|
| `#71` | Visibilité bouton "Envoyer message" | UX | |
|
||||||
|
| `#59` | Listing rues invisible (clavier) | UX | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## RÉCAPITULATIF
|
## RÉCAPITULATIF
|
||||||
|
|
||||||
| Phase | Période | Jours | Tâches | Focus |
|
| Phase | Période | Jours | Tâches | Focus |
|
||||||
|-----------|--------------|-------|--------|---------------------|
|
|-----------|--------------|-------|--------|---------------------|
|
||||||
| 1 | 16-18/01 | 3 | 5 | Bugs critiques |
|
| 1 | 16-18/01 | 3 | 7 | Bugs critiques |
|
||||||
| 2 | 19-25/01 | 7 | 10 | Stripe iOS + UX |
|
| 2 | 19-25/01 | 7 | 8 | Stripe iOS + UX |
|
||||||
| 3 | 26/01-07/02 | 10 | 25 | MAP / Carte |
|
| 3 | 26/01-07/02 | 10 | 25 | MAP / Carte |
|
||||||
| 4 | 08-14/02 | 6 | 11 | Stripe + Passages |
|
| 4 | 08-14/02 | 6 | 11 | Stripe + Passages |
|
||||||
| 5 | 15-22/02 | 7 | 22 | Admin + Membres |
|
| 5 | 15-22/02 | 7 | 22 | Admin + Membres |
|
||||||
| 6 | 23-28/02 | 5 | 15 | Export + Divers |
|
| 6 | 23-28/02 | 5 | 15 | Export + Divers |
|
||||||
| **TOTAL** | **44 jours** | | **88** | |
|
| 7 | Sans date | - | 2 | Divers |
|
||||||
|
| **TOTAL** | **44 jours** | | **90** | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user