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 8af9f13e..00000000 Binary files a/app/assets/images/logo-geosector-512.png-autosave.kra and /dev/null differ diff --git a/app/deploy-app.sh b/app/deploy-app.sh index e08b5b2d..e75ba1f8 100755 --- a/app/deploy-app.sh +++ b/app/deploy-app.sh @@ -254,9 +254,13 @@ EOF echo_info "Fixing web assets structure..." ./copy-web-images.sh || echo_error "Failed to fix web assets" - # Créer l'archive depuis le build + # Créer l'archive depuis le build (avec exclusions pour réduire la taille) echo_info "Creating archive from build..." - tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} . || echo_error "Failed to create archive" + tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} \ + --exclude='*.symbols' \ + --exclude='*.kra' \ + --exclude='.DS_Store' \ + . || echo_error "Failed to create archive" create_local_backup "${TEMP_ARCHIVE}" "dev" diff --git a/app/geosector_app.iml b/app/geosector_app.iml deleted file mode 100755 index 628505df..00000000 --- a/app/geosector_app.iml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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
+ + +