feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API

- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-10-05 20:11:15 +02:00
parent 2786252307
commit 570a1fa1f0
212 changed files with 24275 additions and 11321 deletions

View File

@@ -544,24 +544,85 @@ class SectorController
$stmt->execute($params);
}
// Gestion des membres
if (isset($data['membres'])) {
// Gestion des membres (reçus comme 'users' depuis Flutter)
if (isset($data['users'])) {
$this->logService->info('[UPDATE USERS] Début modification des membres', [
'sector_id' => $id,
'users_demandes' => $data['users'],
'nb_users' => count($data['users'])
]);
// Récupérer l'opération du secteur pour l'INSERT
$opQuery = "SELECT fk_operation FROM ope_sectors WHERE id = :sector_id";
$this->logService->info('[UPDATE USERS] SQL - Récupération fk_operation', [
'query' => $opQuery,
'params' => ['sector_id' => $id]
]);
$opStmt = $this->db->prepare($opQuery);
$opStmt->execute(['sector_id' => $id]);
$operationId = $opStmt->fetch()['fk_operation'];
$this->logService->info('[UPDATE USERS] fk_operation récupéré', [
'operation_id' => $operationId
]);
// Supprimer les affectations existantes
$deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id";
$this->logService->info('[UPDATE USERS] SQL - Suppression des anciens membres', [
'query' => $deleteQuery,
'params' => ['sector_id' => $id]
]);
$deleteStmt = $this->db->prepare($deleteQuery);
$deleteStmt->execute(['sector_id' => $id]);
$deletedCount = $deleteStmt->rowCount();
$this->logService->info('[UPDATE USERS] Membres supprimés', [
'nb_deleted' => $deletedCount
]);
// Ajouter les nouvelles affectations
if (!empty($data['membres'])) {
$insertQuery = "INSERT INTO ope_users_sectors (fk_user, fk_sector) VALUES (:user_id, :sector_id)";
if (!empty($data['users'])) {
$insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, fk_user_creat, chk_active)
VALUES (:operation_id, :user_id, :sector_id, NOW(), :user_creat, 1)";
$this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [
'query' => $insertQuery
]);
$insertStmt = $this->db->prepare($insertQuery);
foreach ($data['membres'] as $memberId) {
$insertStmt->execute([
'user_id' => $memberId,
'sector_id' => $id
]);
$insertedUsers = [];
$failedUsers = [];
foreach ($data['users'] as $memberId) {
try {
$params = [
'operation_id' => $operationId,
'user_id' => $memberId,
'sector_id' => $id,
'user_creat' => $_SESSION['user_id'] ?? null
];
$this->logService->info('[UPDATE USERS] SQL - INSERT user', [
'params' => $params
]);
$insertStmt->execute($params);
$insertedUsers[] = $memberId;
$this->logService->info('[UPDATE USERS] User inséré avec succès', [
'user_id' => $memberId
]);
} catch (\PDOException $e) {
$failedUsers[] = $memberId;
$this->logService->warning('[UPDATE USERS] ERREUR insertion user', [
'sector_id' => $id,
'user_id' => $memberId,
'error' => $e->getMessage(),
'error_code' => $e->getCode()
]);
}
}
$this->logService->info('[UPDATE USERS] Résultat des insertions', [
'users_demandes' => $data['users'],
'users_inseres' => $insertedUsers,
'users_echoues' => $failedUsers,
'nb_succes' => count($insertedUsers),
'nb_echecs' => count($failedUsers)
]);
}
}
@@ -651,7 +712,8 @@ class SectorController
$this->logService->info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]);
$passageCounters = $this->updatePassagesForSector($id, $data['sector']);
}
// Commit des modifications (users et/ou secteur)
$this->db->commit();
// Récupérer le secteur mis à jour
@@ -711,14 +773,29 @@ class SectorController
$passagesDecrypted[] = $passage;
}
// Récupérer les users affectés
// Récupérer les users affectés (avec READ UNCOMMITTED pour forcer la lecture des données fraîches)
$usersQuery = "SELECT u.id, u.first_name, u.sect_name, u.encrypted_name, ous.fk_sector
FROM ope_users_sectors ous
JOIN users u ON ous.fk_user = u.id
WHERE ous.fk_sector = :sector_id";
WHERE ous.fk_sector = :sector_id
ORDER BY u.id";
$this->logService->info('[UPDATE USERS] SQL - Récupération finale des users', [
'query' => $usersQuery,
'params' => ['sector_id' => $id]
]);
$usersStmt = $this->db->prepare($usersQuery);
$usersStmt->execute(['sector_id' => $id]);
$usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC);
$userIds = array_column($usersSectors, 'id');
$this->logService->info('[UPDATE USERS] Users récupérés après commit', [
'sector_id' => $id,
'users_ids' => $userIds,
'nb_users' => count($userIds),
'users_demandes_initialement' => $data['users'] ?? []
]);
// Déchiffrer les noms des utilisateurs
$usersDecrypted = [];
@@ -1066,6 +1143,7 @@ class SectorController
/**
* Mettre à jour les passages affectés à un secteur lors de la modification du périmètre
* VERSION OPTIMISÉE avec requêtes groupées
* Retourne un tableau avec les compteurs détaillés
*/
private function updatePassagesForSector($sectorId, $newSectorCoords): array
@@ -1080,18 +1158,18 @@ class SectorController
try {
// Récupérer l'opération et l'entité du secteur
$sectorQuery = "SELECT o.id as operation_id, o.fk_entite, s.fk_operation
$sectorQuery = "SELECT o.id as operation_id, o.fk_entite, s.fk_operation
FROM ope_sectors s
JOIN operations o ON s.fk_operation = o.id
WHERE s.id = :sector_id";
$sectorStmt = $this->db->prepare($sectorQuery);
$sectorStmt->execute(['sector_id' => $sectorId]);
$sectorInfo = $sectorStmt->fetch();
if (!$sectorInfo) {
return 0;
return $counters;
}
$operationId = $sectorInfo['operation_id'];
$entityId = $sectorInfo['fk_entite'];
@@ -1099,7 +1177,7 @@ class SectorController
$points = explode('#', rtrim($newSectorCoords, '#'));
$coordinates = [];
$polygonPoints = [];
foreach ($points as $point) {
if (!empty($point)) {
list($lat, $lng) = explode('/', $point);
@@ -1110,170 +1188,249 @@ class SectorController
$polygonPoints[] = $polygonPoints[0]; // Fermer le polygone
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// 1. VÉRIFICATION GÉOGRAPHIQUE DES PASSAGES EXISTANTS
$checkPassagesQuery = "SELECT id, gps_lat, gps_lng, fk_type, encrypted_name
FROM ope_pass
WHERE fk_sector = :sector_id
AND gps_lat IS NOT NULL
AND gps_lng IS NOT NULL";
// 1. VÉRIFICATION GÉOGRAPHIQUE DES PASSAGES EXISTANTS (OPTIMISÉE)
// Utiliser une seule requête pour vérifier tous les passages
$checkPassagesQuery = "
SELECT
p.id,
p.gps_lat,
p.gps_lng,
p.fk_type,
p.encrypted_name,
ST_Contains(ST_GeomFromText(:polygon, 4326),
POINT(CAST(p.gps_lng AS DECIMAL(10,8)),
CAST(p.gps_lat AS DECIMAL(10,8)))) as is_inside
FROM ope_pass p
WHERE p.fk_sector = :sector_id
AND p.gps_lat IS NOT NULL
AND p.gps_lng IS NOT NULL";
$checkStmt = $this->db->prepare($checkPassagesQuery);
$checkStmt->execute(['sector_id' => $sectorId]);
$checkStmt->execute([
'sector_id' => $sectorId,
'polygon' => $polygonString
]);
$existingPassages = $checkStmt->fetchAll();
$passagesToDelete = [];
$passagesToOrphan = [];
foreach ($existingPassages as $passage) {
// Vérifier si le passage est dans le nouveau polygone
$pointInPolygonQuery = "SELECT ST_Contains(ST_GeomFromText(:polygon, 4326),
POINT(CAST(:lng AS DECIMAL(10,8)),
CAST(:lat AS DECIMAL(10,8)))) as is_inside";
$pointStmt = $this->db->prepare($pointInPolygonQuery);
$pointStmt->execute([
'polygon' => $polygonString,
'lng' => $passage['gps_lng'],
'lat' => $passage['gps_lat']
]);
$result = $pointStmt->fetch();
if ($result['is_inside'] == 0) {
if ($passage['is_inside'] == 0) {
// Le passage est hors du nouveau périmètre
// Vérifier si c'est un passage non visité (fk_type=2 ET encrypted_name vide)
if ($passage['fk_type'] == 2 && ($passage['encrypted_name'] === '' || $passage['encrypted_name'] === null)) {
// Passage non visité : à supprimer
$passagesToDelete[] = $passage['id'];
$counters['passages_deleted'] = ($counters['passages_deleted'] ?? 0) + 1;
$counters['passages_deleted']++;
} else {
// Passage visité : mettre en orphelin
$orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id = :passage_id";
$orphanStmt = $this->db->prepare($orphanQuery);
$orphanStmt->execute(['passage_id' => $passage['id']]);
// Passage visité : à mettre en orphelin
$passagesToOrphan[] = $passage['id'];
$counters['passages_orphaned']++;
}
} else {
$counters['passages_kept']++;
}
}
// Supprimer les passages non visités qui sont hors zone
// Supprimer les passages non visités en une seule requête
if (!empty($passagesToDelete)) {
$deleteQuery = "DELETE FROM ope_pass WHERE id IN (" . implode(',', $passagesToDelete) . ")";
$this->db->exec($deleteQuery);
$placeholders = str_repeat('?,', count($passagesToDelete) - 1) . '?';
$deleteQuery = "DELETE FROM ope_pass WHERE id IN ($placeholders)";
$deleteStmt = $this->db->prepare($deleteQuery);
$deleteStmt->execute($passagesToDelete);
}
// Mettre en orphelin les passages visités en une seule requête
if (!empty($passagesToOrphan)) {
$placeholders = str_repeat('?,', count($passagesToOrphan) - 1) . '?';
$orphanQuery = "UPDATE ope_pass SET fk_sector = NULL WHERE id IN ($placeholders)";
$orphanStmt = $this->db->prepare($orphanQuery);
$orphanStmt->execute($passagesToOrphan);
}
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES
// 2. CRÉATION/MISE À JOUR DES PASSAGES POUR LES NOUVELLES ADRESSES (OPTIMISÉE)
// Récupérer toutes les adresses du secteur depuis sectors_adresses
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
$addressesStmt = $this->db->prepare($addressesQuery);
$addressesStmt->execute(['sector_id' => $sectorId]);
$addresses = $addressesStmt->fetchAll();
$this->logService->info('[updatePassagesForSector] Adresses dans sectors_adresses', [
'sector_id' => $sectorId,
'nb_addresses' => count($addresses)
]);
// Récupérer le premier utilisateur affecté au secteur
$userQuery = "SELECT fk_user FROM ope_users_sectors WHERE fk_sector = :sector_id LIMIT 1";
$userStmt = $this->db->prepare($userQuery);
$userStmt->execute(['sector_id' => $sectorId]);
$firstUser = $userStmt->fetch();
$firstUserId = $firstUser ? $firstUser['fk_user'] : null;
if ($firstUserId && !empty($addresses)) {
$this->logService->info('[updatePassagesForSector] Création passages pour user', [
$this->logService->info('[updatePassagesForSector] Optimisation passages', [
'user_id' => $firstUserId,
'nb_addresses_to_process' => count($addresses)
'nb_addresses' => count($addresses)
]);
// Préparer la requête de création de passage (même format que dans create)
$createPassageQuery = "INSERT INTO ope_pass (
fk_operation, fk_sector, fk_user, fk_adresse,
numero, rue, rue_bis, ville,
gps_lat, gps_lng, fk_type, encrypted_name,
created_at, fk_user_creat, chk_active
) VALUES (
:operation_id, :sector_id, :user_id, :fk_adresse,
:numero, :rue, :rue_bis, :ville,
:gps_lat, :gps_lng, 2, '',
NOW(), :user_creat, 1
)";
$createStmt = $this->db->prepare($createPassageQuery);
// OPTIMISATION : Récupérer TOUS les passages existants en UNE requête
$addressIds = array_filter(array_column($addresses, 'fk_adresse'));
// Construire la requête pour récupérer tous les passages existants
$existingQuery = "
SELECT id, fk_adresse, numero, rue, rue_bis, ville
FROM ope_pass
WHERE fk_operation = :operation_id
AND (";
$params = ['operation_id' => $operationId];
$conditions = [];
// Condition pour les fk_adresse
if (!empty($addressIds)) {
$placeholders = [];
foreach ($addressIds as $idx => $addrId) {
$key = 'addr_' . $idx;
$placeholders[] = ':' . $key;
$params[$key] = $addrId;
}
$conditions[] = "fk_adresse IN (" . implode(',', $placeholders) . ")";
}
// Condition pour les données d'adresse (numero, rue, ville)
$addressConditions = [];
foreach ($addresses as $idx => $addr) {
$numKey = 'num_' . $idx;
$rueKey = 'rue_' . $idx;
$bisKey = 'bis_' . $idx;
$villeKey = 'ville_' . $idx;
$addressConditions[] = "(numero = :$numKey AND rue = :$rueKey AND rue_bis = :$bisKey AND ville = :$villeKey)";
$params[$numKey] = $addr['numero'];
$params[$rueKey] = $addr['rue'];
$params[$bisKey] = $addr['rue_bis'];
$params[$villeKey] = $addr['ville'];
}
if (!empty($addressConditions)) {
$conditions[] = "(" . implode(' OR ', $addressConditions) . ")";
}
$existingQuery .= implode(' OR ', $conditions) . ")";
$existingStmt = $this->db->prepare($existingQuery);
$existingStmt->execute($params);
$existingPassages = $existingStmt->fetchAll();
// Indexer les passages existants pour recherche rapide
$passagesByAddress = [];
$passagesByData = [];
foreach ($existingPassages as $p) {
if (!empty($p['fk_adresse'])) {
$passagesByAddress[$p['fk_adresse']] = $p;
}
$dataKey = $p['numero'] . '|' . $p['rue'] . '|' . $p['rue_bis'] . '|' . $p['ville'];
$passagesByData[$dataKey] = $p;
}
// Préparer les listes pour batch insert/update
$toInsert = [];
$toUpdate = [];
foreach ($addresses as $address) {
// 2.1 Vérification primaire par fk_adresse
if (!empty($address['fk_adresse'])) {
$checkByAddressQuery = "SELECT id FROM ope_pass
WHERE fk_operation = :operation_id
AND fk_adresse = :fk_adresse";
$checkByAddressStmt = $this->db->prepare($checkByAddressQuery);
$checkByAddressStmt->execute([
'operation_id' => $operationId,
'fk_adresse' => $address['fk_adresse']
]);
if ($checkByAddressStmt->fetch()) {
continue; // Passage déjà existant, passer au suivant
}
// Vérification en mémoire PHP (0 requête)
if (!empty($address['fk_adresse']) && isset($passagesByAddress[$address['fk_adresse']])) {
continue; // Déjà existant avec bon fk_adresse
}
// 2.2 Vérification secondaire par données d'adresse
$checkByDataQuery = "SELECT id FROM ope_pass
WHERE fk_operation = :operation_id
AND numero = :numero
AND rue_bis = :rue_bis
AND rue = :rue
AND ville = :ville";
$checkByDataStmt = $this->db->prepare($checkByDataQuery);
$checkByDataStmt->execute([
'operation_id' => $operationId,
'numero' => $address['numero'],
'rue_bis' => $address['rue_bis'],
'rue' => $address['rue'],
'ville' => $address['ville']
]);
$matchingPassages = $checkByDataStmt->fetchAll();
if (!empty($matchingPassages)) {
// Mettre à jour les passages trouvés avec le fk_adresse
if (!empty($address['fk_adresse'])) {
$updateQuery = "UPDATE ope_pass SET fk_adresse = :fk_adresse WHERE id = :passage_id";
$updateStmt = $this->db->prepare($updateQuery);
foreach ($matchingPassages as $matchingPassage) {
$updateStmt->execute([
'fk_adresse' => $address['fk_adresse'],
'passage_id' => $matchingPassage['id']
]);
$counters['passages_updated']++;
}
$dataKey = $address['numero'] . '|' . $address['rue'] . '|' . $address['rue_bis'] . '|' . $address['ville'];
if (isset($passagesByData[$dataKey])) {
// Passage existant mais sans fk_adresse ou avec fk_adresse différent
if (!empty($address['fk_adresse']) && $passagesByData[$dataKey]['fk_adresse'] != $address['fk_adresse']) {
$toUpdate[] = [
'id' => $passagesByData[$dataKey]['id'],
'fk_adresse' => $address['fk_adresse']
];
}
continue;
} else {
// Nouveau passage à créer
$toInsert[] = $address;
}
// 2.3 Création du passage (aucun passage existant trouvé)
}
// INSERT MULTIPLE en une seule requête
if (!empty($toInsert)) {
$values = [];
$insertParams = [];
$paramIndex = 0;
foreach ($toInsert as $addr) {
$values[] = "(:op$paramIndex, :sect$paramIndex, :usr$paramIndex, :addr$paramIndex,
:num$paramIndex, :rue$paramIndex, :bis$paramIndex, :ville$paramIndex,
:lat$paramIndex, :lng$paramIndex, 2, '', NOW(), :creat$paramIndex, 1)";
$insertParams["op$paramIndex"] = $operationId;
$insertParams["sect$paramIndex"] = $sectorId;
$insertParams["usr$paramIndex"] = $firstUserId;
$insertParams["addr$paramIndex"] = $addr['fk_adresse'];
$insertParams["num$paramIndex"] = $addr['numero'];
$insertParams["rue$paramIndex"] = $addr['rue'];
$insertParams["bis$paramIndex"] = $addr['rue_bis'];
$insertParams["ville$paramIndex"] = $addr['ville'];
$insertParams["lat$paramIndex"] = $addr['gps_lat'];
$insertParams["lng$paramIndex"] = $addr['gps_lng'];
$insertParams["creat$paramIndex"] = $_SESSION['user_id'] ?? null;
$paramIndex++;
}
$insertQuery = "INSERT INTO ope_pass
(fk_operation, fk_sector, fk_user, fk_adresse, numero, rue, rue_bis,
ville, gps_lat, gps_lng, fk_type, encrypted_name, created_at, fk_user_creat, chk_active)
VALUES " . implode(',', $values);
try {
$createStmt->execute([
'operation_id' => $operationId,
'sector_id' => $sectorId,
'user_id' => $firstUserId,
'fk_adresse' => $address['fk_adresse'],
'numero' => $address['numero'],
'rue' => $address['rue'],
'rue_bis' => $address['rue_bis'],
'ville' => $address['ville'],
'gps_lat' => $address['gps_lat'],
'gps_lng' => $address['gps_lng'],
'user_creat' => $_SESSION['user_id'] ?? null
]);
$counters['passages_created']++;
$insertStmt = $this->db->prepare($insertQuery);
$insertStmt->execute($insertParams);
$counters['passages_created'] = count($toInsert);
} catch (\Exception $e) {
$this->logService->warning('Erreur lors de la création d\'un passage pendant update', [
$this->logService->error('Erreur lors de l\'insertion multiple des passages', [
'sector_id' => $sectorId,
'address' => $address,
'error' => $e->getMessage()
]);
}
}
// UPDATE MULTIPLE avec CASE WHEN
if (!empty($toUpdate)) {
$updateIds = array_column($toUpdate, 'id');
$placeholders = str_repeat('?,', count($updateIds) - 1) . '?';
$caseWhen = [];
$updateParams = [];
foreach ($toUpdate as $upd) {
$caseWhen[] = "WHEN id = ? THEN ?";
$updateParams[] = $upd['id'];
$updateParams[] = $upd['fk_adresse'];
}
$updateQuery = "UPDATE ope_pass
SET fk_adresse = CASE " . implode(' ', $caseWhen) . " END
WHERE id IN ($placeholders)";
try {
$updateStmt = $this->db->prepare($updateQuery);
$updateStmt->execute(array_merge($updateParams, $updateIds));
$counters['passages_updated'] = count($toUpdate);
} catch (\Exception $e) {
$this->logService->error('Erreur lors de la mise à jour multiple des passages', [
'sector_id' => $sectorId,
'error' => $e->getMessage()
]);
}
}
} else {
$this->logService->warning('[updatePassagesForSector] Pas de création de passages', [
'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses',