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 242a90720e
commit b6584c83fa
1625 changed files with 145669 additions and 51249 deletions

316
bao/bin/search-entite Executable file
View File

@@ -0,0 +1,316 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de recherche d'entité par nom/adresse/ville/email
* Usage: ./search-entite <environment> <search_term>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <search_term>");
error("Exemple: " . basename($argv[0]) . " dev plumeliau");
error("Exemple: " . basename($argv[0]) . " rec amicale");
exit(1);
}
$environment = strtoupper($argv[1]);
$searchTerm = strtolower(trim($argv[2]));
if (strlen($searchTerm) < 2) {
error("La recherche doit contenir au moins 2 caractères");
exit(1);
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche: \"$searchTerm\"");
info("Champs: nom, email, adresse1, adresse2, ville\n");
// Pré-filtrage sur les champs en clair (adresse1, adresse2, ville)
$stmt = $pdo->prepare("
SELECT
e.id,
e.encrypted_name,
e.encrypted_email,
e.adresse1,
e.adresse2,
e.code_postal,
e.ville,
e.chk_stripe,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE e.chk_active = 1
AND (LOWER(e.adresse1) LIKE :search1
OR LOWER(e.adresse2) LIKE :search2
OR LOWER(e.ville) LIKE :search3)
GROUP BY e.id
ORDER BY e.id
");
$searchPattern = '%' . $searchTerm . '%';
$stmt->execute([
'search1' => $searchPattern,
'search2' => $searchPattern,
'search3' => $searchPattern
]);
$preFilteredEntites = $stmt->fetchAll();
// Récupérer TOUTES les entités pour chercher dans les champs chiffrés
$stmtAll = $pdo->query("
SELECT
e.id,
e.encrypted_name,
e.encrypted_email,
e.adresse1,
e.adresse2,
e.code_postal,
e.ville,
e.chk_stripe,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE e.chk_active = 1
GROUP BY e.id
ORDER BY e.id
");
$allEntites = $stmtAll->fetchAll();
info("Analyse de " . count($allEntites) . " entités actives...\n");
// Déchiffrer et filtrer
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$matchedEntites = [];
$seenIds = [];
// Ajouter d'abord les résultats pré-filtrés (adresse1, adresse2, ville)
foreach ($preFilteredEntites as $entite) {
$name = $crypto->decryptWithIV($entite['encrypted_name']);
$email = $crypto->decryptSearchable($entite['encrypted_email']);
// Vérifier aussi dans les champs chiffrés
$matches = false;
$matchedFields = [];
if (stripos($entite['adresse1'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'adresse1';
}
if (stripos($entite['adresse2'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'adresse2';
}
if (stripos($entite['ville'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'ville';
}
if ($name && stripos($name, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($email && stripos($email, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'email';
}
if ($matches) {
$matchedEntites[] = [
'id' => $entite['id'],
'name' => $name ?? '-',
'email' => $email ?? '-',
'adresse1' => $entite['adresse1'] ?? '-',
'code_postal' => $entite['code_postal'] ?? '-',
'ville' => $entite['ville'] ?? '-',
'stripe' => $entite['chk_stripe'] ? 'Oui' : 'Non',
'nb_users' => $entite['nb_users'],
'nb_operations' => $entite['nb_operations'],
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$entite['id']] = true;
}
}
// Chercher dans les entités restantes (pour nom et email chiffrés)
foreach ($allEntites as $entite) {
if (isset($seenIds[$entite['id']])) {
continue; // Déjà trouvé
}
$name = $crypto->decryptWithIV($entite['encrypted_name']);
$email = $crypto->decryptSearchable($entite['encrypted_email']);
$matches = false;
$matchedFields = [];
if ($name && stripos($name, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($email && stripos($email, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'email';
}
if ($matches) {
$matchedEntites[] = [
'id' => $entite['id'],
'name' => $name ?? '-',
'email' => $email ?? '-',
'adresse1' => $entite['adresse1'] ?? '-',
'code_postal' => $entite['code_postal'] ?? '-',
'ville' => $entite['ville'] ?? '-',
'stripe' => $entite['chk_stripe'] ? 'Oui' : 'Non',
'nb_users' => $entite['nb_users'],
'nb_operations' => $entite['nb_operations'],
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$entite['id']] = true;
}
}
if (empty($matchedEntites)) {
warning("\nAucune entité trouvée avec: \"$searchTerm\"");
exit(0);
}
// Affichage
title("RÉSULTATS - " . count($matchedEntites) . " entité(s) trouvée(s)");
// Préparer les données pour le tableau
$tableData = [];
$lineNumber = 1;
foreach ($matchedEntites as $entite) {
$tableData[] = [
'line' => $lineNumber++,
'id' => $entite['id'],
'name' => truncate($entite['name'], 30),
'ville' => truncate($entite['ville'], 20),
'cp' => $entite['code_postal'],
'users' => $entite['nb_users'],
'ops' => $entite['nb_operations'],
'stripe' => $entite['stripe'],
'match' => $entite['matched_in'],
];
}
table(
[
'line' => '#',
'id' => 'ID',
'name' => 'Nom',
'ville' => 'Ville',
'cp' => 'CP',
'users' => 'Users',
'ops' => 'Ops',
'stripe' => 'Stripe',
'match' => 'Trouvé dans',
],
$tableData,
true
);
success("Recherche terminée");
// Menu interactif
if (count($matchedEntites) > 0) {
echo "\n";
echo color("═══════════════════════════════════════\n", 'cyan');
echo color(" Actions disponibles\n", 'bold');
echo color("═══════════════════════════════════════\n", 'cyan');
echo " 1. Détail d'une entité\n";
echo " 2. Opérations d'une entité\n";
echo " 3. Membres d'une entité\n";
echo " 0. Quitter\n";
echo color("═══════════════════════════════════════\n", 'cyan');
echo color("\nVotre choix: ", 'yellow');
$handle = fopen('php://stdin', 'r');
$choice = trim(fgets($handle));
if ($choice === '0' || $choice === '') {
echo "\n";
info("Au revoir !");
exit(0);
}
// Demander le numéro de ligne (sauf si une seule trouvée)
$entiteId = null;
if (count($matchedEntites) === 1) {
$entiteId = $matchedEntites[0]['id'];
info("\nEntité sélectionnée: #$entiteId - " . $matchedEntites[0]['name']);
} else {
echo color("\nEntrez le n° de ligne (1-" . count($matchedEntites) . "): ", 'yellow');
$lineInput = trim(fgets($handle));
if (!is_numeric($lineInput) || (int)$lineInput < 1 || (int)$lineInput > count($matchedEntites)) {
fclose($handle);
error("Numéro de ligne invalide (doit être entre 1 et " . count($matchedEntites) . ")");
exit(1);
}
$lineNumber = (int)$lineInput;
$entiteId = $matchedEntites[$lineNumber - 1]['id'];
info("\nEntité sélectionnée: #$entiteId - " . $matchedEntites[$lineNumber - 1]['name']);
}
fclose($handle);
echo "\n";
// Exécuter l'action choisie
switch ($choice) {
case '1':
// Détail de l'entité
$decryptEntiteScript = __DIR__ . '/decrypt-entite';
passthru("$decryptEntiteScript $environment $entiteId");
break;
case '2':
// Opérations de l'entité
$listOperationsScript = __DIR__ . '/list-operations';
passthru("$listOperationsScript $environment --entite=$entiteId");
break;
case '3':
// Membres de l'entité
$listUsersScript = __DIR__ . '/list-users';
passthru("$listUsersScript $environment --entite=$entiteId");
break;
default:
error("Choix invalide: $choice");
exit(1);
}
}
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}