feat: Release v3.1.6 - Amélioration complète des flux de passages

- Optimisation des listes de passages (user/admin)
- Amélioration du flux de création avec validation temps réel
- Amélioration du flux de consultation avec export multi-formats
- Amélioration du flux de modification avec suivi des changements
- Ajout de la génération PDF pour les reçus
- Migration de la structure des uploads
- Implémentation de la file d'attente d'emails
- Ajout des permissions de suppression de passages
- Corrections de bugs et optimisations performances

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-21 17:57:27 +02:00
parent cdb676ea71
commit 6d41a1274f
24 changed files with 4605 additions and 1082 deletions

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env php
<?php
/**
* Script CRON pour traiter la queue d'emails
* Envoie les emails en attente dans la table email_queue
*
* À exécuter toutes les 5 minutes via crontab :
* Exemple: [asterisk]/5 [asterisk] [asterisk] [asterisk] [asterisk] /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php
*/
declare(strict_types=1);
// Configuration
define('MAX_ATTEMPTS', 3);
define('BATCH_SIZE', 50);
define('LOCK_FILE', '/tmp/process_email_queue.lock');
// Empêcher l'exécution multiple simultanée
if (file_exists(LOCK_FILE)) {
$lockTime = filemtime(LOCK_FILE);
// Si le lock a plus de 30 minutes, on le supprime (processus probablement bloqué)
if (time() - $lockTime > 1800) {
unlink(LOCK_FILE);
} else {
die("Le processus est déjà en cours d'exécution\n");
}
}
// Créer le fichier de lock
file_put_contents(LOCK_FILE, getmypid());
// Enregistrer un handler pour supprimer le lock en cas d'arrêt
register_shutdown_function(function() {
if (file_exists(LOCK_FILE)) {
unlink(LOCK_FILE);
}
});
// Simuler l'environnement web pour AppConfig en CLI
if (php_sapi_name() === 'cli') {
// Détecter l'environnement basé sur le hostname ou un paramètre
$hostname = gethostname();
if (strpos($hostname, 'prod') !== false) {
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
} else {
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
}
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
// Définir getallheaders si elle n'existe pas (CLI)
if (!function_exists('getallheaders')) {
function getallheaders() {
return [];
}
}
}
// Chargement de l'environnement
require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../src/Config/AppConfig.php';
require_once __DIR__ . '/../../src/Core/Database.php';
require_once __DIR__ . '/../../src/Services/LogService.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
try {
// Initialisation de la configuration
$appConfig = AppConfig::getInstance();
$dbConfig = $appConfig->getDatabaseConfig();
// Initialiser la base de données avec la configuration
Database::init($dbConfig);
$db = Database::getInstance();
LogService::log('Démarrage du processeur de queue d\'emails', [
'level' => 'info',
'script' => 'process_email_queue.php'
]);
// Récupérer les emails en attente
$stmt = $db->prepare('
SELECT id, fk_pass, to_email, subject, body, headers, attempts
FROM email_queue
WHERE status = ? AND attempts < ?
ORDER BY created_at ASC
LIMIT ?
');
$stmt->execute(['pending', MAX_ATTEMPTS, BATCH_SIZE]);
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($emails)) {
LogService::log('Aucun email en attente dans la queue', [
'level' => 'debug'
]);
exit(0);
}
LogService::log('Emails à traiter', [
'level' => 'info',
'count' => count($emails)
]);
// Configuration SMTP
$smtpConfig = $appConfig->getSmtpConfig();
$emailConfig = $appConfig->getEmailConfig();
$successCount = 0;
$failureCount = 0;
// Traiter chaque email
foreach ($emails as $emailData) {
$emailId = $emailData['id'];
$passageId = $emailData['fk_pass'];
try {
// Incrémenter le compteur de tentatives
$stmt = $db->prepare('UPDATE email_queue SET attempts = attempts + 1 WHERE id = ?');
$stmt->execute([$emailId]);
// Créer l'instance PHPMailer
$mail = new PHPMailer(true);
// Configuration du serveur SMTP
$mail->isSMTP();
$mail->Host = $smtpConfig['host'];
$mail->SMTPAuth = $smtpConfig['auth'] ?? true;
$mail->Username = $smtpConfig['user'];
$mail->Password = $smtpConfig['pass'];
$mail->SMTPSecure = $smtpConfig['secure'];
$mail->Port = $smtpConfig['port'];
$mail->CharSet = 'UTF-8';
// Configuration de l'expéditeur
$fromName = 'Amicale Sapeurs-Pompiers'; // Nom par défaut
$mail->setFrom($emailConfig['from'], $fromName);
// Destinataire
$mail->addAddress($emailData['to_email']);
// Sujet
$mail->Subject = $emailData['subject'];
// Headers personnalisés si présents
if (!empty($emailData['headers'])) {
// Les headers contiennent déjà les informations MIME pour la pièce jointe
// On doit extraire le boundary et reconstruire le message
if (preg_match('/boundary="([^"]+)"/', $emailData['headers'], $matches)) {
$boundary = $matches[1];
// Le body contient déjà le message complet avec pièce jointe
$mail->isHTML(false);
$mail->Body = $emailData['body'];
// Extraire le contenu HTML et la pièce jointe
$parts = explode("--$boundary", $emailData['body']);
foreach ($parts as $part) {
if (strpos($part, 'Content-Type: text/html') !== false) {
// Extraire le contenu HTML
$htmlContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
$mail->isHTML(true);
$mail->Body = $htmlContent;
} elseif (strpos($part, 'Content-Type: application/pdf') !== false) {
// Extraire le PDF encodé en base64
if (preg_match('/filename="([^"]+)"/', $part, $fileMatches)) {
$filename = $fileMatches[1];
$pdfContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
// Supprimer les retours à la ligne du base64
$pdfContent = str_replace(["\r", "\n"], '', $pdfContent);
// Ajouter la pièce jointe
$mail->addStringAttachment(
base64_decode($pdfContent),
$filename,
'base64',
'application/pdf'
);
}
}
}
}
} else {
// Email simple sans pièce jointe
$mail->isHTML(true);
$mail->Body = $emailData['body'];
}
// Ajouter une copie si configuré
if (!empty($emailConfig['contact'])) {
$mail->addBCC($emailConfig['contact']);
}
// Envoyer l'email
if ($mail->send()) {
// Marquer comme envoyé
$stmt = $db->prepare('
UPDATE email_queue
SET status = ?, sent_at = NOW()
WHERE id = ?
');
$stmt->execute(['sent', $emailId]);
// Mettre à jour le passage si nécessaire
if ($passageId > 0) {
$stmt = $db->prepare('
UPDATE ope_pass
SET date_sent_recu = NOW(), chk_email_sent = 1
WHERE id = ?
');
$stmt->execute([$passageId]);
}
$successCount++;
LogService::log('Email envoyé avec succès', [
'level' => 'info',
'emailId' => $emailId,
'passageId' => $passageId,
'to' => $emailData['to_email']
]);
} else {
throw new Exception('Échec de l\'envoi');
}
} catch (Exception $e) {
$failureCount++;
LogService::log('Erreur lors de l\'envoi de l\'email', [
'level' => 'error',
'emailId' => $emailId,
'passageId' => $passageId,
'error' => $e->getMessage(),
'attempts' => $emailData['attempts'] + 1
]);
// Si on a atteint le nombre max de tentatives, marquer comme échoué
if ($emailData['attempts'] + 1 >= MAX_ATTEMPTS) {
$stmt = $db->prepare('
UPDATE email_queue
SET status = ?, error_message = ?
WHERE id = ?
');
$stmt->execute(['failed', $e->getMessage(), $emailId]);
LogService::log('Email marqué comme échoué après ' . MAX_ATTEMPTS . ' tentatives', [
'level' => 'warning',
'emailId' => $emailId,
'passageId' => $passageId
]);
}
}
// Pause courte entre chaque email pour éviter la surcharge
usleep(500000); // 0.5 seconde
}
LogService::log('Traitement de la queue terminé', [
'level' => 'info',
'success' => $successCount,
'failures' => $failureCount,
'total' => count($emails)
]);
} catch (Exception $e) {
LogService::log('Erreur fatale dans le processeur de queue', [
'level' => 'critical',
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Supprimer le lock en cas d'erreur
if (file_exists(LOCK_FILE)) {
unlink(LOCK_FILE);
}
exit(1);
}
// Nettoyer les vieux emails traités (optionnel)
try {
// Supprimer les emails envoyés de plus de 30 jours
$stmt = $db->prepare('
DELETE FROM email_queue
WHERE status = ? AND sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
');
$stmt->execute(['sent']);
$deleted = $stmt->rowCount();
if ($deleted > 0) {
LogService::log('Nettoyage des anciens emails', [
'level' => 'info',
'deleted' => $deleted
]);
}
} catch (Exception $e) {
LogService::log('Erreur lors du nettoyage des anciens emails', [
'level' => 'warning',
'error' => $e->getMessage()
]);
}
// Supprimer le lock
if (file_exists(LOCK_FILE)) {
unlink(LOCK_FILE);
}
echo "Traitement terminé : $successCount envoyés, $failureCount échecs\n";
exit(0);

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Script wrapper pour process_email_queue avec logs journaliers
# Crée automatiquement un nouveau fichier log chaque jour
# Configuration
LOG_DIR="/var/www/geosector/api/logs"
LOG_FILE="$LOG_DIR/email_queue_$(date +%Y%m%d).log"
PHP_SCRIPT="/var/www/geosector/api/scripts/cron/process_email_queue.php"
# Créer le répertoire de logs s'il n'existe pas
mkdir -p "$LOG_DIR"
# Ajouter un timestamp au début de l'exécution
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Démarrage du processeur de queue d'emails" >> "$LOG_FILE"
# Exécuter le script PHP
/usr/bin/php "$PHP_SCRIPT" >> "$LOG_FILE" 2>&1
# Ajouter le statut de sortie
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (succès)" >> "$LOG_FILE"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (erreur: $EXIT_CODE)" >> "$LOG_FILE"
fi
# Nettoyer les logs de plus de 30 jours
find "$LOG_DIR" -name "email_queue_*.log" -type f -mtime +30 -delete 2>/dev/null
exit $EXIT_CODE

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env php
<?php
/**
* Script de test pour vérifier le processeur de queue d'emails
* Affiche les emails en attente sans les envoyer
*/
declare(strict_types=1);
// Simuler l'environnement web pour AppConfig en CLI
if (php_sapi_name() === 'cli') {
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'dapp.geosector.fr'; // DVA par défaut
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
// Définir getallheaders si elle n'existe pas (CLI)
if (!function_exists('getallheaders')) {
function getallheaders() {
return [];
}
}
}
require_once __DIR__ . '/../../src/Core/Database.php';
require_once __DIR__ . '/../../src/Config/AppConfig.php';
try {
// Initialiser la configuration
$appConfig = AppConfig::getInstance();
$dbConfig = $appConfig->getDatabaseConfig();
// Initialiser la base de données avec la configuration
Database::init($dbConfig);
$db = Database::getInstance();
echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n";
// Statistiques générales
$stmt = $db->query('
SELECT
status,
COUNT(*) as count,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM email_queue
GROUP BY status
');
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "STATISTIQUES:\n";
echo "-------------\n";
foreach ($stats as $stat) {
echo sprintf(
"Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n",
$stat['status'],
$stat['count'],
$stat['oldest'] ?? 'N/A',
$stat['newest'] ?? 'N/A'
);
}
echo "\n";
// Emails en attente
$stmt = $db->prepare('
SELECT
eq.id,
eq.fk_pass,
eq.to_email,
eq.subject,
eq.created_at,
eq.attempts,
eq.status,
p.fk_type,
p.montant,
p.nom_recu
FROM email_queue eq
LEFT JOIN ope_pass p ON eq.fk_pass = p.id
WHERE eq.status = ?
ORDER BY eq.created_at DESC
LIMIT 10
');
$stmt->execute(['pending']);
$pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($pendingEmails)) {
echo "Aucun email en attente.\n";
} else {
echo "EMAILS EN ATTENTE (10 plus récents):\n";
echo "------------------------------------\n";
foreach ($pendingEmails as $email) {
echo sprintf(
"ID: %d | Passage: %d | Destinataire: %s\n",
$email['id'],
$email['fk_pass'],
$email['to_email']
);
echo sprintf(
" Sujet: %s\n",
$email['subject']
);
echo sprintf(
" Créé le: %s | Tentatives: %d\n",
$email['created_at'],
$email['attempts']
);
if ($email['fk_pass'] > 0) {
echo sprintf(
" Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n",
$email['fk_type'] == 1 ? 'DON' : 'Autre',
$email['montant'] ?? 0,
$email['nom_recu'] ?? 'Non généré'
);
}
echo "---\n";
}
}
// Emails échoués
$stmt = $db->prepare('
SELECT
id,
fk_pass,
to_email,
subject,
created_at,
attempts,
error_message
FROM email_queue
WHERE status = ?
ORDER BY created_at DESC
LIMIT 5
');
$stmt->execute(['failed']);
$failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($failedEmails)) {
echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n";
echo "--------------------------------\n";
foreach ($failedEmails as $email) {
echo sprintf(
"ID: %d | Passage: %d | Destinataire: %s\n",
$email['id'],
$email['fk_pass'],
$email['to_email']
);
echo sprintf(
" Sujet: %s\n",
$email['subject']
);
echo sprintf(
" Tentatives: %d | Erreur: %s\n",
$email['attempts'],
$email['error_message'] ?? 'Non spécifiée'
);
echo "---\n";
}
}
// Vérifier la configuration SMTP
echo "\nCONFIGURATION SMTP:\n";
echo "-------------------\n";
$smtpConfig = $appConfig->getSmtpConfig();
$emailConfig = $appConfig->getEmailConfig();
echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n";
echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n";
echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n";
echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n";
echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n";
echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n";
echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n";
echo "\n=== FIN DU TEST ===\n";
} catch (Exception $e) {
echo "ERREUR: " . $e->getMessage() . "\n";
exit(1);
}
exit(0);

View File

@@ -0,0 +1,298 @@
#!/usr/bin/env php
<?php
/**
* Script de migration de l'arborescence des uploads
* Réorganise les fichiers existants vers la nouvelle structure simplifiée
*
* Ancienne structure : uploads/entites/{id}/* et uploads/{id}/*
* Nouvelle structure : uploads/{id}/*
*
* Usage: php scripts/migrate_uploads_structure.php [--dry-run]
*/
declare(strict_types=1);
// Chemin de base des uploads
const BASE_PATH = '/var/www/geosector/api/uploads';
const LOG_FILE = '/var/www/geosector/api/logs/migration_uploads_' . date('Ymd_His') . '.log';
// Mode dry-run (simulation sans modification)
$dryRun = in_array('--dry-run', $argv);
// Fonction pour logger
function logMessage(string $message, string $level = 'INFO'): void {
$timestamp = date('Y-m-d H:i:s');
$log = "[$timestamp] [$level] $message" . PHP_EOL;
echo $log;
if (!$GLOBALS['dryRun']) {
file_put_contents(LOG_FILE, $log, FILE_APPEND);
}
}
// Fonction pour déplacer un fichier ou dossier
function moveItem(string $source, string $destination): bool {
global $dryRun;
if (!file_exists($source)) {
logMessage("Source n'existe pas: $source", 'WARNING');
return false;
}
// Créer le dossier de destination si nécessaire
$destDir = dirname($destination);
if (!is_dir($destDir)) {
logMessage("Création du dossier: $destDir");
if (!$dryRun) {
mkdir($destDir, 0775, true);
chown($destDir, 'nginx');
chgrp($destDir, 'nobody');
}
}
// Déplacer l'élément
logMessage("Déplacement: $source -> $destination");
if (!$dryRun) {
if (is_dir($source)) {
// Pour un dossier, utiliser rename
return rename($source, $destination);
} else {
// Pour un fichier
return rename($source, $destination);
}
}
return true;
}
// Fonction pour copier récursivement un dossier
function copyDirectory(string $source, string $dest): bool {
global $dryRun;
if (!is_dir($source)) {
return false;
}
if (!$dryRun) {
if (!is_dir($dest)) {
mkdir($dest, 0775, true);
chown($dest, 'nginx');
chgrp($dest, 'nobody');
}
}
$dir = opendir($source);
while (($file = readdir($dir)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$srcPath = "$source/$file";
$destPath = "$dest/$file";
if (is_dir($srcPath)) {
copyDirectory($srcPath, $destPath);
} else {
logMessage("Copie: $srcPath -> $destPath");
if (!$dryRun) {
copy($srcPath, $destPath);
chmod($destPath, 0664);
chown($destPath, 'nginx');
chgrp($destPath, 'nobody');
}
}
}
closedir($dir);
return true;
}
// Fonction principale de migration
function migrateUploads(): void {
global $dryRun;
logMessage("=== Début de la migration des uploads ===");
logMessage($dryRun ? "MODE DRY-RUN (simulation)" : "MODE RÉEL (modifications effectives)");
// 1. Migrer uploads/entites/* vers uploads/*
$entitesPath = BASE_PATH . '/entites';
if (is_dir($entitesPath)) {
logMessage("Traitement du dossier entites/");
$entites = scandir($entitesPath);
foreach ($entites as $entiteId) {
if ($entiteId === '.' || $entiteId === '..') continue;
$oldPath = "$entitesPath/$entiteId";
$newPath = BASE_PATH . "/$entiteId";
if (!is_dir($oldPath)) continue;
logMessage("Migration entité $entiteId");
// Si le dossier destination existe déjà, fusionner
if (is_dir($newPath)) {
logMessage("Le dossier $entiteId existe déjà à la racine, fusion nécessaire", 'INFO');
// Migrer les sous-dossiers
$subDirs = scandir($oldPath);
foreach ($subDirs as $subDir) {
if ($subDir === '.' || $subDir === '..') continue;
$oldSubPath = "$oldPath/$subDir";
$newSubPath = "$newPath/$subDir";
if ($subDir === 'operations') {
// Traiter spécialement le dossier operations
migrateOperations($oldSubPath, $newSubPath);
} else {
// Pour logo et recus, déplacer directement
if (!is_dir($newSubPath)) {
moveItem($oldSubPath, $newSubPath);
} else {
logMessage("Le dossier $newSubPath existe déjà, fusion du contenu");
copyDirectory($oldSubPath, $newSubPath);
if (!$dryRun) {
// Supprimer l'ancien après copie
exec("rm -rf " . escapeshellarg($oldSubPath));
}
}
}
}
} else {
// Déplacer simplement le dossier entier
moveItem($oldPath, $newPath);
}
}
// Supprimer le dossier entites vide
if (!$dryRun) {
if (count(scandir($entitesPath)) === 2) { // Seulement . et ..
rmdir($entitesPath);
logMessage("Suppression du dossier entites/ vide");
}
}
}
// 2. Nettoyer la structure des dossiers operations
logMessage("Nettoyage de la structure des dossiers operations");
cleanupOperationsStructure();
logMessage("=== Migration terminée ===");
if (!$dryRun) {
logMessage("Logs sauvegardés dans: " . LOG_FILE);
}
}
// Fonction pour migrer le dossier operations avec simplification
function migrateOperations(string $oldPath, string $newPath): void {
global $dryRun;
if (!is_dir($oldPath)) return;
logMessage("Migration du dossier operations: $oldPath");
if (!$dryRun && !is_dir($newPath)) {
mkdir($newPath, 0775, true);
chown($newPath, 'nginx');
chgrp($newPath, 'nobody');
}
$operations = scandir($oldPath);
foreach ($operations as $opId) {
if ($opId === '.' || $opId === '..') continue;
$oldOpPath = "$oldPath/$opId";
$newOpPath = "$newPath/$opId";
// Simplifier la structure: déplacer les xlsx directement dans operations/{id}/
if (is_dir("$oldOpPath/documents/exports/excel")) {
$excelPath = "$oldOpPath/documents/exports/excel";
$files = scandir($excelPath);
foreach ($files as $file) {
if ($file === '.' || $file === '..' || !str_ends_with($file, '.xlsx')) continue;
$oldFilePath = "$excelPath/$file";
$newFilePath = "$newOpPath/$file";
logMessage("Déplacement Excel: $oldFilePath -> $newFilePath");
if (!$dryRun) {
if (!is_dir($newOpPath)) {
mkdir($newOpPath, 0775, true);
chown($newOpPath, 'nginx');
chgrp($newOpPath, 'nobody');
}
rename($oldFilePath, $newFilePath);
chmod($newFilePath, 0664);
chown($newFilePath, 'nginx');
chgrp($newFilePath, 'nobody');
}
}
}
}
}
// Fonction pour nettoyer la structure après migration
function cleanupOperationsStructure(): void {
global $dryRun;
$uploadsDir = BASE_PATH;
$entites = scandir($uploadsDir);
foreach ($entites as $entiteId) {
if ($entiteId === '.' || $entiteId === '..' || $entiteId === 'entites') continue;
$operationsPath = "$uploadsDir/$entiteId/operations";
if (!is_dir($operationsPath)) continue;
$operations = scandir($operationsPath);
foreach ($operations as $opId) {
if ($opId === '.' || $opId === '..') continue;
$opPath = "$operationsPath/$opId";
// Supprimer l'ancienne structure documents/exports/excel si elle est vide
$oldStructure = "$opPath/documents";
if (is_dir($oldStructure)) {
logMessage("Suppression de l'ancienne structure: $oldStructure");
if (!$dryRun) {
exec("rm -rf " . escapeshellarg($oldStructure));
}
}
}
}
}
// Vérifier les permissions
if (!is_dir(BASE_PATH)) {
die("ERREUR: Le dossier " . BASE_PATH . " n'existe pas\n");
}
if (!is_writable(BASE_PATH) && !$dryRun) {
die("ERREUR: Le dossier " . BASE_PATH . " n'est pas accessible en écriture\n");
}
// Lancer la migration
try {
migrateUploads();
if ($dryRun) {
echo "\n";
echo "========================================\n";
echo "SIMULATION TERMINÉE\n";
echo "Pour exécuter réellement la migration:\n";
echo "php " . $argv[0] . "\n";
echo "========================================\n";
} else {
echo "\n";
echo "========================================\n";
echo "MIGRATION TERMINÉE AVEC SUCCÈS\n";
echo "Vérifiez les logs: " . LOG_FILE . "\n";
echo "========================================\n";
}
} catch (Exception $e) {
logMessage("ERREUR FATALE: " . $e->getMessage(), 'ERROR');
exit(1);
}

View File

@@ -0,0 +1,22 @@
-- Script de migration pour ajouter le champ chk_user_delete_pass
-- Ce champ permet aux administrateurs d'autoriser ou non leurs membres à supprimer des passages
-- Date : 2025-08-20
-- À exécuter sur DVA, REC et PROD
-- Ajouter le champ chk_user_delete_pass s'il n'existe pas
ALTER TABLE `entites`
ADD COLUMN IF NOT EXISTS `chk_user_delete_pass` tinyint(1) unsigned NOT NULL DEFAULT 0
COMMENT 'Autoriser les membres à supprimer des passages (1) ou non (0)'
AFTER `chk_username_manuel`;
-- Vérifier l'ajout
SELECT
COLUMN_NAME,
DATA_TYPE,
COLUMN_DEFAULT,
IS_NULLABLE,
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'entites'
AND COLUMN_NAME = 'chk_user_delete_pass';

View File

@@ -0,0 +1,22 @@
-- Migration pour ajouter les champs manquants à la table email_queue
-- À exécuter sur DVA, REC et PROD
-- Ajouter le champ sent_at s'il n'existe pas
ALTER TABLE `email_queue`
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL
COMMENT 'Date/heure d\'envoi effectif de l\'email'
AFTER `status`;
-- Ajouter le champ error_message s'il n'existe pas
ALTER TABLE `email_queue`
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL
COMMENT 'Message d\'erreur en cas d\'échec'
AFTER `attempts`;
-- Ajouter un index sur le status pour optimiser les requêtes
ALTER TABLE `email_queue`
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
-- Ajouter un index sur sent_at pour le nettoyage automatique
ALTER TABLE `email_queue`
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);