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

@@ -119,9 +119,17 @@ class PassageController {
$errors[] = 'La ville est obligatoire';
}
// Validation du nom (chiffré)
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
$errors[] = 'Le nom est obligatoire';
// Validation du nom (chiffré) - obligatoire seulement si (type=1 Effectué ou 5 Lot) ET email présent
$fk_type = isset($data['fk_type']) ? (int)$data['fk_type'] : 0;
$hasEmail = (isset($data['email']) && !empty(trim($data['email']))) ||
(isset($data['encrypted_email']) && !empty($data['encrypted_email']));
if (($fk_type === 1 || $fk_type === 5) && $hasEmail) {
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
$errors[] = 'Le nom est obligatoire pour ce type de passage avec email';
} elseif (isset($data['name']) && empty(trim($data['name']))) {
$errors[] = 'Le nom ne peut pas être vide pour ce type de passage avec email';
}
}
// Validation du montant
@@ -157,6 +165,15 @@ class PassageController {
}
}
// Validation de l'ID Stripe si fourni
if (isset($data['stripe_payment_id']) && !empty($data['stripe_payment_id'])) {
$stripeId = trim($data['stripe_payment_id']);
// L'ID PaymentIntent Stripe doit commencer par 'pi_'
if (!preg_match('/^pi_[a-zA-Z0-9]{24,}$/', $stripeId)) {
$errors[] = 'Format d\'ID de paiement Stripe invalide';
}
}
return empty($errors) ? null : $errors;
}
@@ -210,13 +227,13 @@ class PassageController {
// Requête principale avec jointures
$stmt = $this->db->prepare("
SELECT
SELECT
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.fk_adresse,
p.passed_at, p.fk_type, p.numero, p.rue, p.rue_bis, p.ville,
p.fk_habitat, p.appt, p.niveau, p.residence, p.gps_lat, p.gps_lng,
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
p.encrypted_email, p.encrypted_phone, p.nom_recu, p.date_recu,
p.chk_email_sent, p.docremis, p.date_repasser, p.nb_passages,
p.chk_email_sent, p.stripe_payment_id, p.docremis, p.date_repasser, p.nb_passages,
p.chk_mobile, p.anomalie, p.created_at, p.updated_at, p.chk_active,
o.libelle as operation_libelle,
u.encrypted_name as user_name, u.first_name as user_first_name
@@ -389,11 +406,11 @@ class PassageController {
$offset = ($page - 1) * $limit;
$stmt = $this->db->prepare('
SELECT
SELECT
p.id, p.fk_operation, p.fk_sector, p.fk_user, p.passed_at,
p.numero, p.rue, p.rue_bis, p.ville, p.gps_lat, p.gps_lng,
p.encrypted_name, p.montant, p.fk_type_reglement, p.remarque,
p.encrypted_email, p.encrypted_phone, p.chk_email_sent,
p.encrypted_email, p.encrypted_phone, p.stripe_payment_id, p.chk_email_sent,
p.docremis, p.date_repasser, p.nb_passages, p.chk_mobile,
p.anomalie, p.created_at, p.updated_at,
u.encrypted_name as user_name, u.first_name as user_first_name
@@ -494,7 +511,13 @@ class PassageController {
}
// Chiffrement des données sensibles
$encryptedName = isset($data['name']) ? ApiService::encryptData($data['name']) : (isset($data['encrypted_name']) ? $data['encrypted_name'] : '');
$encryptedName = '';
if (isset($data['name']) && !empty(trim($data['name']))) {
$encryptedName = ApiService::encryptData($data['name']);
} elseif (isset($data['encrypted_name']) && !empty($data['encrypted_name'])) {
$encryptedName = $data['encrypted_name'];
}
// Le nom peut rester vide si les conditions ne l'exigent pas
$encryptedEmail = isset($data['email']) && !empty($data['email']) ?
ApiService::encryptSearchableData($data['email']) : '';
$encryptedPhone = isset($data['phone']) && !empty($data['phone']) ?
@@ -524,6 +547,7 @@ class PassageController {
'remarque' => $data['remarque'] ?? '',
'encrypted_email' => $encryptedEmail,
'encrypted_phone' => $encryptedPhone,
'stripe_payment_id' => isset($data['stripe_payment_id']) ? trim($data['stripe_payment_id']) : null,
'nom_recu' => $data['nom_recu'] ?? null,
'date_recu' => isset($data['date_recu']) ? $data['date_recu'] : null,
'docremis' => isset($data['docremis']) ? (int)$data['docremis'] : 0,
@@ -646,7 +670,7 @@ class PassageController {
}
$stmt = $this->db->prepare('
SELECT p.id, p.fk_operation
SELECT p.id, p.fk_operation, p.fk_type, p.fk_user
FROM ope_pass p
INNER JOIN operations o ON p.fk_operation = o.id
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
@@ -673,6 +697,19 @@ class PassageController {
return;
}
// Si le passage était de type 2 et que l'utilisateur actuel est différent du créateur
// On force l'attribution du passage à l'utilisateur actuel
if ((int)$passage['fk_type'] === 2 && (int)$passage['fk_user'] !== $userId) {
$data['fk_user'] = $userId;
LogService::log('Attribution automatique d\'un passage type 2 à l\'utilisateur', [
'level' => 'info',
'passageId' => $passageId,
'ancien_user' => $passage['fk_user'],
'nouveau_user' => $userId
]);
}
// Construction de la requête de mise à jour dynamique
$updateFields = [];
$params = [];
@@ -697,6 +734,7 @@ class PassageController {
'montant',
'fk_type_reglement',
'remarque',
'stripe_payment_id',
'nom_recu',
'date_recu',
'docremis',
@@ -714,9 +752,10 @@ class PassageController {
}
// Gestion des champs chiffrés
if (isset($data['name'])) {
if (array_key_exists('name', $data)) {
$updateFields[] = "encrypted_name = ?";
$params[] = ApiService::encryptData($data['name']);
// Permettre de vider le nom si les conditions le permettent
$params[] = !empty(trim($data['name'])) ? ApiService::encryptData($data['name']) : '';
}
if (isset($data['email'])) {