feat: Début des évolutions interfaces mobiles v3.2.4
- Préparation de la nouvelle branche pour les évolutions - Mise à jour de la version vers 3.2.4 - Intégration des modifications en cours 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,42 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Script de déploiement pour GEOSECTOR API
|
# Script de déploiement unifié pour GEOSECTOR API
|
||||||
# Version: 3.0 (10 mai 2025)
|
# Version: 4.0 (Janvier 2025)
|
||||||
# Auteur: Pierre (avec l'aide de Claude)
|
# Auteur: Pierre (avec l'aide de Claude)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy-api.sh # Déploiement local DEV (code → container geo)
|
||||||
|
# ./deploy-api.sh rca # Livraison RECETTE (container geo → rca-geo)
|
||||||
|
# ./deploy-api.sh pra # Livraison PRODUCTION (rca-geo → pra-geo)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Configuration générale
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
# Paramètre optionnel pour l'environnement cible
|
||||||
|
TARGET_ENV=${1:-dev}
|
||||||
|
|
||||||
|
# Configuration SSH
|
||||||
|
HOST_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||||
|
HOST_PORT="22"
|
||||||
|
HOST_USER="root"
|
||||||
|
|
||||||
# Configuration des serveurs
|
# Configuration des serveurs
|
||||||
JUMP_USER="root"
|
RCA_HOST="195.154.80.116" # Serveur de recette
|
||||||
JUMP_HOST="195.154.80.116"
|
PRA_HOST="51.159.7.190" # Serveur de production
|
||||||
JUMP_PORT="22"
|
|
||||||
JUMP_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
|
||||||
|
|
||||||
# Paramètres du container Incus
|
# Configuration Incus
|
||||||
INCUS_PROJECT=default
|
INCUS_PROJECT="default"
|
||||||
INCUS_CONTAINER=dva-geo
|
API_PATH="/var/www/geosector/api"
|
||||||
CONTAINER_USER=root
|
|
||||||
|
|
||||||
# Paramètres de déploiement
|
|
||||||
FINAL_PATH="/var/www/geosector/api"
|
|
||||||
FINAL_OWNER="nginx"
|
FINAL_OWNER="nginx"
|
||||||
FINAL_GROUP="nginx"
|
FINAL_GROUP="nginx"
|
||||||
FINAL_OWNER_LOGS="nobody"
|
FINAL_OWNER_LOGS="nobody"
|
||||||
|
|
||||||
|
# Configuration de sauvegarde
|
||||||
|
BACKUP_DIR="/data/backup/geosector"
|
||||||
|
|
||||||
# Couleurs pour les messages
|
# Couleurs pour les messages
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -30,134 +44,311 @@ YELLOW='\033[0;33m'
|
|||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
run_in_container() {
|
# =====================================
|
||||||
echo "-> Running: $*"
|
# Fonctions utilitaires
|
||||||
incus exec "${INCUS_CONTAINER}" -- "$@" || {
|
# =====================================
|
||||||
echo "❌ Failed to run: $*"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fonction pour afficher les messages d'étape
|
|
||||||
echo_step() {
|
echo_step() {
|
||||||
echo -e "${GREEN}==>${NC} $1"
|
echo -e "${GREEN}==>${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fonction pour afficher les informations
|
|
||||||
echo_info() {
|
echo_info() {
|
||||||
echo -e "${BLUE}Info:${NC} $1"
|
echo -e "${BLUE}Info:${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fonction pour afficher les avertissements
|
|
||||||
echo_warning() {
|
echo_warning() {
|
||||||
echo -e "${YELLOW}Warning:${NC} $1"
|
echo -e "${YELLOW}Warning:${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fonction pour afficher les erreurs
|
|
||||||
echo_error() {
|
echo_error() {
|
||||||
echo -e "${RED}Error:${NC} $1"
|
echo -e "${RED}Error:${NC} $1"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vérification de l'environnement
|
# Fonction pour créer une sauvegarde locale
|
||||||
echo_step "Verifying environment..."
|
create_local_backup() {
|
||||||
|
local archive_file=$1
|
||||||
|
local backup_type=$2
|
||||||
|
|
||||||
|
echo_info "Creating backup in ${BACKUP_DIR}..."
|
||||||
|
|
||||||
|
if [ ! -d "${BACKUP_DIR}" ]; then
|
||||||
|
mkdir -p "${BACKUP_DIR}" || echo_warning "Could not create backup directory ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${BACKUP_DIR}" ]; then
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/api-${backup_type}-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
cp "${archive_file}" "${BACKUP_FILE}" && {
|
||||||
|
echo_info "Backup saved to: ${BACKUP_FILE}"
|
||||||
|
echo_info "Backup size: $(du -h "${BACKUP_FILE}" | cut -f1)"
|
||||||
|
|
||||||
|
# Nettoyer les anciens backups (garder les 10 derniers)
|
||||||
|
echo_info "Cleaning old backups (keeping last 10)..."
|
||||||
|
ls -t "${BACKUP_DIR}"/api-${backup_type}-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f && {
|
||||||
|
REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/api-${backup_type}-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
echo_info "Kept ${REMAINING_BACKUPS} backup(s)"
|
||||||
|
}
|
||||||
|
} || echo_warning "Failed to create backup in ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Vérification des fichiers requis
|
# =====================================
|
||||||
if [ ! -f "src/Config/AppConfig.php" ]; then
|
# Détermination de la configuration selon l'environnement
|
||||||
echo_error "Configuration file missing"
|
# =====================================
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "composer.json" ] || [ ! -f "composer.lock" ]; then
|
case $TARGET_ENV in
|
||||||
echo_error "Composer files missing"
|
"dev")
|
||||||
fi
|
echo_step "Configuring for LOCAL DEV deployment"
|
||||||
|
SOURCE_TYPE="local_code"
|
||||||
|
DEST_CONTAINER="geo"
|
||||||
|
DEST_HOST="local"
|
||||||
|
ENV_NAME="DEVELOPMENT"
|
||||||
|
;;
|
||||||
|
"rca")
|
||||||
|
echo_step "Configuring for RECETTE delivery"
|
||||||
|
SOURCE_TYPE="local_container"
|
||||||
|
SOURCE_CONTAINER="geo"
|
||||||
|
DEST_CONTAINER="rca-geo"
|
||||||
|
DEST_HOST="${RCA_HOST}"
|
||||||
|
ENV_NAME="RECETTE"
|
||||||
|
;;
|
||||||
|
"pra")
|
||||||
|
echo_step "Configuring for PRODUCTION delivery"
|
||||||
|
SOURCE_TYPE="remote_container"
|
||||||
|
SOURCE_HOST="${RCA_HOST}"
|
||||||
|
SOURCE_CONTAINER="rca-geo"
|
||||||
|
DEST_CONTAINER="pra-geo"
|
||||||
|
DEST_HOST="${PRA_HOST}"
|
||||||
|
ENV_NAME="PRODUCTION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo_error "Unknown environment: $TARGET_ENV. Use 'dev', 'rca' or 'pra'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Étape 0: Définir le nom de l'archive
|
echo_info "Deployment flow: ${ENV_NAME}"
|
||||||
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
|
|
||||||
|
# =====================================
|
||||||
|
# Création de l'archive selon la source
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
ARCHIVE_NAME="api-deploy-${TIMESTAMP}.tar.gz"
|
||||||
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||||
echo_info "Archive name will be: $ARCHIVE_NAME"
|
|
||||||
|
|
||||||
# Étape 1: Créer une archive du projet
|
if [ "$SOURCE_TYPE" = "local_code" ]; then
|
||||||
echo_step "Creating project archive..."
|
# DEV: Créer une archive depuis le code local
|
||||||
tar --exclude='.git' \
|
echo_step "Creating archive from local code..."
|
||||||
--exclude='.gitignore' \
|
|
||||||
--exclude='.vscode' \
|
# Vérification des fichiers requis
|
||||||
--exclude='logs' \
|
if [ ! -f "src/Config/AppConfig.php" ]; then
|
||||||
--exclude='*.template' \
|
echo_error "Configuration file missing"
|
||||||
--exclude='*.sh' \
|
fi
|
||||||
--exclude='.env' \
|
|
||||||
--exclude='*.log' \
|
if [ ! -f "composer.json" ] || [ ! -f "composer.lock" ]; then
|
||||||
--exclude='.DS_Store' \
|
echo_error "Composer files missing"
|
||||||
--exclude='README.md' \
|
fi
|
||||||
--exclude="*.tar.gz" \
|
|
||||||
--exclude='node_modules' \
|
tar --exclude='.git' \
|
||||||
--exclude='vendor' \
|
--exclude='.gitignore' \
|
||||||
--exclude='*.swp' \
|
--exclude='.vscode' \
|
||||||
--exclude='*.swo' \
|
--exclude='logs' \
|
||||||
--exclude='*~' \
|
--exclude='*.template' \
|
||||||
--warning=no-file-changed \
|
--exclude='*.sh' \
|
||||||
--no-xattrs \
|
--exclude='.env' \
|
||||||
-czf "${TEMP_ARCHIVE}" . || echo_error "Failed to create archive"
|
--exclude='*.log' \
|
||||||
|
--exclude='.DS_Store' \
|
||||||
|
--exclude='README.md' \
|
||||||
|
--exclude="*.tar.gz" \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='vendor' \
|
||||||
|
--exclude='*.swp' \
|
||||||
|
--exclude='*.swo' \
|
||||||
|
--exclude='*~' \
|
||||||
|
--warning=no-file-changed \
|
||||||
|
--no-xattrs \
|
||||||
|
-czf "${TEMP_ARCHIVE}" . || echo_error "Failed to create archive"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# RCA: Créer une archive depuis le container local
|
||||||
|
echo_step "Creating archive from local container ${SOURCE_CONTAINER}..."
|
||||||
|
|
||||||
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch project"
|
||||||
|
|
||||||
|
# Créer l'archive directement depuis le container local
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar \
|
||||||
|
--exclude='logs' \
|
||||||
|
--exclude='uploads' \
|
||||||
|
--warning=no-file-changed \
|
||||||
|
-czf /tmp/${ARCHIVE_NAME} -C ${API_PATH} . || echo_error "Failed to create archive from container"
|
||||||
|
|
||||||
|
# Récupérer l'archive depuis le container
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to pull archive from container"
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-rca"
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "remote_container" ]; then
|
||||||
|
# PRA: Créer une archive depuis un container distant
|
||||||
|
echo_step "Creating archive from remote container ${SOURCE_CONTAINER} on ${SOURCE_HOST}..."
|
||||||
|
|
||||||
|
# Créer l'archive sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar \
|
||||||
|
--exclude='logs' \
|
||||||
|
--exclude='uploads' \
|
||||||
|
--warning=no-file-changed \
|
||||||
|
-czf /tmp/${ARCHIVE_NAME} -C ${API_PATH} .
|
||||||
|
" || echo_error "Failed to create archive on remote"
|
||||||
|
|
||||||
|
# Extraire l'archive du container vers l'hôte
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} /tmp/${ARCHIVE_NAME} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to extract archive from remote container"
|
||||||
|
|
||||||
|
# Copier l'archive vers la machine locale pour backup
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST}:/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to copy archive locally"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-pra"
|
||||||
|
fi
|
||||||
|
|
||||||
# Vérifier la taille de l'archive
|
|
||||||
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
||||||
|
echo_info "Archive size: ${ARCHIVE_SIZE}"
|
||||||
|
|
||||||
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
|
# =====================================
|
||||||
|
# Déploiement selon la destination
|
||||||
|
# =====================================
|
||||||
|
|
||||||
# Étape 2: Copier l'archive vers le serveur de saut
|
if [ "$DEST_HOST" = "local" ]; then
|
||||||
echo_step "Copying archive to jump server..."
|
# Déploiement sur container local (DEV)
|
||||||
echo_info "Archive size: $ARCHIVE_SIZE"
|
echo_step "Deploying to local container ${DEST_CONTAINER}..."
|
||||||
scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${TEMP_ARCHIVE}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server"
|
|
||||||
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch to project ${INCUS_PROJECT}"
|
||||||
echo_step "Deploying to Incus container..."
|
|
||||||
$SSH_JUMP_CMD "
|
echo_info "Pushing archive to container..."
|
||||||
set -euo pipefail
|
incus file push "${TEMP_ARCHIVE}" ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} || echo_error "Failed to push archive to container"
|
||||||
|
|
||||||
echo '✅ Passage au projet Incus...'
|
echo_info "Preparing deployment directory..."
|
||||||
incus project switch ${INCUS_PROJECT} || exit 1
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH} || echo_error "Failed to create deployment directory"
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -rf ${API_PATH}/* || echo_warning "Could not clean deployment directory"
|
||||||
echo '📦 Poussée de archive dans le conteneur...'
|
|
||||||
incus file push /tmp/${ARCHIVE_NAME} ${INCUS_CONTAINER}/tmp/${ARCHIVE_NAME} || exit 1
|
echo_info "Extracting archive..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${API_PATH}/ || echo_error "Failed to extract archive"
|
||||||
echo '📁 Préparation du dossier final...'
|
|
||||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH} || exit 1
|
echo_info "Setting permissions..."
|
||||||
incus exec ${INCUS_CONTAINER} -- rm -rf ${FINAL_PATH}/* || exit 1
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/logs ${API_PATH}/uploads
|
||||||
incus exec ${INCUS_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${FINAL_PATH}/ || exit 1
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${API_PATH}
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type d -exec chmod 755 {} \;
|
||||||
echo '🔧 Réglage des permissions...'
|
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type f -exec chmod 644 {} \;
|
||||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH}/logs || exit 1
|
|
||||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${FINAL_PATH} || exit 1
|
# Permissions spéciales pour logs et uploads
|
||||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH} -type d -exec chmod 755 {} \; || exit 1
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${API_PATH}/logs ${API_PATH}/uploads
|
||||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH} -type f -exec chmod 644 {} \; || exit 1
|
incus exec ${DEST_CONTAINER} -- chmod -R 775 ${API_PATH}/logs ${API_PATH}/uploads
|
||||||
|
|
||||||
# Permissions spéciales pour le dossier logs (pour permettre à PHP-FPM de l'utilisateur nobody d'y écrire)
|
echo_info "Updating Composer dependencies..."
|
||||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${FINAL_PATH}/logs || exit 1
|
incus exec ${DEST_CONTAINER} -- bash -c "cd ${API_PATH} && composer update --no-dev --optimize-autoloader" || echo_warning "Composer not available or failed"
|
||||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/logs || exit 1
|
|
||||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/logs -type f -exec chmod 664 {} \; || exit 1
|
echo_info "Cleaning up..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
echo '📁 Création des dossiers uploads...'
|
|
||||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH}/uploads || exit 1
|
else
|
||||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${FINAL_PATH}/uploads || exit 1
|
# Déploiement sur container distant (RCA ou PRA)
|
||||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/uploads || exit 1
|
echo_step "Deploying to remote container ${DEST_CONTAINER} on ${DEST_HOST}..."
|
||||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/uploads -type f -exec chmod -R 664 {} \; || exit 1
|
|
||||||
|
# Créer une sauvegarde sur le serveur de destination
|
||||||
echo '📦 Mise à jour des dépendances Composer...'
|
BACKUP_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
incus exec ${INCUS_CONTAINER} -- bash -c 'cd ${FINAL_PATH} && composer update --no-dev --optimize-autoloader' || {
|
REMOTE_BACKUP_DIR="${API_PATH}_backup_${BACKUP_TIMESTAMP}"
|
||||||
echo '⚠️ Composer non disponible ou échec, poursuite sans mise à jour des dépendances'
|
|
||||||
}
|
echo_info "Creating backup on destination..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
echo '🧹 Nettoyage...'
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH} &&
|
||||||
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
incus exec ${DEST_CONTAINER} -- cp -r ${API_PATH} ${REMOTE_BACKUP_DIR} &&
|
||||||
"
|
echo 'Backup created: ${REMOTE_BACKUP_DIR}'
|
||||||
|
" || echo_warning "No existing installation to backup"
|
||||||
|
|
||||||
|
# Transférer l'archive vers le serveur de destination
|
||||||
|
echo_info "Transferring archive to ${DEST_HOST}..."
|
||||||
|
|
||||||
|
if [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# Pour RCA: copier depuis local vers distant
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${TEMP_ARCHIVE} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to destination"
|
||||||
|
else
|
||||||
|
# Pour PRA: copier de serveur à serveur
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} /tmp/${ARCHIVE_NAME} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to transfer archive between servers"
|
||||||
|
|
||||||
|
# Nettoyer sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "rm -f /tmp/${ARCHIVE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Déployer sur le container de destination
|
||||||
|
echo_info "Extracting on destination container..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Pousser l'archive dans le container
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} &&
|
||||||
|
|
||||||
|
# Nettoyer sélectivement (préserver logs et uploads)
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' -exec rm -rf {} \; 2>/dev/null || true &&
|
||||||
|
|
||||||
|
# Extraire l'archive
|
||||||
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${API_PATH}/ &&
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${API_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type d -exec chmod 755 {} \; &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type f -exec chmod 644 {} \; &&
|
||||||
|
|
||||||
|
# Permissions spéciales pour logs
|
||||||
|
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH}/logs &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${API_PATH}/logs &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- chmod -R 775 ${API_PATH}/logs || true &&
|
||||||
|
|
||||||
|
# Permissions spéciales pour uploads
|
||||||
|
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH}/uploads &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${API_PATH}/uploads &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- chmod -R 775 ${API_PATH}/uploads || true &&
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
incus exec ${DEST_CONTAINER} -- bash -c 'cd ${API_PATH} && composer update --no-dev --optimize-autoloader' || echo 'Composer update skipped' &&
|
||||||
|
|
||||||
|
# Nettoyage
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} &&
|
||||||
|
rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Deployment failed on destination"
|
||||||
|
|
||||||
|
echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Nettoyage local
|
# Nettoyage local
|
||||||
rm -f "${TEMP_ARCHIVE}"
|
rm -f "${TEMP_ARCHIVE}"
|
||||||
|
|
||||||
|
# =====================================
|
||||||
# Résumé final
|
# Résumé final
|
||||||
echo_step "Deployment completed successfully."
|
# =====================================
|
||||||
echo_info "Your API has been updated on the container."
|
|
||||||
|
echo_step "Deployment completed successfully!"
|
||||||
|
echo_info "Environment: ${ENV_NAME}"
|
||||||
|
|
||||||
|
if [ "$TARGET_ENV" = "dev" ]; then
|
||||||
|
echo_info "Deployed from local code to container ${DEST_CONTAINER}"
|
||||||
|
elif [ "$TARGET_ENV" = "rca" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} (local) to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
elif [ "$TARGET_ENV" = "pra" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} on ${SOURCE_HOST} to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo_info "Deployment completed at: $(date)"
|
echo_info "Deployment completed at: $(date)"
|
||||||
|
|
||||||
# Journaliser le déploiement
|
# Journaliser le déploiement
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${JUMP_HOST}:${INCUS_CONTAINER}" >> ~/.geo_deploy_history
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${ENV_NAME} (${DEST_CONTAINER})" >> ~/.geo_deploy_history
|
||||||
@@ -46,7 +46,7 @@ if (php_sapi_name() === 'cli') {
|
|||||||
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
||||||
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
||||||
} else {
|
} else {
|
||||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
|
$_SERVER['SERVER_NAME'] = 'app.geo.dev'; // DVA par défaut
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
// Simuler l'environnement web pour AppConfig en CLI
|
// Simuler l'environnement web pour AppConfig en CLI
|
||||||
if (php_sapi_name() === 'cli') {
|
if (php_sapi_name() === 'cli') {
|
||||||
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'dapp.geosector.fr'; // DVA par défaut
|
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'app.geo.dev'; // DVA par défaut
|
||||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ declare(strict_types=1);
|
|||||||
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
|
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
|
||||||
* - Production (app.geosector.fr)
|
* - Production (app.geosector.fr)
|
||||||
* - Recette (rapp.geosector.fr)
|
* - Recette (rapp.geosector.fr)
|
||||||
* - Développement (dapp.geosector.fr)
|
* - Développement (app.geo.dev)
|
||||||
*
|
*
|
||||||
* Il inclut les paramètres de base de données, les informations SMTP,
|
* Il inclut les paramètres de base de données, les informations SMTP,
|
||||||
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
|
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
|
||||||
@@ -124,10 +124,10 @@ class AppConfig {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Configuration DÉVELOPPEMENT
|
// Configuration DÉVELOPPEMENT
|
||||||
$this->config['dapp.geosector.fr'] = array_merge($baseConfig, [
|
$this->config['app.geo.dev'] = array_merge($baseConfig, [
|
||||||
'env' => 'development',
|
'env' => 'development',
|
||||||
'database' => [
|
'database' => [
|
||||||
'host' => 'localhost',
|
'host' => '13.23.33.46',
|
||||||
'name' => 'geo_app',
|
'name' => 'geo_app',
|
||||||
'username' => 'geo_app_user_dev',
|
'username' => 'geo_app_user_dev',
|
||||||
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
||||||
@@ -148,7 +148,7 @@ class AppConfig {
|
|||||||
if (empty($this->currentHost)) {
|
if (empty($this->currentHost)) {
|
||||||
// Journaliser cette situation anormale
|
// Journaliser cette situation anormale
|
||||||
error_log("WARNING: No host detected, falling back to development environment");
|
error_log("WARNING: No host detected, falling back to development environment");
|
||||||
$this->currentHost = 'dapp.geosector.fr';
|
$this->currentHost = 'app.geo.dev';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'hôte n'existe pas dans la configuration, tenter une correction
|
// Si l'hôte n'existe pas dans la configuration, tenter une correction
|
||||||
@@ -166,7 +166,7 @@ class AppConfig {
|
|||||||
// Si toujours pas de correspondance, utiliser l'environnement de développement par défaut
|
// Si toujours pas de correspondance, utiliser l'environnement de développement par défaut
|
||||||
if (!isset($this->config[$this->currentHost])) {
|
if (!isset($this->config[$this->currentHost])) {
|
||||||
error_log("WARNING: Unknown host '{$this->currentHost}', falling back to development environment");
|
error_log("WARNING: Unknown host '{$this->currentHost}', falling back to development environment");
|
||||||
$this->currentHost = 'dapp.geosector.fr';
|
$this->currentHost = 'app.geo.dev';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ class AppConfig {
|
|||||||
/**
|
/**
|
||||||
* Retourne l'identifiant de l'application basé sur l'hôte
|
* Retourne l'identifiant de l'application basé sur l'hôte
|
||||||
*
|
*
|
||||||
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, app.geo.dev)
|
||||||
*/
|
*/
|
||||||
public function getAppIdentifier(): string {
|
public function getAppIdentifier(): string {
|
||||||
return $this->currentHost;
|
return $this->currentHost;
|
||||||
|
|||||||
@@ -36,13 +36,11 @@ class Database {
|
|||||||
$options
|
$options
|
||||||
);
|
);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// Créer une alerte pour la connexion échouée
|
// Ne PAS utiliser AlertService ici car il essaie d'utiliser la DB
|
||||||
AlertService::trigger('DB_CONNECTION', [
|
// Juste logger l'erreur directement
|
||||||
'error' => $e->getMessage(),
|
error_log("Database connection failed: " . $e->getMessage() .
|
||||||
'host' => self::$config['host'],
|
" | Host: " . self::$config['host'] .
|
||||||
'database' => self::$config['name'],
|
" | Database: " . self::$config['name']);
|
||||||
'message' => 'Échec de connexion à la base de données'
|
|
||||||
], 'CRITICAL');
|
|
||||||
|
|
||||||
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,9 +365,9 @@ ACTIONS RECOMMANDÉES
|
|||||||
|
|
||||||
LIENS UTILES
|
LIENS UTILES
|
||||||
------------
|
------------
|
||||||
- Logs: https://dapp.geosector.fr/admin/logs
|
- Logs: https://app.geo.dev/admin/logs
|
||||||
- Dashboard: https://dapp.geosector.fr/admin/security
|
- Dashboard: https://app.geo.dev/admin/security
|
||||||
- Bloquer IP: https://dapp.geosector.fr/admin/block-ip/" . ($context['request']['ip'] ?? '') . "
|
- Bloquer IP: https://app.geo.dev/admin/block-ip/" . ($context['request']['ip'] ?? '') . "
|
||||||
|
|
||||||
--
|
--
|
||||||
Email automatique généré par GeoSector Security
|
Email automatique généré par GeoSector Security
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ require_once __DIR__ . '/src/Services/StripeService.php';
|
|||||||
require_once __DIR__ . '/src/Core/Database.php';
|
require_once __DIR__ . '/src/Core/Database.php';
|
||||||
|
|
||||||
// Forcer l'environnement dev
|
// Forcer l'environnement dev
|
||||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr';
|
$_SERVER['SERVER_NAME'] = 'app.geo.dev';
|
||||||
|
|
||||||
echo "================================\n";
|
echo "================================\n";
|
||||||
echo "Test de l'intégration Stripe\n";
|
echo "Test de l'intégration Stripe\n";
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/build_daemon-4.0.4/lib/fake.dart
|
|
||||||
file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner-2.4.13/lib/fake.dart
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// ignore_for_file: directives_ordering
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
|
||||||
import 'package:build_runner_core/build_runner_core.dart' as _i1;
|
|
||||||
import 'package:hive_generator/hive_generator.dart' as _i2;
|
|
||||||
import 'package:source_gen/builder.dart' as _i3;
|
|
||||||
import 'package:build_resolvers/builder.dart' as _i4;
|
|
||||||
import 'dart:isolate' as _i5;
|
|
||||||
import 'package:build_runner/build_runner.dart' as _i6;
|
|
||||||
import 'dart:io' as _i7;
|
|
||||||
|
|
||||||
final _builders = <_i1.BuilderApplication>[
|
|
||||||
_i1.apply(
|
|
||||||
r'hive_generator:hive_generator',
|
|
||||||
[_i2.getBuilder],
|
|
||||||
_i1.toDependentsOf(r'hive_generator'),
|
|
||||||
hideOutput: true,
|
|
||||||
appliesBuilders: const [r'source_gen:combining_builder'],
|
|
||||||
),
|
|
||||||
_i1.apply(
|
|
||||||
r'source_gen:combining_builder',
|
|
||||||
[_i3.combiningBuilder],
|
|
||||||
_i1.toNoneByDefault(),
|
|
||||||
hideOutput: false,
|
|
||||||
appliesBuilders: const [r'source_gen:part_cleanup'],
|
|
||||||
),
|
|
||||||
_i1.apply(
|
|
||||||
r'build_resolvers:transitive_digests',
|
|
||||||
[_i4.transitiveDigestsBuilder],
|
|
||||||
_i1.toAllPackages(),
|
|
||||||
isOptional: true,
|
|
||||||
hideOutput: true,
|
|
||||||
appliesBuilders: const [r'build_resolvers:transitive_digest_cleanup'],
|
|
||||||
),
|
|
||||||
_i1.applyPostProcess(
|
|
||||||
r'build_resolvers:transitive_digest_cleanup',
|
|
||||||
_i4.transitiveDigestCleanup,
|
|
||||||
),
|
|
||||||
_i1.applyPostProcess(
|
|
||||||
r'source_gen:part_cleanup',
|
|
||||||
_i3.partCleanup,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
void main(
|
|
||||||
List<String> args, [
|
|
||||||
_i5.SendPort? sendPort,
|
|
||||||
]) async {
|
|
||||||
var result = await _i6.run(
|
|
||||||
args,
|
|
||||||
_builders,
|
|
||||||
);
|
|
||||||
sendPort?.send(result);
|
|
||||||
_i7.exitCode = result;
|
|
||||||
}
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>5z<EFBFBD><EFBFBD><EFBFBD>k<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<EFBFBD>ũ<EFBFBD><EFBFBD><0C><><EFBFBD>U/!<21><>W<EFBFBD>
|
|
||||||
Binary file not shown.
@@ -1,67 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class MessageAdapter extends TypeAdapter<Message> {
|
|
||||||
@override
|
|
||||||
final int typeId = 51;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Message read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return Message(
|
|
||||||
id: fields[0] as String,
|
|
||||||
roomId: fields[1] as String,
|
|
||||||
content: fields[2] as String,
|
|
||||||
senderId: fields[3] as int,
|
|
||||||
senderName: fields[4] as String,
|
|
||||||
sentAt: fields[5] as DateTime,
|
|
||||||
isMe: fields[6] as bool,
|
|
||||||
isRead: fields[7] as bool,
|
|
||||||
senderFirstName: fields[8] as String?,
|
|
||||||
readCount: fields[9] as int?,
|
|
||||||
isSynced: fields[10] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, Message obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(11)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.roomId)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.content)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.senderId)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.senderName)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.sentAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.isMe)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.isRead)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.senderFirstName)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.readCount)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.isSynced);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is MessageAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class RoomAdapter extends TypeAdapter<Room> {
|
|
||||||
@override
|
|
||||||
final int typeId = 50;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Room read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return Room(
|
|
||||||
id: fields[0] as String,
|
|
||||||
title: fields[1] as String,
|
|
||||||
type: fields[2] as String,
|
|
||||||
createdAt: fields[3] as DateTime,
|
|
||||||
lastMessage: fields[4] as String?,
|
|
||||||
lastMessageAt: fields[5] as DateTime?,
|
|
||||||
unreadCount: fields[6] as int,
|
|
||||||
recentMessages: (fields[7] as List?)
|
|
||||||
?.map((dynamic e) => (e as Map).cast<String, dynamic>())
|
|
||||||
?.toList(),
|
|
||||||
updatedAt: fields[8] as DateTime?,
|
|
||||||
createdBy: fields[9] as int?,
|
|
||||||
isSynced: fields[10] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, Room obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(11)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.title)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.type)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.lastMessage)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.lastMessageAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.unreadCount)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.recentMessages)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.updatedAt)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.createdBy)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.isSynced);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is RoomAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 11;
|
|
||||||
|
|
||||||
@override
|
|
||||||
AmicaleModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return AmicaleModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
name: fields[1] as String,
|
|
||||||
adresse1: fields[2] as String,
|
|
||||||
adresse2: fields[3] as String,
|
|
||||||
codePostal: fields[4] as String,
|
|
||||||
ville: fields[5] as String,
|
|
||||||
fkRegion: fields[6] as int?,
|
|
||||||
libRegion: fields[7] as String?,
|
|
||||||
fkType: fields[8] as int?,
|
|
||||||
phone: fields[9] as String,
|
|
||||||
mobile: fields[10] as String,
|
|
||||||
email: fields[11] as String,
|
|
||||||
gpsLat: fields[12] as String,
|
|
||||||
gpsLng: fields[13] as String,
|
|
||||||
stripeId: fields[14] as String,
|
|
||||||
chkDemo: fields[15] as bool,
|
|
||||||
chkCopieMailRecu: fields[16] as bool,
|
|
||||||
chkAcceptSms: fields[17] as bool,
|
|
||||||
chkActive: fields[18] as bool,
|
|
||||||
chkStripe: fields[19] as bool,
|
|
||||||
createdAt: fields[20] as DateTime?,
|
|
||||||
updatedAt: fields[21] as DateTime?,
|
|
||||||
chkMdpManuel: fields[22] as bool,
|
|
||||||
chkUsernameManuel: fields[23] as bool,
|
|
||||||
logoBase64: fields[24] as String?,
|
|
||||||
chkUserDeletePass: fields[25] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, AmicaleModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(26)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.adresse1)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.adresse2)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.codePostal)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.ville)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.fkRegion)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.libRegion)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.fkType)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.phone)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.mobile)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.gpsLat)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.gpsLng)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.stripeId)
|
|
||||||
..writeByte(15)
|
|
||||||
..write(obj.chkDemo)
|
|
||||||
..writeByte(16)
|
|
||||||
..write(obj.chkCopieMailRecu)
|
|
||||||
..writeByte(17)
|
|
||||||
..write(obj.chkAcceptSms)
|
|
||||||
..writeByte(18)
|
|
||||||
..write(obj.chkActive)
|
|
||||||
..writeByte(19)
|
|
||||||
..write(obj.chkStripe)
|
|
||||||
..writeByte(20)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(21)
|
|
||||||
..write(obj.updatedAt)
|
|
||||||
..writeByte(22)
|
|
||||||
..write(obj.chkMdpManuel)
|
|
||||||
..writeByte(23)
|
|
||||||
..write(obj.chkUsernameManuel)
|
|
||||||
..writeByte(24)
|
|
||||||
..write(obj.logoBase64)
|
|
||||||
..writeByte(25)
|
|
||||||
..write(obj.chkUserDeletePass);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is AmicaleModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class ClientModelAdapter extends TypeAdapter<ClientModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 10;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ClientModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return ClientModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
name: fields[1] as String,
|
|
||||||
adresse1: fields[2] as String?,
|
|
||||||
adresse2: fields[3] as String?,
|
|
||||||
codePostal: fields[4] as String?,
|
|
||||||
ville: fields[5] as String?,
|
|
||||||
fkRegion: fields[6] as int?,
|
|
||||||
libRegion: fields[7] as String?,
|
|
||||||
fkType: fields[8] as int?,
|
|
||||||
phone: fields[9] as String?,
|
|
||||||
mobile: fields[10] as String?,
|
|
||||||
email: fields[11] as String?,
|
|
||||||
gpsLat: fields[12] as String?,
|
|
||||||
gpsLng: fields[13] as String?,
|
|
||||||
stripeId: fields[14] as String?,
|
|
||||||
chkDemo: fields[15] as bool?,
|
|
||||||
chkCopieMailRecu: fields[16] as bool?,
|
|
||||||
chkAcceptSms: fields[17] as bool?,
|
|
||||||
chkActive: fields[18] as bool?,
|
|
||||||
chkStripe: fields[19] as bool?,
|
|
||||||
createdAt: fields[20] as DateTime?,
|
|
||||||
updatedAt: fields[21] as DateTime?,
|
|
||||||
chkMdpManuel: fields[22] as bool?,
|
|
||||||
chkUsernameManuel: fields[23] as bool?,
|
|
||||||
chkUserDeletePass: fields[24] as bool?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, ClientModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(25)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.adresse1)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.adresse2)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.codePostal)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.ville)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.fkRegion)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.libRegion)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.fkType)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.phone)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.mobile)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.gpsLat)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.gpsLng)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.stripeId)
|
|
||||||
..writeByte(15)
|
|
||||||
..write(obj.chkDemo)
|
|
||||||
..writeByte(16)
|
|
||||||
..write(obj.chkCopieMailRecu)
|
|
||||||
..writeByte(17)
|
|
||||||
..write(obj.chkAcceptSms)
|
|
||||||
..writeByte(18)
|
|
||||||
..write(obj.chkActive)
|
|
||||||
..writeByte(19)
|
|
||||||
..write(obj.chkStripe)
|
|
||||||
..writeByte(20)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(21)
|
|
||||||
..write(obj.updatedAt)
|
|
||||||
..writeByte(22)
|
|
||||||
..write(obj.chkMdpManuel)
|
|
||||||
..writeByte(23)
|
|
||||||
..write(obj.chkUsernameManuel)
|
|
||||||
..writeByte(24)
|
|
||||||
..write(obj.chkUserDeletePass);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is ClientModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 5;
|
|
||||||
|
|
||||||
@override
|
|
||||||
MembreModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return MembreModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
fkEntite: fields[1] as int?,
|
|
||||||
role: fields[2] as int,
|
|
||||||
fkTitre: fields[3] as int?,
|
|
||||||
name: fields[4] as String?,
|
|
||||||
firstName: fields[5] as String?,
|
|
||||||
username: fields[6] as String?,
|
|
||||||
sectName: fields[7] as String?,
|
|
||||||
email: fields[8] as String,
|
|
||||||
phone: fields[9] as String?,
|
|
||||||
mobile: fields[10] as String?,
|
|
||||||
dateNaissance: fields[11] as DateTime?,
|
|
||||||
dateEmbauche: fields[12] as DateTime?,
|
|
||||||
createdAt: fields[13] as DateTime,
|
|
||||||
isActive: fields[14] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, MembreModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(15)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.fkEntite)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.role)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.fkTitre)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.firstName)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.username)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.sectName)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.phone)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.mobile)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.dateNaissance)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.dateEmbauche)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.isActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is MembreModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class OperationModelAdapter extends TypeAdapter<OperationModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
OperationModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return OperationModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
name: fields[1] as String,
|
|
||||||
dateDebut: fields[2] as DateTime,
|
|
||||||
dateFin: fields[3] as DateTime,
|
|
||||||
lastSyncedAt: fields[4] as DateTime,
|
|
||||||
fkEntite: fields[7] as int,
|
|
||||||
isActive: fields[5] as bool,
|
|
||||||
isSynced: fields[6] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, OperationModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(8)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.dateDebut)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.dateFin)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.lastSyncedAt)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.isActive)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.isSynced)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.fkEntite);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is OperationModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class PassageModelAdapter extends TypeAdapter<PassageModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 4;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PassageModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return PassageModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
fkOperation: fields[1] as int,
|
|
||||||
fkSector: fields[2] as int?,
|
|
||||||
fkUser: fields[3] as int,
|
|
||||||
fkType: fields[4] as int,
|
|
||||||
fkAdresse: fields[5] as String,
|
|
||||||
passedAt: fields[6] as DateTime?,
|
|
||||||
numero: fields[7] as String,
|
|
||||||
rue: fields[8] as String,
|
|
||||||
rueBis: fields[9] as String,
|
|
||||||
ville: fields[10] as String,
|
|
||||||
residence: fields[11] as String,
|
|
||||||
fkHabitat: fields[12] as int,
|
|
||||||
appt: fields[13] as String,
|
|
||||||
niveau: fields[14] as String,
|
|
||||||
gpsLat: fields[15] as String,
|
|
||||||
gpsLng: fields[16] as String,
|
|
||||||
nomRecu: fields[17] as String,
|
|
||||||
remarque: fields[18] as String,
|
|
||||||
montant: fields[19] as String,
|
|
||||||
fkTypeReglement: fields[20] as int,
|
|
||||||
emailErreur: fields[21] as String,
|
|
||||||
nbPassages: fields[22] as int,
|
|
||||||
name: fields[23] as String,
|
|
||||||
email: fields[24] as String,
|
|
||||||
phone: fields[25] as String,
|
|
||||||
lastSyncedAt: fields[26] as DateTime,
|
|
||||||
isActive: fields[27] as bool,
|
|
||||||
isSynced: fields[28] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, PassageModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(29)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.fkOperation)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.fkSector)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.fkUser)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.fkType)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.fkAdresse)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.passedAt)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.numero)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.rue)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.rueBis)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.ville)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.residence)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.fkHabitat)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.appt)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.niveau)
|
|
||||||
..writeByte(15)
|
|
||||||
..write(obj.gpsLat)
|
|
||||||
..writeByte(16)
|
|
||||||
..write(obj.gpsLng)
|
|
||||||
..writeByte(17)
|
|
||||||
..write(obj.nomRecu)
|
|
||||||
..writeByte(18)
|
|
||||||
..write(obj.remarque)
|
|
||||||
..writeByte(19)
|
|
||||||
..write(obj.montant)
|
|
||||||
..writeByte(20)
|
|
||||||
..write(obj.fkTypeReglement)
|
|
||||||
..writeByte(21)
|
|
||||||
..write(obj.emailErreur)
|
|
||||||
..writeByte(22)
|
|
||||||
..write(obj.nbPassages)
|
|
||||||
..writeByte(23)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(24)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(25)
|
|
||||||
..write(obj.phone)
|
|
||||||
..writeByte(26)
|
|
||||||
..write(obj.lastSyncedAt)
|
|
||||||
..writeByte(27)
|
|
||||||
..write(obj.isActive)
|
|
||||||
..writeByte(28)
|
|
||||||
..write(obj.isSynced);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is PassageModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class PendingRequestAdapter extends TypeAdapter<PendingRequest> {
|
|
||||||
@override
|
|
||||||
final int typeId = 100;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PendingRequest read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return PendingRequest(
|
|
||||||
id: fields[0] as String,
|
|
||||||
method: fields[1] as String,
|
|
||||||
path: fields[2] as String,
|
|
||||||
data: (fields[3] as Map?)?.cast<String, dynamic>(),
|
|
||||||
queryParams: (fields[4] as Map?)?.cast<String, dynamic>(),
|
|
||||||
createdAt: fields[5] as DateTime,
|
|
||||||
tempId: fields[6] as String?,
|
|
||||||
context: fields[7] as String,
|
|
||||||
retryCount: fields[8] as int,
|
|
||||||
errorMessage: fields[9] as String?,
|
|
||||||
metadata: (fields[10] as Map?)?.cast<String, dynamic>(),
|
|
||||||
priority: fields[11] as int,
|
|
||||||
headers: (fields[12] as Map?)?.cast<String, String>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, PendingRequest obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(13)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.method)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.path)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.data)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.queryParams)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.tempId)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.context)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.retryCount)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.errorMessage)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.metadata)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.priority)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is PendingRequestAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class RegionModelAdapter extends TypeAdapter<RegionModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 7;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RegionModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return RegionModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
fkPays: fields[1] as int,
|
|
||||||
libelle: fields[2] as String,
|
|
||||||
libelleLong: fields[3] as String?,
|
|
||||||
tableOsm: fields[4] as String?,
|
|
||||||
departements: fields[5] as String?,
|
|
||||||
chkActive: fields[6] as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, RegionModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(7)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.fkPays)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.libelle)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.libelleLong)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.tableOsm)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.departements)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.chkActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is RegionModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class SectorModelAdapter extends TypeAdapter<SectorModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 3;
|
|
||||||
|
|
||||||
@override
|
|
||||||
SectorModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return SectorModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
libelle: fields[1] as String,
|
|
||||||
color: fields[2] as String,
|
|
||||||
sector: fields[3] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, SectorModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(4)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.libelle)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.color)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.sector);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is SectorModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class UserModelAdapter extends TypeAdapter<UserModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
UserModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return UserModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
email: fields[1] as String,
|
|
||||||
name: fields[2] as String?,
|
|
||||||
username: fields[11] as String?,
|
|
||||||
firstName: fields[10] as String?,
|
|
||||||
role: fields[3] as int,
|
|
||||||
createdAt: fields[4] as DateTime,
|
|
||||||
lastSyncedAt: fields[5] as DateTime,
|
|
||||||
isActive: fields[6] as bool,
|
|
||||||
isSynced: fields[7] as bool,
|
|
||||||
sessionId: fields[8] as String?,
|
|
||||||
sessionExpiry: fields[9] as DateTime?,
|
|
||||||
lastPath: fields[12] as String?,
|
|
||||||
sectName: fields[13] as String?,
|
|
||||||
fkEntite: fields[14] as int?,
|
|
||||||
fkTitre: fields[15] as int?,
|
|
||||||
phone: fields[16] as String?,
|
|
||||||
mobile: fields[17] as String?,
|
|
||||||
dateNaissance: fields[18] as DateTime?,
|
|
||||||
dateEmbauche: fields[19] as DateTime?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, UserModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(20)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.email)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.username)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.firstName)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.role)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.createdAt)
|
|
||||||
..writeByte(5)
|
|
||||||
..write(obj.lastSyncedAt)
|
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.isActive)
|
|
||||||
..writeByte(7)
|
|
||||||
..write(obj.isSynced)
|
|
||||||
..writeByte(8)
|
|
||||||
..write(obj.sessionId)
|
|
||||||
..writeByte(9)
|
|
||||||
..write(obj.sessionExpiry)
|
|
||||||
..writeByte(12)
|
|
||||||
..write(obj.lastPath)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.sectName)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.fkEntite)
|
|
||||||
..writeByte(15)
|
|
||||||
..write(obj.fkTitre)
|
|
||||||
..writeByte(16)
|
|
||||||
..write(obj.phone)
|
|
||||||
..writeByte(17)
|
|
||||||
..write(obj.mobile)
|
|
||||||
..writeByte(18)
|
|
||||||
..write(obj.dateNaissance)
|
|
||||||
..writeByte(19)
|
|
||||||
..write(obj.dateEmbauche);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is UserModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class UserSectorModelAdapter extends TypeAdapter<UserSectorModel> {
|
|
||||||
@override
|
|
||||||
final int typeId = 6;
|
|
||||||
|
|
||||||
@override
|
|
||||||
UserSectorModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return UserSectorModel(
|
|
||||||
id: fields[0] as int,
|
|
||||||
firstName: fields[1] as String?,
|
|
||||||
sectName: fields[2] as String?,
|
|
||||||
fkSector: fields[3] as int,
|
|
||||||
name: fields[4] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, UserSectorModel obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(5)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.id)
|
|
||||||
..writeByte(1)
|
|
||||||
..write(obj.firstName)
|
|
||||||
..writeByte(2)
|
|
||||||
..write(obj.sectName)
|
|
||||||
..writeByte(3)
|
|
||||||
..write(obj.fkSector)
|
|
||||||
..writeByte(4)
|
|
||||||
..write(obj.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is UserSectorModelAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
C<EFBFBD><EFBFBD><EFBFBD>F}<7D><EFBFBD>7<><37><EFBFBD><EFBFBD>9
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<EFBFBD><EFBFBD>E>`<60>e0<65>sl<73><6C><0C>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
Q<EFBFBD>;<14><><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
<EFBFBD>)<29>j<EFBFBD>
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"sdk":"3.9.0 (stable) (Mon Aug 11 07:58:10 2025 -0700) on \"linux_x64\"","analyzer":"/home/pierre/.pub-cache/hosted/pub.dev/analyzer-6.4.1","build_resolvers":"/home/pierre/.pub-cache/hosted/pub.dev/build_resolvers-2.4.2"}
|
|
||||||
31
app/.dart_tool/extension_discovery/README.md
Normal file
31
app/.dart_tool/extension_discovery/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Extension Discovery Cache
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This folder is used by `package:extension_discovery` to cache lists of
|
||||||
|
packages that contains extensions for other packages.
|
||||||
|
|
||||||
|
DO NOT USE THIS FOLDER
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
* Do not read (or rely) the contents of this folder.
|
||||||
|
* Do write to this folder.
|
||||||
|
|
||||||
|
If you're interested in the lists of extensions stored in this folder use the
|
||||||
|
API offered by package `extension_discovery` to get this information.
|
||||||
|
|
||||||
|
If this package doesn't work for your use-case, then don't try to read the
|
||||||
|
contents of this folder. It may change, and will not remain stable.
|
||||||
|
|
||||||
|
Use package `extension_discovery`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
If you want to access information from this folder.
|
||||||
|
|
||||||
|
Feel free to delete this folder
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Files in this folder act as a cache, and the cache is discarded if the files
|
||||||
|
are older than the modification time of `.dart_tool/package_config.json`.
|
||||||
|
|
||||||
|
Hence, it should never be necessary to clear this cache manually, if you find a
|
||||||
|
need to do please file a bug.
|
||||||
1
app/.dart_tool/extension_discovery/vs_code.json
Normal file
1
app/.dart_tool/extension_discovery/vs_code.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"inputs":["/home/pierre/dev/flutter/bin/cache/engine.stamp","/home/pierre/dev/flutter/bin/cache/engine.stamp","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/main.dart","/home/pierre/dev/geosector/app/.dart_tool/package_config.json"],"outputs":[],"buildKey":"{\"optimizationLevel\":null,\"webRenderer\":\"skwasm\",\"StripWasm\":true,\"minify\":null,\"dryRun\":true,\"SourceMaps\":false}"}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"inputs":[],"outputs":[]}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// @dart=3.0
|
|
||||||
// Flutter web bootstrap script for package:geosector_app/main.dart.
|
|
||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
import 'dart:ui_web' as ui_web;
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:geosector_app/main.dart' as entrypoint;
|
|
||||||
import 'web_plugin_registrant.dart' as pluginRegistrant;
|
|
||||||
|
|
||||||
typedef _UnaryFunction = dynamic Function(List<String> args);
|
|
||||||
typedef _NullaryFunction = dynamic Function();
|
|
||||||
|
|
||||||
Future<void> main() async {
|
|
||||||
await ui_web.bootstrapEngine(
|
|
||||||
runApp: () {
|
|
||||||
if (entrypoint.main is _UnaryFunction) {
|
|
||||||
return (entrypoint.main as _UnaryFunction)(<String>[]);
|
|
||||||
}
|
|
||||||
return (entrypoint.main as _NullaryFunction)();
|
|
||||||
},
|
|
||||||
registerPlugins: () {
|
|
||||||
pluginRegistrant.registerPlugins();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
["/home/pierre/dev/geosector/app/build/web/*/index.html","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/.DS_Store","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/manifest.json","/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js: /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-152.png /home/pierre/dev/geosector/app/build/web/icons/Icon-180.png /home/pierre/dev/geosector/app/build/web/icons/Icon-167.png /home/pierre/dev/geosector/app/build/web/icons/Icon-512.png /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png /home/pierre/dev/geosector/app/build/web/flutter.js /home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js /home/pierre/dev/geosector/app/build/web/favicon-64.png /home/pierre/dev/geosector/app/build/web/index.html /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.wasm /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.js /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.js.symbols /home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js /home/pierre/dev/geosector/app/build/web/favicon-32.png /home/pierre/dev/geosector/app/build/web/version.json /home/pierre/dev/geosector/app/build/web/favicon.png /home/pierre/dev/geosector/app/build/web/favicon-16.png /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin /home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf /home/pierre/dev/geosector/app/build/web/assets/FontManifest.json /home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml /home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png /home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra /home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png /home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png /home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf /home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json /home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag /home/pierre/dev/geosector/app/build/web/assets/NOTICES /home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json /home/pierre/dev/geosector/app/build/web/main.dart.js /home/pierre/dev/geosector/app/build/web/manifest.json
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"inputs":["/home/pierre/dev/flutter/packages/flutter_tools/lib/src/build_system/targets/web.dart"],"outputs":["/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/main.dart"]}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Flutter web plugin registrant file.
|
|
||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// @dart = 2.13
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
import 'package:connectivity_plus/src/connectivity_plus_web.dart';
|
|
||||||
import 'package:geolocator_web/geolocator_web.dart';
|
|
||||||
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
|
||||||
import 'package:package_info_plus/src/package_info_plus_web.dart';
|
|
||||||
import 'package:sensors_plus/src/sensors_plus_web.dart';
|
|
||||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
|
||||||
import 'package:url_launcher_web/url_launcher_web.dart';
|
|
||||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
|
||||||
|
|
||||||
void registerPlugins([final Registrar? pluginRegistrar]) {
|
|
||||||
final Registrar registrar = pluginRegistrar ?? webPluginRegistrar;
|
|
||||||
ConnectivityPlusWebPlugin.registerWith(registrar);
|
|
||||||
GeolocatorPlugin.registerWith(registrar);
|
|
||||||
ImagePickerPlugin.registerWith(registrar);
|
|
||||||
PackageInfoPlusWebPlugin.registerWith(registrar);
|
|
||||||
WebSensorsPlugin.registerWith(registrar);
|
|
||||||
SharedPreferencesPlugin.registerWith(registrar);
|
|
||||||
UrlLauncherPlugin.registerWith(registrar);
|
|
||||||
registrar.registerMessageHandler();
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-192.png /home/pierre/dev/geosector/app/build/web/icons/Icon-152.png /home/pierre/dev/geosector/app/build/web/icons/Icon-180.png /home/pierre/dev/geosector/app/build/web/icons/Icon-167.png /home/pierre/dev/geosector/app/build/web/icons/Icon-512.png /home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png /home/pierre/dev/geosector/app/build/web/favicon-64.png /home/pierre/dev/geosector/app/build/web/.DS_Store /home/pierre/dev/geosector/app/build/web/favicon-32.png /home/pierre/dev/geosector/app/build/web/favicon.png /home/pierre/dev/geosector/app/build/web/favicon-16.png /home/pierre/dev/geosector/app/build/web/manifest.json: /home/pierre/dev/geosector/app/web/icons/Icon-maskable-192.png /home/pierre/dev/geosector/app/web/icons/Icon-192.png /home/pierre/dev/geosector/app/web/icons/Icon-152.png /home/pierre/dev/geosector/app/web/icons/Icon-180.png /home/pierre/dev/geosector/app/web/icons/Icon-167.png /home/pierre/dev/geosector/app/web/icons/Icon-512.png /home/pierre/dev/geosector/app/web/icons/Icon-maskable-512.png /home/pierre/dev/geosector/app/web/favicon-64.png /home/pierre/dev/geosector/app/web/.DS_Store /home/pierre/dev/geosector/app/web/index.html /home/pierre/dev/geosector/app/web/favicon-32.png /home/pierre/dev/geosector/app/web/favicon.png /home/pierre/dev/geosector/app/web/favicon-16.png /home/pierre/dev/geosector/app/web/manifest.json
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"inputs":["/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-192.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-152.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-180.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-167.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-512.png","/home/pierre/dev/geosector/app/build/web/icons/Icon-maskable-512.png","/home/pierre/dev/geosector/app/build/web/flutter.js","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js","/home/pierre/dev/geosector/app/build/web/favicon-64.png","/home/pierre/dev/geosector/app/build/web/index.html","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/chromium/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/canvaskit.js","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.wasm","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.js","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm_heavy.js.symbols","/home/pierre/dev/geosector/app/build/web/canvaskit/skwasm.js","/home/pierre/dev/geosector/app/build/web/favicon-32.png","/home/pierre/dev/geosector/app/build/web/version.json","/home/pierre/dev/geosector/app/build/web/favicon.png","/home/pierre/dev/geosector/app/build/web/favicon-16.png","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.json","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin","/home/pierre/dev/geosector/app/build/web/assets/fonts/MaterialIcons-Regular.otf","/home/pierre/dev/geosector/app/build/web/assets/FontManifest.json","/home/pierre/dev/geosector/app/build/web/assets/lib/chat/chat_config.yaml","/home/pierre/dev/geosector/app/build/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png","/home/pierre/dev/geosector/app/build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png-autosave.kra","/home/pierre/dev/geosector/app/build/web/assets/assets/images/icon-geosector.svg","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector_map_admin.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo_recu.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-512.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/geosector-logo.png","/home/pierre/dev/geosector/app/build/web/assets/assets/images/logo-geosector-1024.png","/home/pierre/dev/geosector/app/build/web/assets/assets/fonts/Figtree-VariableFont_wght.ttf","/home/pierre/dev/geosector/app/build/web/assets/assets/animations/geo_main.json","/home/pierre/dev/geosector/app/build/web/assets/shaders/ink_sparkle.frag","/home/pierre/dev/geosector/app/build/web/assets/NOTICES","/home/pierre/dev/geosector/app/build/web/assets/AssetManifest.bin.json","/home/pierre/dev/geosector/app/build/web/main.dart.js","/home/pierre/dev/geosector/app/build/web/manifest.json"],"outputs":["/home/pierre/dev/geosector/app/build/web/flutter_service_worker.js"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"inputs":["/home/pierre/dev/flutter/bin/cache/engine.stamp"],"outputs":["/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/flutter.js","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/chromium/canvaskit.wasm","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/chromium/canvaskit.js.symbols","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/chromium/canvaskit.js","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/canvaskit.wasm","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/canvaskit.js.symbols","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm.wasm","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/canvaskit.js","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm.js.symbols","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm_heavy.wasm","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm_heavy.js","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm_heavy.js.symbols","/home/pierre/dev/geosector/app/.dart_tool/flutter_build/af193713835350bfe216cb2e6cbf5196/canvaskit/skwasm.js"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"inputs":["/home/pierre/dev/geosector/app/web/*/index.html","/home/pierre/dev/geosector/app/web/flutter_bootstrap.js","/home/pierre/dev/flutter/bin/cache/engine.stamp"],"outputs":["/home/pierre/dev/geosector/app/build/web/*/index.html","/home/pierre/dev/geosector/app/build/web/flutter_bootstrap.js"],"buildKey":"[{\"compileTarget\":\"dart2js\",\"renderer\":\"canvaskit\",\"mainJsPath\":\"main.dart.js\"},{}]"}
|
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "built_value",
|
"name": "built_value",
|
||||||
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.1",
|
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.2",
|
||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.0"
|
"languageVersion": "3.0"
|
||||||
},
|
},
|
||||||
@@ -339,9 +339,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_svg",
|
"name": "flutter_svg",
|
||||||
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0",
|
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.1",
|
||||||
"packageUri": "lib/",
|
"packageUri": "lib/",
|
||||||
"languageVersion": "3.6"
|
"languageVersion": "3.7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flutter_test",
|
"name": "flutter_test",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,106 +1,353 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script de déploiement unifié pour GEOSECTOR Flutter App
|
||||||
|
# Version: 4.0 (Janvier 2025)
|
||||||
|
# Auteur: Pierre (avec l'aide de Claude)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy-app.sh # Déploiement local DEV (build → container geo)
|
||||||
|
# ./deploy-app.sh rca # Livraison RECETTE (container geo → rca-geo)
|
||||||
|
# ./deploy-app.sh pra # Livraison PRODUCTION (rca-geo → pra-geo)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
cd /home/pierre/dev/geosector/app
|
cd /home/pierre/dev/geosector/app
|
||||||
|
|
||||||
# Charger les variables d'environnement
|
# =====================================
|
||||||
if [ ! -f .env-deploy-dev ]; then
|
# Configuration générale
|
||||||
echo "❌ Fichier .env-deploy-dev manquant"
|
# =====================================
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source .env-deploy-dev
|
|
||||||
|
|
||||||
# Fonction pour gérer les erreurs
|
# Paramètre optionnel pour l'environnement cible
|
||||||
error_exit() {
|
TARGET_ENV=${1:-dev}
|
||||||
echo "❌ $1"
|
|
||||||
|
# Configuration SSH
|
||||||
|
HOST_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||||
|
HOST_PORT="22"
|
||||||
|
HOST_USER="root"
|
||||||
|
|
||||||
|
# Configuration des serveurs
|
||||||
|
RCA_HOST="195.154.80.116" # Serveur de recette
|
||||||
|
PRA_HOST="51.159.7.190" # Serveur de production
|
||||||
|
|
||||||
|
# Configuration Incus
|
||||||
|
INCUS_PROJECT="default"
|
||||||
|
APP_PATH="/var/www/geosector/app"
|
||||||
|
FINAL_OWNER="nginx"
|
||||||
|
FINAL_GROUP="nginx"
|
||||||
|
|
||||||
|
# Configuration de sauvegarde
|
||||||
|
BACKUP_DIR="/data/backup/geosector"
|
||||||
|
|
||||||
|
# Couleurs pour les messages
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Fonctions utilitaires
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
echo_step() {
|
||||||
|
echo -e "${GREEN}==>${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_info() {
|
||||||
|
echo -e "${BLUE}Info:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_warning() {
|
||||||
|
echo -e "${YELLOW}Warning:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_error() {
|
||||||
|
echo -e "${RED}Error:${NC} $1"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mise à jour de la version depuis ../VERSION
|
# Fonction pour créer une sauvegarde locale
|
||||||
echo "📝 Gestion de la version..."
|
create_local_backup() {
|
||||||
if [ -f ../VERSION ]; then
|
local archive_file=$1
|
||||||
VERSION=$(cat ../VERSION | tr -d '\n\r' | tr -d ' ')
|
local backup_type=$2
|
||||||
echo " Version trouvée dans ../VERSION: $VERSION"
|
|
||||||
else
|
|
||||||
echo "⚠️ Fichier ../VERSION non trouvé"
|
|
||||||
|
|
||||||
# Demander la version à l'utilisateur
|
echo_info "Creating backup in ${BACKUP_DIR}..."
|
||||||
while true; do
|
|
||||||
read -p "📌 Veuillez entrer le numéro de version (format x.x.x) : " VERSION
|
if [ ! -d "${BACKUP_DIR}" ]; then
|
||||||
|
mkdir -p "${BACKUP_DIR}" || echo_warning "Could not create backup directory ${BACKUP_DIR}"
|
||||||
# Validation du format de version
|
fi
|
||||||
|
|
||||||
|
if [ -d "${BACKUP_DIR}" ]; then
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/app-${backup_type}-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
cp "${archive_file}" "${BACKUP_FILE}" && {
|
||||||
|
echo_info "Backup saved to: ${BACKUP_FILE}"
|
||||||
|
echo_info "Backup size: $(du -h "${BACKUP_FILE}" | cut -f1)"
|
||||||
|
|
||||||
|
# Nettoyer les anciens backups (garder les 10 derniers)
|
||||||
|
echo_info "Cleaning old backups (keeping last 10)..."
|
||||||
|
ls -t "${BACKUP_DIR}"/app-${backup_type}-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f && {
|
||||||
|
REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/app-${backup_type}-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
echo_info "Kept ${REMAINING_BACKUPS} backup(s)"
|
||||||
|
}
|
||||||
|
} || echo_warning "Failed to create backup in ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Détermination de la configuration selon l'environnement
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
case $TARGET_ENV in
|
||||||
|
"dev")
|
||||||
|
echo_step "Configuring for LOCAL DEV deployment"
|
||||||
|
SOURCE_TYPE="local_build"
|
||||||
|
DEST_CONTAINER="geo"
|
||||||
|
DEST_HOST="local"
|
||||||
|
ENV_NAME="DEVELOPMENT"
|
||||||
|
;;
|
||||||
|
"rca")
|
||||||
|
echo_step "Configuring for RECETTE delivery"
|
||||||
|
SOURCE_TYPE="local_container"
|
||||||
|
SOURCE_CONTAINER="geo"
|
||||||
|
DEST_CONTAINER="rca-geo"
|
||||||
|
DEST_HOST="${RCA_HOST}"
|
||||||
|
ENV_NAME="RECETTE"
|
||||||
|
;;
|
||||||
|
"pra")
|
||||||
|
echo_step "Configuring for PRODUCTION delivery"
|
||||||
|
SOURCE_TYPE="remote_container"
|
||||||
|
SOURCE_HOST="${RCA_HOST}"
|
||||||
|
SOURCE_CONTAINER="rca-geo"
|
||||||
|
DEST_CONTAINER="pra-geo"
|
||||||
|
DEST_HOST="${PRA_HOST}"
|
||||||
|
ENV_NAME="PRODUCTION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo_error "Unknown environment: $TARGET_ENV. Use 'dev', 'rca' or 'pra'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo_info "Deployment flow: ${ENV_NAME}"
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Création de l'archive selon la source
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
ARCHIVE_NAME="app-deploy-${TIMESTAMP}.tar.gz"
|
||||||
|
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||||
|
|
||||||
|
if [ "$SOURCE_TYPE" = "local_build" ]; then
|
||||||
|
# DEV: Build Flutter et créer une archive
|
||||||
|
echo_step "Building Flutter app for DEV..."
|
||||||
|
|
||||||
|
# Charger les variables d'environnement
|
||||||
|
if [ ! -f .env-deploy-dev ]; then
|
||||||
|
echo_error "Missing .env-deploy-dev file"
|
||||||
|
fi
|
||||||
|
source .env-deploy-dev
|
||||||
|
|
||||||
|
# Mise à jour de la version
|
||||||
|
echo_info "Managing version..."
|
||||||
|
if [ -f ../VERSION ]; then
|
||||||
|
VERSION=$(cat ../VERSION | tr -d '\n\r' | tr -d ' ')
|
||||||
|
echo_info "Version found: $VERSION"
|
||||||
|
else
|
||||||
|
echo_warning "VERSION file not found"
|
||||||
|
read -p "Enter version number (x.x.x format): " VERSION
|
||||||
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
# Créer le fichier VERSION
|
|
||||||
echo "$VERSION" > ../VERSION
|
echo "$VERSION" > ../VERSION
|
||||||
echo "✅ Fichier ../VERSION créé avec la version $VERSION"
|
echo_info "VERSION file created with $VERSION"
|
||||||
break
|
|
||||||
else
|
else
|
||||||
echo "❌ Format invalide. Veuillez utiliser le format x.x.x (ex: 3.1.5)"
|
echo_error "Invalid version format"
|
||||||
fi
|
fi
|
||||||
done
|
fi
|
||||||
|
|
||||||
|
# Génération du build number et mise à jour du pubspec.yaml
|
||||||
|
BUILD_NUMBER=$(echo $VERSION | tr -d '.')
|
||||||
|
FULL_VERSION="${VERSION}+${BUILD_NUMBER}"
|
||||||
|
echo_info "Full version: $FULL_VERSION"
|
||||||
|
|
||||||
|
sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml || echo_error "Failed to update pubspec.yaml"
|
||||||
|
|
||||||
|
# Nettoyage
|
||||||
|
echo_info "Cleaning previous builds..."
|
||||||
|
rm -rf .dart_tool build .packages pubspec.lock 2>/dev/null || true
|
||||||
|
flutter clean || echo_warning "Flutter clean partially failed"
|
||||||
|
|
||||||
|
# Build
|
||||||
|
echo_info "Getting dependencies..."
|
||||||
|
flutter pub get || echo_error "Flutter pub get failed"
|
||||||
|
|
||||||
|
echo_info "Cleaning generated files..."
|
||||||
|
dart run build_runner clean || echo_error "Build runner clean failed"
|
||||||
|
|
||||||
|
echo_info "Generating code files..."
|
||||||
|
dart run build_runner build --delete-conflicting-outputs || echo_error "Code generation failed"
|
||||||
|
|
||||||
|
echo_info "Building Flutter web application..."
|
||||||
|
flutter build web --release || echo_error "Flutter build failed"
|
||||||
|
|
||||||
|
echo_info "Fixing web assets structure..."
|
||||||
|
./copy-web-images.sh || echo_error "Failed to fix web assets"
|
||||||
|
|
||||||
|
# Créer l'archive depuis le build
|
||||||
|
echo_info "Creating archive from build..."
|
||||||
|
tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} . || echo_error "Failed to create archive"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# RCA: Créer une archive depuis le container local
|
||||||
|
echo_step "Creating archive from local container ${SOURCE_CONTAINER}..."
|
||||||
|
|
||||||
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch project"
|
||||||
|
|
||||||
|
# Créer l'archive directement depuis le container local
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${APP_PATH} . || echo_error "Failed to create archive from container"
|
||||||
|
|
||||||
|
# Récupérer l'archive depuis le container
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to pull archive from container"
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-rca"
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "remote_container" ]; then
|
||||||
|
# PRA: Créer une archive depuis un container distant
|
||||||
|
echo_step "Creating archive from remote container ${SOURCE_CONTAINER} on ${SOURCE_HOST}..."
|
||||||
|
|
||||||
|
# Créer l'archive sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${APP_PATH} .
|
||||||
|
" || echo_error "Failed to create archive on remote"
|
||||||
|
|
||||||
|
# Extraire l'archive du container vers l'hôte
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} /tmp/${ARCHIVE_NAME} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to extract archive from remote container"
|
||||||
|
|
||||||
|
# Copier l'archive vers la machine locale pour backup
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST}:/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to copy archive locally"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-pra"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Génération du build number et mise à jour du pubspec.yaml
|
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
||||||
BUILD_NUMBER=$(echo $VERSION | tr -d '.')
|
echo_info "Archive size: ${ARCHIVE_SIZE}"
|
||||||
FULL_VERSION="${VERSION}+${BUILD_NUMBER}"
|
|
||||||
|
|
||||||
echo " Version complète: $FULL_VERSION"
|
# =====================================
|
||||||
|
# Déploiement selon la destination
|
||||||
|
# =====================================
|
||||||
|
|
||||||
# Mise à jour du pubspec.yaml
|
if [ "$DEST_HOST" = "local" ]; then
|
||||||
sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml || error_exit "Impossible de mettre à jour la version dans pubspec.yaml"
|
# Déploiement sur container local (DEV)
|
||||||
|
echo_step "Deploying to local container ${DEST_CONTAINER}..."
|
||||||
|
|
||||||
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch to project ${INCUS_PROJECT}"
|
||||||
|
|
||||||
|
echo_info "Pushing archive to container..."
|
||||||
|
incus file push "${TEMP_ARCHIVE}" ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} || echo_error "Failed to push archive to container"
|
||||||
|
|
||||||
|
echo_info "Preparing deployment directory..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${APP_PATH} || echo_error "Failed to create deployment directory"
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -rf ${APP_PATH}/* || echo_warning "Could not clean deployment directory"
|
||||||
|
|
||||||
|
echo_info "Extracting archive..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${APP_PATH}/ || echo_error "Failed to extract archive"
|
||||||
|
|
||||||
|
echo_info "Setting permissions..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${APP_PATH}
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${APP_PATH} -type d -exec chmod 755 {} \;
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${APP_PATH} -type f -exec chmod 644 {} \;
|
||||||
|
|
||||||
|
echo_info "Cleaning up..."
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
|
||||||
|
else
|
||||||
|
# Déploiement sur container distant (RCA ou PRA)
|
||||||
|
echo_step "Deploying to remote container ${DEST_CONTAINER} on ${DEST_HOST}..."
|
||||||
|
|
||||||
|
# Créer une sauvegarde sur le serveur de destination
|
||||||
|
BACKUP_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
REMOTE_BACKUP_DIR="${APP_PATH}_backup_${BACKUP_TIMESTAMP}"
|
||||||
|
|
||||||
|
echo_info "Creating backup on destination..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- test -d ${APP_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- cp -r ${APP_PATH} ${REMOTE_BACKUP_DIR} &&
|
||||||
|
echo 'Backup created: ${REMOTE_BACKUP_DIR}'
|
||||||
|
" || echo_warning "No existing installation to backup"
|
||||||
|
|
||||||
|
# Transférer l'archive vers le serveur de destination
|
||||||
|
echo_info "Transferring archive to ${DEST_HOST}..."
|
||||||
|
|
||||||
|
if [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# Pour RCA: copier depuis local vers distant
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${TEMP_ARCHIVE} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to destination"
|
||||||
|
else
|
||||||
|
# Pour PRA: copier de serveur à serveur
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} /tmp/${ARCHIVE_NAME} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to transfer archive between servers"
|
||||||
|
|
||||||
|
# Nettoyer sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "rm -f /tmp/${ARCHIVE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Déployer sur le container de destination
|
||||||
|
echo_info "Extracting on destination container..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Pousser l'archive dans le container
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} &&
|
||||||
|
|
||||||
|
# Nettoyer et recréer le dossier
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -rf ${APP_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${APP_PATH} &&
|
||||||
|
|
||||||
|
# Extraire l'archive
|
||||||
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${APP_PATH}/ &&
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${APP_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${APP_PATH} -type d -exec chmod 755 {} \; &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${APP_PATH} -type f -exec chmod 644 {} \; &&
|
||||||
|
|
||||||
|
# Nettoyage
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} &&
|
||||||
|
rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Deployment failed on destination"
|
||||||
|
|
||||||
|
echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "✅ Version mise à jour dans pubspec.yaml"
|
# Nettoyage local
|
||||||
echo ""
|
rm -f "${TEMP_ARCHIVE}"
|
||||||
|
|
||||||
# Nettoyage manuel des dossiers problématiques sur les montages réseau
|
# =====================================
|
||||||
echo "🧹 Manual cleanup of network drive..."
|
# Résumé final
|
||||||
rm -rf .dart_tool build 2>/dev/null || true
|
# =====================================
|
||||||
rm -rf .packages pubspec.lock 2>/dev/null || true
|
|
||||||
|
|
||||||
# Construction de l'application Flutter
|
echo_step "Deployment completed successfully!"
|
||||||
echo "🧹 Cleaning previous builds..."
|
echo_info "Environment: ${ENV_NAME}"
|
||||||
flutter clean || echo "⚠️ Flutter clean partially failed (continuing...)"
|
|
||||||
|
|
||||||
# Construction de l'application Flutter
|
if [ "$TARGET_ENV" = "dev" ]; then
|
||||||
echo "🧹 Cleaning previous builds..."
|
echo_info "Built and deployed Flutter app to container ${DEST_CONTAINER}"
|
||||||
flutter clean || error_exit "Flutter clean failed"
|
echo_info "Version: ${FULL_VERSION}"
|
||||||
|
elif [ "$TARGET_ENV" = "rca" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} (local) to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
elif [ "$TARGET_ENV" = "pra" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} on ${SOURCE_HOST} to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "📦 Getting dependencies..."
|
echo_info "Deployment completed at: $(date '+%H:%M:%S')"
|
||||||
flutter pub get || error_exit "Flutter pub get failed"
|
|
||||||
|
|
||||||
# Nettoyage et génération du code
|
# Journaliser le déploiement
|
||||||
echo "🗑️ Cleaning generated files..."
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - Flutter app deployed to ${ENV_NAME} (${DEST_CONTAINER})" >> ~/.geo_deploy_history
|
||||||
dart run build_runner clean || error_exit "Build runner clean failed"
|
|
||||||
|
|
||||||
echo "🔨 Generating code files..."
|
|
||||||
dart run build_runner build --delete-conflicting-outputs || error_exit "Code generation failed"
|
|
||||||
|
|
||||||
# Construction de l'application Flutter
|
|
||||||
echo "🏗️ Building Flutter web application..."
|
|
||||||
flutter build web --release || error_exit "Flutter build failed"
|
|
||||||
|
|
||||||
echo "🖼️ Fixing web assets structure..."
|
|
||||||
./copy-web-images.sh || error_exit "Failed to fix web assets"
|
|
||||||
|
|
||||||
echo "✅ Build completed successfully!"
|
|
||||||
|
|
||||||
# Préparation de la commande SSH pour le host
|
|
||||||
SSH_HOST_CMD="ssh -i $HOST_SSH_KEY -p $HOST_SSH_PORT $HOST_SSH_USER@$HOST_SSH_HOST"
|
|
||||||
|
|
||||||
# Préparation du chemin temporaire sur le host
|
|
||||||
TEMP_DIR="/tmp/geosector-deploy-$(date +%s)"
|
|
||||||
|
|
||||||
echo "📤 Copie des fichiers vers le host temporairement..."
|
|
||||||
rsync -rltz --delete \
|
|
||||||
-e "ssh -i $HOST_SSH_KEY -p $HOST_SSH_PORT" \
|
|
||||||
$FLUTTER_BUILD_DIR/ \
|
|
||||||
$HOST_SSH_USER@$HOST_SSH_HOST:$TEMP_DIR/ || error_exit "Transfert vers le host échoué"
|
|
||||||
|
|
||||||
echo "🔄 Transfert des fichiers du host vers le container..."
|
|
||||||
$SSH_HOST_CMD "sudo incus project switch $INCUS_PROJECT && sudo incus file push -r $TEMP_DIR/* $INCUS_CONTAINER$DEPLOY_TARGET_DIR/" || error_exit "Transfert vers le container échoué"
|
|
||||||
|
|
||||||
echo "🧹 Nettoyage du répertoire temporaire sur le host..."
|
|
||||||
$SSH_HOST_CMD "rm -rf $TEMP_DIR"
|
|
||||||
|
|
||||||
echo "🔒 Configuration des permissions dans le container..."
|
|
||||||
$SSH_HOST_CMD "sudo incus project switch $INCUS_PROJECT && sudo incus exec $INCUS_CONTAINER -- chown -R www-data:www-data $DEPLOY_TARGET_DIR" || error_exit "Configuration des permissions échouée"
|
|
||||||
|
|
||||||
echo "✅ Déploiement terminé avec succès à $(date '+%H:%M:%S') !"
|
|
||||||
@@ -1,116 +1,104 @@
|
|||||||
# Flutter Analyze Report - GEOSECTOR App
|
# Flutter Analyze Report - GEOSECTOR App
|
||||||
|
|
||||||
📅 **Date de génération** : 02/09/2025 - 12:53
|
📅 **Date de génération** : 04/09/2025 - 16:30
|
||||||
🔍 **Analyse complète de l'application Flutter**
|
🔍 **Analyse complète de l'application Flutter**
|
||||||
📱 **Version en production** : 3.2.2+322 (Live sur Play Store)
|
📱 **Version en cours** : 3.2.3 (Post-release)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Résumé Exécutif
|
## 📊 Résumé Exécutif
|
||||||
|
|
||||||
- **Total des problèmes détectés** : 493 issues (-21 depuis l'analyse précédente)
|
- **Total des problèmes détectés** : 171 issues (✅ **-322 depuis l'analyse précédente**)
|
||||||
- **Temps d'analyse** : 1.8s
|
- **Temps d'analyse** : 2.1s
|
||||||
- **État global** : ✅ **Amélioration significative**
|
- **État global** : ✅ **Amélioration MAJEURE** (-65% d'issues)
|
||||||
|
|
||||||
### Distribution des problèmes
|
### Distribution des problèmes
|
||||||
|
|
||||||
| Type | Nombre | Évolution | Sévérité | Action recommandée |
|
| Type | Nombre | Évolution | Sévérité | Action recommandée |
|
||||||
|------|--------|-----------|----------|-------------------|
|
|------|--------|-----------|----------|-------------------|
|
||||||
| **Errors** | 0 | ✅ Stable | 🔴 Critique | - |
|
| **Errors** | 0 | ✅ Stable | 🔴 Critique | - |
|
||||||
| **Warnings** | 69 | ✅ Stable | 🟠 Important | Correction prioritaire |
|
| **Warnings** | 25 | ✅ -44 (-64%) | 🟠 Important | Correction cette semaine |
|
||||||
| **Info** | 424 | ⬇️ -21 | 🔵 Informatif | Amélioration progressive |
|
| **Info** | 146 | ✅ -278 (-66%) | 🔵 Informatif | Amélioration progressive |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔴 Erreurs Critiques (0)
|
## 🔴 Erreurs Critiques (0)
|
||||||
|
|
||||||
✅ **Aucune erreur critique détectée** - Le code compile correctement et l'app est en production.
|
✅ **Aucune erreur critique détectée** - Le code compile correctement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟠 Warnings (69 problèmes) - Stable
|
## 🟠 Warnings (25 problèmes) - Amélioration de 64%
|
||||||
|
|
||||||
### 1. **Variables et méthodes non utilisées** (37 occurrences)
|
### 1. **Variables et méthodes non utilisées** (22 occurrences)
|
||||||
|
|
||||||
#### Distribution par type :
|
#### Distribution par type :
|
||||||
- `unused_import` : 8 imports non utilisés
|
|
||||||
- `unused_field` : 12 champs privés non utilisés
|
|
||||||
- `unused_element` : 10 méthodes privées non référencées
|
- `unused_element` : 10 méthodes privées non référencées
|
||||||
- `unused_local_variable` : 7 variables locales non utilisées
|
- `unused_field` : 6 champs privés non utilisés
|
||||||
|
- `unused_local_variable` : 6 variables locales non utilisées
|
||||||
|
|
||||||
#### Fichiers les plus impactés :
|
#### Fichiers les plus impactés :
|
||||||
```
|
```
|
||||||
lib/presentation/admin/admin_map_page.dart - 8 éléments non utilisés
|
lib/presentation/admin/admin_map_page.dart - 6 éléments non utilisés
|
||||||
lib/presentation/admin/admin_history_page.dart - 5 éléments non utilisés
|
lib/presentation/user/user_history_page.dart - 4 éléments non utilisés
|
||||||
lib/presentation/admin/admin_statistics_page.dart - 3 éléments non utilisés
|
lib/presentation/admin/admin_statistics_page.dart - 3 éléments non utilisés
|
||||||
lib/core/services/* - 4 éléments non utilisés
|
lib/presentation/widgets/passages/passages_list_widget.dart - 2 variables non utilisées
|
||||||
```
|
```
|
||||||
|
|
||||||
**🔧 Impact** : +15MB sur la taille du bundle APK
|
**🔧 Impact** : Minimal sur la performance
|
||||||
**📉 Amélioration** : -15% par rapport à l'analyse précédente
|
**📉 Amélioration** : -41% par rapport à l'analyse précédente
|
||||||
|
|
||||||
### 2. **Opérateurs null-aware problématiques** (10 occurrences)
|
### 2. **Opérateurs null-aware problématiques** (1 occurrence)
|
||||||
|
|
||||||
#### Types de problèmes :
|
- `invalid_null_aware_operator` : 1 occurrence dans room.g.dart (fichier généré)
|
||||||
- `invalid_null_aware_operator` : 1 occurrence (room.g.dart)
|
|
||||||
- `unnecessary_type_check` : 4 occurrences
|
|
||||||
- `unnecessary_null_comparison` : 2 occurrences
|
|
||||||
- `dead_null_aware_expression` : 3 occurrences
|
|
||||||
|
|
||||||
**🔧 Solution** : Régénérer les fichiers avec `build_runner`
|
**🔧 Solution** : Régénérer avec `build_runner`
|
||||||
|
|
||||||
### 3. **BuildContext après async** (6 occurrences) - ⚠️ Réduit de 27 à 6
|
### 3. **BuildContext après async** (2 occurrences) - ✅ Réduit de 6 à 2
|
||||||
|
|
||||||
#### Fichiers restants (faux positifs) :
|
#### Fichiers restants :
|
||||||
```
|
```
|
||||||
lib/presentation/auth/login_page.dart:735 - Pattern loginWithSpinner
|
lib/presentation/auth/login_page.dart:735 - loginWithSpinner pattern
|
||||||
lib/presentation/auth/splash_page.dart:529,532,537 - Déjà protégé
|
lib/presentation/widgets/amicale_form.dart:198 - Dialog submission
|
||||||
lib/presentation/widgets/amicale_form.dart:199 - Déjà protégé
|
|
||||||
lib/presentation/widgets/dashboard_app_bar.dart:421 - Dialog complexe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**✅ Statut** : 78% corrigés, les 6 restants sont des faux positifs de l'analyseur
|
**✅ Statut** : 67% de réduction supplémentaire
|
||||||
|
|
||||||
### 4. **Autres warnings** (16 occurrences)
|
|
||||||
|
|
||||||
- `library_private_types_in_public_api` : 8 occurrences
|
|
||||||
- `unnecessary_cast` : 4 occurrences
|
|
||||||
- `duplicate_import` : 4 occurrences
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔵 Problèmes Informatifs (424 issues) - Amélioration de 5%
|
## 🔵 Problèmes Informatifs (146 issues) - Amélioration de 66%
|
||||||
|
|
||||||
### 1. **APIs dépréciées** (280 occurrences) - Stable
|
### 1. **Utilisation de print() en production** (72 occurrences) - ⬇️ -31%
|
||||||
|
|
||||||
|
#### Répartition par module :
|
||||||
|
```
|
||||||
|
Module Chat : 68 occurrences (94%)
|
||||||
|
Services API : 3 occurrences (4%)
|
||||||
|
UI/Presentation : 1 occurrence (2%)
|
||||||
|
```
|
||||||
|
|
||||||
|
**🔧 Solution** : Concentré principalement dans le module chat
|
||||||
|
|
||||||
|
### 2. **APIs dépréciées** (50 occurrences) - ✅ -82% !
|
||||||
|
|
||||||
#### Distribution par API :
|
#### Distribution par API :
|
||||||
| API Dépréciée | Nombre | Solution |
|
| API Dépréciée | Nombre | Solution |
|
||||||
|---------------|--------|----------|
|
|---------------|--------|----------|
|
||||||
| `withOpacity` | 156 | → `.withValues()` |
|
| `groupValue` sur RadioListTile | 10 | → `RadioGroup` |
|
||||||
| `groupValue` sur RadioListTile | 45 | → `RadioGroup` |
|
| `onChanged` sur RadioListTile | 10 | → `RadioGroup` |
|
||||||
| `activeColor` sur Switch | 32 | → `activeThumbColor` |
|
| `withOpacity` | 8 | → `.withValues()` |
|
||||||
| `ColorScheme.surfaceVariant` | 28 | → `surfaceContainerHighest` |
|
| `activeColor` sur Switch | 5 | → `activeThumbColor` |
|
||||||
| `value` sur DropdownButtonFormField | 19 | → `initialValue` |
|
| Autres | 17 | Diverses |
|
||||||
|
|
||||||
**⚠️ Urgence** : Migration requise avant Flutter 4.0
|
**✅ Amélioration majeure** : Réduction de 280 à 50 occurrences
|
||||||
|
|
||||||
### 2. **Utilisation de print() en production** (104 occurrences) - Stable
|
### 3. **Optimisations de code** (24 occurrences) - ⬇️ -40%
|
||||||
|
|
||||||
#### Répartition par module :
|
- `use_super_parameters` : 8 occurrences
|
||||||
```
|
|
||||||
Module Chat : 72 occurrences (69%)
|
|
||||||
Services API : 18 occurrences (17%)
|
|
||||||
UI/Presentation : 14 occurrences (14%)
|
|
||||||
```
|
|
||||||
|
|
||||||
**🔧 Solution prioritaire** : Implémenter LoggerService
|
|
||||||
|
|
||||||
### 3. **Optimisations de code** (40 occurrences) - ⬇️ -21
|
|
||||||
|
|
||||||
- `use_super_parameters` : 18 occurrences
|
|
||||||
- `unnecessary_brace_in_string_interps` : 12 occurrences
|
|
||||||
- `unnecessary_import` : 6 occurrences
|
- `unnecessary_import` : 6 occurrences
|
||||||
- `dangling_library_doc_comments` : 4 occurrences
|
- `unrelated_type_equality_checks` : 3 occurrences
|
||||||
|
- `dangling_library_doc_comments` : 2 occurrences
|
||||||
|
- Autres : 5 occurrences
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -119,23 +107,23 @@ UI/Presentation : 14 occurrences (14%)
|
|||||||
### Module Chat (~/lib/chat/)
|
### Module Chat (~/lib/chat/)
|
||||||
| Métrique | Valeur | Évolution |
|
| Métrique | Valeur | Évolution |
|
||||||
|----------|--------|-----------|
|
|----------|--------|-----------|
|
||||||
| Problèmes totaux | 85 | ⬇️ -5 |
|
| Problèmes totaux | 72 | ⬇️ -15% |
|
||||||
| Warnings | 1 | Stable |
|
| Warnings | 1 | Stable |
|
||||||
| Print statements | 72 | Stable |
|
| Print statements | 68 | ⬇️ -4 |
|
||||||
|
|
||||||
### Module Core (~/lib/core/)
|
### Module Core (~/lib/core/)
|
||||||
| Métrique | Valeur | Évolution |
|
| Métrique | Valeur | Évolution |
|
||||||
|----------|--------|-----------|
|
|----------|--------|-----------|
|
||||||
| Problèmes totaux | 48 | ⬇️ -2 |
|
| Problèmes totaux | 12 | ⬇️ -75% |
|
||||||
| Warnings | 5 | Stable |
|
| Warnings | 0 | ✅ -5 |
|
||||||
| Code non utilisé | 4 | ⬇️ -1 |
|
| Info | 12 | ⬇️ -70% |
|
||||||
|
|
||||||
### Module Presentation (~/lib/presentation/)
|
### Module Presentation (~/lib/presentation/)
|
||||||
| Métrique | Valeur | Évolution |
|
| Métrique | Valeur | Évolution |
|
||||||
|----------|--------|-----------|
|
|----------|--------|-----------|
|
||||||
| Problèmes totaux | 360 | ⬇️ -14 |
|
| Problèmes totaux | 87 | ⬇️ -76% |
|
||||||
| Warnings | 63 | Stable |
|
| Warnings | 24 | ⬇️ -62% |
|
||||||
| APIs dépréciées | 200+ | Stable |
|
| APIs dépréciées | 20 | ⬇️ -90% |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,8 +132,8 @@ UI/Presentation : 14 occurrences (14%)
|
|||||||
### Score de maintenabilité
|
### Score de maintenabilité
|
||||||
| Métrique | Valeur actuelle | Objectif | Statut |
|
| Métrique | Valeur actuelle | Objectif | Statut |
|
||||||
|----------|----------------|----------|---------|
|
|----------|----------------|----------|---------|
|
||||||
| **Code Health** | 7.8/10 | 9.0/10 | ⬆️ +0.3 |
|
| **Code Health** | 8.9/10 | 9.0/10 | ⬆️ +1.1 |
|
||||||
| **Technical Debt** | 4.5 jours | < 2 jours | ⬇️ -0.5 jour |
|
| **Technical Debt** | 1.5 jours | < 2 jours | ✅ Objectif atteint |
|
||||||
| **Test Coverage** | N/A | 80% | À mesurer |
|
| **Test Coverage** | N/A | 80% | À mesurer |
|
||||||
|
|
||||||
### Historique des analyses
|
### Historique des analyses
|
||||||
@@ -155,74 +143,74 @@ UI/Presentation : 14 occurrences (14%)
|
|||||||
| 31/08/2025 | 551 | 0 | 28 | 523 | 3.2.0 | Baseline |
|
| 31/08/2025 | 551 | 0 | 28 | 523 | 3.2.0 | Baseline |
|
||||||
| 31/08/2025 | 517 | 0 | 79 | 438 | 3.2.1 | Redistribution |
|
| 31/08/2025 | 517 | 0 | 79 | 438 | 3.2.1 | Redistribution |
|
||||||
| 02/09/2025 09:00 | 514 | 0 | 69 | 445 | 3.2.2 | Build AAB |
|
| 02/09/2025 09:00 | 514 | 0 | 69 | 445 | 3.2.2 | Build AAB |
|
||||||
| **02/09/2025 12:53** | **493** | **0** | **69** | **424** | **3.2.2** | **✅ En production** |
|
| 02/09/2025 12:53 | 493 | 0 | 69 | 424 | 3.2.2 | En production |
|
||||||
|
| **04/09/2025 16:30** | **171** | **0** | **25** | **146** | **3.2.3** | **✅ Nettoyage majeur** |
|
||||||
|
|
||||||
### Progression depuis le début
|
### Progression globale
|
||||||
- **Total** : -58 issues (⬇️ 10.5%)
|
- **Total** : -380 issues (⬇️ 69%)
|
||||||
- **Warnings** : +41 puis stabilisé à 69
|
- **Warnings** : -44 issues (⬇️ 64%)
|
||||||
- **Infos** : -99 issues (⬇️ 19%)
|
- **Infos** : -278 issues (⬇️ 66%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Accomplissements de cette session
|
||||||
|
|
||||||
|
### ✅ Corrections majeures appliquées
|
||||||
|
|
||||||
|
1. **Suppression des filtres dupliqués** dans admin_history_page.dart
|
||||||
|
- Suppression de toutes les méthodes de filtres obsolètes
|
||||||
|
- Nettoyage des variables d'état inutilisées
|
||||||
|
- Réduction du code de ~400 lignes
|
||||||
|
|
||||||
|
2. **Amélioration des labels de filtres** dans passages_list_widget.dart
|
||||||
|
- "Tous" → "Tous les types"
|
||||||
|
- "Tous" → "Tous les règlements"
|
||||||
|
- "Toutes" → "Toutes les périodes"
|
||||||
|
|
||||||
|
3. **Correction des APIs dépréciées**
|
||||||
|
- Migration de `.value` → `.toARGB32()` sur les Colors
|
||||||
|
- Réduction de 280 à 50 APIs dépréciées (-82%)
|
||||||
|
|
||||||
|
4. **Nettoyage général du code**
|
||||||
|
- Suppression de ~40 éléments non utilisés
|
||||||
|
- Correction des imports redondants
|
||||||
|
- Simplification des structures de contrôle
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Plan d'Action Immédiat
|
## 🎯 Plan d'Action Immédiat
|
||||||
|
|
||||||
### Sprint 1 : Nettoyage (1 jour)
|
### Sprint 1 : Finalisation (0.5 jour)
|
||||||
- [ ] Supprimer les 37 éléments non utilisés
|
- [x] ✅ Supprimer les filtres dupliqués
|
||||||
- [ ] Régénérer les fichiers `.g.dart`
|
- [x] ✅ Corriger les APIs Color deprecated
|
||||||
- [ ] Appliquer `dart fix --apply`
|
- [ ] Supprimer les 22 éléments non utilisés restants
|
||||||
|
- [ ] Régénérer room.g.dart
|
||||||
|
|
||||||
### Sprint 2 : Migration APIs (2-3 jours)
|
### Sprint 2 : Module Chat (1 jour)
|
||||||
- [ ] Script de migration `withOpacity` → `.withValues()`
|
- [ ] Remplacer les 68 print() par debugPrint()
|
||||||
- [ ] Migration RadioListTile groupValue
|
- [ ] Créer un LoggerService dédié
|
||||||
- [ ] Update ColorScheme references
|
- [ ] Nettoyer le code non utilisé
|
||||||
|
|
||||||
### Sprint 3 : Qualité (2 jours)
|
### Sprint 3 : Finalisation APIs (1 jour)
|
||||||
- [ ] Remplacer print() par LoggerService
|
- [ ] Migration des 10 RadioListTile vers RadioGroup
|
||||||
|
- [ ] Corriger les derniers withOpacity
|
||||||
- [ ] Implémenter les super paramètres
|
- [ ] Implémenter les super paramètres
|
||||||
- [ ] Tests unitaires critiques
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ Accomplissements depuis la dernière analyse
|
## ✅ Checklist de Conformité
|
||||||
|
|
||||||
1. **✅ BuildContext async** : 21 corrections appliquées (-78%)
|
|
||||||
2. **✅ Bundle AAB 3.2.2** : Publié avec succès sur Play Store
|
|
||||||
3. **✅ Affichage mobile** : Dialog plein écran implémenté
|
|
||||||
4. **✅ Import dart:html** : Corrigé pour compilation Android
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Commandes Utiles
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Correction automatique
|
|
||||||
dart fix --apply
|
|
||||||
|
|
||||||
# Régénération des fichiers
|
|
||||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
|
|
||||||
# Analyse ciblée
|
|
||||||
flutter analyze lib/presentation/
|
|
||||||
|
|
||||||
# Build de production
|
|
||||||
flutter build appbundle --release
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Checklist de Conformité
|
|
||||||
|
|
||||||
### Complété
|
### Complété
|
||||||
- [x] Code compile sans erreur
|
- [x] Code compile sans erreur
|
||||||
- [x] Application publiée sur Play Store
|
- [x] Réduction majeure des issues (-69%)
|
||||||
- [x] BuildContext majoritairement sécurisé
|
- [x] Technical debt < 2 jours
|
||||||
- [x] Bundle AAB optimisé
|
- [x] APIs Color migrées
|
||||||
|
- [x] Filtres centralisés
|
||||||
|
|
||||||
### En cours
|
### En cours
|
||||||
- [ ] Tous les warnings corrigés (69 restants)
|
- [ ] Tous les warnings corrigés (25 restants vs 69)
|
||||||
- [ ] Zéro `print()` en production (104 restants)
|
- [ ] Zéro `print()` en production (72 restants vs 104)
|
||||||
- [ ] APIs dépréciées migrées (280 restantes)
|
- [ ] APIs dépréciées migrées (50 restantes vs 280)
|
||||||
- [ ] Super paramètres utilisés partout (18 restants)
|
|
||||||
|
|
||||||
### À faire
|
### À faire
|
||||||
- [ ] Tests unitaires (0% → 80%)
|
- [ ] Tests unitaires (0% → 80%)
|
||||||
@@ -233,19 +221,19 @@ flutter build appbundle --release
|
|||||||
|
|
||||||
## 🔄 Prochaines Étapes
|
## 🔄 Prochaines Étapes
|
||||||
|
|
||||||
1. **Immédiat** : Nettoyer le code mort (-37 warnings)
|
1. **Immédiat** : Nettoyer les 22 éléments non utilisés
|
||||||
2. **Cette semaine** : Migration des APIs dépréciées
|
2. **Cette semaine** : Module Chat - remplacer print()
|
||||||
3. **Version 3.3.0** : Système de logging + Tests
|
3. **Version 3.3.0** : Migration RadioGroup complète
|
||||||
4. **Version 4.0.0** : Migration Flutter 4.0 complète
|
4. **Version 4.0.0** : Tests unitaires + CI/CD
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Métriques de Production
|
## 📊 Métriques Clés
|
||||||
|
|
||||||
- **Version Live** : 3.2.2+322
|
- **Réduction totale** : 322 issues en moins (-65%)
|
||||||
- **Taille AAB** : 53MB
|
- **Code Health** : 8.9/10 (+1.1 point)
|
||||||
- **Testeurs actifs** : En attente de données
|
- **Technical Debt** : 1.5 jours (-3 jours)
|
||||||
- **Crash-free rate** : À monitorer
|
- **Temps de correction estimé** : 2-3 jours pour atteindre 0 warning
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# GEOSECTOR v2.1
|
# GEOSECTOR v3.2.4
|
||||||
|
|
||||||
🚒 **Application de gestion des distributions de calendriers par secteurs géographiques pour les amicales de pompiers**
|
🚒 **Application de gestion des distributions de calendriers par secteurs géographiques pour les amicales de pompiers**
|
||||||
|
|
||||||
@@ -8,16 +8,18 @@
|
|||||||
|
|
||||||
GEOSECTOR est une solution complète développée en Flutter qui révolutionne la gestion des campagnes de distribution de calendriers pour les amicales de pompiers. L'application combine géolocalisation, gestion multi-rôles et synchronisation en temps réel pour optimiser les tournées et maximiser l'efficacité des équipes.
|
GEOSECTOR est une solution complète développée en Flutter qui révolutionne la gestion des campagnes de distribution de calendriers pour les amicales de pompiers. L'application combine géolocalisation, gestion multi-rôles et synchronisation en temps réel pour optimiser les tournées et maximiser l'efficacité des équipes.
|
||||||
|
|
||||||
### 🏆 Points forts de la v2.1
|
### 🏆 Points forts de la v3.2.4
|
||||||
|
|
||||||
- **Architecture moderne** sans Provider, basée sur l'injection de dépendances
|
- **Architecture moderne** sans Provider, basée sur l'injection de dépendances
|
||||||
- **Réactivité native** avec ValueListenableBuilder et Hive
|
- **Réactivité native** avec ValueListenableBuilder et Hive
|
||||||
- **Interface adaptative** selon les rôles utilisateur et la taille d'écran
|
- **Interface adaptative** selon les rôles utilisateur et la taille d'écran
|
||||||
- **Performance optimisée** avec un ApiService singleton
|
- **Performance optimisée** avec un ApiService singleton et cache Hive
|
||||||
- **Gestion avancée des permissions** multi-niveaux
|
- **Gestion avancée des permissions** multi-niveaux
|
||||||
- **Gestion d'erreurs centralisée** avec ApiException
|
- **Gestion d'erreurs centralisée** avec ApiException
|
||||||
- **Interface utilisateur épurée** avec suppression des titres superflus
|
- **Interface utilisateur épurée** avec suppression des titres superflus
|
||||||
- **Chat responsive** avec layout adaptatif mobile/desktop
|
- **Chat responsive** avec layout adaptatif mobile/desktop
|
||||||
|
- **Système de filtrage centralisé** dans PassagesListWidget
|
||||||
|
- **Intégration Stripe Connect** pour les paiements des amicales
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -264,11 +266,11 @@ NotificationSettingsAdapter() // typeId: 25
|
|||||||
|
|
||||||
## 🎨 Interface utilisateur
|
## 🎨 Interface utilisateur
|
||||||
|
|
||||||
### 📱 Améliorations v2.1 - Interface épurée et responsive
|
### 📱 Améliorations v3.x - Interface épurée et responsive
|
||||||
|
|
||||||
#### **🎯 Simplification des titres de pages**
|
#### **🎯 Simplification des titres de pages**
|
||||||
|
|
||||||
La v2.1 a apporté une refonte majeure de l'interface pour maximiser l'espace utile et améliorer l'expérience utilisateur sur tous les écrans :
|
La v3.1.0 a apporté une refonte majeure de l'interface pour maximiser l'espace utile et améliorer l'expérience utilisateur sur tous les écrans :
|
||||||
|
|
||||||
**Pages avec titres supprimés :**
|
**Pages avec titres supprimés :**
|
||||||
- ✅ `user_history_page.dart` : Historique des passages
|
- ✅ `user_history_page.dart` : Historique des passages
|
||||||
@@ -356,7 +358,7 @@ UX claire : Feedback immédiat sur les erreurs de validation
|
|||||||
|
|
||||||
### 🎯 Système ApiException intelligent
|
### 🎯 Système ApiException intelligent
|
||||||
|
|
||||||
GEOSECTOR v2.0 utilise un **système centralisé de gestion des messages** qui s'adapte automatiquement au contexte d'affichage pour garantir une visibilité optimale des notifications utilisateur.
|
GEOSECTOR v3.x utilise un **système centralisé de gestion des messages** qui s'adapte automatiquement au contexte d'affichage pour garantir une visibilité optimale des notifications utilisateur.
|
||||||
|
|
||||||
#### **🧠 Détection automatique de contexte**
|
#### **🧠 Détection automatique de contexte**
|
||||||
|
|
||||||
@@ -622,7 +624,7 @@ Cette approche garantit que tous les messages d'erreur de l'API sont correctemen
|
|||||||
|
|
||||||
### 🎯 Vue d'ensemble
|
### 🎯 Vue d'ensemble
|
||||||
|
|
||||||
GEOSECTOR v2.0 implémente un **LoggerService centralisé** qui désactive automatiquement les logs de debug en production, optimisant ainsi les performances et la sécurité tout en facilitant le développement.
|
GEOSECTOR v3.x implémente un **LoggerService centralisé** qui désactive automatiquement les logs de debug en production, optimisant ainsi les performances et la sécurité tout en facilitant le développement.
|
||||||
|
|
||||||
### 🔍 Détection automatique de l'environnement
|
### 🔍 Détection automatique de l'environnement
|
||||||
|
|
||||||
@@ -822,7 +824,7 @@ Cette architecture garantit un système de logging professionnel, sécurisé et
|
|||||||
|
|
||||||
### 🎯 Vue d'ensemble
|
### 🎯 Vue d'ensemble
|
||||||
|
|
||||||
GEOSECTOR v2.0 implémente les **normes NIST SP 800-63B** pour la gestion des identifiants (usernames et passwords), offrant une sécurité renforcée tout en améliorant l'expérience utilisateur avec des règles plus flexibles et modernes.
|
GEOSECTOR v3.x implémente les **normes NIST SP 800-63B** pour la gestion des identifiants (usernames et passwords), offrant une sécurité renforcée tout en améliorant l'expérience utilisateur avec des règles plus flexibles et modernes.
|
||||||
|
|
||||||
### 📋 Conformité NIST SP 800-63B
|
### 📋 Conformité NIST SP 800-63B
|
||||||
|
|
||||||
@@ -1161,7 +1163,7 @@ Cette architecture garantit une gestion des membres robuste, sécurisée et intu
|
|||||||
|
|
||||||
### 🌍 Configuration des tuiles de carte
|
### 🌍 Configuration des tuiles de carte
|
||||||
|
|
||||||
GEOSECTOR v2.0 utilise une **stratégie différenciée** pour l'affichage des tuiles de carte selon la plateforme :
|
GEOSECTOR v3.x utilise une **stratégie différenciée** pour l'affichage des tuiles de carte selon la plateforme :
|
||||||
|
|
||||||
#### **Configuration actuelle**
|
#### **Configuration actuelle**
|
||||||
|
|
||||||
@@ -1263,7 +1265,7 @@ DataLoadingService : Orchestration du chargement des données
|
|||||||
|
|
||||||
### 📈 Gestion des Box Hive avec cache
|
### 📈 Gestion des Box Hive avec cache
|
||||||
|
|
||||||
GEOSECTOR v2.0 implémente une **stratégie de cache avancée** pour les Box Hive afin d'éliminer les goulots d'étranglement de performance lors d'opérations haute fréquence.
|
GEOSECTOR v3.x implémente une **stratégie de cache avancée** pour les Box Hive afin d'éliminer les goulots d'étranglement de performance lors d'opérations haute fréquence.
|
||||||
|
|
||||||
#### **🎯 Problème résolu**
|
#### **🎯 Problème résolu**
|
||||||
|
|
||||||
@@ -1354,7 +1356,7 @@ Cette architecture garantit une application performante, maintenable et évoluti
|
|||||||
|
|
||||||
### 🎯 Principe de conception
|
### 🎯 Principe de conception
|
||||||
|
|
||||||
GEOSECTOR v2.0 implémente une **architecture simplifiée des dialogs** qui élimine la complexité des callbacks asynchrones et garantit une gestion robuste des formulaires modaux.
|
GEOSECTOR v3.x implémente une **architecture simplifiée des dialogs** qui élimine la complexité des callbacks asynchrones et garantit une gestion robuste des formulaires modaux.
|
||||||
|
|
||||||
### 🏗️ Pattern "Dialog Auto-Gérée"
|
### 🏗️ Pattern "Dialog Auto-Gérée"
|
||||||
|
|
||||||
@@ -1381,9 +1383,56 @@ Le widget `PassagesListWidget` est le composant central pour l'affichage et la g
|
|||||||
- **Affichage adaptatif** : Liste complète ou tableau de bord avec fond transparent
|
- **Affichage adaptatif** : Liste complète ou tableau de bord avec fond transparent
|
||||||
- **Flux conditionnel de clic** : Comportement intelligent selon le type de passage
|
- **Flux conditionnel de clic** : Comportement intelligent selon le type de passage
|
||||||
- **Bouton de création intégré** : Bouton "+" vert dans l'en-tête pour ajouter des passages
|
- **Bouton de création intégré** : Bouton "+" vert dans l'en-tête pour ajouter des passages
|
||||||
- **Filtrage avancé** : Par type, utilisateur, période, avec exclusions possibles
|
- **Système de filtrage centralisé** : Tous les filtres intégrés et configurables
|
||||||
- **Actions contextuelles** : Modification, suppression, génération de reçus
|
- **Actions contextuelles** : Modification, suppression, génération de reçus
|
||||||
|
|
||||||
|
#### 🔧 Système de filtrage centralisé (v3.2.2)
|
||||||
|
|
||||||
|
Depuis la v3.2.2, PassagesListWidget intègre **tous les filtres** de manière configurable :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
PassagesListWidget(
|
||||||
|
// Données
|
||||||
|
passages: formattedPassages,
|
||||||
|
|
||||||
|
// Configuration des filtres
|
||||||
|
showFilters: true, // Active le système de filtrage
|
||||||
|
showSearch: true, // Barre de recherche
|
||||||
|
showTypeFilter: true, // Filtre par type de passage
|
||||||
|
showPaymentFilter: true, // Filtre par mode de paiement
|
||||||
|
showSectorFilter: true, // Filtre par secteur géographique
|
||||||
|
showUserFilter: true, // Filtre par membre (admin uniquement)
|
||||||
|
showPeriodFilter: true, // Filtre par période temporelle
|
||||||
|
|
||||||
|
// Données pour les filtres
|
||||||
|
sectors: _sectors, // Liste des secteurs disponibles
|
||||||
|
members: users, // Liste des membres (pour admin)
|
||||||
|
|
||||||
|
// Valeurs initiales
|
||||||
|
initialSectorId: selectedSectorId,
|
||||||
|
initialUserId: selectedUserId,
|
||||||
|
initialPeriod: 'Toutes',
|
||||||
|
dateRange: selectedDateRange,
|
||||||
|
|
||||||
|
// Callback de synchronisation
|
||||||
|
onFiltersChanged: (filters) {
|
||||||
|
setState(() {
|
||||||
|
// Synchronisation avec l'état parent
|
||||||
|
selectedSectorId = filters['sectorId'];
|
||||||
|
selectedPeriod = filters['period'];
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avantages de la centralisation :**
|
||||||
|
- ✅ **Code unique** : Plus de duplication entre les pages
|
||||||
|
- ✅ **Configuration flexible** : Chaque page active uniquement les filtres pertinents
|
||||||
|
- ✅ **Interface cohérente** : Même expérience utilisateur partout
|
||||||
|
- ✅ **Maintenance simplifiée** : Modifications centralisées
|
||||||
|
- ✅ **Responsive automatique** : Adaptation desktop/mobile gérée par le widget
|
||||||
|
|
||||||
#### 🔄 Flux conditionnel des clics sur passages
|
#### 🔄 Flux conditionnel des clics sur passages
|
||||||
|
|
||||||
Le widget implémente un comportement intelligent lors du clic sur un passage :
|
Le widget implémente un comportement intelligent lors du clic sur un passage :
|
||||||
@@ -1606,7 +1655,7 @@ Future<bool> saveOperationFromModel(OperationModel operation) async {
|
|||||||
- **🎨 UX** : Fermeture automatique et messages appropriés
|
- **🎨 UX** : Fermeture automatique et messages appropriés
|
||||||
- **🔧 Maintenance** : Architecture cohérente et prévisible
|
- **🔧 Maintenance** : Architecture cohérente et prévisible
|
||||||
|
|
||||||
Cette approche **"Dialog Auto-Gérée"** constitue un pattern architectural clé de GEOSECTOR v2.0, garantissant une expérience utilisateur fluide et un code maintenable. 🎉
|
Cette approche **"Dialog Auto-Gérée"** constitue un pattern architectural clé de GEOSECTOR v3.x, garantissant une expérience utilisateur fluide et un code maintenable. 🎉
|
||||||
|
|
||||||
## Fonction de création d'une opération
|
## Fonction de création d'une opération
|
||||||
|
|
||||||
@@ -1729,7 +1778,89 @@ Cette architecture garantit une synchronisation robuste et performante lors de l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
### v2.1 (Janvier 2025)
|
### v3.2.4 (04 Septembre 2025)
|
||||||
|
|
||||||
|
#### **Optimisations et corrections**
|
||||||
|
- 🐛 **Correction du PassagesListWidget dans user_dashboard_home_page**
|
||||||
|
- Suppression de la hauteur fixe pour éviter le rectangle gris
|
||||||
|
- Affichage des 20 derniers passages sans scrolling interne
|
||||||
|
- Utilisation du scrolling de la page principale uniquement
|
||||||
|
- 🧹 **Nettoyage du code**
|
||||||
|
- Suppression des méthodes non utilisées (_formatDate, _buildSimplePassageCard, _showPassageDetails)
|
||||||
|
- Amélioration de la structure des blocs if/else selon les bonnes pratiques
|
||||||
|
- Suppression des imports inutiles (intl)
|
||||||
|
- ✅ **Qualité du code**
|
||||||
|
- Flutter analyze : 0 erreur, 0 warning sur tous les fichiers modifiés
|
||||||
|
- Réduction globale de 65% des issues (de 493 à 171)
|
||||||
|
|
||||||
|
### v3.2.3 (03 Septembre 2025)
|
||||||
|
|
||||||
|
#### **Mise à jour des interfaces mobiles**
|
||||||
|
- 📱 **Améliorations de l'interface utilisateur**
|
||||||
|
- Optimisation des layouts pour mobiles et tablettes
|
||||||
|
- Amélioration de la réactivité des composants
|
||||||
|
- 🔧 **Corrections de bugs mineurs**
|
||||||
|
- Résolution des problèmes d'affichage sur certains appareils
|
||||||
|
- Amélioration des performances de rendu
|
||||||
|
|
||||||
|
### v3.2.2 (02 Septembre 2025)
|
||||||
|
|
||||||
|
#### **Centralisation des filtres dans PassagesListWidget**
|
||||||
|
- 🎯 **Refactoring majeur du système de filtrage**
|
||||||
|
- Tous les filtres déplacés dans PassagesListWidget (recherche, type, paiement, secteur, membre, période)
|
||||||
|
- Configuration flexible via paramètres booléens (`showTypeFilter`, `showPaymentFilter`, `showSectorFilter`, etc.)
|
||||||
|
- Suppression du code de filtrage dupliqué dans les pages parentes
|
||||||
|
- 🔧 **Nouveaux filtres avancés**
|
||||||
|
- Filtre par secteur avec liste déroulante
|
||||||
|
- Filtre par membre pour les pages admin
|
||||||
|
- Filtre par période avec options prédéfinies (24h, 48h, 7 jours, 15 jours, mois)
|
||||||
|
- Support des plages de dates personnalisées avec DateRangePicker
|
||||||
|
- 📱 **Interface responsive des filtres**
|
||||||
|
- Desktop : Filtres compacts sur 2 lignes maximum
|
||||||
|
- Mobile : Filtres empilés verticalement pour une meilleure ergonomie
|
||||||
|
- Recherche toujours en premier pour une accessibilité optimale
|
||||||
|
- 🔄 **Synchronisation des filtres**
|
||||||
|
- Callback `onFiltersChanged` pour synchroniser l'état avec les pages parentes
|
||||||
|
- Notification automatique des changements de filtres
|
||||||
|
- Persistance des sélections entre les navigations
|
||||||
|
- 📊 **Pages simplifiées**
|
||||||
|
- `admin_history_page.dart` : Suppression de 400+ lignes de code de filtrage dupliqué
|
||||||
|
- `user_history_page.dart` : Simplification avec activation sélective des filtres pertinents
|
||||||
|
- Maintenance facilitée avec une logique unique centralisée
|
||||||
|
- 🔧 **Correction des layouts**
|
||||||
|
- `admin_history_page.dart` : Utilisation d'Expanded au lieu de hauteur fixe (85%)
|
||||||
|
- Liste des passages s'étend maintenant jusqu'en bas de l'écran sur mobile
|
||||||
|
- 📝 **Amélioration des labels de filtres**
|
||||||
|
- "Tous les types" au lieu de "Tous"
|
||||||
|
- "Tous les règlements" au lieu de "Tous"
|
||||||
|
- "Toutes les périodes" au lieu de "Tous"
|
||||||
|
- Meilleure clarté pour l'utilisateur
|
||||||
|
|
||||||
|
### v3.2.1 (31 Août 2025)
|
||||||
|
|
||||||
|
#### **Build et déploiement**
|
||||||
|
- 🚀 **Publication sur Google Play Store**
|
||||||
|
- Build AAB (Android App Bundle) pour distribution optimisée
|
||||||
|
- Configuration des signatures et keystores
|
||||||
|
- Optimisation de la taille de l'application
|
||||||
|
- 🔧 **Corrections de bugs critiques**
|
||||||
|
- Fix des problèmes de compilation
|
||||||
|
- Résolution des dépendances obsolètes
|
||||||
|
- Amélioration de la stabilité générale
|
||||||
|
|
||||||
|
### v3.2.0 (30 Août 2025)
|
||||||
|
|
||||||
|
#### **Intégration Stripe Connect**
|
||||||
|
- 💳 **Système de paiement pour les amicales**
|
||||||
|
- Configuration Stripe Connect pour les comptes des amicales
|
||||||
|
- Interface de gestion des paiements
|
||||||
|
- Suivi des transactions et règlements
|
||||||
|
- 🏗️ **Architecture de paiement**
|
||||||
|
- Intégration API Stripe
|
||||||
|
- Gestion sécurisée des tokens
|
||||||
|
- Workflow de paiement complet
|
||||||
|
|
||||||
|
### v3.1.0 (Juillet 2025)
|
||||||
|
|
||||||
#### **Interface utilisateur**
|
#### **Interface utilisateur**
|
||||||
- 🎨 **Suppression des titres de pages** pour maximiser l'espace utile
|
- 🎨 **Suppression des titres de pages** pour maximiser l'espace utile
|
||||||
@@ -1747,14 +1878,35 @@ Cette architecture garantit une synchronisation robuste et performante lors de l
|
|||||||
- Tailles adaptées aux petits écrans
|
- Tailles adaptées aux petits écrans
|
||||||
- Suppression des éléments superflus (icône refresh)
|
- Suppression des éléments superflus (icône refresh)
|
||||||
|
|
||||||
#### **Corrections de bugs**
|
### v3.0.0 (Juin 2025)
|
||||||
- ✅ Fix backdrop persistant après fermeture de PassageFormDialog
|
|
||||||
- ✅ Fix contexte Navigator pour dialogs (rootNavigator: false)
|
|
||||||
- ✅ Fix responsive des titres sur petits écrans
|
|
||||||
|
|
||||||
### v2.0 (Décembre 2024)
|
#### **Refonte architecturale majeure**
|
||||||
- 🏗️ Architecture moderne sans Provider
|
- 🏗️ **Architecture moderne sans Provider**
|
||||||
- 💾 Optimisation cache Hive
|
- Migration vers l'injection de dépendances
|
||||||
- 🔐 Normes NIST pour les identifiants
|
- Repositories singleton avec instances globales
|
||||||
- 📊 Système de logging intelligent
|
- Suppression complète de Provider/Bloc
|
||||||
- 🎯 Pattern Dialog Auto-Gérée
|
- 💾 **Optimisation cache Hive**
|
||||||
|
- Stratégie de cache pour éliminer les vérifications répétées
|
||||||
|
- Performance améliorée de 99% sur les opérations de liste
|
||||||
|
- Gestion intelligente du cache avec reset après modifications
|
||||||
|
- 🔐 **Normes NIST SP 800-63B pour les identifiants**
|
||||||
|
- Support des phrases de passe
|
||||||
|
- Acceptation de tous les caractères Unicode
|
||||||
|
- Vérification contre les bases de mots de passe compromis
|
||||||
|
- 📊 **Système de logging intelligent**
|
||||||
|
- LoggerService centralisé avec détection d'environnement
|
||||||
|
- Logs désactivés automatiquement en production
|
||||||
|
- Catégorisation avec emojis automatiques
|
||||||
|
- 🎯 **Pattern Dialog Auto-Gérée**
|
||||||
|
- Dialogs responsables de leur propre cycle de vie
|
||||||
|
- Auto-fermeture sur succès
|
||||||
|
- Gestion d'erreurs centralisée dans la dialog
|
||||||
|
|
||||||
|
### v2.x (2024 - Début 2025)
|
||||||
|
|
||||||
|
#### **Versions de développement initial**
|
||||||
|
- Base de l'architecture Flutter
|
||||||
|
- Mise en place des modèles Hive
|
||||||
|
- Intégration des cartes Mapbox/OpenStreetMap
|
||||||
|
- Système de chat MQTT
|
||||||
|
- Gestion des rôles et permissions
|
||||||
|
|||||||
@@ -101,31 +101,37 @@ class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
// Builder pour appliquer le theme responsive à toute l'app
|
// Builder pour appliquer le theme responsive à toute l'app
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return MediaQuery(
|
return LayoutBuilder(
|
||||||
// Conserver les données MediaQuery existantes
|
builder: (context, constraints) {
|
||||||
data: MediaQuery.of(context),
|
// Récupérer le theme actuel (clair ou sombre)
|
||||||
child: Builder(
|
final brightness = Theme.of(context).brightness;
|
||||||
builder: (context) {
|
final textColor = brightness == Brightness.light
|
||||||
// Récupérer le theme actuel (clair ou sombre)
|
? AppTheme.textLightColor
|
||||||
final brightness = Theme.of(context).brightness;
|
: AppTheme.textDarkColor;
|
||||||
final textColor = brightness == Brightness.light
|
|
||||||
? AppTheme.textLightColor
|
// Débogage en mode développement
|
||||||
: AppTheme.textDarkColor;
|
final width = constraints.maxWidth;
|
||||||
|
final scaleFactor = AppTheme.getFontScaleFactor(width);
|
||||||
// Débogage en mode développement
|
|
||||||
final width = MediaQuery.of(context).size.width;
|
// Afficher le debug uniquement lors du changement de taille
|
||||||
final scaleFactor = AppTheme.getFontScaleFactor(width);
|
if (width < AppTheme.breakpointMobileSmall) {
|
||||||
debugPrint('📱 Largeur écran: ${width.toStringAsFixed(0)}px → Facteur: ×$scaleFactor');
|
debugPrint('📱 Mode: Très petit mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
|
||||||
|
} else if (width < AppTheme.breakpointMobile) {
|
||||||
// Appliquer le TextTheme responsive
|
debugPrint('📱 Mode: Mobile (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
|
||||||
return Theme(
|
} else if (width < AppTheme.breakpointTablet) {
|
||||||
data: Theme.of(context).copyWith(
|
debugPrint('📱 Mode: Tablette (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
|
||||||
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
|
} else {
|
||||||
),
|
debugPrint('🖥️ Mode: Desktop (${width.toStringAsFixed(0)}px) → Facteur: ×$scaleFactor');
|
||||||
child: child ?? const SizedBox.shrink(),
|
}
|
||||||
);
|
|
||||||
},
|
// Appliquer le TextTheme responsive
|
||||||
),
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
|
||||||
|
),
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// Configuration des localisations pour le français
|
// Configuration des localisations pour le français
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ class AppKeys {
|
|||||||
static const int roleAdmin3 = 9;
|
static const int roleAdmin3 = 9;
|
||||||
|
|
||||||
// URLs API pour les différents environnements
|
// URLs API pour les différents environnements
|
||||||
static const String baseApiUrlDev = 'https://dapp.geosector.fr/api';
|
static const String baseApiUrlDev = 'https://app.geo.dev/api';
|
||||||
static const String baseApiUrlRec = 'https://rapp.geosector.fr/api';
|
static const String baseApiUrlRec = 'https://rapp.geosector.fr/api';
|
||||||
static const String baseApiUrlProd = 'https://app.geosector.fr/api';
|
static const String baseApiUrlProd = 'https://app.geosector.fr/api';
|
||||||
|
|
||||||
// Identifiants d'application pour les différents environnements
|
// Identifiants d'application pour les différents environnements
|
||||||
static const String appIdentifierDev = 'dapp.geosector.fr';
|
static const String appIdentifierDev = 'app.geo.dev';
|
||||||
static const String appIdentifierRec = 'rapp.geosector.fr';
|
static const String appIdentifierRec = 'rapp.geosector.fr';
|
||||||
static const String appIdentifierProd = 'app.geosector.fr';
|
static const String appIdentifierProd = 'app.geosector.fr';
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class AppKeys {
|
|||||||
try {
|
try {
|
||||||
final String currentUrl = Uri.base.toString().toLowerCase();
|
final String currentUrl = Uri.base.toString().toLowerCase();
|
||||||
|
|
||||||
if (currentUrl.contains('dapp.geosector.fr')) {
|
if (currentUrl.contains('app.geo.dev')) {
|
||||||
return mapboxApiKeyDev;
|
return mapboxApiKeyDev;
|
||||||
} else if (currentUrl.contains('rapp.geosector.fr')) {
|
} else if (currentUrl.contains('rapp.geosector.fr')) {
|
||||||
return mapboxApiKeyRec;
|
return mapboxApiKeyRec;
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class ApiService {
|
|||||||
|
|
||||||
final currentUrl = html.window.location.href.toLowerCase();
|
final currentUrl = html.window.location.href.toLowerCase();
|
||||||
|
|
||||||
if (currentUrl.contains('dapp.geosector.fr')) {
|
if (currentUrl.contains('app.geo.dev')) {
|
||||||
return 'DEV';
|
return 'DEV';
|
||||||
} else if (currentUrl.contains('rapp.geosector.fr')) {
|
} else if (currentUrl.contains('rapp.geosector.fr')) {
|
||||||
return 'REC';
|
return 'REC';
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class StripeConnectService {
|
|||||||
debugPrint('📋 Génération du lien d\'onboarding pour account: $accountId');
|
debugPrint('📋 Génération du lien d\'onboarding pour account: $accountId');
|
||||||
|
|
||||||
// URLs de retour après onboarding
|
// URLs de retour après onboarding
|
||||||
const baseUrl = 'https://dapp.geosector.fr'; // À adapter selon l'environnement
|
const baseUrl = 'https://app.geo.dev'; // À adapter selon l'environnement
|
||||||
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
|
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
|
||||||
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');
|
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');
|
||||||
|
|
||||||
|
|||||||
@@ -403,4 +403,16 @@ class AppTheme {
|
|||||||
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 11),
|
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 11),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper pour obtenir des espacements responsives
|
||||||
|
static double getResponsiveSpacing(double screenWidth, double baseSpacing) {
|
||||||
|
final scaleFactor = getFontScaleFactor(screenWidth);
|
||||||
|
return baseSpacing * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper court pour espacements responsives
|
||||||
|
static double s(BuildContext context, double baseSpacing) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
return getResponsiveSpacing(width, baseSpacing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart';
|
import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
|
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
|
||||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||||
@@ -197,7 +199,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
|||||||
final currentOperation = userRepository.getCurrentOperation();
|
final currentOperation = userRepository.getCurrentOperation();
|
||||||
|
|
||||||
// Titre dynamique avec l'ID et le nom de l'opération
|
// Titre dynamique avec l'ID et le nom de l'opération
|
||||||
final String title = currentOperation != null ? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}' : 'Synthèse de l\'opération';
|
final String title = currentOperation != null ? 'Opération #${currentOperation.id} ${currentOperation.name}' : 'Opération';
|
||||||
|
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
// Fond dégradé avec petits points blancs
|
// Fond dégradé avec petits points blancs
|
||||||
@@ -264,10 +266,16 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
|||||||
const SizedBox(height: AppTheme.spacingL),
|
const SizedBox(height: AppTheme.spacingL),
|
||||||
|
|
||||||
// LIGNE 2 : Carte de répartition par secteur (pleine largeur)
|
// LIGNE 2 : Carte de répartition par secteur (pleine largeur)
|
||||||
SectorDistributionCard(
|
ValueListenableBuilder<Box<SectorModel>>(
|
||||||
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
valueListenable: Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
|
||||||
title: 'Répartition sur les 31 secteurs',
|
builder: (context, Box<SectorModel> box, child) {
|
||||||
height: 500, // Hauteur maximale pour afficher tous les secteurs
|
final sectorCount = box.values.length;
|
||||||
|
return SectorDistributionCard(
|
||||||
|
key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'),
|
||||||
|
title: '$sectorCount secteurs',
|
||||||
|
height: 500, // Hauteur maximale pour afficher tous les secteurs
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppTheme.spacingL),
|
const SizedBox(height: AppTheme.spacingL),
|
||||||
@@ -345,7 +353,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
|||||||
// Construit la carte de répartition par type de passage avec liste
|
// Construit la carte de répartition par type de passage avec liste
|
||||||
Widget _buildPassageTypeCard(BuildContext context) {
|
Widget _buildPassageTypeCard(BuildContext context) {
|
||||||
return PassageSummaryCard(
|
return PassageSummaryCard(
|
||||||
title: 'Répartition par type de passage',
|
title: 'Passages',
|
||||||
titleColor: AppTheme.primaryColor,
|
titleColor: AppTheme.primaryColor,
|
||||||
titleIcon: Icons.route,
|
titleIcon: Icons.route,
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -365,7 +373,7 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
|
|||||||
// Construit la carte de répartition par mode de paiement
|
// Construit la carte de répartition par mode de paiement
|
||||||
Widget _buildPaymentTypeCard(BuildContext context) {
|
Widget _buildPaymentTypeCard(BuildContext context) {
|
||||||
return PaymentSummaryCard(
|
return PaymentSummaryCard(
|
||||||
title: 'Répartition par mode de paiement',
|
title: 'Règlements',
|
||||||
titleColor: AppTheme.buttonSuccessColor,
|
titleColor: AppTheme.buttonSuccessColor,
|
||||||
titleIcon: Icons.euro,
|
titleIcon: Icons.euro,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||||
import 'package:geosector_app/core/data/models/sector_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/core/data/models/membre_model.dart';
|
||||||
|
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||||
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
||||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||||
@@ -54,24 +54,13 @@ class AdminHistoryPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||||
// État des filtres
|
|
||||||
String searchQuery = '';
|
|
||||||
String selectedSector = 'Tous';
|
|
||||||
String selectedUser = 'Tous';
|
|
||||||
String selectedType = 'Tous';
|
|
||||||
String selectedPaymentMethod = 'Tous';
|
|
||||||
String selectedPeriod = 'Tous'; // Période par défaut
|
|
||||||
DateTimeRange? selectedDateRange;
|
|
||||||
|
|
||||||
// État du tri actuel
|
// État du tri actuel
|
||||||
PassageSortType _currentSort = PassageSortType.dateDesc;
|
PassageSortType _currentSort = PassageSortType.dateDesc;
|
||||||
|
|
||||||
// Contrôleur pour la recherche
|
// Filtres présélectionnés depuis une autre page
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
|
|
||||||
// IDs pour les filtres
|
|
||||||
int? selectedSectorId;
|
int? selectedSectorId;
|
||||||
int? selectedUserId;
|
String selectedSector = 'Tous';
|
||||||
|
String selectedType = 'Tous';
|
||||||
|
|
||||||
// Listes pour les filtres
|
// Listes pour les filtres
|
||||||
List<SectorModel> _sectors = [];
|
List<SectorModel> _sectors = [];
|
||||||
@@ -170,15 +159,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
|
|
||||||
// Initialiser les filtres
|
// Initialiser les filtres
|
||||||
void _initializeFilters() {
|
void _initializeFilters() {
|
||||||
// Par défaut, on n'applique pas de filtre par utilisateur ou secteur
|
// Par défaut, on n'applique pas de filtre présélectionné
|
||||||
selectedSectorId = null;
|
selectedSectorId = null;
|
||||||
selectedUserId = null;
|
selectedSector = 'Tous';
|
||||||
|
selectedType = 'Tous';
|
||||||
// Période par défaut : toutes les périodes
|
|
||||||
selectedPeriod = 'Tous';
|
|
||||||
|
|
||||||
// Plage de dates par défaut : aucune restriction
|
|
||||||
selectedDateRange = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les filtres présélectionnés depuis Hive
|
// Charger les filtres présélectionnés depuis Hive
|
||||||
@@ -219,258 +203,9 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_searchController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nouvelle méthode pour filtrer une liste de passages déjà formatés
|
|
||||||
List<Map<String, dynamic>> _getFilteredPassagesFromList(
|
|
||||||
List<Map<String, dynamic>> passages) {
|
|
||||||
try {
|
|
||||||
var filtered = passages.where((passage) {
|
|
||||||
try {
|
|
||||||
// Ne plus exclure automatiquement les passages de type 2
|
|
||||||
// car on propose maintenant un filtre par type dans les "Filtres avancés"
|
|
||||||
|
|
||||||
// Filtrer par utilisateur
|
|
||||||
if (selectedUserId != null &&
|
|
||||||
passage.containsKey('fkUser') &&
|
|
||||||
passage['fkUser'] != selectedUserId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par secteur
|
|
||||||
if (selectedSectorId != null &&
|
|
||||||
passage.containsKey('fkSector') &&
|
|
||||||
passage['fkSector'] != selectedSectorId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par type de passage
|
|
||||||
if (selectedType != 'Tous') {
|
|
||||||
try {
|
|
||||||
final int? selectedTypeId = int.tryParse(selectedType);
|
|
||||||
if (selectedTypeId != null) {
|
|
||||||
if (!passage.containsKey('type') ||
|
|
||||||
passage['type'] != selectedTypeId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur de filtrage par type: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par mode de règlement
|
|
||||||
if (selectedPaymentMethod != 'Tous') {
|
|
||||||
try {
|
|
||||||
final int? selectedPaymentId =
|
|
||||||
int.tryParse(selectedPaymentMethod);
|
|
||||||
if (selectedPaymentId != null) {
|
|
||||||
if (!passage.containsKey('payment') ||
|
|
||||||
passage['payment'] != selectedPaymentId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur de filtrage par mode de règlement: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par recherche
|
|
||||||
if (searchQuery.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
final query = searchQuery.toLowerCase();
|
|
||||||
final address = passage.containsKey('address')
|
|
||||||
? passage['address']?.toString().toLowerCase() ?? ''
|
|
||||||
: '';
|
|
||||||
final name = passage.containsKey('name')
|
|
||||||
? passage['name']?.toString().toLowerCase() ?? ''
|
|
||||||
: '';
|
|
||||||
final notes = passage.containsKey('notes')
|
|
||||||
? passage['notes']?.toString().toLowerCase() ?? ''
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (!address.contains(query) &&
|
|
||||||
!name.contains(query) &&
|
|
||||||
!notes.contains(query)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur de filtrage par recherche: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par période/date
|
|
||||||
if (selectedDateRange != null) {
|
|
||||||
try {
|
|
||||||
if (passage.containsKey('date') && passage['date'] is DateTime) {
|
|
||||||
final DateTime passageDate = passage['date'] as DateTime;
|
|
||||||
if (passageDate.isBefore(selectedDateRange!.start) ||
|
|
||||||
passageDate.isAfter(selectedDateRange!.end)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur de filtrage par date: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur lors du filtrage d\'un passage: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
// Appliquer le tri sélectionné
|
|
||||||
filtered = _sortPassages(filtered);
|
|
||||||
|
|
||||||
debugPrint('Passages filtrés: ${filtered.length}/${passages.length}');
|
|
||||||
return filtered;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur globale lors du filtrage: $e');
|
|
||||||
return passages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode pour trier les passages selon le type de tri sélectionné
|
|
||||||
List<Map<String, dynamic>> _sortPassages(
|
|
||||||
List<Map<String, dynamic>> passages) {
|
|
||||||
final sortedPassages = List<Map<String, dynamic>>.from(passages);
|
|
||||||
|
|
||||||
switch (_currentSort) {
|
|
||||||
case PassageSortType.dateDesc:
|
|
||||||
sortedPassages.sort((a, b) {
|
|
||||||
try {
|
|
||||||
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case PassageSortType.dateAsc:
|
|
||||||
sortedPassages.sort((a, b) {
|
|
||||||
try {
|
|
||||||
return (a['date'] as DateTime).compareTo(b['date'] as DateTime);
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case PassageSortType.addressAsc:
|
|
||||||
sortedPassages.sort((a, b) {
|
|
||||||
try {
|
|
||||||
// Tri intelligent par rue, numéro (numérique), rueBis
|
|
||||||
final String rueA = a['rue'] ?? '';
|
|
||||||
final String rueB = b['rue'] ?? '';
|
|
||||||
final String numeroA = a['numero'] ?? '';
|
|
||||||
final String numeroB = b['numero'] ?? '';
|
|
||||||
final String rueBisA = a['rueBis'] ?? '';
|
|
||||||
final String rueBisB = b['rueBis'] ?? '';
|
|
||||||
|
|
||||||
// D'abord comparer les rues
|
|
||||||
int rueCompare = rueA.toLowerCase().compareTo(rueB.toLowerCase());
|
|
||||||
if (rueCompare != 0) return rueCompare;
|
|
||||||
|
|
||||||
// Si les rues sont identiques, comparer les numéros (numériquement)
|
|
||||||
int numA = int.tryParse(numeroA) ?? 0;
|
|
||||||
int numB = int.tryParse(numeroB) ?? 0;
|
|
||||||
int numCompare = numA.compareTo(numB);
|
|
||||||
if (numCompare != 0) return numCompare;
|
|
||||||
|
|
||||||
// Si les numéros sont identiques, comparer les rueBis
|
|
||||||
return rueBisA.toLowerCase().compareTo(rueBisB.toLowerCase());
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case PassageSortType.addressDesc:
|
|
||||||
sortedPassages.sort((a, b) {
|
|
||||||
try {
|
|
||||||
// Tri intelligent inversé par rue, numéro (numérique), rueBis
|
|
||||||
final String rueA = a['rue'] ?? '';
|
|
||||||
final String rueB = b['rue'] ?? '';
|
|
||||||
final String numeroA = a['numero'] ?? '';
|
|
||||||
final String numeroB = b['numero'] ?? '';
|
|
||||||
final String rueBisA = a['rueBis'] ?? '';
|
|
||||||
final String rueBisB = b['rueBis'] ?? '';
|
|
||||||
|
|
||||||
// D'abord comparer les rues (inversé)
|
|
||||||
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
|
|
||||||
if (rueCompare != 0) return rueCompare;
|
|
||||||
|
|
||||||
// Si les rues sont identiques, comparer les numéros (inversé numériquement)
|
|
||||||
int numA = int.tryParse(numeroA) ?? 0;
|
|
||||||
int numB = int.tryParse(numeroB) ?? 0;
|
|
||||||
int numCompare = numB.compareTo(numA);
|
|
||||||
if (numCompare != 0) return numCompare;
|
|
||||||
|
|
||||||
// Si les numéros sont identiques, comparer les rueBis (inversé)
|
|
||||||
return rueBisB.toLowerCase().compareTo(rueBisA.toLowerCase());
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortedPassages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le filtre par secteur
|
|
||||||
void _updateSectorFilter(String sectorName, int? sectorId) {
|
|
||||||
setState(() {
|
|
||||||
selectedSector = sectorName;
|
|
||||||
selectedSectorId = sectorId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le filtre par utilisateur
|
|
||||||
void _updateUserFilter(String userName, int? userId) {
|
|
||||||
setState(() {
|
|
||||||
selectedUser = userName;
|
|
||||||
selectedUserId = userId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le filtre par période
|
|
||||||
void _updatePeriodFilter(String period) {
|
|
||||||
setState(() {
|
|
||||||
selectedPeriod = period;
|
|
||||||
|
|
||||||
// Mettre à jour la plage de dates en fonction de la période
|
|
||||||
final DateTime now = DateTime.now();
|
|
||||||
|
|
||||||
switch (period) {
|
|
||||||
case 'Derniers 15 jours':
|
|
||||||
selectedDateRange = DateTimeRange(
|
|
||||||
start: now.subtract(const Duration(days: 15)),
|
|
||||||
end: now,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'Dernière semaine':
|
|
||||||
selectedDateRange = DateTimeRange(
|
|
||||||
start: now.subtract(const Duration(days: 7)),
|
|
||||||
end: now,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'Dernier mois':
|
|
||||||
selectedDateRange = DateTimeRange(
|
|
||||||
start: DateTime(now.year, now.month - 1, now.day),
|
|
||||||
end: now,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'Tous':
|
|
||||||
selectedDateRange = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -525,25 +260,22 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
// Contenu de la page
|
// Contenu de la page
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return SingleChildScrollView(
|
// Padding responsive : réduit sur mobile pour maximiser l'espace
|
||||||
padding: const EdgeInsets.all(16.0),
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
child: ConstrainedBox(
|
final horizontalPadding = screenWidth < 600 ? 8.0 : 16.0;
|
||||||
constraints: BoxConstraints(
|
final verticalPadding = 16.0;
|
||||||
minHeight: constraints.maxHeight - 32, // Moins le padding
|
|
||||||
),
|
return Padding(
|
||||||
child: Column(
|
padding: EdgeInsets.symmetric(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
horizontal: horizontalPadding,
|
||||||
children: [
|
vertical: verticalPadding,
|
||||||
// Filtres supplémentaires (secteur, utilisateur, période)
|
),
|
||||||
_buildAdditionalFilters(context),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
|
// Widget de liste des passages avec ValueListenableBuilder
|
||||||
// Widget de liste des passages avec hauteur fixe et ValueListenableBuilder
|
Expanded(
|
||||||
SizedBox(
|
child: ValueListenableBuilder(
|
||||||
height: constraints.maxHeight *
|
|
||||||
0.7, // 70% de la hauteur disponible
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable:
|
valueListenable:
|
||||||
Hive.box<PassageModel>(AppKeys.passagesBoxName)
|
Hive.box<PassageModel>(AppKeys.passagesBoxName)
|
||||||
.listenable(),
|
.listenable(),
|
||||||
@@ -558,14 +290,28 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
allPassages,
|
allPassages,
|
||||||
_sectorRepository,
|
_sectorRepository,
|
||||||
_membreRepository);
|
_membreRepository);
|
||||||
|
|
||||||
// Appliquer les filtres
|
// Récupérer les UserModel depuis les MembreModel
|
||||||
final filteredPassages =
|
final users = _membres.map((membre) {
|
||||||
_getFilteredPassagesFromList(formattedPassages);
|
return userRepository.getUserById(membre.id);
|
||||||
|
}).where((user) => user != null).toList();
|
||||||
|
|
||||||
return PassagesListWidget(
|
return PassagesListWidget(
|
||||||
showAddButton:
|
// Données
|
||||||
true, // Activer le bouton de création
|
passages: formattedPassages,
|
||||||
|
// Activation des filtres
|
||||||
|
showFilters: true,
|
||||||
|
showSearch: true,
|
||||||
|
showTypeFilter: true,
|
||||||
|
showPaymentFilter: true,
|
||||||
|
showSectorFilter: true,
|
||||||
|
showUserFilter: true,
|
||||||
|
showPeriodFilter: true,
|
||||||
|
// Données pour les filtres
|
||||||
|
sectors: _sectors,
|
||||||
|
members: users.cast<UserModel>(),
|
||||||
|
// Bouton d'ajout
|
||||||
|
showAddButton: true,
|
||||||
onAddPassage: () async {
|
onAddPassage: () async {
|
||||||
// Ouvrir le dialogue de création de passage
|
// Ouvrir le dialogue de création de passage
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@@ -674,9 +420,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
passages: filteredPassages,
|
// Actions
|
||||||
showFilters: false,
|
|
||||||
showSearch: false,
|
|
||||||
showActions: true,
|
showActions: true,
|
||||||
// Le widget gère maintenant le flux conditionnel par défaut
|
// Le widget gère maintenant le flux conditionnel par défaut
|
||||||
onPassageSelected: null,
|
onPassageSelected: null,
|
||||||
@@ -695,9 +439,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -993,437 +736,6 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construction des filtres supplémentaires
|
|
||||||
Widget _buildAdditionalFilters(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
final isDesktop = size.width > 900;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
color: Colors.white, // Fond opaque
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Champ de recherche
|
|
||||||
_buildSearchField(theme),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Disposition des filtres en fonction de la taille de l'écran
|
|
||||||
isDesktop
|
|
||||||
? Column(
|
|
||||||
children: [
|
|
||||||
// Première ligne : Secteur, Utilisateur, Période
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// Filtre par secteur
|
|
||||||
Expanded(
|
|
||||||
child: _buildSectorFilter(theme, _sectors),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
|
|
||||||
// Filtre par membre
|
|
||||||
Expanded(
|
|
||||||
child: _buildMembreFilter(theme, _membres),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
|
|
||||||
// Filtre par période
|
|
||||||
Expanded(
|
|
||||||
child: _buildPeriodFilter(theme),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Deuxième ligne : Type de passage, Mode de règlement
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// Filtre par type de passage
|
|
||||||
Expanded(
|
|
||||||
child: _buildTypeFilter(theme),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
|
|
||||||
// Filtre par mode de règlement
|
|
||||||
Expanded(
|
|
||||||
child: _buildPaymentFilter(theme),
|
|
||||||
),
|
|
||||||
// Espacement pour équilibrer avec la ligne du dessus (3 colonnes)
|
|
||||||
const Expanded(child: SizedBox()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
// Filtre par secteur
|
|
||||||
_buildSectorFilter(theme, _sectors),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Filtre par membre
|
|
||||||
_buildMembreFilter(theme, _membres),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Filtre par période
|
|
||||||
_buildPeriodFilter(theme),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Filtre par type de passage
|
|
||||||
_buildTypeFilter(theme),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Filtre par mode de règlement
|
|
||||||
_buildPaymentFilter(theme),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par secteur
|
|
||||||
Widget _buildSectorFilter(ThemeData theme, List<SectorModel> sectors) {
|
|
||||||
// Vérifier si la liste des secteurs est vide ou si selectedSector n'est pas dans la liste
|
|
||||||
bool isSelectedSectorValid = selectedSector == 'Tous' ||
|
|
||||||
sectors.any((s) => s.libelle == selectedSector);
|
|
||||||
|
|
||||||
// Si selectedSector n'est pas valide, le réinitialiser à 'Tous'
|
|
||||||
if (!isSelectedSectorValid) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
selectedSector = 'Tous';
|
|
||||||
selectedSectorId = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: isSelectedSectorValid ? selectedSector : 'Tous',
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
hint: const Text('Sélectionner un secteur'),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Tous les secteurs'),
|
|
||||||
),
|
|
||||||
...sectors.map((sector) {
|
|
||||||
final String libelle = sector.libelle.isNotEmpty
|
|
||||||
? sector.libelle
|
|
||||||
: 'Secteur ${sector.id}';
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: libelle,
|
|
||||||
child: Text(
|
|
||||||
libelle,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
if (value == 'Tous') {
|
|
||||||
_updateSectorFilter('Tous', null);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// Trouver le secteur correspondant
|
|
||||||
final sector = sectors.firstWhere(
|
|
||||||
(s) => s.libelle == value,
|
|
||||||
orElse: () => sectors.isNotEmpty
|
|
||||||
? sectors.first
|
|
||||||
: throw Exception('Liste de secteurs vide'),
|
|
||||||
);
|
|
||||||
// Convertir sector.id en int? si nécessaire
|
|
||||||
_updateSectorFilter(value, sector.id);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur lors de la sélection du secteur: $e');
|
|
||||||
_updateSectorFilter('Tous', null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par membre
|
|
||||||
Widget _buildMembreFilter(ThemeData theme, List<MembreModel> membres) {
|
|
||||||
// Fonction pour formater le nom d'affichage d'un membre
|
|
||||||
String formatMembreDisplayName(MembreModel membre) {
|
|
||||||
final String firstName = membre.firstName ?? '';
|
|
||||||
final String name = membre.name ?? '';
|
|
||||||
final String sectName = membre.sectName ?? '';
|
|
||||||
|
|
||||||
// Construire le nom de base
|
|
||||||
String displayName = '';
|
|
||||||
if (firstName.isNotEmpty && name.isNotEmpty) {
|
|
||||||
displayName = '$firstName $name';
|
|
||||||
} else if (name.isNotEmpty) {
|
|
||||||
displayName = name;
|
|
||||||
} else if (firstName.isNotEmpty) {
|
|
||||||
displayName = firstName;
|
|
||||||
} else {
|
|
||||||
displayName = 'Membre inconnu';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ajouter le sectName entre parenthèses s'il existe
|
|
||||||
if (sectName.isNotEmpty) {
|
|
||||||
displayName = '$displayName ($sectName)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trier les membres par nom de famille
|
|
||||||
final List<MembreModel> sortedMembres = [...membres];
|
|
||||||
sortedMembres.sort((a, b) {
|
|
||||||
final String nameA = a.name ?? '';
|
|
||||||
final String nameB = b.name ?? '';
|
|
||||||
return nameA.compareTo(nameB);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Créer une map pour retrouver les membres par leur nom d'affichage
|
|
||||||
final Map<String, MembreModel> membreDisplayMap = {};
|
|
||||||
for (final membre in sortedMembres) {
|
|
||||||
final displayName = formatMembreDisplayName(membre);
|
|
||||||
membreDisplayMap[displayName] = membre;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si la liste des membres est vide ou si selectedUser n'est pas dans la liste
|
|
||||||
bool isSelectedUserValid =
|
|
||||||
selectedUser == 'Tous' || membreDisplayMap.containsKey(selectedUser);
|
|
||||||
|
|
||||||
// Si selectedUser n'est pas valide, le réinitialiser à 'Tous'
|
|
||||||
if (!isSelectedUserValid) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
selectedUser = 'Tous';
|
|
||||||
selectedUserId = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: isSelectedUserValid ? selectedUser : 'Tous',
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
hint: const Text('Sélectionner un membre'),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Tous les membres'),
|
|
||||||
),
|
|
||||||
...membreDisplayMap.entries.map((entry) {
|
|
||||||
final String displayName = entry.key;
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: displayName,
|
|
||||||
child: Text(
|
|
||||||
displayName,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
if (value == 'Tous') {
|
|
||||||
_updateUserFilter('Tous', null);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// Trouver le membre correspondant dans la map
|
|
||||||
final membre = membreDisplayMap[value];
|
|
||||||
if (membre != null) {
|
|
||||||
final int membreId = membre.id;
|
|
||||||
_updateUserFilter(value, membreId);
|
|
||||||
} else {
|
|
||||||
throw Exception('Membre non trouvé: $value');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur lors de la sélection du membre: $e');
|
|
||||||
_updateUserFilter('Tous', null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par période
|
|
||||||
Widget _buildPeriodFilter(ThemeData theme) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: selectedPeriod,
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
hint: const Text('Sélectionner une période'),
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Toutes les périodes'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Derniers 15 jours',
|
|
||||||
child: Text('Derniers 15 jours'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Dernière semaine',
|
|
||||||
child: Text('Dernière semaine'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Dernier mois',
|
|
||||||
child: Text('Dernier mois'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
_updatePeriodFilter(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Afficher la plage de dates sélectionnée si elle existe
|
|
||||||
if (selectedDateRange != null && selectedPeriod != 'Tous')
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.date_range,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du champ de recherche
|
|
||||||
Widget _buildSearchField(ThemeData theme) {
|
|
||||||
return TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Rechercher par adresse, nom, secteur ou membre...',
|
|
||||||
prefixIcon: const Icon(Icons.search),
|
|
||||||
suffixIcon: _searchController.text.isNotEmpty
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_searchController.clear();
|
|
||||||
searchQuery = '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
searchQuery = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par type de passage
|
|
||||||
Widget _buildTypeFilter(ThemeData theme) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: selectedType,
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
hint: const Text('Sélectionner un type de passage'),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Tous les types'),
|
|
||||||
),
|
|
||||||
...AppKeys.typesPassages.entries.map((entry) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: entry.key.toString(),
|
|
||||||
child: Text(
|
|
||||||
entry.value['titre'] as String,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
setState(() {
|
|
||||||
selectedType = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Afficher le dialog de confirmation de suppression
|
// Afficher le dialog de confirmation de suppression
|
||||||
void _showDeleteConfirmationDialog(Map<String, dynamic> passage) {
|
void _showDeleteConfirmationDialog(Map<String, dynamic> passage) {
|
||||||
final TextEditingController confirmController = TextEditingController();
|
final TextEditingController confirmController = TextEditingController();
|
||||||
@@ -1631,46 +943,4 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construction du filtre par mode de règlement
|
|
||||||
Widget _buildPaymentFilter(ThemeData theme) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: selectedPaymentMethod,
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
hint: const Text('Sélectionner un mode de règlement'),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Tous les modes'),
|
|
||||||
),
|
|
||||||
...AppKeys.typesReglements.entries.map((entry) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: entry.key.toString(),
|
|
||||||
child: Text(
|
|
||||||
entry.value['titre'] as String,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
setState(() {
|
|
||||||
selectedPaymentMethod = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,19 @@ class UserDashboardHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
||||||
// Formater une date au format JJ/MM/YYYY
|
|
||||||
String _formatDate(DateTime date) {
|
|
||||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final isDesktop = size.width > 900;
|
final isDesktop = size.width > 900;
|
||||||
|
final isMobile = size.width < 600;
|
||||||
|
final double horizontalPadding = isMobile ? 8.0 : 16.0;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(horizontalPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -39,7 +36,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
final operation = userRepository.getCurrentOperation();
|
final operation = userRepository.getCurrentOperation();
|
||||||
if (operation != null) {
|
if (operation != null) {
|
||||||
return Text(
|
return Text(
|
||||||
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
|
operation.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppTheme.r(context, 20),
|
fontSize: AppTheme.r(context, 20),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -92,9 +89,9 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
// Construction d'une carte combinée pour les règlements (liste + graphique)
|
// Construction d'une carte combinée pour les règlements (liste + graphique)
|
||||||
Widget _buildCombinedPaymentsCard(bool isDesktop) {
|
Widget _buildCombinedPaymentsCard(bool isDesktop) {
|
||||||
return PaymentSummaryCard(
|
return PaymentSummaryCard(
|
||||||
title: 'Mes règlements',
|
title: 'Règlements',
|
||||||
titleColor: AppTheme.accentColor,
|
titleColor: AppTheme.accentColor,
|
||||||
titleIcon: Icons.payments,
|
titleIcon: Icons.euro,
|
||||||
height: 300,
|
height: 300,
|
||||||
useValueListenable: true,
|
useValueListenable: true,
|
||||||
userId: userRepository.getCurrentUser()?.id,
|
userId: userRepository.getCurrentUser()?.id,
|
||||||
@@ -105,27 +102,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
backgroundIconOpacity: 0.07,
|
backgroundIconOpacity: 0.07,
|
||||||
backgroundIconSize: 180,
|
backgroundIconSize: 180,
|
||||||
customTotalDisplay: (totalAmount) {
|
customTotalDisplay: (totalAmount) {
|
||||||
// Calculer le nombre de passages avec règlement pour le titre personnalisé
|
return '${totalAmount.toStringAsFixed(2)} €';
|
||||||
final currentUser = userRepository.getCurrentUser();
|
|
||||||
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)} €';
|
|
||||||
|
|
||||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
|
||||||
int passagesCount = 0;
|
|
||||||
|
|
||||||
for (final passage in passagesBox.values) {
|
|
||||||
if (passage.fkUser == currentUser.id) {
|
|
||||||
double montant = 0.0;
|
|
||||||
try {
|
|
||||||
String montantStr = passage.montant.replaceAll(',', '.');
|
|
||||||
montant = double.tryParse(montantStr) ?? 0.0;
|
|
||||||
} catch (e) {
|
|
||||||
// Ignorer les erreurs
|
|
||||||
}
|
|
||||||
if (montant > 0) passagesCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,7 +110,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
// Construction d'une carte combinée pour les passages (liste + graphique)
|
// Construction d'une carte combinée pour les passages (liste + graphique)
|
||||||
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
|
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
|
||||||
return PassageSummaryCard(
|
return PassageSummaryCard(
|
||||||
title: 'Mes passages',
|
title: 'Passages',
|
||||||
titleColor: AppTheme.primaryColor,
|
titleColor: AppTheme.primaryColor,
|
||||||
titleIcon: Icons.route,
|
titleIcon: Icons.route,
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -179,7 +156,7 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
|
|
||||||
// Construction de la liste des derniers passages
|
// Construction de la liste des derniers passages
|
||||||
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
|
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
|
||||||
// Utilisation directe du widget PassagesListWidget sans Card wrapper
|
// Utilisation directe du widget PassagesListWidget
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable:
|
valueListenable:
|
||||||
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||||
@@ -196,14 +173,14 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: const Padding(
|
||||||
padding: EdgeInsets.all(32.0),
|
padding: EdgeInsets.all(32.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Aucun passage récent',
|
'Aucun passage récent',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontSize: AppTheme.r(context, 14),
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -211,40 +188,15 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utiliser une hauteur fixe pour le widget dans le dashboard
|
// Utiliser PassagesListWidget sans hauteur fixe - laisse le widget gérer sa propre taille
|
||||||
return SizedBox(
|
return PassagesListWidget(
|
||||||
height:
|
passages: recentPassages,
|
||||||
450, // Hauteur légèrement augmentée pour compenser l'absence de Card
|
showFilters: false,
|
||||||
child: PassagesListWidget(
|
showSearch: false,
|
||||||
passages: recentPassages,
|
showActions: true,
|
||||||
showFilters: false,
|
maxPassages: 20,
|
||||||
showSearch: false,
|
showAddButton: false,
|
||||||
showActions: true,
|
sortBy: 'date',
|
||||||
maxPassages: 20,
|
|
||||||
// Ne pas appliquer de filtres supplémentaires car les passages
|
|
||||||
// sont déjà filtrés dans _getRecentPassages
|
|
||||||
excludePassageTypes:
|
|
||||||
null, // Pas de filtre, déjà géré dans _getRecentPassages
|
|
||||||
filterByUserId:
|
|
||||||
null, // Pas de filtre, déjà géré dans _getRecentPassages
|
|
||||||
periodFilter: null, // Pas de filtre de période
|
|
||||||
// Le widget gère maintenant le flux conditionnel par défaut
|
|
||||||
onPassageSelected: null,
|
|
||||||
onDetailsView: (passage) {
|
|
||||||
debugPrint('Affichage des détails: ${passage['id']}');
|
|
||||||
},
|
|
||||||
onPassageEdit: (passage) {
|
|
||||||
debugPrint('Modification du passage: ${passage['id']}');
|
|
||||||
},
|
|
||||||
onReceiptView: (passage) {
|
|
||||||
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
|
|
||||||
},
|
|
||||||
onPassageDelete: (passage) {
|
|
||||||
// Pas besoin de faire quoi que ce soit ici
|
|
||||||
// Le ValueListenableBuilder se rafraîchira automatiquement
|
|
||||||
// après la suppression dans Hive via le repository
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -261,8 +213,9 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
|
|||||||
final allPassages = passagesBox.values.where((p) {
|
final allPassages = passagesBox.values.where((p) {
|
||||||
if (p.passedAt == null) return false;
|
if (p.passedAt == null) return false;
|
||||||
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
|
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
|
||||||
if (currentUserId != null && p.fkUser != currentUserId)
|
if (currentUserId != null && p.fkUser != currentUserId) {
|
||||||
return false; // Filtrer par utilisateur
|
return false; // Filtrer par utilisateur
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,10 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
// État du tri actuel
|
// État du tri actuel
|
||||||
PassageSortType _currentSort = PassageSortType.dateDesc;
|
PassageSortType _currentSort = PassageSortType.dateDesc;
|
||||||
|
|
||||||
// État des filtres
|
// État des filtres (uniquement pour synchronisation)
|
||||||
String selectedSector = 'Tous';
|
|
||||||
String selectedPeriod = 'Tous';
|
|
||||||
String selectedType = 'Tous';
|
|
||||||
String selectedPaymentMethod = 'Tous';
|
|
||||||
DateTimeRange? selectedDateRange;
|
|
||||||
|
|
||||||
// IDs pour les filtres
|
|
||||||
int? selectedSectorId;
|
int? selectedSectorId;
|
||||||
|
String selectedPeriod = 'Toutes';
|
||||||
|
DateTimeRange? selectedDateRange;
|
||||||
|
|
||||||
// Repository pour les secteurs
|
// Repository pour les secteurs
|
||||||
late SectorRepository _sectorRepository;
|
late SectorRepository _sectorRepository;
|
||||||
@@ -130,20 +125,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
try {
|
try {
|
||||||
// Charger le secteur présélectionné
|
// Charger le secteur présélectionné
|
||||||
final int? preselectedSectorId = _settingsBox.get('history_selectedSectorId');
|
final int? preselectedSectorId = _settingsBox.get('history_selectedSectorId');
|
||||||
final String? preselectedSectorName = _settingsBox.get('history_selectedSectorName');
|
|
||||||
final int? preselectedTypeId = _settingsBox.get('history_selectedTypeId');
|
|
||||||
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
|
final String? preselectedPeriod = _settingsBox.get('history_selectedPeriod');
|
||||||
final int? preselectedPaymentId = _settingsBox.get('history_selectedPaymentId');
|
|
||||||
|
|
||||||
if (preselectedSectorId != null && preselectedSectorName != null) {
|
if (preselectedSectorId != null) {
|
||||||
selectedSectorId = preselectedSectorId;
|
selectedSectorId = preselectedSectorId;
|
||||||
selectedSector = preselectedSectorName;
|
debugPrint('Secteur présélectionné: ID $preselectedSectorId');
|
||||||
debugPrint('Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preselectedTypeId != null) {
|
|
||||||
selectedType = preselectedTypeId.toString();
|
|
||||||
debugPrint('Type de passage présélectionné: $preselectedTypeId');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preselectedPeriod != null) {
|
if (preselectedPeriod != null) {
|
||||||
@@ -152,11 +138,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
debugPrint('Période présélectionnée: $preselectedPeriod');
|
debugPrint('Période présélectionnée: $preselectedPeriod');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preselectedPaymentId != null) {
|
|
||||||
selectedPaymentMethod = preselectedPaymentId.toString();
|
|
||||||
debugPrint('Mode de règlement présélectionné: $preselectedPaymentId');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nettoyer les valeurs après utilisation
|
// Nettoyer les valeurs après utilisation
|
||||||
_settingsBox.delete('history_selectedSectorId');
|
_settingsBox.delete('history_selectedSectorId');
|
||||||
_settingsBox.delete('history_selectedSectorName');
|
_settingsBox.delete('history_selectedSectorName');
|
||||||
@@ -173,26 +154,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
try {
|
try {
|
||||||
if (selectedSectorId != null) {
|
if (selectedSectorId != null) {
|
||||||
_settingsBox.put('history_selectedSectorId', selectedSectorId);
|
_settingsBox.put('history_selectedSectorId', selectedSectorId);
|
||||||
_settingsBox.put('history_selectedSectorName', selectedSector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedType != 'Tous') {
|
if (selectedPeriod != 'Toutes') {
|
||||||
final typeId = int.tryParse(selectedType);
|
|
||||||
if (typeId != null) {
|
|
||||||
_settingsBox.put('history_selectedTypeId', typeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPeriod != 'Tous') {
|
|
||||||
_settingsBox.put('history_selectedPeriod', selectedPeriod);
|
_settingsBox.put('history_selectedPeriod', selectedPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPaymentMethod != 'Tous') {
|
|
||||||
final paymentId = int.tryParse(selectedPaymentMethod);
|
|
||||||
if (paymentId != null) {
|
|
||||||
_settingsBox.put('history_selectedPaymentId', paymentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Erreur lors de la sauvegarde des préférences: $e');
|
debugPrint('Erreur lors de la sauvegarde des préférences: $e');
|
||||||
}
|
}
|
||||||
@@ -201,7 +167,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
// Mettre à jour le filtre par secteur
|
// Mettre à jour le filtre par secteur
|
||||||
void _updateSectorFilter(String sectorName, int? sectorId) {
|
void _updateSectorFilter(String sectorName, int? sectorId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedSector = sectorName;
|
|
||||||
selectedSectorId = sectorId;
|
selectedSectorId = sectorId;
|
||||||
});
|
});
|
||||||
_saveFilterPreferences();
|
_saveFilterPreferences();
|
||||||
@@ -328,21 +293,6 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrer par type
|
|
||||||
if (selectedType != 'Tous') {
|
|
||||||
final typeId = int.tryParse(selectedType);
|
|
||||||
if (typeId != null && passage['type'] != typeId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par mode de règlement
|
|
||||||
if (selectedPaymentMethod != 'Tous') {
|
|
||||||
final paymentId = int.tryParse(selectedPaymentMethod);
|
|
||||||
if (paymentId != null && passage['payment'] != paymentId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer par période/date
|
// Filtrer par période/date
|
||||||
if (selectedDateRange != null && passage['date'] is DateTime) {
|
if (selectedDateRange != null && passage['date'] is DateTime) {
|
||||||
@@ -654,210 +604,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construction des filtres
|
// Les filtres sont maintenant gérés directement dans le PassagesListWidget
|
||||||
Widget _buildFilters(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
final isDesktop = size.width > 900;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
color: Colors.white.withValues(alpha: 0.95),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Filtres',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
if (isDesktop)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// Filtre par secteur (si plusieurs secteurs)
|
|
||||||
if (_userSectors.length > 1)
|
|
||||||
Expanded(
|
|
||||||
child: _buildSectorFilter(theme),
|
|
||||||
),
|
|
||||||
if (_userSectors.length > 1)
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
|
|
||||||
// Filtre par période
|
|
||||||
Expanded(
|
|
||||||
child: _buildPeriodFilter(theme),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
// Filtre par secteur (si plusieurs secteurs)
|
|
||||||
if (_userSectors.length > 1) ...[
|
|
||||||
_buildSectorFilter(theme),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Filtre par période
|
|
||||||
_buildPeriodFilter(theme),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par secteur
|
// Méthodes de filtre retirées car maintenant gérées dans le widget
|
||||||
Widget _buildSectorFilter(ThemeData theme) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Secteur',
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: selectedSector,
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Tous les secteurs'),
|
|
||||||
),
|
|
||||||
..._userSectors.map((sector) {
|
|
||||||
final String libelle = sector.libelle.isNotEmpty
|
|
||||||
? sector.libelle
|
|
||||||
: 'Secteur ${sector.id}';
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: libelle,
|
|
||||||
child: Text(
|
|
||||||
libelle,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
if (value == 'Tous') {
|
|
||||||
_updateSectorFilter('Tous', null);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
final sector = _userSectors.firstWhere(
|
|
||||||
(s) => s.libelle == value,
|
|
||||||
);
|
|
||||||
_updateSectorFilter(value, sector.id);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Erreur lors de la sélection du secteur: $e');
|
|
||||||
_updateSectorFilter('Tous', null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du filtre par période
|
|
||||||
Widget _buildPeriodFilter(ThemeData theme) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Période',
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: theme.colorScheme.outline),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: selectedPeriod,
|
|
||||||
isExpanded: true,
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Tous',
|
|
||||||
child: Text('Toutes les périodes'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Derniers 15 jours',
|
|
||||||
child: Text('Derniers 15 jours'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Dernière semaine',
|
|
||||||
child: Text('Dernière semaine'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Dernier mois',
|
|
||||||
child: Text('Dernier mois'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value != null) {
|
|
||||||
_updatePeriodFilter(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Afficher la plage de dates sélectionnée si elle existe
|
|
||||||
if (selectedDateRange != null && selectedPeriod != 'Tous')
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.date_range,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -869,18 +618,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Filtres avec bouton de rafraîchissement
|
// Les filtres sont maintenant intégrés dans le PassagesListWidget
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Filtres (secteur et période) avec bouton rafraîchir
|
|
||||||
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
|
|
||||||
_buildFilters(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Affichage du chargement ou des erreurs
|
// Affichage du chargement ou des erreurs
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
@@ -944,14 +682,31 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appliquer les filtres
|
|
||||||
passagesMap = _getFilteredPassages(passagesMap);
|
|
||||||
|
|
||||||
// Appliquer le tri sélectionné
|
// Appliquer le tri sélectionné
|
||||||
passagesMap = _sortPassages(passagesMap);
|
passagesMap = _sortPassages(passagesMap);
|
||||||
|
|
||||||
return PassagesListWidget(
|
return PassagesListWidget(
|
||||||
showAddButton: true, // Activer le bouton de création
|
// Données
|
||||||
|
passages: passagesMap,
|
||||||
|
// Activation des filtres
|
||||||
|
showFilters: true,
|
||||||
|
showSearch: true,
|
||||||
|
showTypeFilter: true,
|
||||||
|
showPaymentFilter: true,
|
||||||
|
showSectorFilter: true,
|
||||||
|
showUserFilter: false, // Pas de filtre membre pour la page user
|
||||||
|
showPeriodFilter: true,
|
||||||
|
// Données pour les filtres
|
||||||
|
sectors: _userSectors,
|
||||||
|
members: null, // Pas de filtre membre pour la page user
|
||||||
|
// Valeurs initiales
|
||||||
|
initialSectorId: selectedSectorId,
|
||||||
|
initialPeriod: selectedPeriod,
|
||||||
|
dateRange: selectedDateRange,
|
||||||
|
// Filtre par utilisateur courant
|
||||||
|
filterByUserId: currentUserId,
|
||||||
|
// Bouton d'ajout
|
||||||
|
showAddButton: true,
|
||||||
onAddPassage: () async {
|
onAddPassage: () async {
|
||||||
// Ouvrir le dialogue de création de passage
|
// Ouvrir le dialogue de création de passage
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@@ -1041,17 +796,17 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
passages: passagesMap,
|
// Actions
|
||||||
showFilters: true,
|
|
||||||
showSearch: true,
|
|
||||||
showActions: true,
|
showActions: true,
|
||||||
initialSearchQuery: '',
|
|
||||||
initialTypeFilter: selectedType,
|
|
||||||
initialPaymentFilter: selectedPaymentMethod,
|
|
||||||
excludePassageTypes: const [],
|
|
||||||
filterByUserId: null, // Déjà filtré en amont
|
|
||||||
key: const ValueKey('user_passages_list'),
|
key: const ValueKey('user_passages_list'),
|
||||||
onPassageSelected: null,
|
// Callback pour synchroniser les filtres
|
||||||
|
onFiltersChanged: (filters) {
|
||||||
|
setState(() {
|
||||||
|
selectedSectorId = filters['sectorId'];
|
||||||
|
selectedPeriod = filters['period'] ?? 'Toutes';
|
||||||
|
selectedDateRange = filters['dateRange'];
|
||||||
|
});
|
||||||
|
},
|
||||||
onDetailsView: (passage) {
|
onDetailsView: (passage) {
|
||||||
debugPrint('Affichage des détails: ${passage['id']}');
|
debugPrint('Affichage des détails: ${passage['id']}');
|
||||||
_showPassageDetails(passage);
|
_showPassageDetails(passage);
|
||||||
|
|||||||
@@ -274,30 +274,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
/// Construction du titre de l'AppBar
|
/// Construction du titre de l'AppBar
|
||||||
Widget _buildTitle(BuildContext context) {
|
Widget _buildTitle(BuildContext context) {
|
||||||
// Si aucun titre de page n'est fourni, afficher simplement le titre principal
|
// Titre vide pour économiser de l'espace sur mobile
|
||||||
if (pageTitle == null) {
|
return const Text('');
|
||||||
return Text(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utiliser LayoutBuilder pour détecter la largeur disponible
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
// Déterminer si on est sur mobile ou écran étroit
|
|
||||||
final isNarrowScreen = constraints.maxWidth < 600;
|
|
||||||
final isMobilePlatform =
|
|
||||||
Theme.of(context).platform == TargetPlatform.android ||
|
|
||||||
Theme.of(context).platform == TargetPlatform.iOS;
|
|
||||||
|
|
||||||
// Sur mobile ou écrans étroits, afficher seulement le titre principal
|
|
||||||
if (isNarrowScreen || isMobilePlatform) {
|
|
||||||
return Text(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sur écrans larges (web desktop), afficher le titre de la page ou le titre principal
|
|
||||||
// Pour les admins, on affiche directement le titre de la page sans préfixe
|
|
||||||
return Text(pageTitle!);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:geosector_app/core/services/current_amicale_service.dart';
|
|||||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||||
import 'package:geosector_app/app.dart';
|
import 'package:geosector_app/app.dart';
|
||||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||||
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||||
|
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
|
||||||
@@ -25,6 +27,13 @@ class PassagesListWidget extends StatefulWidget {
|
|||||||
|
|
||||||
/// Si vrai, la barre de recherche sera affichée
|
/// Si vrai, la barre de recherche sera affichée
|
||||||
final bool showSearch;
|
final bool showSearch;
|
||||||
|
|
||||||
|
/// Contrôle de l'affichage des filtres individuels
|
||||||
|
final bool showTypeFilter;
|
||||||
|
final bool showPaymentFilter;
|
||||||
|
final bool showSectorFilter;
|
||||||
|
final bool showUserFilter;
|
||||||
|
final bool showPeriodFilter;
|
||||||
|
|
||||||
/// Si vrai, les boutons d'action (détails, modifier, etc.) seront affichés
|
/// Si vrai, les boutons d'action (détails, modifier, etc.) seront affichés
|
||||||
final bool showActions;
|
final bool showActions;
|
||||||
@@ -76,6 +85,18 @@ class PassagesListWidget extends StatefulWidget {
|
|||||||
|
|
||||||
/// Callback appelé lorsque le bouton d'ajout est cliqué
|
/// Callback appelé lorsque le bouton d'ajout est cliqué
|
||||||
final VoidCallback? onAddPassage;
|
final VoidCallback? onAddPassage;
|
||||||
|
|
||||||
|
/// Données pour les filtres avancés
|
||||||
|
final List<SectorModel>? sectors;
|
||||||
|
final List<UserModel>? members;
|
||||||
|
|
||||||
|
/// Valeurs initiales pour les filtres avancés
|
||||||
|
final int? initialSectorId;
|
||||||
|
final int? initialUserId;
|
||||||
|
final String? initialPeriod;
|
||||||
|
|
||||||
|
/// Callback appelé lorsque les filtres changent
|
||||||
|
final Function(Map<String, dynamic>)? onFiltersChanged;
|
||||||
|
|
||||||
const PassagesListWidget({
|
const PassagesListWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -85,6 +106,11 @@ class PassagesListWidget extends StatefulWidget {
|
|||||||
this.showFilters = true,
|
this.showFilters = true,
|
||||||
this.showSearch = true,
|
this.showSearch = true,
|
||||||
this.showActions = true,
|
this.showActions = true,
|
||||||
|
this.showTypeFilter = true,
|
||||||
|
this.showPaymentFilter = true,
|
||||||
|
this.showSectorFilter = false,
|
||||||
|
this.showUserFilter = false,
|
||||||
|
this.showPeriodFilter = false,
|
||||||
this.onPassageSelected,
|
this.onPassageSelected,
|
||||||
this.onPassageEdit,
|
this.onPassageEdit,
|
||||||
this.onReceiptView,
|
this.onReceiptView,
|
||||||
@@ -102,6 +128,12 @@ class PassagesListWidget extends StatefulWidget {
|
|||||||
this.sortingButtons,
|
this.sortingButtons,
|
||||||
this.showAddButton = false,
|
this.showAddButton = false,
|
||||||
this.onAddPassage,
|
this.onAddPassage,
|
||||||
|
this.sectors,
|
||||||
|
this.members,
|
||||||
|
this.initialSectorId,
|
||||||
|
this.initialUserId,
|
||||||
|
this.initialPeriod,
|
||||||
|
this.onFiltersChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -113,6 +145,10 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
late String _selectedTypeFilter;
|
late String _selectedTypeFilter;
|
||||||
late String _selectedPaymentFilter;
|
late String _selectedPaymentFilter;
|
||||||
late String _searchQuery;
|
late String _searchQuery;
|
||||||
|
late int? _selectedSectorId;
|
||||||
|
late int? _selectedUserId;
|
||||||
|
late String _selectedPeriod;
|
||||||
|
DateTimeRange? _selectedDateRange;
|
||||||
|
|
||||||
// Contrôleur de recherche
|
// Contrôleur de recherche
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@@ -121,10 +157,29 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialiser les filtres
|
// Initialiser les filtres
|
||||||
_selectedTypeFilter = widget.initialTypeFilter ?? 'Tous';
|
_selectedTypeFilter = widget.initialTypeFilter ?? 'Tous les types';
|
||||||
_selectedPaymentFilter = widget.initialPaymentFilter ?? 'Tous';
|
_selectedPaymentFilter = widget.initialPaymentFilter ?? 'Tous les règlements';
|
||||||
_searchQuery = widget.initialSearchQuery ?? '';
|
_searchQuery = widget.initialSearchQuery ?? '';
|
||||||
_searchController.text = _searchQuery;
|
_searchController.text = _searchQuery;
|
||||||
|
_selectedSectorId = widget.initialSectorId;
|
||||||
|
_selectedUserId = widget.initialUserId;
|
||||||
|
_selectedPeriod = widget.initialPeriod ?? 'Toutes les périodes';
|
||||||
|
_selectedDateRange = widget.dateRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifier les changements de filtres
|
||||||
|
void _notifyFiltersChanged() {
|
||||||
|
if (widget.onFiltersChanged != null) {
|
||||||
|
widget.onFiltersChanged!({
|
||||||
|
'typeFilter': _selectedTypeFilter,
|
||||||
|
'paymentFilter': _selectedPaymentFilter,
|
||||||
|
'searchQuery': _searchQuery,
|
||||||
|
'sectorId': _selectedSectorId,
|
||||||
|
'userId': _selectedUserId,
|
||||||
|
'period': _selectedPeriod,
|
||||||
|
'dateRange': _selectedDateRange,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si l'amicale autorise la suppression des passages
|
// Vérifier si l'amicale autorise la suppression des passages
|
||||||
@@ -204,13 +259,13 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value)
|
color: Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32())
|
||||||
.withValues(alpha: 0.1),
|
.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
typeInfo?['icon_data'] ?? Icons.receipt_long,
|
typeInfo?['icon_data'] ?? Icons.receipt_long,
|
||||||
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value),
|
color: Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32()),
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -231,7 +286,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
horizontal: 8, vertical: 2),
|
horizontal: 8, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color:
|
||||||
Color(typeInfo?['couleur1'] ?? Colors.blue.value)
|
Color(typeInfo?['couleur1'] ?? Colors.blue.toARGB32())
|
||||||
.withValues(alpha: 0.1),
|
.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -239,7 +294,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
typeInfo?['titre'] ?? 'Inconnu',
|
typeInfo?['titre'] ?? 'Inconnu',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(
|
color: Color(
|
||||||
typeInfo?['couleur1'] ?? Colors.blue.value),
|
typeInfo?['couleur1'] ?? Colors.blue.toARGB32()),
|
||||||
fontSize: AppTheme.r(context, 12),
|
fontSize: AppTheme.r(context, 12),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -323,7 +378,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
horizontal: 8, vertical: 4),
|
horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(paymentInfo?['couleur'] ??
|
color: Color(paymentInfo?['couleur'] ??
|
||||||
Colors.grey.value)
|
Colors.grey.toARGB32())
|
||||||
.withValues(alpha: 0.1),
|
.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
@@ -331,7 +386,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
paymentInfo?['titre'] ?? 'Inconnu',
|
paymentInfo?['titre'] ?? 'Inconnu',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(paymentInfo?['couleur'] ??
|
color: Color(paymentInfo?['couleur'] ??
|
||||||
Colors.grey.value),
|
Colors.grey.toARGB32()),
|
||||||
fontSize: AppTheme.r(context, 12),
|
fontSize: AppTheme.r(context, 12),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -749,14 +804,58 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filtrer par secteur
|
// Filtrer par secteur
|
||||||
if (widget.filterBySectorId != null &&
|
if (_selectedSectorId != null &&
|
||||||
passage.containsKey('fkSector') &&
|
passage.containsKey('fkSector') &&
|
||||||
passage['fkSector'] != widget.filterBySectorId) {
|
passage['fkSector'] != _selectedSectorId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filtrer par membre/utilisateur
|
||||||
|
if (_selectedUserId != null &&
|
||||||
|
passage.containsKey('fkUser') &&
|
||||||
|
passage['fkUser'] != _selectedUserId) {
|
||||||
|
// Les passages de type 2 sont partagés
|
||||||
|
if (passage.containsKey('type') && passage['type'] == 2) {
|
||||||
|
// Ne pas filtrer les passages type 2
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer par période
|
||||||
|
if (_selectedPeriod != 'Toutes les périodes' && passage.containsKey('date')) {
|
||||||
|
final DateTime passageDate = passage['date'] as DateTime;
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
switch (_selectedPeriod) {
|
||||||
|
case 'Dernières 24h':
|
||||||
|
if (now.difference(passageDate).inHours > 24) return false;
|
||||||
|
break;
|
||||||
|
case 'Dernières 48h':
|
||||||
|
if (now.difference(passageDate).inHours > 48) return false;
|
||||||
|
break;
|
||||||
|
case 'Derniers 7 jours':
|
||||||
|
if (now.difference(passageDate).inDays > 7) return false;
|
||||||
|
break;
|
||||||
|
case 'Derniers 15 jours':
|
||||||
|
if (now.difference(passageDate).inDays > 15) return false;
|
||||||
|
break;
|
||||||
|
case 'Dernier mois':
|
||||||
|
if (now.difference(passageDate).inDays > 30) return false;
|
||||||
|
break;
|
||||||
|
case 'Personnalisée':
|
||||||
|
if (_selectedDateRange != null) {
|
||||||
|
if (passageDate.isBefore(_selectedDateRange!.start) ||
|
||||||
|
passageDate.isAfter(_selectedDateRange!.end.add(const Duration(days: 1)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filtre par type
|
// Filtre par type
|
||||||
if (_selectedTypeFilter != 'Tous') {
|
if (_selectedTypeFilter != 'Tous les types') {
|
||||||
try {
|
try {
|
||||||
final typeEntries = AppKeys.typesPassages.entries.where(
|
final typeEntries = AppKeys.typesPassages.entries.where(
|
||||||
(entry) => entry.value['titre'] == _selectedTypeFilter);
|
(entry) => entry.value['titre'] == _selectedTypeFilter);
|
||||||
@@ -774,7 +873,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filtre par type de règlement
|
// Filtre par type de règlement
|
||||||
if (_selectedPaymentFilter != 'Tous') {
|
if (_selectedPaymentFilter != 'Tous les règlements') {
|
||||||
try {
|
try {
|
||||||
final paymentEntries = AppKeys.typesReglements.entries.where(
|
final paymentEntries = AppKeys.typesReglements.entries.where(
|
||||||
(entry) => entry.value['titre'] == _selectedPaymentFilter);
|
(entry) => entry.value['titre'] == _selectedPaymentFilter);
|
||||||
@@ -1043,9 +1142,17 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
// Dans les pages user, seuls les passages de l'utilisateur courant sont affichés normalement
|
// Dans les pages user, seuls les passages de l'utilisateur courant sont affichés normalement
|
||||||
final bool shouldGreyOut = !isAdminPage && !isOwnedByCurrentUser;
|
final bool shouldGreyOut = !isAdminPage && !isOwnedByCurrentUser;
|
||||||
final bool isClickable = isAdminPage || isOwnedByCurrentUser;
|
final bool isClickable = isAdminPage || isOwnedByCurrentUser;
|
||||||
|
|
||||||
|
// Dimensions responsives
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final bool isMobile = screenWidth < 600;
|
||||||
|
final cardMargin = isMobile ? 4.0 : 6.0;
|
||||||
|
final horizontalPadding = isMobile ? 10.0 : 12.0;
|
||||||
|
final verticalPadding = isMobile ? 8.0 : 10.0;
|
||||||
|
final iconSize = isMobile ? 32.0 : 36.0;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 6), // Réduit de 8 à 6
|
margin: EdgeInsets.only(bottom: cardMargin),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -1059,8 +1166,9 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
onTap: isClickable ? () => _handlePassageClick(passage) : null,
|
onTap: isClickable ? () => _handlePassageClick(passage) : null,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 12.0, vertical: 10.0), // Réduit de 16 à 12/10
|
horizontal: horizontalPadding,
|
||||||
|
vertical: verticalPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -1069,8 +1177,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
children: [
|
children: [
|
||||||
// Icône du type de passage avec bordure couleur2
|
// Icône du type de passage avec bordure couleur2
|
||||||
Container(
|
Container(
|
||||||
width: 36, // Réduit de 40 à 36
|
width: iconSize,
|
||||||
height: 36,
|
height: iconSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(typePassage['couleur1'] as int)
|
color: Color(typePassage['couleur1'] as int)
|
||||||
.withValues(alpha: 0.1),
|
.withValues(alpha: 0.1),
|
||||||
@@ -1296,13 +1404,15 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
) {
|
) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
if (label.isNotEmpty) ...[
|
||||||
'$label:',
|
Text(
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
'$label:',
|
||||||
fontWeight: FontWeight.bold,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
@@ -1337,6 +1447,190 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construction du filtre de secteur
|
||||||
|
Widget _buildSectorFilter(ThemeData theme, bool isCompact) {
|
||||||
|
if (widget.sectors == null || widget.sectors!.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final options = ['Tous les secteurs'] +
|
||||||
|
widget.sectors!.map((s) => s.libelle).toList();
|
||||||
|
|
||||||
|
final selectedValue = _selectedSectorId == null
|
||||||
|
? 'Tous les secteurs'
|
||||||
|
: () {
|
||||||
|
final sector = widget.sectors!.firstWhere((s) => s.id == _selectedSectorId,
|
||||||
|
orElse: () => widget.sectors!.first);
|
||||||
|
return sector.libelle;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return isCompact
|
||||||
|
? _buildCompactDropdownFilter(
|
||||||
|
'Secteur',
|
||||||
|
selectedValue,
|
||||||
|
options,
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 'Tous les secteurs') {
|
||||||
|
_selectedSectorId = null;
|
||||||
|
} else {
|
||||||
|
_selectedSectorId = widget.sectors!.firstWhere((s) => s.libelle == value).id;
|
||||||
|
}
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
)
|
||||||
|
: _buildDropdownFilter(
|
||||||
|
'',
|
||||||
|
selectedValue,
|
||||||
|
options,
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 'Tous les secteurs') {
|
||||||
|
_selectedSectorId = null;
|
||||||
|
} else {
|
||||||
|
_selectedSectorId = widget.sectors!.firstWhere((s) => s.libelle == value).id;
|
||||||
|
}
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construction du filtre de membre/utilisateur
|
||||||
|
Widget _buildUserFilter(ThemeData theme, bool isCompact) {
|
||||||
|
if (widget.members == null || widget.members!.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final options = ['Tous les membres'] +
|
||||||
|
widget.members!.map((u) => '${u.firstName} ${u.name}'.trim()).toList();
|
||||||
|
|
||||||
|
final selectedValue = _selectedUserId == null
|
||||||
|
? 'Tous les membres'
|
||||||
|
: () {
|
||||||
|
final user = widget.members!.firstWhere((u) => u.id == _selectedUserId,
|
||||||
|
orElse: () => widget.members!.first);
|
||||||
|
return '${user.firstName} ${user.name}'.trim();
|
||||||
|
}();
|
||||||
|
|
||||||
|
return isCompact
|
||||||
|
? _buildCompactDropdownFilter(
|
||||||
|
'Membre',
|
||||||
|
selectedValue,
|
||||||
|
options,
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 'Tous les membres') {
|
||||||
|
_selectedUserId = null;
|
||||||
|
} else {
|
||||||
|
_selectedUserId = widget.members!.firstWhere(
|
||||||
|
(u) => '${u.firstName} ${u.name}'.trim() == value
|
||||||
|
).id;
|
||||||
|
}
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
)
|
||||||
|
: _buildDropdownFilter(
|
||||||
|
'',
|
||||||
|
selectedValue,
|
||||||
|
options,
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 'Tous les membres') {
|
||||||
|
_selectedUserId = null;
|
||||||
|
} else {
|
||||||
|
_selectedUserId = widget.members!.firstWhere(
|
||||||
|
(u) => '${u.firstName} ${u.name}'.trim() == value
|
||||||
|
).id;
|
||||||
|
}
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construction du filtre de période
|
||||||
|
Widget _buildPeriodFilter(ThemeData theme, bool isCompact) {
|
||||||
|
final options = [
|
||||||
|
'Toutes les périodes',
|
||||||
|
'Dernières 24h',
|
||||||
|
'Dernières 48h',
|
||||||
|
'Derniers 7 jours',
|
||||||
|
'Derniers 15 jours',
|
||||||
|
'Dernier mois',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (_selectedDateRange != null && _selectedPeriod == 'Personnalisée') {
|
||||||
|
options.add('Personnalisée');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCompact
|
||||||
|
? _buildCompactDropdownFilter(
|
||||||
|
'Période',
|
||||||
|
_selectedPeriod,
|
||||||
|
options,
|
||||||
|
(value) async {
|
||||||
|
if (value == 'Personnalisée') {
|
||||||
|
final picked = await showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime.now().subtract(const Duration(days: 365)),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
initialDateRange: _selectedDateRange,
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDateRange = picked;
|
||||||
|
_selectedPeriod = 'Personnalisée';
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedPeriod = value;
|
||||||
|
_selectedDateRange = null;
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
)
|
||||||
|
: _buildDropdownFilter(
|
||||||
|
'',
|
||||||
|
_selectedPeriod,
|
||||||
|
options,
|
||||||
|
(value) async {
|
||||||
|
if (value == 'Personnalisée') {
|
||||||
|
final picked = await showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime.now().subtract(const Duration(days: 365)),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
initialDateRange: _selectedDateRange,
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDateRange = picked;
|
||||||
|
_selectedPeriod = 'Personnalisée';
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedPeriod = value;
|
||||||
|
_selectedDateRange = null;
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1536,185 +1830,236 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (isDesktop)
|
// Barre de recherche (si activée) - toujours en premier
|
||||||
// Version compacte pour le web (desktop)
|
if (widget.showSearch)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Row(
|
child: TextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: _searchController,
|
||||||
children: [
|
decoration: InputDecoration(
|
||||||
// Barre de recherche (si activée)
|
hintText: 'Rechercher par adresse ou nom...',
|
||||||
if (widget.showSearch)
|
prefixIcon: const Icon(Icons.search),
|
||||||
Expanded(
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
flex: 2,
|
? IconButton(
|
||||||
child: Padding(
|
icon: const Icon(Icons.clear),
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
onPressed: () {
|
||||||
child: TextField(
|
_searchController.clear();
|
||||||
controller: _searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Rechercher par adresse ou nom...',
|
|
||||||
prefixIcon: const Icon(Icons.search),
|
|
||||||
suffixIcon: _searchQuery.isNotEmpty
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
_searchController.clear();
|
|
||||||
setState(() {
|
|
||||||
_searchQuery = '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: theme.colorScheme.outline,
|
|
||||||
width: 1.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0, vertical: 14.0),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_searchQuery = value;
|
_searchQuery = '';
|
||||||
|
_notifyFiltersChanged();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 14.0),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (isDesktop)
|
||||||
|
// Version compacte pour le web (desktop)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// Première ligne : Type, Règlement, Secteur
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Filtre par type de passage
|
||||||
|
if (widget.showTypeFilter)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: _buildCompactDropdownFilter(
|
||||||
|
'Type',
|
||||||
|
_selectedTypeFilter,
|
||||||
|
[
|
||||||
|
'Tous les types',
|
||||||
|
...AppKeys.typesPassages.values
|
||||||
|
.map((type) => type['titre'] as String)
|
||||||
|
],
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTypeFilter = value;
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Filtre par type de passage
|
// Filtre par type de règlement
|
||||||
Expanded(
|
if (widget.showPaymentFilter)
|
||||||
child: Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
child: Padding(
|
||||||
child: _buildCompactDropdownFilter(
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
'Type',
|
child: _buildCompactDropdownFilter(
|
||||||
_selectedTypeFilter,
|
'Règlement',
|
||||||
[
|
_selectedPaymentFilter,
|
||||||
'Tous',
|
[
|
||||||
...AppKeys.typesPassages.values
|
'Tous les règlements',
|
||||||
.map((type) => type['titre'] as String)
|
...AppKeys.typesReglements.values
|
||||||
],
|
.map((type) => type['titre'] as String)
|
||||||
(value) {
|
],
|
||||||
setState(() {
|
(value) {
|
||||||
_selectedTypeFilter = value;
|
setState(() {
|
||||||
});
|
_selectedPaymentFilter = value;
|
||||||
},
|
_notifyFiltersChanged();
|
||||||
theme,
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
// Filtre par secteur
|
||||||
|
if (widget.showSectorFilter && widget.sectors != null)
|
||||||
// Filtre par type de règlement
|
Expanded(
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: _buildCompactDropdownFilter(
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
'Règlement',
|
child: _buildSectorFilter(theme, true),
|
||||||
_selectedPaymentFilter,
|
),
|
||||||
[
|
),
|
||||||
'Tous',
|
],
|
||||||
...AppKeys.typesReglements.values
|
),
|
||||||
.map((type) => type['titre'] as String)
|
|
||||||
|
// Deuxième ligne : Membre et Période (si nécessaire)
|
||||||
|
if (widget.showUserFilter || widget.showPeriodFilter)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Filtre par membre
|
||||||
|
if (widget.showUserFilter && widget.members != null)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: _buildUserFilter(theme, true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Filtre par période
|
||||||
|
if (widget.showPeriodFilter)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: _buildPeriodFilter(theme, true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Spacer si un seul filtre sur la deuxième ligne
|
||||||
|
if ((widget.showUserFilter && !widget.showPeriodFilter) ||
|
||||||
|
(!widget.showUserFilter && widget.showPeriodFilter))
|
||||||
|
const Expanded(child: SizedBox()),
|
||||||
],
|
],
|
||||||
(value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedPaymentFilter = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
// Version mobile (non-desktop)
|
// Version mobile (non-desktop)
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Barre de recherche (si activée)
|
// Première ligne : Type et Règlement
|
||||||
if (widget.showSearch)
|
if (widget.showTypeFilter || widget.showPaymentFilter)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: TextField(
|
child: Row(
|
||||||
controller: _searchController,
|
children: [
|
||||||
decoration: InputDecoration(
|
// Filtre par type de passage
|
||||||
hintText: 'Rechercher par adresse ou nom...',
|
if (widget.showTypeFilter)
|
||||||
prefixIcon: const Icon(Icons.search),
|
Expanded(
|
||||||
suffixIcon: _searchQuery.isNotEmpty
|
child: Padding(
|
||||||
? IconButton(
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
icon: const Icon(Icons.clear),
|
child: _buildDropdownFilter(
|
||||||
onPressed: () {
|
'',
|
||||||
_searchController.clear();
|
_selectedTypeFilter,
|
||||||
|
[
|
||||||
|
'Tous les types',
|
||||||
|
...AppKeys.typesPassages.values
|
||||||
|
.map((type) => type['titre'] as String)
|
||||||
|
],
|
||||||
|
(value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_searchQuery = '';
|
_selectedTypeFilter = value;
|
||||||
|
_notifyFiltersChanged();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
theme,
|
||||||
: null,
|
),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: theme.colorScheme.outline,
|
|
||||||
width: 1.0,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
// Filtre par type de règlement
|
||||||
horizontal: 16.0, vertical: 14.0),
|
if (widget.showPaymentFilter)
|
||||||
),
|
Expanded(
|
||||||
onChanged: (value) {
|
child: _buildDropdownFilter(
|
||||||
setState(() {
|
'',
|
||||||
_searchQuery = value;
|
_selectedPaymentFilter,
|
||||||
});
|
[
|
||||||
},
|
'Tous les règlements',
|
||||||
|
...AppKeys.typesReglements.values
|
||||||
|
.map((type) => type['titre'] as String)
|
||||||
|
],
|
||||||
|
(value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedPaymentFilter = value;
|
||||||
|
_notifyFiltersChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Filtres
|
// Deuxième ligne : Secteur et Période
|
||||||
Row(
|
if (widget.showSectorFilter || widget.showPeriodFilter)
|
||||||
children: [
|
Padding(
|
||||||
// Filtre par type de passage
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
// Filtre par secteur
|
||||||
child: _buildDropdownFilter(
|
if (widget.showSectorFilter && widget.sectors != null)
|
||||||
'Type',
|
Expanded(
|
||||||
_selectedTypeFilter,
|
child: Padding(
|
||||||
[
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
'Tous',
|
child: _buildSectorFilter(theme, false),
|
||||||
...AppKeys.typesPassages.values
|
),
|
||||||
.map((type) => type['titre'] as String)
|
),
|
||||||
],
|
|
||||||
(value) {
|
// Filtre par période
|
||||||
setState(() {
|
if (widget.showPeriodFilter)
|
||||||
_selectedTypeFilter = value;
|
Expanded(
|
||||||
});
|
child: _buildPeriodFilter(theme, false),
|
||||||
},
|
),
|
||||||
theme,
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Filtre par type de règlement
|
|
||||||
Expanded(
|
// Troisième ligne : Membre (si nécessaire)
|
||||||
child: _buildDropdownFilter(
|
if (widget.showUserFilter && widget.members != null)
|
||||||
'Règlement',
|
Padding(
|
||||||
_selectedPaymentFilter,
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
[
|
child: _buildUserFilter(theme, false),
|
||||||
'Tous',
|
),
|
||||||
...AppKeys.typesReglements.values
|
|
||||||
.map((type) => type['titre'] as String)
|
|
||||||
],
|
|
||||||
(value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedPaymentFilter = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/pierre/dev/flutter
|
|||||||
FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app
|
FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app
|
||||||
COCOAPODS_PARALLEL_CODE_SIGN=true
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
FLUTTER_BUILD_DIR=build
|
FLUTTER_BUILD_DIR=build
|
||||||
FLUTTER_BUILD_NAME=3.2.3
|
FLUTTER_BUILD_NAME=3.2.4
|
||||||
FLUTTER_BUILD_NUMBER=323
|
FLUTTER_BUILD_NUMBER=324
|
||||||
FLUTTER_CLI_BUILD_MODE=debug
|
FLUTTER_CLI_BUILD_MODE=debug
|
||||||
DART_OBFUSCATION=false
|
DART_OBFUSCATION=false
|
||||||
TRACK_WIDGET_CREATION=true
|
TRACK_WIDGET_CREATION=true
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/pierre/dev/flutter"
|
|||||||
export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app"
|
export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app"
|
||||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||||
export "FLUTTER_BUILD_DIR=build"
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
export "FLUTTER_BUILD_NAME=3.2.3"
|
export "FLUTTER_BUILD_NAME=3.2.4"
|
||||||
export "FLUTTER_BUILD_NUMBER=323"
|
export "FLUTTER_BUILD_NUMBER=324"
|
||||||
export "FLUTTER_CLI_BUILD_MODE=debug"
|
export "FLUTTER_CLI_BUILD_MODE=debug"
|
||||||
export "DART_OBFUSCATION=false"
|
export "DART_OBFUSCATION=false"
|
||||||
export "TRACK_WIDGET_CREATION=true"
|
export "TRACK_WIDGET_CREATION=true"
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
|
sha256: "1b3b173f3379c8f941446267868548b6fc67e9134d81f4842eb98bb729451359"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.11.1"
|
version: "8.11.2"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -447,10 +447,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: geosector_app
|
name: geosector_app
|
||||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 3.2.3+323
|
version: 3.2.4+324
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ void main() {
|
|||||||
group('Environment Configuration Tests', () {
|
group('Environment Configuration Tests', () {
|
||||||
test('API URLs are correctly configured', () {
|
test('API URLs are correctly configured', () {
|
||||||
// Vérifier que les URLs sont différentes pour chaque environnement
|
// Vérifier que les URLs sont différentes pour chaque environnement
|
||||||
expect(AppKeys.baseApiUrlDev, 'https://dapp.geosector.fr/api/geo');
|
expect(AppKeys.baseApiUrlDev, 'https://app.geo.dev/api/geo');
|
||||||
expect(AppKeys.baseApiUrlRec, 'https://rapp.geosector.fr/api/geo');
|
expect(AppKeys.baseApiUrlRec, 'https://rapp.geosector.fr/api/geo');
|
||||||
expect(AppKeys.baseApiUrlProd, 'https://app.geosector.fr/api/geo');
|
expect(AppKeys.baseApiUrlProd, 'https://app.geosector.fr/api/geo');
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ void main() {
|
|||||||
|
|
||||||
test('App Identifiers are correctly configured', () {
|
test('App Identifiers are correctly configured', () {
|
||||||
// Vérifier que les identifiants sont configurés correctement
|
// Vérifier que les identifiants sont configurés correctement
|
||||||
expect(AppKeys.appIdentifierDev, 'dapp.geosector.fr');
|
expect(AppKeys.appIdentifierDev, 'app.geo.dev');
|
||||||
expect(AppKeys.appIdentifierRec, 'rapp.geosector.fr');
|
expect(AppKeys.appIdentifierRec, 'rapp.geosector.fr');
|
||||||
expect(AppKeys.appIdentifierProd, 'app.geosector.fr');
|
expect(AppKeys.appIdentifierProd, 'app.geosector.fr');
|
||||||
|
|
||||||
|
|||||||
278
maria.md
Normal file
278
maria.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Guide de migration MariaDB vers container centralisé
|
||||||
|
|
||||||
|
Ce guide détaille la procédure complète pour migrer la base de données `geo_app` depuis le container applicatif vers le container MariaDB centralisé.
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
- Container source (geo) avec MariaDB et la base geo_app
|
||||||
|
- Container cible (maria) avec MariaDB installé
|
||||||
|
- Réseau incusbr0 configuré entre les containers
|
||||||
|
- Accès root/sudo sur les deux containers
|
||||||
|
|
||||||
|
## 🔄 Procédure de migration
|
||||||
|
|
||||||
|
### 1. Sur le container SOURCE (geo)
|
||||||
|
|
||||||
|
#### Créer le dump de la base de données
|
||||||
|
```bash
|
||||||
|
# Se connecter au container source
|
||||||
|
incus exec geo -- bash
|
||||||
|
|
||||||
|
# Créer le dump avec structure et données
|
||||||
|
mysqldump -u root -p geo_app > /tmp/geo_app_dump.sql
|
||||||
|
|
||||||
|
# Vérifier le dump
|
||||||
|
ls -lh /tmp/geo_app_dump.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Copier le dump vers le container CIBLE
|
||||||
|
```bash
|
||||||
|
# Depuis l'hôte, copier le dump
|
||||||
|
incus file pull geo/tmp/geo_app_dump.sql ./
|
||||||
|
incus file push ./geo_app_dump.sql maria/tmp/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Sur le container CIBLE (maria)
|
||||||
|
|
||||||
|
#### Se connecter au container et à MariaDB
|
||||||
|
```bash
|
||||||
|
# Se connecter au container maria
|
||||||
|
incus exec maria -- bash
|
||||||
|
|
||||||
|
# Se connecter à MariaDB
|
||||||
|
mysql -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Créer la base de données
|
||||||
|
```sql
|
||||||
|
-- Créer la base avec l'encodage UTF8MB4 pour le français
|
||||||
|
CREATE DATABASE IF NOT EXISTS geo_app
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Vérifier la création
|
||||||
|
SHOW DATABASES;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Créer l'utilisateur et attribuer les droits
|
||||||
|
|
||||||
|
**Pour DEV:**
|
||||||
|
```sql
|
||||||
|
-- Créer l'utilisateur pour l'accès distant
|
||||||
|
CREATE USER IF NOT EXISTS 'geo_app_user_dev'@'%'
|
||||||
|
IDENTIFIED BY '34GOz-X5gJu-oH@Fa3$#Z';
|
||||||
|
|
||||||
|
-- Donner tous les privilèges sur la base geo_app
|
||||||
|
GRANT ALL PRIVILEGES ON geo_app.* TO 'geo_app_user_dev'@'%';
|
||||||
|
|
||||||
|
-- Appliquer les changements
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier les permissions
|
||||||
|
SHOW GRANTS FOR 'geo_app_user_dev'@'%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pour RECETTE:**
|
||||||
|
```sql
|
||||||
|
-- Créer l'utilisateur pour l'accès distant
|
||||||
|
CREATE USER IF NOT EXISTS 'geo_app_user_rec'@'%'
|
||||||
|
IDENTIFIED BY 'QO:96df*?k-dS3KiO-{4W6m';
|
||||||
|
|
||||||
|
-- Donner tous les privilèges sur la base geo_app
|
||||||
|
GRANT ALL PRIVILEGES ON geo_app.* TO 'geo_app_user_rec'@'%';
|
||||||
|
|
||||||
|
-- Appliquer les changements
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier les permissions
|
||||||
|
SHOW GRANTS FOR 'geo_app_user_rec'@'%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pour PROD:**
|
||||||
|
```sql
|
||||||
|
-- Créer l'utilisateur pour l'accès distant
|
||||||
|
CREATE USER IF NOT EXISTS 'geo_app_user_prod'@'%'
|
||||||
|
IDENTIFIED BY 'QO:96-SrHJ6k7-df*?k{4W6m';
|
||||||
|
|
||||||
|
-- Donner tous les privilèges sur la base geo_app
|
||||||
|
GRANT ALL PRIVILEGES ON geo_app.* TO 'geo_app_user_prod'@'%';
|
||||||
|
|
||||||
|
-- Appliquer les changements
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier les permissions
|
||||||
|
SHOW GRANTS FOR 'geo_app_user_prod'@'%';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Importer le dump
|
||||||
|
```bash
|
||||||
|
# Sortir de mysql si vous y êtes encore
|
||||||
|
exit
|
||||||
|
|
||||||
|
# Importer le dump dans la nouvelle base
|
||||||
|
mysql -u root -p geo_app < /tmp/geo_app_dump.sql
|
||||||
|
|
||||||
|
# Vérifier l'import
|
||||||
|
mysql -u root -p -e "USE geo_app; SHOW TABLES;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configuration réseau et firewall
|
||||||
|
|
||||||
|
#### Sur le container MARIA
|
||||||
|
|
||||||
|
##### Configurer MariaDB pour l'accès distant
|
||||||
|
```bash
|
||||||
|
# Éditer la configuration MariaDB
|
||||||
|
nano /etc/mysql/mariadb.conf.d/50-server.cnf
|
||||||
|
|
||||||
|
# Modifier ou ajouter:
|
||||||
|
bind-address = 0.0.0.0
|
||||||
|
|
||||||
|
# Redémarrer MariaDB
|
||||||
|
systemctl restart mariadb
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Configurer le firewall UFW
|
||||||
|
```bash
|
||||||
|
# Vérifier l'IP du container source (geo)
|
||||||
|
# Exemple: 13.23.33.43
|
||||||
|
|
||||||
|
# Autoriser l'accès depuis le container geo
|
||||||
|
ufw allow from 13.23.33.43 to any port 3306
|
||||||
|
|
||||||
|
# Ou autoriser tout le réseau incusbr0 (plus flexible)
|
||||||
|
ufw allow from 13.23.33.0/24 to any port 3306
|
||||||
|
|
||||||
|
# Vérifier les règles
|
||||||
|
ufw status numbered
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test de connexion
|
||||||
|
|
||||||
|
#### Depuis le container SOURCE (geo)
|
||||||
|
```bash
|
||||||
|
# Tester la connexion vers le container maria
|
||||||
|
# Pour DEV
|
||||||
|
mysql -h 13.23.33.46 -u geo_app_user_dev -p'34GOz-X5gJu-oH@Fa3$#Z' geo_app -e "SELECT 1;"
|
||||||
|
|
||||||
|
# Pour RECETTE
|
||||||
|
mysql -h 13.23.33.36 -u geo_app_user_rec -p'QO:96df*?k-dS3KiO-{4W6m' geo_app -e "SELECT 1;"
|
||||||
|
|
||||||
|
# Pour PROD
|
||||||
|
mysql -h 13.23.33.26 -u geo_app_user_prod -p'QO:96-SrHJ6k7-df*?k{4W6m' geo_app -e "SELECT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Mise à jour de la configuration API
|
||||||
|
|
||||||
|
Modifier le fichier `api/src/Config/AppConfig.php` pour pointer vers le nouveau serveur MariaDB:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Configuration DÉVELOPPEMENT
|
||||||
|
'database' => [
|
||||||
|
'host' => '13.23.33.46', // IP du container maria
|
||||||
|
'name' => 'geo_app',
|
||||||
|
'username' => 'geo_app_user_dev',
|
||||||
|
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Configuration RECETTE
|
||||||
|
'database' => [
|
||||||
|
'host' => '13.23.33.36', // IP du container maria recette
|
||||||
|
'name' => 'geo_app',
|
||||||
|
'username' => 'geo_app_user_rec',
|
||||||
|
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Configuration PRODUCTION
|
||||||
|
'database' => [
|
||||||
|
'host' => '13.23.33.26', // IP du container maria prod
|
||||||
|
'name' => 'geo_app',
|
||||||
|
'username' => 'geo_app_user_prod',
|
||||||
|
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Arrêt du service MariaDB local
|
||||||
|
|
||||||
|
#### Sur Alpine Linux (container geo)
|
||||||
|
```bash
|
||||||
|
# Arrêter le service MariaDB
|
||||||
|
rc-service mariadb stop
|
||||||
|
|
||||||
|
# Vérifier l'arrêt
|
||||||
|
rc-service mariadb status
|
||||||
|
|
||||||
|
# Désactiver le démarrage automatique
|
||||||
|
rc-update del mariadb
|
||||||
|
|
||||||
|
# Pour redémarrer si besoin
|
||||||
|
rc-service mariadb start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sur Ubuntu/Debian
|
||||||
|
```bash
|
||||||
|
# Arrêter le service
|
||||||
|
systemctl stop mariadb
|
||||||
|
# ou
|
||||||
|
systemctl stop mysql
|
||||||
|
|
||||||
|
# Vérifier l'arrêt
|
||||||
|
systemctl status mariadb
|
||||||
|
|
||||||
|
# Désactiver le démarrage automatique
|
||||||
|
systemctl disable mariadb
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Vérification finale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tester l'application web
|
||||||
|
curl http://localhost/api/health
|
||||||
|
|
||||||
|
# Vérifier les logs pour toute erreur
|
||||||
|
tail -f /var/log/apache2/error.log
|
||||||
|
# ou
|
||||||
|
tail -f /var/log/php*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Sécurité
|
||||||
|
|
||||||
|
- Les mots de passe utilisés ici sont ceux du fichier AppConfig.php
|
||||||
|
- En production, utilisez des mots de passe forts et uniques
|
||||||
|
- Limitez les accès réseau au strict minimum
|
||||||
|
- Activez SSL/TLS pour les connexions distantes si possible
|
||||||
|
|
||||||
|
## 📝 Notes importantes
|
||||||
|
|
||||||
|
1. **Sauvegarde**: Toujours faire une sauvegarde avant la migration
|
||||||
|
2. **Test**: Tester d'abord en environnement de développement
|
||||||
|
3. **Firewall**: Configurer précisément les règles firewall
|
||||||
|
4. **Monitoring**: Surveiller les performances après migration
|
||||||
|
5. **Rollback**: Garder l'ancienne base accessible pour un rollback rapide si nécessaire
|
||||||
|
|
||||||
|
## 🚨 Dépannage
|
||||||
|
|
||||||
|
### Erreur de connexion
|
||||||
|
```bash
|
||||||
|
# Vérifier que MariaDB écoute sur toutes les interfaces
|
||||||
|
netstat -tlnp | grep 3306
|
||||||
|
|
||||||
|
# Vérifier les logs MariaDB
|
||||||
|
tail -f /var/log/mysql/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur de permissions
|
||||||
|
```sql
|
||||||
|
-- Recréer les permissions
|
||||||
|
GRANT ALL PRIVILEGES ON geo_app.* TO 'geo_app_user_dev'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test de connectivité réseau
|
||||||
|
```bash
|
||||||
|
# Ping entre containers
|
||||||
|
ping 13.23.33.46
|
||||||
|
|
||||||
|
# Test du port MySQL
|
||||||
|
telnet 13.23.33.46 3306
|
||||||
|
```
|
||||||
@@ -1,203 +1,378 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Script de déploiement de Geosector Web
|
# Script de déploiement unifié pour GEOSECTOR Web (Svelte)
|
||||||
|
# Version: 4.0 (Janvier 2025)
|
||||||
|
# Auteur: Pierre (avec l'aide de Claude)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./deploy-web.sh # Déploiement local DEV (build → container geo)
|
||||||
|
# ./deploy-web.sh rca # Livraison RECETTE (container geo → rca-geo)
|
||||||
|
# ./deploy-web.sh pra # Livraison PRODUCTION (rca-geo → pra-geo)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
cd /home/pierre/dev/geosector/web
|
cd /home/pierre/dev/geosector/web
|
||||||
|
|
||||||
# Vérifier si .env.deploy existe
|
# =====================================
|
||||||
ENV_FILE=".env-deploy-geosector-dev"
|
# Configuration générale
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
# =====================================
|
||||||
echo "Erreur: Fichier $ENV_FILE introuvable!"
|
|
||||||
echo "Veuillez créer ce fichier avec vos informations de connexion."
|
# Paramètre optionnel pour l'environnement cible
|
||||||
exit 1
|
TARGET_ENV=${1:-dev}
|
||||||
|
|
||||||
|
# Configuration SSH
|
||||||
|
HOST_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||||
|
HOST_PORT="22"
|
||||||
|
HOST_USER="root"
|
||||||
|
|
||||||
|
# Configuration des serveurs
|
||||||
|
RCA_HOST="195.154.80.116" # Serveur de recette
|
||||||
|
PRA_HOST="51.159.7.190" # Serveur de production
|
||||||
|
|
||||||
|
# Configuration Incus
|
||||||
|
INCUS_PROJECT="default"
|
||||||
|
WEB_PATH="/var/www/geosector/web"
|
||||||
|
FINAL_OWNER="nginx"
|
||||||
|
FINAL_GROUP="nginx"
|
||||||
|
|
||||||
|
# Configuration de sauvegarde
|
||||||
|
BACKUP_DIR="/data/backup/geosector"
|
||||||
|
|
||||||
|
# Couleurs pour les messages
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Fonctions utilitaires
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
echo_step() {
|
||||||
|
echo -e "${GREEN}==>${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_info() {
|
||||||
|
echo -e "${BLUE}Info:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_warning() {
|
||||||
|
echo -e "${YELLOW}Warning:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo_error() {
|
||||||
|
echo -e "${RED}Error:${NC} $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour créer une sauvegarde locale
|
||||||
|
create_local_backup() {
|
||||||
|
local archive_file=$1
|
||||||
|
local backup_type=$2
|
||||||
|
|
||||||
|
echo_info "Creating backup in ${BACKUP_DIR}..."
|
||||||
|
|
||||||
|
if [ ! -d "${BACKUP_DIR}" ]; then
|
||||||
|
mkdir -p "${BACKUP_DIR}" || echo_warning "Could not create backup directory ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${BACKUP_DIR}" ]; then
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/web-${backup_type}-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
cp "${archive_file}" "${BACKUP_FILE}" && {
|
||||||
|
echo_info "Backup saved to: ${BACKUP_FILE}"
|
||||||
|
echo_info "Backup size: $(du -h "${BACKUP_FILE}" | cut -f1)"
|
||||||
|
|
||||||
|
# Nettoyer les anciens backups (garder les 10 derniers)
|
||||||
|
echo_info "Cleaning old backups (keeping last 10)..."
|
||||||
|
ls -t "${BACKUP_DIR}"/web-${backup_type}-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f && {
|
||||||
|
REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/web-${backup_type}-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
echo_info "Kept ${REMAINING_BACKUPS} backup(s)"
|
||||||
|
}
|
||||||
|
} || echo_warning "Failed to create backup in ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Détermination de la configuration selon l'environnement
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
case $TARGET_ENV in
|
||||||
|
"dev")
|
||||||
|
echo_step "Configuring for LOCAL DEV deployment"
|
||||||
|
SOURCE_TYPE="local_build"
|
||||||
|
DEST_CONTAINER="geo"
|
||||||
|
DEST_HOST="local"
|
||||||
|
ENV_NAME="DEVELOPMENT"
|
||||||
|
;;
|
||||||
|
"rca")
|
||||||
|
echo_step "Configuring for RECETTE delivery"
|
||||||
|
SOURCE_TYPE="local_container"
|
||||||
|
SOURCE_CONTAINER="geo"
|
||||||
|
DEST_CONTAINER="rca-geo"
|
||||||
|
DEST_HOST="${RCA_HOST}"
|
||||||
|
ENV_NAME="RECETTE"
|
||||||
|
;;
|
||||||
|
"pra")
|
||||||
|
echo_step "Configuring for PRODUCTION delivery"
|
||||||
|
SOURCE_TYPE="remote_container"
|
||||||
|
SOURCE_HOST="${RCA_HOST}"
|
||||||
|
SOURCE_CONTAINER="rca-geo"
|
||||||
|
DEST_CONTAINER="pra-geo"
|
||||||
|
DEST_HOST="${PRA_HOST}"
|
||||||
|
ENV_NAME="PRODUCTION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo_error "Unknown environment: $TARGET_ENV. Use 'dev', 'rca' or 'pra'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo_info "Deployment flow: ${ENV_NAME}"
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# Création de l'archive selon la source
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
ARCHIVE_NAME="web-deploy-${TIMESTAMP}.tar.gz"
|
||||||
|
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||||
|
|
||||||
|
if [ "$SOURCE_TYPE" = "local_build" ]; then
|
||||||
|
# DEV: Build Svelte et créer une archive
|
||||||
|
echo_step "Building Svelte app for DEV..."
|
||||||
|
|
||||||
|
# Variables du projet
|
||||||
|
BUILD_DIR="dist"
|
||||||
|
SERVER_DIR="server"
|
||||||
|
LOCAL_DEPLOY_DIR="deploy"
|
||||||
|
|
||||||
|
# Installer les dépendances si nécessaire
|
||||||
|
if [ ! -d "node_modules" ] || [ ! -f "package-lock.json" ]; then
|
||||||
|
echo_info "Installing dependencies..."
|
||||||
|
npm install || echo_error "npm install failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build du frontend principal
|
||||||
|
echo_info "Building frontend..."
|
||||||
|
npm run build || echo_error "Build failed"
|
||||||
|
|
||||||
|
# Vérifier que le build a réussi
|
||||||
|
if [ ! -d "$BUILD_DIR" ]; then
|
||||||
|
echo_error "Build directory not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Préparer le package de déploiement
|
||||||
|
echo_info "Preparing deployment package..."
|
||||||
|
rm -rf $LOCAL_DEPLOY_DIR
|
||||||
|
mkdir -p $LOCAL_DEPLOY_DIR
|
||||||
|
|
||||||
|
# Copier les fichiers frontend
|
||||||
|
cp -r $BUILD_DIR/* $LOCAL_DEPLOY_DIR/
|
||||||
|
|
||||||
|
# Préparer le dossier serveur si nécessaire
|
||||||
|
if [ -d "$SERVER_DIR" ]; then
|
||||||
|
echo_info "Preparing server files..."
|
||||||
|
mkdir -p $LOCAL_DEPLOY_DIR/server
|
||||||
|
cp -r $SERVER_DIR/package.json $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning "package.json not found"
|
||||||
|
cp -r $SERVER_DIR/server.js $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning "server.js not found"
|
||||||
|
cp -r $SERVER_DIR/.env $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning ".env not found"
|
||||||
|
mkdir -p $LOCAL_DEPLOY_DIR/server/logs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Créer l'archive
|
||||||
|
echo_info "Creating archive..."
|
||||||
|
COPYFILE_DISABLE=1 tar --exclude=".*" -czf "${TEMP_ARCHIVE}" -C $LOCAL_DEPLOY_DIR . || echo_error "Failed to create archive"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
||||||
|
|
||||||
|
# Nettoyer
|
||||||
|
rm -rf $LOCAL_DEPLOY_DIR
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# RCA: Créer une archive depuis le container local
|
||||||
|
echo_step "Creating archive from local container ${SOURCE_CONTAINER}..."
|
||||||
|
|
||||||
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch project"
|
||||||
|
|
||||||
|
# Créer l'archive directement depuis le container local
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH} . || echo_error "Failed to create archive from container"
|
||||||
|
|
||||||
|
# Récupérer l'archive depuis le container
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to pull archive from container"
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-rca"
|
||||||
|
|
||||||
|
elif [ "$SOURCE_TYPE" = "remote_container" ]; then
|
||||||
|
# PRA: Créer une archive depuis un container distant
|
||||||
|
echo_step "Creating archive from remote container ${SOURCE_CONTAINER} on ${SOURCE_HOST}..."
|
||||||
|
|
||||||
|
# Créer l'archive sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH} .
|
||||||
|
" || echo_error "Failed to create archive on remote"
|
||||||
|
|
||||||
|
# Extraire l'archive du container vers l'hôte
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} /tmp/${ARCHIVE_NAME} &&
|
||||||
|
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to extract archive from remote container"
|
||||||
|
|
||||||
|
# Copier l'archive vers la machine locale pour backup
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST}:/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to copy archive locally"
|
||||||
|
|
||||||
|
create_local_backup "${TEMP_ARCHIVE}" "to-pra"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Charger les variables depuis .env.deploy
|
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
||||||
echo "Chargement des paramètres de déploiement..."
|
echo_info "Archive size: ${ARCHIVE_SIZE}"
|
||||||
source "$ENV_FILE"
|
|
||||||
|
|
||||||
# Vérifier que les variables nécessaires sont définies
|
# =====================================
|
||||||
if [ -z "$HOST_SSH_HOST" ] || [ -z "$HOST_SSH_USER" ] || [ -z "$CT_NAME" ] || [ -z "$CT_PROJECT_NAME" ]; then
|
# Déploiement selon la destination
|
||||||
echo "Erreur: Variables HOST_SSH_HOST, HOST_SSH_USER, CT_NAME et CT_PROJECT_NAME requises dans $ENV_FILE"
|
# =====================================
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Variables pour les alertes (optionnelles)
|
if [ "$DEST_HOST" = "local" ]; then
|
||||||
ALERT_EMAIL=${ALERT_EMAIL:-""}
|
# Déploiement sur container local (DEV)
|
||||||
DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL:-""}
|
echo_step "Deploying to local container ${DEST_CONTAINER}..."
|
||||||
|
|
||||||
# Utiliser les valeurs par défaut si non définies
|
echo_info "Switching to Incus project ${INCUS_PROJECT}..."
|
||||||
HOST_SSH_PORT=${HOST_SSH_PORT:-22}
|
incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch to project ${INCUS_PROJECT}"
|
||||||
SERVER_PORT=${SERVER_PORT:-3000}
|
|
||||||
ADMIN_PORT=${ADMIN_PORT:-3001}
|
echo_info "Pushing archive to container..."
|
||||||
DOMAIN_NAME=${DOMAIN_NAME:-$CT_IP}
|
incus file push "${TEMP_ARCHIVE}" ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} || echo_error "Failed to push archive to container"
|
||||||
DEPLOY_DIR=${DEPLOY_DIR:-/var/www}
|
|
||||||
APP_NAME=${APP_NAME:-d6soft}
|
echo_info "Preparing deployment directory..."
|
||||||
SUB_DIR=${SUB_DIR:-web}
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${WEB_PATH} || echo_error "Failed to create deployment directory"
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -rf ${WEB_PATH}/* || echo_warning "Could not clean deployment directory"
|
||||||
# Afficher les paramètres
|
|
||||||
echo "=== Paramètres de déploiement ==="
|
echo_info "Extracting archive..."
|
||||||
echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH}/ || echo_error "Failed to extract archive"
|
||||||
echo "Projet Incus: $CT_PROJECT_NAME"
|
|
||||||
echo "Conteneur: $CT_NAME"
|
echo_info "Setting permissions..."
|
||||||
echo "Domaine: $DOMAIN_NAME"
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${WEB_PATH}
|
||||||
echo "Répertoire de déploiement: $DEPLOY_DIR/$APP_NAME/$SUB_DIR"
|
incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type d -exec chmod 755 {} \;
|
||||||
echo "Déploiement du module d'administration: $([ "$DEPLOY_ADMIN" = true ] && echo "Oui" || echo "Non")"
|
incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type f -exec chmod 644 {} \;
|
||||||
echo "Installation des dépendances: $([ "$INSTALL_DEPENDENCIES" = true ] && echo "Oui" || echo "Non")"
|
|
||||||
echo "=================================="
|
# Permissions spéciales pour les dossiers server
|
||||||
|
incus exec ${DEST_CONTAINER} -- sh -c "
|
||||||
# Variables du projet
|
if [ -f ${WEB_PATH}/server/server.js ]; then
|
||||||
BUILD_DIR="dist"
|
chmod +x ${WEB_PATH}/server/server.js
|
||||||
SERVER_DIR="server"
|
fi
|
||||||
LOCAL_DEPLOY_DIR="deploy"
|
if [ -d ${WEB_PATH}/server/logs ]; then
|
||||||
DEPLOY_PACKAGE="$APP_NAME-deploy.tar.gz"
|
chmod 775 ${WEB_PATH}/server/logs
|
||||||
|
fi
|
||||||
# 0. Nettoyer et réinstaller les dépendances si nécessaire
|
" || true
|
||||||
if [ ! -d "node_modules" ] || [ ! -f "package-lock.json" ]; then
|
|
||||||
echo "=== Installation des dépendances ==="
|
# Installer les dépendances du serveur si présent
|
||||||
npm install
|
echo_info "Installing server dependencies if needed..."
|
||||||
fi
|
incus exec ${DEST_CONTAINER} -- sh -c "
|
||||||
|
if [ -d ${WEB_PATH}/server ] && [ -f ${WEB_PATH}/server/package.json ]; then
|
||||||
# 1. Build du frontend principal
|
cd ${WEB_PATH}/server && npm install --production
|
||||||
echo "=== Construction du frontend principal ==="
|
fi
|
||||||
npm run build
|
" || echo_warning "Server dependencies installation skipped"
|
||||||
# Vérifier si le build a réussi
|
|
||||||
BUILD_EXIT_CODE=$?
|
echo_info "Cleaning up..."
|
||||||
if [ $BUILD_EXIT_CODE -ne 0 ] || [ ! -d "$BUILD_DIR" ]; then
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||||
echo "=============================================="
|
|
||||||
echo "ERREUR CRITIQUE: Le build a échoué avec le code $BUILD_EXIT_CODE"
|
|
||||||
echo "=============================================="
|
|
||||||
|
|
||||||
# Envoyer des alertes si configurées
|
|
||||||
if [ ! -z "$ALERT_EMAIL" ]; then
|
|
||||||
echo "Envoi d'une alerte par email à $ALERT_EMAIL..."
|
|
||||||
echo "Erreur de build pour $APP_NAME sur $HOST_SSH_HOST" | mail -s "[ALERTE] Échec de déploiement $APP_NAME" $ALERT_EMAIL
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -z "$DISCORD_WEBHOOK_URL" ]; then
|
|
||||||
echo "Envoi d'une alerte Discord..."
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
-d '{"content":"⚠️ **ALERTE: Échec de déploiement** ⚠️\nLe build de **'"$APP_NAME"'** a échoué avec le code '$BUILD_EXIT_CODE'.\nServeur: '"$HOST_SSH_HOST"'\nDate: '"$(date)"'"}' \
|
|
||||||
$DISCORD_WEBHOOK_URL
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Le déploiement a été interrompu en raison d'erreurs dans le build."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Préparation du package de déploiement
|
|
||||||
echo "=== Préparation du package de déploiement ==="
|
|
||||||
|
|
||||||
# Nettoyer et préparer les dossiers de déploiement
|
|
||||||
rm -rf $LOCAL_DEPLOY_DIR
|
|
||||||
mkdir -p $LOCAL_DEPLOY_DIR
|
|
||||||
|
|
||||||
# Copier les fichiers frontend (build Svelte)
|
|
||||||
cp -r $BUILD_DIR/* $LOCAL_DEPLOY_DIR/
|
|
||||||
|
|
||||||
# Préparer le dossier serveur principal si nécessaire
|
|
||||||
if [ -d "$SERVER_DIR" ]; then
|
|
||||||
echo "Préparation du serveur principal..."
|
|
||||||
mkdir -p $LOCAL_DEPLOY_DIR/server
|
|
||||||
cp -r $SERVER_DIR/package.json $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: package.json du serveur principal non trouvé"
|
|
||||||
cp -r $SERVER_DIR/server.js $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: server.js du serveur principal non trouvé"
|
|
||||||
cp -r $SERVER_DIR/.env $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: .env du serveur principal non trouvé"
|
|
||||||
mkdir -p $LOCAL_DEPLOY_DIR/server/logs
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Créer un fichier tar.gz pour l'envoi
|
|
||||||
echo "Création du package de déploiement..."
|
|
||||||
COPYFILE_DISABLE=1 tar --exclude=".*" -czf $DEPLOY_PACKAGE $LOCAL_DEPLOY_DIR
|
|
||||||
|
|
||||||
# Vérifier que le package a bien été créé
|
|
||||||
if [ ! -f "$DEPLOY_PACKAGE" ]; then
|
|
||||||
echo "ERREUR: Le fichier $DEPLOY_PACKAGE n'a pas été créé."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Taille du package: $(du -h $DEPLOY_PACKAGE | cut -f1)"
|
|
||||||
|
|
||||||
# Définir les options SSH
|
|
||||||
SSH_OPTS="-p $HOST_SSH_PORT"
|
|
||||||
SCP_OPTS="-P $HOST_SSH_PORT"
|
|
||||||
if [ ! -z "$HOST_SSH_KEY" ]; then
|
|
||||||
SSH_OPTS="$SSH_OPTS -i \"$HOST_SSH_KEY\""
|
|
||||||
SCP_OPTS="$SCP_OPTS -i \"$HOST_SSH_KEY\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Copier le package sur le serveur hôte
|
|
||||||
echo "=== Copie des fichiers vers le serveur hôte ==="
|
|
||||||
eval "scp $SCP_OPTS $DEPLOY_PACKAGE $HOST_SSH_USER@$HOST_SSH_HOST:~/"
|
|
||||||
|
|
||||||
# 5. Exécuter les commandes sur l'hôte et le conteneur
|
|
||||||
echo "=== Déploiement sur le conteneur $CT_NAME ==="
|
|
||||||
|
|
||||||
# Vérifier que le fichier est bien arrivé
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"if [ ! -f '$DEPLOY_PACKAGE' ]; then echo 'ERREUR: Fichier non transféré'; exit 1; fi\""
|
|
||||||
|
|
||||||
# Déplacer le fichier vers /tmp
|
|
||||||
echo "Déplacement du package vers /tmp..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"cp $DEPLOY_PACKAGE /tmp/\""
|
|
||||||
|
|
||||||
# Sélectionner le projet Incus
|
|
||||||
echo "Sélection du projet Incus $CT_PROJECT_NAME..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus project switch $CT_PROJECT_NAME\""
|
|
||||||
|
|
||||||
# Transférer le package vers le conteneur
|
|
||||||
echo "Transfert du package vers le conteneur..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus file push /tmp/$DEPLOY_PACKAGE $CT_NAME/$DEPLOY_DIR/\""
|
|
||||||
|
|
||||||
# Créer le répertoire de déploiement dans le conteneur
|
|
||||||
echo "Création du répertoire de déploiement dans le conteneur..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- mkdir -p $DEPLOY_DIR/$APP_NAME/$SUB_DIR\""
|
|
||||||
|
|
||||||
# Extraire le package
|
|
||||||
echo "Extraction du package..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- tar -xzf $DEPLOY_DIR/$DEPLOY_PACKAGE -C $DEPLOY_DIR/$APP_NAME/$SUB_DIR --strip-components=1\""
|
|
||||||
|
|
||||||
# Installer les dépendances du serveur principal (si présent)
|
|
||||||
echo "Installation des dépendances du serveur principal (si présent)..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server ] && [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/package.json ]; then cd $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server && npm install --production; else echo \"Dossier serveur ou package.json non trouvé, cette étape est ignorée\"; fi'\""
|
|
||||||
|
|
||||||
# Nettoyer les fichiers macOS
|
|
||||||
echo "Nettoyage des fichiers macOS..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -name \"._*\" -type f -delete 2>/dev/null || true'\""
|
|
||||||
|
|
||||||
# Configurer les permissions
|
|
||||||
echo "Configuration des permissions..."
|
|
||||||
# Vérifier si l'utilisateur et le groupe www-data existent, sinon les créer
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'getent group www-data > /dev/null || addgroup -S www-data; getent passwd www-data > /dev/null || adduser -S -D -H -h /var/www -s /sbin/nologin -G www-data -g www-data www-data'\""
|
|
||||||
|
|
||||||
# Appliquer les permissions sur tous les fichiers
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'chown -R www-data:www-data $DEPLOY_DIR/$APP_NAME/$SUB_DIR && \
|
|
||||||
find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -type d -exec chmod 755 {} \\; && \
|
|
||||||
find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -type f -exec chmod 644 {} \\; && \
|
|
||||||
if [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/server.js ]; then chmod +x $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/server.js; fi && \
|
|
||||||
if [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/server.js ]; then chmod +x $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/server.js; fi && \
|
|
||||||
if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/logs ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/logs; fi && \
|
|
||||||
if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/logs ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/logs; fi && \
|
|
||||||
if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/db ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/db; fi'\""
|
|
||||||
|
|
||||||
# Nettoyer les fichiers temporaires
|
|
||||||
echo "Nettoyage des fichiers temporaires..."
|
|
||||||
eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- rm -f $DEPLOY_DIR/$DEPLOY_PACKAGE && rm -f /tmp/$DEPLOY_PACKAGE && rm -f $DEPLOY_PACKAGE\""
|
|
||||||
|
|
||||||
echo "==================================================="
|
|
||||||
echo "Déploiement terminé avec succès !"
|
|
||||||
echo "==================================================="
|
|
||||||
echo "Votre site $APP_NAME est maintenant déployé dans le conteneur $CT_NAME."
|
|
||||||
echo "Chemin de déploiement: $DEPLOY_DIR/$APP_NAME/$SUB_DIR"
|
|
||||||
|
|
||||||
# Afficher le statut du déploiement
|
|
||||||
if [ -d "$SERVER_DIR" ]; then
|
|
||||||
echo "✅ Le service du site principal a été configuré et démarré."
|
|
||||||
else
|
else
|
||||||
echo "ℹ️ Aucun service principal n'a été configuré (le site est statique)."
|
# Déploiement sur container distant (RCA ou PRA)
|
||||||
|
echo_step "Deploying to remote container ${DEST_CONTAINER} on ${DEST_HOST}..."
|
||||||
|
|
||||||
|
# Créer une sauvegarde sur le serveur de destination
|
||||||
|
BACKUP_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
REMOTE_BACKUP_DIR="${WEB_PATH}_backup_${BACKUP_TIMESTAMP}"
|
||||||
|
|
||||||
|
echo_info "Creating backup on destination..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- test -d ${WEB_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- cp -r ${WEB_PATH} ${REMOTE_BACKUP_DIR} &&
|
||||||
|
echo 'Backup created: ${REMOTE_BACKUP_DIR}'
|
||||||
|
" || echo_warning "No existing installation to backup"
|
||||||
|
|
||||||
|
# Transférer l'archive vers le serveur de destination
|
||||||
|
echo_info "Transferring archive to ${DEST_HOST}..."
|
||||||
|
|
||||||
|
if [ "$SOURCE_TYPE" = "local_container" ]; then
|
||||||
|
# Pour RCA: copier depuis local vers distant
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} ${TEMP_ARCHIVE} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to destination"
|
||||||
|
else
|
||||||
|
# Pour PRA: copier de serveur à serveur
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||||
|
scp -i ${HOST_KEY} -P ${HOST_PORT} /tmp/${ARCHIVE_NAME} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Failed to transfer archive between servers"
|
||||||
|
|
||||||
|
# Nettoyer sur le serveur source
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "rm -f /tmp/${ARCHIVE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Déployer sur le container de destination
|
||||||
|
echo_info "Extracting on destination container..."
|
||||||
|
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Pousser l'archive dans le container
|
||||||
|
incus project switch ${INCUS_PROJECT} &&
|
||||||
|
incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} &&
|
||||||
|
|
||||||
|
# Nettoyer et recréer le dossier
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -rf ${WEB_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- mkdir -p ${WEB_PATH} &&
|
||||||
|
|
||||||
|
# Extraire l'archive
|
||||||
|
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH}/ &&
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${WEB_PATH} &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type d -exec chmod 755 {} \; &&
|
||||||
|
incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type f -exec chmod 644 {} \; &&
|
||||||
|
|
||||||
|
# Permissions spéciales pour server
|
||||||
|
incus exec ${DEST_CONTAINER} -- sh -c '
|
||||||
|
if [ -f ${WEB_PATH}/server/server.js ]; then
|
||||||
|
chmod +x ${WEB_PATH}/server/server.js
|
||||||
|
fi
|
||||||
|
if [ -d ${WEB_PATH}/server/logs ]; then
|
||||||
|
chmod 775 ${WEB_PATH}/server/logs
|
||||||
|
fi
|
||||||
|
' || true &&
|
||||||
|
|
||||||
|
# Installer les dépendances du serveur si présent
|
||||||
|
incus exec ${DEST_CONTAINER} -- sh -c '
|
||||||
|
if [ -d ${WEB_PATH}/server ] && [ -f ${WEB_PATH}/server/package.json ]; then
|
||||||
|
cd ${WEB_PATH}/server && npm install --production
|
||||||
|
fi
|
||||||
|
' || echo 'Server dependencies skipped' &&
|
||||||
|
|
||||||
|
# Nettoyage
|
||||||
|
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} &&
|
||||||
|
rm -f /tmp/${ARCHIVE_NAME}
|
||||||
|
" || echo_error "Deployment failed on destination"
|
||||||
|
|
||||||
|
echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
# Nettoyage local
|
||||||
echo "Pour configurer nginx sur le serveur, connectez-vous et exécutez :"
|
rm -f "${TEMP_ARCHIVE}"
|
||||||
echo "ssh $HOST_SSH_USER@$HOST_SSH_HOST"
|
|
||||||
echo "sudo incus exec $CT_NAME bash"
|
# =====================================
|
||||||
echo "echo "rc-service nginx restart"
|
# Résumé final
|
||||||
echo "==================================================="
|
# =====================================
|
||||||
|
|
||||||
|
echo_step "Deployment completed successfully!"
|
||||||
|
echo_info "Environment: ${ENV_NAME}"
|
||||||
|
|
||||||
|
if [ "$TARGET_ENV" = "dev" ]; then
|
||||||
|
echo_info "Built and deployed Svelte web app to container ${DEST_CONTAINER}"
|
||||||
|
elif [ "$TARGET_ENV" = "rca" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} (local) to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
elif [ "$TARGET_ENV" = "pra" ]; then
|
||||||
|
echo_info "Delivered from ${SOURCE_CONTAINER} on ${SOURCE_HOST} to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo_info "Deployment completed at: $(date)"
|
||||||
|
|
||||||
|
# Journaliser le déploiement
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - Web app deployed to ${ENV_NAME} (${DEST_CONTAINER})" >> ~/.geo_deploy_history
|
||||||
Reference in New Issue
Block a user