Compare commits
11 Commits
v3.5.2
...
gestion-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599b9fcda0 | ||
|
|
6a609fb467 | ||
|
|
7763d02fae | ||
|
|
b9672a6228 | ||
|
|
4244b961fd | ||
|
|
f3f1a9c5e8 | ||
|
|
15a0f2d2be | ||
|
|
86a9a35594 | ||
|
|
e5ab857913 | ||
|
|
2aa2706179 | ||
|
|
41f1db1169 |
111
CONTEXT-AI.md
Normal file → Executable file
111
CONTEXT-AI.md
Normal file → Executable file
@@ -120,36 +120,115 @@
|
|||||||
|
|
||||||
### Branches GitLab
|
### Branches GitLab
|
||||||
|
|
||||||
- **main/master**: [Production-ready code]
|
- **main**: Code stable prêt pour la production
|
||||||
- **develop**: [Integration branch for features]
|
- **develop**: Branche d'intégration pour les fonctionnalités en cours de développement
|
||||||
- **feature/[feature-name]**: [Feature development]
|
- **feature/[feature-name]**: Branches de développement pour les nouvelles fonctionnalités
|
||||||
- **bugfix/[bug-name]**: [Bug fixes]
|
- Exemple: `feature/geolocalisation-casernes` pour l'ajout de la géolocalisation des casernes
|
||||||
- **release/[version]**: [Release preparation]
|
- **bugfix/[bug-name]**: Branches pour les corrections de bugs
|
||||||
|
- **release/[version]**: Branches de préparation des versions
|
||||||
|
|
||||||
### Processus de Merge Request
|
### Processus de Merge Request
|
||||||
|
|
||||||
1. [Créer une branche à partir de develop]
|
1. Créer une branche à partir de `main` ou `develop` selon la nature du changement
|
||||||
2. [Développer la fonctionnalité/correction]
|
|
||||||
3. [Soumettre une MR vers develop]
|
```bash
|
||||||
4. [Code review]
|
git checkout -b feature/nom-de-la-fonctionnalite main
|
||||||
5. [CI/CD validation]
|
```
|
||||||
6. [Merge]
|
|
||||||
|
2. Développer la fonctionnalité ou correction avec des commits atomiques
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add fichier1 fichier2
|
||||||
|
git commit -m "Description claire du changement"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Pousser la branche vers le dépôt distant
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push -u origin feature/nom-de-la-fonctionnalite
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Créer une Merge Request via l'interface GitLab ou en utilisant l'URL fournie
|
||||||
|
|
||||||
|
- URL: `http://51.68.36.203/d6soft/geosector/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature/nom-de-la-fonctionnalite`
|
||||||
|
|
||||||
|
5. Attendre la revue de code et les validations CI/CD
|
||||||
|
|
||||||
|
6. Une fois approuvée, fusionner la branche:
|
||||||
|
```bash
|
||||||
|
git checkout main
|
||||||
|
git merge feature/nom-de-la-fonctionnalite
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
### CI/CD Pipeline
|
### CI/CD Pipeline
|
||||||
|
|
||||||
[Description de votre pipeline CI/CD dans GitLab]
|
Le projet utilise un pipeline CI/CD GitLab pour automatiser les tests et le déploiement:
|
||||||
|
|
||||||
|
1. **Build**: Compilation du code et vérification de la syntaxe
|
||||||
|
|
||||||
|
- PHP: Vérification de la syntaxe et des dépendances Composer
|
||||||
|
- Flutter: Compilation et génération des assets
|
||||||
|
|
||||||
|
2. **Test**: Exécution des tests automatisés
|
||||||
|
|
||||||
|
- Tests unitaires pour l'API PHP
|
||||||
|
- Tests de widgets pour l'application Flutter
|
||||||
|
|
||||||
|
3. **Deploy**: Déploiement automatique vers les environnements
|
||||||
|
- Déploiement vers DEV après chaque merge dans `develop`
|
||||||
|
- Déploiement vers RECETTE après validation manuelle
|
||||||
|
- Déploiement vers PROD après validation manuelle sur une MR vers `main`
|
||||||
|
|
||||||
## Intégration avec GitLab
|
## Intégration avec GitLab
|
||||||
|
|
||||||
### Issues et Kanban
|
### Issues et Kanban
|
||||||
|
|
||||||
- **Labels**: [Liste des labels principaux et leur signification]
|
- **Labels**:
|
||||||
- **Milestones**: [Comment les milestones sont utilisées]
|
|
||||||
- **Boards**: [Description des tableaux Kanban]
|
- `feature`: Nouvelles fonctionnalités
|
||||||
|
- `bug`: Corrections de bugs
|
||||||
|
- `enhancement`: Améliorations de fonctionnalités existantes
|
||||||
|
- `documentation`: Mises à jour de la documentation
|
||||||
|
- `api`: Modifications de l'API
|
||||||
|
- `ui`: Modifications de l'interface utilisateur
|
||||||
|
- `priority:high`: Priorité élevée
|
||||||
|
- `priority:medium`: Priorité moyenne
|
||||||
|
- `priority:low`: Priorité basse
|
||||||
|
|
||||||
|
- **Milestones**:
|
||||||
|
|
||||||
|
- Organisées par versions majeures (1.0, 1.1, etc.)
|
||||||
|
- Chaque milestone contient les issues prévues pour la version
|
||||||
|
- Date d'échéance définie pour chaque milestone
|
||||||
|
|
||||||
|
- **Boards**:
|
||||||
|
- **Backlog**: Issues à traiter dans le futur
|
||||||
|
- **To Do**: Issues prêtes à être développées
|
||||||
|
- **In Progress**: Issues en cours de développement
|
||||||
|
- **Review**: Issues en attente de revue de code
|
||||||
|
- **Done**: Issues terminées et déployées
|
||||||
|
|
||||||
### Automatisations
|
### Automatisations
|
||||||
|
|
||||||
[Description des automatisations GitLab utilisées]
|
- **Webhooks**: Notifications automatiques dans Slack pour les événements importants
|
||||||
|
|
||||||
|
- Nouvelles Merge Requests
|
||||||
|
- Commentaires sur les MRs
|
||||||
|
- Builds échoués
|
||||||
|
- Déploiements réussis
|
||||||
|
|
||||||
|
- **Merge Request Templates**: Templates prédéfinis pour les MRs avec:
|
||||||
|
|
||||||
|
- Description de la fonctionnalité
|
||||||
|
- Checklist de vérification
|
||||||
|
- Instructions de test
|
||||||
|
- Captures d'écran (si applicable)
|
||||||
|
|
||||||
|
- **CI/CD Automatisé**: Déclenchement automatique des pipelines sur:
|
||||||
|
- Push vers une branche
|
||||||
|
- Création d'une Merge Request
|
||||||
|
- Mise à jour d'une Merge Request
|
||||||
|
|
||||||
## Déploiement
|
## Déploiement
|
||||||
|
|
||||||
|
|||||||
128
api/.vscode/settings.json
vendored
128
api/.vscode/settings.json
vendored
@@ -1,124 +1,22 @@
|
|||||||
{
|
{
|
||||||
"window.zoomLevel": 1, // Permet de zoomer, pratique si vous faites une présentation
|
|
||||||
|
|
||||||
// Apparence
|
|
||||||
// -- Editeur
|
|
||||||
"workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée
|
|
||||||
"editor.minimap.enabled": false, // On veut voir la minimap
|
|
||||||
"editor.minimap.showSlider": "always", // On veut voir la minimap
|
|
||||||
"editor.minimap.size": "fill", // On veut voir la minimap
|
|
||||||
"editor.minimap.scale": 2,
|
|
||||||
"editor.tokenColorCustomizations": {
|
|
||||||
"textMateRules": [
|
|
||||||
{
|
|
||||||
"scope": ["storage.type.function", "storage.type.class"],
|
|
||||||
"settings": {
|
|
||||||
"fontStyle": "bold",
|
|
||||||
"foreground": "#4B9CD3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editor.minimap.renderCharacters": true,
|
|
||||||
"editor.minimap.maxColumn": 120,
|
|
||||||
"breadcrumbs.enabled": false,
|
|
||||||
// -- Tabs
|
|
||||||
"workbench.editor.wrapTabs": true, // On veut voir les tabs
|
|
||||||
"workbench.editor.tabSizing": "shrink", // On veut voir les tabs
|
|
||||||
"workbench.editor.pinnedTabSizing": "compact",
|
|
||||||
"workbench.editor.enablePreview": false, // Un clic sur un fichier l'ouvre
|
|
||||||
|
|
||||||
// -- Sidebar
|
|
||||||
"workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar
|
|
||||||
"workbench.tree.renderIndentGuides": "always",
|
|
||||||
// -- Code
|
|
||||||
"editor.occurrencesHighlight": "singleFile", // On veut voir les occurences d'une variable
|
|
||||||
"editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne
|
|
||||||
"editor.renderControlCharacters": true, // On veut voir les caractères de contrôle
|
|
||||||
// Thème
|
|
||||||
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace",
|
|
||||||
"editor.fontLigatures": false,
|
|
||||||
"editor.fontSize": 13,
|
|
||||||
"editor.lineHeight": 22,
|
|
||||||
"editor.guides.bracketPairs": "active",
|
|
||||||
|
|
||||||
// Ergonomie
|
|
||||||
"editor.wordWrap": "off",
|
|
||||||
"editor.rulers": [],
|
|
||||||
"editor.suggest.insertMode": "replace", // L'autocomplétion remplace le mot en cours
|
|
||||||
"editor.acceptSuggestionOnCommitCharacter": false, // Evite que l'autocomplétion soit accepté lors d'un . par exemple
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnPaste": true,
|
|
||||||
"editor.linkedEditing": true, // Quand on change un élément HTML, change la balise fermante
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.unicodeHighlight.nonBasicASCII": false,
|
|
||||||
|
|
||||||
"[php]": {
|
|
||||||
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnPaste": true
|
|
||||||
},
|
|
||||||
"intelephense.format.braces": "k&r",
|
|
||||||
"intelephense.format.enable": true,
|
|
||||||
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnPaste": true
|
|
||||||
},
|
|
||||||
"prettier.printWidth": 360,
|
|
||||||
"prettier.semi": true,
|
|
||||||
"prettier.singleQuote": true,
|
|
||||||
"prettier.tabWidth": 2,
|
|
||||||
"prettier.trailingComma": "es5",
|
|
||||||
|
|
||||||
"explorer.autoReveal": false,
|
|
||||||
"explorer.confirmDragAndDrop": false,
|
|
||||||
"emmet.triggerExpansionOnTab": true,
|
|
||||||
|
|
||||||
// Fichiers
|
|
||||||
"files.defaultLanguage": "markdown",
|
|
||||||
"files.autoSaveWorkspaceFilesOnly": true,
|
|
||||||
"files.exclude": {
|
|
||||||
"**/.idea": true
|
|
||||||
},
|
|
||||||
// Languages
|
|
||||||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
|
||||||
|
|
||||||
// Extensions
|
|
||||||
"tailwindCSS.experimental.configFile": "frontend/tailwind.config.js",
|
|
||||||
"editor.quickSuggestions": {
|
|
||||||
"strings": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"[svelte]": {
|
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"prettier.documentSelectors": ["**/*.svelte"],
|
|
||||||
"svelte.plugin.svelte.diagnostics.enable": false,
|
|
||||||
"problems.decorations.enabled": false,
|
|
||||||
"js/ts.implicitProjectConfig.checkJs": false,
|
|
||||||
"svelte.enable-ts-plugin": false,
|
|
||||||
"workbench.colorCustomizations": {
|
"workbench.colorCustomizations": {
|
||||||
"activityBar.activeBackground": "#ff6433",
|
"activityBar.activeBackground": "#fa1b49",
|
||||||
"activityBar.background": "#ff6433",
|
"activityBar.background": "#fa1b49",
|
||||||
"activityBar.foreground": "#15202b",
|
"activityBar.foreground": "#e7e7e7",
|
||||||
"activityBar.inactiveForeground": "#15202b99",
|
"activityBar.inactiveForeground": "#e7e7e799",
|
||||||
"activityBarBadge.background": "#00ff3d",
|
"activityBarBadge.background": "#155e02",
|
||||||
"activityBarBadge.foreground": "#15202b",
|
"activityBarBadge.foreground": "#e7e7e7",
|
||||||
"commandCenter.border": "#e7e7e799",
|
"commandCenter.border": "#e7e7e799",
|
||||||
"sash.hoverBorder": "#ff6433",
|
"sash.hoverBorder": "#fa1b49",
|
||||||
"statusBar.background": "#ff3d00",
|
"statusBar.background": "#dd0531",
|
||||||
"statusBar.foreground": "#e7e7e7",
|
"statusBar.foreground": "#e7e7e7",
|
||||||
"statusBarItem.hoverBackground": "#ff6433",
|
"statusBarItem.hoverBackground": "#fa1b49",
|
||||||
"statusBarItem.remoteBackground": "#ff3d00",
|
"statusBarItem.remoteBackground": "#dd0531",
|
||||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||||
"titleBar.activeBackground": "#ff3d00",
|
"titleBar.activeBackground": "#dd0531",
|
||||||
"titleBar.activeForeground": "#e7e7e7",
|
"titleBar.activeForeground": "#e7e7e7",
|
||||||
"titleBar.inactiveBackground": "#ff3d0099",
|
"titleBar.inactiveBackground": "#dd053199",
|
||||||
"titleBar.inactiveForeground": "#e7e7e799"
|
"titleBar.inactiveForeground": "#e7e7e799"
|
||||||
},
|
},
|
||||||
"peacock.color": "#ff3d00"
|
"peacock.color": "#dd0531"
|
||||||
}
|
}
|
||||||
|
|||||||
75
api/CLAUDE.md
Executable file
75
api/CLAUDE.md
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Directives importantes
|
||||||
|
- **Langue** : Toujours répondre en français
|
||||||
|
- **Approche de travail** :
|
||||||
|
- Travailler par étapes claires et structurées
|
||||||
|
- TOUJOURS présenter et proposer les modifications avant de les implémenter
|
||||||
|
- Attendre la validation de l'utilisateur avant de modifier le code
|
||||||
|
- Expliquer le problème identifié et la solution proposée
|
||||||
|
- **Vérification du schéma** : TOUJOURS vérifier `docs/geo_app.sql` avant de faire des modifications sur les tables de la base de données
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
- Install dependencies: `composer install` - install PHP dependencies
|
||||||
|
- Update dependencies: `composer update` - update PHP dependencies to latest versions
|
||||||
|
- Deploy to REC: `./livre-api.sh rec` - deploy from DVA to RECETTE environment
|
||||||
|
- Deploy to PROD: `./livre-api.sh prod` - deploy from RECETTE to PRODUCTION environment
|
||||||
|
- Export operations: `php export_operation.php` - export operations data
|
||||||
|
|
||||||
|
## Code Architecture
|
||||||
|
This is a PHP 8.3 API without framework, using a custom MVC-like architecture:
|
||||||
|
|
||||||
|
- **Entry point**: `index.php` handles all requests through custom Router
|
||||||
|
- **Core components**:
|
||||||
|
- `Router`: Maps URLs to controller methods, handles HTTP methods
|
||||||
|
- `Database`: PDO wrapper for MariaDB connections
|
||||||
|
- `Session`: Secure session management
|
||||||
|
- `Request/Response`: HTTP request/response handling
|
||||||
|
- **Controllers**: Located in `src/Controllers/`, handle business logic
|
||||||
|
- **Services**: Located in `src/Services/`, provide reusable functionality (logging, email, exports)
|
||||||
|
- **Configuration**: `src/Config/AppConfig.php` - singleton configuration management
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
- No framework dependency - pure PHP 8.3 with composer autoloading
|
||||||
|
- PDO for database access with prepared statements
|
||||||
|
- RESTful API design with JSON responses
|
||||||
|
- CORS handling for cross-origin requests
|
||||||
|
- Session-based authentication
|
||||||
|
- File uploads handled in `uploads/` directory
|
||||||
|
- Logs stored in `logs/` directory
|
||||||
|
|
||||||
|
## Database
|
||||||
|
- MariaDB 10.11 with InnoDB tables
|
||||||
|
- Migration scripts in `scripts/php/migrate_*.php`
|
||||||
|
- Schema comparison tool: `scripts/python/compare_schemas.py`
|
||||||
|
- Database sync: `scripts/cron/sync_databases.php`
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
- Session cookies with httponly, secure flags
|
||||||
|
- CORS configured for specific origins
|
||||||
|
- XSS, clickjacking protection headers
|
||||||
|
- PDO prepared statements for SQL injection prevention
|
||||||
|
- File upload validation in FileService
|
||||||
|
|
||||||
|
## Bonnes pratiques spécifiques
|
||||||
|
|
||||||
|
### Gestion des transactions PDO
|
||||||
|
- Toujours vérifier `$db->inTransaction()` avant d'appeler `rollBack()`
|
||||||
|
- Encadrer les opérations critiques dans des try/catch avec transaction
|
||||||
|
|
||||||
|
### Paramètres SQL
|
||||||
|
- Utiliser des noms de paramètres uniques dans les requêtes SQL
|
||||||
|
- Ne jamais réutiliser le même nom de paramètre plusieurs fois dans une requête
|
||||||
|
- Exemple : `:sector_polygon1`, `:sector_polygon2` au lieu de `:sector_polygon` répété
|
||||||
|
|
||||||
|
### Format des réponses API
|
||||||
|
- Les données doivent être placées à la racine de la réponse JSON, pas dans un groupe "data"
|
||||||
|
- Suivre le modèle de `LoginController` pour la structure des réponses
|
||||||
|
- Retourner des objets complets, pas seulement des IDs (ex: sector complet, pas sector_id)
|
||||||
|
|
||||||
|
### Gestion des sessions
|
||||||
|
- La session stocke `entity_id` depuis `fk_entite` lors du login
|
||||||
|
- Utiliser `Session::getEntityId()` pour récupérer l'ID de l'entité
|
||||||
|
- L'authentification utilise des Bearer tokens contenant le session_id
|
||||||
13
api/alter_table_geometry.sql
Normal file
13
api/alter_table_geometry.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Modifier la table pour accepter tous les types de géométries (POLYGON et MULTIPOLYGON)
|
||||||
|
|
||||||
|
-- Option 1 : Modifier la colonne existante (recommandé)
|
||||||
|
ALTER TABLE x_departements_contours
|
||||||
|
MODIFY COLUMN contour GEOMETRY NOT NULL COMMENT 'Géométrie du contour du département (Polygon ou MultiPolygon)';
|
||||||
|
|
||||||
|
-- Vérifier la modification
|
||||||
|
DESCRIBE x_departements_contours;
|
||||||
|
|
||||||
|
-- Option 2 : Si l'option 1 ne fonctionne pas, recréer la colonne
|
||||||
|
-- ALTER TABLE x_departements_contours DROP COLUMN contour;
|
||||||
|
-- ALTER TABLE x_departements_contours ADD COLUMN contour GEOMETRY NOT NULL AFTER nom_dept;
|
||||||
|
-- ALTER TABLE x_departements_contours ADD SPATIAL INDEX idx_contour (contour);
|
||||||
0
api/bootstrap.php
Normal file → Executable file
0
api/bootstrap.php
Normal file → Executable file
15
api/composer.json
Normal file → Executable file
15
api/composer.json
Normal file → Executable file
@@ -3,16 +3,17 @@
|
|||||||
"description": "API Multi-sites",
|
"description": "API Multi-sites",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.3",
|
||||||
"phpmailer/phpmailer": "^6.8",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-json": "*"
|
"ext-pdo": "*",
|
||||||
|
"phpmailer/phpmailer": "^6.8",
|
||||||
|
"phpoffice/phpspreadsheet": "^2.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"classmap": [
|
||||||
"App\\": "src/"
|
"src/"
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
|
|||||||
604
api/composer.lock
generated
Normal file → Executable file
604
api/composer.lock
generated
Normal file → Executable file
@@ -4,20 +4,284 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "03e608fa83a14a82b3f9223977e9674e",
|
"content-hash": "cf5e9de2a9687d04e4e094ad368ce366",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "composer/pcre",
|
||||||
"version": "v6.9.3",
|
"version": "3.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/composer/pcre.git",
|
||||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan": "<1.11.10"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.12 || ^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||||
|
"phpunit/phpunit": "^8 || ^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||||
|
"keywords": [
|
||||||
|
"PCRE",
|
||||||
|
"preg",
|
||||||
|
"regex",
|
||||||
|
"regular expression"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/composer/pcre/issues",
|
||||||
|
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-11-12T16:29:46+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maennchen/zipstream-php",
|
||||||
|
"version": "3.1.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||||
|
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||||
|
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php-64bit": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"brianium/paratest": "^7.7",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.16",
|
||||||
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
|
"mikey179/vfsstream": "^1.6",
|
||||||
|
"php-coveralls/php-coveralls": "^2.5",
|
||||||
|
"phpunit/phpunit": "^11.0",
|
||||||
|
"vimeo/psalm": "^6.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"guzzlehttp/psr7": "^2.4",
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ZipStream\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paul Duncan",
|
||||||
|
"email": "pabs@pablotron.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jonatan Männchen",
|
||||||
|
"email": "jonatan@maennchen.ch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jesse Donat",
|
||||||
|
"email": "donatj@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "András Kolesár",
|
||||||
|
"email": "kolesar@kolesar.hu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||||
|
"keywords": [
|
||||||
|
"stream",
|
||||||
|
"zip"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||||
|
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/maennchen",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-01-27T12:07:53+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/complex",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Complex\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@lange.demon.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with complex numbers",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||||
|
"keywords": [
|
||||||
|
"complex",
|
||||||
|
"mathematics"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2022-12-06T16:21:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "markbaker/matrix",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpdocumentor/phpdocumentor": "2.*",
|
||||||
|
"phploc/phploc": "^4.0",
|
||||||
|
"phpmd/phpmd": "2.*",
|
||||||
|
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||||
|
"sebastian/phpcpd": "^4.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Matrix\\": "classes/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"email": "mark@demon-angel.eu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP Class for working with matrices",
|
||||||
|
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||||
|
"keywords": [
|
||||||
|
"mathematics",
|
||||||
|
"matrix",
|
||||||
|
"vector"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||||
|
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||||
|
},
|
||||||
|
"time": "2022-12-02T22:17:43+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phpmailer/phpmailer",
|
||||||
|
"version": "v6.10.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -77,7 +341,7 @@
|
|||||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
|
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -85,7 +349,323 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-11-24T18:04:13+00:00"
|
"time": "2025-04-24T15:19:31+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phpoffice/phpspreadsheet",
|
||||||
|
"version": "2.3.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||||
|
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||||
|
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/pcre": "^1 || ^2 || ^3",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||||
|
"markbaker/complex": "^3.0",
|
||||||
|
"markbaker/matrix": "^3.0",
|
||||||
|
"php": "^8.1",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"psr/http-factory": "^1.0",
|
||||||
|
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||||
|
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"mitoteam/jpgraph": "^10.3",
|
||||||
|
"mpdf/mpdf": "^8.1.1",
|
||||||
|
"phpcompatibility/php-compatibility": "^9.3",
|
||||||
|
"phpstan/phpstan": "^1.1",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
|
"phpunit/phpunit": "^9.6 || ^10.5",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
|
"tecnickcom/tcpdf": "^6.5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"ext-intl": "PHP Internationalization Functions",
|
||||||
|
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||||
|
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||||
|
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Maarten Balliauw",
|
||||||
|
"homepage": "https://blog.maartenballiauw.be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mark Baker",
|
||||||
|
"homepage": "https://markbakeruk.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Franck Lefevre",
|
||||||
|
"homepage": "https://rootslabs.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Erik Tilt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adrien Crivelli"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||||
|
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||||
|
"keywords": [
|
||||||
|
"OpenXML",
|
||||||
|
"excel",
|
||||||
|
"gnumeric",
|
||||||
|
"ods",
|
||||||
|
"php",
|
||||||
|
"spreadsheet",
|
||||||
|
"xls",
|
||||||
|
"xlsx"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||||
|
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.3.8"
|
||||||
|
},
|
||||||
|
"time": "2025-02-08T03:01:45+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-client",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-client.git",
|
||||||
|
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||||
|
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.0 || ^8.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Client\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP clients",
|
||||||
|
"homepage": "https://github.com/php-fig/http-client",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-client",
|
||||||
|
"psr",
|
||||||
|
"psr-18"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-client"
|
||||||
|
},
|
||||||
|
"time": "2023-09-23T14:17:50+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-factory",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-factory.git",
|
||||||
|
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||||
|
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
|
||||||
|
"keywords": [
|
||||||
|
"factory",
|
||||||
|
"http",
|
||||||
|
"message",
|
||||||
|
"psr",
|
||||||
|
"psr-17",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-factory"
|
||||||
|
},
|
||||||
|
"time": "2024-04-15T12:06:14+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||||
|
},
|
||||||
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/simple-cache",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/simple-cache.git",
|
||||||
|
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||||
|
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\SimpleCache\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interfaces for simple caching",
|
||||||
|
"keywords": [
|
||||||
|
"cache",
|
||||||
|
"caching",
|
||||||
|
"psr",
|
||||||
|
"psr-16",
|
||||||
|
"simple-cache"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2021-10-29T13:26:27+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
@@ -95,10 +675,10 @@
|
|||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.3",
|
||||||
"ext-pdo": "*",
|
"ext-json": "*",
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-json": "*"
|
"ext-pdo": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
|
|||||||
27
api/create_table_x_departements_contours.sql
Normal file
27
api/create_table_x_departements_contours.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- Script de création de la table x_departements_contours
|
||||||
|
-- À exécuter manuellement en tant qu'administrateur de la base de données
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
|
||||||
|
`nom_dept` varchar(100) NOT NULL,
|
||||||
|
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
|
||||||
|
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
|
||||||
|
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
|
||||||
|
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
|
||||||
|
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||||
|
SPATIAL KEY `idx_contour` (`contour`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Contours géographiques des départements français';
|
||||||
|
|
||||||
|
-- Index pour améliorer les performances des requêtes par bounding box
|
||||||
|
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
|
||||||
|
|
||||||
|
-- Vérifier que la table a été créée
|
||||||
|
SHOW CREATE TABLE x_departements_contours\G
|
||||||
|
|
||||||
|
-- Vérifier les index
|
||||||
|
SHOW INDEX FROM x_departements_contours;
|
||||||
@@ -10,7 +10,7 @@ set -euo pipefail
|
|||||||
JUMP_USER="root"
|
JUMP_USER="root"
|
||||||
JUMP_HOST="195.154.80.116"
|
JUMP_HOST="195.154.80.116"
|
||||||
JUMP_PORT="22"
|
JUMP_PORT="22"
|
||||||
JUMP_KEY="/Users/pierre/.ssh/id_rsa_mbpi"
|
JUMP_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||||
|
|
||||||
# Paramètres du container Incus
|
# Paramètres du container Incus
|
||||||
INCUS_PROJECT=default
|
INCUS_PROJECT=default
|
||||||
@@ -73,6 +73,7 @@ fi
|
|||||||
|
|
||||||
# Étape 0: Définir le nom de l'archive
|
# Étape 0: Définir le nom de l'archive
|
||||||
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
|
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
|
||||||
|
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||||
echo_info "Archive name will be: $ARCHIVE_NAME"
|
echo_info "Archive name will be: $ARCHIVE_NAME"
|
||||||
|
|
||||||
# Étape 1: Créer une archive du projet
|
# Étape 1: Créer une archive du projet
|
||||||
@@ -88,18 +89,24 @@ tar --exclude='.git' \
|
|||||||
--exclude='.DS_Store' \
|
--exclude='.DS_Store' \
|
||||||
--exclude='README.md' \
|
--exclude='README.md' \
|
||||||
--exclude="*.tar.gz" \
|
--exclude="*.tar.gz" \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='vendor' \
|
||||||
|
--exclude='*.swp' \
|
||||||
|
--exclude='*.swo' \
|
||||||
|
--exclude='*~' \
|
||||||
|
--warning=no-file-changed \
|
||||||
--no-xattrs \
|
--no-xattrs \
|
||||||
-czf "${ARCHIVE_NAME}" . || echo_error "Failed to create archive"
|
-czf "${TEMP_ARCHIVE}" . || echo_error "Failed to create archive"
|
||||||
|
|
||||||
# Vérifier la taille de l'archive
|
# Vérifier la taille de l'archive
|
||||||
ARCHIVE_SIZE=$(du -h "${ARCHIVE_NAME}" | cut -f1)
|
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
||||||
|
|
||||||
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
|
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
|
||||||
|
|
||||||
# Étape 2: Copier l'archive vers le serveur de saut
|
# Étape 2: Copier l'archive vers le serveur de saut
|
||||||
echo_step "Copying archive to jump server..."
|
echo_step "Copying archive to jump server..."
|
||||||
echo_info "Archive size: $ARCHIVE_SIZE"
|
echo_info "Archive size: $ARCHIVE_SIZE"
|
||||||
scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${ARCHIVE_NAME}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server"
|
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"
|
||||||
|
|
||||||
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
|
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
|
||||||
echo_step "Deploying to Incus container..."
|
echo_step "Deploying to Incus container..."
|
||||||
@@ -128,13 +135,19 @@ $SSH_JUMP_CMD "
|
|||||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/logs || exit 1
|
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
|
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/logs -type f -exec chmod 664 {} \; || exit 1
|
||||||
|
|
||||||
|
echo '📁 Création des dossiers uploads...'
|
||||||
|
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH}/uploads || exit 1
|
||||||
|
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${FINAL_PATH}/uploads || exit 1
|
||||||
|
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/uploads || exit 1
|
||||||
|
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/uploads -type f -exec chmod -R 664 {} \; || exit 1
|
||||||
|
|
||||||
echo '🧹 Nettoyage...'
|
echo '🧹 Nettoyage...'
|
||||||
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||||
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||||
"
|
"
|
||||||
|
|
||||||
# Nettoyage local
|
# Nettoyage local
|
||||||
rm -f "${ARCHIVE_NAME}"
|
rm -f "${TEMP_ARCHIVE}"
|
||||||
|
|
||||||
# Résumé final
|
# Résumé final
|
||||||
echo_step "Deployment completed successfully."
|
echo_step "Deployment completed successfully."
|
||||||
|
|||||||
0
api/docs/CDC.md
Normal file → Executable file
0
api/docs/CDC.md
Normal file → Executable file
276
api/docs/EXPORT-SYSTEM.md
Executable file
276
api/docs/EXPORT-SYSTEM.md
Executable file
@@ -0,0 +1,276 @@
|
|||||||
|
# Système d'Export/Import d'Opérations - Geosector API
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le système d'export/import permet de sauvegarder et restaurer des opérations complètes avec toutes leurs données associées (passages, utilisateurs, secteurs, relations).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Routes API
|
||||||
|
|
||||||
|
#### Exports
|
||||||
|
|
||||||
|
- `GET /api/operations/{id}/export/excel` - Export Excel (consultation)
|
||||||
|
- `GET /api/operations/{id}/export/json` - Export JSON (sauvegarde)
|
||||||
|
- `GET /api/operations/{id}/export/full` - Export combiné (Excel + JSON)
|
||||||
|
|
||||||
|
#### Gestion des sauvegardes
|
||||||
|
|
||||||
|
- `GET /api/operations/{id}/backups` - Liste des sauvegardes
|
||||||
|
- `GET /api/operations/{id}/backups/{backup_id}` - Télécharger une sauvegarde
|
||||||
|
- `DELETE /api/operations/{id}/backups/{backup_id}` - Supprimer une sauvegarde
|
||||||
|
|
||||||
|
### Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
uploads/entites/{entite_id}/operations/{operation_id}/documents/exports/
|
||||||
|
├── excel/
|
||||||
|
│ └── geosector-export-{operation_id}-{timestamp}.xlsx
|
||||||
|
└── json/
|
||||||
|
└── geosector-backup-{operation_id}-{type}-{timestamp}.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export Excel
|
||||||
|
|
||||||
|
### Contenu
|
||||||
|
|
||||||
|
Le fichier Excel contient 4 feuilles :
|
||||||
|
|
||||||
|
#### 1. Feuille "Passages"
|
||||||
|
|
||||||
|
- **Colonnes** : ID_Passage, Date, Heure, Prénom, Nom, Tournée, Type, N°, Rue, Ville, Habitat, Donateur, Email, Tél, Montant, Règlement, Remarque, FK_User, FK_Sector, FK_Operation
|
||||||
|
- **Données déchiffrées** : Noms, emails, téléphones
|
||||||
|
- **Formatage** : Dates françaises (dd/mm/yyyy), types traduits
|
||||||
|
|
||||||
|
#### 2. Feuille "Utilisateurs"
|
||||||
|
|
||||||
|
- **Colonnes** : ID_User, Nom, Prénom, Email, Téléphone, Mobile, Rôle, Date_création, Actif, FK_Entite
|
||||||
|
- **Données déchiffrées** : Informations personnelles
|
||||||
|
|
||||||
|
#### 3. Feuille "Secteurs"
|
||||||
|
|
||||||
|
- **Colonnes** : ID_Sector, Libellé, Couleur, Date_création, Actif, FK_Operation
|
||||||
|
|
||||||
|
#### 4. Feuille "Secteurs-Utilisateurs"
|
||||||
|
|
||||||
|
- **Colonnes** : ID_Relation, FK_Sector, Nom_Secteur, FK_User, Nom_Utilisateur, Date_assignation, FK_Operation
|
||||||
|
|
||||||
|
### Paramètres optionnels
|
||||||
|
|
||||||
|
- `?user_id={id}` - Filtrer les passages par utilisateur
|
||||||
|
|
||||||
|
### Exemple d'utilisation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export complet
|
||||||
|
GET /api/operations/2644/export/excel
|
||||||
|
|
||||||
|
# Export filtré par utilisateur
|
||||||
|
GET /api/operations/2644/export/excel?user_id=123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export JSON
|
||||||
|
|
||||||
|
### Structure du fichier JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"export_metadata": {
|
||||||
|
"version": "1.0",
|
||||||
|
"export_date": "2025-06-21T16:19:23Z",
|
||||||
|
"source_entite_id": 5,
|
||||||
|
"export_type": "full_operation"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"id": 2644,
|
||||||
|
"libelle": "OPE 2024-25",
|
||||||
|
"date_deb": "2024-09-01",
|
||||||
|
"date_fin": "2025-05-30",
|
||||||
|
"fk_entite": 5,
|
||||||
|
"chk_distinct_sectors": 1,
|
||||||
|
"created_at": "2024-08-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"users": [...],
|
||||||
|
"sectors": [...],
|
||||||
|
"passages": [...],
|
||||||
|
"user_sectors": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types d'export JSON
|
||||||
|
|
||||||
|
- **manual** : Export à la demande (par défaut)
|
||||||
|
- **auto** : Sauvegarde automatique (avant modifications importantes)
|
||||||
|
|
||||||
|
### Paramètres
|
||||||
|
|
||||||
|
- `?type=manual|auto` - Type d'export
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Contrôles d'accès
|
||||||
|
|
||||||
|
- ✅ Authentification obligatoire
|
||||||
|
- ✅ Vérification d'appartenance à l'entité
|
||||||
|
- ✅ Isolation des données par entité
|
||||||
|
- ✅ Logs détaillés de toutes les opérations
|
||||||
|
|
||||||
|
### Données sensibles
|
||||||
|
|
||||||
|
- ✅ Chiffrement/déchiffrement automatique
|
||||||
|
- ✅ Données personnelles protégées
|
||||||
|
- ✅ Pas d'exposition des clés de chiffrement
|
||||||
|
|
||||||
|
## Stockage et organisation
|
||||||
|
|
||||||
|
### Enregistrement en base
|
||||||
|
|
||||||
|
Tous les fichiers sont enregistrés dans la table `medias` :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
support = 'operation'
|
||||||
|
support_id = {operation_id}
|
||||||
|
file_type = 'xlsx' | 'json'
|
||||||
|
description = 'Export Excel opération - {libelle}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Métadonnées des fichiers
|
||||||
|
|
||||||
|
- **ID** : Identifiant unique en base
|
||||||
|
- **Filename** : Nom du fichier généré
|
||||||
|
- **Path** : Chemin relatif depuis la racine
|
||||||
|
- **Size** : Taille en octets
|
||||||
|
- **Type** : excel | json
|
||||||
|
|
||||||
|
## Exemples de réponses API
|
||||||
|
|
||||||
|
### Export Excel réussi
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Export Excel généré avec succès",
|
||||||
|
"file": {
|
||||||
|
"id": 123,
|
||||||
|
"filename": "geosector-export-2644-20250621-161923.xlsx",
|
||||||
|
"path": "uploads/entites/5/operations/2644/documents/exports/excel/geosector-export-2644-20250621-161923.xlsx",
|
||||||
|
"size": 45678,
|
||||||
|
"type": "excel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export complet réussi
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Export complet généré avec succès",
|
||||||
|
"files": {
|
||||||
|
"excel": {
|
||||||
|
"id": 123,
|
||||||
|
"filename": "geosector-export-2644-20250621-161923.xlsx",
|
||||||
|
"path": "uploads/entites/5/operations/2644/documents/exports/excel/geosector-export-2644-20250621-161923.xlsx",
|
||||||
|
"size": 45678,
|
||||||
|
"type": "excel"
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"id": 124,
|
||||||
|
"filename": "geosector-backup-2644-manual-20250621-161923.json",
|
||||||
|
"path": "uploads/entites/5/operations/2644/documents/exports/json/geosector-backup-2644-manual-20250621-161923.json",
|
||||||
|
"size": 12345,
|
||||||
|
"type": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Liste des sauvegardes
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"backups": [
|
||||||
|
{
|
||||||
|
"id": 124,
|
||||||
|
"fichier": "geosector-backup-2644-manual-20250621-161923.json",
|
||||||
|
"file_type": "json",
|
||||||
|
"file_size": 12345,
|
||||||
|
"description": "Sauvegarde JSON opération - manual - OPE 2024-25",
|
||||||
|
"created_at": "2025-06-21 16:19:23",
|
||||||
|
"fk_user_creat": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"fichier": "geosector-export-2644-20250621-161923.xlsx",
|
||||||
|
"file_type": "xlsx",
|
||||||
|
"file_size": 45678,
|
||||||
|
"description": "Export Excel opération - OPE 2024-25",
|
||||||
|
"created_at": "2025-06-21 16:19:23",
|
||||||
|
"fk_user_creat": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation et dépendances
|
||||||
|
|
||||||
|
### PhpSpreadsheet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require phpoffice/phpspreadsheet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permissions de dossiers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 755 uploads/
|
||||||
|
chmod 755 uploads/entites/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des erreurs
|
||||||
|
|
||||||
|
### Erreurs courantes
|
||||||
|
|
||||||
|
- **401** : Non authentifié
|
||||||
|
- **403** : Pas d'accès à l'entité
|
||||||
|
- **404** : Opération non trouvée
|
||||||
|
- **500** : Erreur de génération
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Tous les événements sont loggés via `LogService` :
|
||||||
|
|
||||||
|
- Exports réussis (level: info)
|
||||||
|
- Erreurs de génération (level: error)
|
||||||
|
- Tentatives d'accès non autorisées (level: warning)
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Nettoyage automatique (à implémenter)
|
||||||
|
|
||||||
|
- Sauvegardes auto > 30 jours
|
||||||
|
- Fichiers temporaires > 24h
|
||||||
|
- Vérification cohérence base/fichiers
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- Espace disque utilisé
|
||||||
|
- Nombre de fichiers par entité
|
||||||
|
- Fréquence des exports
|
||||||
|
|
||||||
|
## Évolutions futures
|
||||||
|
|
||||||
|
### Import/Restauration
|
||||||
|
|
||||||
|
- Validation des fichiers JSON
|
||||||
|
- Import transactionnel
|
||||||
|
- Gestion des conflits d'IDs
|
||||||
|
- Mapping entités source/cible
|
||||||
|
|
||||||
|
### Optimisations
|
||||||
|
|
||||||
|
- Compression des fichiers
|
||||||
|
- Export asynchrone pour gros volumes
|
||||||
|
- Cache des exports fréquents
|
||||||
|
- API de streaming pour téléchargements
|
||||||
376
api/docs/FILE-SYSTEM-API.md
Executable file
376
api/docs/FILE-SYSTEM-API.md
Executable file
@@ -0,0 +1,376 @@
|
|||||||
|
# API de Gestion des Fichiers - Geosector
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
L'API de gestion des fichiers permet aux administrateurs de naviguer, rechercher et gérer les fichiers stockés dans l'application Geosector avec des contrôles d'accès basés sur les rôles.
|
||||||
|
|
||||||
|
## Contrôles d'accès
|
||||||
|
|
||||||
|
### Rôle 2 (Admin d'entité)
|
||||||
|
|
||||||
|
- Accès limité aux fichiers de son entité uniquement
|
||||||
|
- Chemin racine : `/uploads/entites/{son_entite_id}/`
|
||||||
|
- Peut naviguer dans tous les sous-dossiers de son entité
|
||||||
|
|
||||||
|
### Rôle > 2 (Super admin)
|
||||||
|
|
||||||
|
- Accès complet à tous les fichiers
|
||||||
|
- Chemin racine : `/uploads/` (accès total)
|
||||||
|
- Peut naviguer dans toutes les entités et dossiers système
|
||||||
|
|
||||||
|
## Routes disponibles
|
||||||
|
|
||||||
|
### Navigation et listing
|
||||||
|
|
||||||
|
#### `GET /api/files/browse`
|
||||||
|
|
||||||
|
Navigation dans l'arborescence avec recherche et pagination.
|
||||||
|
|
||||||
|
**Paramètres de requête :**
|
||||||
|
|
||||||
|
- `path` (string) : Chemin à explorer (ex: `entites/5/operations`)
|
||||||
|
- `page` (int) : Page (défaut: 1)
|
||||||
|
- `per_page` (int) : Éléments par page (défaut: 50, max: 100)
|
||||||
|
- `search` (string) : Recherche dans nom, nom original, description
|
||||||
|
- `type` (string) : Filtrage par extension (pdf, jpg, xlsx, etc.)
|
||||||
|
- `category` (string) : Filtrage par catégorie métier
|
||||||
|
- `sort` (string) : Tri (name, date, size, type) - défaut: date
|
||||||
|
- `order` (string) : Ordre (asc, desc) - défaut: desc
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/browse?path=entites/5/operations&search=2024&type=xlsx&page=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"current_path": "entites/5/operations",
|
||||||
|
"parent_path": "entites/5",
|
||||||
|
"pagination": {
|
||||||
|
"current_page": 1,
|
||||||
|
"per_page": 50,
|
||||||
|
"total_items": 127,
|
||||||
|
"total_pages": 3,
|
||||||
|
"has_next": true,
|
||||||
|
"has_prev": false
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"search": "2024",
|
||||||
|
"type": "xlsx",
|
||||||
|
"category": null,
|
||||||
|
"sort": "date",
|
||||||
|
"order": "desc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"fichier": "planning_2024_op2644.xlsx",
|
||||||
|
"original_name": "Planning Opération 2024.xlsx",
|
||||||
|
"file_type": "xlsx",
|
||||||
|
"file_category": "planning",
|
||||||
|
"description": "Planning détaillé opération 2024",
|
||||||
|
"file_size": 1024000,
|
||||||
|
"file_path": "entites/5/operations/2644/documents/planning_2024_op2644.xlsx",
|
||||||
|
"created_at": "2025-06-22 08:30:00",
|
||||||
|
"creator_name": "Jean Dupont"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total_files": 45,
|
||||||
|
"total_size": 25600000,
|
||||||
|
"by_category": {
|
||||||
|
"planning": 12,
|
||||||
|
"export": 20,
|
||||||
|
"backup": 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/files/list/{support}/{id}`
|
||||||
|
|
||||||
|
Liste des fichiers par support (entite, user, operation, passage).
|
||||||
|
|
||||||
|
**Paramètres :**
|
||||||
|
|
||||||
|
- `support` : Type de support (entite, user, operation, passage)
|
||||||
|
- `id` : ID de l'élément
|
||||||
|
- Mêmes paramètres de requête que `/browse`
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/list/operation/2644?category=export&page=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recherche
|
||||||
|
|
||||||
|
#### `GET /api/files/search`
|
||||||
|
|
||||||
|
Recherche globale dans tous les fichiers accessibles.
|
||||||
|
|
||||||
|
**Paramètres de requête :**
|
||||||
|
|
||||||
|
- `q` (string, requis) : Terme de recherche
|
||||||
|
- `page`, `per_page`, `type`, `category`, `sort`, `order` : Mêmes que browse
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/search?q=planning&type=xlsx&category=planning
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actions sur fichiers
|
||||||
|
|
||||||
|
#### `GET /api/files/download/{id}`
|
||||||
|
|
||||||
|
Téléchargement sécurisé d'un fichier.
|
||||||
|
|
||||||
|
**Réponse :** Fichier en téléchargement direct avec headers appropriés.
|
||||||
|
|
||||||
|
#### `DELETE /api/files/{id}`
|
||||||
|
|
||||||
|
Suppression sécurisée d'un fichier (physique + base de données).
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Fichier supprimé avec succès"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `GET /api/files/info/{id}`
|
||||||
|
|
||||||
|
Informations détaillées d'un fichier.
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"file": {
|
||||||
|
"id": 123,
|
||||||
|
"fichier": "planning_2024.xlsx",
|
||||||
|
"original_name": "Planning Opération 2024.xlsx",
|
||||||
|
"file_type": "xlsx",
|
||||||
|
"file_category": "planning",
|
||||||
|
"file_size": 1024000,
|
||||||
|
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"description": "Planning détaillé",
|
||||||
|
"support": "operation",
|
||||||
|
"support_id": 2644,
|
||||||
|
"fk_entite": 5,
|
||||||
|
"created_at": "2025-06-22 08:30:00",
|
||||||
|
"updated_at": "2025-06-22 08:30:00",
|
||||||
|
"creator_name": "Jean Dupont",
|
||||||
|
"modifier_name": null,
|
||||||
|
"file_exists": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Statistiques
|
||||||
|
|
||||||
|
#### `GET /api/files/stats`
|
||||||
|
|
||||||
|
Statistiques d'utilisation des fichiers.
|
||||||
|
|
||||||
|
**Pour admin d'entité (rôle 2) :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"entite_id": 5,
|
||||||
|
"storage": {
|
||||||
|
"total_files": 245,
|
||||||
|
"total_size": 157286400,
|
||||||
|
"by_support": {
|
||||||
|
"entite": { "count": 12, "size": 45000000 },
|
||||||
|
"operation": { "count": 180, "size": 98000000 },
|
||||||
|
"user": { "count": 45, "size": 12000000 },
|
||||||
|
"passage": { "count": 8, "size": 2286400 }
|
||||||
|
},
|
||||||
|
"by_category": {
|
||||||
|
"document": 25,
|
||||||
|
"export": 120,
|
||||||
|
"avatar": 45,
|
||||||
|
"photo": 55
|
||||||
|
},
|
||||||
|
"by_type": {
|
||||||
|
"xlsx": 85,
|
||||||
|
"jpg": 120,
|
||||||
|
"pdf": 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pour super admin (rôle > 2) :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"global_stats": {
|
||||||
|
"total_files": 2450,
|
||||||
|
"total_size": 1572864000,
|
||||||
|
"entites_count": 25,
|
||||||
|
"by_entite": [
|
||||||
|
{ "entite_id": 5, "files": 245, "size": 157286400 },
|
||||||
|
{ "entite_id": 12, "files": 180, "size": 98000000 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Métadonnées
|
||||||
|
|
||||||
|
#### `GET /api/files/metadata`
|
||||||
|
|
||||||
|
Informations sur les catégories, extensions et limites autorisées.
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"categories": {
|
||||||
|
"entite": ["logo", "document", "reglement", "statut"],
|
||||||
|
"user": ["avatar", "photo"],
|
||||||
|
"operation": ["planning", "liste", "export", "backup"],
|
||||||
|
"passage": ["recu", "photo", "justificatif", "carte"]
|
||||||
|
},
|
||||||
|
"extensions": ["pdf", "jpg", "jpeg", "png", "gif", "webp", "xlsx", "xls", "json", "csv"],
|
||||||
|
"mime_types": {
|
||||||
|
"pdf": "application/pdf",
|
||||||
|
"jpg": "image/jpeg",
|
||||||
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
},
|
||||||
|
"max_file_sizes": {
|
||||||
|
"entite": 20971520, // 20 MB
|
||||||
|
"user": 5242880, // 5 MB
|
||||||
|
"operation": 20971520, // 20 MB
|
||||||
|
"passage": 10485760 // 10 MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catégories de fichiers
|
||||||
|
|
||||||
|
### Distinction Extension vs Catégorie
|
||||||
|
|
||||||
|
- **Extension** (`file_type`) : Type technique (pdf, jpg, xlsx, png, etc.)
|
||||||
|
- **Catégorie** (`file_category`) : Type métier (logo, carte, photo, document, planning, etc.)
|
||||||
|
|
||||||
|
### Catégories par support
|
||||||
|
|
||||||
|
#### Entité
|
||||||
|
|
||||||
|
- `logo` : Logo de l'entité
|
||||||
|
- `document` : Documents généraux
|
||||||
|
- `reglement` : Règlements internes
|
||||||
|
- `statut` : Statuts de l'entité
|
||||||
|
|
||||||
|
#### Utilisateur
|
||||||
|
|
||||||
|
- `avatar` : Photo de profil
|
||||||
|
- `photo` : Photos diverses
|
||||||
|
|
||||||
|
#### Opération
|
||||||
|
|
||||||
|
- `planning` : Plannings d'opération
|
||||||
|
- `liste` : Listes diverses
|
||||||
|
- `export` : Exports de données
|
||||||
|
- `backup` : Sauvegardes automatiques
|
||||||
|
|
||||||
|
#### Passage
|
||||||
|
|
||||||
|
- `recu` : Reçus de passage
|
||||||
|
- `photo` : Photos de passage
|
||||||
|
- `justificatif` : Justificatifs divers
|
||||||
|
- `carte` : Cartes et plans
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Validation des chemins
|
||||||
|
|
||||||
|
- Empêche les traversées de répertoire (`../`)
|
||||||
|
- Validation stricte selon le rôle utilisateur
|
||||||
|
- Contrôle d'accès au niveau fichier
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- Tous les téléchargements sont loggés
|
||||||
|
- Toutes les suppressions sont tracées
|
||||||
|
- Erreurs d'accès enregistrées
|
||||||
|
|
||||||
|
### Contrôles d'intégrité
|
||||||
|
|
||||||
|
- Vérification de l'existence physique des fichiers
|
||||||
|
- Validation des permissions avant chaque action
|
||||||
|
- Contrôle de cohérence base/fichiers
|
||||||
|
|
||||||
|
## Exemples d'utilisation
|
||||||
|
|
||||||
|
### Navigation dans les opérations d'une entité
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/browse?path=entites/5/operations&sort=name&order=asc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recherche de tous les exports Excel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/search?q=export&type=xlsx&category=export
|
||||||
|
```
|
||||||
|
|
||||||
|
### Statistiques de stockage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Téléchargement d'un fichier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/files/download/123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suppression d'un fichier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE /api/files/123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Codes d'erreur
|
||||||
|
|
||||||
|
- **401** : Non authentifié
|
||||||
|
- **403** : Accès refusé (rôle insuffisant ou fichier d'une autre entité)
|
||||||
|
- **404** : Fichier ou chemin non trouvé
|
||||||
|
- **400** : Paramètres invalides (terme de recherche manquant, etc.)
|
||||||
|
- **500** : Erreur serveur
|
||||||
|
|
||||||
|
## Migration base de données
|
||||||
|
|
||||||
|
Pour utiliser le système, exécuter la migration :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Ajout de la colonne file_category
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD COLUMN `file_category` varchar(50) DEFAULT NULL COMMENT 'Catégorie du fichier (logo, carte, photo, document, etc.)' AFTER `file_type`;
|
||||||
|
|
||||||
|
-- Index pour optimiser les requêtes
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD INDEX `idx_file_category` (`file_category`);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : Juin 2025
|
||||||
|
**Auteur** : API Geosector Team
|
||||||
430
api/docs/GESTION-SECTORS.md
Normal file
430
api/docs/GESTION-SECTORS.md
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
# GESTION-SECTORS.md
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Ce document décrit le système de gestion des secteurs dans l'API Geosector, incluant la connexion aux bases de données d'adresses externes, la validation des limites départementales, et le processus complet de création de secteurs avec génération automatique des passages.
|
||||||
|
|
||||||
|
## Évolutions récentes
|
||||||
|
|
||||||
|
### Gestion des sessions
|
||||||
|
- La session stocke maintenant `entity_id` depuis `fk_entite` lors du login
|
||||||
|
- Méthode `Session::getEntityId()` disponible pour récupérer l'ID de l'entité
|
||||||
|
- Utilisation cohérente de l'entity_id dans toutes les opérations
|
||||||
|
|
||||||
|
### Gestion des passages orphelins
|
||||||
|
- Les passages avec `fk_sector = 0` sont automatiquement intégrés au nouveau secteur
|
||||||
|
- Évite les doublons pour les passages ayant déjà une `fk_adresse`
|
||||||
|
- Mise à jour atomique dans la transaction de création du secteur
|
||||||
|
|
||||||
|
## Architecture multi-bases
|
||||||
|
|
||||||
|
### Bases de données principales
|
||||||
|
|
||||||
|
1. **Base principale** (`geosector_app`)
|
||||||
|
- Contient toutes les tables de l'application
|
||||||
|
- Tables concernées : `ope_sectors`, `sectors_adresses`, `ope_pass`, `ope_users_sectors`, `x_departements_contours`
|
||||||
|
|
||||||
|
2. **Base adresses** (dans conteneurs Incus séparés)
|
||||||
|
- DVA : `dva-maria` (13.23.33.46) - base `adresses`
|
||||||
|
- RCA : `rca-maria` (13.23.33.36) - base `adresses`
|
||||||
|
- PRA : `pra-maria` (13.23.33.26) - base `adresses`
|
||||||
|
- Credentials : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||||
|
- Tables par département : `cp22`, `cp23`, etc.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Dans `src/Config/AppConfig.php` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
'addresses_database' => [
|
||||||
|
'host' => '13.23.33.46', // Varie selon l'environnement
|
||||||
|
'name' => 'adresses',
|
||||||
|
'username' => 'adr_geo_user',
|
||||||
|
'password' => 'd66,AdrGeoDev.User',
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des contours départementaux
|
||||||
|
|
||||||
|
### Table x_departements_contours
|
||||||
|
|
||||||
|
Création manuelle de la table (sans DROP permissions) :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`code_dept` varchar(3) NOT NULL,
|
||||||
|
`nom_dept` varchar(100) NOT NULL,
|
||||||
|
`contour` GEOMETRY NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||||
|
SPATIAL KEY `idx_contour` (`contour`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||||
|
COMMENT='Contours géographiques des départements français';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import des contours
|
||||||
|
|
||||||
|
1. **Fichier source** : `docs/contour-des-departements.geojson` (depuis data.gouv.fr)
|
||||||
|
2. **Import automatique** : Uniquement lors de la connexion de l'admin `d6soft`
|
||||||
|
3. **Script** : `scripts/init_departements_contours.php`
|
||||||
|
4. **Résultat** : 96 départements importés avec support Polygon et MultiPolygon
|
||||||
|
|
||||||
|
## Services principaux
|
||||||
|
|
||||||
|
### AddressService
|
||||||
|
|
||||||
|
Gère la récupération des adresses depuis la base externe :
|
||||||
|
|
||||||
|
```php
|
||||||
|
class AddressService {
|
||||||
|
// Récupère toutes les adresses dans un polygone
|
||||||
|
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
|
||||||
|
|
||||||
|
// Compte les adresses dans un polygone
|
||||||
|
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caractéristiques** :
|
||||||
|
- Détection automatique des départements touchés par le secteur
|
||||||
|
- Interrogation de toutes les tables cp{dept} concernées
|
||||||
|
- Gestion des secteurs multi-départements
|
||||||
|
|
||||||
|
### DepartmentBoundaryService
|
||||||
|
|
||||||
|
Vérifie les limites départementales des secteurs :
|
||||||
|
|
||||||
|
```php
|
||||||
|
class DepartmentBoundaryService {
|
||||||
|
// Vérifie si un secteur est contenu dans un département
|
||||||
|
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array
|
||||||
|
|
||||||
|
// Liste tous les départements touchés par un secteur
|
||||||
|
public function getDepartmentsForSector(array $sectorCoordinates): array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retour type** :
|
||||||
|
```php
|
||||||
|
[
|
||||||
|
'is_contained' => bool,
|
||||||
|
'message' => string,
|
||||||
|
'intersecting_departments' => [
|
||||||
|
['code_dept' => '22', 'nom_dept' => 'Côtes-d\'Armor', 'percentage_overlap' => 75.5],
|
||||||
|
['code_dept' => '29', 'nom_dept' => 'Finistère', 'percentage_overlap' => 24.5]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Processus de création de secteur
|
||||||
|
|
||||||
|
### 1. Structure du payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": 123,
|
||||||
|
"fk_entite": 45,
|
||||||
|
"operation_id": 789,
|
||||||
|
"sector": {
|
||||||
|
"id": 0,
|
||||||
|
"libelle": "Secteur Centre-Ville",
|
||||||
|
"color": "#FF5733",
|
||||||
|
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||||
|
},
|
||||||
|
"users": [12, 34, 56, 78]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Étapes de création
|
||||||
|
|
||||||
|
1. **Validation** des données et de l'opération
|
||||||
|
2. **Vérification** des limites départementales (warning si débordement)
|
||||||
|
3. **Début de transaction** pour garantir la cohérence des données
|
||||||
|
4. **Insertion** du secteur dans `ope_sectors`
|
||||||
|
5. **Affectation** des utilisateurs dans `ope_users_sectors` avec :
|
||||||
|
- `fk_operation`, `fk_user`, `fk_sector`
|
||||||
|
- `created_at`, `fk_user_creat`, `chk_active = 1`
|
||||||
|
6. **Intégration des passages orphelins** :
|
||||||
|
- Recherche des passages avec `fk_sector = 0` dans le polygone
|
||||||
|
- Mise à jour de leur `fk_sector` vers le nouveau secteur
|
||||||
|
- Exclusion des passages ayant déjà une `fk_adresse`
|
||||||
|
7. **Récupération** des adresses via `AddressService`
|
||||||
|
8. **Stockage** des adresses dans `sectors_adresses`
|
||||||
|
9. **Création** des passages dans `ope_pass` pour chaque adresse :
|
||||||
|
- Affectés au premier utilisateur de la liste
|
||||||
|
- Avec toutes les FK nécessaires (entité, opération, secteur, user)
|
||||||
|
- Données d'adresse complètes
|
||||||
|
10. **Commit** de la transaction ou **rollback** en cas d'erreur
|
||||||
|
|
||||||
|
### 3. Réponse API pour CREATE
|
||||||
|
|
||||||
|
**Format standardisé** : Les données sont placées à la racine, sans groupe "data" intermédiaire.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Secteur créé avec succès",
|
||||||
|
"sector": {
|
||||||
|
"id": 123,
|
||||||
|
"libelle": "Secteur Centre-Ville",
|
||||||
|
"color": "#FF5733",
|
||||||
|
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||||
|
},
|
||||||
|
"passages_sector": [
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"fk_operation": 789,
|
||||||
|
"fk_sector": 123,
|
||||||
|
"fk_user": 12,
|
||||||
|
"fk_type": 2,
|
||||||
|
"fk_adresse": "cp22.12345",
|
||||||
|
"passed_at": null,
|
||||||
|
"numero": "10",
|
||||||
|
"rue": "Rue de la Paix",
|
||||||
|
"rue_bis": "",
|
||||||
|
"ville": "Saint-Brieuc",
|
||||||
|
"residence": null,
|
||||||
|
"fk_habitat": null,
|
||||||
|
"appt": null,
|
||||||
|
"niveau": null,
|
||||||
|
"gps_lat": "48.117266",
|
||||||
|
"gps_lng": "-1.6777926",
|
||||||
|
"nom_recu": null,
|
||||||
|
"name": "", // Décrypté depuis encrypted_name
|
||||||
|
"remarque": null,
|
||||||
|
"email": "", // Décrypté depuis encrypted_email
|
||||||
|
"phone": "", // Décrypté depuis encrypted_phone
|
||||||
|
"montant": null,
|
||||||
|
"fk_type_reglement": null,
|
||||||
|
"email_erreur": null,
|
||||||
|
"nb_passages": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"passages_integrated": 5, // Passages orphelins intégrés
|
||||||
|
"passages_created": 10, // Nouveaux passages créés
|
||||||
|
"users_sectors": [
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"first_name": "Jean",
|
||||||
|
"sect_name": "JDU",
|
||||||
|
"fk_sector": 123,
|
||||||
|
"name": "Dupont" // Décrypté depuis encrypted_name
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Réponse API pour UPDATE
|
||||||
|
|
||||||
|
La réponse est identique à CREATE avec des compteurs supplémentaires :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Secteur modifié avec succès",
|
||||||
|
"sector": {
|
||||||
|
"id": 123,
|
||||||
|
"libelle": "Secteur Centre-Ville Modifié",
|
||||||
|
"color": "#FF5733",
|
||||||
|
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||||
|
},
|
||||||
|
"passages_sector": [
|
||||||
|
// Liste complète de TOUS les passages actuels du secteur
|
||||||
|
],
|
||||||
|
"passages_orphaned": 3, // Passages mis en orphelin (hors polygone)
|
||||||
|
"passages_updated": 5, // Passages mis à jour avec fk_adresse
|
||||||
|
"passages_created": 10, // Nouveaux passages créés
|
||||||
|
"passages_total": 25, // Nombre total de passages dans le secteur
|
||||||
|
"users_sectors": [
|
||||||
|
// Liste des utilisateurs affectés
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes importantes** :
|
||||||
|
- Les champs sensibles (name, email, phone) sont stockés cryptés et décryptés à la volée
|
||||||
|
- La structure est identique entre CREATE et UPDATE pour faciliter l'intégration
|
||||||
|
- Tous les champs sont retournés, même s'ils sont null
|
||||||
|
- Code HTTP : 201 pour CREATE, 200 pour UPDATE
|
||||||
|
|
||||||
|
## Gestion des secteurs multi-départements
|
||||||
|
|
||||||
|
### Détection automatique
|
||||||
|
|
||||||
|
Le système détecte automatiquement quand un secteur touche plusieurs départements :
|
||||||
|
|
||||||
|
1. **Analyse spatiale** : Utilisation de `ST_Intersects` pour identifier tous les départements touchés
|
||||||
|
2. **Calcul de pourcentage** : `ST_Area(ST_Intersection)` pour calculer le % de recouvrement
|
||||||
|
3. **Interrogation multi-tables** : Requête sur toutes les tables cp{dept} concernées
|
||||||
|
|
||||||
|
### Exemple de secteur multi-départements
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Secteur à cheval sur 22 (Côtes-d'Armor) et 29 (Finistère)
|
||||||
|
$coordinates = [
|
||||||
|
[48.5778, -3.8280], // Morlaix (29)
|
||||||
|
[48.5778, -3.7280], // Vers l'est (22)
|
||||||
|
[48.4778, -3.7280],
|
||||||
|
[48.4778, -3.8280]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Le système va automatiquement :
|
||||||
|
// 1. Détecter que le secteur touche 22 et 29
|
||||||
|
// 2. Interroger cp22 et cp29 pour les adresses
|
||||||
|
// 3. Créer les passages pour toutes les adresses trouvées
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables de données
|
||||||
|
|
||||||
|
### ope_sectors
|
||||||
|
- `id` : Identifiant unique
|
||||||
|
- `libelle` : Nom du secteur
|
||||||
|
- `color` : Couleur d'affichage
|
||||||
|
- `sector` : Coordonnées (format lat/lng#lat/lng#...)
|
||||||
|
- `fk_entite` : Lien vers l'entité
|
||||||
|
|
||||||
|
### sectors_adresses
|
||||||
|
- `fk_sector` : Lien vers le secteur
|
||||||
|
- `fk_address` : ID de l'adresse dans la base externe
|
||||||
|
- `numero`, `voie`, `code_postal`, `commune`
|
||||||
|
- `latitude`, `longitude`
|
||||||
|
|
||||||
|
### ope_pass (passages)
|
||||||
|
- `fk_entite`, `fk_operation`, `fk_sector`, `fk_user`
|
||||||
|
- `numero`, `voie`, `code_postal`, `commune`
|
||||||
|
- `latitude`, `longitude`
|
||||||
|
- `created_at`, `fk_user_creat`, `chk_active`
|
||||||
|
|
||||||
|
### ope_users_sectors
|
||||||
|
- `fk_operation` : Lien vers l'opération
|
||||||
|
- `fk_user` : Lien vers l'utilisateur (ope_users)
|
||||||
|
- `fk_sector` : Lien vers le secteur
|
||||||
|
- `created_at`, `fk_user_creat`, `chk_active`
|
||||||
|
|
||||||
|
## Logs et monitoring
|
||||||
|
|
||||||
|
Le système génère des logs détaillés pour :
|
||||||
|
- Nombre d'adresses trouvées par département
|
||||||
|
- Secteurs hors limites départementales
|
||||||
|
- Passages créés avec succès
|
||||||
|
- Erreurs de connexion aux bases d'adresses
|
||||||
|
- Performance des requêtes spatiales
|
||||||
|
|
||||||
|
## Scripts de test
|
||||||
|
|
||||||
|
- `test_sector_departments.php` : Test des limites départementales
|
||||||
|
- `test_addresses_connection.php` : Test de connexion à la base d'adresses
|
||||||
|
|
||||||
|
## Notes importantes
|
||||||
|
|
||||||
|
1. **Fail-safe** : La création de secteur continue même si la base d'adresses est inaccessible
|
||||||
|
2. **Transactions** :
|
||||||
|
- Toute la création est dans une transaction pour garantir la cohérence
|
||||||
|
- Toujours vérifier `inTransaction()` avant d'appeler `rollBack()`
|
||||||
|
- Gestion correcte des erreurs PDO avec try/catch
|
||||||
|
3. **Performance** : Les requêtes spatiales utilisent des index spatiaux pour optimiser les performances
|
||||||
|
4. **Modification de secteur** : Plus complexe car nécessite de gérer les passages existants (non implémenté)
|
||||||
|
5. **Paramètres SQL** : Utiliser des noms uniques pour éviter l'erreur "Invalid parameter number"
|
||||||
|
6. **Jointures** : Les données utilisateur viennent de la table `users`, pas `ope_users` (qui n'a pas nom/prenom)
|
||||||
|
|
||||||
|
## Bilan de la gestion des adresses et passages
|
||||||
|
|
||||||
|
### Vue d'ensemble du cycle de vie
|
||||||
|
|
||||||
|
```
|
||||||
|
Base Adresses (cp22, cp23...) → sectors_adresses → ope_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. CRÉATION D'UN SECTEUR
|
||||||
|
|
||||||
|
#### Flux des données :
|
||||||
|
1. **Récupération des adresses** depuis la base externe (`AddressService`)
|
||||||
|
2. **Intégration des passages orphelins** (`fk_sector = NULL`) situés dans le polygone
|
||||||
|
3. **Stockage dans `sectors_adresses`** de toutes les adresses du polygone
|
||||||
|
4. **Création automatique de passages** (`ope_pass`) pour chaque adresse SAUF celles déjà utilisées par les passages orphelins
|
||||||
|
|
||||||
|
#### Détails :
|
||||||
|
- **Passages créés** : `fk_type = 2`, `encrypted_name = ''` (vide), affectés au premier utilisateur
|
||||||
|
- **Passages orphelins** : mis à jour avec le nouveau `fk_sector`
|
||||||
|
- **Évite les doublons** : les adresses déjà utilisées par des passages orphelins ne génèrent pas de nouveau passage
|
||||||
|
|
||||||
|
### 2. MISE À JOUR D'UN SECTEUR
|
||||||
|
|
||||||
|
#### Processus de mise à jour :
|
||||||
|
1. **Mise à jour des attributs** (libelle, color, sector)
|
||||||
|
2. **Mise à jour des membres affectés**
|
||||||
|
3. **Suppression/recréation des adresses** dans `sectors_adresses`
|
||||||
|
4. **Gestion intelligente des passages** via `updatePassagesForSector` :
|
||||||
|
|
||||||
|
#### Gestion des passages lors de l'UPDATE :
|
||||||
|
|
||||||
|
##### a) Vérification géographique des passages existants
|
||||||
|
- Pour chaque passage du secteur, vérification si ses coordonnées GPS sont dans le nouveau polygone
|
||||||
|
- **Si DANS le polygone** : Conservation du passage
|
||||||
|
- **Si HORS du polygone** : Mise en orphelin (`fk_sector = NULL`)
|
||||||
|
|
||||||
|
##### b) Traitement des nouvelles adresses
|
||||||
|
Pour chaque adresse dans `sectors_adresses` :
|
||||||
|
1. **Vérification primaire** : Recherche par `fk_adresse`
|
||||||
|
2. **Vérification secondaire** : Si pas trouvé, recherche par `numero`, `rue_bis`, `rue`, `ville`
|
||||||
|
- Si trouvé → Mise à jour du `fk_adresse` dans le(s) passage(s)
|
||||||
|
3. **Création** : Si aucun passage existant, création avec :
|
||||||
|
- `fk_type = 2`, `encrypted_name = ''`
|
||||||
|
- Affecté au premier utilisateur du secteur
|
||||||
|
- Toutes les données de l'adresse
|
||||||
|
|
||||||
|
### 3. SUPPRESSION D'UN SECTEUR
|
||||||
|
|
||||||
|
#### Traitement différencié des passages :
|
||||||
|
1. **Passages "non visités"** (`fk_type = 2` ET `encrypted_name` vide) :
|
||||||
|
- Suppression définitive de la base
|
||||||
|
- Ces passages correspondent aux adresses non visitées
|
||||||
|
|
||||||
|
2. **Passages "visités"** (tous les autres) :
|
||||||
|
- Mise à jour : `fk_sector = NULL`
|
||||||
|
- Deviennent des passages orphelins
|
||||||
|
- Conservent toutes leurs données (contact, montant, etc.)
|
||||||
|
|
||||||
|
#### Autres suppressions :
|
||||||
|
- Suppression des affectations membres (`ope_users_sectors`)
|
||||||
|
- Suppression des adresses (`sectors_adresses`)
|
||||||
|
- Suppression du secteur lui-même
|
||||||
|
|
||||||
|
### Tableau récapitulatif
|
||||||
|
|
||||||
|
| Action | sectors_adresses | ope_pass dans polygone | ope_pass hors polygone | Nouvelles adresses |
|
||||||
|
|--------|------------------|------------------------|------------------------|-------------------|
|
||||||
|
| CREATE | Insertion depuis base externe | - | Intégration si orphelins | Création automatique de passages |
|
||||||
|
| UPDATE | Suppression/recréation | Conservation | Mise en orphelin | Création si pas de passage existant |
|
||||||
|
| DELETE | Suppression totale | Suppression si non visités / Orphelin si visités | - | - |
|
||||||
|
|
||||||
|
### Points d'attention
|
||||||
|
|
||||||
|
1. **Cohérence géographique** : Lors d'un UPDATE, le système vérifie automatiquement et met en orphelin les passages hors du nouveau périmètre
|
||||||
|
2. **Passages orphelins** : Peuvent être réintégrés lors de la création d'un nouveau secteur englobant
|
||||||
|
3. **Mise à jour du fk_adresse** : Lors d'un UPDATE, les passages existants peuvent recevoir leur `fk_adresse` s'ils correspondent à une adresse
|
||||||
|
4. **Performance** : La création/mise à jour génère potentiellement des milliers de passages selon la densité d'adresses
|
||||||
|
|
||||||
|
## Erreurs communes et solutions
|
||||||
|
|
||||||
|
### "There is no active transaction"
|
||||||
|
- **Cause** : Appel à `rollBack()` sans transaction active
|
||||||
|
- **Solution** : Vérifier `$db->inTransaction()` avant rollback
|
||||||
|
|
||||||
|
### "Column not found: fk_address"
|
||||||
|
- **Cause** : La colonne s'appelle `fk_adresse` (avec 'e')
|
||||||
|
- **Solution** : Corriger les noms de colonnes dans les requêtes
|
||||||
|
|
||||||
|
### "Invalid parameter number"
|
||||||
|
- **Cause** : Réutilisation du même nom de paramètre dans une requête
|
||||||
|
- **Solution** : Utiliser des noms uniques (`:param1`, `:param2`, etc.)
|
||||||
|
|
||||||
|
### "Unknown column 'ou.nom'"
|
||||||
|
- **Cause** : La table `ope_users` n'a pas de colonnes nom/prenom
|
||||||
|
- **Solution** : Joindre avec la table `users` qui contient `encrypted_name` et `first_name`
|
||||||
|
|
||||||
|
### "Class 'ApiService' not found"
|
||||||
|
- **Cause** : Import manquant dans le controller
|
||||||
|
- **Solution** : Ajouter `use App\Services\ApiService;` et `require_once`
|
||||||
@@ -1,612 +0,0 @@
|
|||||||
# API D6MON - Documentation
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
L'API D6MON est une interface RESTful permettant d'interagir avec l'application D6MON. Cette API gère l'authentification des utilisateurs, la gestion des profils utilisateurs et la gestion des entités.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Base URL
|
|
||||||
|
|
||||||
```
|
|
||||||
https://app.d6mon.com/api/mon
|
|
||||||
```
|
|
||||||
|
|
||||||
### En-têtes requis
|
|
||||||
|
|
||||||
Pour toutes les requêtes à l'API, les en-têtes suivants sont requis :
|
|
||||||
|
|
||||||
```
|
|
||||||
Content-Type: application/json
|
|
||||||
X-App-Identifier: app.d6mon.com
|
|
||||||
X-Client-Type: mobile
|
|
||||||
```
|
|
||||||
|
|
||||||
Pour les endpoints protégés (nécessitant une authentification), ajoutez également :
|
|
||||||
|
|
||||||
```
|
|
||||||
Authorization: Bearer {token}
|
|
||||||
```
|
|
||||||
|
|
||||||
Où `{token}` est le jeton d'authentification obtenu lors de la connexion.
|
|
||||||
|
|
||||||
## Authentification
|
|
||||||
|
|
||||||
### Connexion
|
|
||||||
|
|
||||||
**Endpoint :** `POST /login`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"password": "motdepasse"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"token": "session_token_here",
|
|
||||||
"user": {
|
|
||||||
"id": 123,
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"entity_id": 456
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inscription
|
|
||||||
|
|
||||||
**Endpoint :** `POST /register`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"entity_id": 456
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Inscription réussie. Un email contenant vos identifiants vous a été envoyé.",
|
|
||||||
"data": {
|
|
||||||
"user": {
|
|
||||||
"id": 123,
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"display_name": "Nom d'affichage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mot de passe oublié
|
|
||||||
|
|
||||||
**Endpoint :** `POST /lost-password`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "utilisateur@exemple.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Un nouveau mot de passe a été envoyé à votre adresse email."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Déconnexion
|
|
||||||
|
|
||||||
**Endpoint :** `POST /logout`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Déconnecté avec succès"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gestion des utilisateurs
|
|
||||||
|
|
||||||
### Récupérer le profil de l'utilisateur connecté
|
|
||||||
|
|
||||||
**Endpoint :** `GET /user/profile`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"id": 123,
|
|
||||||
"entity_id": 456,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"avatar": "url_avatar",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"phone": "+33612345678",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"seat_name": "Siège",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"connected_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true,
|
|
||||||
"entity": {
|
|
||||||
"id": 456,
|
|
||||||
"name": "Nom de l'entité",
|
|
||||||
"email": "entite@exemple.com",
|
|
||||||
"phone": "+33123456789",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mettre à jour le profil de l'utilisateur connecté
|
|
||||||
|
|
||||||
**Endpoint :** `PUT /user/profile`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"display_name": "Nouveau nom d'affichage",
|
|
||||||
"first_name": "Nouveau prénom",
|
|
||||||
"last_name": "Nouveau nom",
|
|
||||||
"phone": "+33612345678",
|
|
||||||
"address1": "Nouvelle adresse ligne 1",
|
|
||||||
"address2": "Nouvelle adresse ligne 2",
|
|
||||||
"code_postal": "75001",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"seat_name": "Nouveau siège"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
Même format que `GET /user/profile` avec les données mises à jour.
|
|
||||||
|
|
||||||
### Changer le mot de passe
|
|
||||||
|
|
||||||
**Endpoint :** `POST /user/change-password`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"current_password": "ancien_mot_de_passe",
|
|
||||||
"new_password": "nouveau_mot_de_passe"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Mot de passe changé avec succès"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Récupérer un utilisateur par ID
|
|
||||||
|
|
||||||
**Endpoint :** `GET /user/{id}`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"id": 123,
|
|
||||||
"entity_id": 456,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"avatar": "url_avatar",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"phone": "+33612345678",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"seat_name": "Siège",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"connected_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Récupérer la liste des utilisateurs
|
|
||||||
|
|
||||||
**Endpoint :** `GET /users`
|
|
||||||
|
|
||||||
**Paramètres de requête :**
|
|
||||||
|
|
||||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
|
||||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
|
||||||
- `entity_id` (optionnel) : Filtrer par ID d'entité
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"entity_id": 456,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"avatar": "url_avatar",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"connected_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"total": 100,
|
|
||||||
"page": 1,
|
|
||||||
"limit": 20,
|
|
||||||
"pages": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Créer un nouvel utilisateur
|
|
||||||
|
|
||||||
**Endpoint :** `POST /user`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"entity_id": 456,
|
|
||||||
"phone": "+33612345678",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"seat_name": "Siège"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Utilisateur créé avec succès. Un email avec les identifiants a été envoyé.",
|
|
||||||
"data": {
|
|
||||||
"id": 123,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"email": "utilisateur@exemple.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Désactiver un utilisateur
|
|
||||||
|
|
||||||
**Endpoint :** `DELETE /user/{id}`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Utilisateur désactivé avec succès"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gestion des entités
|
|
||||||
|
|
||||||
### Récupérer toutes les entités
|
|
||||||
|
|
||||||
**Endpoint :** `GET /entities`
|
|
||||||
|
|
||||||
**Paramètres de requête :**
|
|
||||||
|
|
||||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
|
||||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
|
||||||
- `search` (optionnel) : Terme de recherche
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"id": 456,
|
|
||||||
"name": "Nom de l'entité",
|
|
||||||
"email": "entite@exemple.com",
|
|
||||||
"phone": "+33123456789",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"total": 50,
|
|
||||||
"page": 1,
|
|
||||||
"limit": 20,
|
|
||||||
"pages": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Récupérer une entité par ID
|
|
||||||
|
|
||||||
**Endpoint :** `GET /entity/{id}`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"id": 456,
|
|
||||||
"name": "Nom de l'entité",
|
|
||||||
"email": "entite@exemple.com",
|
|
||||||
"phone": "+33123456789",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true,
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"avatar": "url_avatar",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Créer une nouvelle entité
|
|
||||||
|
|
||||||
**Endpoint :** `POST /entity`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Nom de l'entité",
|
|
||||||
"email": "entite@exemple.com",
|
|
||||||
"phone": "+33123456789",
|
|
||||||
"address1": "Adresse ligne 1",
|
|
||||||
"address2": "Adresse ligne 2",
|
|
||||||
"code_postal": "75000",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Entité créée avec succès",
|
|
||||||
"data": {
|
|
||||||
"id": 456,
|
|
||||||
"name": "Nom de l'entité"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mettre à jour une entité
|
|
||||||
|
|
||||||
**Endpoint :** `PUT /entity/{id}`
|
|
||||||
|
|
||||||
**Corps de la requête :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Nouveau nom de l'entité",
|
|
||||||
"email": "nouvelle-entite@exemple.com",
|
|
||||||
"phone": "+33987654321",
|
|
||||||
"address1": "Nouvelle adresse ligne 1",
|
|
||||||
"address2": "Nouvelle adresse ligne 2",
|
|
||||||
"code_postal": "75001",
|
|
||||||
"city": "Paris",
|
|
||||||
"country": "France"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
Même format que `GET /entity/{id}` avec les données mises à jour.
|
|
||||||
|
|
||||||
### Désactiver une entité
|
|
||||||
|
|
||||||
**Endpoint :** `DELETE /entity/{id}`
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Entité désactivée avec succès"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Récupérer les utilisateurs d'une entité
|
|
||||||
|
|
||||||
**Endpoint :** `GET /entity/{id}/users`
|
|
||||||
|
|
||||||
**Paramètres de requête :**
|
|
||||||
|
|
||||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
|
||||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
|
||||||
|
|
||||||
**Réponse réussie :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"display_name": "Nom d'affichage",
|
|
||||||
"first_name": "Prénom",
|
|
||||||
"last_name": "Nom",
|
|
||||||
"avatar": "url_avatar",
|
|
||||||
"email": "utilisateur@exemple.com",
|
|
||||||
"phone": "+33612345678",
|
|
||||||
"created_at": "2023-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2023-01-01T00:00:00Z",
|
|
||||||
"connected_at": "2023-01-01T00:00:00Z",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"total": 25,
|
|
||||||
"page": 1,
|
|
||||||
"limit": 20,
|
|
||||||
"pages": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Structure des données
|
|
||||||
|
|
||||||
### Table `users`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
entity_id INT,
|
|
||||||
display_name VARCHAR(100) NOT NULL,
|
|
||||||
first_name VARCHAR(100),
|
|
||||||
encrypted_last_name VARCHAR(512),
|
|
||||||
avatar VARCHAR(255),
|
|
||||||
encrypted_email VARCHAR(512),
|
|
||||||
encrypted_phone VARCHAR(255),
|
|
||||||
address1 VARCHAR(255),
|
|
||||||
address2 VARCHAR(255),
|
|
||||||
code_postal VARCHAR(20),
|
|
||||||
city VARCHAR(100),
|
|
||||||
country VARCHAR(100),
|
|
||||||
seat_name VARCHAR(20),
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
connected_at DATETIME,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
FOREIGN KEY (entity_id) REFERENCES entities(id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Table `entities`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE entities (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
encrypted_name VARCHAR(512) NOT NULL,
|
|
||||||
encrypted_email VARCHAR(512),
|
|
||||||
encrypted_phone VARCHAR(255),
|
|
||||||
address1 VARCHAR(255),
|
|
||||||
address2 VARCHAR(255),
|
|
||||||
code_postal VARCHAR(20),
|
|
||||||
city VARCHAR(100),
|
|
||||||
country VARCHAR(100),
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
L'API utilise plusieurs mécanismes pour assurer la sécurité des données :
|
|
||||||
|
|
||||||
1. **Authentification par jeton** : Un jeton d'authentification est requis pour accéder aux endpoints protégés.
|
|
||||||
2. **Chiffrement des données sensibles** : Les données sensibles comme les noms, emails et numéros de téléphone sont chiffrées en base de données.
|
|
||||||
3. **Validation des entrées** : Toutes les entrées utilisateur sont validées avant traitement.
|
|
||||||
4. **Gestion des erreurs** : Les erreurs sont gérées de manière sécurisée sans divulguer d'informations sensibles.
|
|
||||||
|
|
||||||
## Codes d'erreur
|
|
||||||
|
|
||||||
- `400 Bad Request` : Requête invalide ou données manquantes
|
|
||||||
- `401 Unauthorized` : Authentification requise ou échouée
|
|
||||||
- `403 Forbidden` : Accès non autorisé à la ressource
|
|
||||||
- `404 Not Found` : Ressource non trouvée
|
|
||||||
- `409 Conflict` : Conflit avec l'état actuel de la ressource
|
|
||||||
- `500 Internal Server Error` : Erreur serveur
|
|
||||||
|
|
||||||
## Notes d'implémentation
|
|
||||||
|
|
||||||
- Les mots de passe sont hachés avec l'algorithme bcrypt.
|
|
||||||
- Les données sensibles sont chiffrées avec AES-256-CBC.
|
|
||||||
- Les emails sont envoyés pour les opérations importantes (inscription, réinitialisation de mot de passe).
|
|
||||||
- Les sessions sont gérées côté serveur avec un délai d'expiration.
|
|
||||||
339
api/docs/README-UPLOAD.md
Executable file
339
api/docs/README-UPLOAD.md
Executable file
@@ -0,0 +1,339 @@
|
|||||||
|
# Système de Gestion des Fichiers - API Geosector
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Ce document décrit l'organisation et la gestion des fichiers uploadés dans l'API Geosector. Le système permet de stocker et organiser différents types de fichiers par entité, utilisateur, opération et passage.
|
||||||
|
|
||||||
|
## Structure des Dossiers
|
||||||
|
|
||||||
|
```
|
||||||
|
uploads/
|
||||||
|
├── entites/
|
||||||
|
│ ├── {entite_id}/
|
||||||
|
│ │ ├── documents/ # PDF, Excel généraux de l'entité
|
||||||
|
│ │ ├── images/ # Images de l'entité
|
||||||
|
│ │ ├── users/ # Dossier pour les fichiers des utilisateurs
|
||||||
|
│ │ │ └── {user_id}/ # Images par utilisateur (avatars, etc.)
|
||||||
|
│ │ └── operations/ # Dossier pour les opérations
|
||||||
|
│ │ └── {operation_id}/
|
||||||
|
│ │ ├── documents/ # Fichiers Excel de l'opération
|
||||||
|
│ │ └── passages/ # Fichiers des passages de cette opération
|
||||||
|
│ │ └── {passage_id}/ # PDF et images par passage
|
||||||
|
│ └── temp/ # Fichiers temporaires avant validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemples de chemins
|
||||||
|
|
||||||
|
- Document d'entité : `uploads/entites/5/documents/reglement_2024.pdf`
|
||||||
|
- Avatar utilisateur : `uploads/entites/5/users/123/avatar.jpg`
|
||||||
|
- Excel d'opération : `uploads/entites/5/operations/2644/documents/planning.xlsx`
|
||||||
|
- Photo de passage : `uploads/entites/5/operations/2644/passages/789/photo_1.jpg`
|
||||||
|
|
||||||
|
## Structure de la Table `medias`
|
||||||
|
|
||||||
|
### Table existante enrichie
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Structure complète de la table medias
|
||||||
|
CREATE TABLE `medias` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
|
||||||
|
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de l\'élément associé',
|
||||||
|
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
|
||||||
|
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
|
||||||
|
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
|
||||||
|
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
|
||||||
|
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
|
||||||
|
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'entité propriétaire',
|
||||||
|
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'opération (pour passages)',
|
||||||
|
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
|
||||||
|
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de l\'image',
|
||||||
|
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de l\'image',
|
||||||
|
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
|
||||||
|
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
|
||||||
|
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
|
||||||
|
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `idx_entite` (`fk_entite`),
|
||||||
|
KEY `idx_operation` (`fk_operation`),
|
||||||
|
KEY `idx_support_type` (`support`, `support_id`),
|
||||||
|
KEY `idx_file_type` (`file_type`),
|
||||||
|
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration SQL pour table existante
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Ajout des nouvelles colonnes à la table existante
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD COLUMN `file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)' AFTER `fichier`,
|
||||||
|
ADD COLUMN `file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets' AFTER `file_type`,
|
||||||
|
ADD COLUMN `mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier' AFTER `file_size`,
|
||||||
|
ADD COLUMN `original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé' AFTER `mime_type`,
|
||||||
|
ADD COLUMN `fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'entité propriétaire' AFTER `support_id`,
|
||||||
|
ADD COLUMN `fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'opération (pour passages)' AFTER `fk_entite`,
|
||||||
|
ADD COLUMN `file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier' AFTER `original_name`,
|
||||||
|
ADD COLUMN `original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de l\'image' AFTER `file_path`,
|
||||||
|
ADD COLUMN `original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de l\'image' AFTER `original_width`,
|
||||||
|
ADD COLUMN `processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement' AFTER `original_height`,
|
||||||
|
ADD COLUMN `processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement' AFTER `processed_width`,
|
||||||
|
ADD COLUMN `is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)' AFTER `processed_height`;
|
||||||
|
|
||||||
|
-- Ajout des index pour optimiser les requêtes
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD INDEX `idx_entite` (`fk_entite`),
|
||||||
|
ADD INDEX `idx_operation` (`fk_operation`),
|
||||||
|
ADD INDEX `idx_support_type` (`support`, `support_id`),
|
||||||
|
ADD INDEX `idx_file_type` (`file_type`);
|
||||||
|
|
||||||
|
-- Ajout des contraintes de clés étrangères
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types de Support
|
||||||
|
|
||||||
|
### 1. Entité (`support = 'entite'`)
|
||||||
|
|
||||||
|
- **Fichiers autorisés** : PDF, Excel, Images (JPG, PNG)
|
||||||
|
- **Taille max** : 20 MB
|
||||||
|
- **Usage** : Documents généraux de l'entité (règlements, statuts, etc.)
|
||||||
|
- **Chemin** : `uploads/entites/{entite_id}/documents/`
|
||||||
|
|
||||||
|
### 2. Utilisateur (`support = 'user'`)
|
||||||
|
|
||||||
|
- **Fichiers autorisés** : Images uniquement (JPG, PNG, GIF, WebP)
|
||||||
|
- **Taille max** : 5 MB
|
||||||
|
- **Usage** : Avatars, photos de profil
|
||||||
|
- **Chemin** : `uploads/entites/{entite_id}/users/{user_id}/`
|
||||||
|
- **Traitement** : Redimensionnement automatique
|
||||||
|
|
||||||
|
### 3. Opération (`support = 'operation'`)
|
||||||
|
|
||||||
|
- **Fichiers autorisés** : Excel uniquement (XLS, XLSX)
|
||||||
|
- **Taille max** : 20 MB
|
||||||
|
- **Usage** : Plannings, listes, données d'opération
|
||||||
|
- **Chemin** : `uploads/entites/{entite_id}/operations/{operation_id}/documents/`
|
||||||
|
|
||||||
|
### 4. Passage (`support = 'passage'`)
|
||||||
|
|
||||||
|
- **Fichiers autorisés** : PDF et Images (JPG, PNG, PDF)
|
||||||
|
- **Taille max** : 10 MB par fichier
|
||||||
|
- **Usage** : Reçus, photos de passage, justificatifs
|
||||||
|
- **Chemin** : `uploads/entites/{entite_id}/operations/{operation_id}/passages/{passage_id}/`
|
||||||
|
- **Traitement** : Redimensionnement automatique pour les images
|
||||||
|
|
||||||
|
## Traitement Automatique des Images
|
||||||
|
|
||||||
|
### Règles de redimensionnement
|
||||||
|
|
||||||
|
- **Dimension maximale** : 250px (hauteur ou largeur, selon la plus grande)
|
||||||
|
- **Résolution** : 72 DPI (optimisé web)
|
||||||
|
- **Préservation du ratio** : Redimensionnement proportionnel
|
||||||
|
- **Formats supportés** : JPG, PNG, GIF, WebP
|
||||||
|
- **Qualité JPEG** : 85% (bon compromis qualité/poids)
|
||||||
|
|
||||||
|
### Exemples de transformation
|
||||||
|
|
||||||
|
```
|
||||||
|
Image originale 1000x800px → Image traitée 250x200px
|
||||||
|
Image originale 600x1200px → Image traitée 125x250px
|
||||||
|
Image originale 200x150px → Pas de redimensionnement (déjà < 250px)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow de traitement
|
||||||
|
|
||||||
|
1. **Upload** → Validation du type MIME
|
||||||
|
2. **Analyse** → Détection des dimensions originales
|
||||||
|
3. **Traitement** → Redimensionnement si nécessaire
|
||||||
|
4. **Optimisation** → Compression et résolution web
|
||||||
|
5. **Sauvegarde** → Image optimisée + métadonnées
|
||||||
|
6. **Nettoyage** → Suppression du fichier temporaire
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Routes de gestion des fichiers
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Upload de fichiers
|
||||||
|
POST /api/medias/upload
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
Body: {
|
||||||
|
"file": [fichier],
|
||||||
|
"support": "entite|user|operation|passage",
|
||||||
|
"support_id": 123,
|
||||||
|
"description": "Description du fichier"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération d'un fichier
|
||||||
|
GET /api/medias/{id}
|
||||||
|
|
||||||
|
// Liste des fichiers par support
|
||||||
|
GET /api/medias/list/{support}/{support_id}
|
||||||
|
|
||||||
|
// Suppression d'un fichier
|
||||||
|
DELETE /api/medias/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemples de requêtes
|
||||||
|
|
||||||
|
#### Upload d'un avatar utilisateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.geosector.fr/medias/upload" \
|
||||||
|
-H "Authorization: Bearer {token}" \
|
||||||
|
-F "file=@avatar.jpg" \
|
||||||
|
-F "support=user" \
|
||||||
|
-F "support_id=123" \
|
||||||
|
-F "description=Avatar utilisateur"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Upload d'une photo de passage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.geosector.fr/medias/upload" \
|
||||||
|
-H "Authorization: Bearer {token}" \
|
||||||
|
-F "file=@photo_passage.jpg" \
|
||||||
|
-F "support=passage" \
|
||||||
|
-F "support_id=789" \
|
||||||
|
-F "description=Photo du passage"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sécurité et Contrôles
|
||||||
|
|
||||||
|
### Validation des fichiers
|
||||||
|
|
||||||
|
- **Types MIME** : Vérification stricte du type de fichier
|
||||||
|
- **Extensions** : Validation de l'extension par rapport au contenu
|
||||||
|
- **Taille** : Limite selon le type de support
|
||||||
|
- **Contenu** : Scan antivirus recommandé en production
|
||||||
|
|
||||||
|
### Contrôles d'accès
|
||||||
|
|
||||||
|
- **Authentification** : Token JWT requis
|
||||||
|
- **Autorisation** : Utilisateur ne peut accéder qu'aux fichiers de son entité
|
||||||
|
- **Vérification** : Contrôle que l'utilisateur appartient à l'entité du fichier
|
||||||
|
- **Logs** : Traçabilité complète des uploads et accès
|
||||||
|
|
||||||
|
### Nommage des fichiers
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Format : {timestamp}_{random}_{sanitized_name}.{extension}
|
||||||
|
// Exemple : 1640995200_a1b2c3_document_reglement.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des Erreurs
|
||||||
|
|
||||||
|
### Codes d'erreur HTTP
|
||||||
|
|
||||||
|
- **400** : Fichier invalide ou paramètres manquants
|
||||||
|
- **401** : Non authentifié
|
||||||
|
- **403** : Accès refusé à cette entité
|
||||||
|
- **413** : Fichier trop volumineux
|
||||||
|
- **415** : Type de fichier non supporté
|
||||||
|
- **500** : Erreur serveur lors du traitement
|
||||||
|
|
||||||
|
### Messages d'erreur
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Type de fichier non autorisé pour ce support",
|
||||||
|
"code": "INVALID_FILE_TYPE",
|
||||||
|
"allowed_types": ["jpg", "png", "gif", "webp"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance et Nettoyage
|
||||||
|
|
||||||
|
### Nettoyage automatique
|
||||||
|
|
||||||
|
- **Fichiers temporaires** : Suppression après 24h
|
||||||
|
- **Fichiers orphelins** : Détection et suppression des fichiers sans référence en base
|
||||||
|
- **Anciennes opérations** : Suppression en cascade lors de la suppression d'une opération
|
||||||
|
|
||||||
|
### Commandes de maintenance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nettoyage des fichiers temporaires
|
||||||
|
php scripts/cleanup_temp_files.php
|
||||||
|
|
||||||
|
# Détection des fichiers orphelins
|
||||||
|
php scripts/find_orphan_files.php
|
||||||
|
|
||||||
|
# Statistiques d'utilisation
|
||||||
|
php scripts/storage_stats.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performances et Optimisation
|
||||||
|
|
||||||
|
### Optimisations
|
||||||
|
|
||||||
|
- **CDN** : Recommandé pour la distribution des fichiers
|
||||||
|
- **Cache** : Headers de cache appropriés pour les fichiers statiques
|
||||||
|
- **Compression** : Gzip pour les réponses API
|
||||||
|
- **Index** : Index optimisés sur la table medias
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- **Espace disque** : Surveillance de l'utilisation
|
||||||
|
- **Performance** : Temps de traitement des images
|
||||||
|
- **Erreurs** : Logs des échecs d'upload et de traitement
|
||||||
|
|
||||||
|
## Exemples d'Utilisation
|
||||||
|
|
||||||
|
### Cas d'usage typiques
|
||||||
|
|
||||||
|
1. **Upload d'avatar utilisateur**
|
||||||
|
|
||||||
|
- Fichier JPG de 2MB
|
||||||
|
- Redimensionnement automatique à 250x250px
|
||||||
|
- Stockage dans `uploads/entites/5/users/123/`
|
||||||
|
|
||||||
|
2. **Document d'opération**
|
||||||
|
|
||||||
|
- Fichier Excel de planning
|
||||||
|
- Stockage dans `uploads/entites/5/operations/2644/documents/`
|
||||||
|
- Pas de traitement (fichier conservé tel quel)
|
||||||
|
|
||||||
|
3. **Photo de passage**
|
||||||
|
- Photo JPG de 8MB prise sur mobile
|
||||||
|
- Redimensionnement automatique à 250px max
|
||||||
|
- Stockage dans `uploads/entites/5/operations/2644/passages/789/`
|
||||||
|
|
||||||
|
### Intégration frontend
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Upload avec progress
|
||||||
|
const uploadFile = async (file, support, supportId, description) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('support', support);
|
||||||
|
formData.append('support_id', supportId);
|
||||||
|
formData.append('description', description);
|
||||||
|
|
||||||
|
const response = await fetch('/api/medias/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : Juin 2025
|
||||||
|
**Auteur** : API Geosector Team
|
||||||
0
api/docs/TECHBOOK.md
Normal file → Executable file
0
api/docs/TECHBOOK.md
Normal file → Executable file
0
api/docs/api-analysis.md
Normal file → Executable file
0
api/docs/api-analysis.md
Normal file → Executable file
1
api/docs/contour-des-departements.geojson
Normal file
1
api/docs/contour-des-departements.geojson
Normal file
File diff suppressed because one or more lines are too long
53
api/docs/departements_limitrophes.md
Normal file
53
api/docs/departements_limitrophes.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Départements limitrophes
|
||||||
|
|
||||||
|
Ce document liste les départements limitrophes pour chaque département français.
|
||||||
|
À utiliser pour remplir le champ `dept_limitrophes` dans la table `x_departements`.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
Le champ `dept_limitrophes` contient une liste de codes départements séparés par des virgules.
|
||||||
|
Exemple : "22,35,56" pour un département limitrophe avec les Côtes-d'Armor (22), l'Ille-et-Vilaine (35) et le Morbihan (56).
|
||||||
|
|
||||||
|
## Liste par département
|
||||||
|
|
||||||
|
### Bretagne
|
||||||
|
- **22 - Côtes-d'Armor** : 29,35,56
|
||||||
|
- **29 - Finistère** : 22,56
|
||||||
|
- **35 - Ille-et-Vilaine** : 22,44,49,50,53,56
|
||||||
|
- **56 - Morbihan** : 22,29,35,44
|
||||||
|
|
||||||
|
### Pays de la Loire
|
||||||
|
- **44 - Loire-Atlantique** : 35,49,56,85
|
||||||
|
- **49 - Maine-et-Loire** : 35,37,44,53,72,79,85,86
|
||||||
|
- **53 - Mayenne** : 14,35,49,50,61,72
|
||||||
|
- **72 - Sarthe** : 14,27,28,37,41,49,53,61
|
||||||
|
- **85 - Vendée** : 17,44,49,79
|
||||||
|
|
||||||
|
### Normandie
|
||||||
|
- **14 - Calvados** : 27,50,53,61,72
|
||||||
|
- **27 - Eure** : 14,28,60,61,72,76,78,95
|
||||||
|
- **50 - Manche** : 14,35,53,61
|
||||||
|
- **61 - Orne** : 14,27,28,35,41,50,53,72
|
||||||
|
- **76 - Seine-Maritime** : 27,60,80
|
||||||
|
|
||||||
|
### Île-de-France
|
||||||
|
- **75 - Paris** : 92,93,94
|
||||||
|
- **77 - Seine-et-Marne** : 02,10,45,51,60,89,91,93,94,95
|
||||||
|
- **78 - Yvelines** : 27,28,91,92,95
|
||||||
|
- **91 - Essonne** : 28,45,77,78,92,94
|
||||||
|
- **92 - Hauts-de-Seine** : 75,78,91,93,94,95
|
||||||
|
- **93 - Seine-Saint-Denis** : 75,77,92,94,95
|
||||||
|
- **94 - Val-de-Marne** : 75,77,91,92,93
|
||||||
|
- **95 - Val-d'Oise** : 27,60,77,78,92,93
|
||||||
|
|
||||||
|
### Hauts-de-France
|
||||||
|
- **02 - Aisne** : 08,51,59,60,77,80
|
||||||
|
- **59 - Nord** : 02,62,80 (+ frontière Belgique)
|
||||||
|
- **60 - Oise** : 02,27,76,77,80,95
|
||||||
|
- **62 - Pas-de-Calais** : 59,80 (+ frontière Belgique et côte Manche)
|
||||||
|
- **80 - Somme** : 02,27,59,60,62,76
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Cette liste est à compléter pour tous les départements français
|
||||||
|
- Les départements d'outre-mer n'ont généralement pas de départements limitrophes terrestres
|
||||||
|
- Certains départements peuvent avoir des limites maritimes non représentées ici
|
||||||
|
- Source recommandée : données INSEE ou IGN pour une liste complète et exacte
|
||||||
0
api/docs/flowIncus.md
Normal file → Executable file
0
api/docs/flowIncus.md
Normal file → Executable file
602
api/docs/geo_app.sql
Executable file
602
api/docs/geo_app.sql
Executable file
@@ -0,0 +1,602 @@
|
|||||||
|
-- -------------------------------------------------------------
|
||||||
|
-- TablePlus 6.4.8(608)
|
||||||
|
--
|
||||||
|
-- https://tableplus.com/
|
||||||
|
--
|
||||||
|
-- Database: geo_app
|
||||||
|
-- Generation Time: 2025-06-09 18:03:43.5140
|
||||||
|
-- -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `chat_anonymous_users` (
|
||||||
|
`id` varchar(50) NOT NULL,
|
||||||
|
`device_id` varchar(100) NOT NULL,
|
||||||
|
`name` varchar(100) DEFAULT NULL,
|
||||||
|
`email` varchar(100) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`converted_to_user_id` int(10) unsigned DEFAULT NULL,
|
||||||
|
`metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_device_id` (`device_id`),
|
||||||
|
KEY `idx_converted_user` (`converted_to_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_attachments` (
|
||||||
|
`id` varchar(50) NOT NULL,
|
||||||
|
`fk_message` varchar(50) NOT NULL,
|
||||||
|
`file_name` varchar(255) NOT NULL,
|
||||||
|
`file_path` varchar(500) NOT NULL,
|
||||||
|
`file_type` varchar(100) NOT NULL,
|
||||||
|
`file_size` int(10) unsigned NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_message` (`fk_message`),
|
||||||
|
CONSTRAINT `fk_chat_attachments_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_audience_targets` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_room` varchar(50) NOT NULL,
|
||||||
|
`target_type` enum('role','entity','all','combined') NOT NULL DEFAULT 'all',
|
||||||
|
`target_id` varchar(50) DEFAULT NULL,
|
||||||
|
`role_filter` varchar(20) DEFAULT NULL,
|
||||||
|
`entity_filter` varchar(50) DEFAULT NULL,
|
||||||
|
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_room` (`fk_room`),
|
||||||
|
KEY `idx_type` (`target_type`),
|
||||||
|
CONSTRAINT `fk_chat_audience_targets_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_broadcast_lists` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_room` varchar(50) NOT NULL,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`fk_user_creator` int(10) unsigned NOT NULL,
|
||||||
|
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_room` (`fk_room`),
|
||||||
|
KEY `idx_user_creator` (`fk_user_creator`),
|
||||||
|
CONSTRAINT `fk_chat_broadcast_lists_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `chat_messages` (
|
||||||
|
`id` varchar(50) NOT NULL,
|
||||||
|
`fk_room` varchar(50) NOT NULL,
|
||||||
|
`fk_user` int(10) unsigned DEFAULT NULL,
|
||||||
|
`sender_type` enum('user','anonymous','system') NOT NULL DEFAULT 'user',
|
||||||
|
`content` text DEFAULT NULL,
|
||||||
|
`content_type` enum('text','image','file') NOT NULL DEFAULT 'text',
|
||||||
|
`date_sent` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`date_delivered` timestamp NULL DEFAULT NULL,
|
||||||
|
`date_read` timestamp NULL DEFAULT NULL,
|
||||||
|
`statut` enum('envoye','livre','lu','error') NOT NULL DEFAULT 'envoye',
|
||||||
|
`is_announcement` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_room` (`fk_room`),
|
||||||
|
KEY `idx_user` (`fk_user`),
|
||||||
|
KEY `idx_date` (`date_sent`),
|
||||||
|
KEY `idx_status` (`statut`),
|
||||||
|
KEY `idx_messages_unread` (`fk_room`,`statut`),
|
||||||
|
CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_notifications` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_user` int(10) unsigned NOT NULL,
|
||||||
|
`fk_message` varchar(50) DEFAULT NULL,
|
||||||
|
`fk_room` varchar(50) DEFAULT NULL,
|
||||||
|
`type` varchar(50) NOT NULL,
|
||||||
|
`contenu` text DEFAULT NULL,
|
||||||
|
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`date_lecture` timestamp NULL DEFAULT NULL,
|
||||||
|
`statut` enum('non_lue','lue') NOT NULL DEFAULT 'non_lue',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user` (`fk_user`),
|
||||||
|
KEY `idx_message` (`fk_message`),
|
||||||
|
KEY `idx_room` (`fk_room`),
|
||||||
|
KEY `idx_statut` (`statut`),
|
||||||
|
KEY `idx_notifications_unread` (`fk_user`,`statut`),
|
||||||
|
CONSTRAINT `fk_chat_notifications_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_chat_notifications_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_offline_queue` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(10) unsigned NOT NULL,
|
||||||
|
`operation_type` varchar(50) NOT NULL,
|
||||||
|
`operation_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`operation_data`)),
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`processed_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`status` enum('pending','processing','completed','failed') NOT NULL DEFAULT 'pending',
|
||||||
|
`error_message` text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_participants` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`id_room` varchar(50) NOT NULL,
|
||||||
|
`id_user` int(10) unsigned DEFAULT NULL,
|
||||||
|
`anonymous_id` varchar(50) DEFAULT NULL,
|
||||||
|
`role` enum('administrateur','participant','en_lecture_seule') NOT NULL DEFAULT 'participant',
|
||||||
|
`date_ajout` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`notification_activee` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
`last_read_message_id` varchar(50) DEFAULT NULL,
|
||||||
|
`via_target` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`can_reply` tinyint(1) unsigned DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uc_room_user` (`id_room`,`id_user`),
|
||||||
|
KEY `idx_room` (`id_room`),
|
||||||
|
KEY `idx_user` (`id_user`),
|
||||||
|
KEY `idx_anonymous_id` (`anonymous_id`),
|
||||||
|
KEY `idx_participants_active` (`id_room`,`id_user`,`notification_activee`),
|
||||||
|
CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`id_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_read_messages` (
|
||||||
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_message` varchar(50) NOT NULL,
|
||||||
|
`fk_user` int(10) unsigned NOT NULL,
|
||||||
|
`date_read` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uc_message_user` (`fk_message`,`fk_user`),
|
||||||
|
KEY `idx_message` (`fk_message`),
|
||||||
|
KEY `idx_user` (`fk_user`),
|
||||||
|
CONSTRAINT `fk_chat_read_messages_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `chat_rooms` (
|
||||||
|
`id` varchar(50) NOT NULL,
|
||||||
|
`type` enum('privee','groupe','liste_diffusion','broadcast','announcement') NOT NULL,
|
||||||
|
`title` varchar(100) DEFAULT NULL,
|
||||||
|
`date_creation` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`fk_user` int(10) unsigned NOT NULL,
|
||||||
|
`fk_entite` int(10) unsigned DEFAULT NULL,
|
||||||
|
`statut` enum('active','archive') NOT NULL DEFAULT 'active',
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`reply_permission` enum('all','admins_only','sender_only','none') NOT NULL DEFAULT 'all',
|
||||||
|
`is_pinned` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`expiry_date` timestamp NULL DEFAULT NULL,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user` (`fk_user`),
|
||||||
|
KEY `idx_entite` (`fk_entite`),
|
||||||
|
KEY `idx_type` (`type`),
|
||||||
|
KEY `idx_statut` (`statut`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `email_counter` (
|
||||||
|
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
||||||
|
`hour_start` timestamp NULL DEFAULT NULL,
|
||||||
|
`count` int(10) unsigned DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `email_queue` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`to_email` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(255) DEFAULT NULL,
|
||||||
|
`body` text DEFAULT NULL,
|
||||||
|
`headers` text DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`status` enum('pending','sent','failed') DEFAULT 'pending',
|
||||||
|
`attempts` int(10) unsigned DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `entites` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||||
|
`adresse1` varchar(45) DEFAULT '',
|
||||||
|
`adresse2` varchar(45) DEFAULT '',
|
||||||
|
`code_postal` varchar(5) DEFAULT '',
|
||||||
|
`ville` varchar(45) DEFAULT '',
|
||||||
|
`fk_region` int(10) unsigned DEFAULT NULL,
|
||||||
|
`fk_type` int(10) unsigned DEFAULT 1,
|
||||||
|
`encrypted_phone` varchar(128) DEFAULT '',
|
||||||
|
`encrypted_mobile` varchar(128) DEFAULT '',
|
||||||
|
`encrypted_email` varchar(255) DEFAULT '',
|
||||||
|
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`chk_stripe` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`encrypted_stripe_id` varchar(255) DEFAULT '',
|
||||||
|
`encrypted_iban` varchar(255) DEFAULT '',
|
||||||
|
`encrypted_bic` varchar(128) DEFAULT '',
|
||||||
|
`chk_demo` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT 'Gestion des mots de passe manuelle O/N',
|
||||||
|
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `entites_ibfk_1` (`fk_region`),
|
||||||
|
KEY `entites_ibfk_2` (`fk_type`),
|
||||||
|
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1230 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `medias` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
|
||||||
|
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de élément associé',
|
||||||
|
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
|
||||||
|
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
|
||||||
|
`file_category` varchar(50) DEFAULT NULL COMMENT 'export, logo, carte, etc.',
|
||||||
|
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
|
||||||
|
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
|
||||||
|
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
|
||||||
|
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de entité propriétaire',
|
||||||
|
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de opération (pour passages)',
|
||||||
|
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
|
||||||
|
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de image',
|
||||||
|
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de image',
|
||||||
|
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
|
||||||
|
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
|
||||||
|
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
|
||||||
|
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `idx_entite` (`fk_entite`),
|
||||||
|
KEY `idx_operation` (`fk_operation`),
|
||||||
|
KEY `idx_support_type` (`support`, `support_id`),
|
||||||
|
KEY `idx_file_type` (`file_type`),
|
||||||
|
KEY `idx_file_category` (`file_category`),
|
||||||
|
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE `ope_pass` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_sector` int(10) unsigned DEFAULT 0,
|
||||||
|
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id',
|
||||||
|
`passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage',
|
||||||
|
`fk_type` int(10) unsigned DEFAULT 0,
|
||||||
|
`numero` varchar(10) NOT NULL DEFAULT '',
|
||||||
|
`rue` varchar(75) NOT NULL DEFAULT '',
|
||||||
|
`rue_bis` varchar(1) NOT NULL DEFAULT '',
|
||||||
|
`ville` varchar(75) NOT NULL DEFAULT '',
|
||||||
|
`fk_habitat` int(10) unsigned DEFAULT 1,
|
||||||
|
`appt` varchar(5) DEFAULT '',
|
||||||
|
`niveau` varchar(5) DEFAULT '',
|
||||||
|
`residence` varchar(75) DEFAULT '',
|
||||||
|
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
|
||||||
|
`montant` decimal(7,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`fk_type_reglement` int(10) unsigned DEFAULT 1,
|
||||||
|
`remarque` text DEFAULT '',
|
||||||
|
`encrypted_email` varchar(255) DEFAULT '',
|
||||||
|
`nom_recu` varchar(50) DEFAULT NULL,
|
||||||
|
`date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception',
|
||||||
|
`date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu',
|
||||||
|
`date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu',
|
||||||
|
`email_erreur` varchar(30) DEFAULT '',
|
||||||
|
`chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`encrypted_phone` varchar(128) NOT NULL DEFAULT '',
|
||||||
|
`is_striped` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`docremis` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
|
||||||
|
`nb_passages` int(11) DEFAULT 1 COMMENT 'Nb passages pour les a repasser',
|
||||||
|
`chk_gps_maj` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`chk_map_create` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`chk_mobile` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`chk_synchro` tinyint(1) unsigned DEFAULT 1 COMMENT 'chk synchro entre web et appli',
|
||||||
|
`chk_api_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`chk_maj_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`anomalie` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_operation` (`fk_operation`),
|
||||||
|
KEY `fk_sector` (`fk_sector`),
|
||||||
|
KEY `fk_user` (`fk_user`),
|
||||||
|
KEY `fk_type` (`fk_type`),
|
||||||
|
KEY `fk_type_reglement` (`fk_type_reglement`),
|
||||||
|
KEY `email` (`encrypted_email`),
|
||||||
|
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=19499566 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `ope_pass_histo` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`date_histo` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date historique',
|
||||||
|
`sujet` varchar(50) DEFAULT NULL,
|
||||||
|
`remarque` varchar(250) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE,
|
||||||
|
KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE,
|
||||||
|
CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=6752 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `ope_sectors` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_old_sector` int(10) unsigned DEFAULT NULL,
|
||||||
|
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||||
|
`sector` text NOT NULL DEFAULT '',
|
||||||
|
`color` varchar(7) NOT NULL DEFAULT '#4B77BE',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id` (`id`),
|
||||||
|
KEY `fk_operation` (`fk_operation`),
|
||||||
|
CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=27675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `ope_users` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `ope_users_ibfk_1` (`fk_operation`),
|
||||||
|
KEY `ope_users_ibfk_2` (`fk_user`),
|
||||||
|
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=199006 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `ope_users_sectors` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id` (`id`),
|
||||||
|
KEY `fk_operation` (`fk_operation`),
|
||||||
|
KEY `fk_user` (`fk_user`),
|
||||||
|
KEY `fk_sector` (`fk_sector`),
|
||||||
|
CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=48082 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `operations` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_entite` int(10) unsigned NOT NULL DEFAULT 1,
|
||||||
|
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||||
|
`date_deb` date NOT NULL DEFAULT '0000-00-00',
|
||||||
|
`date_fin` date NOT NULL DEFAULT '0000-00-00',
|
||||||
|
`chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_entite` (`fk_entite`),
|
||||||
|
KEY `date_deb` (`date_deb`),
|
||||||
|
CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=3121 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `params` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(35) NOT NULL DEFAULT '',
|
||||||
|
`valeur` varchar(255) NOT NULL DEFAULT '',
|
||||||
|
`aide` varchar(150) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `sectors_adresses` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_adresse` varchar(25) DEFAULT NULL COMMENT 'adresses.cp??.id',
|
||||||
|
`osm_id` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||||
|
`osm_name` varchar(50) NOT NULL DEFAULT '',
|
||||||
|
`numero` varchar(5) NOT NULL DEFAULT '',
|
||||||
|
`rue_bis` varchar(5) NOT NULL DEFAULT '',
|
||||||
|
`rue` varchar(60) NOT NULL DEFAULT '',
|
||||||
|
`cp` varchar(5) NOT NULL DEFAULT '',
|
||||||
|
`ville` varchar(60) NOT NULL DEFAULT '',
|
||||||
|
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||||
|
`osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `sectors_adresses_fk_sector_index` (`fk_sector`),
|
||||||
|
KEY `sectors_adresses_numero_index` (`numero`),
|
||||||
|
KEY `sectors_adresses_rue_index` (`rue`),
|
||||||
|
KEY `sectors_adresses_ville_index` (`ville`),
|
||||||
|
CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1562946 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_entite` int(10) unsigned DEFAULT 1,
|
||||||
|
`fk_role` int(10) unsigned DEFAULT 1,
|
||||||
|
`fk_titre` int(10) unsigned DEFAULT 1,
|
||||||
|
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||||
|
`first_name` varchar(45) DEFAULT NULL,
|
||||||
|
`sect_name` varchar(60) DEFAULT '',
|
||||||
|
`encrypted_user_name` varchar(128) DEFAULT '',
|
||||||
|
`user_pass_hash` varchar(60) DEFAULT NULL,
|
||||||
|
`encrypted_phone` varchar(128) DEFAULT NULL,
|
||||||
|
`encrypted_mobile` varchar(128) DEFAULT NULL,
|
||||||
|
`encrypted_email` varchar(255) DEFAULT '',
|
||||||
|
`chk_alert_email` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
`chk_suivi` tinyint(1) unsigned DEFAULT 0,
|
||||||
|
`date_naissance` date DEFAULT NULL,
|
||||||
|
`date_embauche` date DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||||
|
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||||
|
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_entite` (`fk_entite`),
|
||||||
|
KEY `username` (`encrypted_user_name`),
|
||||||
|
KEY `users_ibfk_2` (`fk_role`),
|
||||||
|
KEY `users_ibfk_3` (`fk_titre`),
|
||||||
|
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=10027748 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_departements` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` varchar(3) DEFAULT NULL,
|
||||||
|
`fk_region` int(10) unsigned DEFAULT 1,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `x_departements_ibfk_1` (`fk_region`),
|
||||||
|
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_devises` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` varchar(3) DEFAULT NULL,
|
||||||
|
`symbole` varchar(6) DEFAULT NULL,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_entites_types` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_pays` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` varchar(3) DEFAULT NULL,
|
||||||
|
`fk_continent` int(10) unsigned DEFAULT NULL,
|
||||||
|
`fk_devise` int(10) unsigned DEFAULT 1,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `x_pays_ibfk_1` (`fk_devise`),
|
||||||
|
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes' `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_regions` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_pays` int(10) unsigned DEFAULT 1,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`libelle_long` varchar(45) DEFAULT NULL,
|
||||||
|
`table_osm` varchar(45) DEFAULT NULL,
|
||||||
|
`departements` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `x_regions_ibfk_1` (`fk_pays`),
|
||||||
|
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_types_passages` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(10) DEFAULT NULL,
|
||||||
|
`color_button` varchar(15) DEFAULT NULL,
|
||||||
|
`color_mark` varchar(15) DEFAULT NULL,
|
||||||
|
`color_table` varchar(15) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_types_reglements` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_users_roles` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_users_titres` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`libelle` varchar(45) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `x_villes` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_departement` int(10) unsigned DEFAULT 1,
|
||||||
|
`libelle` varchar(65) DEFAULT NULL,
|
||||||
|
`code_postal` varchar(5) DEFAULT NULL,
|
||||||
|
`code_insee` varchar(5) DEFAULT NULL,
|
||||||
|
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||||
|
KEY `x_villes_ibfk_1` (`fk_departement`),
|
||||||
|
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE TABLE `z_sessions` (
|
||||||
|
`sid` text NOT NULL,
|
||||||
|
`fk_user` int(11) NOT NULL,
|
||||||
|
`role` varchar(10) DEFAULT NULL,
|
||||||
|
`date_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`ip` varchar(50) NOT NULL,
|
||||||
|
`browser` varchar(150) NOT NULL,
|
||||||
|
`data` mediumtext DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||||
|
|
||||||
|
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `chat_conversations_unread` AS select `r`.`id` AS `id`,`r`.`type` AS `type`,`r`.`title` AS `title`,`r`.`date_creation` AS `date_creation`,`r`.`fk_user` AS `fk_user`,`r`.`fk_entite` AS `fk_entite`,`r`.`statut` AS `statut`,`r`.`description` AS `description`,`r`.`reply_permission` AS `reply_permission`,`r`.`is_pinned` AS `is_pinned`,`r`.`expiry_date` AS `expiry_date`,`r`.`updated_at` AS `updated_at`,count(distinct `m`.`id`) AS `total_messages`,count(distinct `rm`.`id`) AS `read_messages`,count(distinct `m`.`id`) - count(distinct `rm`.`id`) AS `unread_messages`,(select `geo_app`.`chat_messages`.`date_sent` from `chat_messages` where `geo_app`.`chat_messages`.`fk_room` = `r`.`id` order by `geo_app`.`chat_messages`.`date_sent` desc limit 1) AS `last_message_date` from ((`chat_rooms` `r` left join `chat_messages` `m` on(`r`.`id` = `m`.`fk_room`)) left join `chat_read_messages` `rm` on(`m`.`id` = `rm`.`fk_message`)) group by `r`.`id`;
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
0
api/docs/geosector_app.sql
Normal file → Executable file
0
api/docs/geosector_app.sql
Normal file → Executable file
128
api/docs/x_departements_contours.sql
Normal file
128
api/docs/x_departements_contours.sql
Normal file
File diff suppressed because one or more lines are too long
128
api/docs/x_departements_contours_corrected.sql
Normal file
128
api/docs/x_departements_contours_corrected.sql
Normal file
File diff suppressed because one or more lines are too long
128
api/docs/x_departements_contours_fixed.sql
Normal file
128
api/docs/x_departements_contours_fixed.sql
Normal file
File diff suppressed because one or more lines are too long
145
api/export_operation.php
Executable file
145
api/export_operation.php
Executable file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
|
||||||
|
global $Session, $Conf, $Route;
|
||||||
|
// appel de geolib en admin
|
||||||
|
require_once __DIR__ . '/../pub/res/php/geolib.php';
|
||||||
|
|
||||||
|
function nettoie_input($input) {
|
||||||
|
return htmlspecialchars(strip_tags(trim($input)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getinfos($sql) {
|
||||||
|
// This is a placeholder function. Replace with actual database query logic.
|
||||||
|
// For example, you might use PDO to execute the query and return the results.
|
||||||
|
// $db = Database::getInstance();
|
||||||
|
// $stmt = $db->prepare($sql);
|
||||||
|
// $stmt->execute();
|
||||||
|
// return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function eLog($message) {
|
||||||
|
error_log($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Route->_action) {
|
||||||
|
case "export_operation":
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
if (isset($data->cid)) {
|
||||||
|
$cid = nettoie_input($data->cid);
|
||||||
|
$idMembre = "0";
|
||||||
|
$libMembre = "";
|
||||||
|
if (isset($data->idMembre) && isset($data->libMembre)) {
|
||||||
|
$idMembre = nettoie_input($data->idMembre);
|
||||||
|
$libMembre = nettoie_input($data->libMembre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On crée le dossier de l'amicale s'il n'est pas déjà créé
|
||||||
|
$dir = 'pub/files/upload/' . $Conf->_entite["rowid"];
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'SELECT p.date_eve, u.prenom, u.libelle AS nom, u.nom_tournee, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville, p.fk_habitat, p.appt, p.niveau, p.libelle, p.email, p.phone, p.montant, xtr.libelle AS reglement, p.remarque FROM ope_pass p LEFT JOIN users u ON u.rowid=p.fk_user LEFT JOIN x_types_reglements xtr ON xtr.rowid=p.fk_type_reglement WHERE p.fk_operation=' . $cid;
|
||||||
|
if ($idMembre != "0") {
|
||||||
|
$sql .= ' AND p.fk_user=' . $idMembre . ';';
|
||||||
|
}
|
||||||
|
$pass = getinfos($sql);
|
||||||
|
|
||||||
|
$aData = array();
|
||||||
|
$aData[] = array(
|
||||||
|
'Date',
|
||||||
|
'Heure',
|
||||||
|
'Prenom',
|
||||||
|
'Nom',
|
||||||
|
'Tournee',
|
||||||
|
'Type',
|
||||||
|
'N°',
|
||||||
|
'Rue',
|
||||||
|
'Ville',
|
||||||
|
'Habitat',
|
||||||
|
'Donateur',
|
||||||
|
'Email',
|
||||||
|
'Tel',
|
||||||
|
'Montant',
|
||||||
|
'Reglement',
|
||||||
|
'Remarque'
|
||||||
|
);
|
||||||
|
foreach ($pass as $p) {
|
||||||
|
switch ($p["fk_type"]) {
|
||||||
|
case 1:
|
||||||
|
$ptype = "Effectué";
|
||||||
|
$preglement = $p["reglement"];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$ptype = "A finaliser";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
$ptype = "Refusé";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
$ptype = "Don";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
$ptype = "Habitat vide";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$ptype = $p["fk_type"];
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($p["fk_habitat"] == 1) {
|
||||||
|
$phabitat = "Individuel";
|
||||||
|
} else {
|
||||||
|
$phabitat = "Etage " . $p["niveau"] . " - Appt " . $p["appt"];
|
||||||
|
}
|
||||||
|
$dateEve = date("d/m/Y", strtotime($p["date_eve"]));
|
||||||
|
$heureEve = date("H:i", strtotime($p["date_eve"]));
|
||||||
|
$nom = str_replace("/", "-", $p["nom"]);
|
||||||
|
$tournee = str_replace("/", "-", $p["nom_tournee"]);
|
||||||
|
$aData[] = array(
|
||||||
|
$dateEve,
|
||||||
|
$heureEve,
|
||||||
|
$p["prenom"],
|
||||||
|
$nom,
|
||||||
|
$tournee,
|
||||||
|
$ptype,
|
||||||
|
$p["numero"] . $p["rue_bis"],
|
||||||
|
$p["rue"],
|
||||||
|
$p["ville"],
|
||||||
|
$phabitat,
|
||||||
|
$p["libelle"],
|
||||||
|
$p["email"],
|
||||||
|
$p["phone"],
|
||||||
|
$p["montant"],
|
||||||
|
$preglement,
|
||||||
|
$p["remarque"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||||
|
$activeWorksheet = $spreadsheet->getActiveSheet()
|
||||||
|
->fromArray($aData, null, 'A1');
|
||||||
|
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
if ($idMembre == "0") {
|
||||||
|
$xlsxName = $dir . '/geosector-ope-' . $cid . '-' . date("Ymd-His") . '.xlsx';
|
||||||
|
} else {
|
||||||
|
$libMembre = str_replace("/", "-", $libMembre);
|
||||||
|
$libMembre = str_replace("*", "-", $libMembre);
|
||||||
|
$xlsxName = $dir . '/geosector-ope-' . $cid . '-' . $libMembre . '-' . date("Ymd-His") . '.xlsx';
|
||||||
|
}
|
||||||
|
eLog("Export Operation : " . $xlsxName);
|
||||||
|
$writer->save($xlsxName);
|
||||||
|
|
||||||
|
$ret = array('url' => $xlsxName);
|
||||||
|
echo json_encode($ret);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
11
api/index.php
Normal file → Executable file
11
api/index.php
Normal file → Executable file
@@ -7,6 +7,7 @@ require_once __DIR__ . '/bootstrap.php';
|
|||||||
// Chargement des fichiers principaux
|
// Chargement des fichiers principaux
|
||||||
require_once __DIR__ . '/src/Config/AppConfig.php';
|
require_once __DIR__ . '/src/Config/AppConfig.php';
|
||||||
require_once __DIR__ . '/src/Core/Database.php';
|
require_once __DIR__ . '/src/Core/Database.php';
|
||||||
|
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
|
||||||
require_once __DIR__ . '/src/Core/Router.php';
|
require_once __DIR__ . '/src/Core/Router.php';
|
||||||
require_once __DIR__ . '/src/Core/Session.php';
|
require_once __DIR__ . '/src/Core/Session.php';
|
||||||
require_once __DIR__ . '/src/Core/Request.php';
|
require_once __DIR__ . '/src/Core/Request.php';
|
||||||
@@ -19,14 +20,22 @@ require_once __DIR__ . '/src/Controllers/LogController.php';
|
|||||||
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/EntiteController.php';
|
require_once __DIR__ . '/src/Controllers/EntiteController.php';
|
||||||
require_once __DIR__ . '/src/Controllers/UserController.php';
|
require_once __DIR__ . '/src/Controllers/UserController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/OperationController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/PassageController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/VilleController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/FileController.php';
|
||||||
|
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
||||||
|
|
||||||
// Initialiser la configuration
|
// Initialiser la configuration
|
||||||
$appConfig = AppConfig::getInstance();
|
$appConfig = AppConfig::getInstance();
|
||||||
$config = $appConfig->getFullConfig();
|
$config = $appConfig->getFullConfig();
|
||||||
|
|
||||||
// Initialiser la base de données
|
// Initialiser la base de données principale
|
||||||
Database::init($config['database']);
|
Database::init($config['database']);
|
||||||
|
|
||||||
|
// Initialiser la base de données des adresses
|
||||||
|
AddressesDatabase::init($appConfig->getAddressesDatabaseConfig());
|
||||||
|
|
||||||
// Configuration CORS
|
// Configuration CORS
|
||||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
$allowedOrigins = $config['api']['allowed_origins'];
|
$allowedOrigins = $config['api']['allowed_origins'];
|
||||||
|
|||||||
106
api/livre-api.sh
106
api/livre-api.sh
@@ -1,25 +1,47 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Vérification des arguments
|
# Vérification des arguments
|
||||||
if [ $# -ne 2 ]; then
|
if [ $# -ne 1 ]; then
|
||||||
echo "Usage: $0 <source_container> <destination_container>"
|
echo "Usage: $0 <environment>"
|
||||||
echo "Example: $0 dva-geo rca-geo"
|
echo " rec : Livrer de DVA (dva-geo) vers RECETTE (rca-geo)"
|
||||||
|
echo " prod : Livrer de RECETTE (rca-geo) vers PRODUCTION (pra-geo)"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 rec # DVA → RECETTE"
|
||||||
|
echo " $0 prod # RECETTE → PRODUCTION"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HOST_IP="195.154.80.116"
|
HOST_IP="195.154.80.116"
|
||||||
HOST_USER=root
|
HOST_USER=root
|
||||||
HOST_KEY=/Users/pierre/.ssh/id_rsa_mbpi
|
HOST_KEY=/home/pierre/.ssh/id_rsa_mbpi
|
||||||
HOST_PORT=22
|
HOST_PORT=22
|
||||||
|
|
||||||
SOURCE_CONTAINER=$1
|
# Mapping des environnements
|
||||||
DEST_CONTAINER=$2
|
ENVIRONMENT=$1
|
||||||
|
case $ENVIRONMENT in
|
||||||
|
"rca")
|
||||||
|
SOURCE_CONTAINER="dva-geo"
|
||||||
|
DEST_CONTAINER="rca-geo"
|
||||||
|
ENV_NAME="RECETTE"
|
||||||
|
;;
|
||||||
|
"pra")
|
||||||
|
SOURCE_CONTAINER="rca-geo"
|
||||||
|
DEST_CONTAINER="pra-geo"
|
||||||
|
ENV_NAME="PRODUCTION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Environnement '$ENVIRONMENT' non reconnu"
|
||||||
|
echo "Utilisez 'rec' pour RECETTE ou 'prod' pour PRODUCTION"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
API_PATH="/var/www/geosector/api"
|
API_PATH="/var/www/geosector/api"
|
||||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
BACKUP_DIR="${API_PATH}_backup_${TIMESTAMP}"
|
BACKUP_DIR="${API_PATH}_backup_${TIMESTAMP}"
|
||||||
PROJECT="default"
|
PROJECT="default"
|
||||||
|
|
||||||
echo "🔄 Copie de l'API de $SOURCE_CONTAINER vers $DEST_CONTAINER (projet: $PROJECT)"
|
echo "🔄 Livraison vers $ENV_NAME : $SOURCE_CONTAINER → $DEST_CONTAINER (projet: $PROJECT)"
|
||||||
|
|
||||||
# Vérifier si les containers existent
|
# Vérifier si les containers existent
|
||||||
echo "🔍 Vérification des containers..."
|
echo "🔍 Vérification des containers..."
|
||||||
@@ -47,37 +69,24 @@ else
|
|||||||
echo "⚠️ Le dossier API n'existe pas sur la destination"
|
echo "⚠️ Le dossier API n'existe pas sur la destination"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sauvegarder spécifiquement le dossier logs
|
|
||||||
echo "📋 Sauvegarde du dossier logs..."
|
|
||||||
# Vérifier si le dossier logs existe
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH/logs"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
# Le dossier logs existe, le sauvegarder
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p /tmp/geosector_logs_backup"
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $API_PATH/logs /tmp/geosector_logs_backup/"
|
|
||||||
echo "✅ Dossier logs sauvegardé temporairement"
|
|
||||||
else
|
|
||||||
echo "⚠️ Le dossier logs n'existe pas sur la destination"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copier le dossier API entre les containers
|
# Copier le dossier API entre les containers
|
||||||
echo "📋 Copie des fichiers en cours..."
|
echo "📋 Copie des fichiers en cours..."
|
||||||
|
|
||||||
# Approche directe: utiliser incus copy pour copier directement entre containers
|
# Nettoyage sélectif : supprimer seulement le code, pas les données (logs et uploads)
|
||||||
echo "📤 Transfert direct entre containers..."
|
echo "🧹 Nettoyage sélectif (préservation de logs et uploads)..."
|
||||||
# Nettoyer le dossier de destination
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' -exec rm -rf {} \;"
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $API_PATH"
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH"
|
|
||||||
|
|
||||||
# Copier directement du container source vers le container destination
|
# Copier directement du container source vers le container destination (en excluant logs et uploads)
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $API_PATH . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $API_PATH"
|
echo "📤 Transfert du code (hors logs et uploads)..."
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $API_PATH --exclude='uploads' --exclude='logs' . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $API_PATH"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "❌ Erreur lors du transfert direct entre containers"
|
echo "❌ Erreur lors du transfert entre containers"
|
||||||
echo "⚠️ Tentative de restauration de la sauvegarde..."
|
echo "⚠️ Tentative de restauration de la sauvegarde..."
|
||||||
# Vérifier si la sauvegarde existe
|
# Vérifier si la sauvegarde existe
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR"
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR"
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
# La sauvegarde existe, la restaurer
|
# La sauvegarde existe, la restaurer
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $API_PATH"
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $API_PATH"
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $API_PATH"
|
||||||
echo "✅ Restauration réussie"
|
echo "✅ Restauration réussie"
|
||||||
else
|
else
|
||||||
@@ -86,21 +95,7 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Pas de fichiers temporaires à nettoyer avec cette approche
|
echo "✅ Code transféré avec succès (logs et uploads préservés)"
|
||||||
|
|
||||||
# Restaurer le dossier logs
|
|
||||||
echo "📋 Restauration du dossier logs..."
|
|
||||||
# Vérifier si la sauvegarde des logs existe
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d /tmp/geosector_logs_backup/logs"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
# La sauvegarde des logs existe, la restaurer
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH/logs"
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r /tmp/geosector_logs_backup/logs/* $API_PATH/logs/"
|
|
||||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf /tmp/geosector_logs_backup"
|
|
||||||
echo "✅ Dossier logs restauré"
|
|
||||||
else
|
|
||||||
echo "⚠️ Aucune sauvegarde de logs à restaurer"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Changer le propriétaire et les permissions des fichiers
|
# Changer le propriétaire et les permissions des fichiers
|
||||||
echo "👤 Application des droits et permissions pour tous les fichiers..."
|
echo "👤 Application des droits et permissions pour tous les fichiers..."
|
||||||
@@ -128,6 +123,23 @@ else
|
|||||||
echo "⚠️ Le dossier logs n'existe pas"
|
echo "⚠️ Le dossier logs n'existe pas"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Vérifier et corriger les permissions du dossier uploads s'il existe
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH/uploads"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
# S'assurer que uploads a les bonnes permissions
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nobody $API_PATH/uploads"
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chmod -R 775 $API_PATH/uploads"
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH/uploads -type f -exec chmod 664 {} \;"
|
||||||
|
echo "✅ Droits vérifiés pour le dossier uploads (nginx:nginx avec permissions 775)"
|
||||||
|
else
|
||||||
|
# Créer le dossier uploads s'il n'existe pas
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH/uploads"
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nobody $API_PATH/uploads"
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chmod -R 775 $API_PATH/uploads"
|
||||||
|
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH/uploads -type f -exec chmod 664 {} \;"
|
||||||
|
echo "✅ Dossier uploads créé avec les bonnes permissions (nginx:nginx avec permissions 775/664)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "✅ Propriétaire et permissions appliqués avec succès"
|
echo "✅ Propriétaire et permissions appliqués avec succès"
|
||||||
|
|
||||||
# Vérifier la copie
|
# Vérifier la copie
|
||||||
@@ -139,6 +151,8 @@ else
|
|||||||
echo "❌ Erreur: Le dossier API n'a pas été copié correctement"
|
echo "❌ Erreur: Le dossier API n'a pas été copié correctement"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Opération terminée! L'API a été copiée de $SOURCE_CONTAINER vers $DEST_CONTAINER"
|
echo "✅ Livraison vers $ENV_NAME terminée avec succès!"
|
||||||
echo "📁 Une sauvegarde a été créée dans $BACKUP_DIR sur $DEST_CONTAINER"
|
echo "📤 Source: $SOURCE_CONTAINER → Destination: $DEST_CONTAINER"
|
||||||
echo "👤 Les fichiers appartiennent maintenant à l'utilisateur nginx"
|
echo "📁 Sauvegarde créée: $BACKUP_DIR sur $DEST_CONTAINER"
|
||||||
|
echo "🔒 Données préservées: logs/ et uploads/ intouchés"
|
||||||
|
echo "👤 Permissions: nginx:nginx (755/644) + logs (nginx:nobody 775/664)"
|
||||||
|
|||||||
28
api/migration_add_departements_contours.sql
Normal file
28
api/migration_add_departements_contours.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- Table pour stocker les contours (polygones) des départements
|
||||||
|
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
|
||||||
|
`nom_dept` varchar(100) NOT NULL,
|
||||||
|
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
|
||||||
|
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
|
||||||
|
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
|
||||||
|
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
|
||||||
|
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||||
|
SPATIAL KEY `idx_contour` (`contour`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Index pour améliorer les performances
|
||||||
|
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
|
||||||
|
|
||||||
|
-- Exemples d'insertion (à remplacer par les vraies données)
|
||||||
|
-- Les coordonnées doivent être dans l'ordre : longitude latitude
|
||||||
|
-- Le polygone doit être fermé (premier point = dernier point)
|
||||||
|
/*
|
||||||
|
INSERT INTO x_departements_contours (code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng) VALUES
|
||||||
|
('22', 'Côtes-d\'Armor', ST_GeomFromText('POLYGON((-3.65 48.90, -2.00 48.90, -2.00 48.05, -3.65 48.05, -3.65 48.90))'), 48.05, 48.90, -3.65, -2.00),
|
||||||
|
('29', 'Finistère', ST_GeomFromText('POLYGON((-5.14 48.75, -3.38 48.75, -3.38 47.64, -5.14 47.64, -5.14 48.75))'), 47.64, 48.75, -5.14, -3.38);
|
||||||
|
*/
|
||||||
26
api/migration_add_file_category.sql
Executable file
26
api/migration_add_file_category.sql
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
-- Migration pour ajouter la colonne file_category à la table medias
|
||||||
|
-- Date: 2025-06-22
|
||||||
|
-- Description: Ajout du champ file_category pour distinguer les types métier des fichiers
|
||||||
|
|
||||||
|
-- Ajout de la colonne file_category
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD COLUMN `file_category` varchar(50) DEFAULT NULL COMMENT 'Catégorie du fichier (logo, carte, photo, document, etc.)' AFTER `file_type`;
|
||||||
|
|
||||||
|
-- Ajout de l'index pour optimiser les requêtes
|
||||||
|
ALTER TABLE `medias`
|
||||||
|
ADD INDEX `idx_file_category` (`file_category`);
|
||||||
|
|
||||||
|
-- Mise à jour des données existantes avec des catégories par défaut selon le support
|
||||||
|
UPDATE `medias` SET `file_category` = 'document' WHERE `support` = 'entite' AND `file_category` IS NULL;
|
||||||
|
UPDATE `medias` SET `file_category` = 'avatar' WHERE `support` = 'user' AND `file_category` IS NULL;
|
||||||
|
UPDATE `medias` SET `file_category` = 'export' WHERE `support` = 'operation' AND `file_category` IS NULL;
|
||||||
|
UPDATE `medias` SET `file_category` = 'recu' WHERE `support` = 'passage' AND `file_category` IS NULL;
|
||||||
|
|
||||||
|
-- Vérification des modifications
|
||||||
|
SELECT
|
||||||
|
support,
|
||||||
|
file_category,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM medias
|
||||||
|
GROUP BY support, file_category
|
||||||
|
ORDER BY support, file_category;
|
||||||
16
api/migration_add_ope_users_fields.sql
Executable file
16
api/migration_add_ope_users_fields.sql
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Migration pour ajouter les champs utilisateur dans ope_users
|
||||||
|
-- Date: 2025-06-23
|
||||||
|
-- Description: Ajout des champs fk_role, first_name, encrypted_name, sect_name dans ope_users
|
||||||
|
-- pour conserver un historique propre de chaque opération
|
||||||
|
|
||||||
|
USE geo_app;
|
||||||
|
|
||||||
|
-- Ajout des nouvelles colonnes dans ope_users
|
||||||
|
ALTER TABLE ope_users
|
||||||
|
ADD COLUMN fk_role int unsigned DEFAULT 1 AFTER fk_user,
|
||||||
|
ADD COLUMN first_name varchar(45) DEFAULT '' AFTER fk_role,
|
||||||
|
ADD COLUMN encrypted_name varchar(255) DEFAULT '' AFTER first_name,
|
||||||
|
ADD COLUMN sect_name varchar(60) DEFAULT '' AFTER encrypted_name;
|
||||||
|
|
||||||
|
-- Vérification de la structure modifiée
|
||||||
|
DESCRIBE ope_users;
|
||||||
29
api/migration_add_sectors_adresses.sql
Normal file
29
api/migration_add_sectors_adresses.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-- Migration pour ajouter la table sectors_adresses
|
||||||
|
-- Cette table stocke les adresses associées à chaque secteur
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `sectors_adresses` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`fk_sector` int(11) NOT NULL,
|
||||||
|
`fk_address` bigint(20) NOT NULL COMMENT 'ID de l''adresse dans la base adresses',
|
||||||
|
`numero` varchar(10) DEFAULT NULL,
|
||||||
|
`voie` varchar(255) DEFAULT NULL,
|
||||||
|
`code_postal` varchar(5) DEFAULT NULL,
|
||||||
|
`commune` varchar(100) DEFAULT NULL,
|
||||||
|
`latitude` decimal(10,8) NOT NULL,
|
||||||
|
`longitude` decimal(11,8) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_sector` (`fk_sector`),
|
||||||
|
KEY `idx_address` (`fk_address`),
|
||||||
|
KEY `idx_code_postal` (`code_postal`),
|
||||||
|
KEY `idx_commune` (`commune`),
|
||||||
|
KEY `idx_coords` (`latitude`, `longitude`),
|
||||||
|
CONSTRAINT `fk_sectors_adresses_sector` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Index pour améliorer les performances des requêtes géographiques
|
||||||
|
CREATE INDEX idx_sectors_adresses_geo ON sectors_adresses (fk_sector, latitude, longitude);
|
||||||
|
|
||||||
|
-- Commentaire sur la table
|
||||||
|
ALTER TABLE `sectors_adresses`
|
||||||
|
COMMENT = 'Table de liaison entre les secteurs et les adresses géographiques contenues dans leurs périmètres';
|
||||||
45
api/migrations/add_dept_limitrophes.sql
Normal file
45
api/migrations/add_dept_limitrophes.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
-- Ajout du champ dept_limitrophes dans la table x_departements
|
||||||
|
-- Ce champ contiendra la liste des codes départements limitrophes séparés par des virgules
|
||||||
|
-- Exemple : "22,35,44,56" pour le Morbihan (56)
|
||||||
|
|
||||||
|
ALTER TABLE x_departements
|
||||||
|
ADD COLUMN dept_limitrophes VARCHAR(100) DEFAULT NULL
|
||||||
|
COMMENT 'Liste des codes départements limitrophes séparés par des virgules'
|
||||||
|
AFTER libelle;
|
||||||
|
|
||||||
|
-- Exemples de mise à jour pour quelques départements bretons
|
||||||
|
-- À compléter avec tous les départements
|
||||||
|
|
||||||
|
-- Côtes-d'Armor (22) : limitrophe avec 29, 35, 56
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22';
|
||||||
|
|
||||||
|
-- Finistère (29) : limitrophe avec 22, 56
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29';
|
||||||
|
|
||||||
|
-- Ille-et-Vilaine (35) : limitrophe avec 22, 44, 49, 50, 53, 56
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35';
|
||||||
|
|
||||||
|
-- Morbihan (56) : limitrophe avec 22, 29, 35, 44
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56';
|
||||||
|
|
||||||
|
-- Loire-Atlantique (44) : limitrophe avec 35, 49, 56, 85
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44';
|
||||||
|
|
||||||
|
-- Hauts-de-France
|
||||||
|
-- Aisne (02) : limitrophe avec 08, 51, 59, 60, 77, 80
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02';
|
||||||
|
|
||||||
|
-- Nord (59) : limitrophe avec 02, 62, 80 (+ frontière Belgique)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59';
|
||||||
|
|
||||||
|
-- Oise (60) : limitrophe avec 02, 27, 76, 77, 80, 95
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60';
|
||||||
|
|
||||||
|
-- Pas-de-Calais (62) : limitrophe avec 59, 80
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62';
|
||||||
|
|
||||||
|
-- Somme (80) : limitrophe avec 02, 27, 59, 60, 62, 76
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80';
|
||||||
|
|
||||||
|
-- Note : Ces données sont à compléter pour tous les départements français
|
||||||
|
-- Source recommandée : données INSEE ou IGN pour la liste complète et exacte
|
||||||
55
api/migrations/integrate_contours_to_departements.sql
Normal file
55
api/migrations/integrate_contours_to_departements.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
-- Script pour intégrer les données de x_departements_contours dans x_departements
|
||||||
|
-- Ce script ajoute une colonne contour à x_departements et y copie les données
|
||||||
|
-- Note : Les colonnes bbox_* ne sont pas migrées car elles ne sont pas utilisées dans l'API
|
||||||
|
|
||||||
|
-- 1. Ajouter la colonne contour à x_departements si elle n'existe pas déjà
|
||||||
|
ALTER TABLE x_departements
|
||||||
|
ADD COLUMN IF NOT EXISTS contour GEOMETRY DEFAULT NULL
|
||||||
|
COMMENT 'Contour géographique du département'
|
||||||
|
AFTER dept_limitrophes;
|
||||||
|
|
||||||
|
-- 2. Mettre à jour x_departements avec les contours depuis x_departements_contours
|
||||||
|
-- On copie directement les contours non NULL
|
||||||
|
UPDATE x_departements d
|
||||||
|
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
|
||||||
|
SET d.contour = dc.contour
|
||||||
|
WHERE dc.contour IS NOT NULL;
|
||||||
|
|
||||||
|
-- 3. Vérifier les départements sans contour (hors DOM-TOM et Corse historique)
|
||||||
|
SELECT
|
||||||
|
d.code,
|
||||||
|
d.libelle,
|
||||||
|
CASE
|
||||||
|
WHEN d.code IN ('20', '971', '972', '973', '974', '975', '976') THEN 'DOM-TOM ou Corse historique - Normal'
|
||||||
|
WHEN d.contour IS NULL THEN 'Pas de contour'
|
||||||
|
ELSE 'OK'
|
||||||
|
END as statut
|
||||||
|
FROM x_departements d
|
||||||
|
WHERE d.contour IS NULL
|
||||||
|
AND d.code NOT IN ('20', '971', '972', '973', '974', '975', '976')
|
||||||
|
ORDER BY d.code;
|
||||||
|
|
||||||
|
-- 4. Créer l'index spatial
|
||||||
|
-- Les valeurs NULL sont autorisées dans un index spatial MySQL
|
||||||
|
ALTER TABLE x_departements
|
||||||
|
ADD SPATIAL INDEX idx_contour (contour);
|
||||||
|
|
||||||
|
-- 5. Vérifier le nombre de départements mis à jour
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_departements,
|
||||||
|
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as departements_avec_contour,
|
||||||
|
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as departements_sans_contour
|
||||||
|
FROM x_departements;
|
||||||
|
|
||||||
|
-- 6. Lister les départements qui n'ont pas de contour (s'il y en a)
|
||||||
|
SELECT code, libelle
|
||||||
|
FROM x_departements
|
||||||
|
WHERE contour IS NULL
|
||||||
|
ORDER BY code;
|
||||||
|
|
||||||
|
-- 7. Optionnel : Après vérification, vous pouvez supprimer la table x_departements_contours
|
||||||
|
-- ATTENTION : Ne décommentez cette ligne qu'après avoir vérifié que toutes les données sont bien migrées
|
||||||
|
-- DROP TABLE IF EXISTS x_departements_contours;
|
||||||
|
|
||||||
|
-- 8. Mettre à jour les statistiques de la table pour optimiser les requêtes spatiales
|
||||||
|
ANALYZE TABLE x_departements;
|
||||||
131
api/migrations/update_all_dept_limitrophes.sql
Normal file
131
api/migrations/update_all_dept_limitrophes.sql
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
-- Mise à jour complète des départements limitrophes pour tous les départements français
|
||||||
|
-- Format : liste des codes départements séparés par des virgules
|
||||||
|
|
||||||
|
-- Auvergne-Rhône-Alpes
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '07,15,43,48' WHERE code = '01'; -- Ain : Ardèche, Cantal, Haute-Loire, Lozère
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,05,26,84' WHERE code = '03'; -- Allier : Alpes-de-Haute-Provence, Hautes-Alpes, Drôme, Vaucluse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,26,30,43,48,84' WHERE code = '07'; -- Ardèche : Ain, Drôme, Gard, Haute-Loire, Lozère, Vaucluse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '12,19,43,46,48' WHERE code = '15'; -- Cantal : Aveyron, Corrèze, Haute-Loire, Lot, Lozère
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,05,73,84' WHERE code = '26'; -- Drôme : Alpes-de-Haute-Provence, Hautes-Alpes, Savoie, Vaucluse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '05,06,13,73' WHERE code = '38'; -- Isère : Hautes-Alpes, Alpes-Maritimes, Bouches-du-Rhône, Savoie
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '07,15,19,43,48,63' WHERE code = '42'; -- Loire : Ardèche, Cantal, Corrèze, Haute-Loire, Lozère, Puy-de-Dôme
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,07,15,42,48,63' WHERE code = '43'; -- Haute-Loire : Ain, Ardèche, Cantal, Loire, Lozère, Puy-de-Dôme
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '03,15,23,42,43' WHERE code = '63'; -- Puy-de-Dôme : Allier, Cantal, Creuse, Loire, Haute-Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,38,39,71' WHERE code = '69'; -- Rhône : Ain, Isère, Jura, Saône-et-Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,25,38,39,74' WHERE code = '73'; -- Savoie : Ain, Doubs, Isère, Jura, Haute-Savoie
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,73' WHERE code = '74'; -- Haute-Savoie : Ain, Savoie (+ frontières Suisse et Italie)
|
||||||
|
|
||||||
|
-- Bourgogne-Franche-Comté
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '10,45,52,58,77,89' WHERE code = '21'; -- Côte-d'Or : Aube, Loiret, Haute-Marne, Nièvre, Seine-et-Marne, Yonne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '39,68,70,73,90' WHERE code = '25'; -- Doubs : Jura, Haut-Rhin, Haute-Saône, Savoie, Territoire de Belfort (+ frontière Suisse)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,25,69,70,71,73' WHERE code = '39'; -- Jura : Ain, Doubs, Rhône, Haute-Saône, Saône-et-Loire, Savoie
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '03,18,21,45,71,89' WHERE code = '58'; -- Nièvre : Allier, Cher, Côte-d'Or, Loiret, Saône-et-Loire, Yonne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '21,25,39,52,88' WHERE code = '70'; -- Haute-Saône : Côte-d'Or, Doubs, Jura, Haute-Marne, Vosges
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '01,03,21,39,58,69' WHERE code = '71'; -- Saône-et-Loire : Ain, Allier, Côte-d'Or, Jura, Nièvre, Rhône
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '10,21,45,58,77' WHERE code = '89'; -- Yonne : Aube, Côte-d'Or, Loiret, Nièvre, Seine-et-Marne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '25,68,70' WHERE code = '90'; -- Territoire de Belfort : Doubs, Haut-Rhin, Haute-Saône
|
||||||
|
|
||||||
|
-- Bretagne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22'; -- Côtes-d'Armor
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29'; -- Finistère
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35'; -- Ille-et-Vilaine
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56'; -- Morbihan
|
||||||
|
|
||||||
|
-- Centre-Val de Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '03,23,36,41,58' WHERE code = '18'; -- Cher
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '27,37,41,45,61,72,78,91' WHERE code = '28'; -- Eure-et-Loir
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '18,23,37,41,86,87' WHERE code = '36'; -- Indre
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '36,41,49,72,86' WHERE code = '37'; -- Indre-et-Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '18,28,36,37,45,72' WHERE code = '41'; -- Loir-et-Cher
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '18,21,28,41,58,77,89,91' WHERE code = '45'; -- Loiret
|
||||||
|
|
||||||
|
-- Corse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '2B' WHERE code = '2A'; -- Corse-du-Sud : Haute-Corse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '2A' WHERE code = '2B'; -- Haute-Corse : Corse-du-Sud
|
||||||
|
|
||||||
|
-- Grand Est
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,51,55' WHERE code = '08'; -- Ardennes (+ frontière Belgique)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '21,51,52,77,89' WHERE code = '10'; -- Aube
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,08,10,52,77' WHERE code = '51'; -- Marne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '10,21,51,55,70,88' WHERE code = '52'; -- Haute-Marne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '55,57,88' WHERE code = '54'; -- Meurthe-et-Moselle
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '08,52,54,57' WHERE code = '55'; -- Meuse (+ frontière Belgique)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '54,55,67' WHERE code = '57'; -- Moselle (+ frontières Luxembourg et Allemagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '25,57,68,88,90' WHERE code = '67'; -- Bas-Rhin (+ frontière Allemagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '67,70,88,90' WHERE code = '68'; -- Haut-Rhin (+ frontières Allemagne et Suisse)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '52,54,67,68,70' WHERE code = '88'; -- Vosges
|
||||||
|
|
||||||
|
-- Hauts-de-France
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02'; -- Aisne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59'; -- Nord (+ frontière Belgique)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60'; -- Oise
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62'; -- Pas-de-Calais (+ frontière Belgique)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80'; -- Somme
|
||||||
|
|
||||||
|
-- Île-de-France
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '92,93,94' WHERE code = '75'; -- Paris
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '02,10,45,51,60,89,91,93,94,95' WHERE code = '77'; -- Seine-et-Marne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '27,28,91,92,95' WHERE code = '78'; -- Yvelines
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '28,45,77,78,92,94' WHERE code = '91'; -- Essonne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '75,78,91,93,94,95' WHERE code = '92'; -- Hauts-de-Seine
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '75,77,92,94,95' WHERE code = '93'; -- Seine-Saint-Denis
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '75,77,91,92,93' WHERE code = '94'; -- Val-de-Marne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '27,60,77,78,92,93' WHERE code = '95'; -- Val-d'Oise
|
||||||
|
|
||||||
|
-- Normandie
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '27,50,53,61,72' WHERE code = '14'; -- Calvados
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '14,28,60,61,72,76,78,95' WHERE code = '27'; -- Eure
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '14,35,53,61' WHERE code = '50'; -- Manche
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '14,27,28,35,41,50,53,72' WHERE code = '61'; -- Orne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '27,60,80' WHERE code = '76'; -- Seine-Maritime
|
||||||
|
|
||||||
|
-- Nouvelle-Aquitaine
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '17,24,33,87' WHERE code = '16'; -- Charente
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '16,33,79,85' WHERE code = '17'; -- Charente-Maritime
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '15,23,24,46,87' WHERE code = '19'; -- Corrèze
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '18,19,36,86,87' WHERE code = '23'; -- Creuse
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '16,19,33,46,47,87' WHERE code = '24'; -- Dordogne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '16,17,24,40,47' WHERE code = '33'; -- Gironde
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '32,33,47,64,65' WHERE code = '40'; -- Landes
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '24,32,40,46,82' WHERE code = '47'; -- Lot-et-Garonne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '40,65' WHERE code = '64'; -- Pyrénées-Atlantiques (+ frontière Espagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '17,49,85,86' WHERE code = '79'; -- Deux-Sèvres
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '16,23,36,37,79' WHERE code = '86'; -- Vienne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '16,19,23,24,36,86' WHERE code = '87'; -- Haute-Vienne
|
||||||
|
|
||||||
|
-- Occitanie
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '11,31,66' WHERE code = '09'; -- Ariège : Aude, Haute-Garonne, Pyrénées-Orientales (+ frontières Espagne et Andorre)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '09,31,66' WHERE code = '11'; -- Aude
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '15,30,34,46,48,81,82' WHERE code = '12'; -- Aveyron
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '07,12,34,48,84' WHERE code = '30'; -- Gard
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '09,32,65,82' WHERE code = '31'; -- Haute-Garonne : Ariège, Gers, Hautes-Pyrénées, Tarn-et-Garonne (+ frontière Espagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '31,40,47,65,82' WHERE code = '32'; -- Gers
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '11,12,30' WHERE code = '34'; -- Hérault
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '12,15,19,24,47,81,82' WHERE code = '46'; -- Lot
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '07,12,15,30,43' WHERE code = '48'; -- Lozère
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '31,32,40,64' WHERE code = '65'; -- Hautes-Pyrénées (+ frontière Espagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '09,11' WHERE code = '66'; -- Pyrénées-Orientales (+ frontière Espagne)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '12,34,46,82' WHERE code = '81'; -- Tarn
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '12,31,32,46,47,81' WHERE code = '82'; -- Tarn-et-Garonne
|
||||||
|
|
||||||
|
-- Pays de la Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44'; -- Loire-Atlantique
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '35,37,44,53,72,79,85,86' WHERE code = '49'; -- Maine-et-Loire
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '14,35,49,50,61,72' WHERE code = '53'; -- Mayenne
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '14,27,28,37,41,49,53,61' WHERE code = '72'; -- Sarthe
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '17,44,49,79' WHERE code = '85'; -- Vendée
|
||||||
|
|
||||||
|
-- Provence-Alpes-Côte d'Azur
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '05,06,26,83,84' WHERE code = '04'; -- Alpes-de-Haute-Provence
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,26,38,73' WHERE code = '05'; -- Hautes-Alpes (+ frontière Italie)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,83' WHERE code = '06'; -- Alpes-Maritimes (+ frontières Italie et Monaco)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '30,83,84' WHERE code = '13'; -- Bouches-du-Rhône
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,06,13,84' WHERE code = '83'; -- Var
|
||||||
|
UPDATE x_departements SET dept_limitrophes = '04,07,13,26,30,83' WHERE code = '84'; -- Vaucluse
|
||||||
|
|
||||||
|
-- Départements et régions d'outre-mer (pas de limitrophes terrestres)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '971'; -- Guadeloupe
|
||||||
|
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '972'; -- Martinique
|
||||||
|
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '973'; -- Guyane (frontières Brésil et Suriname)
|
||||||
|
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '974'; -- La Réunion
|
||||||
|
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '976'; -- Mayotte
|
||||||
0
api/scripts/README.md
Normal file → Executable file
0
api/scripts/README.md
Normal file → Executable file
33
api/scripts/check_geometry_validity.sql
Normal file
33
api/scripts/check_geometry_validity.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- Script de diagnostic pour vérifier les problèmes de géométrie dans x_departements_contours
|
||||||
|
|
||||||
|
-- 1. Vérifier les contours NULL
|
||||||
|
SELECT 'Contours NULL:' as diagnostic;
|
||||||
|
SELECT code_dept, nom_dept
|
||||||
|
FROM x_departements_contours
|
||||||
|
WHERE contour IS NULL;
|
||||||
|
|
||||||
|
-- 2. Vérifier les types de géométrie et si elles sont vides
|
||||||
|
SELECT 'Types de géométrie:' as diagnostic;
|
||||||
|
SELECT
|
||||||
|
code_dept,
|
||||||
|
nom_dept,
|
||||||
|
ST_GeometryType(contour) as geometry_type,
|
||||||
|
ST_IsEmpty(contour) as is_empty
|
||||||
|
FROM x_departements_contours
|
||||||
|
WHERE contour IS NOT NULL;
|
||||||
|
|
||||||
|
-- 3. Statistiques générales
|
||||||
|
SELECT 'Statistiques:' as diagnostic;
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as contours_null,
|
||||||
|
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as contours_non_null
|
||||||
|
FROM x_departements_contours;
|
||||||
|
|
||||||
|
-- 4. Lister spécifiquement les DOM-TOM et Corse
|
||||||
|
SELECT 'DOM-TOM et Corse:' as diagnostic;
|
||||||
|
SELECT code_dept, nom_dept,
|
||||||
|
CASE WHEN contour IS NULL THEN 'NULL' ELSE 'OK' END as contour_status
|
||||||
|
FROM x_departements_contours
|
||||||
|
WHERE code_dept IN ('20', '2A', '2B', '971', '972', '973', '974', '975', '976')
|
||||||
|
ORDER BY code_dept;
|
||||||
0
api/scripts/config.php
Normal file → Executable file
0
api/scripts/config.php
Normal file → Executable file
32
api/scripts/create_addresses_users.sql
Normal file
32
api/scripts/create_addresses_users.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- Script de création des utilisateurs pour la base de données des adresses
|
||||||
|
-- À exécuter sur chaque serveur MariaDB (dva-maria, rca-maria, pra-maria)
|
||||||
|
|
||||||
|
-- Créer l'utilisateur avec accès depuis l'IP du container API correspondant
|
||||||
|
-- IMPORTANT: Remplacer 'API_CONTAINER_IP' par l'IP réelle du container API
|
||||||
|
|
||||||
|
-- Pour l'environnement DEV (dva-maria)
|
||||||
|
-- Si l'API est dans le container dva-api avec l'IP 13.23.33.45 par exemple :
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.45' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.45';
|
||||||
|
|
||||||
|
-- Pour l'environnement RECETTE (rca-maria)
|
||||||
|
-- Si l'API est dans le container rca-api avec l'IP 13.23.33.35 par exemple :
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.35' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.35';
|
||||||
|
|
||||||
|
-- Pour l'environnement PROD (pra-maria)
|
||||||
|
-- Si l'API est dans le container pra-api avec l'IP 13.23.33.25 par exemple :
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.25' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.25';
|
||||||
|
|
||||||
|
-- Alternative : Créer un utilisateur accessible depuis tout le sous-réseau
|
||||||
|
-- ATTENTION : Moins sécurisé, à utiliser uniquement si les containers sont dans un réseau privé isolé
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.%';
|
||||||
|
|
||||||
|
-- Appliquer les privilèges
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier la création
|
||||||
|
SELECT user, host FROM mysql.user WHERE user = 'adresses_user';
|
||||||
|
SHOW GRANTS FOR 'adresses_user'@'13.23.33.%';
|
||||||
41
api/scripts/create_addresses_users_by_env.sql
Normal file
41
api/scripts/create_addresses_users_by_env.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- Script de création des utilisateurs pour la base de données des adresses
|
||||||
|
-- Avec segmentation par environnement basée sur les plages d'IPs
|
||||||
|
|
||||||
|
-- ===================================
|
||||||
|
-- DÉVELOPPEMENT (dva-maria)
|
||||||
|
-- IPs autorisées : 13.23.33.40-49
|
||||||
|
-- ===================================
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.4%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.4%';
|
||||||
|
|
||||||
|
-- Aussi créer un accès localhost pour les tests directs
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||||
|
|
||||||
|
-- ===================================
|
||||||
|
-- RECETTE (rca-maria)
|
||||||
|
-- IPs autorisées : 13.23.33.30-39
|
||||||
|
-- ===================================
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.3%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.3%';
|
||||||
|
|
||||||
|
-- Aussi créer un accès localhost pour les tests directs
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||||
|
|
||||||
|
-- ===================================
|
||||||
|
-- PRODUCTION (pra-maria)
|
||||||
|
-- IPs autorisées : 13.23.33.20-29
|
||||||
|
-- ===================================
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.2%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.2%';
|
||||||
|
|
||||||
|
-- Aussi créer un accès localhost pour les tests directs
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||||
|
|
||||||
|
-- Appliquer les privilèges
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier la création
|
||||||
|
SELECT user, host FROM mysql.user WHERE user = 'adresses_user' ORDER BY host;
|
||||||
0
api/scripts/cron/sync_databases.php
Normal file → Executable file
0
api/scripts/cron/sync_databases.php
Normal file → Executable file
37
api/scripts/fix_geometry_for_spatial_index.sql
Normal file
37
api/scripts/fix_geometry_for_spatial_index.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- Script pour diagnostiquer et corriger les problèmes d'index spatial
|
||||||
|
|
||||||
|
-- 1. Vérifier s'il y a des géométries vides
|
||||||
|
SELECT 'Géométries vides:' as diagnostic;
|
||||||
|
SELECT code, libelle
|
||||||
|
FROM x_departements
|
||||||
|
WHERE contour IS NOT NULL AND ST_IsEmpty(contour) = 1;
|
||||||
|
|
||||||
|
-- 2. Essayer de créer un index spatial sur une copie de la table pour tester
|
||||||
|
CREATE TABLE x_departements_test LIKE x_departements;
|
||||||
|
|
||||||
|
-- 3. Copier uniquement les départements métropolitains avec contours
|
||||||
|
INSERT INTO x_departements_test
|
||||||
|
SELECT * FROM x_departements
|
||||||
|
WHERE contour IS NOT NULL
|
||||||
|
AND code NOT IN ('20', '971', '972', '973', '974', '975', '976');
|
||||||
|
|
||||||
|
-- 4. Tenter de créer l'index spatial sur la table de test
|
||||||
|
ALTER TABLE x_departements_test ADD SPATIAL INDEX idx_contour_test (contour);
|
||||||
|
|
||||||
|
-- Si ça fonctionne, le problème vient des départements spécifiques
|
||||||
|
-- Si ça ne fonctionne pas, il y a un problème avec les données géométriques
|
||||||
|
|
||||||
|
-- 5. Alternative : recréer les géométries à partir du texte WKT
|
||||||
|
-- Cela peut corriger certains problèmes de format
|
||||||
|
UPDATE x_departements d
|
||||||
|
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
|
||||||
|
SET d.contour = ST_GeomFromText(ST_AsText(dc.contour))
|
||||||
|
WHERE dc.contour IS NOT NULL
|
||||||
|
AND d.code IN (SELECT code FROM x_departements WHERE contour IS NOT NULL LIMIT 1);
|
||||||
|
|
||||||
|
-- 6. Nettoyer
|
||||||
|
DROP TABLE IF EXISTS x_departements_test;
|
||||||
|
|
||||||
|
-- Note : Pour l'instant, l'index normal créé avec contour(32) permettra
|
||||||
|
-- le fonctionnement de l'API, même si les performances seront moindres
|
||||||
|
-- qu'avec un vrai index spatial.
|
||||||
0
api/scripts/geosector.sql
Normal file → Executable file
0
api/scripts/geosector.sql
Normal file → Executable file
0
api/scripts/geosector_app.sql
Normal file → Executable file
0
api/scripts/geosector_app.sql
Normal file → Executable file
263
api/scripts/import_departements_from_file.php
Normal file
263
api/scripts/import_departements_from_file.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script d'import des contours départementaux depuis un fichier GeoJSON local
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DepartementContoursFileImporter {
|
||||||
|
private PDO $db;
|
||||||
|
private array $log = [];
|
||||||
|
|
||||||
|
public function __construct(PDO $db) {
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la table existe
|
||||||
|
*/
|
||||||
|
public function tableExists(): bool {
|
||||||
|
try {
|
||||||
|
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la table est vide
|
||||||
|
*/
|
||||||
|
private function isTableEmpty(): bool {
|
||||||
|
try {
|
||||||
|
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
return $result['count'] == 0;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importe les départements depuis un fichier GeoJSON
|
||||||
|
*/
|
||||||
|
public function importFromFile(string $filePath): array {
|
||||||
|
$this->log[] = "Début de l'import depuis le fichier : $filePath";
|
||||||
|
$this->log[] = "";
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$this->log[] = "✗ Fichier non trouvé : $filePath";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la table existe
|
||||||
|
if (!$this->tableExists()) {
|
||||||
|
$this->log[] = "✗ La table x_departements_contours n'existe pas";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la table est vide
|
||||||
|
if (!$this->isTableEmpty()) {
|
||||||
|
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire le fichier GeoJSON
|
||||||
|
$this->log[] = "Lecture du fichier GeoJSON...";
|
||||||
|
$jsonContent = file_get_contents($filePath);
|
||||||
|
|
||||||
|
if ($jsonContent === false) {
|
||||||
|
$this->log[] = "✗ Impossible de lire le fichier";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser le JSON
|
||||||
|
$geojson = json_decode($jsonContent, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$this->log[] = "✗ Erreur JSON : " . json_last_error_msg();
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($geojson['features']) || !is_array($geojson['features'])) {
|
||||||
|
$this->log[] = "✗ Format GeoJSON invalide : pas de features";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log[] = "✓ Fichier chargé : " . count($geojson['features']) . " départements trouvés";
|
||||||
|
$this->log[] = "";
|
||||||
|
|
||||||
|
// Préparer la requête d'insertion
|
||||||
|
$sql = "INSERT INTO x_departements_contours
|
||||||
|
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||||
|
VALUES
|
||||||
|
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
|
||||||
|
$success = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
// Démarrer une transaction
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($geojson['features'] as $feature) {
|
||||||
|
// Extraire les informations
|
||||||
|
$code = $feature['properties']['code'] ?? null;
|
||||||
|
$nom = $feature['properties']['nom'] ?? null;
|
||||||
|
$geometry = $feature['geometry'] ?? null;
|
||||||
|
|
||||||
|
if (!$code || !$nom || !$geometry) {
|
||||||
|
$this->log[] = "✗ Données manquantes pour un département";
|
||||||
|
$errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir la géométrie en WKT
|
||||||
|
$wktData = $this->geometryToWkt($geometry);
|
||||||
|
|
||||||
|
if (!$wktData) {
|
||||||
|
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
|
||||||
|
$errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt->execute([
|
||||||
|
'code' => $code,
|
||||||
|
'nom' => $nom,
|
||||||
|
'polygon' => $wktData['wkt'],
|
||||||
|
'min_lat' => $wktData['bbox']['min_lat'],
|
||||||
|
'max_lat' => $wktData['bbox']['max_lat'],
|
||||||
|
'min_lng' => $wktData['bbox']['min_lng'],
|
||||||
|
'max_lng' => $wktData['bbox']['max_lng']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->log[] = "✓ $code - $nom importé";
|
||||||
|
$success++;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider ou annuler la transaction
|
||||||
|
if ($success > 0) {
|
||||||
|
$this->db->commit();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✓ Transaction validée";
|
||||||
|
} else {
|
||||||
|
$this->db->rollBack();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->db->rollBack();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
|
||||||
|
$this->log[] = "✗ Transaction annulée";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
|
||||||
|
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une géométrie GeoJSON en WKT
|
||||||
|
*/
|
||||||
|
private function geometryToWkt(array $geometry): ?array {
|
||||||
|
$type = $geometry['type'] ?? null;
|
||||||
|
$coordinates = $geometry['coordinates'] ?? null;
|
||||||
|
|
||||||
|
if (!$type || !$coordinates) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wkt = null;
|
||||||
|
$allPoints = [];
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'Polygon':
|
||||||
|
// Un seul polygone
|
||||||
|
$ring = $coordinates[0]; // Anneau extérieur
|
||||||
|
$points = [];
|
||||||
|
foreach ($ring as $point) {
|
||||||
|
$points[] = $point[0] . ' ' . $point[1];
|
||||||
|
$allPoints[] = $point;
|
||||||
|
}
|
||||||
|
$wkt = 'POLYGON((' . implode(',', $points) . '))';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MultiPolygon':
|
||||||
|
// Plusieurs polygones
|
||||||
|
$polygons = [];
|
||||||
|
foreach ($coordinates as $polygon) {
|
||||||
|
$ring = $polygon[0]; // Anneau extérieur du polygone
|
||||||
|
$points = [];
|
||||||
|
foreach ($ring as $point) {
|
||||||
|
$points[] = $point[0] . ' ' . $point[1];
|
||||||
|
$allPoints[] = $point;
|
||||||
|
}
|
||||||
|
$polygons[] = '((' . implode(',', $points) . '))';
|
||||||
|
}
|
||||||
|
$wkt = 'MULTIPOLYGON(' . implode(',', $polygons) . ')';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$wkt || empty($allPoints)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer la bounding box
|
||||||
|
$lats = array_map(function($p) { return $p[1]; }, $allPoints);
|
||||||
|
$lngs = array_map(function($p) { return $p[0]; }, $allPoints);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'wkt' => $wkt,
|
||||||
|
'bbox' => [
|
||||||
|
'min_lat' => min($lats),
|
||||||
|
'max_lat' => max($lats),
|
||||||
|
'min_lng' => min($lngs),
|
||||||
|
'max_lng' => max($lngs)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le script est exécuté directement
|
||||||
|
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
|
||||||
|
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../src/Core/Database.php';
|
||||||
|
|
||||||
|
// Chemin vers le fichier GeoJSON
|
||||||
|
$filePath = __DIR__ . '/../docs/contour-des-departements.geojson';
|
||||||
|
|
||||||
|
// Vérifier les arguments
|
||||||
|
if ($argc > 1) {
|
||||||
|
$filePath = $argv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Import des contours départementaux depuis un fichier\n";
|
||||||
|
echo "==================================================\n\n";
|
||||||
|
echo "Fichier : $filePath\n\n";
|
||||||
|
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
Database::init($appConfig->getDatabaseConfig());
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
$importer = new DepartementContoursFileImporter($db);
|
||||||
|
$log = $importer->importFromFile($filePath);
|
||||||
|
|
||||||
|
foreach ($log as $line) {
|
||||||
|
echo $line . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
175
api/scripts/import_department_boundaries.php
Normal file
175
api/scripts/import_department_boundaries.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script d'import des contours des départements français
|
||||||
|
*
|
||||||
|
* Les données peuvent provenir de :
|
||||||
|
* - IGN Admin Express : https://geoservices.ign.fr/adminexpress
|
||||||
|
* - data.gouv.fr : https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/
|
||||||
|
* - OpenStreetMap via Overpass API
|
||||||
|
*
|
||||||
|
* Format attendu : GeoJSON ou Shapefile converti en SQL
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../src/Core/Database.php';
|
||||||
|
|
||||||
|
echo "Import des contours des départements\n";
|
||||||
|
echo "===================================\n\n";
|
||||||
|
|
||||||
|
// Initialiser la base de données
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
Database::init($appConfig->getDatabaseConfig());
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// Exemple de données pour quelques départements bretons
|
||||||
|
// En production, ces données viendraient d'un fichier GeoJSON ou d'une API
|
||||||
|
$departements = [
|
||||||
|
[
|
||||||
|
'code' => '22',
|
||||||
|
'nom' => 'Côtes-d\'Armor',
|
||||||
|
// Contour simplifié - en réalité il faudrait des centaines de points
|
||||||
|
'points' => [
|
||||||
|
[-3.6546, 48.9012], [-3.3856, 48.8756], [-3.1234, 48.8234],
|
||||||
|
[-2.7856, 48.7845], [-2.4567, 48.7234], [-2.1234, 48.6456],
|
||||||
|
[-2.0123, 48.5234], [-2.0456, 48.3456], [-2.1567, 48.1234],
|
||||||
|
[-2.3456, 48.0567], [-2.6789, 48.0789], [-3.0123, 48.1234],
|
||||||
|
[-3.3456, 48.2345], [-3.5678, 48.4567], [-3.6234, 48.6789],
|
||||||
|
[-3.6546, 48.9012] // Fermer le polygone
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => '29',
|
||||||
|
'nom' => 'Finistère',
|
||||||
|
'points' => [
|
||||||
|
[-5.1423, 48.7523], [-4.8234, 48.6845], [-4.5123, 48.6234],
|
||||||
|
[-4.2345, 48.5678], [-3.9876, 48.4567], [-3.7234, 48.3456],
|
||||||
|
[-3.4567, 48.2345], [-3.3876, 48.0123], [-3.4234, 47.8234],
|
||||||
|
[-3.5678, 47.6456], [-3.8765, 47.6789], [-4.2345, 47.7234],
|
||||||
|
[-4.5678, 47.8234], [-4.8765, 47.9345], [-5.0876, 48.1234],
|
||||||
|
[-5.1234, 48.3456], [-5.1345, 48.5678], [-5.1423, 48.7523]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => '35',
|
||||||
|
'nom' => 'Ille-et-Vilaine',
|
||||||
|
'points' => [
|
||||||
|
[-2.0123, 48.6456], [-1.7234, 48.5678], [-1.4567, 48.4567],
|
||||||
|
[-1.2345, 48.3456], [-1.0234, 48.2345], [-1.0567, 48.0123],
|
||||||
|
[-1.1234, 47.8234], [-1.2567, 47.6456], [-1.4678, 47.6789],
|
||||||
|
[-1.7234, 47.7234], [-1.9876, 47.8234], [-2.1234, 47.9345],
|
||||||
|
[-2.2345, 48.1234], [-2.1567, 48.3456], [-2.0678, 48.5234],
|
||||||
|
[-2.0123, 48.6456]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => '56',
|
||||||
|
'nom' => 'Morbihan',
|
||||||
|
'points' => [
|
||||||
|
[-3.4567, 48.2345], [-3.2345, 48.1234], [-2.9876, 48.0123],
|
||||||
|
[-2.7234, 47.9234], [-2.4567, 47.8345], [-2.2345, 47.7456],
|
||||||
|
[-2.1234, 47.6234], [-2.2567, 47.4567], [-2.4678, 47.3456],
|
||||||
|
[-2.7234, 47.3789], [-3.0123, 47.4234], [-3.2876, 47.5234],
|
||||||
|
[-3.5234, 47.6345], [-3.6789, 47.7456], [-3.7234, 47.9234],
|
||||||
|
[-3.6567, 48.0789], [-3.4567, 48.2345]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Préparer la requête d'insertion
|
||||||
|
$sql = "INSERT INTO departements_contours
|
||||||
|
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||||
|
VALUES
|
||||||
|
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
nom_dept = VALUES(nom_dept),
|
||||||
|
contour = VALUES(contour),
|
||||||
|
bbox_min_lat = VALUES(bbox_min_lat),
|
||||||
|
bbox_max_lat = VALUES(bbox_max_lat),
|
||||||
|
bbox_min_lng = VALUES(bbox_min_lng),
|
||||||
|
bbox_max_lng = VALUES(bbox_max_lng),
|
||||||
|
updated_at = CURRENT_TIMESTAMP";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
|
||||||
|
foreach ($departements as $dept) {
|
||||||
|
echo "Import du département {$dept['code']} - {$dept['nom']}...\n";
|
||||||
|
|
||||||
|
// Créer le polygone WKT
|
||||||
|
$polygonPoints = [];
|
||||||
|
$lats = [];
|
||||||
|
$lngs = [];
|
||||||
|
|
||||||
|
foreach ($dept['points'] as $point) {
|
||||||
|
$lng = $point[0];
|
||||||
|
$lat = $point[1];
|
||||||
|
$polygonPoints[] = "$lng $lat";
|
||||||
|
$lats[] = $lat;
|
||||||
|
$lngs[] = $lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
$polygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||||
|
|
||||||
|
// Calculer la bounding box
|
||||||
|
$minLat = min($lats);
|
||||||
|
$maxLat = max($lats);
|
||||||
|
$minLng = min($lngs);
|
||||||
|
$maxLng = max($lngs);
|
||||||
|
|
||||||
|
// Exécuter l'insertion
|
||||||
|
$stmt->execute([
|
||||||
|
'code' => $dept['code'],
|
||||||
|
'nom' => $dept['nom'],
|
||||||
|
'polygon' => $polygon,
|
||||||
|
'min_lat' => $minLat,
|
||||||
|
'max_lat' => $maxLat,
|
||||||
|
'min_lng' => $minLng,
|
||||||
|
'max_lng' => $maxLng
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "✓ Département {$dept['code']} importé\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
echo "\n✓ Import terminé avec succès!\n\n";
|
||||||
|
|
||||||
|
// Vérifier les données importées
|
||||||
|
$checkSql = "SELECT code_dept, nom_dept,
|
||||||
|
ST_Area(contour) as area,
|
||||||
|
ST_NumPoints(ST_ExteriorRing(contour)) as num_points
|
||||||
|
FROM x_departements_contours
|
||||||
|
ORDER BY code_dept";
|
||||||
|
|
||||||
|
$result = $db->query($checkSql);
|
||||||
|
echo "Départements importés:\n";
|
||||||
|
echo "---------------------\n";
|
||||||
|
|
||||||
|
foreach ($result as $row) {
|
||||||
|
echo sprintf("- %s (%s) : %d points, aire: %.4f\n",
|
||||||
|
$row['nom_dept'],
|
||||||
|
$row['code_dept'],
|
||||||
|
$row['num_points'],
|
||||||
|
$row['area']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo "✗ Erreur lors de l'import : " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
echo "Note importante:\n";
|
||||||
|
echo "---------------\n";
|
||||||
|
echo "Ce script utilise des données simplifiées pour l'exemple.\n";
|
||||||
|
echo "Pour un usage en production, vous devez :\n";
|
||||||
|
echo "1. Télécharger les vrais contours depuis l'IGN ou data.gouv.fr\n";
|
||||||
|
echo "2. Les convertir en format GeoJSON ou SQL\n";
|
||||||
|
echo "3. Adapter ce script pour lire ces fichiers\n";
|
||||||
|
echo "\n";
|
||||||
|
echo "Sources recommandées:\n";
|
||||||
|
echo "- IGN Admin Express: https://geoservices.ign.fr/adminexpress\n";
|
||||||
|
echo "- data.gouv.fr: https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/\n";
|
||||||
318
api/scripts/init_departements_contours.php
Normal file
318
api/scripts/init_departements_contours.php
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script d'initialisation des contours des départements français
|
||||||
|
* À exécuter une seule fois lors de la connexion de l'admin d6soft
|
||||||
|
*
|
||||||
|
* Utilise l'API geo.api.gouv.fr pour récupérer les contours GeoJSON
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DepartementContoursInitializer {
|
||||||
|
private PDO $db;
|
||||||
|
private array $log = [];
|
||||||
|
|
||||||
|
// Liste complète des départements français (métropole + DOM-TOM)
|
||||||
|
private array $departements = [
|
||||||
|
// Métropole
|
||||||
|
'01' => 'Ain', '02' => 'Aisne', '03' => 'Allier', '04' => 'Alpes-de-Haute-Provence',
|
||||||
|
'05' => 'Hautes-Alpes', '06' => 'Alpes-Maritimes', '07' => 'Ardèche', '08' => 'Ardennes',
|
||||||
|
'09' => 'Ariège', '10' => 'Aube', '11' => 'Aude', '12' => 'Aveyron',
|
||||||
|
'13' => 'Bouches-du-Rhône', '14' => 'Calvados', '15' => 'Cantal', '16' => 'Charente',
|
||||||
|
'17' => 'Charente-Maritime', '18' => 'Cher', '19' => 'Corrèze', '2A' => 'Corse-du-Sud',
|
||||||
|
'2B' => 'Haute-Corse', '21' => 'Côte-d\'Or', '22' => 'Côtes-d\'Armor', '23' => 'Creuse',
|
||||||
|
'24' => 'Dordogne', '25' => 'Doubs', '26' => 'Drôme', '27' => 'Eure',
|
||||||
|
'28' => 'Eure-et-Loir', '29' => 'Finistère', '30' => 'Gard', '31' => 'Haute-Garonne',
|
||||||
|
'32' => 'Gers', '33' => 'Gironde', '34' => 'Hérault', '35' => 'Ille-et-Vilaine',
|
||||||
|
'36' => 'Indre', '37' => 'Indre-et-Loire', '38' => 'Isère', '39' => 'Jura',
|
||||||
|
'40' => 'Landes', '41' => 'Loir-et-Cher', '42' => 'Loire', '43' => 'Haute-Loire',
|
||||||
|
'44' => 'Loire-Atlantique', '45' => 'Loiret', '46' => 'Lot', '47' => 'Lot-et-Garonne',
|
||||||
|
'48' => 'Lozère', '49' => 'Maine-et-Loire', '50' => 'Manche', '51' => 'Marne',
|
||||||
|
'52' => 'Haute-Marne', '53' => 'Mayenne', '54' => 'Meurthe-et-Moselle', '55' => 'Meuse',
|
||||||
|
'56' => 'Morbihan', '57' => 'Moselle', '58' => 'Nièvre', '59' => 'Nord',
|
||||||
|
'60' => 'Oise', '61' => 'Orne', '62' => 'Pas-de-Calais', '63' => 'Puy-de-Dôme',
|
||||||
|
'64' => 'Pyrénées-Atlantiques', '65' => 'Hautes-Pyrénées', '66' => 'Pyrénées-Orientales', '67' => 'Bas-Rhin',
|
||||||
|
'68' => 'Haut-Rhin', '69' => 'Rhône', '70' => 'Haute-Saône', '71' => 'Saône-et-Loire',
|
||||||
|
'72' => 'Sarthe', '73' => 'Savoie', '74' => 'Haute-Savoie', '75' => 'Paris',
|
||||||
|
'76' => 'Seine-Maritime', '77' => 'Seine-et-Marne', '78' => 'Yvelines', '79' => 'Deux-Sèvres',
|
||||||
|
'80' => 'Somme', '81' => 'Tarn', '82' => 'Tarn-et-Garonne', '83' => 'Var',
|
||||||
|
'84' => 'Vaucluse', '85' => 'Vendée', '86' => 'Vienne', '87' => 'Haute-Vienne',
|
||||||
|
'88' => 'Vosges', '89' => 'Yonne', '90' => 'Territoire de Belfort', '91' => 'Essonne',
|
||||||
|
'92' => 'Hauts-de-Seine', '93' => 'Seine-Saint-Denis', '94' => 'Val-de-Marne', '95' => 'Val-d\'Oise',
|
||||||
|
// DOM-TOM
|
||||||
|
'971' => 'Guadeloupe', '972' => 'Martinique', '973' => 'Guyane', '974' => 'La Réunion',
|
||||||
|
'975' => 'Saint-Pierre-et-Miquelon', '976' => 'Mayotte', '977' => 'Saint-Barthélemy',
|
||||||
|
'978' => 'Saint-Martin', '984' => 'Terres australes et antarctiques françaises',
|
||||||
|
'986' => 'Wallis-et-Futuna', '987' => 'Polynésie française', '988' => 'Nouvelle-Calédonie'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(PDO $db) {
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la table existe
|
||||||
|
*/
|
||||||
|
public function tableExists(): bool {
|
||||||
|
try {
|
||||||
|
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la table est vide
|
||||||
|
*/
|
||||||
|
private function isTableEmpty(): bool {
|
||||||
|
try {
|
||||||
|
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
return $result['count'] == 0;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le contour d'un département depuis l'API geo.api.gouv.fr
|
||||||
|
*/
|
||||||
|
private function fetchDepartementContour(string $code, string $nom): ?array {
|
||||||
|
// URL de l'API pour récupérer le contour du département en GeoJSON
|
||||||
|
$url = "https://geo.api.gouv.fr/departements/{$code}?geometry=contour";
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'timeout' => 30,
|
||||||
|
'header' => "User-Agent: Geosector/1.0\r\n"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = @file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
$this->log[] = "✗ Erreur API pour département $code ($nom)";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
// L'API peut retourner le contour dans 'contour' ou 'geometry'
|
||||||
|
if (isset($data['contour']) && isset($data['contour']['coordinates'])) {
|
||||||
|
return $data['contour'];
|
||||||
|
} elseif (isset($data['geometry']) && isset($data['geometry']['coordinates'])) {
|
||||||
|
return $data['geometry'];
|
||||||
|
} else {
|
||||||
|
$this->log[] = "✗ Pas de contour pour département $code ($nom)";
|
||||||
|
// Debug : afficher les clés disponibles
|
||||||
|
if (is_array($data)) {
|
||||||
|
$this->log[] = " Clés disponibles : " . implode(', ', array_keys($data));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit les coordonnées GeoJSON en WKT Polygon pour MySQL
|
||||||
|
*/
|
||||||
|
private function geoJsonToWkt(array $coordinates): ?array {
|
||||||
|
if (empty($coordinates) || !is_array($coordinates[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoJSON peut avoir plusieurs niveaux d'imbrication selon le type
|
||||||
|
// Pour un Polygon simple
|
||||||
|
if (isset($coordinates[0][0]) && is_numeric($coordinates[0][0])) {
|
||||||
|
$ring = $coordinates;
|
||||||
|
}
|
||||||
|
// Pour un MultiPolygon, prendre le premier polygone
|
||||||
|
elseif (isset($coordinates[0][0][0])) {
|
||||||
|
$ring = $coordinates[0][0];
|
||||||
|
}
|
||||||
|
// Pour un Polygon standard
|
||||||
|
else {
|
||||||
|
$ring = $coordinates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$points = [];
|
||||||
|
$lats = [];
|
||||||
|
$lngs = [];
|
||||||
|
|
||||||
|
foreach ($ring as $point) {
|
||||||
|
if (count($point) >= 2) {
|
||||||
|
$lng = $point[0];
|
||||||
|
$lat = $point[1];
|
||||||
|
$points[] = "$lng $lat";
|
||||||
|
$lats[] = $lat;
|
||||||
|
$lngs[] = $lng;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($points) < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fermer le polygone si nécessaire
|
||||||
|
if ($points[0] !== $points[count($points) - 1]) {
|
||||||
|
$points[] = $points[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'wkt' => 'POLYGON((' . implode(',', $points) . '))',
|
||||||
|
'bbox' => [
|
||||||
|
'min_lat' => min($lats),
|
||||||
|
'max_lat' => max($lats),
|
||||||
|
'min_lng' => min($lngs),
|
||||||
|
'max_lng' => max($lngs)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importe tous les départements
|
||||||
|
*/
|
||||||
|
public function importAll(): array {
|
||||||
|
$this->log[] = "Début de l'import des contours départementaux";
|
||||||
|
$this->log[] = "Source : API geo.api.gouv.fr";
|
||||||
|
$this->log[] = "";
|
||||||
|
|
||||||
|
// Vérifier que la table est vide avant d'importer
|
||||||
|
if (!$this->isTableEmpty()) {
|
||||||
|
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer la requête d'insertion
|
||||||
|
$sql = "INSERT INTO x_departements_contours
|
||||||
|
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||||
|
VALUES
|
||||||
|
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
|
||||||
|
$success = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
// Démarrer une transaction
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($this->departements as $code => $nom) {
|
||||||
|
// Petite pause pour ne pas surcharger l'API
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
|
||||||
|
$contour = $this->fetchDepartementContour($code, $nom);
|
||||||
|
|
||||||
|
if (!$contour) {
|
||||||
|
$errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wktData = $this->geoJsonToWkt($contour['coordinates']);
|
||||||
|
|
||||||
|
if (!$wktData) {
|
||||||
|
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
|
||||||
|
$errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt->execute([
|
||||||
|
'code' => $code,
|
||||||
|
'nom' => $nom,
|
||||||
|
'polygon' => $wktData['wkt'],
|
||||||
|
'min_lat' => $wktData['bbox']['min_lat'],
|
||||||
|
'max_lat' => $wktData['bbox']['max_lat'],
|
||||||
|
'min_lng' => $wktData['bbox']['min_lng'],
|
||||||
|
'max_lng' => $wktData['bbox']['max_lng']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->log[] = "✓ $code - $nom importé";
|
||||||
|
$success++;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si tout s'est bien passé, valider la transaction
|
||||||
|
if ($success > 0) {
|
||||||
|
$this->db->commit();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✓ Transaction validée";
|
||||||
|
} else {
|
||||||
|
$this->db->rollBack();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->db->rollBack();
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
|
||||||
|
$this->log[] = "✗ Transaction annulée";
|
||||||
|
$errors = count($this->departements);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log[] = "";
|
||||||
|
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
|
||||||
|
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute l'initialisation si nécessaire
|
||||||
|
*/
|
||||||
|
public static function runIfNeeded(PDO $db, string $username): ?array {
|
||||||
|
// Vérifier que c'est bien l'admin d6soft
|
||||||
|
if ($username !== 'd6soft') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$initializer = new self($db);
|
||||||
|
|
||||||
|
// Vérifier si la table existe
|
||||||
|
if (!$initializer->tableExists()) {
|
||||||
|
return ["✗ La table x_departements_contours n'existe pas. Veuillez la créer avec le script SQL fourni."];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si elle est vide
|
||||||
|
if (!$initializer->isTableEmpty()) {
|
||||||
|
return null; // Table déjà remplie, rien à faire
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le fichier local existe
|
||||||
|
$localFile = __DIR__ . '/../docs/contour-des-departements.geojson';
|
||||||
|
if (file_exists($localFile)) {
|
||||||
|
// Utiliser le fichier local
|
||||||
|
require_once __DIR__ . '/import_departements_from_file.php';
|
||||||
|
$fileImporter = new \DepartementContoursFileImporter($db);
|
||||||
|
return $fileImporter->importFromFile($localFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, utiliser l'API (qui ne fonctionne pas bien actuellement)
|
||||||
|
return $initializer->importAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le script est exécuté directement (pour tests)
|
||||||
|
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
|
||||||
|
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/../src/Core/Database.php';
|
||||||
|
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
Database::init($appConfig->getDatabaseConfig());
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "Test d'import des contours départementaux\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
$initializer = new DepartementContoursInitializer($db);
|
||||||
|
$log = $initializer->importAll();
|
||||||
|
|
||||||
|
foreach ($log as $line) {
|
||||||
|
echo $line . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
0
api/scripts/php/MigrationConfig.php
Normal file → Executable file
0
api/scripts/php/MigrationConfig.php
Normal file → Executable file
0
api/scripts/php/migrate.php
Normal file → Executable file
0
api/scripts/php/migrate.php
Normal file → Executable file
0
api/scripts/php/migrate_entites.php
Normal file → Executable file
0
api/scripts/php/migrate_entites.php
Normal file → Executable file
0
api/scripts/php/migrate_medias.php
Normal file → Executable file
0
api/scripts/php/migrate_medias.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass_histo.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass_histo.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_operations.php
Normal file → Executable file
0
api/scripts/php/migrate_operations.php
Normal file → Executable file
0
api/scripts/php/migrate_sectors_adresses.php
Normal file → Executable file
0
api/scripts/php/migrate_sectors_adresses.php
Normal file → Executable file
0
api/scripts/php/migrate_users.php
Normal file → Executable file
0
api/scripts/php/migrate_users.php
Normal file → Executable file
0
api/scripts/php/migrate_x_departements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_departements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_devises.php
Normal file → Executable file
0
api/scripts/php/migrate_x_devises.php
Normal file → Executable file
0
api/scripts/php/migrate_x_entites_types.php
Normal file → Executable file
0
api/scripts/php/migrate_x_entites_types.php
Normal file → Executable file
0
api/scripts/php/migrate_x_pays.php
Normal file → Executable file
0
api/scripts/php/migrate_x_pays.php
Normal file → Executable file
0
api/scripts/php/migrate_x_regions.php
Normal file → Executable file
0
api/scripts/php/migrate_x_regions.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_passages.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_passages.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_reglements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_reglements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_users_roles.php
Normal file → Executable file
0
api/scripts/php/migrate_x_users_roles.php
Normal file → Executable file
0
api/scripts/php/migrate_x_villes.php
Normal file → Executable file
0
api/scripts/php/migrate_x_villes.php
Normal file → Executable file
106
api/scripts/setup_addresses_access.sh
Normal file
106
api/scripts/setup_addresses_access.sh
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script pour configurer l'accès à la base de données des adresses
|
||||||
|
# sur chaque environnement Incus
|
||||||
|
|
||||||
|
echo "Configuration de l'accès à la base de données des adresses"
|
||||||
|
echo "=========================================================="
|
||||||
|
|
||||||
|
# Fonction pour créer l'utilisateur sur un container
|
||||||
|
create_user_on_container() {
|
||||||
|
local container=$1
|
||||||
|
local api_ip=$2
|
||||||
|
local maria_ip=$3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuration pour $container..."
|
||||||
|
echo "Container MariaDB IP: $maria_ip"
|
||||||
|
echo "Container API IP: $api_ip"
|
||||||
|
|
||||||
|
# Se connecter au container MariaDB et créer l'utilisateur
|
||||||
|
incus exec $container -- mysql -u root -p -e "
|
||||||
|
-- Créer l'utilisateur pour l'accès depuis l'API
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'$api_ip' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'$api_ip';
|
||||||
|
|
||||||
|
-- Créer aussi un utilisateur localhost pour les tests directs
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||||
|
|
||||||
|
-- Optionnel : créer un utilisateur pour tout le sous-réseau
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.%';
|
||||||
|
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier
|
||||||
|
SELECT user, host FROM mysql.user WHERE user = 'adresses_user';
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "✓ Utilisateur créé sur $container"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour tester la connexion
|
||||||
|
test_connection() {
|
||||||
|
local api_container=$1
|
||||||
|
local maria_ip=$2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test de connexion depuis $api_container vers $maria_ip..."
|
||||||
|
|
||||||
|
incus exec $api_container -- mysql -h $maria_ip -u adresses_user -p'd66,AdrGeo.User' -e "
|
||||||
|
SELECT DATABASE();
|
||||||
|
SHOW TABLES FROM adresses LIMIT 5;
|
||||||
|
"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✓ Connexion réussie!"
|
||||||
|
else
|
||||||
|
echo "✗ Échec de la connexion"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configuration pour chaque environnement
|
||||||
|
echo ""
|
||||||
|
echo "1. DÉVELOPPEMENT (DVA)"
|
||||||
|
read -p "IP du container dva-api [par défaut: 13.23.33.45]: " DVA_API_IP
|
||||||
|
DVA_API_IP=${DVA_API_IP:-13.23.33.45}
|
||||||
|
create_user_on_container "dva-maria" "$DVA_API_IP" "13.23.33.46"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. RECETTE (RCA)"
|
||||||
|
read -p "IP du container rca-api [par défaut: 13.23.33.35]: " RCA_API_IP
|
||||||
|
RCA_API_IP=${RCA_API_IP:-13.23.33.35}
|
||||||
|
create_user_on_container "rca-maria" "$RCA_API_IP" "13.23.33.36"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. PRODUCTION (PRA)"
|
||||||
|
read -p "IP du container pra-api [par défaut: 13.23.33.25]: " PRA_API_IP
|
||||||
|
PRA_API_IP=${PRA_API_IP:-13.23.33.25}
|
||||||
|
create_user_on_container "pra-maria" "$PRA_API_IP" "13.23.33.26"
|
||||||
|
|
||||||
|
# Tests de connexion
|
||||||
|
echo ""
|
||||||
|
echo "=========================================================="
|
||||||
|
echo "Tests de connexion"
|
||||||
|
echo "=========================================================="
|
||||||
|
|
||||||
|
read -p "Voulez-vous tester les connexions? (o/n): " test_choice
|
||||||
|
if [ "$test_choice" = "o" ]; then
|
||||||
|
test_connection "dva-api" "13.23.33.46"
|
||||||
|
test_connection "rca-api" "13.23.33.36"
|
||||||
|
test_connection "pra-api" "13.23.33.26"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuration terminée!"
|
||||||
|
echo ""
|
||||||
|
echo "Notes importantes:"
|
||||||
|
echo "- Les utilisateurs ont été créés avec accès SELECT uniquement sur la base 'adresses'"
|
||||||
|
echo "- Trois types d'accès ont été configurés:"
|
||||||
|
echo " 1. Depuis l'IP spécifique de chaque container API"
|
||||||
|
echo " 2. Depuis localhost (pour les tests directs)"
|
||||||
|
echo " 3. Depuis tout le sous-réseau 13.23.33.% (optionnel, moins sécurisé)"
|
||||||
|
echo ""
|
||||||
|
echo "Pour tester manuellement depuis un container API:"
|
||||||
|
echo "incus exec [container-api] -- mysql -h [ip-maria] -u adresses_user -p'd66,AdrGeo.User' adresses"
|
||||||
136
api/scripts/setup_addresses_access_by_env.sh
Normal file
136
api/scripts/setup_addresses_access_by_env.sh
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script pour configurer l'accès à la base de données des adresses
|
||||||
|
# avec segmentation par environnement basée sur les plages d'IPs
|
||||||
|
|
||||||
|
echo "Configuration de l'accès à la base de données des adresses"
|
||||||
|
echo "=========================================================="
|
||||||
|
echo ""
|
||||||
|
echo "Architecture des IPs par environnement :"
|
||||||
|
echo "- DÉVELOPPEMENT : 13.23.33.40-49 (13.23.33.4%)"
|
||||||
|
echo "- RECETTE : 13.23.33.30-39 (13.23.33.3%)"
|
||||||
|
echo "- PRODUCTION : 13.23.33.20-29 (13.23.33.2%)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Fonction pour créer l'utilisateur sur un container
|
||||||
|
create_user_on_container() {
|
||||||
|
local container=$1
|
||||||
|
local ip_pattern=$2
|
||||||
|
local env_name=$3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuration pour $env_name ($container)..."
|
||||||
|
echo "Pattern IP autorisé : $ip_pattern"
|
||||||
|
|
||||||
|
# Se connecter au container MariaDB et créer l'utilisateur
|
||||||
|
incus exec $container -- mysql -u root -p -e "
|
||||||
|
-- Créer l'utilisateur pour l'accès depuis la plage IP de l'environnement
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'$ip_pattern' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'$ip_pattern';
|
||||||
|
|
||||||
|
-- Créer aussi un utilisateur localhost pour les tests directs
|
||||||
|
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||||
|
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||||
|
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- Vérifier
|
||||||
|
SELECT user, host FROM mysql.user WHERE user = 'adresses_user' ORDER BY host;
|
||||||
|
"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✓ Utilisateur créé sur $container"
|
||||||
|
else
|
||||||
|
echo "✗ Erreur lors de la création sur $container"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour tester la connexion
|
||||||
|
test_connection() {
|
||||||
|
local api_container=$1
|
||||||
|
local maria_ip=$2
|
||||||
|
local env_name=$3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test de connexion $env_name : $api_container -> $maria_ip..."
|
||||||
|
|
||||||
|
incus exec $api_container -- mysql -h $maria_ip -u adresses_user -p'd66,AdrGeo.User' -e "
|
||||||
|
SELECT CONCAT('Connexion réussie depuis ', @@hostname, ' vers ', '$maria_ip') as Status;
|
||||||
|
SELECT DATABASE();
|
||||||
|
SHOW TABLES FROM adresses LIMIT 3;
|
||||||
|
" 2>/dev/null
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✓ Connexion réussie!"
|
||||||
|
else
|
||||||
|
echo "✗ Échec de la connexion"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Menu de sélection
|
||||||
|
echo ""
|
||||||
|
echo "Que voulez-vous configurer ?"
|
||||||
|
echo "1. Environnement DÉVELOPPEMENT uniquement (dva-maria)"
|
||||||
|
echo "2. Environnement RECETTE uniquement (rca-maria)"
|
||||||
|
echo "3. Environnement PRODUCTION uniquement (pra-maria)"
|
||||||
|
echo "4. Tous les environnements"
|
||||||
|
echo ""
|
||||||
|
read -p "Votre choix (1-4): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
create_user_on_container "dva-maria" "13.23.33.4%" "DÉVELOPPEMENT"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
create_user_on_container "rca-maria" "13.23.33.3%" "RECETTE"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
create_user_on_container "pra-maria" "13.23.33.2%" "PRODUCTION"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
create_user_on_container "dva-maria" "13.23.33.4%" "DÉVELOPPEMENT"
|
||||||
|
create_user_on_container "rca-maria" "13.23.33.3%" "RECETTE"
|
||||||
|
create_user_on_container "pra-maria" "13.23.33.2%" "PRODUCTION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Choix invalide"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Tests de connexion
|
||||||
|
echo ""
|
||||||
|
echo "=========================================================="
|
||||||
|
echo "Tests de connexion"
|
||||||
|
echo "=========================================================="
|
||||||
|
|
||||||
|
read -p "Voulez-vous tester les connexions? (o/n): " test_choice
|
||||||
|
if [ "$test_choice" = "o" ]; then
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
test_connection "dva-api" "13.23.33.46" "DÉVELOPPEMENT"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
test_connection "rca-api" "13.23.33.36" "RECETTE"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
test_connection "pra-api" "13.23.33.26" "PRODUCTION"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
test_connection "dva-api" "13.23.33.46" "DÉVELOPPEMENT"
|
||||||
|
test_connection "rca-api" "13.23.33.36" "RECETTE"
|
||||||
|
test_connection "pra-api" "13.23.33.26" "PRODUCTION"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuration terminée!"
|
||||||
|
echo ""
|
||||||
|
echo "Récapitulatif de la sécurité mise en place :"
|
||||||
|
echo "- Chaque environnement a sa propre plage d'IPs autorisée"
|
||||||
|
echo "- DÉVELOPPEMENT : seuls les containers en 13.23.33.4x peuvent accéder à dva-maria"
|
||||||
|
echo "- RECETTE : seuls les containers en 13.23.33.3x peuvent accéder à rca-maria"
|
||||||
|
echo "- PRODUCTION : seuls les containers en 13.23.33.2x peuvent accéder à pra-maria"
|
||||||
|
echo ""
|
||||||
|
echo "Cela empêche un container de DEV d'accéder aux données de PROD par exemple."
|
||||||
51
api/src/Config/AppConfig.php
Normal file → Executable file
51
api/src/Config/AppConfig.php
Normal file → Executable file
@@ -71,6 +71,12 @@ class AppConfig {
|
|||||||
'api_key' => '', // À remplir avec la clé API SMS OVH
|
'api_key' => '', // À remplir avec la clé API SMS OVH
|
||||||
'api_secret' => '', // À remplir avec le secret API SMS OVH
|
'api_secret' => '', // À remplir avec le secret API SMS OVH
|
||||||
],
|
],
|
||||||
|
'backup' => [
|
||||||
|
'encryption_key' => 'K8mN2pQ5rT9wX3zA6bE1fH4jL7oS0vY2', // Clé de 32 caractères pour AES-256
|
||||||
|
'compression' => true,
|
||||||
|
'compression_level' => 6,
|
||||||
|
'cipher' => 'AES-256-CBC'
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Configuration PRODUCTION
|
// Configuration PRODUCTION
|
||||||
@@ -82,6 +88,12 @@ class AppConfig {
|
|||||||
'username' => 'geo_app_user_prod',
|
'username' => 'geo_app_user_prod',
|
||||||
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
|
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
|
||||||
],
|
],
|
||||||
|
'addresses_database' => [
|
||||||
|
'host' => '13.23.33.26',
|
||||||
|
'name' => 'adresses',
|
||||||
|
'username' => 'adr_geo_user',
|
||||||
|
'password' => 'd66,AdrGeo.User',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Configuration RECETTE
|
// Configuration RECETTE
|
||||||
@@ -93,6 +105,12 @@ class AppConfig {
|
|||||||
'username' => 'geo_app_user_rec',
|
'username' => 'geo_app_user_rec',
|
||||||
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
|
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
|
||||||
],
|
],
|
||||||
|
'addresses_database' => [
|
||||||
|
'host' => '13.23.33.36',
|
||||||
|
'name' => 'adresses',
|
||||||
|
'username' => 'adr_geo_user',
|
||||||
|
'password' => 'd66,AdrGeoRec.User',
|
||||||
|
],
|
||||||
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
|
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -105,6 +123,12 @@ class AppConfig {
|
|||||||
'username' => 'geo_app_user_dev',
|
'username' => 'geo_app_user_dev',
|
||||||
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
||||||
],
|
],
|
||||||
|
'addresses_database' => [
|
||||||
|
'host' => '13.23.33.46',
|
||||||
|
'name' => 'adresses',
|
||||||
|
'username' => 'adr_geo_user',
|
||||||
|
'password' => 'd66,AdrGeoDev.User',
|
||||||
|
],
|
||||||
// Vous pouvez activer des fonctionnalités de débogage en développement
|
// Vous pouvez activer des fonctionnalités de débogage en développement
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
// Configurez des endpoints de test pour Stripe, etc.
|
// Configurez des endpoints de test pour Stripe, etc.
|
||||||
@@ -228,6 +252,15 @@ class AppConfig {
|
|||||||
return $this->getCurrentConfig()['database'];
|
return $this->getCurrentConfig()['database'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la configuration de la base de données des adresses
|
||||||
|
*
|
||||||
|
* @return array Configuration de la base de données des adresses
|
||||||
|
*/
|
||||||
|
public function getAddressesDatabaseConfig(): array {
|
||||||
|
return $this->getCurrentConfig()['addresses_database'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne la clé de chiffrement
|
* Retourne la clé de chiffrement
|
||||||
*
|
*
|
||||||
@@ -336,6 +369,24 @@ class AppConfig {
|
|||||||
return $this->clientIp;
|
return $this->clientIp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la configuration des backups
|
||||||
|
*
|
||||||
|
* @return array Configuration des backups
|
||||||
|
*/
|
||||||
|
public function getBackupConfig(): array {
|
||||||
|
return $this->getCurrentConfig()['backup'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la clé de chiffrement des backups
|
||||||
|
*
|
||||||
|
* @return string Clé de chiffrement des backups
|
||||||
|
*/
|
||||||
|
public function getBackupEncryptionKey(): string {
|
||||||
|
return $this->getCurrentConfig()['backup']['encryption_key'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Détermine l'adresse IP du client en tenant compte des proxys et load balancers
|
* Détermine l'adresse IP du client en tenant compte des proxys et load balancers
|
||||||
*
|
*
|
||||||
|
|||||||
10
api/src/Controllers/EntiteController.php
Normal file → Executable file
10
api/src/Controllers/EntiteController.php
Normal file → Executable file
@@ -386,9 +386,10 @@ class EntiteController {
|
|||||||
* Met à jour une entité existante avec les données fournies
|
* Met à jour une entité existante avec les données fournies
|
||||||
* Seuls les administrateurs (rôle > 2) peuvent modifier certains champs
|
* Seuls les administrateurs (rôle > 2) peuvent modifier certains champs
|
||||||
*
|
*
|
||||||
|
* @param string $id ID de l'entité depuis l'URL
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function updateEntite(): void {
|
public function updateEntite(string $id = null): void {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification et les droits d'accès
|
// Vérifier l'authentification et les droits d'accès
|
||||||
$userId = Session::getUserId();
|
$userId = Session::getUserId();
|
||||||
@@ -419,7 +420,10 @@ class EntiteController {
|
|||||||
// Récupérer les données de la requête
|
// Récupérer les données de la requête
|
||||||
$data = Request::getJson();
|
$data = Request::getJson();
|
||||||
|
|
||||||
if (!isset($data['id']) || empty($data['id'])) {
|
// Priorité à l'ID de l'URL, sinon utiliser celui du JSON
|
||||||
|
$entiteId = $id ? (int)$id : (isset($data['id']) ? (int)$data['id'] : null);
|
||||||
|
|
||||||
|
if (!$entiteId) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'ID de l\'entité requis'
|
'message' => 'ID de l\'entité requis'
|
||||||
@@ -427,8 +431,6 @@ class EntiteController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$entiteId = (int)$data['id'];
|
|
||||||
|
|
||||||
// Récupérer les données actuelles de l'entité pour vérifier si l'adresse a changé
|
// Récupérer les données actuelles de l'entité pour vérifier si l'adresse a changé
|
||||||
$stmt = $this->db->prepare('SELECT adresse1, adresse2, code_postal, ville FROM entites WHERE id = ?');
|
$stmt = $this->db->prepare('SELECT adresse1, adresse2, code_postal, ville FROM entites WHERE id = ?');
|
||||||
$stmt->execute([$entiteId]);
|
$stmt->execute([$entiteId]);
|
||||||
|
|||||||
1066
api/src/Controllers/FileController.php
Executable file
1066
api/src/Controllers/FileController.php
Executable file
File diff suppressed because it is too large
Load Diff
0
api/src/Controllers/LogController.php
Normal file → Executable file
0
api/src/Controllers/LogController.php
Normal file → Executable file
58
api/src/Controllers/LoginController.php
Normal file → Executable file
58
api/src/Controllers/LoginController.php
Normal file → Executable file
@@ -49,8 +49,8 @@ class LoginController {
|
|||||||
|
|
||||||
// Récupérer le type d'utilisateur
|
// Récupérer le type d'utilisateur
|
||||||
// admin accessible uniquement aux fk_role>1
|
// admin accessible uniquement aux fk_role>1
|
||||||
// sinon tout user peut se connecter à l'interface utilisateur
|
// user accessible uniquement aux fk_role=1
|
||||||
$roleCondition = ($interface === 'user') ? '' : 'AND fk_role>1';
|
$roleCondition = ($interface === 'user') ? 'AND fk_role=1' : 'AND fk_role>1';
|
||||||
|
|
||||||
// Log pour le debug
|
// Log pour le debug
|
||||||
LogService::log('Tentative de connexion GeoSector', [
|
LogService::log('Tentative de connexion GeoSector', [
|
||||||
@@ -155,11 +155,37 @@ class LoginController {
|
|||||||
'email' => $email,
|
'email' => $email,
|
||||||
'name' => $decryptedName,
|
'name' => $decryptedName,
|
||||||
'first_name' => $user['first_name'] ?? '',
|
'first_name' => $user['first_name'] ?? '',
|
||||||
'fk_role' => $user['fk_role'] ?? '0'
|
'fk_role' => $user['fk_role'] ?? '0',
|
||||||
// 'interface' supprimée pour se baser uniquement sur le rôle
|
'fk_entite' => $user['fk_entite'] ?? '0',
|
||||||
];
|
];
|
||||||
Session::login($sessionData);
|
Session::login($sessionData);
|
||||||
|
|
||||||
|
// Vérifier et exécuter l'initialisation des contours départementaux pour d6soft
|
||||||
|
if ($username === 'd6soft') {
|
||||||
|
require_once __DIR__ . '/../../scripts/init_departements_contours.php';
|
||||||
|
$initLog = \DepartementContoursInitializer::runIfNeeded($this->db, $username);
|
||||||
|
|
||||||
|
if ($initLog !== null) {
|
||||||
|
// Logger l'initialisation
|
||||||
|
LogService::log('Initialisation des contours départementaux', [
|
||||||
|
'level' => 'info',
|
||||||
|
'username' => $username,
|
||||||
|
'log_count' => count($initLog)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Logger aussi les dernières lignes du log pour diagnostic
|
||||||
|
$lastLines = array_slice($initLog, -5);
|
||||||
|
foreach ($lastLines as $line) {
|
||||||
|
if (strpos($line, '✗') !== false || strpos($line, 'terminé') !== false) {
|
||||||
|
LogService::log('Import contours: ' . $line, [
|
||||||
|
'level' => 'info',
|
||||||
|
'username' => $username
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Préparation des données utilisateur pour la réponse (uniquement les champs du user)
|
// Préparation des données utilisateur pour la réponse (uniquement les champs du user)
|
||||||
$userData = [
|
$userData = [
|
||||||
'id' => $user['id'],
|
'id' => $user['id'],
|
||||||
@@ -228,13 +254,16 @@ class LoginController {
|
|||||||
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
|
||||||
// Interface admin avec rôle 2 : les 3 dernières opérations dont l'active
|
// Interface admin avec rôle 2 : les 3 dernières opérations dont l'active
|
||||||
$operationLimit = 3;
|
$operationLimit = 3;
|
||||||
|
} elseif ($interface === 'admin' && $user['fk_role'] > 2) {
|
||||||
|
// Interface admin avec rôle > 2 : les 10 dernières opérations dont l'active
|
||||||
|
$operationLimit = 10;
|
||||||
} else {
|
} else {
|
||||||
// Autres cas : pas d'opérations
|
// Autres cas : pas d'opérations
|
||||||
$operationLimit = 0;
|
$operationLimit = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($operationLimit > 0 && !empty($user['fk_entite'])) {
|
if ($operationLimit > 0 && !empty($user['fk_entite'])) {
|
||||||
$operationQuery = "SELECT id, libelle, date_deb, date_fin
|
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
|
||||||
FROM operations
|
FROM operations
|
||||||
WHERE fk_entite = ?";
|
WHERE fk_entite = ?";
|
||||||
|
|
||||||
@@ -253,9 +282,11 @@ class LoginController {
|
|||||||
foreach ($operations as $operation) {
|
foreach ($operations as $operation) {
|
||||||
$operationsData[] = [
|
$operationsData[] = [
|
||||||
'id' => $operation['id'],
|
'id' => $operation['id'],
|
||||||
'name' => $operation['libelle'],
|
'fk_entite' => $operation['fk_entite'],
|
||||||
|
'libelle' => $operation['libelle'],
|
||||||
'date_deb' => $operation['date_deb'],
|
'date_deb' => $operation['date_deb'],
|
||||||
'date_fin' => $operation['date_fin']
|
'date_fin' => $operation['date_fin'],
|
||||||
|
'chk_active' => $operation['chk_active']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +437,7 @@ class LoginController {
|
|||||||
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
|
||||||
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
|
||||||
$membresStmt = $this->db->prepare(
|
$membresStmt = $this->db->prepare(
|
||||||
'SELECT id, fk_role, fk_titre, encrypted_name, first_name, sect_name,
|
'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
|
||||||
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
|
||||||
date_naissance, date_embauche, chk_active
|
date_naissance, date_embauche, chk_active
|
||||||
FROM users
|
FROM users
|
||||||
@@ -422,6 +453,7 @@ class LoginController {
|
|||||||
$membreItem = [
|
$membreItem = [
|
||||||
'id' => $membre['id'],
|
'id' => $membre['id'],
|
||||||
'fk_role' => $membre['fk_role'],
|
'fk_role' => $membre['fk_role'],
|
||||||
|
'fk_entite' => $membre['fk_entite'],
|
||||||
'fk_titre' => $membre['fk_titre'],
|
'fk_titre' => $membre['fk_titre'],
|
||||||
'first_name' => $membre['first_name'] ?? '',
|
'first_name' => $membre['first_name'] ?? '',
|
||||||
'sect_name' => $membre['sect_name'] ?? '',
|
'sect_name' => $membre['sect_name'] ?? '',
|
||||||
@@ -433,21 +465,29 @@ class LoginController {
|
|||||||
// Déchiffrement du nom
|
// Déchiffrement du nom
|
||||||
if (!empty($membre['encrypted_name'])) {
|
if (!empty($membre['encrypted_name'])) {
|
||||||
$membreItem['name'] = ApiService::decryptData($membre['encrypted_name']);
|
$membreItem['name'] = ApiService::decryptData($membre['encrypted_name']);
|
||||||
|
} else {
|
||||||
|
$membreItem['name'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déchiffrement du nom d'utilisateur
|
// Déchiffrement du nom d'utilisateur
|
||||||
if (!empty($membre['encrypted_user_name'])) {
|
if (!empty($membre['encrypted_user_name'])) {
|
||||||
$membreItem['username'] = ApiService::decryptSearchableData($membre['encrypted_user_name']);
|
$membreItem['username'] = ApiService::decryptSearchableData($membre['encrypted_user_name']);
|
||||||
|
} else {
|
||||||
|
$membreItem['username'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déchiffrement du téléphone
|
// Déchiffrement du téléphone
|
||||||
if (!empty($membre['encrypted_phone'])) {
|
if (!empty($membre['encrypted_phone'])) {
|
||||||
$membreItem['phone'] = ApiService::decryptData($membre['encrypted_phone']);
|
$membreItem['phone'] = ApiService::decryptData($membre['encrypted_phone']);
|
||||||
|
} else {
|
||||||
|
$membreItem['phone'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déchiffrement du mobile
|
// Déchiffrement du mobile
|
||||||
if (!empty($membre['encrypted_mobile'])) {
|
if (!empty($membre['encrypted_mobile'])) {
|
||||||
$membreItem['mobile'] = ApiService::decryptData($membre['encrypted_mobile']);
|
$membreItem['mobile'] = ApiService::decryptData($membre['encrypted_mobile']);
|
||||||
|
} else {
|
||||||
|
$membreItem['mobile'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Déchiffrement de l'email
|
// Déchiffrement de l'email
|
||||||
@@ -456,6 +496,8 @@ class LoginController {
|
|||||||
if ($decryptedEmail) {
|
if ($decryptedEmail) {
|
||||||
$membreItem['email'] = $decryptedEmail;
|
$membreItem['email'] = $decryptedEmail;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$membreItem['email'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$membresData[] = $membreItem;
|
$membresData[] = $membreItem;
|
||||||
|
|||||||
1516
api/src/Controllers/OperationController.php
Executable file
1516
api/src/Controllers/OperationController.php
Executable file
File diff suppressed because it is too large
Load Diff
803
api/src/Controllers/PassageController.php
Executable file
803
api/src/Controllers/PassageController.php
Executable file
@@ -0,0 +1,803 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/LogService.php';
|
||||||
|
require_once __DIR__ . '/../Services/ApiService.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use Database;
|
||||||
|
use AppConfig;
|
||||||
|
use Request;
|
||||||
|
use Response;
|
||||||
|
use Session;
|
||||||
|
use LogService;
|
||||||
|
use ApiService;
|
||||||
|
use Exception;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
class PassageController {
|
||||||
|
private PDO $db;
|
||||||
|
private AppConfig $appConfig;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
$this->appConfig = AppConfig::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'entité de l'utilisateur connecté
|
||||||
|
*
|
||||||
|
* @param int $userId ID de l'utilisateur
|
||||||
|
* @return int|null ID de l'entité ou null si non trouvé
|
||||||
|
*/
|
||||||
|
private function getUserEntiteId(int $userId): ?int {
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return $user ? (int)$user['fk_entite'] : null;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération de l\'entité utilisateur', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'userId' => $userId
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur a accès à l'opération
|
||||||
|
*
|
||||||
|
* @param int $userId ID de l'utilisateur
|
||||||
|
* @param int $operationId ID de l'opération
|
||||||
|
* @return bool True si l'utilisateur a accès
|
||||||
|
*/
|
||||||
|
private function hasAccessToOperation(int $userId, int $operationId): bool {
|
||||||
|
try {
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM operations
|
||||||
|
WHERE id = ? AND fk_entite = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId, $entiteId]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return $result && $result['count'] > 0;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la vérification d\'accès à l\'opération', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'userId' => $userId,
|
||||||
|
'operationId' => $operationId
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les données d'un passage
|
||||||
|
*
|
||||||
|
* @param array $data Données à valider
|
||||||
|
* @param int|null $passageId ID du passage (pour update)
|
||||||
|
* @return array|null Erreurs de validation ou null
|
||||||
|
*/
|
||||||
|
private function validatePassageData(array $data, ?int $passageId = null): ?array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Validation de l'opération
|
||||||
|
if (!isset($data['fk_operation']) || empty($data['fk_operation'])) {
|
||||||
|
$errors[] = 'L\'ID de l\'opération est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation de l'utilisateur
|
||||||
|
if (!isset($data['fk_user']) || empty($data['fk_user'])) {
|
||||||
|
$errors[] = 'L\'ID de l\'utilisateur est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation de l'adresse
|
||||||
|
if (!isset($data['numero']) || empty(trim($data['numero']))) {
|
||||||
|
$errors[] = 'Le numéro de rue est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['rue']) || empty(trim($data['rue']))) {
|
||||||
|
$errors[] = 'Le nom de rue est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['ville']) || empty(trim($data['ville']))) {
|
||||||
|
$errors[] = 'La ville est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du nom (chiffré)
|
||||||
|
if (!isset($data['encrypted_name']) && !isset($data['name'])) {
|
||||||
|
$errors[] = 'Le nom est obligatoire';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation du montant
|
||||||
|
if (isset($data['montant'])) {
|
||||||
|
$montant = (float)$data['montant'];
|
||||||
|
if ($montant < 0) {
|
||||||
|
$errors[] = 'Le montant ne peut pas être négatif';
|
||||||
|
}
|
||||||
|
if ($montant > 999999.99) {
|
||||||
|
$errors[] = 'Le montant ne peut pas dépasser 999999.99';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation de l'email si fourni
|
||||||
|
if (isset($data['email']) && !empty($data['email'])) {
|
||||||
|
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors[] = 'Format d\'email invalide';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation des coordonnées GPS si fournies
|
||||||
|
if (isset($data['gps_lat']) && !empty($data['gps_lat'])) {
|
||||||
|
$lat = (float)$data['gps_lat'];
|
||||||
|
if ($lat < -90 || $lat > 90) {
|
||||||
|
$errors[] = 'Latitude invalide (doit être entre -90 et 90)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['gps_lng']) && !empty($data['gps_lng'])) {
|
||||||
|
$lng = (float)$data['gps_lng'];
|
||||||
|
if ($lng < -180 || $lng > 180) {
|
||||||
|
$errors[] = 'Longitude invalide (doit être entre -180 et 180)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($errors) ? null : $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les passages de l'entité de l'utilisateur
|
||||||
|
*/
|
||||||
|
public function getPassages(): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paramètres de pagination
|
||||||
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Filtres optionnels
|
||||||
|
$operationId = isset($_GET['operation_id']) ? (int)$_GET['operation_id'] : null;
|
||||||
|
$userId_filter = isset($_GET['user_id']) ? (int)$_GET['user_id'] : null;
|
||||||
|
|
||||||
|
// Construction de la requête
|
||||||
|
$whereConditions = ['o.fk_entite = ?'];
|
||||||
|
$params = [$entiteId];
|
||||||
|
|
||||||
|
if ($operationId) {
|
||||||
|
$whereConditions[] = 'p.fk_operation = ?';
|
||||||
|
$params[] = $operationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userId_filter) {
|
||||||
|
$whereConditions[] = 'p.fk_user = ?';
|
||||||
|
$params[] = $userId_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = implode(' AND ', $whereConditions);
|
||||||
|
|
||||||
|
// Requête principale avec jointures
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
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_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
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN operations o ON p.fk_operation = o.id
|
||||||
|
INNER JOIN users u ON p.fk_user = u.id
|
||||||
|
WHERE $whereClause AND p.chk_active = 1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
$stmt->execute($params);
|
||||||
|
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Déchiffrement des données sensibles
|
||||||
|
foreach ($passages as &$passage) {
|
||||||
|
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
$passage['email'] = !empty($passage['encrypted_email']) ?
|
||||||
|
ApiService::decryptSearchableData($passage['encrypted_email']) : '';
|
||||||
|
$passage['phone'] = !empty($passage['encrypted_phone']) ?
|
||||||
|
ApiService::decryptData($passage['encrypted_phone']) : '';
|
||||||
|
$passage['user_name'] = ApiService::decryptData($passage['user_name']);
|
||||||
|
|
||||||
|
// Suppression des champs chiffrés
|
||||||
|
unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compter le total pour la pagination
|
||||||
|
$countStmt = $this->db->prepare("
|
||||||
|
SELECT COUNT(*) as total
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN operations o ON p.fk_operation = o.id
|
||||||
|
WHERE $whereClause AND p.chk_active = 1
|
||||||
|
");
|
||||||
|
$countStmt->execute(array_slice($params, 0, -2)); // Enlever limit et offset
|
||||||
|
$totalResult = $countStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$total = $totalResult['total'];
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'passages' => $passages,
|
||||||
|
'pagination' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => ceil($total / $limit)
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération des passages', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la récupération des passages'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un passage spécifique par son ID
|
||||||
|
*/
|
||||||
|
public function getPassageById(string $id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passageId = (int)$id;
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
o.libelle as operation_libelle,
|
||||||
|
u.encrypted_name as user_name, u.first_name as user_first_name
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN operations o ON p.fk_operation = o.id
|
||||||
|
INNER JOIN users u ON p.fk_user = u.id
|
||||||
|
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([$passageId, $entiteId]);
|
||||||
|
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$passage) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Passage non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déchiffrement des données sensibles
|
||||||
|
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
$passage['email'] = !empty($passage['encrypted_email']) ?
|
||||||
|
ApiService::decryptSearchableData($passage['encrypted_email']) : '';
|
||||||
|
$passage['phone'] = !empty($passage['encrypted_phone']) ?
|
||||||
|
ApiService::decryptData($passage['encrypted_phone']) : '';
|
||||||
|
$passage['user_name'] = ApiService::decryptData($passage['user_name']);
|
||||||
|
|
||||||
|
// Suppression des champs chiffrés
|
||||||
|
unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'passage' => $passage
|
||||||
|
], 200);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération du passage', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la récupération du passage'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les passages d'une opération spécifique
|
||||||
|
*/
|
||||||
|
public function getPassagesByOperation(string $operation_id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationId = (int)$operation_id;
|
||||||
|
|
||||||
|
// Vérifier l'accès à l'opération
|
||||||
|
if (!$this->hasAccessToOperation($userId, $operationId)) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous n\'avez pas accès à cette opération'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paramètres de pagination
|
||||||
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
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.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
|
||||||
|
FROM ope_pass p
|
||||||
|
INNER JOIN users u ON p.fk_user = u.id
|
||||||
|
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([$operationId, $limit, $offset]);
|
||||||
|
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Déchiffrement des données sensibles
|
||||||
|
foreach ($passages as &$passage) {
|
||||||
|
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
$passage['email'] = !empty($passage['encrypted_email']) ?
|
||||||
|
ApiService::decryptSearchableData($passage['encrypted_email']) : '';
|
||||||
|
$passage['phone'] = !empty($passage['encrypted_phone']) ?
|
||||||
|
ApiService::decryptData($passage['encrypted_phone']) : '';
|
||||||
|
$passage['user_name'] = ApiService::decryptData($passage['user_name']);
|
||||||
|
|
||||||
|
// Suppression des champs chiffrés
|
||||||
|
unset($passage['encrypted_name'], $passage['encrypted_email'], $passage['encrypted_phone']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compter le total
|
||||||
|
$countStmt = $this->db->prepare('
|
||||||
|
SELECT COUNT(*) as total
|
||||||
|
FROM ope_pass
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$countStmt->execute([$operationId]);
|
||||||
|
$totalResult = $countStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$total = $totalResult['total'];
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'passages' => $passages,
|
||||||
|
'pagination' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => ceil($total / $limit)
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la récupération des passages par opération', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'operationId' => $operation_id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la récupération des passages'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau passage
|
||||||
|
*/
|
||||||
|
public function createPassage(): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
// Validation des données
|
||||||
|
$errors = $this->validatePassageData($data);
|
||||||
|
if ($errors) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreurs de validation',
|
||||||
|
'errors' => $errors
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationId = (int)$data['fk_operation'];
|
||||||
|
|
||||||
|
// Vérifier l'accès à l'opération
|
||||||
|
if (!$this->hasAccessToOperation($userId, $operationId)) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous n\'avez pas accès à cette opération'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiffrement des données sensibles
|
||||||
|
$encryptedName = isset($data['name']) ? ApiService::encryptData($data['name']) : (isset($data['encrypted_name']) ? $data['encrypted_name'] : '');
|
||||||
|
$encryptedEmail = isset($data['email']) && !empty($data['email']) ?
|
||||||
|
ApiService::encryptSearchableData($data['email']) : '';
|
||||||
|
$encryptedPhone = isset($data['phone']) && !empty($data['phone']) ?
|
||||||
|
ApiService::encryptData($data['phone']) : '';
|
||||||
|
|
||||||
|
// Préparation des données pour l'insertion
|
||||||
|
$insertData = [
|
||||||
|
'fk_operation' => $operationId,
|
||||||
|
'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0,
|
||||||
|
'fk_user' => (int)$data['fk_user'],
|
||||||
|
'fk_adresse' => $data['fk_adresse'] ?? '',
|
||||||
|
'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null,
|
||||||
|
'fk_type' => isset($data['fk_type']) ? (int)$data['fk_type'] : 0,
|
||||||
|
'numero' => trim($data['numero']),
|
||||||
|
'rue' => trim($data['rue']),
|
||||||
|
'rue_bis' => $data['rue_bis'] ?? '',
|
||||||
|
'ville' => trim($data['ville']),
|
||||||
|
'fk_habitat' => isset($data['fk_habitat']) ? (int)$data['fk_habitat'] : 1,
|
||||||
|
'appt' => $data['appt'] ?? '',
|
||||||
|
'niveau' => $data['niveau'] ?? '',
|
||||||
|
'residence' => $data['residence'] ?? '',
|
||||||
|
'gps_lat' => $data['gps_lat'] ?? '',
|
||||||
|
'gps_lng' => $data['gps_lng'] ?? '',
|
||||||
|
'encrypted_name' => $encryptedName,
|
||||||
|
'montant' => isset($data['montant']) ? (float)$data['montant'] : 0.00,
|
||||||
|
'fk_type_reglement' => isset($data['fk_type_reglement']) ? (int)$data['fk_type_reglement'] : 1,
|
||||||
|
'remarque' => $data['remarque'] ?? '',
|
||||||
|
'encrypted_email' => $encryptedEmail,
|
||||||
|
'encrypted_phone' => $encryptedPhone,
|
||||||
|
'nom_recu' => $data['nom_recu'] ?? null,
|
||||||
|
'date_recu' => isset($data['date_recu']) ? $data['date_recu'] : null,
|
||||||
|
'docremis' => isset($data['docremis']) ? (int)$data['docremis'] : 0,
|
||||||
|
'date_repasser' => isset($data['date_repasser']) ? $data['date_repasser'] : null,
|
||||||
|
'nb_passages' => isset($data['nb_passages']) ? (int)$data['nb_passages'] : 1,
|
||||||
|
'chk_mobile' => isset($data['chk_mobile']) ? (int)$data['chk_mobile'] : 0,
|
||||||
|
'anomalie' => isset($data['anomalie']) ? (int)$data['anomalie'] : 0,
|
||||||
|
'fk_user_creat' => $userId
|
||||||
|
];
|
||||||
|
|
||||||
|
// Construction de la requête d'insertion
|
||||||
|
$fields = array_keys($insertData);
|
||||||
|
$placeholders = array_fill(0, count($fields), '?');
|
||||||
|
|
||||||
|
$sql = 'INSERT INTO ope_pass (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute(array_values($insertData));
|
||||||
|
|
||||||
|
$passageId = $this->db->lastInsertId();
|
||||||
|
|
||||||
|
LogService::log('Création d\'un nouveau passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'userId' => $userId,
|
||||||
|
'passageId' => $passageId,
|
||||||
|
'operationId' => $operationId
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Passage créé avec succès',
|
||||||
|
'passage_id' => $passageId
|
||||||
|
], 201);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la création du passage', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la création du passage'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un passage existant
|
||||||
|
*/
|
||||||
|
public function updatePassage(string $id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passageId = (int)$id;
|
||||||
|
$data = Request::getJson();
|
||||||
|
|
||||||
|
// Vérifier que le passage existe et appartient à l'entité de l'utilisateur
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT p.id, p.fk_operation
|
||||||
|
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
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId, $entiteId]);
|
||||||
|
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$passage) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Passage non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation des données
|
||||||
|
$errors = $this->validatePassageData($data, $passageId);
|
||||||
|
if ($errors) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreurs de validation',
|
||||||
|
'errors' => $errors
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construction de la requête de mise à jour dynamique
|
||||||
|
$updateFields = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Champs pouvant être mis à jour
|
||||||
|
$updatableFields = [
|
||||||
|
'fk_sector',
|
||||||
|
'fk_user',
|
||||||
|
'fk_adresse',
|
||||||
|
'passed_at',
|
||||||
|
'fk_type',
|
||||||
|
'numero',
|
||||||
|
'rue',
|
||||||
|
'rue_bis',
|
||||||
|
'ville',
|
||||||
|
'fk_habitat',
|
||||||
|
'appt',
|
||||||
|
'niveau',
|
||||||
|
'residence',
|
||||||
|
'gps_lat',
|
||||||
|
'gps_lng',
|
||||||
|
'montant',
|
||||||
|
'fk_type_reglement',
|
||||||
|
'remarque',
|
||||||
|
'nom_recu',
|
||||||
|
'date_recu',
|
||||||
|
'docremis',
|
||||||
|
'date_repasser',
|
||||||
|
'nb_passages',
|
||||||
|
'chk_mobile',
|
||||||
|
'anomalie'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($updatableFields as $field) {
|
||||||
|
if (isset($data[$field])) {
|
||||||
|
$updateFields[] = "$field = ?";
|
||||||
|
$params[] = $data[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion des champs chiffrés
|
||||||
|
if (isset($data['name'])) {
|
||||||
|
$updateFields[] = "encrypted_name = ?";
|
||||||
|
$params[] = ApiService::encryptData($data['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['email'])) {
|
||||||
|
$updateFields[] = "encrypted_email = ?";
|
||||||
|
$params[] = !empty($data['email']) ? ApiService::encryptSearchableData($data['email']) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['phone'])) {
|
||||||
|
$updateFields[] = "encrypted_phone = ?";
|
||||||
|
$params[] = !empty($data['phone']) ? ApiService::encryptData($data['phone']) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($updateFields)) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Aucune donnée à mettre à jour'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajout des champs de mise à jour
|
||||||
|
$updateFields[] = "updated_at = NOW()";
|
||||||
|
$updateFields[] = "fk_user_modif = ?";
|
||||||
|
$params[] = $userId;
|
||||||
|
$params[] = $passageId;
|
||||||
|
|
||||||
|
$sql = 'UPDATE ope_pass SET ' . implode(', ', $updateFields) . ' WHERE id = ?';
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
LogService::log('Mise à jour d\'un passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'userId' => $userId,
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Passage mis à jour avec succès'
|
||||||
|
], 200);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la mise à jour du passage', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la mise à jour du passage'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime (désactive) un passage
|
||||||
|
*/
|
||||||
|
public function deletePassage(string $id): void {
|
||||||
|
try {
|
||||||
|
$userId = Session::getUserId();
|
||||||
|
if (!$userId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||||
|
], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passageId = (int)$id;
|
||||||
|
|
||||||
|
// Vérifier que le passage existe et appartient à l'entité de l'utilisateur
|
||||||
|
$entiteId = $this->getUserEntiteId($userId);
|
||||||
|
if (!$entiteId) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT p.id
|
||||||
|
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
|
||||||
|
');
|
||||||
|
$stmt->execute([$passageId, $entiteId]);
|
||||||
|
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$passage) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Passage non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Désactiver le passage (soft delete)
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
UPDATE ope_pass
|
||||||
|
SET chk_active = 0, updated_at = NOW(), fk_user_modif = ?
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([$userId, $passageId]);
|
||||||
|
|
||||||
|
LogService::log('Suppression d\'un passage', [
|
||||||
|
'level' => 'info',
|
||||||
|
'userId' => $userId,
|
||||||
|
'passageId' => $passageId
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Passage supprimé avec succès'
|
||||||
|
], 200);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la suppression du passage', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'passageId' => $id,
|
||||||
|
'userId' => $userId ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors de la suppression du passage'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1307
api/src/Controllers/SectorController.php
Normal file
1307
api/src/Controllers/SectorController.php
Normal file
File diff suppressed because it is too large
Load Diff
312
api/src/Controllers/UserController.php
Normal file → Executable file
312
api/src/Controllers/UserController.php
Normal file → Executable file
@@ -26,10 +26,6 @@ class UserController {
|
|||||||
$this->appConfig = AppConfig::getInstance();
|
$this->appConfig = AppConfig::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function getUsers(): void {
|
public function getUsers(): void {
|
||||||
Session::requireAuth();
|
Session::requireAuth();
|
||||||
|
|
||||||
@@ -53,7 +49,7 @@ class UserController {
|
|||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.encrypt_email,
|
u.encrypted_email,
|
||||||
u.encrypted_name,
|
u.encrypted_name,
|
||||||
u.first_name,
|
u.first_name,
|
||||||
u.fk_role as role,
|
u.fk_role as role,
|
||||||
@@ -71,7 +67,7 @@ class UserController {
|
|||||||
|
|
||||||
// Déchiffrement des données sensibles pour chaque utilisateur
|
// Déchiffrement des données sensibles pour chaque utilisateur
|
||||||
foreach ($users as &$user) {
|
foreach ($users as &$user) {
|
||||||
$user['email'] = ApiService::decryptSearchableData($user['encrypt_email']);
|
$user['email'] = ApiService::decryptSearchableData($user['encrypted_email']);
|
||||||
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
||||||
|
|
||||||
if (!empty($user['entite_name'])) {
|
if (!empty($user['entite_name'])) {
|
||||||
@@ -79,7 +75,7 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Suppression des champs chiffrés
|
// Suppression des champs chiffrés
|
||||||
unset($user['encrypt_email']);
|
unset($user['encrypted_email']);
|
||||||
unset($user['encrypted_name']);
|
unset($user['encrypted_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,20 +119,18 @@ class UserController {
|
|||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
u.encrypt_email,
|
u.encrypted_email,
|
||||||
u.encrypted_name,
|
u.encrypted_name,
|
||||||
u.first_name,
|
u.first_name,
|
||||||
u.sect_name,
|
u.sect_name,
|
||||||
u.encrypt_phone,
|
u.encrypted_phone,
|
||||||
u.encrypt_mobile,
|
u.encrypted_mobile,
|
||||||
u.fk_role as role,
|
u.fk_role as role,
|
||||||
u.fk_entite,
|
u.fk_entite,
|
||||||
u.infos,
|
|
||||||
u.chk_alert_email,
|
u.chk_alert_email,
|
||||||
u.chk_suivi,
|
u.chk_suivi,
|
||||||
u.date_naissance,
|
u.date_naissance,
|
||||||
u.date_embauche,
|
u.date_embauche,
|
||||||
u.matricule,
|
|
||||||
u.chk_active,
|
u.chk_active,
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at,
|
u.updated_at,
|
||||||
@@ -162,20 +156,20 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Déchiffrement des données sensibles
|
// Déchiffrement des données sensibles
|
||||||
$user['email'] = ApiService::decryptSearchableData($user['encrypt_email']);
|
$user['email'] = ApiService::decryptSearchableData($user['encrypted_email']);
|
||||||
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
||||||
$user['phone'] = ApiService::decryptData($user['encrypt_phone'] ?? '');
|
$user['phone'] = ApiService::decryptData($user['encrypted_phone'] ?? '');
|
||||||
$user['mobile'] = ApiService::decryptData($user['encrypt_mobile'] ?? '');
|
$user['mobile'] = ApiService::decryptData($user['encrypted_mobile'] ?? '');
|
||||||
|
|
||||||
if (!empty($user['entite_name'])) {
|
if (!empty($user['entite_name'])) {
|
||||||
$user['entite_name'] = ApiService::decryptData($user['entite_name']);
|
$user['entite_name'] = ApiService::decryptData($user['entite_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppression des champs chiffrés
|
// Suppression des champs chiffrés
|
||||||
unset($user['encrypt_email']);
|
unset($user['encrypted_email']);
|
||||||
unset($user['encrypted_name']);
|
unset($user['encrypted_name']);
|
||||||
unset($user['encrypt_phone']);
|
unset($user['encrypted_phone']);
|
||||||
unset($user['encrypt_mobile']);
|
unset($user['encrypted_mobile']);
|
||||||
|
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
@@ -230,11 +224,11 @@ class UserController {
|
|||||||
$email = trim(strtolower($data['email']));
|
$email = trim(strtolower($data['email']));
|
||||||
$name = trim($data['name']);
|
$name = trim($data['name']);
|
||||||
$firstName = isset($data['first_name']) ? trim($data['first_name']) : '';
|
$firstName = isset($data['first_name']) ? trim($data['first_name']) : '';
|
||||||
$role = isset($data['role']) ? trim($data['role']) : '1';
|
$role = isset($data['role']) ? (int)$data['role'] : 1;
|
||||||
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
|
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
|
||||||
|
|
||||||
// Vérification des longueurs d'entrée
|
// Vérification des longueurs d'entrée
|
||||||
if (strlen($email) > 255 || strlen($name) > 255) {
|
if (strlen($email) > 75 || strlen($name) > 50) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Email ou nom trop long'
|
'message' => 'Email ou nom trop long'
|
||||||
@@ -256,7 +250,7 @@ class UserController {
|
|||||||
$encryptedName = ApiService::encryptData($name);
|
$encryptedName = ApiService::encryptData($name);
|
||||||
|
|
||||||
// Vérification de l'existence de l'email
|
// Vérification de l'existence de l'email
|
||||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypt_email = ?');
|
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?');
|
||||||
$checkStmt->execute([$encryptedEmail]);
|
$checkStmt->execute([$encryptedEmail]);
|
||||||
if ($checkStmt->fetch()) {
|
if ($checkStmt->fetch()) {
|
||||||
Response::json([
|
Response::json([
|
||||||
@@ -274,26 +268,24 @@ class UserController {
|
|||||||
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
|
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
|
||||||
$mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null;
|
$mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null;
|
||||||
$sectName = isset($data['sect_name']) ? trim($data['sect_name']) : '';
|
$sectName = isset($data['sect_name']) ? trim($data['sect_name']) : '';
|
||||||
$infos = isset($data['infos']) ? trim($data['infos']) : '';
|
|
||||||
$alertEmail = isset($data['chk_alert_email']) ? (int)$data['chk_alert_email'] : 1;
|
$alertEmail = isset($data['chk_alert_email']) ? (int)$data['chk_alert_email'] : 1;
|
||||||
$suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0;
|
$suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0;
|
||||||
$dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null;
|
$dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null;
|
||||||
$dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null;
|
$dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null;
|
||||||
$matricule = isset($data['matricule']) ? trim($data['matricule']) : '';
|
|
||||||
|
|
||||||
// Insertion en base de données
|
// Insertion en base de données
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
encrypt_email, user_pswd, encrypted_name, first_name,
|
encrypted_email, user_pass_hash, encrypted_name, first_name,
|
||||||
sect_name, encrypt_phone, encrypt_mobile, fk_role,
|
sect_name, encrypted_phone, encrypted_mobile, fk_role,
|
||||||
fk_entite, infos, chk_alert_email, chk_suivi,
|
fk_entite, chk_alert_email, chk_suivi,
|
||||||
date_naissance, date_embauche, matricule,
|
date_naissance, date_embauche,
|
||||||
created_at, fk_user_creat, chk_active
|
created_at, fk_user_creat, chk_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?,
|
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
|
?, ?,
|
||||||
NOW(), ?, 1
|
NOW(), ?, 1
|
||||||
)
|
)
|
||||||
');
|
');
|
||||||
@@ -307,12 +299,10 @@ class UserController {
|
|||||||
$mobile,
|
$mobile,
|
||||||
$role,
|
$role,
|
||||||
$entiteId,
|
$entiteId,
|
||||||
$infos,
|
|
||||||
$alertEmail,
|
$alertEmail,
|
||||||
$suivi,
|
$suivi,
|
||||||
$dateNaissance,
|
$dateNaissance,
|
||||||
$dateEmbauche,
|
$dateEmbauche,
|
||||||
$matricule,
|
|
||||||
$currentUserId
|
$currentUserId
|
||||||
]);
|
]);
|
||||||
$userId = $this->db->lastInsertId();
|
$userId = $this->db->lastInsertId();
|
||||||
@@ -387,7 +377,7 @@ class UserController {
|
|||||||
$email = trim(strtolower($data['email']));
|
$email = trim(strtolower($data['email']));
|
||||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||||
|
|
||||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypt_email = ? AND id != ?');
|
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ? AND id != ?');
|
||||||
$checkStmt->execute([$encryptedEmail, $id]);
|
$checkStmt->execute([$encryptedEmail, $id]);
|
||||||
if ($checkStmt->fetch()) {
|
if ($checkStmt->fetch()) {
|
||||||
Response::json([
|
Response::json([
|
||||||
@@ -397,8 +387,8 @@ class UserController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$updateFields[] = "encrypt_email = :encrypt_email";
|
$updateFields[] = "encrypted_email = :encrypted_email";
|
||||||
$params['encrypt_email'] = $encryptedEmail;
|
$params['encrypted_email'] = $encryptedEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['name'])) {
|
if (isset($data['name'])) {
|
||||||
@@ -407,13 +397,13 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['phone'])) {
|
if (isset($data['phone'])) {
|
||||||
$updateFields[] = "encrypt_phone = :encrypt_phone";
|
$updateFields[] = "encrypted_phone = :encrypted_phone";
|
||||||
$params['encrypt_phone'] = ApiService::encryptData(trim($data['phone']));
|
$params['encrypted_phone'] = ApiService::encryptData(trim($data['phone']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['mobile'])) {
|
if (isset($data['mobile'])) {
|
||||||
$updateFields[] = "encrypt_mobile = :encrypt_mobile";
|
$updateFields[] = "encrypted_mobile = :encrypted_mobile";
|
||||||
$params['encrypt_mobile'] = ApiService::encryptData(trim($data['mobile']));
|
$params['encrypted_mobile'] = ApiService::encryptData(trim($data['mobile']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traitement des champs non chiffrés
|
// Traitement des champs non chiffrés
|
||||||
@@ -422,12 +412,10 @@ class UserController {
|
|||||||
'sect_name',
|
'sect_name',
|
||||||
'fk_role',
|
'fk_role',
|
||||||
'fk_entite',
|
'fk_entite',
|
||||||
'infos',
|
|
||||||
'chk_alert_email',
|
'chk_alert_email',
|
||||||
'chk_suivi',
|
'chk_suivi',
|
||||||
'date_naissance',
|
'date_naissance',
|
||||||
'date_embauche',
|
'date_embauche',
|
||||||
'matricule',
|
|
||||||
'chk_active'
|
'chk_active'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -447,7 +435,7 @@ class UserController {
|
|||||||
], 400);
|
], 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$updateFields[] = "user_pswd = :password";
|
$updateFields[] = "user_pass_hash = :password";
|
||||||
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +461,7 @@ class UserController {
|
|||||||
'level' => 'info',
|
'level' => 'info',
|
||||||
'modifiedBy' => $currentUserId,
|
'modifiedBy' => $currentUserId,
|
||||||
'userId' => $id,
|
'userId' => $id,
|
||||||
'fields' => array_keys($data)
|
'fields' => array_keys($data),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Response::json([
|
Response::json([
|
||||||
@@ -502,24 +490,23 @@ class UserController {
|
|||||||
public function deleteUser(string $id): void {
|
public function deleteUser(string $id): void {
|
||||||
Session::requireAuth();
|
Session::requireAuth();
|
||||||
|
|
||||||
// Vérification des droits d'accès (rôle administrateur)
|
|
||||||
$currentUserId = Session::getUserId();
|
$currentUserId = Session::getUserId();
|
||||||
|
|
||||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
// Récupérer les infos de l'utilisateur courant
|
||||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
$stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
|
||||||
$stmt->execute([$currentUserId]);
|
$stmt->execute([$currentUserId]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$userRole = $result ? $result['fk_role'] : null;
|
|
||||||
|
|
||||||
if ($userRole != '1' && $userRole != '2') {
|
if (!$currentUser) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Accès non autorisé'
|
'message' => 'Utilisateur courant non trouvé'
|
||||||
], 403);
|
], 403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentUserId = Session::getUserId();
|
$userRole = (int)$currentUser['fk_role'];
|
||||||
|
$userEntite = $currentUser['fk_entite'];
|
||||||
|
|
||||||
// Empêcher la suppression de son propre compte
|
// Empêcher la suppression de son propre compte
|
||||||
if ($currentUserId == $id) {
|
if ($currentUserId == $id) {
|
||||||
@@ -530,37 +517,234 @@ class UserController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur cible
|
||||||
|
$stmt2 = $this->db->prepare('SELECT fk_entite FROM users WHERE id = ?');
|
||||||
|
$stmt2->execute([$id]);
|
||||||
|
$userToDelete = $stmt2->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$userToDelete) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Utilisateur cible non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrôle des droits
|
||||||
|
if ($userRole === 1) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => "Vous n'avez pas le droit de supprimer un utilisateur"
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
} elseif ($userRole === 2) {
|
||||||
|
if ($userEntite != $userToDelete['fk_entite']) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => "Vous n'avez pas le droit de supprimer un utilisateur d'une autre amicale"
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fk_role > 2 : tout est permis (hors auto-suppression)
|
||||||
|
|
||||||
|
// ——— Gestion du transfert éventuel ———
|
||||||
|
$transferTo = isset($_GET['transfer_to']) ? trim($_GET['transfer_to']) : null;
|
||||||
|
|
||||||
|
if ($transferTo) {
|
||||||
|
try {
|
||||||
|
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
|
||||||
|
$stmt3 = $this->db->prepare('
|
||||||
|
UPDATE ope_pass
|
||||||
|
SET fk_user = :new_user_id
|
||||||
|
WHERE fk_user = :delete_user_id
|
||||||
|
');
|
||||||
|
$stmt3->execute([
|
||||||
|
'new_user_id' => $transferTo,
|
||||||
|
'delete_user_id' => $id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transferredCount = $stmt3->rowCount();
|
||||||
|
|
||||||
|
LogService::log('Passages transférés avant suppression utilisateur', [
|
||||||
|
'level' => 'info',
|
||||||
|
'from_user' => $id,
|
||||||
|
'to_user' => $transferTo,
|
||||||
|
'passages_transferred' => $transferredCount
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors du transfert des passages',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— Suppression réelle de l'utilisateur ——
|
||||||
try {
|
try {
|
||||||
// Désactivation de l'utilisateur plutôt que suppression
|
// Supprimer les enregistrements dépendants dans ope_users
|
||||||
$stmt = $this->db->prepare('
|
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
|
||||||
UPDATE users
|
$stmtOpeUsers->execute([$id]);
|
||||||
SET chk_active = 0,
|
|
||||||
updated_at = NOW(),
|
// Supprimer les enregistrements dépendants dans ope_users_sectors
|
||||||
fk_user_modif = ?
|
$stmtOpeUsersSectors = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_user = ?');
|
||||||
WHERE id = ?
|
$stmtOpeUsersSectors->execute([$id]);
|
||||||
');
|
|
||||||
$stmt->execute([$currentUserId, $id]);
|
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
if ($stmt->rowCount() === 0) {
|
if ($stmt->rowCount() === 0) {
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Utilisateur non trouvé'
|
'message' => 'Utilisateur non trouvé ou déjà supprimé'
|
||||||
], 404);
|
], 404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogService::log('Utilisateur GeoSector désactivé', [
|
LogService::log('Utilisateur GeoSector supprimé', [
|
||||||
'level' => 'info',
|
'level' => 'info',
|
||||||
'deactivatedBy' => $currentUserId,
|
'deletedBy' => $currentUserId,
|
||||||
'userId' => $id
|
'userId' => $id,
|
||||||
|
'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Response::json([
|
Response::json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Utilisateur désactivé avec succès'
|
'message' => 'Utilisateur supprimé avec succès'
|
||||||
]);
|
]);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
LogService::log('Erreur lors de la désactivation d\'un utilisateur GeoSector', [
|
LogService::log('Erreur lors de la suppression d\'un utilisateur GeoSector', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'userId' => $id
|
||||||
|
]);
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur serveur'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword(string $id): void {
|
||||||
|
Session::requireAuth();
|
||||||
|
|
||||||
|
$currentUserId = Session::getUserId();
|
||||||
|
|
||||||
|
// Récupérer les infos de l'utilisateur courant
|
||||||
|
$stmt = $this->db->prepare('SELECT fk_role, fk_entite, chk_active FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$currentUserId]);
|
||||||
|
$currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$currentUser) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Utilisateur courant non trouvé'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'utilisateur courant est actif
|
||||||
|
if ($currentUser['chk_active'] != 1) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Votre compte n\'est pas actif'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userRole = (int)$currentUser['fk_role'];
|
||||||
|
$userEntite = $currentUser['fk_entite'];
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur cible
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT id, encrypted_email, encrypted_name, fk_entite, chk_active
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$targetUser) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Utilisateur non trouvé'
|
||||||
|
], 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'utilisateur est actif
|
||||||
|
if ($targetUser['chk_active'] != 1) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'L\'utilisateur n\'est pas actif'
|
||||||
|
], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrôle des droits selon le rôle
|
||||||
|
if ($userRole === 1) {
|
||||||
|
// Role 1 : peut uniquement réinitialiser son propre mot de passe
|
||||||
|
if ($currentUserId != $id) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous ne pouvez réinitialiser que votre propre mot de passe'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} elseif ($userRole === 2) {
|
||||||
|
// Role 2 : peut réinitialiser les mots de passe de sa propre entité
|
||||||
|
if ($userEntite != $targetUser['fk_entite']) {
|
||||||
|
Response::json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Vous ne pouvez réinitialiser que les mots de passe des utilisateurs de votre entité'
|
||||||
|
], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Role > 2 : peut tout faire
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Déchiffrement des données
|
||||||
|
$email = ApiService::decryptSearchableData($targetUser['encrypted_email']);
|
||||||
|
$name = ApiService::decryptData($targetUser['encrypted_name']);
|
||||||
|
|
||||||
|
// Génération d'un nouveau mot de passe sécurisé
|
||||||
|
$newPassword = ApiService::generateSecurePassword();
|
||||||
|
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Mise à jour du mot de passe en base de données
|
||||||
|
$updateStmt = $this->db->prepare('
|
||||||
|
UPDATE users
|
||||||
|
SET user_pass_hash = :password,
|
||||||
|
updated_at = NOW(),
|
||||||
|
fk_user_modif = :modifier_id
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$updateStmt->execute([
|
||||||
|
'password' => $passwordHash,
|
||||||
|
'modifier_id' => $currentUserId,
|
||||||
|
'id' => $id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Envoi de l'email avec le nouveau mot de passe
|
||||||
|
ApiService::sendEmail($email, $name, 'password_reset', ['password' => $newPassword]);
|
||||||
|
|
||||||
|
LogService::log('Mot de passe réinitialisé', [
|
||||||
|
'level' => 'info',
|
||||||
|
'resetBy' => $currentUserId,
|
||||||
|
'userId' => $id,
|
||||||
|
'email' => $email
|
||||||
|
]);
|
||||||
|
|
||||||
|
Response::json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Mot de passe réinitialisé avec succès. Un email a été envoyé à l\'utilisateur.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
LogService::log('Erreur lors de la réinitialisation du mot de passe', [
|
||||||
'level' => 'error',
|
'level' => 'error',
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'userId' => $id
|
'userId' => $id
|
||||||
|
|||||||
0
api/src/Controllers/VilleController.php
Normal file → Executable file
0
api/src/Controllers/VilleController.php
Normal file → Executable file
46
api/src/Core/AddressesDatabase.php
Normal file
46
api/src/Core/AddressesDatabase.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class AddressesDatabase {
|
||||||
|
private static ?PDO $instance = null;
|
||||||
|
private static array $config;
|
||||||
|
|
||||||
|
public static function init(array $config): void {
|
||||||
|
self::$config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance(): PDO {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
try {
|
||||||
|
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",
|
||||||
|
self::$config['host'],
|
||||||
|
self::$config['name']
|
||||||
|
);
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
self::$instance = new PDO(
|
||||||
|
$dsn,
|
||||||
|
self::$config['username'],
|
||||||
|
self::$config['password'],
|
||||||
|
$options
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new RuntimeException("Addresses database connection failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ferme la connexion à la base de données des adresses
|
||||||
|
*/
|
||||||
|
public static function close(): void {
|
||||||
|
self::$instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
api/src/Core/Database.php
Normal file → Executable file
0
api/src/Core/Database.php
Normal file → Executable file
0
api/src/Core/Request.php
Normal file → Executable file
0
api/src/Core/Request.php
Normal file → Executable file
0
api/src/Core/Response.php
Normal file → Executable file
0
api/src/Core/Response.php
Normal file → Executable file
44
api/src/Core/Router.php
Normal file → Executable file
44
api/src/Core/Router.php
Normal file → Executable file
@@ -37,15 +37,56 @@ class Router {
|
|||||||
$this->post('users', ['UserController', 'createUser']);
|
$this->post('users', ['UserController', 'createUser']);
|
||||||
$this->put('users/:id', ['UserController', 'updateUser']);
|
$this->put('users/:id', ['UserController', 'updateUser']);
|
||||||
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
||||||
|
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
|
||||||
$this->post('logout', ['LoginController', 'logout']);
|
$this->post('logout', ['LoginController', 'logout']);
|
||||||
|
|
||||||
// Routes entités
|
// Routes entités
|
||||||
$this->get('entites', ['EntiteController', 'getEntites']);
|
$this->get('entites', ['EntiteController', 'getEntites']);
|
||||||
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
|
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
|
||||||
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
|
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
|
||||||
|
$this->put('entites/:id', ['EntiteController', 'updateEntite']);
|
||||||
|
|
||||||
|
// Routes opérations
|
||||||
|
$this->get('operations', ['OperationController', 'getOperations']);
|
||||||
|
$this->get('operations/:id', ['OperationController', 'getOperationById']);
|
||||||
|
$this->post('operations', ['OperationController', 'createOperation']);
|
||||||
|
$this->put('operations/:id', ['OperationController', 'updateOperation']);
|
||||||
|
$this->delete('operations/:id', ['OperationController', 'deleteOperation']);
|
||||||
|
|
||||||
|
// Routes d'export d'opérations
|
||||||
|
$this->get('operations/:id/export/excel', ['OperationController', 'exportExcel']);
|
||||||
|
$this->get('operations/:id/export/json', ['OperationController', 'exportJson']);
|
||||||
|
$this->get('operations/:id/export/full', ['OperationController', 'exportFull']);
|
||||||
|
$this->get('operations/:id/backups', ['OperationController', 'getBackups']);
|
||||||
|
$this->get('operations/:id/backups/:backup_id', ['OperationController', 'downloadBackup']);
|
||||||
|
$this->delete('operations/:id/backups/:backup_id', ['OperationController', 'deleteBackup']);
|
||||||
|
|
||||||
|
// Routes passages
|
||||||
|
$this->get('passages', ['PassageController', 'getPassages']);
|
||||||
|
$this->get('passages/:id', ['PassageController', 'getPassageById']);
|
||||||
|
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
|
||||||
|
$this->post('passages', ['PassageController', 'createPassage']);
|
||||||
|
$this->put('passages/:id', ['PassageController', 'updatePassage']);
|
||||||
|
$this->delete('passages/:id', ['PassageController', 'deletePassage']);
|
||||||
|
|
||||||
// Routes villes
|
// Routes villes
|
||||||
$this->get('villes', ['VilleController', 'searchVillesByPostalCode']);
|
$this->get('villes', ['VilleController', 'searchVillesByPostalCode']);
|
||||||
|
|
||||||
|
// Routes fichiers
|
||||||
|
$this->get('files/browse', ['FileController', 'browse']);
|
||||||
|
$this->get('files/search', ['FileController', 'search']);
|
||||||
|
$this->get('files/stats', ['FileController', 'getStats']);
|
||||||
|
$this->get('files/metadata', ['FileController', 'getMetadata']);
|
||||||
|
$this->get('files/list/:support/:id', ['FileController', 'listBySupport']);
|
||||||
|
$this->get('files/info/:id', ['FileController', 'getFileInfo']);
|
||||||
|
$this->get('files/download/:id', ['FileController', 'download']);
|
||||||
|
$this->delete('files/:id', ['FileController', 'deleteFile']);
|
||||||
|
|
||||||
|
// Routes secteurs
|
||||||
|
$this->get('sectors', ['SectorController', 'index']);
|
||||||
|
$this->post('sectors', ['SectorController', 'create']);
|
||||||
|
$this->put('sectors/:id', ['SectorController', 'update']);
|
||||||
|
$this->delete('sectors/:id', ['SectorController', 'delete']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void {
|
public function handle(): void {
|
||||||
@@ -187,7 +228,8 @@ class Router {
|
|||||||
error_log("Routes disponibles pour $method: " . implode(', ', array_keys($this->routes[$method])));
|
error_log("Routes disponibles pour $method: " . implode(', ', array_keys($this->routes[$method])));
|
||||||
|
|
||||||
foreach ($this->routes[$method] as $route => $handler) {
|
foreach ($this->routes[$method] as $route => $handler) {
|
||||||
$pattern = preg_replace('/{[^}]+}/', '([^/]+)', $route);
|
// Correction: utiliser :param au lieu de {param}
|
||||||
|
$pattern = preg_replace('/:([^\/]+)/', '([^/]+)', $route);
|
||||||
$pattern = "@^" . $pattern . "$@D";
|
$pattern = "@^" . $pattern . "$@D";
|
||||||
error_log("Test pattern: $pattern contre uri: $uri");
|
error_log("Test pattern: $pattern contre uri: $uri");
|
||||||
|
|
||||||
|
|||||||
5
api/src/Core/Session.php
Normal file → Executable file
5
api/src/Core/Session.php
Normal file → Executable file
@@ -27,6 +27,7 @@ class Session {
|
|||||||
public static function login(array $userData): void {
|
public static function login(array $userData): void {
|
||||||
$_SESSION['user_id'] = $userData['id'];
|
$_SESSION['user_id'] = $userData['id'];
|
||||||
$_SESSION['user_email'] = $userData['email'] ?? '';
|
$_SESSION['user_email'] = $userData['email'] ?? '';
|
||||||
|
$_SESSION['entity_id'] = $userData['fk_entite'] ?? null;
|
||||||
$_SESSION['authenticated'] = true;
|
$_SESSION['authenticated'] = true;
|
||||||
$_SESSION['last_activity'] = time();
|
$_SESSION['last_activity'] = time();
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ class Session {
|
|||||||
return $_SESSION['user_email'] ?? null;
|
return $_SESSION['user_email'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getEntityId(): ?int {
|
||||||
|
return $_SESSION['entity_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public static function requireAuth(): void {
|
public static function requireAuth(): void {
|
||||||
if (!self::isAuthenticated()) {
|
if (!self::isAuthenticated()) {
|
||||||
// Log détaillé pour le debug
|
// Log détaillé pour le debug
|
||||||
|
|||||||
345
api/src/Services/AddressService.php
Normal file
345
api/src/Services/AddressService.php
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/LogService.php';
|
||||||
|
|
||||||
|
class AddressService {
|
||||||
|
private ?PDO $addressesDb = null;
|
||||||
|
private PDO $mainDb;
|
||||||
|
private LogService $logService;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->logService = new LogService();
|
||||||
|
try {
|
||||||
|
$this->addressesDb = AddressesDatabase::getInstance();
|
||||||
|
$this->logService->info('[AddressService] Connexion à la base d\'adresses réussie');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Si la connexion échoue, on continue sans la base d'adresses
|
||||||
|
$this->logService->error('[AddressService] Connexion à la base d\'adresses impossible', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
$this->addressesDb = null;
|
||||||
|
}
|
||||||
|
$this->mainDb = Database::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la connexion à la base d'adresses est active
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isConnected(): bool {
|
||||||
|
return $this->addressesDb !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine le département de l'entité courante
|
||||||
|
*
|
||||||
|
* @param int|null $entityId ID de l'entité
|
||||||
|
* @return string|null Code département (ex: "22", "23")
|
||||||
|
*/
|
||||||
|
private function getDepartmentForEntity(?int $entityId = null): ?string {
|
||||||
|
if (!$entityId) {
|
||||||
|
$entityId = $_SESSION['entity_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$entityId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$query = "SELECT departement FROM entites WHERE id = :entity_id";
|
||||||
|
$stmt = $this->mainDb->prepare($query);
|
||||||
|
$stmt->execute(['entity_id' => $entityId]);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
|
||||||
|
return $result ? $result['departement'] : null;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
|
||||||
|
* Gère automatiquement les secteurs multi-départements
|
||||||
|
*
|
||||||
|
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||||
|
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||||
|
* @return array Array des adresses trouvées
|
||||||
|
*/
|
||||||
|
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array {
|
||||||
|
// Si pas de connexion à la base d'adresses, retourner un tableau vide
|
||||||
|
if (!$this->addressesDb) {
|
||||||
|
$this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [
|
||||||
|
'entity_id' => $entityId
|
||||||
|
]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logService->info('[AddressService] Début recherche adresses', [
|
||||||
|
'entity_id' => $entityId,
|
||||||
|
'nb_coordinates' => count($coordinates)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (count($coordinates) < 3) {
|
||||||
|
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||||
|
}
|
||||||
|
|
||||||
|
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||||
|
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||||
|
$boundaryService = new \DepartmentBoundaryService();
|
||||||
|
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||||
|
|
||||||
|
if (empty($departmentsTouched)) {
|
||||||
|
// Si aucun département n'est trouvé par analyse spatiale,
|
||||||
|
// chercher d'abord dans le département de l'entité et ses limitrophes
|
||||||
|
$entityDept = $this->getDepartmentForEntity($entityId);
|
||||||
|
$this->logService->info('[AddressService] Département de l\'entité', [
|
||||||
|
'departement' => $entityDept
|
||||||
|
]);
|
||||||
|
if (!$entityDept) {
|
||||||
|
$this->logService->error('[AddressService] Impossible de déterminer le département de l\'entité', [
|
||||||
|
'entity_id' => $entityId
|
||||||
|
]);
|
||||||
|
throw new RuntimeException("Impossible de déterminer le département");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les départements prioritaires (entité + limitrophes)
|
||||||
|
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
|
||||||
|
|
||||||
|
// Log pour debug
|
||||||
|
$this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [
|
||||||
|
'departements_prioritaires' => implode(', ', $priorityDepts)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Utiliser les départements prioritaires pour la recherche
|
||||||
|
$departmentsTouched = [];
|
||||||
|
foreach ($priorityDepts as $deptCode) {
|
||||||
|
$departmentsTouched[] = ['code_dept' => $deptCode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le polygone SQL à partir des coordonnées
|
||||||
|
$polygonPoints = [];
|
||||||
|
foreach ($coordinates as $coord) {
|
||||||
|
if (!isset($coord[0]) || !isset($coord[1])) {
|
||||||
|
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
|
||||||
|
}
|
||||||
|
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fermer le polygone
|
||||||
|
$polygonPoints[] = $polygonPoints[0];
|
||||||
|
|
||||||
|
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||||
|
|
||||||
|
// Collecter les adresses de tous les départements touchés
|
||||||
|
$allAddresses = [];
|
||||||
|
|
||||||
|
foreach ($departmentsTouched as $dept) {
|
||||||
|
$deptCode = $dept['code_dept'];
|
||||||
|
$tableName = "cp" . $deptCode;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Requête pour récupérer les adresses dans le polygone pour ce département
|
||||||
|
$sql = "SELECT
|
||||||
|
id,
|
||||||
|
numero,
|
||||||
|
rue as voie,
|
||||||
|
cp as code_postal,
|
||||||
|
ville as commune,
|
||||||
|
gps_lat as latitude,
|
||||||
|
gps_lng as longitude,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
code_insee,
|
||||||
|
nom_ld,
|
||||||
|
ville_acheminement,
|
||||||
|
rue_afnor,
|
||||||
|
source,
|
||||||
|
certification,
|
||||||
|
:dept_code as departement
|
||||||
|
FROM `$tableName`
|
||||||
|
WHERE ST_Contains(
|
||||||
|
ST_GeomFromText(:polygon, 4326),
|
||||||
|
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
|
||||||
|
)
|
||||||
|
AND gps_lat != ''
|
||||||
|
AND gps_lng != ''";
|
||||||
|
|
||||||
|
$stmt = $this->addressesDb->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
'polygon' => $polygonString,
|
||||||
|
'dept_code' => $deptCode
|
||||||
|
]);
|
||||||
|
|
||||||
|
$addresses = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Ajouter les adresses à la collection globale
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$allAddresses[] = $address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log pour debug
|
||||||
|
$this->logService->info('[AddressService] Recherche dans table', [
|
||||||
|
'table' => $tableName,
|
||||||
|
'departement' => $deptCode,
|
||||||
|
'nb_adresses' => count($addresses)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log l'erreur mais continue avec les autres départements
|
||||||
|
$this->logService->error('[AddressService] Erreur SQL', [
|
||||||
|
'table' => $tableName,
|
||||||
|
'departement' => $deptCode,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'code' => $e->getCode()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logService->info('[AddressService] Fin recherche adresses', [
|
||||||
|
'total_adresses' => count($allAddresses)
|
||||||
|
]);
|
||||||
|
return $allAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les adresses dans un rayon autour d'un point
|
||||||
|
*
|
||||||
|
* @param float $latitude Latitude du centre
|
||||||
|
* @param float $longitude Longitude du centre
|
||||||
|
* @param float $radiusMeters Rayon en mètres
|
||||||
|
* @param int|null $entityId ID de l'entité (pour déterminer le département)
|
||||||
|
* @return array Array des adresses trouvées
|
||||||
|
*/
|
||||||
|
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array {
|
||||||
|
// Déterminer le département
|
||||||
|
$dept = $this->getDepartmentForEntity($entityId);
|
||||||
|
if (!$dept) {
|
||||||
|
throw new RuntimeException("Impossible de déterminer le département de l'entité");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nom de la table selon le département
|
||||||
|
$tableName = "cp" . $dept;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
|
||||||
|
$sql = "SELECT
|
||||||
|
id,
|
||||||
|
numero,
|
||||||
|
rue as voie,
|
||||||
|
cp as code_postal,
|
||||||
|
ville as commune,
|
||||||
|
gps_lat as latitude,
|
||||||
|
gps_lng as longitude,
|
||||||
|
ST_Distance_Sphere(
|
||||||
|
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
|
||||||
|
ST_GeomFromText(:point, 4326)
|
||||||
|
) as distance
|
||||||
|
FROM `$tableName`
|
||||||
|
WHERE ST_Distance_Sphere(
|
||||||
|
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
|
||||||
|
ST_GeomFromText(:point, 4326)
|
||||||
|
) <= :radius
|
||||||
|
AND gps_lat != ''
|
||||||
|
AND gps_lng != ''
|
||||||
|
ORDER BY distance";
|
||||||
|
|
||||||
|
$point = "POINT($longitude $latitude)";
|
||||||
|
|
||||||
|
$stmt = $this->addressesDb->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
'point' => $point,
|
||||||
|
'radius' => $radiusMeters
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte le nombre d'adresses dans un polygone
|
||||||
|
* Gère automatiquement les secteurs multi-départements
|
||||||
|
*
|
||||||
|
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
|
||||||
|
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
|
||||||
|
* @return int Nombre d'adresses
|
||||||
|
*/
|
||||||
|
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int {
|
||||||
|
// Si pas de connexion à la base d'adresses, retourner 0
|
||||||
|
if (!$this->addressesDb) {
|
||||||
|
error_log("AddressService: Pas de connexion à la base d'adresses, retour de 0 adresses");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($coordinates) < 3) {
|
||||||
|
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
|
||||||
|
}
|
||||||
|
|
||||||
|
// D'abord, déterminer tous les départements touchés par ce secteur
|
||||||
|
require_once __DIR__ . '/DepartmentBoundaryService.php';
|
||||||
|
$boundaryService = new \DepartmentBoundaryService();
|
||||||
|
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
|
||||||
|
|
||||||
|
if (empty($departmentsTouched)) {
|
||||||
|
// Si aucun département n'est trouvé, utiliser le département de l'entité
|
||||||
|
$dept = $this->getDepartmentForEntity($entityId);
|
||||||
|
if (!$dept) {
|
||||||
|
throw new RuntimeException("Impossible de déterminer le département");
|
||||||
|
}
|
||||||
|
$departmentsTouched = [['code_dept' => $dept]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le polygone SQL à partir des coordonnées
|
||||||
|
$polygonPoints = [];
|
||||||
|
foreach ($coordinates as $coord) {
|
||||||
|
if (!isset($coord[0]) || !isset($coord[1])) {
|
||||||
|
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
|
||||||
|
}
|
||||||
|
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fermer le polygone
|
||||||
|
$polygonPoints[] = $polygonPoints[0];
|
||||||
|
|
||||||
|
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||||
|
|
||||||
|
// Compter les adresses dans tous les départements touchés
|
||||||
|
$totalCount = 0;
|
||||||
|
|
||||||
|
foreach ($departmentsTouched as $dept) {
|
||||||
|
$deptCode = $dept['code_dept'];
|
||||||
|
$tableName = "cp" . $deptCode;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "SELECT COUNT(*) as count
|
||||||
|
FROM `$tableName`
|
||||||
|
WHERE ST_Contains(
|
||||||
|
ST_GeomFromText(:polygon, 4326),
|
||||||
|
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
|
||||||
|
)
|
||||||
|
AND gps_lat != ''
|
||||||
|
AND gps_lng != ''";
|
||||||
|
|
||||||
|
$stmt = $this->addressesDb->prepare($sql);
|
||||||
|
$stmt->execute(['polygon' => $polygonString]);
|
||||||
|
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
$deptCount = (int)$result['count'];
|
||||||
|
$totalCount += $deptCount;
|
||||||
|
|
||||||
|
// Log pour debug
|
||||||
|
error_log("Département $deptCode : $deptCount adresses comptées");
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log l'erreur mais continue avec les autres départements
|
||||||
|
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
api/src/Services/ApiService.php
Normal file → Executable file
0
api/src/Services/ApiService.php
Normal file → Executable file
304
api/src/Services/BackupEncryptionService.php
Executable file
304
api/src/Services/BackupEncryptionService.php
Executable file
@@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/LogService.php';
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de chiffrement et compression des sauvegardes
|
||||||
|
*
|
||||||
|
* Ce service gère le processus complet de sécurisation des backups JSON :
|
||||||
|
* 1. Compression GZIP pour réduire la taille
|
||||||
|
* 2. Chiffrement AES-256-CBC pour la sécurité
|
||||||
|
* 3. Déchiffrement et décompression pour la restauration
|
||||||
|
*/
|
||||||
|
class BackupEncryptionService {
|
||||||
|
private AppConfig $appConfig;
|
||||||
|
private string $encryptionKey;
|
||||||
|
private array $backupConfig;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->appConfig = AppConfig::getInstance();
|
||||||
|
$this->encryptionKey = $this->appConfig->getBackupEncryptionKey();
|
||||||
|
$this->backupConfig = $this->appConfig->getBackupConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chiffre et compresse les données JSON d'un backup
|
||||||
|
*
|
||||||
|
* @param string $jsonData Données JSON à sauvegarder
|
||||||
|
* @return string Données chiffrées et compressées en base64
|
||||||
|
* @throws Exception En cas d'erreur de compression ou chiffrement
|
||||||
|
*/
|
||||||
|
public function encryptBackup(string $jsonData): string {
|
||||||
|
try {
|
||||||
|
// Étape 1: Compression GZIP si activée
|
||||||
|
$dataToEncrypt = $jsonData;
|
||||||
|
if ($this->backupConfig['compression']) {
|
||||||
|
$compressed = gzencode($jsonData, $this->backupConfig['compression_level']);
|
||||||
|
if ($compressed === false) {
|
||||||
|
throw new Exception('Erreur lors de la compression GZIP');
|
||||||
|
}
|
||||||
|
$dataToEncrypt = $compressed;
|
||||||
|
|
||||||
|
LogService::log('Compression backup réussie', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'original_size' => strlen($jsonData),
|
||||||
|
'compressed_size' => strlen($compressed),
|
||||||
|
'compression_ratio' => round((1 - strlen($compressed) / strlen($jsonData)) * 100, 2) . '%'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 2: Génération d'un IV aléatoire pour AES-256-CBC
|
||||||
|
$ivLength = openssl_cipher_iv_length($this->backupConfig['cipher']);
|
||||||
|
$iv = openssl_random_pseudo_bytes($ivLength);
|
||||||
|
|
||||||
|
if ($iv === false || strlen($iv) !== $ivLength) {
|
||||||
|
throw new Exception('Erreur lors de la génération de l\'IV');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 3: Chiffrement AES-256-CBC
|
||||||
|
$encrypted = openssl_encrypt(
|
||||||
|
$dataToEncrypt,
|
||||||
|
$this->backupConfig['cipher'],
|
||||||
|
$this->encryptionKey,
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($encrypted === false) {
|
||||||
|
throw new Exception('Erreur lors du chiffrement AES-256');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 4: Concaténation IV + données chiffrées et encodage base64
|
||||||
|
$finalData = base64_encode($iv . $encrypted);
|
||||||
|
|
||||||
|
LogService::log('Chiffrement backup réussi', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'data_size' => strlen($dataToEncrypt),
|
||||||
|
'encrypted_size' => strlen($finalData),
|
||||||
|
'cipher' => $this->backupConfig['cipher']
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $finalData;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors du chiffrement du backup', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'data_size' => strlen($jsonData)
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déchiffre et décompresse les données d'un backup
|
||||||
|
*
|
||||||
|
* @param string $encryptedData Données chiffrées en base64
|
||||||
|
* @return string Données JSON originales
|
||||||
|
* @throws Exception En cas d'erreur de déchiffrement ou décompression
|
||||||
|
*/
|
||||||
|
public function decryptBackup(string $encryptedData): string {
|
||||||
|
try {
|
||||||
|
// Étape 1: Décodage base64
|
||||||
|
$rawData = base64_decode($encryptedData);
|
||||||
|
if ($rawData === false) {
|
||||||
|
throw new Exception('Erreur lors du décodage base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 2: Extraction de l'IV
|
||||||
|
$ivLength = openssl_cipher_iv_length($this->backupConfig['cipher']);
|
||||||
|
if (strlen($rawData) < $ivLength) {
|
||||||
|
throw new Exception('Données corrompues : taille insuffisante pour l\'IV');
|
||||||
|
}
|
||||||
|
|
||||||
|
$iv = substr($rawData, 0, $ivLength);
|
||||||
|
$encryptedContent = substr($rawData, $ivLength);
|
||||||
|
|
||||||
|
// Étape 3: Déchiffrement AES-256-CBC
|
||||||
|
$decrypted = openssl_decrypt(
|
||||||
|
$encryptedContent,
|
||||||
|
$this->backupConfig['cipher'],
|
||||||
|
$this->encryptionKey,
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($decrypted === false) {
|
||||||
|
throw new Exception('Erreur lors du déchiffrement : clé invalide ou données corrompues');
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Déchiffrement backup réussi', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'encrypted_size' => strlen($encryptedData),
|
||||||
|
'decrypted_size' => strlen($decrypted)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Étape 4: Décompression GZIP si les données sont compressées
|
||||||
|
$finalData = $decrypted;
|
||||||
|
if ($this->backupConfig['compression']) {
|
||||||
|
// Vérifier si les données sont bien compressées (magic number GZIP)
|
||||||
|
if (substr($decrypted, 0, 2) === "\x1f\x8b") {
|
||||||
|
$decompressed = gzdecode($decrypted);
|
||||||
|
if ($decompressed === false) {
|
||||||
|
throw new Exception('Erreur lors de la décompression GZIP');
|
||||||
|
}
|
||||||
|
$finalData = $decompressed;
|
||||||
|
|
||||||
|
LogService::log('Décompression backup réussie', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'compressed_size' => strlen($decrypted),
|
||||||
|
'decompressed_size' => strlen($decompressed)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Étape 5: Validation que le résultat est du JSON valide
|
||||||
|
$jsonTest = json_decode($finalData, true);
|
||||||
|
if ($jsonTest === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new Exception('Les données déchiffrées ne sont pas du JSON valide : ' . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $finalData;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors du déchiffrement du backup', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'encrypted_size' => strlen($encryptedData)
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un fichier de backup est chiffré
|
||||||
|
*
|
||||||
|
* @param string $filePath Chemin vers le fichier
|
||||||
|
* @return bool True si le fichier est chiffré
|
||||||
|
*/
|
||||||
|
public function isEncryptedBackup(string $filePath): bool {
|
||||||
|
return str_ends_with($filePath, '.json.gz.enc') || str_ends_with($filePath, '.enc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un fichier de backup est compressé (mais pas chiffré)
|
||||||
|
*
|
||||||
|
* @param string $filePath Chemin vers le fichier
|
||||||
|
* @return bool True si le fichier est compressé
|
||||||
|
*/
|
||||||
|
public function isCompressedBackup(string $filePath): bool {
|
||||||
|
return str_ends_with($filePath, '.json.gz') && !str_ends_with($filePath, '.enc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lit un fichier de backup en détectant automatiquement le format
|
||||||
|
*
|
||||||
|
* @param string $filePath Chemin vers le fichier de backup
|
||||||
|
* @return array Données JSON décodées
|
||||||
|
* @throws Exception En cas d'erreur de lecture ou format non supporté
|
||||||
|
*/
|
||||||
|
public function readBackupFile(string $filePath): array {
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
throw new Exception("Fichier de backup non trouvé : {$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileContent = file_get_contents($filePath);
|
||||||
|
if ($fileContent === false) {
|
||||||
|
throw new Exception("Impossible de lire le fichier : {$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fichier chiffré
|
||||||
|
if ($this->isEncryptedBackup($filePath)) {
|
||||||
|
$jsonContent = $this->decryptBackup($fileContent);
|
||||||
|
}
|
||||||
|
// Fichier compressé seulement
|
||||||
|
elseif ($this->isCompressedBackup($filePath)) {
|
||||||
|
$jsonContent = gzdecode($fileContent);
|
||||||
|
if ($jsonContent === false) {
|
||||||
|
throw new Exception('Erreur lors de la décompression du fichier');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fichier JSON brut
|
||||||
|
else {
|
||||||
|
$jsonContent = $fileContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Décodage JSON
|
||||||
|
$data = json_decode($jsonContent, true);
|
||||||
|
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new Exception('JSON invalide : ' . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Lecture backup réussie', [
|
||||||
|
'level' => 'info',
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => filesize($filePath),
|
||||||
|
'is_encrypted' => $this->isEncryptedBackup($filePath),
|
||||||
|
'is_compressed' => $this->isCompressedBackup($filePath)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la lecture du backup', [
|
||||||
|
'level' => 'error',
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le nom de fichier approprié selon la configuration
|
||||||
|
*
|
||||||
|
* @param int $operationId ID de l'opération
|
||||||
|
* @param string $timestamp Timestamp pour l'unicité
|
||||||
|
* @param string $type Type d'export (manual, auto, etc.)
|
||||||
|
* @return string Nom du fichier avec extension appropriée
|
||||||
|
*/
|
||||||
|
public function generateBackupFilename(int $operationId, string $timestamp, string $type = 'manual'): string {
|
||||||
|
$baseName = "backup_operation_{$operationId}_{$timestamp}";
|
||||||
|
|
||||||
|
if ($type !== 'manual') {
|
||||||
|
$baseName .= "_{$type}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension selon la configuration
|
||||||
|
$extension = '.json';
|
||||||
|
|
||||||
|
if ($this->backupConfig['compression']) {
|
||||||
|
$extension .= '.gz';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toujours chiffré
|
||||||
|
$extension .= '.enc';
|
||||||
|
|
||||||
|
return $baseName . $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les statistiques de compression et chiffrement
|
||||||
|
*
|
||||||
|
* @param string $originalJson JSON original
|
||||||
|
* @param string $finalData Données finales chiffrées
|
||||||
|
* @return array Statistiques détaillées
|
||||||
|
*/
|
||||||
|
public function getCompressionStats(string $originalJson, string $finalData): array {
|
||||||
|
$originalSize = strlen($originalJson);
|
||||||
|
$finalSize = strlen($finalData);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'original_size' => $originalSize,
|
||||||
|
'final_size' => $finalSize,
|
||||||
|
'size_reduction' => $originalSize - $finalSize,
|
||||||
|
'compression_ratio' => $originalSize > 0 ? round((1 - $finalSize / $originalSize) * 100, 2) : 0,
|
||||||
|
'is_compressed' => $this->backupConfig['compression'],
|
||||||
|
'is_encrypted' => true,
|
||||||
|
'cipher' => $this->backupConfig['cipher']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
249
api/src/Services/DepartmentBoundaryService.php
Normal file
249
api/src/Services/DepartmentBoundaryService.php
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class DepartmentBoundaryService {
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un polygone (secteur) est entièrement contenu dans un département
|
||||||
|
*
|
||||||
|
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
|
||||||
|
* @param string $departmentCode Code du département (22, 29, etc.)
|
||||||
|
* @return array ['is_contained' => bool, 'message' => string, 'intersecting_departments' => array]
|
||||||
|
*/
|
||||||
|
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array {
|
||||||
|
if (count($sectorCoordinates) < 3) {
|
||||||
|
return [
|
||||||
|
'is_contained' => false,
|
||||||
|
'message' => 'Un secteur doit avoir au moins 3 points',
|
||||||
|
'intersecting_departments' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le polygone du secteur
|
||||||
|
$polygonPoints = [];
|
||||||
|
foreach ($sectorCoordinates as $coord) {
|
||||||
|
if (!isset($coord[0]) || !isset($coord[1])) {
|
||||||
|
return [
|
||||||
|
'is_contained' => false,
|
||||||
|
'message' => 'Coordonnées invalides',
|
||||||
|
'intersecting_departments' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fermer le polygone
|
||||||
|
$polygonPoints[] = $polygonPoints[0];
|
||||||
|
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Vérifier si le secteur est entièrement dans le département cible
|
||||||
|
$sql = "SELECT
|
||||||
|
code_dept,
|
||||||
|
nom_dept,
|
||||||
|
ST_Contains(contour, ST_GeomFromText(:sector_polygon, 4326)) as is_contained,
|
||||||
|
ST_Intersects(contour, ST_GeomFromText(:sector_polygon, 4326)) as intersects
|
||||||
|
FROM x_departements
|
||||||
|
WHERE code = :dept_code
|
||||||
|
AND contour IS NOT NULL";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
'sector_polygon' => $sectorPolygon,
|
||||||
|
'dept_code' => $departmentCode
|
||||||
|
]);
|
||||||
|
|
||||||
|
$targetDept = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$targetDept) {
|
||||||
|
return [
|
||||||
|
'is_contained' => false,
|
||||||
|
'message' => "Le département $departmentCode n'a pas de contour défini",
|
||||||
|
'intersecting_departments' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Si le secteur n'est pas entièrement contenu, trouver tous les départements qu'il touche
|
||||||
|
if (!$targetDept['is_contained']) {
|
||||||
|
$sql = "SELECT
|
||||||
|
code_dept,
|
||||||
|
nom_dept,
|
||||||
|
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
|
||||||
|
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
|
||||||
|
FROM x_departements_contours
|
||||||
|
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
|
||||||
|
ORDER BY percentage_overlap DESC";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
'sector_polygon1' => $sectorPolygon,
|
||||||
|
'sector_polygon2' => $sectorPolygon,
|
||||||
|
'sector_polygon3' => $sectorPolygon
|
||||||
|
]);
|
||||||
|
|
||||||
|
$intersectingDepts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Formater le message
|
||||||
|
$deptsList = array_map(function($d) {
|
||||||
|
return sprintf("%s (%s) : %.1f%%",
|
||||||
|
$d['nom_dept'],
|
||||||
|
$d['code_dept'],
|
||||||
|
$d['percentage_overlap']
|
||||||
|
);
|
||||||
|
}, $intersectingDepts);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_contained' => false,
|
||||||
|
'message' => "Le secteur déborde du département {$targetDept['nom_dept']}. Il est à cheval sur : " . implode(', ', $deptsList),
|
||||||
|
'intersecting_departments' => $intersectingDepts
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_contained' => true,
|
||||||
|
'message' => "Le secteur est entièrement contenu dans le département {$targetDept['nom_dept']}",
|
||||||
|
'intersecting_departments' => [$targetDept]
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
throw new RuntimeException("Erreur lors de la vérification des limites départementales : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les départements qui intersectent avec un secteur
|
||||||
|
*
|
||||||
|
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
|
||||||
|
* @return array Liste des départements avec leur pourcentage de recouvrement
|
||||||
|
*/
|
||||||
|
public function getDepartmentsForSector(array $sectorCoordinates): array {
|
||||||
|
if (count($sectorCoordinates) < 3) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le polygone du secteur
|
||||||
|
$polygonPoints = [];
|
||||||
|
foreach ($sectorCoordinates as $coord) {
|
||||||
|
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
|
||||||
|
}
|
||||||
|
$polygonPoints[] = $polygonPoints[0];
|
||||||
|
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "SELECT
|
||||||
|
code_dept,
|
||||||
|
nom_dept,
|
||||||
|
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
|
||||||
|
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
|
||||||
|
FROM x_departements_contours
|
||||||
|
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
|
||||||
|
ORDER BY percentage_overlap DESC";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
'sector_polygon1' => $sectorPolygon,
|
||||||
|
'sector_polygon2' => $sectorPolygon,
|
||||||
|
'sector_polygon3' => $sectorPolygon
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
throw new RuntimeException("Erreur lors de la recherche des départements : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si les contours des départements sont chargés
|
||||||
|
*
|
||||||
|
* @return array ['loaded' => bool, 'count' => int, 'missing' => array]
|
||||||
|
*/
|
||||||
|
public function checkDepartmentContoursStatus(): array {
|
||||||
|
try {
|
||||||
|
// Compter les départements avec contours
|
||||||
|
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
$count = $stmt->fetch()['count'];
|
||||||
|
|
||||||
|
// Récupérer la liste des départements utilisés dans les entités
|
||||||
|
$sql = "SELECT DISTINCT departement
|
||||||
|
FROM entites
|
||||||
|
WHERE departement IS NOT NULL
|
||||||
|
ORDER BY departement";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
$usedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Vérifier lesquels ont des contours
|
||||||
|
$sql = "SELECT code_dept FROM x_departements_contours WHERE code_dept IN ('" . implode("','", $usedDepts) . "')";
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
$loadedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$missingDepts = array_diff($usedDepts, $loadedDepts);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'loaded' => count($missingDepts) === 0,
|
||||||
|
'count' => $count,
|
||||||
|
'total_used' => count($usedDepts),
|
||||||
|
'missing' => array_values($missingDepts)
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
return [
|
||||||
|
'loaded' => false,
|
||||||
|
'count' => 0,
|
||||||
|
'total_used' => 0,
|
||||||
|
'missing' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les départements limitrophes d'un département donné
|
||||||
|
*
|
||||||
|
* @param string $departmentCode Code du département
|
||||||
|
* @return array Array des codes départements limitrophes
|
||||||
|
*/
|
||||||
|
public function getAdjacentDepartments(string $departmentCode): array {
|
||||||
|
try {
|
||||||
|
$sql = "SELECT dept_limitrophes FROM x_departements WHERE code = :dept_code AND chk_active = 1";
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute(['dept_code' => $departmentCode]);
|
||||||
|
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result || empty($result['dept_limitrophes'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir la chaîne CSV en array
|
||||||
|
return array_map('trim', explode(',', $result['dept_limitrophes']));
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Erreur lors de la récupération des départements limitrophes : " . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les départements à prioriser pour la recherche
|
||||||
|
* (département de l'entité + ses limitrophes)
|
||||||
|
*
|
||||||
|
* @param string $entityDepartment Code du département de l'entité
|
||||||
|
* @return array Array des codes départements à prioriser
|
||||||
|
*/
|
||||||
|
public function getPriorityDepartments(string $entityDepartment): array {
|
||||||
|
$priorityDepts = [$entityDepartment]; // Commencer par le département de l'entité
|
||||||
|
|
||||||
|
// Ajouter les départements limitrophes
|
||||||
|
$adjacentDepts = $this->getAdjacentDepartments($entityDepartment);
|
||||||
|
$priorityDepts = array_merge($priorityDepts, $adjacentDepts);
|
||||||
|
|
||||||
|
// Retourner sans doublons
|
||||||
|
return array_unique($priorityDepts);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
api/src/Services/EmailTemplates.php
Normal file → Executable file
0
api/src/Services/EmailTemplates.php
Normal file → Executable file
933
api/src/Services/ExportService.php
Executable file
933
api/src/Services/ExportService.php
Executable file
@@ -0,0 +1,933 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xls;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../Services/FileService.php';
|
||||||
|
|
||||||
|
|
||||||
|
class ExportService {
|
||||||
|
private \PDO $db;
|
||||||
|
private FileService $fileService;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
$this->fileService = new FileService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un export Excel complet d'une opération
|
||||||
|
*
|
||||||
|
* @param int $operationId ID de l'opération
|
||||||
|
* @param int $entiteId ID de l'entité
|
||||||
|
* @param int|null $userId Filtrer par utilisateur (optionnel)
|
||||||
|
* @return array Informations du fichier généré
|
||||||
|
*/
|
||||||
|
public function generateExcelExport(int $operationId, int $entiteId, ?int $userId = null): array {
|
||||||
|
try {
|
||||||
|
// Récupérer les données de l'opération
|
||||||
|
$operationData = $this->getOperationData($operationId, $entiteId);
|
||||||
|
if (!$operationData) {
|
||||||
|
throw new Exception('Opération non trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le dossier de destination
|
||||||
|
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/excel");
|
||||||
|
|
||||||
|
LogService::log('exportDir', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'exportDir' => $exportDir,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Générer le nom du fichier
|
||||||
|
$timestamp = date('Ymd-His');
|
||||||
|
$userSuffix = $userId ? "-user{$userId}" : '';
|
||||||
|
$filename = "geosector-export-{$operationId}{$userSuffix}-{$timestamp}.xlsx";
|
||||||
|
$filepath = $exportDir . '/' . $filename;
|
||||||
|
|
||||||
|
// Créer le spreadsheet
|
||||||
|
$spreadsheet = new PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||||
|
|
||||||
|
// Insérer les données
|
||||||
|
$this->createPassagesSheet($spreadsheet, $operationId, $userId);
|
||||||
|
$this->createUsersSheet($spreadsheet, $operationId);
|
||||||
|
$this->createSectorsSheet($spreadsheet, $operationId);
|
||||||
|
$this->createUserSectorsSheet($spreadsheet, $operationId);
|
||||||
|
|
||||||
|
// Supprimer la feuille par défaut (Worksheet) qui est créée automatiquement
|
||||||
|
$defaultSheet = $spreadsheet->getSheetByName('Worksheet');
|
||||||
|
if ($defaultSheet) {
|
||||||
|
$spreadsheet->removeSheetByIndex($spreadsheet->getIndex($defaultSheet));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Essayer d'abord le writer XLSX, sinon utiliser CSV
|
||||||
|
try {
|
||||||
|
$writer = new Xls($spreadsheet);
|
||||||
|
$writer->save($filepath);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Si XLSX échoue, utiliser CSV comme fallback
|
||||||
|
$csvPath = str_replace('.xlsx', '.csv', $filepath);
|
||||||
|
$csvWriter = new Csv($spreadsheet);
|
||||||
|
$csvWriter->setDelimiter(';');
|
||||||
|
$csvWriter->setEnclosure('"');
|
||||||
|
$csvWriter->save($csvPath);
|
||||||
|
|
||||||
|
// Mettre à jour les variables pour le CSV
|
||||||
|
$filepath = $csvPath;
|
||||||
|
$filename = str_replace('.xlsx', '.csv', $filename);
|
||||||
|
|
||||||
|
LogService::log('Fallback vers CSV car XLSX a échoué', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'operationId' => $operationId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer les permissions sur le fichier
|
||||||
|
$this->fileService->setFilePermissions($filepath);
|
||||||
|
|
||||||
|
// Déterminer le type de fichier réellement généré
|
||||||
|
$fileType = str_ends_with($filename, '.csv') ? 'csv' : 'xlsx';
|
||||||
|
|
||||||
|
// Enregistrer en base de données
|
||||||
|
$mediaId = $this->fileService->saveToMediasTable($entiteId, $operationId, $filename, $filepath, $fileType, 'Export Excel opération - ' . $operationData['libelle']);
|
||||||
|
|
||||||
|
LogService::log('Export Excel généré', [
|
||||||
|
'level' => 'info',
|
||||||
|
'operationId' => $operationId,
|
||||||
|
'entiteId' => $entiteId,
|
||||||
|
'path' => $exportDir,
|
||||||
|
'filename' => $filename,
|
||||||
|
'mediaId' => $mediaId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $mediaId,
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => str_replace(getcwd() . '/', '', $filepath),
|
||||||
|
'size' => filesize($filepath),
|
||||||
|
'type' => 'excel'
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération de l\'export Excel', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'operationId' => $operationId,
|
||||||
|
'entiteId' => $entiteId
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un export JSON complet d'une opération (chiffré et compressé)
|
||||||
|
*
|
||||||
|
* @param int $operationId ID de l'opération
|
||||||
|
* @param int $entiteId ID de l'entité
|
||||||
|
* @param string $type Type d'export (auto, manual)
|
||||||
|
* @return array Informations du fichier généré
|
||||||
|
*/
|
||||||
|
public function generateJsonExport(int $operationId, int $entiteId, string $type = 'manual'): array {
|
||||||
|
try {
|
||||||
|
// Récupérer toutes les données de l'opération
|
||||||
|
$exportData = $this->collectOperationData($operationId, $entiteId);
|
||||||
|
|
||||||
|
// Créer le dossier de destination
|
||||||
|
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/json");
|
||||||
|
|
||||||
|
// Initialiser le service de chiffrement
|
||||||
|
$backupService = new BackupEncryptionService();
|
||||||
|
|
||||||
|
// Générer le JSON original
|
||||||
|
$jsonData = json_encode($exportData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
// Chiffrer et compresser les données
|
||||||
|
$encryptedData = $backupService->encryptBackup($jsonData);
|
||||||
|
|
||||||
|
// Générer le nom du fichier avec extension appropriée
|
||||||
|
$timestamp = date('Ymd-His');
|
||||||
|
$filename = $backupService->generateBackupFilename($operationId, $timestamp, $type);
|
||||||
|
$filepath = $exportDir . '/' . $filename;
|
||||||
|
|
||||||
|
// Sauvegarder le fichier chiffré
|
||||||
|
file_put_contents($filepath, $encryptedData);
|
||||||
|
|
||||||
|
// Appliquer les permissions sur le fichier
|
||||||
|
$this->fileService->setFilePermissions($filepath);
|
||||||
|
|
||||||
|
// Obtenir les statistiques de compression
|
||||||
|
$stats = $backupService->getCompressionStats($jsonData, $encryptedData);
|
||||||
|
|
||||||
|
// Enregistrer en base de données avec le bon type MIME
|
||||||
|
$mediaId = $this->fileService->saveToMediasTable(
|
||||||
|
$entiteId,
|
||||||
|
$operationId,
|
||||||
|
$filename,
|
||||||
|
$filepath,
|
||||||
|
'enc',
|
||||||
|
"Sauvegarde chiffrée opération - {$type} - " . $exportData['operation']['libelle'],
|
||||||
|
'backup'
|
||||||
|
);
|
||||||
|
|
||||||
|
LogService::log('Export JSON chiffré généré', [
|
||||||
|
'level' => 'info',
|
||||||
|
'operationId' => $operationId,
|
||||||
|
'entiteId' => $entiteId,
|
||||||
|
'filename' => $filename,
|
||||||
|
'type' => $type,
|
||||||
|
'mediaId' => $mediaId,
|
||||||
|
'original_size' => $stats['original_size'],
|
||||||
|
'final_size' => $stats['final_size'],
|
||||||
|
'compression_ratio' => $stats['compression_ratio'] . '%',
|
||||||
|
'is_compressed' => $stats['is_compressed'],
|
||||||
|
'cipher' => $stats['cipher']
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $mediaId,
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => str_replace(getcwd() . '/', '', $filepath),
|
||||||
|
'size' => filesize($filepath),
|
||||||
|
'type' => 'encrypted_json',
|
||||||
|
'compression_stats' => $stats
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
LogService::log('Erreur lors de la génération de l\'export JSON chiffré', [
|
||||||
|
'level' => 'error',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'operationId' => $operationId,
|
||||||
|
'entiteId' => $entiteId
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée la feuille des passages
|
||||||
|
*/
|
||||||
|
private function createPassagesSheet(Spreadsheet $spreadsheet, int $operationId, ?int $userId = null): void {
|
||||||
|
$sheet = $spreadsheet->createSheet();
|
||||||
|
$sheet->setTitle('Passages');
|
||||||
|
|
||||||
|
// En-têtes
|
||||||
|
$headers = [
|
||||||
|
'ID_Passage',
|
||||||
|
'Date',
|
||||||
|
'Heure',
|
||||||
|
'Prénom',
|
||||||
|
'Nom',
|
||||||
|
'Tournée',
|
||||||
|
'Type',
|
||||||
|
'N°',
|
||||||
|
'Bis',
|
||||||
|
'Rue',
|
||||||
|
'Ville',
|
||||||
|
'Habitat',
|
||||||
|
'Donateur',
|
||||||
|
'Email',
|
||||||
|
'Tél',
|
||||||
|
'Montant',
|
||||||
|
'Règlement',
|
||||||
|
'Remarque',
|
||||||
|
'FK_User',
|
||||||
|
'FK_Sector',
|
||||||
|
'FK_Operation'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Écrire les en-têtes
|
||||||
|
$sheet->fromArray([$headers], null, 'A1');
|
||||||
|
|
||||||
|
// Récupérer les données des passages
|
||||||
|
$sql = '
|
||||||
|
SELECT
|
||||||
|
p.id, p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
|
||||||
|
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||||
|
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||||
|
p.fk_user, p.fk_sector, p.fk_operation,
|
||||||
|
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||||
|
xtr.libelle as reglement_libelle
|
||||||
|
FROM ope_pass p
|
||||||
|
LEFT JOIN users u ON u.id = p.fk_user
|
||||||
|
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||||
|
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||||
|
';
|
||||||
|
|
||||||
|
$params = [$operationId];
|
||||||
|
if ($userId) {
|
||||||
|
$sql .= ' AND p.fk_user = ?';
|
||||||
|
$params[] = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' ORDER BY p.passed_at DESC';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Remplir les données
|
||||||
|
$row = 2;
|
||||||
|
foreach ($passages as $passage) {
|
||||||
|
$dateEve = $passage['passed_at'] ? date('d/m/Y', strtotime($passage['passed_at'])) : '';
|
||||||
|
$heureEve = $passage['passed_at'] ? date('H:i', strtotime($passage['passed_at'])) : '';
|
||||||
|
|
||||||
|
// Déchiffrer les données
|
||||||
|
$donateur = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
$email = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : '';
|
||||||
|
$phone = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : '';
|
||||||
|
$userName = ApiService::decryptData($passage['user_name']);
|
||||||
|
|
||||||
|
// Type de passage
|
||||||
|
$typeLabels = [
|
||||||
|
1 => 'Effectué',
|
||||||
|
2 => 'A finaliser',
|
||||||
|
3 => 'Refusé',
|
||||||
|
4 => 'Don',
|
||||||
|
9 => 'Habitat vide'
|
||||||
|
];
|
||||||
|
$typeLabel = $typeLabels[$passage['fk_type']] ?? $passage['fk_type'];
|
||||||
|
|
||||||
|
// Habitat
|
||||||
|
$habitat = $passage['fk_habitat'] == 1 ? 'Individuel' :
|
||||||
|
"Etage {$passage['niveau']} - Appt {$passage['appt']}";
|
||||||
|
|
||||||
|
$rowData = [
|
||||||
|
$passage['id'],
|
||||||
|
$dateEve,
|
||||||
|
$heureEve,
|
||||||
|
$passage['user_first_name'],
|
||||||
|
$userName,
|
||||||
|
$passage['sect_name'],
|
||||||
|
$typeLabel,
|
||||||
|
$passage['numero'],
|
||||||
|
$passage['rue_bis'],
|
||||||
|
$passage['rue'],
|
||||||
|
$passage['ville'],
|
||||||
|
$habitat,
|
||||||
|
$donateur,
|
||||||
|
$email,
|
||||||
|
$phone,
|
||||||
|
$passage['montant'],
|
||||||
|
$passage['reglement_libelle'],
|
||||||
|
$passage['remarque'],
|
||||||
|
$passage['fk_user'],
|
||||||
|
$passage['fk_sector'],
|
||||||
|
$passage['fk_operation']
|
||||||
|
];
|
||||||
|
|
||||||
|
$sheet->fromArray([$rowData], null, "A{$row}");
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-ajuster les colonnes
|
||||||
|
foreach (range('A', 'T') as $col) {
|
||||||
|
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée la feuille des utilisateurs
|
||||||
|
*/
|
||||||
|
private function createUsersSheet(Spreadsheet $spreadsheet, int $operationId): void {
|
||||||
|
$sheet = $spreadsheet->createSheet();
|
||||||
|
$sheet->setTitle('Utilisateurs');
|
||||||
|
|
||||||
|
// En-têtes
|
||||||
|
$headers = [
|
||||||
|
'ID_User',
|
||||||
|
'Nom',
|
||||||
|
'Prénom',
|
||||||
|
'Email',
|
||||||
|
'Téléphone',
|
||||||
|
'Mobile',
|
||||||
|
'Rôle',
|
||||||
|
'Date_création',
|
||||||
|
'Actif',
|
||||||
|
'FK_Entite'
|
||||||
|
];
|
||||||
|
|
||||||
|
$sheet->fromArray([$headers], null, 'A1');
|
||||||
|
|
||||||
|
// Récupérer les utilisateurs de l'opération
|
||||||
|
$sql = '
|
||||||
|
SELECT DISTINCT
|
||||||
|
u.id, u.encrypted_name, u.first_name, u.encrypted_email,
|
||||||
|
u.encrypted_phone, u.encrypted_mobile, u.fk_role, u.created_at,
|
||||||
|
u.chk_active, u.fk_entite,
|
||||||
|
r.libelle as role_libelle
|
||||||
|
FROM users u
|
||||||
|
INNER JOIN ope_users ou ON ou.fk_user = u.id
|
||||||
|
LEFT JOIN x_users_roles r ON r.id = u.fk_role
|
||||||
|
WHERE ou.fk_operation = ? AND ou.chk_active = 1
|
||||||
|
ORDER BY u.encrypted_name
|
||||||
|
';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$row = 2;
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$rowData = [
|
||||||
|
$user['id'],
|
||||||
|
ApiService::decryptData($user['encrypted_name']),
|
||||||
|
$user['first_name'],
|
||||||
|
!empty($user['encrypted_email']) ? ApiService::decryptSearchableData($user['encrypted_email']) : '',
|
||||||
|
!empty($user['encrypted_phone']) ? ApiService::decryptData($user['encrypted_phone']) : '',
|
||||||
|
!empty($user['encrypted_mobile']) ? ApiService::decryptData($user['encrypted_mobile']) : '',
|
||||||
|
$user['role_libelle'],
|
||||||
|
date('d/m/Y H:i', strtotime($user['created_at'])),
|
||||||
|
$user['chk_active'] ? 'Oui' : 'Non',
|
||||||
|
$user['fk_entite']
|
||||||
|
];
|
||||||
|
|
||||||
|
$sheet->fromArray([$rowData], null, "A{$row}");
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (range('A', 'J') as $col) {
|
||||||
|
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée la feuille des secteurs
|
||||||
|
*/
|
||||||
|
private function createSectorsSheet(Spreadsheet $spreadsheet, int $operationId): void {
|
||||||
|
$sheet = $spreadsheet->createSheet();
|
||||||
|
$sheet->setTitle('Secteurs');
|
||||||
|
|
||||||
|
$headers = ['ID_Sector', 'Libellé', 'Couleur', 'Date_création', 'Actif', 'FK_Operation'];
|
||||||
|
$sheet->fromArray([$headers], null, 'A1');
|
||||||
|
|
||||||
|
$sql = '
|
||||||
|
SELECT id, libelle, color, created_at, chk_active, fk_operation
|
||||||
|
FROM ope_sectors
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1
|
||||||
|
ORDER BY libelle
|
||||||
|
';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
$sectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$row = 2;
|
||||||
|
foreach ($sectors as $sector) {
|
||||||
|
$rowData = [
|
||||||
|
$sector['id'],
|
||||||
|
$sector['libelle'],
|
||||||
|
$sector['color'],
|
||||||
|
date('d/m/Y H:i', strtotime($sector['created_at'])),
|
||||||
|
$sector['chk_active'] ? 'Oui' : 'Non',
|
||||||
|
$sector['fk_operation']
|
||||||
|
];
|
||||||
|
|
||||||
|
$sheet->fromArray([$rowData], null, "A{$row}");
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (range('A', 'F') as $col) {
|
||||||
|
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée la feuille des relations secteurs-utilisateurs
|
||||||
|
*/
|
||||||
|
private function createUserSectorsSheet(Spreadsheet $spreadsheet, int $operationId): void {
|
||||||
|
$sheet = $spreadsheet->createSheet();
|
||||||
|
$sheet->setTitle('Secteurs-Utilisateurs');
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'ID_Relation',
|
||||||
|
'FK_Sector',
|
||||||
|
'Nom_Secteur',
|
||||||
|
'FK_User',
|
||||||
|
'Nom_Utilisateur',
|
||||||
|
'Date_assignation',
|
||||||
|
'FK_Operation'
|
||||||
|
];
|
||||||
|
$sheet->fromArray([$headers], null, 'A1');
|
||||||
|
|
||||||
|
$sql = '
|
||||||
|
SELECT
|
||||||
|
ous.id, ous.fk_sector, ous.fk_user, ous.created_at, ous.fk_operation,
|
||||||
|
s.libelle as sector_name,
|
||||||
|
u.encrypted_name as user_name, u.first_name
|
||||||
|
FROM ope_users_sectors ous
|
||||||
|
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
|
||||||
|
INNER JOIN users u ON u.id = ous.fk_user
|
||||||
|
WHERE ous.fk_operation = ? AND ous.chk_active = 1
|
||||||
|
ORDER BY s.libelle, u.encrypted_name
|
||||||
|
';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
$userSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$row = 2;
|
||||||
|
foreach ($userSectors as $us) {
|
||||||
|
$userName = ApiService::decryptData($us['user_name']);
|
||||||
|
$fullUserName = $us['first_name'] ? $us['first_name'] . ' ' . $userName : $userName;
|
||||||
|
|
||||||
|
$rowData = [
|
||||||
|
$us['id'],
|
||||||
|
$us['fk_sector'],
|
||||||
|
$us['sector_name'],
|
||||||
|
$us['fk_user'],
|
||||||
|
$fullUserName,
|
||||||
|
date('d/m/Y H:i', strtotime($us['created_at'])),
|
||||||
|
$us['fk_operation']
|
||||||
|
];
|
||||||
|
|
||||||
|
$sheet->fromArray([$rowData], null, "A{$row}");
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (range('A', 'G') as $col) {
|
||||||
|
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collecte toutes les données d'une opération pour l'export JSON
|
||||||
|
*/
|
||||||
|
private function collectOperationData(int $operationId, int $entiteId): array {
|
||||||
|
// Métadonnées de l'export
|
||||||
|
$exportData = [
|
||||||
|
'export_metadata' => [
|
||||||
|
'version' => '1.0',
|
||||||
|
'export_date' => date('c'),
|
||||||
|
'source_entite_id' => $entiteId,
|
||||||
|
'export_type' => 'full_operation'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Données de l'opération
|
||||||
|
$exportData['operation'] = $this->getOperationData($operationId, $entiteId);
|
||||||
|
|
||||||
|
// Utilisateurs de l'opération
|
||||||
|
$exportData['users'] = $this->getOperationUsers($operationId);
|
||||||
|
|
||||||
|
// Secteurs de l'opération
|
||||||
|
$exportData['sectors'] = $this->getOperationSectors($operationId);
|
||||||
|
|
||||||
|
// Passages de l'opération
|
||||||
|
$exportData['passages'] = $this->getOperationPassages($operationId);
|
||||||
|
|
||||||
|
// Relations utilisateurs-secteurs
|
||||||
|
$exportData['user_sectors'] = $this->getOperationUserSectors($operationId);
|
||||||
|
|
||||||
|
return $exportData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données de l'opération
|
||||||
|
*/
|
||||||
|
private function getOperationData(int $operationId, int $entiteId): ?array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM operations
|
||||||
|
WHERE id = ? AND fk_entite = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId, $entiteId]);
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les utilisateurs de l'opération
|
||||||
|
*/
|
||||||
|
private function getOperationUsers(int $operationId): array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT DISTINCT u.*
|
||||||
|
FROM users u
|
||||||
|
INNER JOIN ope_users ou ON ou.fk_user = u.id
|
||||||
|
WHERE ou.fk_operation = ? AND ou.chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les secteurs de l'opération
|
||||||
|
*/
|
||||||
|
private function getOperationSectors(int $operationId): array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM ope_sectors
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les passages de l'opération
|
||||||
|
*/
|
||||||
|
private function getOperationPassages(int $operationId): array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM ope_pass
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les relations utilisateurs-secteurs
|
||||||
|
*/
|
||||||
|
private function getOperationUserSectors(int $operationId): array {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM ope_users_sectors
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$operationId]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données des passages dans un format simple pour l'export Excel
|
||||||
|
* (inspiré de l'ancienne version qui fonctionne)
|
||||||
|
*/
|
||||||
|
private function getSimplePassagesData(int $operationId, ?int $userId = null): array {
|
||||||
|
// En-têtes (comme dans l'ancienne version)
|
||||||
|
$aData = [];
|
||||||
|
$aData[] = [
|
||||||
|
'Date',
|
||||||
|
'Heure',
|
||||||
|
'Prenom',
|
||||||
|
'Nom',
|
||||||
|
'Tournee',
|
||||||
|
'Type',
|
||||||
|
'N°',
|
||||||
|
'Rue',
|
||||||
|
'Ville',
|
||||||
|
'Habitat',
|
||||||
|
'Donateur',
|
||||||
|
'Email',
|
||||||
|
'Tel',
|
||||||
|
'Montant',
|
||||||
|
'Reglement',
|
||||||
|
'Remarque'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Récupérer les données des passages
|
||||||
|
$sql = '
|
||||||
|
SELECT
|
||||||
|
p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
|
||||||
|
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
|
||||||
|
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
|
||||||
|
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
|
||||||
|
xtr.libelle as reglement_libelle
|
||||||
|
FROM ope_pass p
|
||||||
|
LEFT JOIN users u ON u.id = p.fk_user
|
||||||
|
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
|
||||||
|
WHERE p.fk_operation = ? AND p.chk_active = 1
|
||||||
|
';
|
||||||
|
|
||||||
|
$params = [$operationId];
|
||||||
|
if ($userId) {
|
||||||
|
$sql .= ' AND p.fk_user = ?';
|
||||||
|
$params[] = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' ORDER BY p.passed_at DESC';
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Traiter les données comme dans l'ancienne version
|
||||||
|
foreach ($passages as $p) {
|
||||||
|
// Type de passage
|
||||||
|
switch ($p["fk_type"]) {
|
||||||
|
case 1:
|
||||||
|
$ptype = "Effectué";
|
||||||
|
$preglement = $p["reglement_libelle"];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$ptype = "A finaliser";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
$ptype = "Refusé";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
$ptype = "Don";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
$ptype = "Habitat vide";
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$ptype = $p["fk_type"];
|
||||||
|
$preglement = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Habitat
|
||||||
|
if ($p["fk_habitat"] == 1) {
|
||||||
|
$phabitat = "Individuel";
|
||||||
|
} else {
|
||||||
|
$phabitat = "Etage " . $p["niveau"] . " - Appt " . $p["appt"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
$dateEve = $p["passed_at"] ? date("d/m/Y", strtotime($p["passed_at"])) : "";
|
||||||
|
$heureEve = $p["passed_at"] ? date("H:i", strtotime($p["passed_at"])) : "";
|
||||||
|
|
||||||
|
// Déchiffrer les données
|
||||||
|
$donateur = ApiService::decryptData($p["encrypted_name"]);
|
||||||
|
$email = !empty($p["encrypted_email"]) ? ApiService::decryptSearchableData($p["encrypted_email"]) : "";
|
||||||
|
$phone = !empty($p["encrypted_phone"]) ? ApiService::decryptData($p["encrypted_phone"]) : "";
|
||||||
|
$userName = ApiService::decryptData($p["user_name"]);
|
||||||
|
|
||||||
|
// Nettoyer les données (comme dans l'ancienne version)
|
||||||
|
$nom = str_replace("/", "-", $userName);
|
||||||
|
$tournee = str_replace("/", "-", $p["sect_name"]);
|
||||||
|
|
||||||
|
$aData[] = [
|
||||||
|
$dateEve,
|
||||||
|
$heureEve,
|
||||||
|
$p["user_first_name"],
|
||||||
|
$nom,
|
||||||
|
$tournee,
|
||||||
|
$ptype,
|
||||||
|
$p["numero"] . $p["rue_bis"],
|
||||||
|
$p["rue"],
|
||||||
|
$p["ville"],
|
||||||
|
$phabitat,
|
||||||
|
$donateur,
|
||||||
|
$email,
|
||||||
|
$phone,
|
||||||
|
$p["montant"],
|
||||||
|
$preglement,
|
||||||
|
$p["remarque"]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure une opération à partir d'un backup chiffré
|
||||||
|
*
|
||||||
|
* @param string $backupFilePath Chemin vers le fichier de backup
|
||||||
|
* @param int $targetEntiteId ID de l'entité cible (pour restauration cross-entité)
|
||||||
|
* @return array Résultat de la restauration
|
||||||
|
* @throws Exception En cas d'erreur de restauration
|
||||||
|
*/
|
||||||
|
public function restoreFromBackup(string $backupFilePath, int $targetEntiteId): array {
|
||||||
|
try {
|
||||||
|
// Initialiser le service de chiffrement
|
||||||
|
$backupService = new BackupEncryptionService();
|
||||||
|
|
||||||
|
// Lire et déchiffrer le backup
|
||||||
|
$backupData = $backupService->readBackupFile($backupFilePath);
|
||||||
|
|
||||||
|
// Valider la structure du backup
|
||||||
|
if (!isset($backupData['operation']) || !isset($backupData['export_metadata'])) {
|
||||||
|
throw new Exception('Structure de backup invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationData = $backupData['operation'];
|
||||||
|
$originalEntiteId = $backupData['export_metadata']['source_entite_id'];
|
||||||
|
|
||||||
|
// Commencer la transaction
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
// Créer la nouvelle opération
|
||||||
|
$newOperationId = $this->restoreOperation($operationData, $targetEntiteId);
|
||||||
|
|
||||||
|
// Restaurer les utilisateurs (si même entité)
|
||||||
|
if ($targetEntiteId === $originalEntiteId && isset($backupData['users'])) {
|
||||||
|
$this->restoreUsers($backupData['users'], $newOperationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaurer les secteurs
|
||||||
|
if (isset($backupData['sectors'])) {
|
||||||
|
$this->restoreSectors($backupData['sectors'], $newOperationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaurer les relations utilisateurs-secteurs
|
||||||
|
if (isset($backupData['user_sectors'])) {
|
||||||
|
$this->restoreUserSectors($backupData['user_sectors'], $newOperationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaurer les passages
|
||||||
|
if (isset($backupData['passages'])) {
|
||||||
|
$this->restorePassages($backupData['passages'], $newOperationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
|
||||||
|
LogService::log('Restauration de backup réussie', [
|
||||||
|
'level' => 'info',
|
||||||
|
'backup_file' => $backupFilePath,
|
||||||
|
'original_operation_id' => $operationData['id'],
|
||||||
|
'new_operation_id' => $newOperationId,
|
||||||
|
'target_entite_id' => $targetEntiteId,
|
||||||
|
'original_entite_id' => $originalEntiteId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'new_operation_id' => $newOperationId,
|
||||||
|
'original_operation_id' => $operationData['id'],
|
||||||
|
'restored_data' => [
|
||||||
|
'operation' => true,
|
||||||
|
'users' => isset($backupData['users']) && $targetEntiteId === $originalEntiteId,
|
||||||
|
'sectors' => isset($backupData['sectors']),
|
||||||
|
'user_sectors' => isset($backupData['user_sectors']),
|
||||||
|
'passages' => isset($backupData['passages'])
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->db->rollBack();
|
||||||
|
|
||||||
|
LogService::log('Erreur lors de la restauration du backup', [
|
||||||
|
'level' => 'error',
|
||||||
|
'backup_file' => $backupFilePath,
|
||||||
|
'target_entite_id' => $targetEntiteId,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure les données de l'opération
|
||||||
|
*/
|
||||||
|
private function restoreOperation(array $operationData, int $targetEntiteId): int {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO operations (
|
||||||
|
fk_entite, libelle, date_deb, date_fin, chk_distinct_sectors,
|
||||||
|
fk_user_creat, chk_active, created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, 0, NOW())
|
||||||
|
');
|
||||||
|
|
||||||
|
$userId = Session::getUserId() ?? 1;
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$targetEntiteId,
|
||||||
|
$operationData['libelle'] . ' (Restaurée)',
|
||||||
|
$operationData['date_deb'],
|
||||||
|
$operationData['date_fin'],
|
||||||
|
$operationData['chk_distinct_sectors'] ?? 0,
|
||||||
|
$userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$this->db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure les utilisateurs (uniquement si même entité)
|
||||||
|
*/
|
||||||
|
private function restoreUsers(array $users, int $newOperationId): void {
|
||||||
|
foreach ($users as $user) {
|
||||||
|
// Vérifier si l'utilisateur existe déjà
|
||||||
|
$stmt = $this->db->prepare('SELECT id FROM users WHERE id = ?');
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
// Associer l'utilisateur existant à la nouvelle opération
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT IGNORE INTO ope_users (fk_operation, fk_user, chk_active, created_at)
|
||||||
|
VALUES (?, ?, 1, NOW())
|
||||||
|
');
|
||||||
|
$stmt->execute([$newOperationId, $user['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure les secteurs
|
||||||
|
*/
|
||||||
|
private function restoreSectors(array $sectors, int $newOperationId): void {
|
||||||
|
foreach ($sectors as $sector) {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO ope_sectors (
|
||||||
|
fk_operation, libelle, color, chk_active, created_at
|
||||||
|
) VALUES (?, ?, ?, 1, NOW())
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$newOperationId,
|
||||||
|
$sector['libelle'],
|
||||||
|
$sector['color']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure les relations utilisateurs-secteurs
|
||||||
|
*/
|
||||||
|
private function restoreUserSectors(array $userSectors, int $newOperationId): void {
|
||||||
|
foreach ($userSectors as $us) {
|
||||||
|
// Trouver le nouveau secteur par son libellé
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT id FROM ope_sectors
|
||||||
|
WHERE fk_operation = ? AND libelle = ?
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$stmt->execute([$newOperationId, $us['libelle'] ?? '']);
|
||||||
|
$newSector = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($newSector) {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT IGNORE INTO ope_users_sectors (
|
||||||
|
fk_operation, fk_sector, fk_user, chk_active, created_at
|
||||||
|
) VALUES (?, ?, ?, 1, NOW())
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$newOperationId,
|
||||||
|
$newSector['id'],
|
||||||
|
$us['fk_user']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaure les passages
|
||||||
|
*/
|
||||||
|
private function restorePassages(array $passages, int $newOperationId): void {
|
||||||
|
foreach ($passages as $passage) {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO ope_pass (
|
||||||
|
fk_operation, fk_user, fk_sector, fk_type, passed_at,
|
||||||
|
numero, rue_bis, rue, ville, fk_habitat, appt, niveau,
|
||||||
|
encrypted_name, encrypted_email, encrypted_phone,
|
||||||
|
montant, fk_type_reglement, remarque, chk_active, created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$newOperationId,
|
||||||
|
$passage['fk_user'],
|
||||||
|
$passage['fk_sector'],
|
||||||
|
$passage['fk_type'],
|
||||||
|
$passage['passed_at'],
|
||||||
|
$passage['numero'],
|
||||||
|
$passage['rue_bis'],
|
||||||
|
$passage['rue'],
|
||||||
|
$passage['ville'],
|
||||||
|
$passage['fk_habitat'],
|
||||||
|
$passage['appt'],
|
||||||
|
$passage['niveau'],
|
||||||
|
$passage['encrypted_name'],
|
||||||
|
$passage['encrypted_email'],
|
||||||
|
$passage['encrypted_phone'],
|
||||||
|
$passage['montant'],
|
||||||
|
$passage['fk_type_reglement'],
|
||||||
|
$passage['remarque']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
439
api/src/Services/FileService.php
Executable file
439
api/src/Services/FileService.php
Executable file
@@ -0,0 +1,439 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
class FileService {
|
||||||
|
private const BASE_UPLOADS_DIR = '/var/www/geosector/api/uploads';
|
||||||
|
|
||||||
|
// Permissions pour écriture web
|
||||||
|
private const FILE_PERMS = 0666;
|
||||||
|
private const DIR_PERMS = 0775;
|
||||||
|
private const OWNER_GROUP = 'nginx:nobody';
|
||||||
|
|
||||||
|
private \PDO $db;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un dossier dans l'arborescence uploads
|
||||||
|
*
|
||||||
|
* @param int $entiteId ID de l'entité
|
||||||
|
* @param string $path Chemin relatif à partir de BASE_UPLOADS_DIR (ex: '/5/operations/2644/export')
|
||||||
|
* @return string Le chemin complet du dossier créé
|
||||||
|
*/
|
||||||
|
public function createDirectory(int $entiteId, string $path): string {
|
||||||
|
// Construire le chemin complet
|
||||||
|
$fullPath = self::BASE_UPLOADS_DIR . $path;
|
||||||
|
|
||||||
|
LogService::log('Création de dossier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'entiteId' => $entiteId,
|
||||||
|
'path' => $path,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Créer le dossier avec tous les dossiers parents si nécessaire
|
||||||
|
if (!is_dir($fullPath)) {
|
||||||
|
if (!mkdir($fullPath, self::DIR_PERMS, true)) {
|
||||||
|
LogService::log('Erreur création dossier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
throw new Exception("Impossible de créer le dossier: {$fullPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer les permissions et propriétaire
|
||||||
|
$this->setDirectoryPermissions($fullPath);
|
||||||
|
|
||||||
|
LogService::log('Dossier créé avec succès', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
'permissions' => decoct(self::DIR_PERMS),
|
||||||
|
'owner' => self::OWNER_GROUP,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les permissions et propriétaire sur un dossier
|
||||||
|
*/
|
||||||
|
private function setDirectoryPermissions(string $path): void {
|
||||||
|
// Appliquer les permissions
|
||||||
|
chmod($path, self::DIR_PERMS);
|
||||||
|
|
||||||
|
// Changer le propriétaire et le groupe séparément pour plus de fiabilité
|
||||||
|
$chownUserCommand = "chown nginx " . escapeshellarg($path);
|
||||||
|
exec($chownUserCommand, $output, $returnCode);
|
||||||
|
|
||||||
|
if ($returnCode !== 0) {
|
||||||
|
LogService::log('Avertissement: Impossible de changer le propriétaire', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'path' => $path,
|
||||||
|
'command' => $chownUserCommand,
|
||||||
|
'return_code' => $returnCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chgrpCommand = "chgrp nobody " . escapeshellarg($path);
|
||||||
|
exec($chgrpCommand, $output, $returnCode);
|
||||||
|
|
||||||
|
if ($returnCode !== 0) {
|
||||||
|
LogService::log('Avertissement: Impossible de changer le groupe', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'path' => $path,
|
||||||
|
'command' => $chgrpCommand,
|
||||||
|
'return_code' => $returnCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les permissions sur un fichier
|
||||||
|
*/
|
||||||
|
public function setFilePermissions(string $filepath): void {
|
||||||
|
// Appliquer les permissions fichier
|
||||||
|
chmod($filepath, self::FILE_PERMS);
|
||||||
|
|
||||||
|
// Changer le propriétaire et le groupe séparément pour plus de fiabilité
|
||||||
|
$chownUserCommand = "chown nginx " . escapeshellarg($filepath);
|
||||||
|
exec($chownUserCommand, $output, $returnCode);
|
||||||
|
|
||||||
|
if ($returnCode !== 0) {
|
||||||
|
LogService::log('Avertissement: Impossible de changer le propriétaire du fichier', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'filepath' => $filepath,
|
||||||
|
'command' => $chownUserCommand,
|
||||||
|
'return_code' => $returnCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chgrpCommand = "chgrp nobody " . escapeshellarg($filepath);
|
||||||
|
exec($chgrpCommand, $output, $returnCode);
|
||||||
|
|
||||||
|
if ($returnCode !== 0) {
|
||||||
|
LogService::log('Avertissement: Impossible de changer le groupe du fichier', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'filepath' => $filepath,
|
||||||
|
'command' => $chgrpCommand,
|
||||||
|
'return_code' => $returnCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un fichier
|
||||||
|
*
|
||||||
|
* @param string $filePath Chemin complet vers le fichier ou chemin relatif depuis BASE_UPLOADS_DIR
|
||||||
|
* @param string $fileName Nom du fichier (pour les logs)
|
||||||
|
* @return bool True si suppression réussie, false sinon
|
||||||
|
*/
|
||||||
|
public function deleteFile(string $filePath, string $fileName): bool {
|
||||||
|
// Si le chemin ne commence pas par /, on considère qu'il est relatif à BASE_UPLOADS_DIR
|
||||||
|
if (!str_starts_with($filePath, '/')) {
|
||||||
|
$fullPath = self::BASE_UPLOADS_DIR . '/' . $filePath;
|
||||||
|
} else {
|
||||||
|
$fullPath = $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Tentative de suppression de fichier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'filePath' => $filePath,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe
|
||||||
|
if (!file_exists($fullPath)) {
|
||||||
|
LogService::log('Fichier non trouvé pour suppression', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que c'est bien un fichier (pas un dossier)
|
||||||
|
if (!is_file($fullPath)) {
|
||||||
|
LogService::log('Le chemin ne pointe pas vers un fichier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer d'abord les enregistrements dans la table medias
|
||||||
|
$deletedMediaRecords = $this->deleteMediaRecordsByFile($fullPath, $fileName);
|
||||||
|
|
||||||
|
// Tenter la suppression du fichier physique
|
||||||
|
if (unlink($fullPath)) {
|
||||||
|
LogService::log('Fichier supprimé avec succès', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
'deletedMediaRecords' => $deletedMediaRecords,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LogService::log('Erreur lors de la suppression du fichier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un dossier et tout son contenu
|
||||||
|
*
|
||||||
|
* @param string $filePath Chemin complet vers le dossier ou chemin relatif depuis BASE_UPLOADS_DIR
|
||||||
|
* @return bool True si suppression réussie, false sinon
|
||||||
|
*/
|
||||||
|
public function deleteDir(string $filePath): bool {
|
||||||
|
// Si le chemin ne commence pas par /, on considère qu'il est relatif à BASE_UPLOADS_DIR
|
||||||
|
if (!str_starts_with($filePath, '/')) {
|
||||||
|
$fullPath = self::BASE_UPLOADS_DIR . '/' . $filePath;
|
||||||
|
} else {
|
||||||
|
$fullPath = $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService::log('Tentative de suppression de dossier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'filePath' => $filePath,
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Vérifier que le dossier existe
|
||||||
|
if (!file_exists($fullPath)) {
|
||||||
|
LogService::log('Dossier non trouvé pour suppression', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que c'est bien un dossier
|
||||||
|
if (!is_dir($fullPath)) {
|
||||||
|
LogService::log('Le chemin ne pointe pas vers un dossier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer d'abord les enregistrements dans la table medias pour ce dossier
|
||||||
|
$deletedMediaRecords = $this->deleteMediaRecordsByDirectory($fullPath);
|
||||||
|
|
||||||
|
// Supprimer récursivement le contenu du dossier
|
||||||
|
if ($this->deleteDirectoryRecursive($fullPath)) {
|
||||||
|
LogService::log('Dossier supprimé avec succès', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
'deletedMediaRecords' => $deletedMediaRecords,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LogService::log('Erreur lors de la suppression du dossier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'fullPath' => $fullPath,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les enregistrements medias correspondant à un fichier spécifique
|
||||||
|
*
|
||||||
|
* @param string $fullPath Chemin complet du fichier
|
||||||
|
* @param string $fileName Nom du fichier (pour les logs)
|
||||||
|
* @return int Nombre d'enregistrements supprimés
|
||||||
|
*/
|
||||||
|
private function deleteMediaRecordsByFile(string $fullPath, string $fileName): int {
|
||||||
|
// Convertir le chemin complet en chemin relatif pour la recherche en base
|
||||||
|
$relativePath = str_replace(getcwd() . '/', '', $fullPath);
|
||||||
|
|
||||||
|
// Rechercher les enregistrements correspondants
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT id, fichier, file_path FROM medias
|
||||||
|
WHERE file_path = ? OR fichier = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$relativePath, $fileName]);
|
||||||
|
$mediaRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($mediaRecords)) {
|
||||||
|
LogService::log('Aucun enregistrement media trouvé pour le fichier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'relativePath' => $relativePath,
|
||||||
|
]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer les enregistrements
|
||||||
|
$stmt = $this->db->prepare('DELETE FROM medias WHERE file_path = ? OR fichier = ?');
|
||||||
|
$stmt->execute([$relativePath, $fileName]);
|
||||||
|
$deletedCount = $stmt->rowCount();
|
||||||
|
|
||||||
|
LogService::log('Enregistrements medias supprimés pour fichier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'fileName' => $fileName,
|
||||||
|
'deletedCount' => $deletedCount,
|
||||||
|
'mediaRecords' => array_column($mediaRecords, 'id'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les enregistrements medias correspondant à un dossier et ses sous-dossiers
|
||||||
|
*
|
||||||
|
* @param string $fullPath Chemin complet du dossier
|
||||||
|
* @return int Nombre d'enregistrements supprimés
|
||||||
|
*/
|
||||||
|
private function deleteMediaRecordsByDirectory(string $fullPath): int {
|
||||||
|
// Convertir le chemin complet en chemin relatif pour la recherche en base
|
||||||
|
$relativePath = str_replace(getcwd() . '/', '', $fullPath);
|
||||||
|
|
||||||
|
// Rechercher tous les enregistrements dont le chemin commence par le dossier
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT id, fichier, file_path FROM medias
|
||||||
|
WHERE file_path LIKE ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$relativePath . '%']);
|
||||||
|
$mediaRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($mediaRecords)) {
|
||||||
|
LogService::log('Aucun enregistrement media trouvé pour le dossier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'relativePath' => $relativePath,
|
||||||
|
]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer les enregistrements
|
||||||
|
$stmt = $this->db->prepare('DELETE FROM medias WHERE file_path LIKE ?');
|
||||||
|
$stmt->execute([$relativePath . '%']);
|
||||||
|
$deletedCount = $stmt->rowCount();
|
||||||
|
|
||||||
|
LogService::log('Enregistrements medias supprimés pour dossier', [
|
||||||
|
'level' => 'info',
|
||||||
|
'relativePath' => $relativePath,
|
||||||
|
'deletedCount' => $deletedCount,
|
||||||
|
'mediaRecords' => array_column($mediaRecords, 'id'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime récursivement un dossier et tout son contenu
|
||||||
|
*
|
||||||
|
* @param string $dir Chemin complet vers le dossier
|
||||||
|
* @return bool True si suppression réussie, false sinon
|
||||||
|
*/
|
||||||
|
private function deleteDirectoryRecursive(string $dir): bool {
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = array_diff(scandir($dir), ['.', '..']);
|
||||||
|
$deletedFiles = 0;
|
||||||
|
$totalFiles = count($files);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||||
|
|
||||||
|
if (is_dir($filePath)) {
|
||||||
|
// Récursion pour les sous-dossiers
|
||||||
|
if ($this->deleteDirectoryRecursive($filePath)) {
|
||||||
|
$deletedFiles++;
|
||||||
|
LogService::log('Sous-dossier supprimé', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'subDir' => $filePath,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
LogService::log('Erreur suppression sous-dossier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'subDir' => $filePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Supprimer le fichier
|
||||||
|
if (unlink($filePath)) {
|
||||||
|
$deletedFiles++;
|
||||||
|
LogService::log('Fichier supprimé du dossier', [
|
||||||
|
'level' => 'debug',
|
||||||
|
'file' => $filePath,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
LogService::log('Erreur suppression fichier du dossier', [
|
||||||
|
'level' => 'error',
|
||||||
|
'file' => $filePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer le dossier lui-même s'il est vide
|
||||||
|
if ($deletedFiles === $totalFiles && rmdir($dir)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LogService::log('Impossible de supprimer le dossier principal', [
|
||||||
|
'level' => 'error',
|
||||||
|
'dir' => $dir,
|
||||||
|
'deletedFiles' => $deletedFiles,
|
||||||
|
'totalFiles' => $totalFiles,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre le fichier dans la table medias
|
||||||
|
*/
|
||||||
|
public function saveToMediasTable(int $entiteId, int $operationId, string $filename, string $filepath, string $fileType, string $description, string $fileCategory = 'export'): int {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO medias (
|
||||||
|
support, support_id, fichier, file_type, file_category, file_size, mime_type,
|
||||||
|
original_name, fk_entite, fk_operation, file_path, description,
|
||||||
|
created_at, fk_user_creat
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
|
||||||
|
');
|
||||||
|
|
||||||
|
// Déterminer le type MIME selon l'extension
|
||||||
|
$mimeTypes = [
|
||||||
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'json' => 'application/json',
|
||||||
|
'enc' => 'application/octet-stream'
|
||||||
|
];
|
||||||
|
$mimeType = $mimeTypes[$fileType] ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
$relativePath = str_replace(getcwd() . '/', '', $filepath);
|
||||||
|
$userId = Session::getUserId() ?? 1; // Fallback si pas de session
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'operation',
|
||||||
|
$operationId,
|
||||||
|
$filename,
|
||||||
|
$fileType,
|
||||||
|
$fileCategory,
|
||||||
|
filesize($filepath),
|
||||||
|
$mimeType,
|
||||||
|
$filename,
|
||||||
|
$entiteId,
|
||||||
|
$operationId,
|
||||||
|
$relativePath,
|
||||||
|
$description,
|
||||||
|
$userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$this->db->lastInsertId();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
api/src/Services/LogService.php
Normal file → Executable file
48
api/src/Services/LogService.php
Normal file → Executable file
@@ -26,6 +26,19 @@ class LogService {
|
|||||||
$defaultMetadata['app_identifier'] = $clientInfo['appIdentifier'];
|
$defaultMetadata['app_identifier'] = $clientInfo['appIdentifier'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ajouter les informations de session si disponibles
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
$defaultMetadata['user_id'] = $_SESSION['user_id'];
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['entity_id'])) {
|
||||||
|
$defaultMetadata['entity_id'] = $_SESSION['entity_id'];
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['operation_id'])) {
|
||||||
|
$defaultMetadata['operation_id'] = $_SESSION['operation_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = array_merge_recursive($defaultMetadata, $metadata);
|
$metadata = array_merge_recursive($defaultMetadata, $metadata);
|
||||||
|
|
||||||
$logData = [
|
$logData = [
|
||||||
@@ -73,15 +86,31 @@ class LogService {
|
|||||||
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
|
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
|
||||||
$timestamp = date('Y-m-d\TH:i:s');
|
$timestamp = date('Y-m-d\TH:i:s');
|
||||||
$browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version'];
|
$browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version'];
|
||||||
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
|
|
||||||
|
// Ne pas afficher l'OS s'il est unknown
|
||||||
|
$osInfo = '';
|
||||||
|
if ($clientInfo['os']['name'] !== 'unknown' && $clientInfo['os']['version'] !== 'unknown') {
|
||||||
|
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
|
||||||
|
}
|
||||||
|
|
||||||
// Extraire le niveau de log
|
// Extraire le niveau de log
|
||||||
$level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info';
|
$level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info';
|
||||||
|
|
||||||
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format)
|
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format)
|
||||||
$additionalMetadata = [];
|
$additionalMetadata = [];
|
||||||
|
|
||||||
|
// Ajouter user_id, entity_id et operation_id en premier s'ils existent
|
||||||
|
$priorityKeys = ['user_id', 'entity_id', 'operation_id'];
|
||||||
|
foreach ($priorityKeys as $key) {
|
||||||
|
if (isset($metadata[$key]) && !is_array($metadata[$key])) {
|
||||||
|
$additionalMetadata[$key] = $metadata[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les autres métadonnées
|
||||||
foreach ($metadata as $key => $value) {
|
foreach ($metadata as $key => $value) {
|
||||||
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])) {
|
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])
|
||||||
|
&& !in_array($key, $priorityKeys)) {
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
} else {
|
} else {
|
||||||
@@ -114,4 +143,19 @@ class LogService {
|
|||||||
error_log("Erreur lors de l'écriture des logs: " . $e->getMessage());
|
error_log("Erreur lors de l'écriture des logs: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function info(string $message, array $metadata = []): void {
|
||||||
|
$metadata['level'] = 'info';
|
||||||
|
self::log($message, $metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warning(string $message, array $metadata = []): void {
|
||||||
|
$metadata['level'] = 'warning';
|
||||||
|
self::log($message, $metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function error(string $message, array $metadata = []): void {
|
||||||
|
$metadata['level'] = 'error';
|
||||||
|
self::log($message, $metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
282
api/src/Services/OperationDataService.php
Executable file
282
api/src/Services/OperationDataService.php
Executable file
@@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/ApiService.php';
|
||||||
|
require_once __DIR__ . '/LogService.php';
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use ApiService;
|
||||||
|
use LogService;
|
||||||
|
|
||||||
|
class OperationDataService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare les données d'opération selon l'interface et le rôle utilisateur
|
||||||
|
*
|
||||||
|
* @param PDO $db Instance de la base de données
|
||||||
|
* @param int $entiteId ID de l'entité
|
||||||
|
* @param string $interface 'user' ou 'admin'
|
||||||
|
* @param int $userRole Rôle de l'utilisateur
|
||||||
|
* @param int $userId ID de l'utilisateur connecté
|
||||||
|
* @param int|null $specificOperationId ID d'opération spécifique (pour création d'opération)
|
||||||
|
* @return array Données formatées avec operations, sectors, users_sectors, passages
|
||||||
|
*/
|
||||||
|
public static function prepareOperationData(
|
||||||
|
PDO $db,
|
||||||
|
int $entiteId,
|
||||||
|
string $interface,
|
||||||
|
int $userRole,
|
||||||
|
int $userId,
|
||||||
|
?int $specificOperationId = null
|
||||||
|
): array {
|
||||||
|
|
||||||
|
$operationsData = [];
|
||||||
|
$sectorsData = [];
|
||||||
|
$passagesData = [];
|
||||||
|
$usersSectorsData = [];
|
||||||
|
|
||||||
|
// 1. Récupération des opérations selon les critères
|
||||||
|
$operationLimit = 0;
|
||||||
|
$activeOperationOnly = false;
|
||||||
|
|
||||||
|
if ($interface === 'user') {
|
||||||
|
// Interface utilisateur : seulement la dernière opération active
|
||||||
|
$operationLimit = 1;
|
||||||
|
$activeOperationOnly = true;
|
||||||
|
} elseif ($interface === 'admin' && $userRole == 2) {
|
||||||
|
// Interface admin avec rôle 2 : les 3 dernières opérations dont l'active
|
||||||
|
$operationLimit = 3;
|
||||||
|
} elseif ($interface === 'admin' && $userRole > 2) {
|
||||||
|
// Super admin : les 3 dernières opérations
|
||||||
|
$operationLimit = 3;
|
||||||
|
} else {
|
||||||
|
// Autres cas : pas d'opérations
|
||||||
|
$operationLimit = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si une opération spécifique est demandée (création d'opération)
|
||||||
|
if ($specificOperationId) {
|
||||||
|
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
|
||||||
|
FROM operations
|
||||||
|
WHERE fk_entite = ?
|
||||||
|
ORDER BY id DESC LIMIT 3";
|
||||||
|
|
||||||
|
$operationStmt = $db->prepare($operationQuery);
|
||||||
|
$operationStmt->execute([$entiteId]);
|
||||||
|
$operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$activeOperationId = $specificOperationId;
|
||||||
|
} elseif ($operationLimit > 0) {
|
||||||
|
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
|
||||||
|
FROM operations
|
||||||
|
WHERE fk_entite = ?";
|
||||||
|
|
||||||
|
if ($activeOperationOnly) {
|
||||||
|
$operationQuery .= " AND chk_active = 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationQuery .= " ORDER BY id DESC LIMIT " . $operationLimit;
|
||||||
|
|
||||||
|
$operationStmt = $db->prepare($operationQuery);
|
||||||
|
$operationStmt->execute([$entiteId]);
|
||||||
|
$operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Récupérer l'ID de l'opération active (première opération retournée ou celle avec chk_active=1)
|
||||||
|
$activeOperationId = null;
|
||||||
|
if (!empty($operations)) {
|
||||||
|
foreach ($operations as $operation) {
|
||||||
|
if ($operation['chk_active'] == 1) {
|
||||||
|
$activeOperationId = (int)$operation['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Si aucune opération active trouvée, prendre la première
|
||||||
|
if (!$activeOperationId) {
|
||||||
|
$activeOperationId = (int)$operations[0]['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$operations = [];
|
||||||
|
$activeOperationId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($operations)) {
|
||||||
|
// Formater les données des opérations
|
||||||
|
foreach ($operations as $operation) {
|
||||||
|
$operationsData[] = [
|
||||||
|
'id' => $operation['id'],
|
||||||
|
'fk_entite' => $operation['fk_entite'],
|
||||||
|
'libelle' => $operation['libelle'],
|
||||||
|
'date_deb' => $operation['date_deb'],
|
||||||
|
'date_fin' => $operation['date_fin'],
|
||||||
|
'chk_active' => $operation['chk_active']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Récupérer les secteurs selon l'interface et le rôle
|
||||||
|
if ($activeOperationId) {
|
||||||
|
if ($interface === 'user') {
|
||||||
|
// Interface utilisateur : seulement les secteurs affectés à l'utilisateur
|
||||||
|
$sectorsStmt = $db->prepare(
|
||||||
|
'SELECT s.id, s.libelle, s.color, s.sector
|
||||||
|
FROM ope_sectors s
|
||||||
|
JOIN ope_users_sectors us ON s.id = us.fk_sector
|
||||||
|
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
|
||||||
|
);
|
||||||
|
$sectorsStmt->execute([$activeOperationId, $userId]);
|
||||||
|
} elseif ($interface === 'admin' && ($userRole == 2 || $userRole > 2)) {
|
||||||
|
// Interface admin : tous les secteurs distincts de l'opération
|
||||||
|
$sectorsStmt = $db->prepare(
|
||||||
|
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
|
||||||
|
FROM ope_sectors s
|
||||||
|
WHERE s.fk_operation = ? AND s.chk_active = 1'
|
||||||
|
);
|
||||||
|
$sectorsStmt->execute([$activeOperationId]);
|
||||||
|
} else {
|
||||||
|
$sectors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération des secteurs si une requête a été préparée
|
||||||
|
if (isset($sectorsStmt)) {
|
||||||
|
$sectors = $sectorsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} else {
|
||||||
|
$sectors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sectors)) {
|
||||||
|
$sectorsData = $sectors;
|
||||||
|
|
||||||
|
// 3. Récupérer les passages selon l'interface et le rôle
|
||||||
|
if ($interface === 'user' && !empty($sectors)) {
|
||||||
|
// Interface utilisateur : passages liés aux secteurs de l'utilisateur
|
||||||
|
$sectorIds = array_column($sectors, 'id');
|
||||||
|
$sectorIdsString = implode(',', $sectorIds);
|
||||||
|
|
||||||
|
if (!empty($sectorIdsString)) {
|
||||||
|
$passagesStmt = $db->prepare(
|
||||||
|
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||||
|
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages,
|
||||||
|
chk_email_sent, docremis, date_repasser, chk_mobile, anomalie, created_at, updated_at, chk_active
|
||||||
|
FROM ope_pass
|
||||||
|
WHERE fk_operation = ? AND fk_sector IN ($sectorIdsString) AND chk_active = 1"
|
||||||
|
);
|
||||||
|
$passagesStmt->execute([$activeOperationId]);
|
||||||
|
}
|
||||||
|
} elseif ($interface === 'admin' && ($userRole == 2 || $userRole > 2)) {
|
||||||
|
// Interface admin : tous les passages de l'opération
|
||||||
|
$passagesStmt = $db->prepare(
|
||||||
|
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
|
||||||
|
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages,
|
||||||
|
chk_email_sent, docremis, date_repasser, chk_mobile, anomalie, created_at, updated_at, chk_active
|
||||||
|
FROM ope_pass
|
||||||
|
WHERE fk_operation = ? AND chk_active = 1"
|
||||||
|
);
|
||||||
|
$passagesStmt->execute([$activeOperationId]);
|
||||||
|
} else {
|
||||||
|
$passages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération des passages si une requête a été préparée
|
||||||
|
if (isset($passagesStmt)) {
|
||||||
|
$passages = $passagesStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} else {
|
||||||
|
$passages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($passages)) {
|
||||||
|
// Déchiffrer les données sensibles
|
||||||
|
foreach ($passages as &$passage) {
|
||||||
|
// Déchiffrement du nom
|
||||||
|
$passage['name'] = '';
|
||||||
|
if (!empty($passage['encrypted_name'])) {
|
||||||
|
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
|
||||||
|
}
|
||||||
|
unset($passage['encrypted_name']);
|
||||||
|
|
||||||
|
// Déchiffrement de l'email
|
||||||
|
$passage['email'] = '';
|
||||||
|
if (!empty($passage['encrypted_email'])) {
|
||||||
|
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
|
||||||
|
if ($decryptedEmail) {
|
||||||
|
$passage['email'] = $decryptedEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($passage['encrypted_email']);
|
||||||
|
|
||||||
|
// Déchiffrement du téléphone
|
||||||
|
$passage['phone'] = '';
|
||||||
|
if (!empty($passage['encrypted_phone'])) {
|
||||||
|
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
|
||||||
|
}
|
||||||
|
unset($passage['encrypted_phone']);
|
||||||
|
}
|
||||||
|
$passagesData = $passages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Récupérer les utilisateurs des secteurs partagés
|
||||||
|
if (($interface === 'user' || ($interface === 'admin' && ($userRole == 2 || $userRole > 2))) && !empty($sectors)) {
|
||||||
|
$sectorIds = array_column($sectors, 'id');
|
||||||
|
$sectorIdsString = implode(',', $sectorIds);
|
||||||
|
|
||||||
|
if (!empty($sectorIdsString)) {
|
||||||
|
// Utiliser ope_users au lieu de users pour avoir les données historiques
|
||||||
|
$usersSectorsStmt = $db->prepare(
|
||||||
|
"SELECT DISTINCT ou.fk_user as id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
|
||||||
|
FROM ope_users ou
|
||||||
|
JOIN ope_users_sectors us ON ou.fk_user = us.fk_user AND ou.fk_operation = us.fk_operation
|
||||||
|
WHERE us.fk_sector IN ($sectorIdsString)
|
||||||
|
AND us.fk_operation = ?
|
||||||
|
AND us.chk_active = 1
|
||||||
|
AND ou.chk_active = 1
|
||||||
|
AND ou.fk_user != ?" // Exclure l'utilisateur connecté
|
||||||
|
);
|
||||||
|
$usersSectorsStmt->execute([$activeOperationId, $userId]);
|
||||||
|
$usersSectors = $usersSectorsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($usersSectors)) {
|
||||||
|
// Déchiffrer les noms des utilisateurs
|
||||||
|
foreach ($usersSectors as &$userSector) {
|
||||||
|
if (!empty($userSector['encrypted_name'])) {
|
||||||
|
$userSector['name'] = ApiService::decryptData($userSector['encrypted_name']);
|
||||||
|
unset($userSector['encrypted_name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$usersSectorsData = $usersSectors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'operations' => $operationsData,
|
||||||
|
'sectors' => $sectorsData,
|
||||||
|
'users_sectors' => $usersSectorsData,
|
||||||
|
'passages' => $passagesData
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare la réponse complète pour la création d'opération
|
||||||
|
*
|
||||||
|
* @param PDO $db Instance de la base de données
|
||||||
|
* @param int $newOpeId ID de la nouvelle opération
|
||||||
|
* @param int $entiteId ID de l'entité
|
||||||
|
* @return array Réponse formatée avec status, message, operation_id et données
|
||||||
|
*/
|
||||||
|
public static function prepareOperationResponse(PDO $db, int $newOpeId, int $entiteId): array {
|
||||||
|
// Utiliser le rôle admin pour récupérer toutes les données
|
||||||
|
$operationData = self::prepareOperationData($db, $entiteId, 'admin', 2, 0, $newOpeId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Opération créée avec succès',
|
||||||
|
'operation_id' => $newOpeId,
|
||||||
|
'operations' => $operationData['operations'],
|
||||||
|
'sectors' => $operationData['sectors'],
|
||||||
|
'users_sectors' => $operationData['users_sectors'],
|
||||||
|
'passages' => $operationData['passages']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
0
api/src/Utils/ClientDetector.php
Normal file → Executable file
0
api/src/Utils/ClientDetector.php
Normal file → Executable file
54
api/test_addresses_connection.php
Normal file
54
api/test_addresses_connection.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test de la connexion à la base de données des adresses
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
|
||||||
|
|
||||||
|
echo "Test de connexion à la base de données des adresses\n";
|
||||||
|
echo "==================================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$addressesConfig = $appConfig->getAddressesDatabaseConfig();
|
||||||
|
|
||||||
|
echo "Configuration:\n";
|
||||||
|
echo "- Environnement: " . $appConfig->getEnvironment() . "\n";
|
||||||
|
echo "- Host: " . $addressesConfig['host'] . "\n";
|
||||||
|
echo "- Database: " . $addressesConfig['name'] . "\n";
|
||||||
|
echo "- Username: " . $addressesConfig['username'] . "\n\n";
|
||||||
|
|
||||||
|
// Initialiser la connexion
|
||||||
|
AddressesDatabase::init($addressesConfig);
|
||||||
|
$db = AddressesDatabase::getInstance();
|
||||||
|
|
||||||
|
echo "✓ Connexion réussie!\n\n";
|
||||||
|
|
||||||
|
// Tester une requête simple
|
||||||
|
echo "Test de requête...\n";
|
||||||
|
$stmt = $db->query("SELECT COUNT(*) as total FROM adresses LIMIT 1");
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
echo "✓ Nombre total d'adresses: " . number_format($result['total']) . "\n\n";
|
||||||
|
|
||||||
|
// Tester les fonctions géospatiales
|
||||||
|
echo "Test des fonctions géospatiales...\n";
|
||||||
|
$stmt = $db->query("SELECT ST_AsText(ST_GeomFromText('POINT(2.3522 48.8566)', 4326)) as point");
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
echo "✓ Fonctions géospatiales disponibles: " . $result['point'] . "\n\n";
|
||||||
|
|
||||||
|
// Afficher les colonnes de la table adresses
|
||||||
|
echo "Structure de la table adresses:\n";
|
||||||
|
$stmt = $db->query("DESCRIBE adresses");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
echo "- " . $column['Field'] . " (" . $column['Type'] . ")\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "✗ Erreur: " . $e->getMessage() . "\n";
|
||||||
|
echo "Trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
110
api/test_addresses_dept.php
Normal file
110
api/test_addresses_dept.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script de test de la connexion à la base de données des adresses
|
||||||
|
* Adapté pour la structure par département
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/src/Config/AppConfig.php';
|
||||||
|
require_once __DIR__ . '/src/Core/Database.php';
|
||||||
|
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
|
||||||
|
|
||||||
|
echo "Test de connexion à la base de données des adresses par département\n";
|
||||||
|
echo "==================================================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialiser la configuration
|
||||||
|
$appConfig = AppConfig::getInstance();
|
||||||
|
$addressesConfig = $appConfig->getAddressesDatabaseConfig();
|
||||||
|
|
||||||
|
echo "Configuration:\n";
|
||||||
|
echo "- Environnement: " . $appConfig->getEnvironment() . "\n";
|
||||||
|
echo "- Host: " . $addressesConfig['host'] . "\n";
|
||||||
|
echo "- Database: " . $addressesConfig['name'] . "\n";
|
||||||
|
echo "- Username: " . $addressesConfig['username'] . "\n\n";
|
||||||
|
|
||||||
|
// Initialiser les connexions
|
||||||
|
Database::init($appConfig->getDatabaseConfig());
|
||||||
|
AddressesDatabase::init($addressesConfig);
|
||||||
|
|
||||||
|
$mainDb = Database::getInstance();
|
||||||
|
$addressesDb = AddressesDatabase::getInstance();
|
||||||
|
|
||||||
|
echo "✓ Connexion réussie aux deux bases!\n\n";
|
||||||
|
|
||||||
|
// Lister les tables cp* disponibles
|
||||||
|
echo "Tables d'adresses disponibles:\n";
|
||||||
|
$stmt = $addressesDb->query("SHOW TABLES LIKE 'cp%'");
|
||||||
|
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
// Extraire le numéro de département
|
||||||
|
$dept = str_replace('cp', '', $table);
|
||||||
|
|
||||||
|
// Compter les lignes
|
||||||
|
$countStmt = $addressesDb->query("SELECT COUNT(*) as total FROM `$table`");
|
||||||
|
$count = $countStmt->fetch()['total'];
|
||||||
|
|
||||||
|
echo "- $table (département $dept): " . number_format($count) . " adresses\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Tester avec une entité
|
||||||
|
echo "Test de récupération du département d'une entité:\n";
|
||||||
|
$testEntityId = 1; // Changez selon votre base
|
||||||
|
|
||||||
|
$stmt = $mainDb->prepare("SELECT id, nom, departement FROM entites WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $testEntityId]);
|
||||||
|
$entity = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($entity) {
|
||||||
|
echo "- Entité #{$entity['id']}: {$entity['nom']} (département {$entity['departement']})\n";
|
||||||
|
$tableName = "cp" . $entity['departement'];
|
||||||
|
|
||||||
|
// Vérifier si la table existe
|
||||||
|
if (in_array($tableName, $tables)) {
|
||||||
|
echo "✓ Table $tableName existe\n";
|
||||||
|
|
||||||
|
// Tester une requête sur cette table
|
||||||
|
$stmt = $addressesDb->query("SELECT numero, rue, cp, ville, gps_lat, gps_lng
|
||||||
|
FROM `$tableName`
|
||||||
|
WHERE gps_lat != '' AND gps_lng != ''
|
||||||
|
LIMIT 3");
|
||||||
|
$addresses = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo "\nExemples d'adresses dans le département {$entity['departement']}:\n";
|
||||||
|
foreach ($addresses as $addr) {
|
||||||
|
echo "- {$addr['numero']} {$addr['rue']}, {$addr['cp']} {$addr['ville']} ";
|
||||||
|
echo "(lat: {$addr['gps_lat']}, lng: {$addr['gps_lng']})\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tester une requête géospatiale
|
||||||
|
echo "\nTest de requête géospatiale:\n";
|
||||||
|
$testLat = $addresses[0]['gps_lat'] ?? 48.8566;
|
||||||
|
$testLng = $addresses[0]['gps_lng'] ?? 2.3522;
|
||||||
|
|
||||||
|
$sql = "SELECT COUNT(*) as total
|
||||||
|
FROM `$tableName`
|
||||||
|
WHERE ST_Distance_Sphere(
|
||||||
|
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
|
||||||
|
POINT(:lng, :lat)
|
||||||
|
) <= 1000
|
||||||
|
AND gps_lat != '' AND gps_lng != ''";
|
||||||
|
|
||||||
|
$stmt = $addressesDb->prepare($sql);
|
||||||
|
$stmt->execute(['lat' => $testLat, 'lng' => $testLng]);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
|
||||||
|
echo "✓ Adresses dans un rayon de 1km autour de ($testLat, $testLng): " . $result['total'] . "\n";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo "✗ Table $tableName n'existe pas dans la base adresses\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "✗ Erreur: " . $e->getMessage() . "\n";
|
||||||
|
echo "Trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user