From 5b6808db2532f6fd5d5b7d22c4f87e29b1ee4ae4 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 19 Jan 2026 17:46:03 +0100 Subject: [PATCH] feat: Version 3.6.3 - Carte IGN, mode boussole, corrections Flutter analyze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelles fonctionnalités: - #215 Mode boussole + carte IGN/satellite (Mode terrain) - #53 Définition zoom maximal pour éviter sur-zoom - #14 Correction bug F5 déconnexion - #204 Design couleurs flashy - #205 Écrans utilisateurs simplifiés Corrections Flutter analyze: - Suppression warnings room.g.dart, chat_service.dart, api_service.dart - 0 error, 0 warning, 30 infos (suggestions de style) Autres: - Intégration tuiles IGN Plan et IGN Ortho (geopf.fr) - flutter_compass pour Android/iOS - Réorganisation assets store Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 5 + VERSION | 2 +- api/PM7/d6back.sh | 651 -------- api/PM7/d6back.yaml | 112 -- api/PM7/decpm7.sh | 118 -- api/PM7/sync_geosector.sh | 248 --- api/deploy-api.sh | 18 + app/.dart_tool/package_config.json | 384 ++--- app/.flutter-plugins-dependencies | 2 +- app/android.sh | 19 +- .../logo-geosector-512.png-autosave.kra | Bin 195468 -> 0 bytes app/deploy-app.sh | 8 +- app/geosector_app.iml | 36 - app/lib/app.dart | 74 + app/lib/chat/models/room.g.dart | 2 +- app/lib/chat/services/chat_service.dart | 2 - app/lib/core/constants/app_keys.dart | 12 +- app/lib/core/services/api_service.dart | 14 +- app/lib/core/services/app_info_service.dart | 6 +- .../core/services/current_user_service.dart | 33 +- app/lib/core/services/hive_service.dart | 51 + app/lib/presentation/auth/splash_page.dart | 95 +- app/lib/presentation/pages/history_page.dart | 1469 ++--------------- app/lib/presentation/pages/home_page.dart | 31 +- app/lib/presentation/pages/map_page.dart | 60 +- .../user/user_field_mode_page.dart | 150 +- .../presentation/widgets/btn_passages.dart | 334 ++-- app/lib/presentation/widgets/mapbox_map.dart | 111 +- .../ephemeral/.plugin_symlinks/battery_plus | 2 +- .../.plugin_symlinks/connectivity_plus | 2 +- .../.plugin_symlinks/device_info_plus | 2 +- .../.plugin_symlinks/file_selector_linux | 2 +- .../flutter_local_notifications_linux | 2 +- .../.plugin_symlinks/image_picker_linux | 2 +- .../.plugin_symlinks/path_provider_linux | 2 +- .../.plugin_symlinks/url_launcher_linux | 2 +- .../ephemeral/Flutter-Generated.xcconfig | 4 +- .../ephemeral/flutter_export_environment.sh | 4 +- app/pubspec.lock | 8 + app/pubspec.yaml | 4 +- app/pubspec.yaml.bak | 4 +- .../geosector-1024x500.png | Bin .../geosector-admin-amicale-1800x1800.png | Bin .../geosector-admin-tbord-1800x1800.png | Bin .../geosector-user-carte-1800x1800.png | Bin .../geosector-user-histo-1800x1800.png | Bin .../geosector-user-login-1800x1800.png | Bin .../geosector-user-stripe-1800x1800.png | Bin .../geosector-user-tbord-1800x1800.png | Bin .../geosector_map_admin.png | Bin .../ephemeral/.plugin_symlinks/battery_plus | 2 +- .../.plugin_symlinks/connectivity_plus | 2 +- .../.plugin_symlinks/device_info_plus | 2 +- .../.plugin_symlinks/file_selector_windows | 2 +- .../flutter_local_notifications_windows | 2 +- .../.plugin_symlinks/geolocator_windows | 2 +- .../.plugin_symlinks/image_picker_windows | 2 +- .../.plugin_symlinks/path_provider_windows | 2 +- .../permission_handler_windows | 2 +- .../.plugin_symlinks/url_launcher_windows | 2 +- docs/PLANNING-2026-Q1.md | 288 ++-- docs/planning-geosector-q1-2026.xls | 163 ++ 62 files changed, 1428 insertions(+), 3130 deletions(-) delete mode 100644 api/PM7/d6back.sh delete mode 100644 api/PM7/d6back.yaml delete mode 100644 api/PM7/decpm7.sh delete mode 100644 api/PM7/sync_geosector.sh delete mode 100644 app/assets/images/logo-geosector-512.png-autosave.kra delete mode 100755 app/geosector_app.iml rename app/{assets/images => store_assets}/geosector-1024x500.png (100%) rename app/{assets/images => store_assets}/geosector-admin-amicale-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-admin-tbord-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-user-carte-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-user-histo-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-user-login-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-user-stripe-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector-user-tbord-1800x1800.png (100%) rename app/{assets/images => store_assets}/geosector_map_admin.png (100%) create mode 100644 docs/planning-geosector-q1-2026.xls diff --git a/CLAUDE.md b/CLAUDE.md index c1b9cf1d..ec494bd0 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Web: `cd web && npm run dev` - run Svelte dev server - Web build: `cd web && npm run build` - build web app for production +## Post-modification checks (OBLIGATOIRE) +After modifying any code file, run the appropriate linter: +- Dart/Flutter: `cd app && flutter analyze [modified_files]` +- PHP: `php -l [modified_file]` (syntax check) + ## Code Style Guidelines - Flutter/Dart: Follow Flutter lint rules in analysis_options.yaml - Naming: camelCase for variables/methods, PascalCase for classes/enums diff --git a/VERSION b/VERSION index b7276283..1ac53bb4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.2 +3.6.3 \ No newline at end of file diff --git a/api/PM7/d6back.sh b/api/PM7/d6back.sh deleted file mode 100644 index 5379f790..00000000 --- a/api/PM7/d6back.sh +++ /dev/null @@ -1,651 +0,0 @@ -#!/bin/bash - -set -uo pipefail -# Note: Removed -e to allow script to continue on errors -# Errors are handled explicitly with ERROR_COUNT - -# Parse command line arguments -ONLY_DB=false -if [[ "${1:-}" == "-onlydb" ]]; then - ONLY_DB=true - echo "Mode: Database backup only" -fi - -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_FILE="$SCRIPT_DIR/d6back.yaml" -LOG_DIR="$SCRIPT_DIR/logs" -mkdir -p "$LOG_DIR" -LOG_FILE="$LOG_DIR/d6back-$(date +%Y%m%d).log" -ERROR_COUNT=0 -RECAP_FILE="/tmp/backup_recap_$$.txt" - -# Lock file to prevent concurrent executions -LOCK_FILE="/var/lock/d6back.lock" -exec 200>"$LOCK_FILE" -if ! flock -n 200; then - echo "ERROR: Another backup is already running" >&2 - exit 1 -fi -trap 'flock -u 200' EXIT - -# Clean old log files (keep only last 10) -find "$LOG_DIR" -maxdepth 1 -name "d6back-*.log" -type f 2>/dev/null | sort -r | tail -n +11 | xargs -r rm -f || true - -# Check dependencies - COMMENTED OUT -# for cmd in yq ssh tar openssl; do -# if ! command -v "$cmd" &> /dev/null; then -# echo "ERROR: $cmd is required but not installed" | tee -a "$LOG_FILE" -# exit 1 -# fi -# done - -# Load config -DIR_BACKUP=$(yq '.global.dir_backup' "$CONFIG_FILE" | tr -d '"') -ENC_KEY_PATH=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"') -BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"') -EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"') -KEEP_DIRS=$(yq '.global.keep_dirs' "$CONFIG_FILE" | tr -d '"') -KEEP_DB=$(yq '.global.keep_db' "$CONFIG_FILE" | tr -d '"') - -# Load encryption key -if [[ ! -f "$ENC_KEY_PATH" ]]; then - echo "ERROR: Encryption key not found: $ENC_KEY_PATH" | tee -a "$LOG_FILE" - exit 1 -fi -ENC_KEY=$(cat "$ENC_KEY_PATH") - -echo "=== Backup Started $(date) ===" | tee -a "$LOG_FILE" -echo "Backup directory: $DIR_BACKUP" | tee -a "$LOG_FILE" - -# Check available disk space -DISK_USAGE=$(df "$DIR_BACKUP" | tail -1 | awk '{print $5}' | sed 's/%//') -DISK_FREE=$((100 - DISK_USAGE)) - -if [[ $DISK_FREE -lt 20 ]]; then - echo "WARNING: Low disk space! Only ${DISK_FREE}% free on backup partition" | tee -a "$LOG_FILE" - - # Send warning email - echo "Sending DISK SPACE WARNING email to $EMAIL_TO (${DISK_FREE}% free)" | tee -a "$LOG_FILE" - if command -v msmtp &> /dev/null; then - { - echo "To: $EMAIL_TO" - echo "Subject: Backup${BACKUP_SERVER} WARNING - Low disk space (${DISK_FREE}% free)" - echo "" - echo "WARNING: Low disk space on $(hostname)" - echo "" - echo "Backup directory: $DIR_BACKUP" - echo "Disk usage: ${DISK_USAGE}%" - echo "Free space: ${DISK_FREE}%" - echo "" - echo "The backup will continue but please free up some space soon." - echo "" - echo "Date: $(date '+%d.%m.%Y %H:%M')" - } | msmtp "$EMAIL_TO" - echo "DISK SPACE WARNING email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE" - else - echo "WARNING: msmtp not found - DISK WARNING email NOT sent" | tee -a "$LOG_FILE" - fi -else - echo "Disk space OK: ${DISK_FREE}% free" | tee -a "$LOG_FILE" -fi - -# Initialize recap file -echo "BACKUP REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE" -echo "========================================" >> "$RECAP_FILE" -echo "" >> "$RECAP_FILE" - -# Function to format size in MB with thousand separator -format_size_mb() { - local file="$1" - if [[ -f "$file" ]]; then - local size_kb=$(du -k "$file" | cut -f1) - local size_mb=$((size_kb / 1024)) - # Add thousand separator with printf and sed - printf "%d" "$size_mb" | sed ':a;s/\B[0-9]\{3\}\>/\.&/;ta' - else - echo "0" - fi -} - -# Function to calculate age in days -get_age_days() { - local file="$1" - local now=$(date +%s) - local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0) - echo $(( (now - file_time) / 86400 )) -} - -# Function to get week number of year for a file -get_week_year() { - local file="$1" - local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0) - date -d "@$file_time" +"%Y-%W" -} - -# Function to cleanup old backups according to retention policy -cleanup_old_backups() { - local DELETED_COUNT=0 - local KEPT_COUNT=0 - - echo "" | tee -a "$LOG_FILE" - echo "=== Starting Backup Retention Cleanup ===" | tee -a "$LOG_FILE" - - # Parse retention periods - local KEEP_DIRS_DAYS=${KEEP_DIRS%d} # Remove 'd' suffix - - # Parse database retention (5d,3w,15m) - IFS=',' read -r KEEP_DB_DAILY KEEP_DB_WEEKLY KEEP_DB_MONTHLY <<< "$KEEP_DB" - local KEEP_DB_DAILY_DAYS=${KEEP_DB_DAILY%d} - local KEEP_DB_WEEKLY_WEEKS=${KEEP_DB_WEEKLY%w} - local KEEP_DB_MONTHLY_MONTHS=${KEEP_DB_MONTHLY%m} - - # Convert to days - local KEEP_DB_WEEKLY_DAYS=$((KEEP_DB_WEEKLY_WEEKS * 7)) - local KEEP_DB_MONTHLY_DAYS=$((KEEP_DB_MONTHLY_MONTHS * 30)) - - echo "Retention policy: dirs=${KEEP_DIRS_DAYS}d, db=${KEEP_DB_DAILY_DAYS}d/${KEEP_DB_WEEKLY_WEEKS}w/${KEEP_DB_MONTHLY_MONTHS}m" | tee -a "$LOG_FILE" - - # Process each host directory - for host_dir in "$DIR_BACKUP"/*; do - if [[ ! -d "$host_dir" ]]; then - continue - fi - - local host_name=$(basename "$host_dir") - echo " Cleaning host: $host_name" | tee -a "$LOG_FILE" - - # Clean directory backups (*.tar.gz but not *.sql.gz.enc) - while IFS= read -r -d '' file; do - if [[ $(basename "$file") == *".sql.gz.enc" ]]; then - continue # Skip SQL files - fi - - local age_days=$(get_age_days "$file") - - if [[ $age_days -gt $KEEP_DIRS_DAYS ]]; then - rm -f "$file" - echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DIRS_DAYS}d)" | tee -a "$LOG_FILE" - ((DELETED_COUNT++)) - else - ((KEPT_COUNT++)) - fi - done < <(find "$host_dir" -name "*.tar.gz" -type f -print0 2>/dev/null) - - # Clean database backups with retention policy - declare -A db_files - - while IFS= read -r -d '' file; do - local filename=$(basename "$file") - local db_name=${filename%%_*} - - if [[ -z "${db_files[$db_name]:-}" ]]; then - db_files[$db_name]="$file" - else - db_files[$db_name]+=$'\n'"$file" - fi - done < <(find "$host_dir" -name "*.sql.gz.enc" -type f -print0 2>/dev/null) - - # Process each database - for db_name in "${!db_files[@]}"; do - # Sort files by age (newest first) - mapfile -t files < <(echo "${db_files[$db_name]}" | while IFS= read -r f; do - echo "$f" - done | xargs -I {} stat -c "%Y {}" {} 2>/dev/null | sort -rn | cut -d' ' -f2-) - - # Track which files to keep - declare -A keep_daily - declare -A keep_weekly - - for file in "${files[@]}"; do - local age_days=$(get_age_days "$file") - - if [[ $age_days -le $KEEP_DB_DAILY_DAYS ]]; then - # Keep all files within daily retention - ((KEPT_COUNT++)) - - elif [[ $age_days -le $KEEP_DB_WEEKLY_DAYS ]]; then - # Weekly retention: keep one per day - local file_date=$(date -d "@$(stat -c %Y "$file")" +"%Y-%m-%d") - - if [[ -z "${keep_daily[$file_date]:-}" ]]; then - keep_daily[$file_date]="$file" - ((KEPT_COUNT++)) - else - rm -f "$file" - ((DELETED_COUNT++)) - fi - - elif [[ $age_days -le $KEEP_DB_MONTHLY_DAYS ]]; then - # Monthly retention: keep one per week - local week_year=$(get_week_year "$file") - - if [[ -z "${keep_weekly[$week_year]:-}" ]]; then - keep_weekly[$week_year]="$file" - ((KEPT_COUNT++)) - else - rm -f "$file" - ((DELETED_COUNT++)) - fi - - else - # Beyond retention period - rm -f "$file" - echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DB_MONTHLY_DAYS}d)" | tee -a "$LOG_FILE" - ((DELETED_COUNT++)) - fi - done - - unset keep_daily keep_weekly - done - - unset db_files - done - - echo "Cleanup completed: ${DELETED_COUNT} deleted, ${KEPT_COUNT} kept" | tee -a "$LOG_FILE" - - # Add cleanup summary to recap file - echo "" >> "$RECAP_FILE" - echo "CLEANUP SUMMARY:" >> "$RECAP_FILE" - echo " Files deleted: $DELETED_COUNT" >> "$RECAP_FILE" - echo " Files kept: $KEPT_COUNT" >> "$RECAP_FILE" -} - -# Function to backup a single database (must be defined before use) -backup_database() { - local database="$1" - local timestamp="$(date +%Y%m%d_%H)" - local backup_file="$backup_dir/sql/${database}_${timestamp}.sql.gz.enc" - - echo " Backing up database: $database" | tee -a "$LOG_FILE" - - if [[ "$ssh_user" != "root" ]]; then - CMD_PREFIX="sudo" - else - CMD_PREFIX="" - fi - - # Execute backup with encryption - # First test MySQL connection to get clear error messages (|| true to continue on error) - MYSQL_TEST=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF -[client] -user=$db_user -password=$db_pass -host=$db_host -EOF -chmod 600 /tmp/d6back.cnf -mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SELECT 1\" 2>&1 -rm -f /tmp/d6back.cnf'" 2>/dev/null || true) - - if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF -[client] -user=$db_user -password=$db_pass -host=$db_host -EOF -chmod 600 /tmp/d6back.cnf -mariadb-dump --defaults-extra-file=/tmp/d6back.cnf --single-transaction --lock-tables=false --add-drop-table --create-options --databases $database 2>/dev/null | sed -e \"/^CREATE DATABASE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" -e \"/^USE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" | gzip -rm -f /tmp/d6back.cnf'" | \ - openssl enc -aes-256-cbc -salt -pass pass:"$ENC_KEY" -pbkdf2 > "$backup_file" 2>/dev/null; then - - # Validate backup file size (encrypted SQL should be > 100 bytes) - if [[ -f "$backup_file" ]]; then - file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0) - if [[ $file_size -lt 100 ]]; then - # Analyze MySQL connection test results - if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then - echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE" - echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE" - elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then - echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE" - elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then - echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE" - else - echo " ERROR: Backup file too small (${file_size} bytes): $database on $host_name/$container_name" | tee -a "$LOG_FILE" - fi - - ((ERROR_COUNT++)) - rm -f "$backup_file" - else - size=$(du -h "$backup_file" | cut -f1) - size_mb=$(format_size_mb "$backup_file") - echo " ✓ Saved (encrypted): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE" - echo " SQL: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE" - - # Test backup integrity - if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$backup_file" | gunzip -t 2>/dev/null; then - echo " ERROR: Backup integrity check failed for $database" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - fi - else - echo " ERROR: Backup file not created: $database" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - else - # Analyze MySQL connection test for failed backup - if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then - echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE" - echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE" - elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then - echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE" - elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then - echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE" - else - echo " ERROR: Failed to backup database $database on $host_name/$container_name" | tee -a "$LOG_FILE" - fi - - ((ERROR_COUNT++)) - rm -f "$backup_file" - fi -} - -# Process each host -host_count=$(yq '.hosts | length' "$CONFIG_FILE") - -for ((i=0; i<$host_count; i++)); do - host_name=$(yq ".hosts[$i].name" "$CONFIG_FILE" | tr -d '"') - host_ip=$(yq ".hosts[$i].ip" "$CONFIG_FILE" | tr -d '"') - ssh_user=$(yq ".hosts[$i].user" "$CONFIG_FILE" | tr -d '"') - ssh_key=$(yq ".hosts[$i].key" "$CONFIG_FILE" | tr -d '"') - ssh_port=$(yq ".hosts[$i].port // 22" "$CONFIG_FILE" | tr -d '"') - - echo "Processing host: $host_name ($host_ip)" | tee -a "$LOG_FILE" - echo "" >> "$RECAP_FILE" - echo "HOST: $host_name ($host_ip)" >> "$RECAP_FILE" - echo "----------------------------" >> "$RECAP_FILE" - - # Test SSH connection - if ! ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 -o StrictHostKeyChecking=no "$ssh_user@$host_ip" "true" 2>/dev/null; then - echo " ERROR: Cannot connect to $host_name ($host_ip)" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - continue - fi - - # Process containers - container_count=$(yq ".hosts[$i].containers | length" "$CONFIG_FILE" 2>/dev/null || echo "0") - - for ((c=0; c<$container_count; c++)); do - container_name=$(yq ".hosts[$i].containers[$c].name" "$CONFIG_FILE" | tr -d '"') - - echo " Processing container: $container_name" | tee -a "$LOG_FILE" - - # Add container to recap - echo "" >> "$RECAP_FILE" - echo " Container: $container_name" >> "$RECAP_FILE" - - # Create backup directories - backup_dir="$DIR_BACKUP/$host_name/$container_name" - mkdir -p "$backup_dir" - mkdir -p "$backup_dir/sql" - - # Backup directories (skip if -onlydb mode) - if [[ "$ONLY_DB" == "false" ]]; then - dir_count=$(yq ".hosts[$i].containers[$c].dirs | length" "$CONFIG_FILE" 2>/dev/null || echo "0") - - for ((d=0; d<$dir_count; d++)); do - dir_path=$(yq ".hosts[$i].containers[$c].dirs[$d]" "$CONFIG_FILE" | sed 's/^"\|"$//g') - - # Use sudo if not root - if [[ "$ssh_user" != "root" ]]; then - CMD_PREFIX="sudo" - else - CMD_PREFIX="" - fi - - # Special handling for /var/www - backup each subdirectory separately - if [[ "$dir_path" == "/var/www" ]]; then - echo " Backing up subdirectories of $dir_path" | tee -a "$LOG_FILE" - - # Get list of subdirectories - subdirs=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "$CMD_PREFIX incus exec $container_name -- find /var/www -maxdepth 1 -type d ! -path /var/www" 2>/dev/null || echo "") - - for subdir in $subdirs; do - subdir_name=$(basename "$subdir" | tr '/' '_') - backup_file="$backup_dir/www_${subdir_name}_$(date +%Y%m%d_%H).tar.gz" - - echo " Backing up: $subdir" | tee -a "$LOG_FILE" - - if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "$CMD_PREFIX incus exec $container_name -- tar czf - $subdir 2>/dev/null" > "$backup_file"; then - - # Validate backup file size (tar.gz should be > 1KB) - if [[ -f "$backup_file" ]]; then - file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0) - if [[ $file_size -lt 1024 ]]; then - echo " WARNING: Backup file very small (${file_size} bytes): $subdir" | tee -a "$LOG_FILE" - # Keep the file but note it's small - size=$(du -h "$backup_file" | cut -f1) - size_mb=$(format_size_mb "$backup_file") - echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE" - echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE" - else - size=$(du -h "$backup_file" | cut -f1) - size_mb=$(format_size_mb "$backup_file") - echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE" - echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE" - fi - - # Test tar integrity - if ! tar tzf "$backup_file" >/dev/null 2>&1; then - echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - else - echo " ERROR: Backup file not created: $subdir" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - else - echo " ERROR: Failed to backup $subdir" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - rm -f "$backup_file" - fi - done - else - # Normal backup for other directories - dir_name=$(basename "$dir_path" | tr '/' '_') - backup_file="$backup_dir/${dir_name}_$(date +%Y%m%d_%H).tar.gz" - - echo " Backing up: $dir_path" | tee -a "$LOG_FILE" - - if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "$CMD_PREFIX incus exec $container_name -- tar czf - $dir_path 2>/dev/null" > "$backup_file"; then - - # Validate backup file size (tar.gz should be > 1KB) - if [[ -f "$backup_file" ]]; then - file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0) - if [[ $file_size -lt 1024 ]]; then - echo " WARNING: Backup file very small (${file_size} bytes): $dir_path" | tee -a "$LOG_FILE" - # Keep the file but note it's small - size=$(du -h "$backup_file" | cut -f1) - size_mb=$(format_size_mb "$backup_file") - echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE" - echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE" - else - size=$(du -h "$backup_file" | cut -f1) - size_mb=$(format_size_mb "$backup_file") - echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE" - echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE" - fi - - # Test tar integrity - if ! tar tzf "$backup_file" >/dev/null 2>&1; then - echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - else - echo " ERROR: Backup file not created: $dir_path" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - fi - else - echo " ERROR: Failed to backup $dir_path" | tee -a "$LOG_FILE" - ((ERROR_COUNT++)) - rm -f "$backup_file" - fi - fi - done - fi # End of directory backup section - - # Backup databases - db_user=$(yq ".hosts[$i].containers[$c].db_user" "$CONFIG_FILE" 2>/dev/null | tr -d '"') - db_pass=$(yq ".hosts[$i].containers[$c].db_pass" "$CONFIG_FILE" 2>/dev/null | tr -d '"') - db_host=$(yq ".hosts[$i].containers[$c].db_host // \"localhost\"" "$CONFIG_FILE" 2>/dev/null | tr -d '"') - - # Check if we're in onlydb mode - if [[ "$ONLY_DB" == "true" ]]; then - # Use onlydb list if it exists - onlydb_count=$(yq ".hosts[$i].containers[$c].onlydb | length" "$CONFIG_FILE" 2>/dev/null || echo "0") - if [[ "$onlydb_count" != "0" ]] && [[ "$onlydb_count" != "null" ]]; then - db_count="$onlydb_count" - use_onlydb=true - else - # No onlydb list, skip this container in onlydb mode - continue - fi - else - # Normal mode - use databases list - db_count=$(yq ".hosts[$i].containers[$c].databases | length" "$CONFIG_FILE" 2>/dev/null || echo "0") - use_onlydb=false - fi - - if [[ -n "$db_user" ]] && [[ -n "$db_pass" ]] && [[ "$db_count" != "0" ]]; then - for ((db=0; db<$db_count; db++)); do - if [[ "$use_onlydb" == "true" ]]; then - db_name=$(yq ".hosts[$i].containers[$c].onlydb[$db]" "$CONFIG_FILE" | tr -d '"') - else - db_name=$(yq ".hosts[$i].containers[$c].databases[$db]" "$CONFIG_FILE" | tr -d '"') - fi - - if [[ "$db_name" == "ALL" ]]; then - echo " Fetching all databases..." | tee -a "$LOG_FILE" - - # Get database list - if [[ "$ssh_user" != "root" ]]; then - db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "sudo incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF -[client] -user=$db_user -password=$db_pass -host=$db_host -EOF -chmod 600 /tmp/d6back.cnf -mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null -rm -f /tmp/d6back.cnf'" | \ - grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "") - else - db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \ - "incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF -[client] -user=$db_user -password=$db_pass -host=$db_host -EOF -chmod 600 /tmp/d6back.cnf -mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null -rm -f /tmp/d6back.cnf'" | \ - grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "") - fi - - # Backup each database - for single_db in $db_list; do - backup_database "$single_db" - done - else - backup_database "$db_name" - fi - done - fi - done -done - -echo "=== Backup Completed $(date) ===" | tee -a "$LOG_FILE" - -# Cleanup old backups according to retention policy -cleanup_old_backups - -# Show summary -total_size=$(du -sh "$DIR_BACKUP" 2>/dev/null | cut -f1) -echo "Total backup size: $total_size" | tee -a "$LOG_FILE" - -# Add summary to recap -echo "" >> "$RECAP_FILE" -echo "========================================" >> "$RECAP_FILE" - -# Add size details per host/container -echo "BACKUP SIZES:" >> "$RECAP_FILE" -for host_dir in "$DIR_BACKUP"/*; do - if [[ -d "$host_dir" ]]; then - host_name=$(basename "$host_dir") - host_size=$(du -sh "$host_dir" 2>/dev/null | cut -f1) - echo "" >> "$RECAP_FILE" - echo " $host_name: $host_size" >> "$RECAP_FILE" - - # Size per container - for container_dir in "$host_dir"/*; do - if [[ -d "$container_dir" ]]; then - container_name=$(basename "$container_dir") - container_size=$(du -sh "$container_dir" 2>/dev/null | cut -f1) - echo " - $container_name: $container_size" >> "$RECAP_FILE" - fi - done - fi -done - -echo "" >> "$RECAP_FILE" -echo "TOTAL SIZE: $total_size" >> "$RECAP_FILE" -echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE" - -# Prepare email subject with date format -DATE_SUBJECT=$(date '+%d.%m.%Y %H') - -# Send recap email -if [[ $ERROR_COUNT -gt 0 ]]; then - echo "Total errors: $ERROR_COUNT" | tee -a "$LOG_FILE" - - # Add errors to recap - echo "" >> "$RECAP_FILE" - echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE" - echo "----------------------------" >> "$RECAP_FILE" - grep -i "ERROR" "$LOG_FILE" >> "$RECAP_FILE" - - # Send email with ERROR in subject - echo "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" | tee -a "$LOG_FILE" - if command -v msmtp &> /dev/null; then - { - echo "To: $EMAIL_TO" - echo "Subject: Backup${BACKUP_SERVER} ERROR $DATE_SUBJECT" - echo "" - cat "$RECAP_FILE" - } | msmtp "$EMAIL_TO" - echo "ERROR email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE" - else - echo "WARNING: msmtp not found - ERROR email NOT sent" | tee -a "$LOG_FILE" - fi -else - echo "Backup completed successfully with no errors" | tee -a "$LOG_FILE" - - # Send success recap email - echo "Sending SUCCESS recap email to $EMAIL_TO" | tee -a "$LOG_FILE" - if command -v msmtp &> /dev/null; then - { - echo "To: $EMAIL_TO" - echo "Subject: Backup${BACKUP_SERVER} $DATE_SUBJECT" - echo "" - cat "$RECAP_FILE" - } | msmtp "$EMAIL_TO" - echo "SUCCESS recap email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE" - else - echo "WARNING: msmtp not found - SUCCESS recap email NOT sent" | tee -a "$LOG_FILE" - fi -fi - -# Clean up recap file -rm -f "$RECAP_FILE" - -# Exit with error code if there were errors -if [[ $ERROR_COUNT -gt 0 ]]; then - exit 1 -fi diff --git a/api/PM7/d6back.yaml b/api/PM7/d6back.yaml deleted file mode 100644 index 53c933cf..00000000 --- a/api/PM7/d6back.yaml +++ /dev/null @@ -1,112 +0,0 @@ -# Configuration for MariaDB and directories backup -# Backup structure: $dir_backup/$hostname/$containername/ for dirs -# $dir_backup/$hostname/$containername/sql/ for databases - -# Global parameters -global: - backup_server: PM7 # Nom du serveur de backup (PM7, PM1, etc.) - email_to: support@unikoffice.com # Email de notification - dir_backup: /var/pierre/back # Base backup directory - enc_key: /home/pierre/.key_enc # Encryption key for SQL backups - keep_dirs: 7d # Garde 7 jours pour les dirs - keep_db: 5d,3w,15m # 5 jours complets, 3 semaines (1/jour), 15 mois (1/semaine) - -# Hosts configuration -hosts: - - name: IN2 - ip: 145.239.9.105 - user: debian - key: /home/pierre/.ssh/backup_key - port: 22 - dirs: - - /etc/nginx - containers: - - name: nx4 - db_user: root - db_pass: MyDebServer,90b - db_host: localhost - dirs: - - /etc/nginx - - /var/www - databases: - - ALL # Backup all databases - onlydb: # Used only with -onlydb parameter (optional) - - turing - - - name: IN3 - ip: 195.154.80.116 - user: pierre - key: /home/pierre/.ssh/backup_key - port: 22 - dirs: - - /etc/nginx - containers: - - name: nx4 - db_user: root - db_pass: MyAlpLocal,90b - db_host: localhost - dirs: - - /etc/nginx - - /var/www - databases: - - ALL # Backup all databases - onlydb: # Used only with -onlydb parameter (optional) - - geosector - - - name: rca-geo - dirs: - - /etc/nginx - - /var/www - - - name: dva-res - db_user: root - db_pass: MyAlpineDb.90b - db_host: localhost - dirs: - - /etc/nginx - - /var/www - databases: - - ALL - onlydb: - - resalice - - - name: dva-front - dirs: - - /etc/nginx - - /var/www - - - name: maria3 - db_user: root - db_pass: MyAlpLocal,90b - db_host: localhost - dirs: - - /etc/my.cnf.d - - /var/osm - - /var/log - databases: - - ALL - onlydb: - - cleo - - rca_geo - - - name: IN4 - ip: 51.159.7.190 - user: pierre - key: /home/pierre/.ssh/backup_key - port: 22 - dirs: - - /etc/nginx - containers: - - name: maria4 - db_user: root - db_pass: MyAlpLocal,90b - db_host: localhost - dirs: - - /etc/my.cnf.d - - /var/osm - - /var/log - databases: - - ALL - onlydb: - - cleo - - pra_geo diff --git a/api/PM7/decpm7.sh b/api/PM7/decpm7.sh deleted file mode 100644 index 9299ae48..00000000 --- a/api/PM7/decpm7.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -CONFIG_FILE="backpm7.yaml" - -# Check if file argument is provided -if [ $# -eq 0 ]; then - echo -e "${RED}Error: No input file specified${NC}" - echo "Usage: $0 " - echo "Example: $0 wordpress_20250905_14.sql.gz.enc" - exit 1 -fi - -INPUT_FILE="$1" - -# Check if input file exists -if [ ! -f "$INPUT_FILE" ]; then - echo -e "${RED}Error: File not found: $INPUT_FILE${NC}" - exit 1 -fi - -# Function to load encryption key from config -load_key_from_config() { - if [ ! -f "$CONFIG_FILE" ]; then - echo -e "${YELLOW}Warning: $CONFIG_FILE not found${NC}" - return 1 - fi - - # Check for yq - if ! command -v yq &> /dev/null; then - echo -e "${RED}Error: yq is required to read config file${NC}" - echo "Install with: sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq" - return 1 - fi - - local key_path=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"') - - if [ -z "$key_path" ]; then - echo -e "${RED}Error: enc_key not found in $CONFIG_FILE${NC}" - return 1 - fi - - if [ ! -f "$key_path" ]; then - echo -e "${RED}Error: Encryption key file not found: $key_path${NC}" - return 1 - fi - - ENC_KEY=$(cat "$key_path") - echo -e "${GREEN}Encryption key loaded from: $key_path${NC}" - return 0 -} - -# Check file type early - accept both old and new naming -if [[ "$INPUT_FILE" != *.sql.gz.enc ]] && [[ "$INPUT_FILE" != *.sql.tar.gz.enc ]]; then - echo -e "${RED}Error: File must be a .sql.gz.enc or .sql.tar.gz.enc file${NC}" - echo "This tool only decrypts SQL backup files created by backpm7.sh" - exit 1 -fi - -# Get encryption key from config -if ! load_key_from_config; then - echo -e "${RED}Error: Cannot load encryption key${NC}" - echo "Make sure $CONFIG_FILE exists and contains enc_key path" - exit 1 -fi - -# Process SQL backup file -echo -e "${BLUE}Decrypting SQL backup: $INPUT_FILE${NC}" - -# Determine output file - extract just the filename and put in current directory -BASENAME=$(basename "$INPUT_FILE") -if [[ "$BASENAME" == *.sql.tar.gz.enc ]]; then - OUTPUT_FILE="${BASENAME%.sql.tar.gz.enc}.sql" -else - OUTPUT_FILE="${BASENAME%.sql.gz.enc}.sql" -fi - -# Decrypt and decompress in one command -echo "Decrypting to: $OUTPUT_FILE" - -# Decrypt and decompress in one pipeline -if openssl enc -aes-256-cbc -d -salt -pass pass:"$ENC_KEY" -pbkdf2 -in "$INPUT_FILE" | gunzip > "$OUTPUT_FILE" 2>/dev/null; then - # Get file size - size=$(du -h "$OUTPUT_FILE" | cut -f1) - echo -e "${GREEN}✓ Successfully decrypted: $OUTPUT_FILE ($size)${NC}" - - # Show first few lines of SQL - echo -e "${BLUE}First 5 lines of SQL:${NC}" - head -n 5 "$OUTPUT_FILE" -else - echo -e "${RED}✗ Decryption failed${NC}" - echo "Possible causes:" - echo " - Wrong encryption key" - echo " - Corrupted file" - echo " - File was encrypted differently" - - # Try to help debug - echo -e "\n${YELLOW}Debug info:${NC}" - echo "File size: $(du -h "$INPUT_FILE" | cut -f1)" - echo "First bytes (should start with 'Salted__'):" - hexdump -C "$INPUT_FILE" | head -n 1 - - # Let's also check what key we're using (first 10 chars) - echo "Key begins with: ${ENC_KEY:0:10}..." - - exit 1 -fi - -echo -e "${GREEN}Operation completed successfully${NC}" \ No newline at end of file diff --git a/api/PM7/sync_geosector.sh b/api/PM7/sync_geosector.sh deleted file mode 100644 index fb7ef718..00000000 --- a/api/PM7/sync_geosector.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/bash -# -# sync_geosector.sh - Synchronise les backups geosector depuis PM7 vers maria3 (IN3) et maria4 (IN4) -# -# Ce script : -# 1. Trouve le dernier backup chiffré de geosector sur PM7 -# 2. Le déchiffre et décompresse localement -# 3. Le transfère et l'importe dans IN3/maria3/geosector -# 4. Le transfère et l'importe dans IN4/maria4/geosector -# -# Installation: /var/pierre/bat/sync_geosector.sh -# Usage: ./sync_geosector.sh [--force] [--date YYYYMMDD_HH] -# - -set -uo pipefail -# Note: Removed -e to allow script to continue on sync errors -# Errors are handled explicitly with ERROR_COUNT - -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_FILE="$SCRIPT_DIR/d6back.yaml" -BACKUP_DIR="/var/pierre/back/IN3/nx4/sql" -ENC_KEY_FILE="/home/pierre/.key_enc" -SSH_KEY="/home/pierre/.ssh/backup_key" -TEMP_DIR="/tmp/geosector_sync" -LOG_FILE="/var/pierre/bat/logs/sync_geosector.log" -RECAP_FILE="/tmp/sync_geosector_recap_$$.txt" - -# Load email config from d6back.yaml -if [[ -f "$CONFIG_FILE" ]]; then - EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"') - BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"') -else - EMAIL_TO="support@unikoffice.com" - BACKUP_SERVER="BACKUP" -fi - -# Serveurs cibles -IN3_HOST="195.154.80.116" -IN3_USER="pierre" -IN3_CONTAINER="maria3" - -IN4_HOST="51.159.7.190" -IN4_USER="pierre" -IN4_CONTAINER="maria4" - -# Credentials MariaDB -DB_USER="root" -IN3_DB_PASS="MyAlpLocal,90b" # maria3 -IN4_DB_PASS="MyAlpLocal,90b" # maria4 -DB_NAME="geosector" - -# Fonctions utilitaires -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" -} - -error() { - log "ERROR: $*" - exit 1 -} - -cleanup() { - if [[ -d "$TEMP_DIR" ]]; then - log "Nettoyage de $TEMP_DIR" - rm -rf "$TEMP_DIR" - fi - rm -f "$RECAP_FILE" -} - -trap cleanup EXIT - -# Lecture de la clé de chiffrement -if [[ ! -f "$ENC_KEY_FILE" ]]; then - error "Clé de chiffrement non trouvée: $ENC_KEY_FILE" -fi -ENC_KEY=$(cat "$ENC_KEY_FILE") - -# Parsing des arguments -FORCE=0 -SPECIFIC_DATE="" - -while [[ $# -gt 0 ]]; do - case $1 in - --force) - FORCE=1 - shift - ;; - --date) - SPECIFIC_DATE="$2" - shift 2 - ;; - *) - echo "Usage: $0 [--force] [--date YYYYMMDD_HH]" - exit 1 - ;; - esac -done - -# Trouver le fichier backup -if [[ -n "$SPECIFIC_DATE" ]]; then - BACKUP_FILE="$BACKUP_DIR/geosector_${SPECIFIC_DATE}.sql.gz.enc" - if [[ ! -f "$BACKUP_FILE" ]]; then - error "Backup non trouvé: $BACKUP_FILE" - fi -else - # Chercher le plus récent - BACKUP_FILE=$(find "$BACKUP_DIR" -name "geosector_*.sql.gz.enc" -type f -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-) - if [[ -z "$BACKUP_FILE" ]]; then - error "Aucun backup geosector trouvé dans $BACKUP_DIR" - fi -fi - -BACKUP_BASENAME=$(basename "$BACKUP_FILE") -log "Backup sélectionné: $BACKUP_BASENAME" - -# Initialiser le fichier récapitulatif -echo "SYNC GEOSECTOR REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE" -echo "========================================" >> "$RECAP_FILE" -echo "" >> "$RECAP_FILE" -echo "Backup source: $BACKUP_BASENAME" >> "$RECAP_FILE" -echo "" >> "$RECAP_FILE" - -# Créer le répertoire temporaire -mkdir -p "$TEMP_DIR" -DECRYPTED_FILE="$TEMP_DIR/geosector.sql" - -# Étape 1: Déchiffrer et décompresser -log "Déchiffrement et décompression du backup..." -if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$BACKUP_FILE" | gunzip > "$DECRYPTED_FILE"; then - error "Échec du déchiffrement/décompression" -fi - -FILE_SIZE=$(du -h "$DECRYPTED_FILE" | cut -f1) -log "Fichier SQL déchiffré: $FILE_SIZE" - -echo "Decrypted SQL size: $FILE_SIZE" >> "$RECAP_FILE" -echo "" >> "$RECAP_FILE" - -# Compteur d'erreurs -ERROR_COUNT=0 - -# Fonction pour synchroniser vers un serveur -sync_to_server() { - local HOST=$1 - local USER=$2 - local CONTAINER=$3 - local DB_PASS=$4 - local SERVER_NAME=$5 - - log "=== Synchronisation vers $SERVER_NAME ($HOST) ===" - echo "TARGET: $SERVER_NAME ($HOST/$CONTAINER)" >> "$RECAP_FILE" - - # Test de connexion SSH - if ! ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$USER@$HOST" "echo 'SSH OK'" &>/dev/null; then - log "ERROR: Impossible de se connecter à $HOST via SSH" - echo " ✗ SSH connection FAILED" >> "$RECAP_FILE" - ((ERROR_COUNT++)) - return 1 - fi - - # Import dans MariaDB - log "Import dans $SERVER_NAME/$CONTAINER/geosector..." - - # Drop et recréer la base sur le serveur distant - if ! ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' -e 'DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"; then - log "ERROR: Échec de la création de la base sur $SERVER_NAME" - echo " ✗ Database creation FAILED" >> "$RECAP_FILE" - ((ERROR_COUNT++)) - return 1 - fi - - # Filtrer et importer le SQL (enlever CREATE DATABASE et USE avec timestamp) - log "Filtrage et import du SQL..." - if ! sed -e '/^CREATE DATABASE.*geosector_[0-9]/d' \ - -e '/^USE.*geosector_[0-9]/d' \ - "$DECRYPTED_FILE" | \ - ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' $DB_NAME"; then - log "ERROR: Échec de l'import sur $SERVER_NAME" - echo " ✗ SQL import FAILED" >> "$RECAP_FILE" - ((ERROR_COUNT++)) - return 1 - fi - - log "$SERVER_NAME: Import réussi" - echo " ✓ Import SUCCESS" >> "$RECAP_FILE" - echo "" >> "$RECAP_FILE" -} - -# Synchronisation vers IN3/maria3 -sync_to_server "$IN3_HOST" "$IN3_USER" "$IN3_CONTAINER" "$IN3_DB_PASS" "IN3/maria3" - -# Synchronisation vers IN4/maria4 -sync_to_server "$IN4_HOST" "$IN4_USER" "$IN4_CONTAINER" "$IN4_DB_PASS" "IN4/maria4" - -# Finaliser le récapitulatif -echo "========================================" >> "$RECAP_FILE" -echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE" - -# Préparer le sujet email avec date -DATE_SUBJECT=$(date '+%d.%m.%Y %H') - -# Envoyer l'email récapitulatif -if [[ $ERROR_COUNT -gt 0 ]]; then - log "Total errors: $ERROR_COUNT" - - # Ajouter les erreurs au récap - echo "" >> "$RECAP_FILE" - echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE" - echo "----------------------------" >> "$RECAP_FILE" - grep -i "ERROR" "$LOG_FILE" | tail -20 >> "$RECAP_FILE" - - # Envoyer email avec ERROR dans le sujet - log "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" - if command -v msmtp &> /dev/null; then - { - echo "To: $EMAIL_TO" - echo "Subject: Sync${BACKUP_SERVER} ERROR $DATE_SUBJECT" - echo "" - cat "$RECAP_FILE" - } | msmtp "$EMAIL_TO" - log "ERROR email sent successfully to $EMAIL_TO" - else - log "WARNING: msmtp not found - ERROR email NOT sent" - fi - - log "=== Synchronisation terminée avec des erreurs ===" - exit 1 -else - log "=== Synchronisation terminée avec succès ===" - log "Les bases geosector sur maria3 et maria4 sont à jour avec le backup $BACKUP_BASENAME" - - # Envoyer email de succès - log "Sending SUCCESS recap email to $EMAIL_TO" - if command -v msmtp &> /dev/null; then - { - echo "To: $EMAIL_TO" - echo "Subject: Sync${BACKUP_SERVER} $DATE_SUBJECT" - echo "" - cat "$RECAP_FILE" - } | msmtp "$EMAIL_TO" - log "SUCCESS recap email sent successfully to $EMAIL_TO" - else - log "WARNING: msmtp not found - SUCCESS recap email NOT sent" - fi - - exit 0 -fi diff --git a/api/deploy-api.sh b/api/deploy-api.sh index 1858df27..c2642094 100755 --- a/api/deploy-api.sh +++ b/api/deploy-api.sh @@ -179,6 +179,14 @@ if [ "$SOURCE_TYPE" = "local_code" ]; then --exclude='*.swp' \ --exclude='*.swo' \ --exclude='*~' \ + --exclude='docs/*.geojson' \ + --exclude='docs/*.sql' \ + --exclude='docs/*.pdf' \ + --exclude='composer.phar' \ + --exclude='scripts/migration*' \ + --exclude='scripts/php' \ + --exclude='CLAUDE.md' \ + --exclude='TODO-API.md' \ -czf "${ARCHIVE_PATH}" . 2>/dev/null || echo_error "Failed to create archive" echo_info "Archive created: ${ARCHIVE_PATH}" @@ -198,6 +206,16 @@ elif [ "$SOURCE_TYPE" = "remote_container" ]; then --exclude='uploads' \ --exclude='sessions' \ --exclude='opendata' \ + --exclude='docs/*.geojson' \ + --exclude='docs/*.sql' \ + --exclude='docs/*.pdf' \ + --exclude='composer.phar' \ + --exclude='scripts/migration*' \ + --exclude='scripts/php' \ + --exclude='CLAUDE.md' \ + --exclude='TODO-API.md' \ + --exclude='*.tar.gz' \ + --exclude='vendor' \ -czf /tmp/${ARCHIVE_NAME} -C ${API_PATH} . " || echo_error "Failed to create archive on remote" diff --git a/app/.dart_tool/package_config.json b/app/.dart_tool/package_config.json index 3989fe6a..08c8aa77 100644 --- a/app/.dart_tool/package_config.json +++ b/app/.dart_tool/package_config.json @@ -3,7 +3,7 @@ "packages": [ { "name": "_fe_analyzer_shared", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/_fe_analyzer_shared-72.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-72.0.0", "packageUri": "lib/", "languageVersion": "3.3" }, @@ -15,295 +15,295 @@ }, { "name": "analyzer", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/analyzer-6.7.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/analyzer-6.7.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "archive", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/archive-4.0.7", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/archive-4.0.7", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "args", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/args-2.7.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/args-2.7.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "async", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/async-2.11.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/async-2.11.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "battery_plus", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "battery_plus_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus_platform_interface-2.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/battery_plus_platform_interface-2.0.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "boolean_selector", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/boolean_selector-2.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "build", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build-2.4.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build-2.4.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "build_config", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_config-1.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_config-1.1.1", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "build_daemon", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_daemon-4.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_daemon-4.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "build_resolvers", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_resolvers-2.4.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_resolvers-2.4.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "build_runner", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_runner-2.4.13", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner-2.4.13", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "build_runner_core", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_runner_core-7.3.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner_core-7.3.2", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "built_collection", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/built_collection-5.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_collection-5.1.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "built_value", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/built_value-8.12.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.12.3", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "characters", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/characters-1.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/characters-1.3.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "charcode", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/charcode-1.4.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/charcode-1.4.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "checked_yaml", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/checked_yaml-2.0.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/checked_yaml-2.0.3", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "class_to_string", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/class_to_string-1.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/class_to_string-1.1.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "cli_util", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cli_util-0.4.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cli_util-0.4.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "clock", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/clock-1.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/clock-1.1.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "code_builder", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/code_builder-4.10.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/code_builder-4.10.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "collection", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/collection-1.18.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.18.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "connectivity_plus", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "connectivity_plus_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "convert", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/convert-3.1.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/convert-3.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "cookie_jar", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cookie_jar-4.0.8", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cookie_jar-4.0.8", "packageUri": "lib/", "languageVersion": "2.15" }, { "name": "cross_file", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cross_file-0.3.4+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "crypto", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/crypto-3.0.7", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.7", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "csslib", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/csslib-1.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/csslib-1.0.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "cupertino_icons", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cupertino_icons-1.0.8", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "dart_earcut", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dart_earcut-1.2.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dart_earcut-1.2.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "dart_style", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dart_style-2.3.7", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dart_style-2.3.7", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "dbus", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dbus-0.7.11", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dbus-0.7.11", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "device_info_plus", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "device_info_plus_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus_platform_interface-7.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/device_info_plus_platform_interface-7.0.2", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "dio", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio-5.9.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "dio_cache_interceptor", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cache_interceptor-3.5.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-3.5.1", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "dio_cache_interceptor_hive_store", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cache_interceptor_hive_store-3.2.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor_hive_store-3.2.2", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "dio_cookie_manager", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cookie_manager-3.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cookie_manager-3.3.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "dio_web_adapter", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_web_adapter-2.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_web_adapter-2.1.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "fake_async", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/fake_async-1.3.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "ffi", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/ffi-2.1.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/ffi-2.1.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file-7.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file-7.0.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "file_selector_linux", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file_selector_macos", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_macos-0.9.4+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file_selector_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_platform_interface-2.6.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "file_selector_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "fixnum", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/fixnum-1.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, @@ -313,39 +313,45 @@ "packageUri": "lib/", "languageVersion": "3.3" }, + { + "name": "flutter_compass", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_compass-0.8.1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "flutter_launcher_icons", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_launcher_icons-0.14.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_launcher_icons-0.14.4", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_lints", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_lints-5.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_lints-5.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "flutter_local_notifications", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_linux", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_platform_interface-9.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_platform_interface-9.1.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3", "packageUri": "lib/", "languageVersion": "3.4" }, @@ -357,31 +363,31 @@ }, { "name": "flutter_map", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_map-7.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-7.0.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_map_cache", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_map_cache-1.5.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map_cache-1.5.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_plugin_android_lifecycle", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "flutter_stripe", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_stripe-11.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_stripe-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_svg", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_svg-2.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.1.0", "packageUri": "lib/", "languageVersion": "3.4" }, @@ -399,487 +405,487 @@ }, { "name": "freezed_annotation", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/freezed_annotation-2.4.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/freezed_annotation-2.4.4", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "frontend_server_client", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/frontend_server_client-4.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "geolocator", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator-13.0.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator-13.0.4", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_android-4.6.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-4.6.1", "packageUri": "lib/", "languageVersion": "2.15" }, { "name": "geolocator_apple", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_platform_interface-4.2.6", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_platform_interface-4.2.6", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_web", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_web-4.1.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "glob", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/glob-2.1.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "go_router", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/go_router-15.1.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-15.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "google_fonts", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/google_fonts-6.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/google_fonts-6.3.0", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "graphs", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/graphs-2.3.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/graphs-2.3.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "hive", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive-2.2.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "hive_flutter", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive_flutter-1.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "hive_generator", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive_generator-2.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive_generator-2.0.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "html", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/html-0.15.6", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/html-0.15.6", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "http", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http-1.6.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.6.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "http_multi_server", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http_multi_server-3.2.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "http_parser", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http_parser-4.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.0.2", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "image", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image-4.7.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.7.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "image_picker", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker-0.8.9", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-0.8.9", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "image_picker_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_android-0.8.12+21", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+21", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "image_picker_for_web", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_for_web-2.1.12", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-2.1.12", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "image_picker_ios", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_ios-0.8.12+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_linux", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_macos", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_macos-0.2.1+2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_platform_interface-2.10.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "intl", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/intl-0.19.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.19.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "io", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/io-1.0.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/io-1.0.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "js", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/js-0.7.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/js-0.7.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "json_annotation", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/json_annotation-4.9.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/json_annotation-4.9.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "latlong2", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/latlong2-0.9.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/latlong2-0.9.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "leak_tracker", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker-10.0.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker-10.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_flutter_testing", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_testing", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker_testing-3.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "lints", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/lints-5.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/lints-5.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "lists", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/lists-1.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/lists-1.0.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "logger", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/logger-2.6.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/logger-2.6.2", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "logging", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/logging-1.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/logging-1.3.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "macros", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/macros-0.1.2-main.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/macros-0.1.2-main.4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "matcher", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/matcher-0.12.16+1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/matcher-0.12.16+1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "material_color_utilities", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/material_color_utilities-0.11.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "mek_data_class", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_data_class-1.4.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mek_data_class-1.4.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "mek_stripe_terminal", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "meta", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/meta-1.15.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/meta-1.15.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "mgrs_dart", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mgrs_dart-2.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "mime", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mime-2.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "nfc_manager", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "nm", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nm-0.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "one_for_all", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/one_for_all-1.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/one_for_all-1.1.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "package_config", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/package_config-2.2.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/package_config-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path-1.9.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "path_parsing", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_parsing-1.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_parsing-1.1.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "path_provider", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider-2.1.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider-2.1.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path_provider_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_android-2.2.15", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "path_provider_foundation", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "path_provider_linux", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "path_provider_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_platform_interface-2.1.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "path_provider_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "permission_handler", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler-12.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler-12.0.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_android-13.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_apple", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_apple-9.4.7", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.7", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "permission_handler_html", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_html-0.1.3+5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "permission_handler_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_platform_interface-4.3.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_platform_interface-4.3.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "petitparser", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/petitparser-6.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.0.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "platform", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/platform-3.1.6", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/platform-3.1.6", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "plugin_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/plugin_platform_interface-2.1.8", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "polylabel", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/polylabel-1.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/polylabel-1.0.1", "packageUri": "lib/", "languageVersion": "2.13" }, { "name": "pool", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pool-1.5.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pool-1.5.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "posix", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/posix-6.0.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/posix-6.0.3", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "proj4dart", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/proj4dart-2.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "pub_semver", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pub_semver-2.2.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "pubspec_parse", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pubspec_parse-1.4.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pubspec_parse-1.4.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "qr", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/qr-3.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/qr-3.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "qr_flutter", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/qr_flutter-4.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/qr_flutter-4.1.0", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "recase", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/recase-4.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/recase-4.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "retry", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/retry-3.1.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "shelf", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/shelf-1.4.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shelf-1.4.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "shelf_web_socket", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/shelf_web_socket-2.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shelf_web_socket-2.0.1", "packageUri": "lib/", "languageVersion": "3.3" }, @@ -891,271 +897,271 @@ }, { "name": "source_gen", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_gen-1.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_gen-1.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "source_helper", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_helper-1.3.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_helper-1.3.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "source_span", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_span-1.10.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stack_trace", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stack_trace-1.11.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stack_trace-1.11.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stream_channel", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stream_channel-2.1.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stream_channel-2.1.2", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "stream_transform", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stream_transform-2.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stream_transform-2.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "string_scanner", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/string_scanner-1.2.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.2.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stripe_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_android-11.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_android-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "stripe_ios", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_ios-11.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_ios-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "stripe_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_platform_interface-11.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_platform_interface-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "syncfusion_flutter_charts", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/syncfusion_flutter_charts-27.2.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-27.2.5", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "syncfusion_flutter_core", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/syncfusion_flutter_core-27.2.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-27.2.5", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "term_glyph", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/term_glyph-1.2.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "test_api", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/test_api-0.7.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/test_api-0.7.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "timezone", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/timezone-0.10.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/timezone-0.10.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "timing", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/timing-1.0.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/timing-1.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "typed_data", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/typed_data-1.4.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/typed_data-1.4.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "unicode", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/unicode-0.3.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/unicode-0.3.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "universal_html", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/universal_html-2.2.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/universal_html-2.2.4", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "universal_io", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/universal_io-2.2.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/universal_io-2.2.2", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "upower", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/upower-0.7.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/upower-0.7.0", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "url_launcher", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher-6.3.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher-6.3.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_android", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_android-6.3.14", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "url_launcher_ios", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_ios-6.3.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "url_launcher_linux", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_macos", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_macos-3.2.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_platform_interface", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_platform_interface-2.3.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "url_launcher_web", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_web-2.3.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_windows", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "uuid", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/uuid-4.5.2", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "vector_graphics", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics-1.1.18", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.18", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_graphics_codec", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics_codec-1.1.13", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_graphics_compiler", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics_compiler-1.1.16", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.16", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_math", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_math-2.1.4", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "vm_service", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vm_service-14.2.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vm_service-14.2.5", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "watcher", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/watcher-1.2.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/watcher-1.2.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web-1.1.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web_socket-1.0.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web_socket-1.0.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket_channel", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web_socket_channel-3.0.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "win32", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/win32-5.10.1", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/win32-5.10.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "win32_registry", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/win32_registry-1.1.5", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/win32_registry-1.1.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "wkt_parser", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/wkt_parser-2.0.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "xdg_directories", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/xdg_directories-1.1.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "xml", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/xml-6.5.0", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "yaml", - "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/yaml-3.1.3", + "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3", "packageUri": "lib/", "languageVersion": "3.4" }, @@ -1166,10 +1172,10 @@ "languageVersion": "3.0" } ], - "generated": "2026-01-16T12:37:51.884211Z", + "generated": "2026-01-19T15:01:26.574661Z", "generator": "pub", "generatorVersion": "3.5.4", "flutterRoot": "file:///home/pierre/.local/flutter", "flutterVersion": "3.24.5", - "pubCache": "file:///home/pierre/dev/geosector/app/.pub-cache-local" + "pubCache": "file:///home/pierre/.pub-cache" } diff --git a/app/.flutter-plugins-dependencies b/app/.flutter-plugins-dependencies index 13adc821..620794af 100644 --- a/app/.flutter-plugins-dependencies +++ b/app/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_ios-0.8.12+2/","native_build":true,"dependencies":[]},{"name":"mek_stripe_terminal","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_apple-9.4.7/","native_build":true,"dependencies":[]},{"name":"stripe_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_ios-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_ios-6.3.3/","native_build":true,"dependencies":[]}],"android":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_android-4.6.1/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_android-0.8.12+21/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"mek_stripe_terminal","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[]},{"name":"stripe_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_android-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"file_selector_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_macos-0.9.4+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_macos-0.2.1+2/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"path_provider_foundation","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":false,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":false,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"path_provider_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"url_launcher_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"path_provider_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[]},{"name":"url_launcher_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[]}],"web":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[]},{"name":"geolocator_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_for_web-2.1.12/","dependencies":[]},{"name":"permission_handler_html","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[]},{"name":"url_launcher_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"battery_plus","dependencies":[]},{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_stripe","dependencies":["stripe_android","stripe_ios"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"mek_stripe_terminal","dependencies":[]},{"name":"nfc_manager","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"stripe_android","dependencies":[]},{"name":"stripe_ios","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-01-16 13:38:00.688799","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_compass","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_compass-0.8.1/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/","native_build":true,"dependencies":[]},{"name":"mek_stripe_terminal","path":"/home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.7/","native_build":true,"dependencies":[]},{"name":"stripe_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/stripe_ios-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/","native_build":true,"dependencies":[]}],"android":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_compass","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_compass-0.8.1/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-4.6.1/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+21/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"mek_stripe_terminal","path":"/home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[]},{"name":"stripe_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/stripe_android-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"file_selector_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":false,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":false,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"path_provider_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"url_launcher_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"path_provider_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[]},{"name":"url_launcher_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[]}],"web":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[]},{"name":"geolocator_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-2.1.12/","dependencies":[]},{"name":"permission_handler_html","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[]},{"name":"url_launcher_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"battery_plus","dependencies":[]},{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_compass","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_stripe","dependencies":["stripe_android","stripe_ios"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"mek_stripe_terminal","dependencies":[]},{"name":"nfc_manager","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"stripe_android","dependencies":[]},{"name":"stripe_ios","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-01-19 16:03:59.511412","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/app/android.sh b/app/android.sh index 3bcaa58f..55fa4f88 100755 --- a/app/android.sh +++ b/app/android.sh @@ -252,6 +252,21 @@ else fi echo +# Étape 2.5 : Patcher nfc_manager pour AGP 8+ +print_message "Étape 2.5/5 : Patch nfc_manager pour Android Gradle Plugin 8+..." +NFC_PATCH_SCRIPT="./fastlane/scripts/commun/fix-nfc-manager.sh" +if [ -f "$NFC_PATCH_SCRIPT" ]; then + bash "$NFC_PATCH_SCRIPT" + if [ $? -eq 0 ]; then + print_success "Patch nfc_manager appliqué" + else + print_warning "Le patch nfc_manager a échoué (peut être déjà appliqué)" + fi +else + print_warning "Script de patch nfc_manager introuvable : $NFC_PATCH_SCRIPT" +fi +echo + # Étape 3 : Analyser le code (optionnel mais recommandé) print_message "Étape 3/5 : Analyse du code Dart..." flutter analyze --no-fatal-infos --no-fatal-warnings || { @@ -415,8 +430,10 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then read -p "Installer l'APK debug sur l'appareil connecté ? (y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then + print_message "Désinstallation de l'ancienne version..." + adb uninstall fr.geosector.app3 2>/dev/null || print_warning "Aucune version précédente trouvée" print_message "Installation sur l'appareil..." - adb install -r "$APK_NAME" + adb install "$APK_NAME" if [ $? -eq 0 ]; then print_success "APK installé avec succès" diff --git a/app/assets/images/logo-geosector-512.png-autosave.kra b/app/assets/images/logo-geosector-512.png-autosave.kra deleted file mode 100644 index 8af9f13e79ba046f2f7f9a8e9be998cbb379acaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 195468 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%0D2xIZcb>k%ocmXbS^OFqD0`U66%_e%@RhH z2IR4US3f1t7RSW$jGfvV%-6;7 zGPKm>m{h|)J=OGqkvpY!4^mC9(QlNpevP&Rd9D9ysoitQRMTsH+~Ln}wxXI|>pRT> zBcr0PGocmdH>i@TdiF!_c&vO7siR{hU&u@;N|?b0G_&zWW|l3%!Ui;a%>T+PqKZBs z?z;F06kkI75Ex4}{by9u+szDD)PG+Qf0s^e*@sRuUBGkB$^rAi%G*Lo;2Php%cvv@BBXFgL2taezNrwhg6C$>VFg0*?|||<)?(pBs2>%_-gVa zJO2Y_s;7D!^egY!cOp@me*Cyb?XKqcLu-7|kRH?AGw{lO%t*m*ZIU-b%p?qCvo3|s zQAgosv69<@YL@PuM%A?4X?{0X0^yr|pK~2|SuG5ABlN8bp^$rJ-siSxp0_&|4yoj~ z;+@lJSywDlLzi1GPbbVik>TRU%p);o&iV1S+rne^K^7)2cP%}H2xe|Y(^VuC^M-fK z{5?|K0W8>AeGf*{t$>SNKjR-yi}3rUUf%p?o3Gd zE=sAfy?4=3=5M#}aL%lUz~j!f>Px3E7ZyB DYqPN(6E%W7UqadR&5UTLNUKOS(3giECN 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/lib/app.dart b/app/lib/app.dart index 5c28fa1c..58b784fd 100755 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/services/theme_service.dart'; import 'package:go_router/go_router.dart'; +// Import conditionnel pour le web +import 'package:universal_html/html.dart' as html; import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; @@ -45,10 +48,80 @@ class GeosectorApp extends StatefulWidget { } class _GeosectorAppState extends State with WidgetsBindingObserver { + // Clé globale pour accéder au contexte de l'app (pour les dialogues) + static final GlobalKey navigatorKey = GlobalKey(); + @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + + // Sur Web, intercepter F5 / Ctrl+R pour proposer un refresh des données + if (kIsWeb) { + _setupF5Interceptor(); + } + } + + /// Configure l'interception de F5/Ctrl+R sur Web + void _setupF5Interceptor() { + html.window.onKeyDown.listen((event) { + // Détecter F5 ou Ctrl+R + final isF5 = event.key == 'F5'; + final isCtrlR = (event.ctrlKey || event.metaKey) && event.key?.toLowerCase() == 'r'; + + if (isF5 || isCtrlR) { + event.preventDefault(); + debugPrint('🔄 F5/Ctrl+R intercepté - Affichage du dialogue de refresh'); + _showRefreshDialog(); + } + }); + debugPrint('🌐 Intercepteur F5/Ctrl+R configuré pour Web'); + } + + /// Affiche le dialogue de confirmation de refresh + void _showRefreshDialog() { + final context = navigatorKey.currentContext; + if (context == null) { + debugPrint('⚠️ Impossible d\'afficher le dialogue - contexte non disponible'); + return; + } + + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Row( + children: [ + Icon(Icons.refresh, color: Colors.blue), + SizedBox(width: 12), + Text('Recharger les données ?'), + ], + ), + content: const Text( + 'Voulez-vous actualiser vos données depuis le serveur ?\n\n' + 'Vos modifications non synchronisées seront conservées.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + debugPrint('❌ Refresh annulé par l\'utilisateur'); + }, + child: const Text('Non'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + debugPrint('✅ Refresh demandé par l\'utilisateur'); + // TODO: Implémenter le refresh des données via API + }, + child: const Text('Oui, recharger'), + ), + ], + ); + }, + ); } @override @@ -159,6 +232,7 @@ class _GeosectorAppState extends State with WidgetsBindingObserver /// Création du routeur avec configuration pour URLs propres GoRouter _createRouter() { return GoRouter( + navigatorKey: navigatorKey, initialLocation: '/', routes: [ GoRoute( diff --git a/app/lib/chat/models/room.g.dart b/app/lib/chat/models/room.g.dart index 077d57a4..eb03ecaf 100644 --- a/app/lib/chat/models/room.g.dart +++ b/app/lib/chat/models/room.g.dart @@ -26,7 +26,7 @@ class RoomAdapter extends TypeAdapter { unreadCount: fields[6] as int, recentMessages: (fields[7] as List?) ?.map((dynamic e) => (e as Map).cast()) - ?.toList(), + .toList(), updatedAt: fields[8] as DateTime?, createdBy: fields[9] as int?, isSynced: fields[10] as bool, diff --git a/app/lib/chat/services/chat_service.dart b/app/lib/chat/services/chat_service.dart index c97cd70d..42b1c19a 100644 --- a/app/lib/chat/services/chat_service.dart +++ b/app/lib/chat/services/chat_service.dart @@ -28,10 +28,8 @@ class ChatService { Timer? _syncTimer; DateTime? _lastSyncTimestamp; - DateTime? _lastFullSync; static const Duration _syncInterval = Duration(seconds: 30); // Sync toutes les 30 secondes static const Duration _initialSyncDelay = Duration(seconds: 10); // Délai avant première sync - static const Duration _fullSyncInterval = Duration(minutes: 5); /// Initialisation avec gestion des rôles et configuration YAML static Future init({ diff --git a/app/lib/core/constants/app_keys.dart b/app/lib/core/constants/app_keys.dart index e07397d5..eaafb0e7 100755 --- a/app/lib/core/constants/app_keys.dart +++ b/app/lib/core/constants/app_keys.dart @@ -146,9 +146,9 @@ class AppKeys { 1: { 'titres': 'Effectués', 'titre': 'Effectué', - 'couleur1': 0xFF00E09D, // Vert (Figma) - 'couleur2': 0xFF00E09D, // Vert (Figma) - 'couleur3': 0xFF00E09D, // Vert (Figma) + 'couleur1': 0xFF008000, // Vert foncé + 'couleur2': 0xFF008000, // Vert foncé + 'couleur3': 0xFF008000, // Vert foncé 'icon_data': Icons.task_alt, }, 2: { @@ -170,9 +170,9 @@ class AppKeys { 4: { 'titres': 'Dons', 'titre': 'Don', - 'couleur1': 0xFF395AA7, // Bleu (Figma) - 'couleur2': 0xFF395AA7, // Bleu (Figma) - 'couleur3': 0xFF395AA7, // Bleu (Figma) + 'couleur1': 0xFF00BCD4, // Cyan + 'couleur2': 0xFF00BCD4, // Cyan + 'couleur3': 0xFF00BCD4, // Cyan 'icon_data': Icons.volunteer_activism, }, 5: { diff --git a/app/lib/core/services/api_service.dart b/app/lib/core/services/api_service.dart index 269fb31f..3f68bf49 100755 --- a/app/lib/core/services/api_service.dart +++ b/app/lib/core/services/api_service.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; @@ -69,10 +68,15 @@ class ApiService { _dio.options.headers.addAll(headers); // Gestionnaire de cookies pour les sessions PHP - // Utilise CookieJar en mémoire (cookies maintenus pendant la durée de vie de l'app) - final cookieJar = CookieJar(); - _dio.interceptors.add(CookieManager(cookieJar)); - debugPrint('🍪 [API] Gestionnaire de cookies activé'); + // IMPORTANT: Désactivé sur Web car les navigateurs bloquent la manipulation des cookies via XHR + // Sur Web, on utilise uniquement le header Authorization avec Bearer token + if (!kIsWeb) { + final cookieJar = CookieJar(); + _dio.interceptors.add(CookieManager(cookieJar)); + debugPrint('🍪 [API] Gestionnaire de cookies activé (mobile)'); + } else { + debugPrint('🌐 [API] Mode Web - pas de CookieManager (Bearer token uniquement)'); + } _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { diff --git a/app/lib/core/services/app_info_service.dart b/app/lib/core/services/app_info_service.dart index a4582da8..4d28fe2a 100755 --- a/app/lib/core/services/app_info_service.dart +++ b/app/lib/core/services/app_info_service.dart @@ -1,6 +1,6 @@ // ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY // This file is automatically generated by deploy-app.sh script -// Last update: 2026-01-16 13:37:45 +// Last update: 2026-01-19 15:35:06 // Source: ../VERSION file // // GEOSECTOR App Version Service @@ -8,10 +8,10 @@ class AppInfoService { // Version number (format: x.x.x) - static const String version = '3.6.2'; + static const String version = '3.6.3'; // Build number (version without dots: xxx) - static const String buildNumber = '362'; + static const String buildNumber = '363'; // Full version string (format: vx.x.x+xxx) static String get fullVersion => 'v$version+$buildNumber'; diff --git a/app/lib/core/services/current_user_service.dart b/app/lib/core/services/current_user_service.dart index 0eda3b09..f28d5991 100755 --- a/app/lib/core/services/current_user_service.dart +++ b/app/lib/core/services/current_user_service.dart @@ -140,18 +140,43 @@ class CurrentUserService extends ChangeNotifier { Future loadFromHive() async { try { - final box = Hive.box(AppKeys.userBoxName); // Nouvelle Box - final user = box.get('current_user'); + // 1. Récupérer l'ID utilisateur depuis settings + if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { + debugPrint('⚠️ Box settings non ouverte, impossible de charger l\'utilisateur'); + _currentUser = null; + notifyListeners(); + return; + } + + final settingsBox = Hive.box(AppKeys.settingsBoxName); + final userId = settingsBox.get('current_user_id'); + + if (userId == null) { + debugPrint('ℹ️ Aucun current_user_id trouvé dans settings'); + _currentUser = null; + notifyListeners(); + return; + } + + debugPrint('🔍 Recherche utilisateur avec ID: $userId'); + + // 2. Récupérer l'utilisateur avec le bon ID + final box = Hive.box(AppKeys.userBoxName); + final user = box.get(userId); if (user?.hasValidSession == true) { _currentUser = user; - debugPrint('📥 Utilisateur chargé depuis Hive: ${user?.email}'); + debugPrint('📥 Utilisateur chargé depuis Hive: ${user?.email} (ID: $userId)'); // Charger le mode d'affichage sauvegardé lors de la connexion await _loadDisplayMode(); } else { _currentUser = null; - debugPrint('ℹ️ Aucun utilisateur valide trouvé dans Hive'); + if (user == null) { + debugPrint('ℹ️ Utilisateur ID $userId non trouvé dans la box'); + } else { + debugPrint('ℹ️ Session expirée pour l\'utilisateur ${user.email}'); + } } notifyListeners(); diff --git a/app/lib/core/services/hive_service.dart b/app/lib/core/services/hive_service.dart index 46a7d2f2..2c76f0a7 100755 --- a/app/lib/core/services/hive_service.dart +++ b/app/lib/core/services/hive_service.dart @@ -50,6 +50,57 @@ class HiveService { bool _isInitialized = false; bool get isInitialized => _isInitialized; + + // === INITIALISATION LÉGÈRE POUR F5 (préserve les données) === + + /// Initialisation légère de Hive SANS destruction des données + /// Utilisée pour le F5 sur Web afin de vérifier si une session existe + /// Retourne true si l'initialisation a réussi et qu'une session utilisateur existe + Future initializeWithoutReset() async { + try { + debugPrint('🔧 Initialisation légère de Hive (préservation des données)...'); + + // 1. Initialisation de base de Hive (idempotent) + await Hive.initFlutter(); + debugPrint('✅ Hive.initFlutter() terminé'); + + // 2. Enregistrement des adaptateurs (idempotent) + _registerAdapters(); + + // 3. Ouvrir les boxes SANS les détruire + await _createAllBoxes(); + + // 4. Vérifier si une session utilisateur existe + bool hasSession = false; + try { + if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { + final settingsBox = Hive.box(AppKeys.settingsBoxName); + final userId = settingsBox.get('current_user_id'); + if (userId != null) { + // Vérifier que l'utilisateur existe dans la box user + if (Hive.isBoxOpen(AppKeys.userBoxName)) { + final userBox = Hive.box(AppKeys.userBoxName); + final user = userBox.get(userId); + if (user != null && user.hasValidSession) { + hasSession = true; + debugPrint('✅ Session utilisateur trouvée pour ID: $userId'); + } + } + } + } + } catch (e) { + debugPrint('⚠️ Erreur vérification session: $e'); + } + + _isInitialized = true; + debugPrint('✅ Initialisation légère terminée, session existante: $hasSession'); + return hasSession; + } catch (e) { + debugPrint('❌ Erreur lors de l\'initialisation légère: $e'); + return false; + } + } + // === INITIALISATION COMPLÈTE (appelée par main.dart) === /// Initialisation complète de Hive avec réinitialisation totale diff --git a/app/lib/presentation/auth/splash_page.dart b/app/lib/presentation/auth/splash_page.dart index d41a223c..d3050429 100755 --- a/app/lib/presentation/auth/splash_page.dart +++ b/app/lib/presentation/auth/splash_page.dart @@ -313,13 +313,29 @@ class _SplashPageState extends State with SingleTickerProviderStateM // Appeler le nouvel endpoint API pour restaurer la session // IMPORTANT: Utiliser getWithoutQueue() pour ne JAMAIS mettre cette requête en file d'attente // Les refresh de session sont liés à une session spécifique et ne doivent pas être rejoués + debugPrint('🔄 Appel API: user/session avec sessionId: ${sessionId.substring(0, 10)}...'); + final response = await ApiService.instance.getWithoutQueue( - '/api/user/session', + 'user/session', queryParameters: {'mode': displayMode}, ); // Gestion des codes de retour HTTP final statusCode = response.statusCode ?? 0; + + // Vérifier que la réponse est bien du JSON et pas du HTML + if (response.data is String) { + final dataStr = response.data as String; + if (dataStr.contains('?; switch (statusCode) { @@ -589,9 +605,9 @@ class _SplashPageState extends State with SingleTickerProviderStateM _progress = 0.12; }); } - + await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début - + if (mounted) { setState(() { _statusMessage = "Chargement des composants..."; @@ -599,21 +615,68 @@ class _SplashPageState extends State with SingleTickerProviderStateM }); } - // Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue) - await HiveService.instance.initializeAndResetHive(); - + // === GESTION F5 WEB : Vérifier session AVANT de détruire les données === + // Sur Web, on essaie d'abord de récupérer une session existante + if (kIsWeb) { + debugPrint('🌐 Web détecté - tentative de récupération de session existante...'); + + if (mounted) { + setState(() { + _statusMessage = "Vérification de session..."; + _progress = 0.20; + }); + } + + // Initialisation légère qui préserve les données + final hasExistingSession = await HiveService.instance.initializeWithoutReset(); + + if (hasExistingSession) { + debugPrint('✅ Session existante détectée, tentative de restauration...'); + + if (mounted) { + setState(() { + _statusMessage = "Restauration de la session..."; + _progress = 0.40; + }); + } + + // Tenter la restauration via l'API + final sessionRestored = await _handleSessionRefreshIfNeeded(); + if (sessionRestored) { + debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation'); + return; + } + + // Si la restauration API échoue, on continue vers le login + debugPrint('⚠️ Restauration API échouée, passage au login normal'); + } else { + debugPrint('ℹ️ Pas de session existante, initialisation normale'); + } + } + + // === INITIALISATION NORMALE (si pas de session F5 ou pas Web) === + // Étape 2: Initialisation Hive complète - 15 à 60% if (mounted) { setState(() { _statusMessage = "Configuration du stockage..."; - _progress = 0.45; + _progress = 0.30; }); } - - await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement - + + await HiveService.instance.initializeAndResetHive(); + if (mounted) { setState(() { _statusMessage = "Préparation des données..."; + _progress = 0.45; + }); + } + + await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement + + if (mounted) { + setState(() { + _statusMessage = "Ouverture des bases..."; _progress = 0.60; }); } @@ -621,19 +684,9 @@ class _SplashPageState extends State with SingleTickerProviderStateM // Étape 3: Ouverture des Box - 60 à 80% await HiveService.instance.ensureBoxesAreOpen(); - // NOUVEAU : Vérifier et nettoyer si nouvelle version (Web uniquement) - // Maintenant que les boxes sont ouvertes, on peut vérifier la version dans Hive + // Vérifier et nettoyer si nouvelle version (Web uniquement) await _checkVersionAndCleanIfNeeded(); - // NOUVEAU : Détecter et gérer le F5 (refresh de page web avec session existante) - final sessionRestored = await _handleSessionRefreshIfNeeded(); - if (sessionRestored) { - // Session restaurée avec succès, on arrête ici - // L'utilisateur a été redirigé vers son interface - debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation'); - return; - } - // Gérer la box pending_requests séparément pour préserver les données try { debugPrint('📦 Gestion de la box pending_requests...'); diff --git a/app/lib/presentation/pages/history_page.dart b/app/lib/presentation/pages/history_page.dart index 1e661b9d..2ff7ec66 100644 --- a/app/lib/presentation/pages/history_page.dart +++ b/app/lib/presentation/pages/history_page.dart @@ -5,14 +5,10 @@ import 'package:geosector_app/core/constants/app_keys.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/data/models/passage_model.dart'; -import 'package:geosector_app/core/data/models/sector_model.dart'; -import 'package:geosector_app/core/data/models/membre_model.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/app_scaffold.dart'; -import 'package:geosector_app/presentation/widgets/charts/charts.dart'; import 'package:geosector_app/presentation/widgets/btn_passages.dart'; -import 'package:intl/intl.dart'; /// Page d'historique unifiée utilisant AppScaffold class HistoryPage extends StatelessWidget { @@ -48,52 +44,20 @@ class HistoryContent extends StatefulWidget { State createState() => _HistoryContentState(); } -// Enum pour gérer les types de tri -enum PassageSortType { - dateDesc, // Plus récent en premier (défaut) - dateAsc, // Plus ancien en premier - addressAsc, // Adresse A-Z - addressDesc, // Adresse Z-A -} - -class _HistoryContentState extends State with SingleTickerProviderStateMixin { +class _HistoryContentState extends State { // Détection du rôle et permissions late final bool isAdmin; late final int currentUserId; // users.id (table centrale) late final int? currentOpeUserId; // ope_users.id (pour comparaisons avec passages) late final bool canDeletePassages; // Permission de suppression pour les users - // TabController pour les onglets Filtres / Statistiques - late TabController _tabController; - - // Filtres principaux (nouveaux) + // Filtres String _selectedTypeFilter = 'Tous les types'; - String _selectedPaymentFilter = 'Tous les règlements'; String _searchQuery = ''; - int? _selectedSectorId; - int? _selectedUserId; // Pour admin seulement - int? _selectedPaymentTypeId; // ID du type de règlement sélectionné - - // Contrôleurs - final TextEditingController _startDateController = TextEditingController(); - final TextEditingController _endDateController = TextEditingController(); - final TextEditingController _searchController = TextEditingController(); - - // Anciens filtres (à supprimer progressivement) - int? selectedSectorId; - String selectedSector = 'Tous'; - String selectedType = 'Tous'; - int? selectedMemberId; int? selectedTypeId; - int? selectedPaymentTypeId; - DateTime? startDate; - DateTime? endDate; - String selectedPeriod = 'Toutes'; - DateTimeRange? selectedDateRange; - // Listes pour les filtres - List _sectors = []; - List _membres = []; // Liste des membres de l'opération + // Contrôleur de recherche + final TextEditingController _searchController = TextEditingController(); // Passages originaux pour l'édition List _originalPassages = []; @@ -103,28 +67,10 @@ class _HistoryContentState extends State with SingleTickerProvid bool _isLoading = true; String _errorMessage = ''; - // État de la section graphiques - bool _isGraphicsExpanded = true; - - // Hauteur dynamique du TabBarView selon l'onglet actif - double _tabBarViewHeight = 280.0; // Hauteur par défaut (Filtres) - - // Onglet précédemment sélectionné (pour détecter les clics sur le même onglet) - int _previousTabIndex = 0; - - // Listener pour les changements de secteur depuis map_page - late final Box _settingsBox; - @override void initState() { super.initState(); - // Initialiser le TabController (2 onglets) - _tabController = TabController(length: 2, vsync: this); - - // Initialiser la box settings et écouter les changements de secteur - _initSettingsListener(); - // Déterminer le rôle et les permissions de l'utilisateur (prend en compte le mode d'affichage) final currentUser = userRepository.getCurrentUser(); isAdmin = CurrentUserService.instance.shouldShowAdminUI; @@ -141,81 +87,25 @@ class _HistoryContentState extends State with SingleTickerProvid } canDeletePassages = isAdmin || userCanDelete; - // Si un memberId est passé en paramètre et que c'est un admin, l'utiliser - if (widget.memberId != null && isAdmin) { - selectedMemberId = widget.memberId; - debugPrint('HistoryPage: Filtre membre activé pour ID ${widget.memberId}'); + // Charger les filtres présélectionnés depuis Hive + _loadPreselectedFilters(); - // Sauvegarder aussi dans Hive pour la persistance - _saveMemberFilter(widget.memberId!); - } else { - // Pour tous les autres cas (admin et user), charger les filtres depuis Hive - _loadPreselectedFilters(); - - // Pour un user standard, toujours filtrer sur son propre ID - if (!isAdmin) { - selectedMemberId = currentUserId; - } - } - - _initializeNewFilters(); - _initializeFilters(); - _updateDateControllers(); - _loadGraphicsExpandedState(); + // Initialiser les données + _initializeData(); } @override void dispose() { - _tabController.dispose(); - _startDateController.dispose(); - _endDateController.dispose(); _searchController.dispose(); - // Pas besoin de fermer _settingsBox car c'est une box partagée super.dispose(); } - // Callback pour gérer les clics sur les onglets - void _onTabTapped(int index) { - setState(() { - // Si on clique sur le même onglet alors que l'ExpansionTile est ouvert → le fermer - if (index == _previousTabIndex && _isGraphicsExpanded) { - _isGraphicsExpanded = false; - _saveGraphicsExpandedState(); - } - // Sinon, ouvrir l'ExpansionTile et ajuster la hauteur - else { - if (!_isGraphicsExpanded) { - _isGraphicsExpanded = true; - _saveGraphicsExpandedState(); - } - // Onglet 0 = Filtres (hauteur plus petite) - // Onglet 1 = Statistiques (hauteur plus grande) - _tabBarViewHeight = index == 0 ? 280.0 : 800.0; - } - - _previousTabIndex = index; - }); - } - // Callback pour gérer les clics sur les boutons de type de passage void _handleTypeSelected(int? typeId) { setState(() { - // Réinitialiser tous les filtres - _selectedPaymentFilter = 'Tous les règlements'; - _selectedPaymentTypeId = null; - selectedPaymentTypeId = null; - startDate = null; - endDate = null; - _startDateController.clear(); - _endDateController.clear(); + // Réinitialiser la recherche _searchQuery = ''; _searchController.clear(); - _selectedSectorId = null; - selectedSectorId = null; - if (isAdmin) { - _selectedUserId = null; - selectedMemberId = null; - } // Appliquer le filtre de type if (typeId == null) { @@ -233,118 +123,90 @@ class _HistoryContentState extends State with SingleTickerProvid }); // Appliquer les filtres - _notifyFiltersChanged(); + _applyFilters(); } - // Initialiser le listener pour les changements de secteur - Future _initSettingsListener() async { + // Charger les filtres présélectionnés depuis Hive + Future _loadPreselectedFilters() async { try { if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - _settingsBox = await Hive.openBox(AppKeys.settingsBoxName); - } else { - _settingsBox = Hive.box(AppKeys.settingsBoxName); + await Hive.openBox(AppKeys.settingsBoxName); } + final settingsBox = Hive.box(AppKeys.settingsBoxName); - // Charger le secteur depuis Hive au démarrage - final savedSectorId = _settingsBox.get('selectedSectorId'); - if (savedSectorId != null && savedSectorId is int) { - if (mounted) { - setState(() { - _selectedSectorId = savedSectorId; - selectedSectorId = savedSectorId; // Sync avec l'ancien système - }); - } - debugPrint('HistoryPage: Secteur chargé depuis Hive: $savedSectorId'); - } - - // Écouter les changements futurs - _settingsBox.listenable(keys: ['selectedSectorId']).addListener(_onSectorChanged); - } catch (e) { - debugPrint('HistoryPage: Erreur initialisation settings listener: $e'); - } - } - - // Callback quand le secteur change depuis map_page - void _onSectorChanged() { - final newSectorId = _settingsBox.get('selectedSectorId'); - if (newSectorId != _selectedSectorId) { - if (mounted) { + // Charger le type de passage sélectionné + final typeId = settingsBox.get('history_selectedTypeId'); + if (typeId != null && typeId is int) { setState(() { - _selectedSectorId = newSectorId; - selectedSectorId = newSectorId; // Sync avec l'ancien système + selectedTypeId = typeId; + final typeInfo = AppKeys.typesPassages[typeId]; + if (typeInfo != null) { + _selectedTypeFilter = typeInfo['titre'] as String; + } }); - debugPrint('HistoryPage: Secteur mis à jour depuis map_page: $newSectorId'); - _notifyFiltersChanged(); + + // Supprimer le typeId de Hive après l'avoir utilisé + settingsBox.delete('history_selectedTypeId'); + + debugPrint('HistoryPage: Type de passage présélectionné: $typeId'); } + } catch (e) { + debugPrint('Erreur lors du chargement des filtres: $e'); } } - // Initialiser les nouveaux filtres - void _initializeNewFilters() { - // Initialiser le contrôleur de recherche - _searchController.text = _searchQuery; + // Initialiser les données + void _initializeData() { + try { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); - // Initialiser les filtres selon le rôle - if (isAdmin) { - // Admin peut sélectionner un membre ou "Tous les membres" - _selectedUserId = selectedMemberId; // Utiliser l'ancien système pour la transition - } else { - // User : toujours filtré sur son ID - _selectedUserId = currentUserId; - } + // Charger les passages + final currentOperation = userRepository.getCurrentOperation(); + if (currentOperation != null) { + if (isAdmin) { + // Admin : tous les passages de l'opération + _originalPassages = passageRepository.getPassagesByOperation(currentOperation.id); + } else { + // User : logique spéciale selon le type de passage + final allPassages = passageRepository.getPassagesByOperation(currentOperation.id); + _originalPassages = allPassages.where((p) { + // Type 2 (À finaliser) : afficher TOUS les passages + if (p.fkType == 2) { + return true; + } + // Autres types : seulement les passages de l'utilisateur + return p.fkUser == currentOpeUserId; + }).toList(); + } + debugPrint('Nombre de passages récupérés: ${_originalPassages.length}'); - // Initialiser le secteur - _selectedSectorId = selectedSectorId; // Utiliser l'ancien système pour la transition + // Initialiser les passages filtrés avec tous les passages + _filteredPassages = List.from(_originalPassages); - debugPrint('HistoryPage: Nouveaux filtres initialisés'); - debugPrint(' _selectedTypeFilter: $_selectedTypeFilter'); - debugPrint(' _selectedPaymentFilter: $_selectedPaymentFilter'); - debugPrint(' _selectedSectorId: $_selectedSectorId'); - debugPrint(' _selectedUserId: $_selectedUserId'); - - // Appliquer les filtres initiaux (seulement si les passages sont déjà chargés) - if (_originalPassages.isNotEmpty) { - _notifyFiltersChanged(); - } - } - - // Vérifier si le type Lot doit être affiché (extrait de PassagesListWidget) - bool _shouldShowLotType() { - final currentUser = userRepository.getCurrentUser(); - if (currentUser != null && currentUser.fkEntite != null) { - final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); - if (userAmicale != null) { - return userAmicale.chkLotActif; + // Appliquer les filtres initiaux après le chargement + _applyFilters(); } + + setState(() { + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _errorMessage = 'Erreur lors du chargement des données: $e'; + }); + debugPrint('Erreur lors de l\'initialisation des données: $e'); } - return true; // Par défaut, on affiche } - // Obtenir la liste filtrée des types de passages (extrait de PassagesListWidget) - List _getFilteredPassageTypes() { - final showLotType = _shouldShowLotType(); - final types = []; - - AppKeys.typesPassages.forEach((typeId, typeInfo) { - // Exclure le type Lot (5) si chkLotActif = false - if (typeId == 5 && !showLotType) { - return; // Skip ce type - } - types.add(typeInfo['titre'] as String); - }); - - return types; - } - - /// Appliquer les filtres aux données et synchroniser GraphicsSection + liste - void _notifyFiltersChanged() { + /// Appliquer les filtres aux données + void _applyFilters() { debugPrint('HistoryPage: Application des filtres'); debugPrint(' Type: $_selectedTypeFilter'); - debugPrint(' Paiement: $_selectedPaymentFilter'); debugPrint(' Recherche: $_searchQuery'); - debugPrint(' Secteur: $_selectedSectorId'); - debugPrint(' Utilisateur: $_selectedUserId'); - debugPrint(' Dates: $startDate à $endDate'); // Appliquer les filtres aux passages originaux List filteredPassages = _originalPassages.where((passage) { @@ -359,54 +221,6 @@ class _HistoryContentState extends State with SingleTickerProvid } } - // Filtre par type de règlement - if (_selectedPaymentTypeId != null && passage.fkTypeReglement != _selectedPaymentTypeId) { - return false; - } - - // Filtre par secteur - if (_selectedSectorId != null && passage.fkSector != _selectedSectorId) { - return false; - } - - // Filtre par utilisateur (admin seulement) - if (isAdmin && _selectedUserId != null && passage.fkUser != _selectedUserId) { - return false; - } - - // Filtre par dates - // Si une date de début ou de fin est définie et que le passage est de type 2 (À finaliser) - // sans date, on l'exclut - if ((startDate != null || endDate != null) && passage.fkType == 2 && passage.passedAt == null) { - return false; // Exclure les passages "À finaliser" sans date quand une date est définie - } - - // Filtre par date de début - ne filtrer que si le passage a une date - if (startDate != null) { - // Si le passage a une date, vérifier qu'elle est après la date de début - if (passage.passedAt != null) { - final passageDate = passage.passedAt!; - if (passageDate.isBefore(startDate!)) { - return false; - } - } - // Si le passage n'a pas de date ET n'est pas de type 2, on le garde - // (les passages type 2 sans date ont déjà été exclus au-dessus) - } - - // Filtre par date de fin - ne filtrer que si le passage a une date - if (endDate != null) { - // Si le passage a une date, vérifier qu'elle est avant la date de fin - if (passage.passedAt != null) { - final passageDate = passage.passedAt!; - if (passageDate.isAfter(endDate!.add(const Duration(days: 1)))) { - return false; - } - } - // Si le passage n'a pas de date ET n'est pas de type 2, on le garde - // (les passages type 2 sans date ont déjà été exclus au-dessus) - } - // Filtre par recherche textuelle if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); @@ -439,769 +253,6 @@ class _HistoryContentState extends State with SingleTickerProvid debugPrint('HistoryPage: ${filteredPassages.length} passages filtrés sur ${_originalPassages.length}'); } - /// Construire la section TabBar + ExpansionTile - Widget _buildTabBarSection() { - return Card( - elevation: 0, - color: Colors.transparent, - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, - colorScheme: Theme.of(context).colorScheme.copyWith( - primary: AppTheme.primaryColor, - ), - ), - child: ExpansionTile( - key: ValueKey('expansion_tile_$_isGraphicsExpanded'), - initiallyExpanded: _isGraphicsExpanded, - trailing: const SizedBox.shrink(), // Masquer la flèche d'expansion - onExpansionChanged: (expanded) { - setState(() { - _isGraphicsExpanded = expanded; - // Réinitialiser _previousTabIndex quand on ferme manuellement - // pour permettre de rouvrir en cliquant sur l'onglet actif - if (!expanded) { - _previousTabIndex = -1; - } - }); - _saveGraphicsExpandedState(); - }, - tilePadding: EdgeInsets.zero, - childrenPadding: EdgeInsets.zero, - title: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - boxShadow: AppTheme.cardShadow, - ), - child: TabBar( - controller: _tabController, - labelColor: AppTheme.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppTheme.primaryColor, - indicatorWeight: 3, - onTap: _onTabTapped, - tabs: const [ - Tab( - icon: Icon(Icons.filter_list, size: 20), - text: 'Filtres', - ), - Tab( - icon: Icon(Icons.analytics_outlined, size: 20), - text: 'Statistiques', - ), - ], - ), - ), - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: _tabBarViewHeight, - child: TabBarView( - controller: _tabController, - children: [ - // Onglet 1 : Filtres - _buildFiltersContent(), - // Onglet 2 : Statistiques - _buildGraphicsContent(), - ], - ), - ), - ], - ), - ), - ); - } - - /// Construire le contenu des filtres (ancien _buildFiltersCard sans la Card) - Widget _buildFiltersContent() { - final screenWidth = MediaQuery.of(context).size.width; - final isDesktop = screenWidth > 800; - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Première ligne : Type de passage et Mode de paiement - Row( - children: [ - // Filtre Type de passage - Expanded( - child: DropdownButtonFormField( - value: _selectedTypeFilter, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - isDense: true, - ), - items: [ - const DropdownMenuItem( - value: 'Tous les types', - child: Text('Passage'), - ), - ..._getFilteredPassageTypes().map((String type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }), - ], - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedTypeFilter = newValue; - }); - _notifyFiltersChanged(); - } - }, - ), - ), - const SizedBox(width: 12), - - // Filtre Mode de règlement - Expanded( - child: DropdownButtonFormField( - value: _selectedPaymentFilter, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - isDense: true, - ), - items: [ - const DropdownMenuItem( - value: 'Tous les règlements', - child: Text('Règlements'), - ), - ...AppKeys.typesReglements.entries.map((entry) { - final typeInfo = entry.value; - final titre = typeInfo['titre'] as String; - return DropdownMenuItem( - value: titre, - child: Text(titre), - ); - }), - ], - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedPaymentFilter = newValue; - // Trouver l'ID correspondant au type de règlement sélectionné - if (newValue == 'Tous les règlements') { - _selectedPaymentTypeId = null; - } else { - final entry = AppKeys.typesReglements.entries.firstWhere( - (e) => e.value['titre'] == newValue, - orElse: () => const MapEntry(-1, {}), - ); - _selectedPaymentTypeId = entry.key != -1 ? entry.key : null; - } - }); - _notifyFiltersChanged(); - } - }, - ), - ), - ], - ), - const SizedBox(height: 12), - - // Deuxième ligne : Secteur et Membre (admin seulement) - Row( - children: [ - // Filtre Secteur - Expanded( - child: ValueListenableBuilder>( - valueListenable: Hive.box(AppKeys.sectorsBoxName).listenable(), - builder: (context, sectorsBox, child) { - final sectors = sectorsBox.values.toList(); - - return DropdownButtonFormField( - value: _selectedSectorId, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - isDense: true, - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Secteurs'), - ), - ...sectors.map((SectorModel sector) { - return DropdownMenuItem( - value: sector.id, - child: Text(sector.libelle), - ); - }), - ], - onChanged: (int? newValue) async { - setState(() { - _selectedSectorId = newValue; - }); - // Sauvegarder dans Hive pour synchronisation avec map_page - try { - if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - await Hive.openBox(AppKeys.settingsBoxName); - } - final settingsBox = Hive.box(AppKeys.settingsBoxName); - if (newValue != null) { - await settingsBox.put('selectedSectorId', newValue); - } else { - await settingsBox.delete('selectedSectorId'); - } - } catch (e) { - debugPrint('Erreur sauvegarde secteur: $e'); - } - _notifyFiltersChanged(); - }, - ); - }, - ), - ), - - // Espace ou filtre Membre (admin seulement) - const SizedBox(width: 12), - if (isAdmin) - Expanded( - child: DropdownButtonFormField( - value: _selectedUserId, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - isDense: true, - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Membres'), - ), - ..._membres.map((MembreModel membre) { - return DropdownMenuItem( - value: membre.opeUserId, - child: Text('${membre.firstName ?? ''} ${membre.name ?? ''}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}'), - ); - }), - ], - onChanged: (int? newValue) { - setState(() { - _selectedUserId = newValue; - }); - _notifyFiltersChanged(); - }, - ), - ) - else - const Expanded(child: SizedBox()), - ], - ), - const SizedBox(height: 12), - - // Troisième ligne : Dates - Row( - children: [ - // Date de début - Expanded( - child: TextFormField( - controller: _startDateController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - hintText: 'Début', - isDense: true, - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_startDateController.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear, size: 20), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - setState(() { - startDate = null; - _startDateController.clear(); - }); - _notifyFiltersChanged(); - }, - ), - IconButton( - icon: const Icon(Icons.calendar_today, size: 20), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: startDate ?? DateTime.now().subtract(const Duration(days: 30)), - firstDate: DateTime(2020), - lastDate: DateTime.now(), - locale: const Locale('fr', 'FR'), - ); - if (picked != null) { - setState(() { - startDate = picked; - }); - _updateDateControllers(); - _notifyFiltersChanged(); - } - }, - ), - const SizedBox(width: 8), - ], - ), - ), - readOnly: false, - onChanged: (value) { - // Valider et parser la date au format JJ/MM/AAAA - if (value.length == 10) { - final parts = value.split('/'); - if (parts.length == 3) { - try { - final day = int.parse(parts[0]); - final month = int.parse(parts[1]); - final year = int.parse(parts[2]); - final date = DateTime(year, month, day); - - // Vérifier que la date est valide - if (date.year >= 2020 && date.isBefore(DateTime.now().add(const Duration(days: 1)))) { - setState(() { - startDate = date; - }); - _notifyFiltersChanged(); - } - } catch (e) { - // Date invalide, ignorer - } - } - } else if (value.isEmpty) { - setState(() { - startDate = null; - }); - _notifyFiltersChanged(); - } - }, - ), - ), - const SizedBox(width: 12), - - // Date de fin - Expanded( - child: TextFormField( - controller: _endDateController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - hintText: 'Fin', - isDense: true, - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_endDateController.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear, size: 20), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - setState(() { - endDate = null; - _endDateController.clear(); - }); - _notifyFiltersChanged(); - }, - ), - IconButton( - icon: const Icon(Icons.calendar_today, size: 20), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: endDate ?? DateTime.now(), - firstDate: startDate ?? DateTime(2020), - lastDate: DateTime.now(), - locale: const Locale('fr', 'FR'), - ); - if (picked != null) { - setState(() { - endDate = picked; - }); - _updateDateControllers(); - _notifyFiltersChanged(); - } - }, - ), - const SizedBox(width: 8), - ], - ), - ), - readOnly: false, - onChanged: (value) { - // Valider et parser la date au format JJ/MM/AAAA - if (value.length == 10) { - final parts = value.split('/'); - if (parts.length == 3) { - try { - final day = int.parse(parts[0]); - final month = int.parse(parts[1]); - final year = int.parse(parts[2]); - final date = DateTime(year, month, day); - - // Vérifier que la date est valide et après la date de début si définie - if (date.year >= 2020 && - date.isBefore(DateTime.now().add(const Duration(days: 1))) && - (startDate == null || date.isAfter(startDate!) || date.isAtSameMomentAs(startDate!))) { - setState(() { - endDate = date; - }); - _notifyFiltersChanged(); - } - } catch (e) { - // Date invalide, ignorer - } - } - } else if (value.isEmpty) { - setState(() { - endDate = null; - }); - _notifyFiltersChanged(); - } - }, - ), - ), - ], - ), - const SizedBox(height: 12), - - // Quatrième ligne : Recherche et actions - Row( - children: [ - // Barre de recherche - Expanded( - flex: 3, - child: TextFormField( - controller: _searchController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - hintText: '...', - prefixIcon: Icon(Icons.search, size: 20), - isDense: true, - ), - onChanged: (String value) { - setState(() { - _searchQuery = value; - }); - _notifyFiltersChanged(); - }, - ), - ), - const SizedBox(width: 12), - - // Bouton Réinitialiser (adaptatif) - isDesktop - ? OutlinedButton.icon( - onPressed: () { - setState(() { - _selectedTypeFilter = 'Tous les types'; - _selectedPaymentFilter = 'Tous les règlements'; - _selectedPaymentTypeId = null; - _selectedSectorId = null; - _selectedUserId = null; - _searchQuery = ''; - startDate = null; - endDate = null; - _searchController.clear(); - _startDateController.clear(); - _endDateController.clear(); - }); - _notifyFiltersChanged(); - }, - icon: const Icon(Icons.filter_alt_off, size: 18), - label: const Text('Réinitialiser'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - ) - : IconButton( - onPressed: () { - setState(() { - _selectedTypeFilter = 'Tous les types'; - _selectedPaymentFilter = 'Tous les règlements'; - _selectedPaymentTypeId = null; - _selectedSectorId = null; - _selectedUserId = null; - _searchQuery = ''; - startDate = null; - endDate = null; - _searchController.clear(); - _startDateController.clear(); - _endDateController.clear(); - }); - _notifyFiltersChanged(); - }, - icon: const Icon(Icons.filter_alt_off, size: 20), - tooltip: 'Réinitialiser les filtres', - style: IconButton.styleFrom( - side: BorderSide(color: Theme.of(context).colorScheme.outline), - ), - ), - ], - ), - ], - ), - ), - ); - } - - // Mettre à jour les contrôleurs de date - void _updateDateControllers() { - if (startDate != null) { - _startDateController.text = '${startDate!.day.toString().padLeft(2, '0')}/${startDate!.month.toString().padLeft(2, '0')}/${startDate!.year}'; - } - if (endDate != null) { - _endDateController.text = '${endDate!.day.toString().padLeft(2, '0')}/${endDate!.month.toString().padLeft(2, '0')}/${endDate!.year}'; - } - } - - // Sauvegarder le filtre membre dans Hive (pour les admins) - void _saveMemberFilter(int memberId) async { - if (!isAdmin) return; // Seulement pour les admins - - try { - if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - await Hive.openBox(AppKeys.settingsBoxName); - } - final settingsBox = Hive.box(AppKeys.settingsBoxName); - await settingsBox.put('selectedMemberId', memberId); - await settingsBox.put('history_selectedMemberId', memberId); - debugPrint('HistoryPage: MemberId $memberId sauvegardé dans Hive'); - } catch (e) { - debugPrint('Erreur lors de la sauvegarde du filtre membre: $e'); - } - } - - // Charger l'état de la section graphiques depuis Hive - void _loadGraphicsExpandedState() async { - try { - // Définir l'état par défaut selon la taille d'écran (mobile = rétracté) - final screenWidth = MediaQuery.of(context).size.width; - final isMobile = screenWidth < 800; - _isGraphicsExpanded = !isMobile; // true sur desktop, false sur mobile - - if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - await Hive.openBox(AppKeys.settingsBoxName); - } - final settingsBox = Hive.box(AppKeys.settingsBoxName); - final saved = settingsBox.get('history_graphics_expanded'); - if (saved != null && saved is bool) { - setState(() { - _isGraphicsExpanded = saved; - }); - debugPrint('HistoryPage: État graphics chargé depuis Hive: $_isGraphicsExpanded'); - } - } catch (e) { - debugPrint('Erreur lors du chargement de l\'état graphics: $e'); - } - } - - // Sauvegarder l'état de la section graphiques dans Hive - void _saveGraphicsExpandedState() async { - try { - if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - await Hive.openBox(AppKeys.settingsBoxName); - } - final settingsBox = Hive.box(AppKeys.settingsBoxName); - await settingsBox.put('history_graphics_expanded', _isGraphicsExpanded); - debugPrint('HistoryPage: État graphics sauvegardé: $_isGraphicsExpanded'); - } catch (e) { - debugPrint('Erreur lors de la sauvegarde de l\'état graphics: $e'); - } - } - - // Charger les filtres présélectionnés depuis Hive - void _loadPreselectedFilters() async { - try { - if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - await Hive.openBox(AppKeys.settingsBoxName); - } - final settingsBox = Hive.box(AppKeys.settingsBoxName); - - // Charger le membre sélectionné (admins seulement) - if (isAdmin) { - final memberId = settingsBox.get('history_selectedMemberId') ?? - settingsBox.get('selectedMemberId'); - if (memberId != null && memberId is int) { - setState(() { - selectedMemberId = memberId; - _selectedUserId = memberId; // Synchroniser avec le nouveau filtre - }); - debugPrint('HistoryPage: Membre présélectionné chargé: $memberId'); - } - } - - // Charger le secteur sélectionné - final sectorId = settingsBox.get('history_selectedSectorId'); - if (sectorId != null && sectorId is int) { - setState(() { - selectedSectorId = sectorId; - _selectedSectorId = sectorId; // Synchroniser avec le nouveau filtre - }); - debugPrint('HistoryPage: Secteur présélectionné chargé: $sectorId'); - } - - // Charger le type de passage sélectionné - final typeId = settingsBox.get('history_selectedTypeId'); - if (typeId != null && typeId is int) { - // Réinitialiser TOUS les filtres avant d'appliquer le type - setState(() { - // Réinitialiser les filtres de type et paiement - _selectedPaymentFilter = 'Tous les règlements'; - _selectedPaymentTypeId = null; - selectedPaymentTypeId = null; - - // Réinitialiser les dates - startDate = null; - endDate = null; - _startDateController.clear(); - _endDateController.clear(); - - // Réinitialiser la recherche - _searchQuery = ''; - _searchController.clear(); - - // Réinitialiser le secteur - _selectedSectorId = null; - selectedSectorId = null; - - // Réinitialiser le membre (admin seulement) - if (isAdmin) { - _selectedUserId = null; - selectedMemberId = null; - } - - // Appliquer le type de passage sélectionné - selectedTypeId = typeId; - final typeInfo = AppKeys.typesPassages[typeId]; - selectedType = typeInfo != null ? typeInfo['titre'] as String : 'Inconnu'; - // Synchroniser avec le nouveau filtre - if (typeInfo != null) { - _selectedTypeFilter = typeInfo['titre'] as String; - } - }); - - // Supprimer le typeId de Hive après l'avoir utilisé - settingsBox.delete('history_selectedTypeId'); - - debugPrint('HistoryPage: Type de passage présélectionné: $typeId (tous les autres filtres réinitialisés)'); - } - - // Charger le type de règlement sélectionné - final paymentTypeId = settingsBox.get('history_selectedPaymentTypeId'); - if (paymentTypeId != null && paymentTypeId is int) { - setState(() { - selectedPaymentTypeId = paymentTypeId; - _selectedPaymentTypeId = paymentTypeId; // Synchroniser avec le nouveau filtre - // Mettre à jour aussi le label du filtre - final paymentInfo = AppKeys.typesReglements[paymentTypeId]; - if (paymentInfo != null) { - _selectedPaymentFilter = paymentInfo['titre'] as String; - } - }); - debugPrint('HistoryPage: Type de règlement présélectionné: $paymentTypeId'); - } - - // Charger les dates de période si disponibles - final startDateMs = settingsBox.get('history_startDate'); - final endDateMs = settingsBox.get('history_endDate'); - - if (startDateMs != null && startDateMs is int) { - setState(() { - startDate = DateTime.fromMillisecondsSinceEpoch(startDateMs); - }); - debugPrint('HistoryPage: Date de début chargée: $startDate'); - } - if (endDateMs != null && endDateMs is int) { - setState(() { - endDate = DateTime.fromMillisecondsSinceEpoch(endDateMs); - }); - debugPrint('HistoryPage: Date de fin chargée: $endDate'); - } - } catch (e) { - debugPrint('Erreur lors du chargement des filtres: $e'); - } - } - - // Initialiser les listes de filtres - void _initializeFilters() { - try { - setState(() { - _isLoading = true; - _errorMessage = ''; - }); - - // Charger les secteurs - if (isAdmin) { - // Admin : tous les secteurs - _sectors = sectorRepository.getAllSectors(); - } else { - // User : seulement ses secteurs assignés - final userSectors = userRepository.getUserSectors(); - final userSectorIds = userSectors.map((us) => us.id).toSet(); - _sectors = sectorRepository.getAllSectors() - .where((s) => userSectorIds.contains(s.id)) - .toList(); - } - debugPrint('Nombre de secteurs récupérés: ${_sectors.length}'); - - // Charger les membres de l'opération (admin seulement) - if (isAdmin) { - // Charger directement depuis MembreModel (déjà unique, pas de déduplication nécessaire) - final membreBox = Hive.box(AppKeys.membresBoxName); - _membres = membreBox.values.whereType().toList(); - debugPrint('Nombre de membres de l\'opération récupérés: ${_membres.length}'); - } - - // Charger les passages - final currentOperation = userRepository.getCurrentOperation(); - if (currentOperation != null) { - if (isAdmin) { - // Admin : tous les passages de l'opération - _originalPassages = passageRepository.getPassagesByOperation(currentOperation.id); - } else { - // User : logique spéciale selon le type de passage - final allPassages = passageRepository.getPassagesByOperation(currentOperation.id); - _originalPassages = allPassages.where((p) { - // Type 2 (À finaliser) : afficher TOUS les passages - if (p.fkType == 2) { - return true; - } - // Autres types : seulement les passages de l'utilisateur - return p.fkUser == currentOpeUserId; - }).toList(); - } - debugPrint('Nombre de passages récupérés: ${_originalPassages.length}'); - - // Initialiser les passages filtrés avec tous les passages - _filteredPassages = List.from(_originalPassages); - - // Appliquer les filtres initiaux après le chargement - _notifyFiltersChanged(); - } - - setState(() { - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - _errorMessage = 'Erreur lors du chargement des données: $e'; - }); - debugPrint('Erreur lors de l\'initialisation des filtres: $e'); - } - } - @override Widget build(BuildContext context) { // Le contenu sans scaffold (AppScaffold est déjà dans HistoryPage) @@ -1219,7 +270,7 @@ class _HistoryContentState extends State with SingleTickerProvid Text(_errorMessage, style: const TextStyle(fontSize: 16)), const SizedBox(height: 16), ElevatedButton( - onPressed: _initializeFilters, + onPressed: _initializeData, child: const Text('Réessayer'), ), ], @@ -1234,28 +285,49 @@ class _HistoryContentState extends State with SingleTickerProvid final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth > 800; - return Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, - vertical: AppTheme.spacingL, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 0. BtnPassages - BtnPassages( - onTypeSelected: _handleTypeSelected, - selectedTypeId: selectedTypeId, + return Column( + children: [ + // 0. BtnPassages collé en haut/gauche/droite + BtnPassages( + onTypeSelected: _handleTypeSelected, + selectedTypeId: selectedTypeId, + ), + // 1. Barre de recherche + Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, + vertical: AppTheme.spacingS, ), - const SizedBox(height: AppTheme.spacingL), - - // 1. TabBar + ExpansionTile (Filtres / Statistiques) - FIXE EN HAUT - _buildTabBarSection(), - - SizedBox(height: _isGraphicsExpanded ? 8 : 16), - - // 2. Liste des passages - EXPANDED pour prendre tout l'espace restant - Expanded( + child: Row( + children: [ + // Barre de recherche + Expanded( + child: TextFormField( + controller: _searchController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + hintText: 'Rechercher...', + prefixIcon: Icon(Icons.search, size: 20), + isDense: true, + ), + onChanged: (String value) { + setState(() { + _searchQuery = value; + }); + _applyFilters(); + }, + ), + ), + ], + ), + ), + // 2. Liste des passages - EXPANDED pour prendre tout l'espace restant + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, + ), child: Card( elevation: 2, child: PassagesListWidget( @@ -1271,267 +343,12 @@ class _HistoryContentState extends State with SingleTickerProvid ), ), ), - ], - ), + ), + ], ); } - // Contenu des statistiques adaptatif selon la taille d'écran - Widget _buildGraphicsContent() { - final screenWidth = MediaQuery.of(context).size.width; - final isDesktop = screenWidth > 800; - - return SingleChildScrollView( - child: Column( - children: [ - // Graphiques en camembert (côte à côte sur desktop) - isDesktop - ? Row( - children: [ - Expanded(child: _buildPassageSummaryCard()), - const SizedBox(width: AppTheme.spacingM), - Expanded(child: _buildPaymentSummaryCard()), - ], - ) - : Column( - children: [ - _buildPassageSummaryCard(), - const SizedBox(height: AppTheme.spacingM), - _buildPaymentSummaryCard(), - ], - ), - - const SizedBox(height: AppTheme.spacingL), - - // Graphique d'activité - _buildActivityChart(), - ], - ), - ); - } - - // Graphique camembert des types de passage - Widget _buildPassageSummaryCard() { - // Calculer les données filtrées pour le graphique - final passagesByType = _calculatePassagesByType(); - - return PassageSummaryCard( - title: isAdmin ? 'Types de passage' : 'Mes types de passage', - titleColor: AppTheme.primaryColor, - titleIcon: Icons.route, - height: 300, - useValueListenable: false, // Utiliser les données filtrées directement - passagesByType: passagesByType, // Passer les données calculées - showAllPassages: isAdmin, - userId: isAdmin ? _selectedUserId : currentUserId, - excludePassageTypes: const [], // Ne pas exclure "À finaliser" ici, c'est déjà filtré - isDesktop: MediaQuery.of(context).size.width > 800, - backgroundIcon: Icons.route, - backgroundIconColor: AppTheme.primaryColor, - backgroundIconOpacity: 0.07, - backgroundIconSize: 120, - ); - } - - // Calculer la répartition des passages par type depuis les données filtrées - Map _calculatePassagesByType() { - final counts = {}; - - for (final passage in _filteredPassages) { - final typeId = passage.fkType; - counts[typeId] = (counts[typeId] ?? 0) + 1; - } - - return counts; - } - - // Graphique camembert des modes de paiement - Widget _buildPaymentSummaryCard() { - // Calculer les données filtrées pour le graphique - final paymentsByType = _calculatePaymentsByType(); - - return PaymentSummaryCard( - title: isAdmin ? 'Modes de règlement' : 'Mes règlements', - titleColor: AppTheme.accentColor, - titleIcon: Icons.euro_symbol, - height: 300, - useValueListenable: false, // Utiliser les données filtrées directement - paymentsByType: paymentsByType, // Passer les données calculées - showAllPayments: isAdmin, - userId: isAdmin ? _selectedUserId : currentUserId, - isDesktop: MediaQuery.of(context).size.width > 800, - backgroundIcon: Icons.euro_symbol, - backgroundIconColor: AppTheme.accentColor, - backgroundIconOpacity: 0.07, - backgroundIconSize: 120, - ); - } - - // Calculer la répartition des montants par type de règlement depuis les données filtrées - Map _calculatePaymentsByType() { - final amounts = {}; - - for (final passage in _filteredPassages) { - // Récupérer le montant du passage - final montantStr = _safeString(passage.montant); - final montantDouble = double.tryParse(montantStr) ?? 0.0; - - // Ne prendre en compte que les passages payés (montant > 0) - if (montantDouble > 0.0) { - // Utiliser le type de règlement pour catégoriser - final typeReglement = passage.fkTypeReglement; - amounts[typeReglement] = (amounts[typeReglement] ?? 0.0) + montantDouble; - } - // Les passages non payés sont ignorés (ne comptent ni dans le total ni dans le graphique) - } - - return amounts; - } - - // Graphique d'évolution temporelle des passages - Widget _buildActivityChart() { - // Calculer les données d'activité depuis les passages filtrés - final activityData = _calculateActivityData(); - - // Calculer le titre dynamique basé sur la plage de dates - final dateRange = _getDateRangeForActivityChart(); - String title; - if (dateRange != null) { - final dateFormat = DateFormat('dd/MM/yyyy'); - title = isAdmin - ? 'Évolution des passages (${dateFormat.format(dateRange.start)} - ${dateFormat.format(dateRange.end)})' - : 'Évolution de mes passages (${dateFormat.format(dateRange.start)} - ${dateFormat.format(dateRange.end)})'; - } else { - title = isAdmin - ? 'Évolution des passages' - : 'Évolution de mes passages'; - } - - return Card( - elevation: 2, - child: ActivityChart( - height: 350, - useValueListenable: false, // Utiliser les données filtrées directement - passageData: activityData, // Passer les données calculées - showAllPassages: isAdmin, - title: title, - daysToShow: 0, // 0 = utiliser toutes les données fournies - userId: isAdmin ? _selectedUserId : currentUserId, - excludePassageTypes: const [], // Ne pas exclure ici, c'est déjà filtré - ), - ); - } - - // Obtenir la plage de dates pour le graphique d'activité - DateTimeRange? _getDateRangeForActivityChart() { - // Si des dates sont explicitement définies par l'utilisateur, les utiliser - if (startDate != null || endDate != null) { - final start = startDate ?? DateTime.now().subtract(const Duration(days: 90)); - final end = endDate ?? DateTime.now(); - return DateTimeRange(start: start, end: end); - } - - // Par défaut : utiliser les 90 derniers jours - final now = DateTime.now(); - return DateTimeRange( - start: now.subtract(const Duration(days: 90)), - end: now, - ); - } - - // Calculer les données d'activité pour le graphique temporel - List> _calculateActivityData() { - final data = >[]; - - // Obtenir la plage de dates à afficher (ne peut plus être null avec la nouvelle logique) - final dateRange = _getDateRangeForActivityChart(); - if (dateRange == null) { - // Fallback : retourner des données vides pour les 90 derniers jours - final now = DateTime.now(); - final dateFormat = DateFormat('yyyy-MM-dd'); - for (int i = 89; i >= 0; i--) { - final date = now.subtract(Duration(days: i)); - data.add({ - 'date': dateFormat.format(date), - 'type_passage': 0, - 'nb': 0, - }); - } - return data; - } - - final dateFormat = DateFormat('yyyy-MM-dd'); - final dataByDate = >{}; - - // Parcourir les passages filtrés pour compter par date et type - // Exclure les passages de type 2 (À finaliser) du graphique - for (final passage in _filteredPassages) { - // Exclure les passages de type 2 - if (passage.fkType == 2) { - continue; - } - - if (passage.passedAt != null) { - // Ne compter que les passages dans la plage de dates - if (passage.passedAt!.isBefore(dateRange.start) || - passage.passedAt!.isAfter(dateRange.end.add(const Duration(days: 1)))) { - continue; - } - - final dateKey = dateFormat.format(passage.passedAt!); - if (!dataByDate.containsKey(dateKey)) { - dataByDate[dateKey] = {}; - } - - final typeId = passage.fkType; - dataByDate[dateKey]![typeId] = (dataByDate[dateKey]![typeId] ?? 0) + 1; - } - } - - // Calculer le nombre de jours dans la plage - final daysDiff = dateRange.end.difference(dateRange.start).inDays + 1; - - // Limiter l'affichage à 90 jours maximum pour la lisibilité - final daysToShow = daysDiff > 90 ? 90 : daysDiff; - final startDate = daysDiff > 90 - ? dateRange.end.subtract(Duration(days: 89)) - : dateRange.start; - - // Créer une entrée pour chaque jour de la plage - for (int i = 0; i < daysToShow; i++) { - final date = startDate.add(Duration(days: i)); - final dateKey = dateFormat.format(date); - final passagesByType = dataByDate[dateKey] ?? {}; - - // Ajouter les données pour chaque type de passage présent ce jour (sauf type 2) - bool hasDataForDay = false; - if (passagesByType.isNotEmpty) { - for (final entry in passagesByType.entries) { - if (entry.key != 2) { // Double vérification pour exclure type 2 - data.add({ - 'date': dateKey, - 'type_passage': entry.key, - 'nb': entry.value, - }); - hasDataForDay = true; - } - } - } - - // Si aucune donnée pour ce jour, ajouter une entrée vide pour maintenir la continuité - if (!hasDataForDay) { - data.add({ - 'date': dateKey, - 'type_passage': 0, - 'nb': 0, - }); - } - } - - return data; - } - - // Afficher le formulaire de création de passage (users seulement) + // Afficher le formulaire de création de passage Future _showPassageFormDialog(BuildContext context) async { await showDialog( context: context, @@ -1543,7 +360,7 @@ class _HistoryContentState extends State with SingleTickerProvid operationRepository: operationRepository, amicaleRepository: amicaleRepository, onSuccess: () { - _initializeFilters(); // Recharger les données + _initializeData(); // Recharger les données }, ), ); @@ -1558,8 +375,7 @@ class _HistoryContentState extends State with SingleTickerProvid // Convertir les passages en Map pour PassagesListWidget List> _convertPassagesToMaps() { try { - // Utiliser les passages filtrés (filtrage déjà appliqué par _notifyFiltersChanged) - // Ne PAS filtrer par passedAt ici car les passages "À finaliser" n'ont pas de date + // Utiliser les passages filtrés var passages = _filteredPassages.toList(); // Convertir chaque passage en Map @@ -1593,9 +409,9 @@ class _HistoryContentState extends State with SingleTickerProvid 'fk_adresse': _safeString(passage.fkAdresse), 'address': fullAddress.isNotEmpty ? fullAddress : 'Adresse non renseignée', 'date': passageDate, - 'datePassage': passageDate, // Ajouter aussi datePassage pour compatibilité + 'datePassage': passageDate, 'passed_at': passage.passedAt?.toIso8601String(), - 'passedAt': passageDate, // Ajouter aussi passedAt comme DateTime + 'passedAt': passageDate, 'numero': _safeString(passage.numero), 'rue': _safeString(passage.rue), 'rue_bis': _safeString(passage.rueBis), @@ -1680,43 +496,6 @@ class _HistoryContentState extends State with SingleTickerProvid } } - - - // Sauvegarder les filtres dans Hive - // NOTE: Méthode non utilisée pour le moment - conservée pour référence future - // void _saveFiltersToHive() async { - // try { - // if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { - // await Hive.openBox(AppKeys.settingsBoxName); - // } - // final settingsBox = Hive.box(AppKeys.settingsBoxName); - // - // // Sauvegarder tous les filtres - // await settingsBox.put('history_selectedSectorId', selectedSectorId); - // if (isAdmin) { - // await settingsBox.put('history_selectedMemberId', selectedMemberId); - // } - // await settingsBox.put('history_selectedTypeId', selectedTypeId); - // await settingsBox.put('history_selectedPaymentTypeId', selectedPaymentTypeId); - // - // // Sauvegarder les dates - // if (startDate != null) { - // await settingsBox.put('history_startDate', startDate!.millisecondsSinceEpoch); - // } else { - // await settingsBox.delete('history_startDate'); - // } - // if (endDate != null) { - // await settingsBox.put('history_endDate', endDate!.millisecondsSinceEpoch); - // } else { - // await settingsBox.delete('history_endDate'); - // } - // - // debugPrint('Filtres sauvegardés dans Hive'); - // } catch (e) { - // debugPrint('Erreur lors de la sauvegarde des filtres: $e'); - // } - // } - // Gérer l'édition d'un passage void _handlePassageEdit(PassageModel passage) async { await showDialog( @@ -1730,7 +509,7 @@ class _HistoryContentState extends State with SingleTickerProvid operationRepository: operationRepository, amicaleRepository: amicaleRepository, onSuccess: () { - _initializeFilters(); // Recharger les données + _initializeData(); // Recharger les données }, ), ); @@ -1762,7 +541,7 @@ class _HistoryContentState extends State with SingleTickerProvid if (confirm == true) { try { await passageRepository.deletePassage(passage.id); - _initializeFilters(); // Recharger les données + _initializeData(); // Recharger les données if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -1784,4 +563,4 @@ class _HistoryContentState extends State with SingleTickerProvid } } } -} \ No newline at end of file +} diff --git a/app/lib/presentation/pages/home_page.dart b/app/lib/presentation/pages/home_page.dart index b260a00d..91b4fb36 100644 --- a/app/lib/presentation/pages/home_page.dart +++ b/app/lib/presentation/pages/home_page.dart @@ -40,17 +40,19 @@ class _HomeContentState extends State { final isDesktop = screenWidth > 800; // Retourner seulement le contenu (sans scaffold) - return SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, - vertical: AppTheme.spacingL, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Widget BtnPassages - const BtnPassages(), - const SizedBox(height: AppTheme.spacingL), + return Column( + children: [ + // Widget BtnPassages collé en haut/gauche/droite + const BtnPassages(), + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, + vertical: AppTheme.spacingL, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ // LIGNE 1 : Graphiques de répartition (type de passage et mode de paiement) isDesktop @@ -172,9 +174,12 @@ class _HomeContentState extends State { ), ), ], - ], + ], + ), + ), ), - ); + ], + ); } // Construit la carte de répartition par type de passage diff --git a/app/lib/presentation/pages/map_page.dart b/app/lib/presentation/pages/map_page.dart index 3035162b..eee3be88 100644 --- a/app/lib/presentation/pages/map_page.dart +++ b/app/lib/presentation/pages/map_page.dart @@ -123,6 +123,9 @@ class _MapPageContentState extends State { // État pour bloquer la sauvegarde du zoom lors du centrage sur secteur bool _isCenteringOnSector = false; + // Source des tuiles de la carte (IGN Plan ou IGN Ortho) + TileSource _tileSource = TileSource.ignPlan; + // Comptages des secteurs (calculés uniquement lors de création/modification de secteurs) Map _sectorPassageCount = {}; Map _sectorMemberCount = {}; @@ -215,6 +218,16 @@ class _MapPageContentState extends State { _settingsBox.put('mapZoom', 15.0); debugPrint('🔍 MapPage: Aucun zoom sauvegardé, utilisation du défaut = 15.0'); } + + // Charger la source des tuiles (IGN Plan par défaut) + final savedTileSource = _settingsBox.get('mapTileSource'); + if (savedTileSource != null) { + _tileSource = TileSource.values.firstWhere( + (t) => t.name == savedTileSource, + orElse: () => TileSource.ignPlan, + ); + debugPrint('🗺️ MapPage: Source tuiles chargée = $_tileSource'); + } } // Méthode pour gérer les changements de sélection de secteur @@ -4151,8 +4164,8 @@ class _MapPageContentState extends State { initialZoom: _currentZoom, mapController: _mapController, disableDrag: _isDraggingPoint, - // Utiliser OpenStreetMap temporairement sur mobile si Mapbox échoue - useOpenStreetMap: !kIsWeb, // true sur mobile, false sur web + // Utiliser les tuiles IGN (Plan ou Ortho selon le choix utilisateur) + tileSource: _tileSource, labelMarkers: _buildSectorLabels(), markers: [ ..._buildMarkers(), @@ -4199,14 +4212,38 @@ class _MapPageContentState extends State { ), )), - // Boutons d'action en haut à droite (Web uniquement et admin seulement) - if (kIsWeb && canEditSectors) - Positioned( - right: 16, - top: 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ + // Bouton switch IGN Plan / Ortho en haut à droite (visible pour tous) + Positioned( + right: 16, + top: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Bouton switch IGN Plan / Ortho + // L'icône montre l'action (vers quoi on bascule), pas l'état actuel + _buildActionButton( + icon: _tileSource == TileSource.ignPlan + ? Icons.satellite_alt // En mode plan → afficher satellite pour basculer + : Icons.map_outlined, // En mode ortho → afficher plan pour basculer + tooltip: _tileSource == TileSource.ignPlan + ? 'Passer en vue satellite' + : 'Passer en vue plan', + color: Colors.white, + iconColor: Colors.blueGrey[700], + onPressed: () { + setState(() { + _tileSource = _tileSource == TileSource.ignPlan + ? TileSource.ignOrtho + : TileSource.ignPlan; + _settingsBox.put('mapTileSource', _tileSource.name); + debugPrint('🗺️ MapPage: Source tuiles changée = $_tileSource'); + }); + }, + ), + // Espacement avant les boutons admin + if (kIsWeb && canEditSectors) const SizedBox(height: 16), + // Boutons admin (création, modification, suppression de secteurs) + if (kIsWeb && canEditSectors) ...[ // Bouton Créer _buildActionButton( icon: Icons.pentagon_outlined, @@ -4246,8 +4283,9 @@ class _MapPageContentState extends State { : null, ), ], - ), + ], ), + ), // Menu contextuel (apparaît selon le mode) - Web uniquement et admin seulement if (kIsWeb && canEditSectors && _mapMode != MapMode.view) diff --git a/app/lib/presentation/user/user_field_mode_page.dart b/app/lib/presentation/user/user_field_mode_page.dart index 40b02443..8402bbee 100644 --- a/app/lib/presentation/user/user_field_mode_page.dart +++ b/app/lib/presentation/user/user_field_mode_page.dart @@ -8,13 +8,14 @@ import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter_compass/flutter_compass.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; -import 'package:geosector_app/core/services/api_service.dart'; import 'package:geosector_app/core/services/current_amicale_service.dart'; import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart'; import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; import 'package:geosector_app/presentation/widgets/grouped_passages_dialog.dart'; +import 'package:geosector_app/presentation/widgets/mapbox_map.dart' show TileSource; import 'package:geosector_app/app.dart'; import 'package:geosector_app/core/utils/api_exception.dart'; @@ -59,10 +60,20 @@ class _UserFieldModePageState extends State // Listener pour les changements de la box passages Box? _passagesBox; + // Source des tuiles de la carte (IGN Plan ou IGN Ortho) + TileSource _tileSource = TileSource.ignPlan; + Box? _settingsBox; + + // Mode boussole (Android/iOS uniquement) + bool _compassModeEnabled = false; + StreamSubscription? _compassSubscription; + double _currentHeading = 0.0; + @override void initState() { super.initState(); _initializeAnimations(); + _loadTileSourceSetting(); // Écouter les changements de la Hive box passages pour rafraîchir la carte _passagesBox = Hive.box(AppKeys.passagesBoxName); @@ -85,6 +96,26 @@ class _UserFieldModePageState extends State } } + // Charger le paramètre de source des tuiles depuis Hive + Future _loadTileSourceSetting() async { + if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { + _settingsBox = await Hive.openBox(AppKeys.settingsBoxName); + } else { + _settingsBox = Hive.box(AppKeys.settingsBoxName); + } + + final savedTileSource = _settingsBox?.get('mapTileSource'); + if (savedTileSource != null && mounted) { + setState(() { + _tileSource = TileSource.values.firstWhere( + (t) => t.name == savedTileSource, + orElse: () => TileSource.ignPlan, + ); + }); + debugPrint('FieldMode: Source tuiles chargée = $_tileSource'); + } + } + void _initializeWebMode() async { // Essayer d'obtenir la position réelle depuis le navigateur try { @@ -539,6 +570,7 @@ class _UserFieldModePageState extends State void dispose() { _positionStreamSubscription?.cancel(); _qualityUpdateTimer?.cancel(); + _compassSubscription?.cancel(); _gpsBlinkController.dispose(); _networkBlinkController.dispose(); _searchController.dispose(); @@ -546,6 +578,35 @@ class _UserFieldModePageState extends State super.dispose(); } + // Activer/désactiver le mode boussole (Android/iOS uniquement) + void _toggleCompassMode() { + if (kIsWeb) return; // Pas de boussole sur web + + setState(() { + _compassModeEnabled = !_compassModeEnabled; + }); + + if (_compassModeEnabled) { + // Activer l'écoute de la boussole + _compassSubscription = FlutterCompass.events?.listen((CompassEvent event) { + if (event.heading != null && mounted) { + setState(() { + _currentHeading = event.heading!; + }); + // Faire pivoter la carte selon la direction + _mapController.rotate(-_currentHeading); + } + }); + debugPrint('FieldMode: Mode boussole activé'); + } else { + // Désactiver l'écoute et remettre la carte vers le nord + _compassSubscription?.cancel(); + _compassSubscription = null; + _mapController.rotate(0); + debugPrint('FieldMode: Mode boussole désactivé'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -823,10 +884,6 @@ class _UserFieldModePageState extends State ); } - final apiService = ApiService.instance; - final mapboxApiKey = - AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment()); - return Stack( children: [ FlutterMap( @@ -837,21 +894,36 @@ class _UserFieldModePageState extends State initialZoom: 17, maxZoom: 19, minZoom: 10, - interactionOptions: const InteractionOptions( + interactionOptions: InteractionOptions( enableMultiFingerGestureRace: true, - flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + // Permettre la rotation uniquement si le mode boussole est activé + flags: _compassModeEnabled + ? InteractiveFlag.all + : InteractiveFlag.all & ~InteractiveFlag.rotate, ), ), children: [ TileLayer( - // Utiliser l'API v4 de Mapbox sur mobile ou OpenStreetMap en fallback - urlTemplate: kIsWeb - ? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey' - : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile + // Utiliser les tuiles IGN (Plan ou Ortho selon le choix utilisateur) + urlTemplate: _tileSource == TileSource.ignOrtho + ? 'https://data.geopf.fr/wmts?' + 'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0' + '&TILEMATRIXSET=PM' + '&LAYER=ORTHOIMAGERY.ORTHOPHOTOS' + '&STYLE=normal' + '&FORMAT=image/jpeg' + '&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}' + : 'https://data.geopf.fr/wmts?' + 'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0' + '&TILEMATRIXSET=PM' + '&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' + '&STYLE=normal' + '&FORMAT=image/png' + '&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}', userAgentPackageName: 'app3.geosector.fr', - additionalOptions: const { - 'attribution': '© OpenStreetMap contributors', - }, + maxNativeZoom: 19, + maxZoom: 20, + minZoom: 7, ), // Markers des passages MarkerLayer( @@ -900,6 +972,56 @@ class _UserFieldModePageState extends State child: const Icon(Icons.my_location), ), ), + // Boutons haut droite (IGN + Boussole) + Positioned( + top: 16, + right: 16, + child: Column( + children: [ + // Bouton switch IGN Plan / Ortho + FloatingActionButton.small( + heroTag: 'tileSource', + backgroundColor: Colors.white, + foregroundColor: Colors.green[700], + tooltip: _tileSource == TileSource.ignPlan + ? 'Passer en vue satellite' + : 'Passer en vue plan', + onPressed: () { + setState(() { + _tileSource = _tileSource == TileSource.ignPlan + ? TileSource.ignOrtho + : TileSource.ignPlan; + }); + // Sauvegarder le choix + _settingsBox?.put('mapTileSource', _tileSource.name); + debugPrint('FieldMode: Source tuiles = $_tileSource'); + }, + // L'icône montre l'action (vers quoi on bascule), pas l'état actuel + child: Icon( + _tileSource == TileSource.ignPlan + ? Icons.satellite_alt // En mode plan → afficher satellite pour basculer + : Icons.map_outlined, // En mode ortho → afficher plan pour basculer + ), + ), + // Bouton mode boussole (uniquement sur mobile) + if (!kIsWeb) ...[ + const SizedBox(height: 8), + FloatingActionButton.small( + heroTag: 'compass', + backgroundColor: _compassModeEnabled ? Colors.green[700] : Colors.white, + foregroundColor: _compassModeEnabled ? Colors.white : Colors.green[700], + tooltip: _compassModeEnabled + ? 'Désactiver le mode boussole' + : 'Activer le mode boussole', + onPressed: _toggleCompassMode, + child: Icon( + _compassModeEnabled ? Icons.explore : Icons.explore_outlined, + ), + ), + ], + ], + ), + ), ], ); } diff --git a/app/lib/presentation/widgets/btn_passages.dart b/app/lib/presentation/widgets/btn_passages.dart index b9249973..e6752c2d 100644 --- a/app/lib/presentation/widgets/btn_passages.dart +++ b/app/lib/presentation/widgets/btn_passages.dart @@ -40,7 +40,7 @@ class BtnPassages extends StatelessWidget { final shouldShowLotType = _shouldShowLotType(); return SizedBox( - height: 80, + height: 92, // 80 + 12 pour le triangle indicateur width: double.infinity, child: ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.passagesBoxName).listenable(), @@ -121,6 +121,7 @@ class BtnPassages extends StatelessWidget { /// Colonne TOTAL (cliquable, affiche tous les passages) Widget _buildTotalColumn(BuildContext context, int total) { final bool isSelected = selectedTypeId == null; + final Color bgColor = Colors.grey[200]!; return InkWell( onTap: () async { @@ -147,55 +148,71 @@ class BtnPassages extends StatelessWidget { } } }, - child: Container( - height: 80, - decoration: BoxDecoration( - color: Colors.grey[200], - border: Border.all( - color: Colors.grey[400]!, - width: isSelected ? 5 : 1, - ), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(AppTheme.borderRadiusMedium), - bottomLeft: Radius.circular(AppTheme.borderRadiusMedium), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.route, - size: 20, - color: Colors.black54, - ), - const SizedBox(height: 2), - Text( - total.toString(), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: bgColor, + border: Border.all( + color: Colors.grey[400]!, + width: 1, + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(AppTheme.borderRadiusMedium), + bottomLeft: Radius.circular(AppTheme.borderRadiusMedium), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.route, + size: 20, + color: Colors.black54, + ), + const SizedBox(height: 2), + Text( + total.toString(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + total > 1 ? 'passages' : 'passage', + style: TextStyle( + fontSize: 10, + color: Colors.grey[700], + ), + textAlign: TextAlign.center, + ), + ], ), ), - const SizedBox(height: 2), - Text( - total > 1 ? 'passages' : 'passage', - style: TextStyle( - fontSize: 10, - color: Colors.grey[700], + ), + // Triangle indicateur de sélection + if (isSelected) + Center( + child: CustomPaint( + size: const Size(20, 12), + painter: _TrianglePainter(color: bgColor), ), - textAlign: TextAlign.center, - ), - ], - ), + ) + else + const SizedBox(height: 12), + ], ), ); } @@ -236,62 +253,78 @@ class BtnPassages extends StatelessWidget { } } }, - child: Container( - height: 80, - decoration: BoxDecoration( - color: couleur.withOpacity(0.1), - border: Border.all( - color: couleur, - width: isSelected ? 5 : 1, - ), - borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - iconData, - size: 20, - color: couleur, - ), - const SizedBox(height: 2), - Text( - count.toString(), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( color: couleur, - ), - ), - const SizedBox(height: 2), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: Text( - titre, - style: TextStyle( - fontSize: 10, + border: Border.all( color: couleur, + width: 1, ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + iconData, + size: 20, + color: Colors.white, + ), + const SizedBox(height: 2), + Text( + count.toString(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 2), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Text( + titre, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), - ], - ), + ), + // Triangle indicateur de sélection + if (isSelected) + Center( + child: CustomPaint( + size: const Size(20, 12), + painter: _TrianglePainter(color: couleur), + ), + ) + else + const SizedBox(height: 12), + ], ), ); } - /// Colonne NOUVEAU PASSAGE (bouton +, fond vert) + /// Colonne NOUVEAU PASSAGE (bouton +, fond blanc) Widget _buildAddColumn(BuildContext context) { return InkWell( onTap: () { @@ -302,47 +335,55 @@ class BtnPassages extends StatelessWidget { _showPassageFormDialog(context); } }, - child: Container( - height: 80, - decoration: BoxDecoration( - color: AppTheme.buttonSuccessColor.withOpacity(0.1), - border: Border.all( - color: AppTheme.buttonSuccessColor, - width: 1, - ), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(AppTheme.borderRadiusMedium), - bottomRight: Radius.circular(AppTheme.borderRadiusMedium), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - Icons.add_circle_outline, - size: 24, - color: AppTheme.buttonSuccessColor, - ), - const SizedBox(height: 2), - Text( - 'Nouveau', - style: TextStyle( - fontSize: 10, - color: AppTheme.buttonSuccessColor, - fontWeight: FontWeight.w600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey[400]!, + width: 1, + ), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(AppTheme.borderRadiusMedium), + bottomRight: Radius.circular(AppTheme.borderRadiusMedium), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.add_circle_outline, + size: 24, + color: Colors.black87, + ), + const SizedBox(height: 2), + Text( + 'Nouveau', + style: TextStyle( + fontSize: 10, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ], ), - textAlign: TextAlign.center, ), - ], - ), + ), + // Espace pour aligner avec les autres colonnes (pas de triangle sur ce bouton) + const SizedBox(height: 12), + ], ), ); } @@ -377,3 +418,30 @@ class BtnPassages extends StatelessWidget { ); } } + +/// CustomPainter pour dessiner un triangle pointant vers le bas +class _TrianglePainter extends CustomPainter { + final Color color; + + _TrianglePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final path = Path() + ..moveTo(0, 0) // Coin supérieur gauche + ..lineTo(size.width, 0) // Coin supérieur droit + ..lineTo(size.width / 2, size.height) // Pointe en bas au centre + ..close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _TrianglePainter oldDelegate) { + return oldDelegate.color != color; + } +} diff --git a/app/lib/presentation/widgets/mapbox_map.dart b/app/lib/presentation/widgets/mapbox_map.dart index a5881325..17b44e6b 100755 --- a/app/lib/presentation/widgets/mapbox_map.dart +++ b/app/lib/presentation/widgets/mapbox_map.dart @@ -8,6 +8,18 @@ import 'package:latlong2/latlong.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/services/api_service.dart'; // Import du service singleton +/// Enum représentant les différentes sources de tuiles disponibles +enum TileSource { + /// Tuiles Mapbox (par défaut) + mapbox, + /// Tuiles OpenStreetMap + openStreetMap, + /// Tuiles IGN Plan (carte routière française) + ignPlan, + /// Tuiles IGN Ortho Photos (photos aériennes) + ignOrtho, +} + /// Widget de carte réutilisable utilisant Mapbox /// /// Ce widget encapsule un FlutterMap avec des tuiles Mapbox et fournit @@ -46,10 +58,14 @@ class MapboxMap extends StatefulWidget { /// Désactive le drag de la carte final bool disableDrag; - + /// Utiliser OpenStreetMap au lieu de Mapbox (en cas de problème de token) + @Deprecated('Utiliser tileSource à la place') final bool useOpenStreetMap; + /// Source des tuiles de la carte (Mapbox, OpenStreetMap, IGN Plan, IGN Ortho) + final TileSource tileSource; + const MapboxMap({ super.key, this.initialPosition = const LatLng(48.1173, -1.6778), // Rennes par défaut @@ -64,6 +80,7 @@ class MapboxMap extends StatefulWidget { this.mapStyle, this.disableDrag = false, this.useOpenStreetMap = false, + this.tileSource = TileSource.mapbox, }); @override @@ -125,7 +142,7 @@ class _MapboxMapState extends State { _cacheInitialized = true; }); } - debugPrint('MapboxMap: Cache initialisé avec succès pour ${widget.useOpenStreetMap ? "OpenStreetMap" : "Mapbox"}'); + debugPrint('MapboxMap: Cache initialisé avec succès'); } catch (e) { debugPrint('MapboxMap: Erreur lors de l\'initialisation du cache: $e'); // En cas d'erreur, on continue sans cache @@ -175,30 +192,76 @@ class _MapboxMapState extends State { ); } + /// Retourne l'URL template pour la source de tuiles sélectionnée + String _getTileUrlTemplate() { + // Rétrocompatibilité avec useOpenStreetMap + // ignore: deprecated_member_use_from_same_package + if (widget.useOpenStreetMap && widget.tileSource == TileSource.mapbox) { + return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + } + + switch (widget.tileSource) { + case TileSource.openStreetMap: + return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + case TileSource.ignPlan: + // IGN Plan IGN v2 - Carte routière française + // Source: https://data.geopf.fr/wmts + return 'https://data.geopf.fr/wmts?' + 'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0' + '&TILEMATRIXSET=PM' + '&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' + '&STYLE=normal' + '&FORMAT=image/png' + '&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}'; + + case TileSource.ignOrtho: + // IGN Ortho Photos - Photos aériennes + // Source: https://data.geopf.fr/wmts + return 'https://data.geopf.fr/wmts?' + 'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0' + '&TILEMATRIXSET=PM' + '&LAYER=ORTHOIMAGERY.ORTHOPHOTOS' + '&STYLE=normal' + '&FORMAT=image/jpeg' + '&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}'; + + case TileSource.mapbox: + default: + // Déterminer l'URL du template de tuiles Mapbox + final String environment = ApiService.instance.getCurrentEnvironment(); + final String mapboxToken = AppKeys.getMapboxApiKey(environment); + + if (kIsWeb) { + return 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken'; + } else { + return 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken'; + } + } + } + + /// Retourne le nom de la source de tuiles pour le debug + String _getTileSourceName() { + // ignore: deprecated_member_use_from_same_package + if (widget.useOpenStreetMap && widget.tileSource == TileSource.mapbox) { + return 'OpenStreetMap (legacy)'; + } + switch (widget.tileSource) { + case TileSource.mapbox: + return 'Mapbox'; + case TileSource.openStreetMap: + return 'OpenStreetMap'; + case TileSource.ignPlan: + return 'IGN Plan'; + case TileSource.ignOrtho: + return 'IGN Ortho Photos'; + } + } + @override Widget build(BuildContext context) { - String urlTemplate; - - if (widget.useOpenStreetMap) { - // Utiliser OpenStreetMap comme alternative - urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; - debugPrint('MapboxMap: Utilisation d\'OpenStreetMap'); - } else { - // Déterminer l'URL du template de tuiles Mapbox - // Utiliser l'environnement actuel pour obtenir la bonne clé API - final String environment = ApiService.instance.getCurrentEnvironment(); - final String mapboxToken = AppKeys.getMapboxApiKey(environment); - - // Essayer différentes API Mapbox selon la plateforme - if (kIsWeb) { - // Sur web, on peut utiliser l'API styles - urlTemplate = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken'; - } else { - // Sur mobile, utiliser l'API v4 qui fonctionne mieux avec les tokens standards - // Format: mapbox.streets pour les rues, mapbox.satellite pour satellite - urlTemplate = 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken'; - } - } + final urlTemplate = _getTileUrlTemplate(); + debugPrint('MapboxMap: Utilisation de ${_getTileSourceName()}'); // Afficher un indicateur pendant l'initialisation du cache if (!_cacheInitialized) { diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/battery_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/battery_plus index db6ac969..27846f43 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/battery_plus +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/battery_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/connectivity_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/connectivity_plus index 7f5ea2e9..1dfc8a9a 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/connectivity_plus +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/connectivity_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/device_info_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/device_info_plus index ea71a676..3eb57813 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/device_info_plus +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/device_info_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux b/app/linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux index 35da74ba..86bc8fab 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_linux b/app/linux/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_linux index 8e3c000a..ecdc5940 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_linux +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_linux @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux b/app/linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux index 09862b96..f478642a 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux index 5b2295e8..263d8799 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux b/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux index e23af96b..aadcdac4 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/ \ No newline at end of file diff --git a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 5a00c87f..3984b9fa 100644 --- a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/pierre/.local/flutter FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=3.6.2 -FLUTTER_BUILD_NUMBER=362 +FLUTTER_BUILD_NAME=3.6.3 +FLUTTER_BUILD_NUMBER=363 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false diff --git a/app/macos/Flutter/ephemeral/flutter_export_environment.sh b/app/macos/Flutter/ephemeral/flutter_export_environment.sh index 0e5b7b33..60f170df 100755 --- a/app/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/app/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/pierre/.local/flutter" export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=3.6.2" -export "FLUTTER_BUILD_NUMBER=362" +export "FLUTTER_BUILD_NAME=3.6.3" +export "FLUTTER_BUILD_NUMBER=363" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/app/pubspec.lock b/app/pubspec.lock index cbe268fc..f08a74b2 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -411,6 +411,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_compass: + dependency: "direct main" + description: + name: flutter_compass + sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850" + url: "https://pub.dev" + source: hosted + version: "0.8.1" flutter_launcher_icons: dependency: "direct dev" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3720bbe2..f539e159 100755 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: geosector_app description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' publish_to: 'none' -version: 3.6.2+362 +version: 3.6.3+363 environment: sdk: '>=3.0.0 <4.0.0' @@ -48,7 +48,7 @@ dependencies: geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS) geolocator_android: 4.6.1 # ✅ Force version sans toARGB32() universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env) - # sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025) + flutter_compass: ^0.8.1 # Mode boussole pour le mode terrain (Android/iOS uniquement) # Chat et notifications # mqtt5_client: ^4.11.0 diff --git a/app/pubspec.yaml.bak b/app/pubspec.yaml.bak index 334cc896..f539e159 100755 --- a/app/pubspec.yaml.bak +++ b/app/pubspec.yaml.bak @@ -1,7 +1,7 @@ name: geosector_app description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' publish_to: 'none' -version: 3.5.9+359 +version: 3.6.3+363 environment: sdk: '>=3.0.0 <4.0.0' @@ -48,7 +48,7 @@ dependencies: geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS) geolocator_android: 4.6.1 # ✅ Force version sans toARGB32() universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env) - # sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025) + flutter_compass: ^0.8.1 # Mode boussole pour le mode terrain (Android/iOS uniquement) # Chat et notifications # mqtt5_client: ^4.11.0 diff --git a/app/assets/images/geosector-1024x500.png b/app/store_assets/geosector-1024x500.png similarity index 100% rename from app/assets/images/geosector-1024x500.png rename to app/store_assets/geosector-1024x500.png diff --git a/app/assets/images/geosector-admin-amicale-1800x1800.png b/app/store_assets/geosector-admin-amicale-1800x1800.png similarity index 100% rename from app/assets/images/geosector-admin-amicale-1800x1800.png rename to app/store_assets/geosector-admin-amicale-1800x1800.png diff --git a/app/assets/images/geosector-admin-tbord-1800x1800.png b/app/store_assets/geosector-admin-tbord-1800x1800.png similarity index 100% rename from app/assets/images/geosector-admin-tbord-1800x1800.png rename to app/store_assets/geosector-admin-tbord-1800x1800.png diff --git a/app/assets/images/geosector-user-carte-1800x1800.png b/app/store_assets/geosector-user-carte-1800x1800.png similarity index 100% rename from app/assets/images/geosector-user-carte-1800x1800.png rename to app/store_assets/geosector-user-carte-1800x1800.png diff --git a/app/assets/images/geosector-user-histo-1800x1800.png b/app/store_assets/geosector-user-histo-1800x1800.png similarity index 100% rename from app/assets/images/geosector-user-histo-1800x1800.png rename to app/store_assets/geosector-user-histo-1800x1800.png diff --git a/app/assets/images/geosector-user-login-1800x1800.png b/app/store_assets/geosector-user-login-1800x1800.png similarity index 100% rename from app/assets/images/geosector-user-login-1800x1800.png rename to app/store_assets/geosector-user-login-1800x1800.png diff --git a/app/assets/images/geosector-user-stripe-1800x1800.png b/app/store_assets/geosector-user-stripe-1800x1800.png similarity index 100% rename from app/assets/images/geosector-user-stripe-1800x1800.png rename to app/store_assets/geosector-user-stripe-1800x1800.png diff --git a/app/assets/images/geosector-user-tbord-1800x1800.png b/app/store_assets/geosector-user-tbord-1800x1800.png similarity index 100% rename from app/assets/images/geosector-user-tbord-1800x1800.png rename to app/store_assets/geosector-user-tbord-1800x1800.png diff --git a/app/assets/images/geosector_map_admin.png b/app/store_assets/geosector_map_admin.png similarity index 100% rename from app/assets/images/geosector_map_admin.png rename to app/store_assets/geosector_map_admin.png diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/battery_plus b/app/windows/flutter/ephemeral/.plugin_symlinks/battery_plus index db6ac969..27846f43 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/battery_plus +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/battery_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/connectivity_plus b/app/windows/flutter/ephemeral/.plugin_symlinks/connectivity_plus index 7f5ea2e9..1dfc8a9a 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/connectivity_plus +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/connectivity_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/device_info_plus b/app/windows/flutter/ephemeral/.plugin_symlinks/device_info_plus index ea71a676..3eb57813 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/device_info_plus +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/device_info_plus @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows index 75e23968..7c12e441 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_windows index 48049a4c..6571dfe9 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/flutter_local_notifications_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/geolocator_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/geolocator_windows index 15603022..6a33d5af 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/geolocator_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/geolocator_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows index f7f4b36c..e1d26ad8 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows index d33dd328..c60af49d 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows index 02051bfa..455b7b7f 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/ \ No newline at end of file diff --git a/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows b/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows index 38ebd733..3b6b0b63 120000 --- a/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows +++ b/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows @@ -1 +1 @@ -/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4/ \ No newline at end of file +/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/ \ No newline at end of file diff --git a/docs/PLANNING-2026-Q1.md b/docs/PLANNING-2026-Q1.md index 01f6b8b8..52785288 100644 --- a/docs/PLANNING-2026-Q1.md +++ b/docs/PLANNING-2026-Q1.md @@ -1,214 +1,176 @@ # Planning Geosector Q1 2026 - COMPLET -**Période** : 16/01/2026 - 16/03/2026 (60 jours) -**Tâches** : 126 tâches actives +**Période** : 16/01/2026 - 28/02/2026 (44 jours) +**Tâches** : 73 tâches restantes (phases 3-6) **Priorités** : UI/UX et MAP en premier **Stack Techno** : Flutter et Hive pour Web et Mobiles / API REST Full PHP8.3 + --- ## PHASE 1 : BUGS CRITIQUES ### 16-18 janvier (3 jours) - 5 tâches -| Date | ID | Tâche | Catégorie | -|------|-----|-------|-----------| -| 16/01 | #17 | ✅ Création membre impossible | BUG | -| 16/01 | #18 | ✅ Création opération impossible | BUG | -| 17/01 | #19 | ✅ Export opération cassé | BUG | -| 17/01 | #20 | Enregistrement des passages ne fonctionne pas | BUG | -| 18/01 | #14 | Bug F5 - déconnexion lors du rafraîchissement | BUG | +| Date | ID | Tâche | Catégorie | Statut | +|-------|-----|--------------------------------------------------|-----------|-------------------| +| 16/01 | `#17` | ✅ Création membre impossible | BUG | Livré et à tester v3.6.2 | +| 16/01 | `#18` | ✅ Création opération impossible | BUG | Livré et à tester v3.6.2 | +| 16/01 | `#19` | ✅ Export opération cassé | BUG | Livré et à tester v3.6.2 | +| 17/01 | `#20` | ✅ Enregistrement des passages ne fonctionne pas | BUG | Livré et à tester v3.6.2 | +| 17/01 | `#61` | ✅ Valider passage directement depuis carte | | Livré et à tester v3.6.2 | +| 18/01 | `#216` | ✅ Vérifier géolocalisation nouveau passage | Passage | Livré et à tester v3.6.2 | +| 18/01 | `#14` | ✅ Bug F5 - déconnexion lors du rafraîchissement | BUG | à livrer v3.6.3 | --- ## PHASE 2 : STRIPE iOS + UX -### 19-25 janvier (7 jours) - 14 tâches +### 19-25 janvier (7 jours) - 10 tâches -**Tâche principale** : #13 Tests Stripe iOS (5 jours du 19 au 23) +**Tâche principale** : `#13` Tests Stripe iOS (5 jours du 19 au 23) -| Date | Stripe iOS | En parallèle (UX) | -|------|------------|-------------------| -| 19/01 | #13 Jour 1 | #204 Design couleurs flashy | -| 19/01 | | #205 Écrans utilisateurs simplifiés | -| 20/01 | #13 Jour 2 | #113 Couleur repasses orange | -| 20/01 | | #72 Épaisseur police lisibilité | -| 21/01 | #13 Jour 3 | #71 Visibilité bouton "Envoyer message" | -| 21/01 | | #59 Listing rues invisible (clavier) | -| 22/01 | #13 Jour 4 | #46 Figer headers tableau Home | -| 22/01 | | #42 Historique adresses cliquables | -| 23/01 | #13 Jour 5 | #74 Simplifier DashboardLayout/AppScaffold | -| 23/01 | | #110 Supprimer refresh session partiels | -| 24/01 | Buffer | #28 Gestion reçus Flutter nouveaux champs | -| 25/01 | Buffer | #50 Modifier secteur au clic | -| 25/01 | | #41 Secteurs avec membres visible carte | +| Date | Stripe iOS | En parallèle (UX) | Statut | +|-------|------------|--------------------------------------------|--------| +| 19/01 | `#13` Jour 1 | ✅ `#204` Design couleurs flashy | à 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 | | `#72`Épaisseur police lisibilité | | +| 21/01 | `#13` Jour 3 | `#71`Visibilité bouton "Envoyer message" | | +| 21/01 | | `#59`Listing rues invisible (clavier) | | +| 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 | | +| 25/01 | | `#50`Modifier secteur au clic | | --- ## PHASE 3 : MAP / CARTE -### 26 janvier - 9 février (15 jours) - 28 tâches +### 26 janvier - 7 février (10 jours) - 25 tâches -| Date | ID | Tâche | -|------|-----|-------| -| 26/01 | #206 | Corriger géolocalisation par défaut Rennes | -| 26/01 | #22 | S'assurer cache Mapbox en place | -| 27/01 | #215 | Mode boussole + carte IGN/satellite zoom max | -| 27/01 | #53 | Définir zoom maximal éviter sur-zoom | -| 28/01 | #37 | Clic sur la carte pour créer un passage | -| 28/01 | #61 | Valider passage directement depuis carte | -| 29/01 | #51 | Déplacer markers double-clic | -| 29/01 | #115 | Déplacement marker sans bouton Enregistrer | -| 30/01 | #123 | Déplacer rapidement un pointeur | -| 30/01 | #58 | Points carte devant textes (z-index) | -| 31/01 | #55 | Optimiser précision GPS mode terrain | -| 31/01 | #56 | Mode Web : se déplacer sur carte terrain | -| 01/02 | #57 | Mode terrain smartphone : zoom auto | -| 01/02 | #60 | Recherche rue hors proximité | -| 02/02 | #209 | Filtres Particuliers / Entreprises | -| 02/02 | #216 | Vérifier géolocalisation nouveau passage | -| 03/02 | #217 | Chercher adresse hors secteur | -| 03/02 | #49 | Secteur sans membre | -| 04/02 | #25 | Membres affectés en 1er modif secteur | -| 04/02 | #31 | Gestion ajout/suppression membre secteur | -| 05/02 | #54 | Style carte type Snapchat | -| 05/02 | #210 | Base SIREN géolocalisation entreprises | -| 06/02 | #67 | Graphique règlements par secteur | -| 06/02 | #104 | Tests multi-départements | -| 07/02 | #89 | Page clients paiements en ligne | -| 07/02 | #94 | Paiement en ligne formulaire passage | -| 08/02 | #96 | Option "Paiement par carte" | -| 08/02 | #99 | Paiement Stripe mode hors ligne | -| 09/02 | Buffer MAP | - | +| Date | ID | Tâche | Statut | +|-------|----------|----------------------------------------------|--------| +| 26/01 | `#206` | Corriger géolocalisation par défaut Rennes | | +| 26/01 | `#22` | S'assurer cache Mapbox en place | | +| 26/01 | `#215` | ✅ Mode boussole + carte IGN/satellite zoom max | à livrer v3.6.3 | +| 27/01 | `#53` | ✅ Définir zoom maximal éviter sur-zoom | à livrer v3.6.3 | +| 27/01 | `#37` | Clic sur la carte pour créer un passage | | +| 28/01 | `#51` | Déplacer markers double-clic | | +| 28/01 | `#115` | Déplacement marker sans bouton Enregistrer | | +| 28/01 | `#123` | Déplacer rapidement un pointeur | | +| 29/01 | `#58` | Points carte devant textes (z-index) | | +| 29/01 | `#55` | Optimiser précision GPS mode terrain | | +| 29/01 | `#56` | Se déplacer librement sur carte terrain | | +| 30/01 | `#57` | Mode terrain smartphone : zoom auto | | +| 30/01 | `#60` | Recherche rue hors proximité | | +| 30/01 | `#209` | Filtres Particuliers / Entreprises | | +| 31/01 | `#49` | Secteur possible sans membre | | +| 01/02 | `#25` | Membres affectés en 1er modif secteur | | +| 02/02 | `#210` | Base SIREN géolocalisation entreprises | | +| 02/02 | `#67` | Graphique règlements par secteur | | +| 03/02 | `#104` | Tests multi-départements | | +| 03/02 | `#89` | Page clients paiements en ligne | | +| 04/02 | `#99` | Paiement Stripe mode hors ligne | | --- ## PHASE 4 : STRIPE + PASSAGES -### 10-21 février (12 jours) - 20 tâches +### 8-14 février (6 jours) - 11 tâches -| Date | ID | Tâche | Cat | -|------|-----|-------|-----| -| 10/02 | #92 | 💳 Stripe (config générale) | STRIPE | -| 10/02 | #93 | Double configuration Stripe | STRIPE | -| 11/02 | #97 | Interface paiement sécurisée intégrée | STRIPE | -| 11/02 | #98 | Génération auto reçu après paiement | STRIPE | -| 13/02 | #207 | Dashboard clic card règlement filtrer | | -| 13/02 | #208 | Type règlement Virement bancaire à ajouter | | -| 14/02 | #62 | 📋 Gestion des passages | PASSAGE | -| 14/02 | #16 | Modifier passage sur l'application | PASSAGE | -| 15/02 | #40 | Suppression lot de passages | PASSAGE | -| 15/02 | #63 | Corbeille passages admin | PASSAGE | -| 16/02 | #64 | Supprimer passages sauvegardés | PASSAGE | -| 16/02 | #66 | Récupérer passages supprimés | PASSAGE | -| 17/02 | #65 | Désactiver envoi reçu temporaire | PASSAGE | -| 17/02 | #118 | Prévenir habitants du passage | PASSAGE | -| 18/02 | #119 | Historique montant année précédente | PASSAGE | -| 19/02 | #81 | Ralentissement suppressions amicales | BUG | -| 19/02 | #219 | Double authentification super-admin (fk_role=9) | ADMIN | -| 20-21/02 | Buffer | - | - | +| Date | ID | Tâche | Cat | Statut | +|-------|------|-------------------------------------------------|---------|--------| +| 08/02 | `#98` | Génération auto reçu après paiement | STRIPE | | +| 08/02 | `#207` | Dashboard clic card règlement filtrer | | | +| 09/02 | `#208` | Type règlement Virement bancaire à ajouter | | | +| 09/02 | `#16` | Modifier passage sur l'application | PASSAGE | | +| 10/02 | `#40` | Suppression lot de passages | PASSAGE | | +| 10/02 | `#63` | Corbeille passages admin | PASSAGE | | +| 11/02 | `#66` | Récupérer passages supprimés | PASSAGE | | +| 11/02 | `#65` | Désactiver envoi reçu temporaire | PASSAGE | | +| 12/02 | `#119` | Historique montant année précédente | PASSAGE | | +| 13/02 | `#81` | Ralentissement suppressions amicales | BUG | | +| 14/02 | `#219` | Double authentification super-admin (fk_role=9) | ADMIN | | --- ## PHASE 5 : ADMIN + MEMBRES -### 22 février - 6 mars (13 jours) - 29 tâches +### 15-22 février (7 jours) - 22 tâches -| Date | ID | Tâche | Cat | -|------|-----|-------|-----| -| 22/02 | #79 | 👑 Mode Super Admin | ADMIN | -| 22/02 | #80 | FAQ gérée depuis Super-Admin | ADMIN | -| 23/02 | #76 | Accès admin limité web uniquement | ADMIN | -| 23/02 | #77 | Choisir rôle admin/membre connexion | ADMIN | -| 24/02 | #78 | Admin peut se connecter utilisateur | ADMIN | -| 24/02 | #82 | Optimiser purge données | ADMIN | -| 25/02 | #83 | Filtres liste amicales | ADMIN | -| 25/02 | #85 | Distinguer amicales actives | ADMIN | -| 26/02 | #24 | Trier liste membres | ADMIN | -| 26/02 | #29 | Filtres liste membres | ADMIN | -| 27/02 | #33 | Communication membres <-> admin | ADMIN | -| 27/02 | #70 | Revoir chat complet | ADMIN | -| 28/02 | #108 | MQTT temps réel ⭐⭐⭐ | ADMIN | -| 28/02 | #43 | Nb amicales partenariat ODP | ADMIN | -| 01/03 | #211 | Modifier lots avec montants | ADMIN | -| 01/03 | #218 | Tests montée charge Poissy | ADMIN | -| 02/03 | #15 | Nouveau membre non synchronisé | MEMBRE | -| 02/03 | #23 | Emails failed intégrer base | MEMBRE | -| 03/03 | #26 | Figer membres combobox | MEMBRE | -| 03/03 | #27 | Autocomplete combobox membres | MEMBRE | -| 04/03 | #30 | Membres sélectionnés haut liste | MEMBRE | -| 04/03 | #32 | Modifier identifiant utilisateur | MEMBRE | -| 05/03 | #34 | Email non obligatoire | MEMBRE | -| 05/03 | #36 | Textes aide fiches membres | MEMBRE | -| 06/03 | #90 | 📧 Processus inscription | MEMBRE | -| 06/03 | #91 | 2 emails séparés inscription | MEMBRE | -| 06/03 | #117 | Prénoms accents majuscule | MEMBRE | -| 06/03 | #122 | Modif rapide email renvoi reçu | MEMBRE | +| Date | ID | Tâche | Cat | Statut | +|-------|------|-------------------------------------|--------|--------| +| 15/02 | `#80` | FAQ gérée depuis Super-Admin | ADMIN | | +| 15/02 | `#76` | Accès admin limité web uniquement | ADMIN | | +| 15/02 | `#82` | Optimiser purge données | ADMIN | | +| 16/02 | `#83` | Filtres liste amicales | ADMIN | | +| 16/02 | `#85` | Distinguer amicales actives | ADMIN | | +| 16/02 | `#24` | Trier liste membres | ADMIN | | +| 17/02 | `#29` | Filtres liste membres | ADMIN | | +| 17/02 | `#70` | Revoir chat complet | ADMIN | | +| 17/02 | `#108` | Temps réel chat et data ⭐⭐⭐ | ADMIN | | +| 18/02 | `#211` | Modifier lots avec montants | ADMIN | | +| 18/02 | `#218` | Tests montée charge Poissy | ADMIN | | +| 18/02 | `#15` | Nouveau membre non synchronisé | MEMBRE | | +| 19/02 | `#23` | Emails failed intégrer base | MEMBRE | | +| 19/02 | `#26` | Figer membres combobox | MEMBRE | | +| 19/02 | `#27` | Autocomplete combobox membres | MEMBRE | | +| 20/02 | `#30` | Membres sélectionnés haut liste | MEMBRE | | +| 20/02 | `#32` | Modifier identifiant utilisateur | MEMBRE | | +| 20/02 | `#34` | Email non obligatoire | MEMBRE | | +| 21/02 | `#36` | Textes aide fiches membres | MEMBRE | | +| 21/02 | `#91` | 2 emails séparés inscription | MEMBRE | | +| 22/02 | `#117` | Prénoms accents majuscule | MEMBRE | | +| 22/02 | `#122` | Modif rapide email renvoi reçu | MEMBRE | | --- ## PHASE 6 : EXPORT + COM + DIVERS -### 7-16 mars (10 jours) - 30 tâches +### 23-28 février (5 jours) - 15 tâches -| Date | ID | Tâche | Cat | -|------|-----|-------|-----| -| 07/03 | #45 | Home filtres et graphes | EXPORT | -| 07/03 | #47 | Home bouton export données | EXPORT | -| 07/03 | #48 | Export par membre | EXPORT | -| 08/03 | #68 | Comparatif année précédente | EXPORT | -| 08/03 | #212 | Bergerac logs + export Excel | EXPORT | -| 09/03 | #35 | Bouton alerte 3s messagerie | COM | -| 09/03 | #109 | SMS impératif ⭐⭐⭐ | COM | -| 10/03 | #69 | Bloquer création opération | OPER | -| 10/03 | #86 | Suppression opé réactiver précédente | OPER | -| 10/03 | #87 | 🏢 Gestion Clients | OPER | -| 11/03 | #88 | Écran Clients créer/améliorer | OPER | -| 11/03 | #116 | Remarque sous adresse | OPER | -| 11/03 | #214 | Opérations afficher texte | OPER | -| 12/03 | #102 | Compatibilité appareils test | TEST | -| 12/03 | #103 | 🧪 Tests | TEST | -| 12/03 | #213 | Lots montant nb calendriers Poissy | TEST | -| 13/03 | #21 | Requêtes en attente dupliquées | AUTRE | -| 13/03 | #38 | Parrainage | AUTRE | -| 13/03 | #39 | Multilingue ? | AUTRE | -| 14/03 | #44 | Envoi contrat | AUTRE | -| 14/03 | #52 | Même adresse par niveau | AUTRE | -| 14/03 | #73 | Reconnaissance biométrique | AUTRE | -| 14/03 | #75 | Refactoriser responsabilités | AUTRE | -| 15/03 | #84 | Mode démo présentations | AUTRE | -| 15/03 | #105 | 🌍 Internationalization | AUTRE | -| 15/03 | #106 | Devises Franc Suisse | AUTRE | -| 15/03 | #107 | 📡 Fonctionnalités futures | AUTRE | -| 16/03 | #111 | iwanttobealone | AUTRE | -| 16/03 | #112 | db-backup site 256 | AUTRE | -| 16/03 | #114 | Liste adresses mail d6soft/unikoffice | AUTRE | -| 16/03 | #120 | Double auth faceId/touchId | AUTRE | -| 16/03 | #121 | Recette (lien) | AUTRE | +| Date | ID | Tâche | Cat | Statut | +|-------|------|--------------------------------------|--------|--------| +| 23/02 | `#45` | Home filtres et graphes | EXPORT | | +| 23/02 | `#48` | Export par membre | EXPORT | | +| 23/02 | `#68` | Comparatif année précédente | EXPORT | | +| 24/02 | `#212` | Bergerac logs + export Excel | EXPORT | | +| 24/02 | `#35` | Bouton alerte 3s messagerie | COM | | +| 24/02 | `#109` | SMS impératif ⭐⭐⭐ | COM | | +| 25/02 | `#69` | Bloquer création opération | OPER | | +| 25/02 | `#86` | Suppression opé réactiver précédente | OPER | | +| 25/02 | `#88` | Écran Clients créer/améliorer | OPER | | +| 26/02 | `#116` | Remarque sous adresse | OPER | | +| 26/02 | `#214` | Opérations afficher texte | OPER | | +| 26/02 | `#213` | Lots montant nb calendriers Poissy | TEST | | +| 27/02 | `#21` | Requêtes en attente dupliquées | AUTRE | | +| 27/02 | `#73` | Reconnaissance biométrique : touchId | AUTRE | | +| 28/02 | `#106` | Devises Franc Suisse | AUTRE | | --- ## RÉCAPITULATIF -| Phase | Période | Jours | Tâches | Focus | -|-------|---------|-------|--------|-------| -| 1 | 16-18/01 | 3 | 5 | Bugs critiques | -| 2 | 19-25/01 | 7 | 14 | **Stripe iOS #13** + UX | -| 3 | 26/01-09/02 | 15 | 28 | **MAP / Carte** | -| 4 | 10-21/02 | 12 | 20 | Stripe + Passages | -| 5 | 22/02-06/03 | 13 | 29 | Admin + Membres | -| 6 | 07-16/03 | 10 | 30 | Export + Divers | -| **TOTAL** | **60 jours** | | **126** | | +| Phase | Période | Jours | Tâches | Focus | +|-----------|--------------|-------|--------|---------------------| +| 1 | 16-18/01 | 3 | 5 | Bugs critiques | +| 2 | 19-25/01 | 7 | 10 | Stripe iOS + UX | +| 3 | 26/01-07/02 | 10 | 25 | MAP / Carte | +| 4 | 08-14/02 | 6 | 11 | Stripe + Passages | +| 5 | 15-22/02 | 7 | 22 | Admin + Membres | +| 6 | 23-28/02 | 5 | 15 | Export + Divers | +| **TOTAL** | **44 jours** | | **88** | | --- ## Jalons clés - **18/01** : Bugs critiques résolus -- **23/01** : Tests Stripe iOS terminés -- **09/02** : Carte/Map finalisée -- **21/02** : Paiements + Passages OK -- **06/03** : Admin + Membres OK -- **16/03** : **Livraison Q1 complète** +- **25/01** : Stripe iOS + UX terminés +- **07/02** : Carte/Map finalisée +- **14/02** : Paiements + Passages OK +- **22/02** : Admin + Membres OK +- **28/02** : **Livraison complète** --- ## Notes -- Rythme : ~2 tâches/jour en moyenne -- Weekends = buffer si besoin -- Tâches ⭐⭐⭐ (#108 MQTT, #109 SMS) intégrées dans le planning -- La tâche #13 (Stripe iOS) reste bloquante pour paiement mobile +- Rythme : ~2-3 tâches/jour +- Weekends inclus comme buffer si retard +- Tâches ⭐⭐⭐ (`#108` temps réel, `#109` SMS) intégrées +- Phase 3 (MAP) reste la plus chargée : 25 tâches en 10 jours diff --git a/docs/planning-geosector-q1-2026.xls b/docs/planning-geosector-q1-2026.xls new file mode 100644 index 00000000..bfa69d7f --- /dev/null +++ b/docs/planning-geosector-q1-2026.xls @@ -0,0 +1,163 @@ + + + + + + + + +

Planning Geosector Q1 2026

+ + + + + + + + + + +
SEMAINE 1 : 16-18 janvier - Phase 1 (Bugs critiques)
IDTâcheStatut
#17Création membre impossibleLivré et à tester
#18Création opération impossibleLivré et à tester
#19Export opération casséLivré et à tester
#20Enregistrement des passages ne fonctionne pasLivré et à tester
#14Bug F5 - déconnexion lors du rafraîchissement
+ + + + + + + + + + + + + + + + +
SEMAINE 2 : 19-25 janvier - Phase 2 (Stripe iOS + UX)
IDTâcheStatut
#13Tests Stripe iOS (5 jours)
#204Design couleurs flashy
#205Écrans utilisateurs simplifiés
#113Couleur repasses orange
#72Épaisseur police lisibilité
#71Visibilité bouton "Envoyer message"
#59Listing rues invisible (clavier)
#42Historique adresses cliquables
#74Simplifier DashboardLayout/AppScaffold
#28Gestion reçus Flutter nouveaux champs
#50Modifier secteur au clic
+ + + + + + + + + + + + + + + + + + + + + + + + + +
SEMAINE 3 : 26 janvier - 1er février - Phase 3 (MAP / Carte)
IDTâcheStatut
#206Corriger géolocalisation par défaut Rennes
#22S'assurer cache Mapbox en place
#215Mode boussole + carte IGN/satellite zoom max
#53Définir zoom maximal éviter sur-zoom
#37Clic sur la carte pour créer un passage
#61Valider passage directement depuis carte
#51Déplacer markers double-clic
#115Déplacement marker sans bouton Enregistrer
#123Déplacer rapidement un pointeur
#58Points carte devant textes (z-index)
#55Optimiser précision GPS mode terrain
#56Se déplacer librement sur carte terrain
#57Mode terrain smartphone : zoom auto
#60Recherche rue hors proximité
#209Filtres Particuliers / Entreprises
#216Vérifier géolocalisation nouveau passage
#217Chercher adresse hors secteur
#49Secteur sans membre
#25Membres affectés en 1er modif secteur
#31Gestion ajout/suppression membre secteur
+ + + + + + + + + + + + + + +
SEMAINE 4 : 2-8 février - Phase 3 (fin) + Phase 4 (début)
IDTâcheStatut
Phase 3 - MAP (fin)
#210Base SIREN géolocalisation entreprises
#67Graphique règlements par secteur
#104Tests multi-départements
#89Page clients paiements en ligne
#99Paiement Stripe mode hors ligne
Phase 4 - Stripe + Passages (début)
#98Génération auto reçu après paiement
#207Dashboard clic card règlement filtrer
+ + + + + + + + + + + + + + + + + + + +
SEMAINE 5 : 9-15 février - Phase 4 (fin) + Phase 5 (début)
IDTâcheStatut
Phase 4 - Stripe + Passages (fin)
#208Type règlement Virement bancaire à ajouter
#16Modifier passage sur l'application
#40Suppression lot de passages
#63Corbeille passages admin
#66Récupérer passages supprimés
#65Désactiver envoi reçu temporaire
#119Historique montant année précédente
#81Ralentissement suppressions amicales
#219Double authentification super-admin (fk_role=9)
Phase 5 - Admin + Membres (début)
#80FAQ gérée depuis Super-Admin
#76Accès admin limité web uniquement
#82Optimiser purge données
+ + + + + + + + + + + + + + + + + + + + + + + + +
SEMAINE 6 : 16-22 février - Phase 5 (Admin + Membres)
IDTâcheStatut
#83Filtres liste amicales
#85Distinguer amicales actives
#24Trier liste membres
#29Filtres liste membres
#70Revoir chat complet
#108Temps réel chat et data ⭐⭐⭐
#211Modifier lots avec montants
#218Tests montée charge Poissy
#15Nouveau membre non synchronisé
#23Emails failed intégrer base
#26Figer membres combobox
#27Autocomplete combobox membres
#30Membres sélectionnés haut liste
#32Modifier identifiant utilisateur
#34Email non obligatoire
#36Textes aide fiches membres
#912 emails séparés inscription
#117Prénoms accents majuscule
#122Modif rapide email renvoi reçu
+ + + + + + + + + + + + + + + + + + + + +
SEMAINE 7 : 23-28 février - Phase 6 (Export + Divers)
IDTâcheStatut
#45Home filtres et graphes
#48Export par membre
#68Comparatif année précédente
#212Bergerac logs + export Excel
#35Bouton alerte 3s messagerie
#109SMS impératif ⭐⭐⭐
#69Bloquer création opération
#86Suppression opé réactiver précédente
#88Écran Clients créer/améliorer
#116Remarque sous adresse
#214Opérations afficher texte
#213Lots montant nb calendriers Poissy
#21Requêtes en attente dupliquées
#73Reconnaissance biométrique : touchId
#106Devises Franc Suisse
+ + +