diff --git a/docs/api-stats-export.md b/docs/api-stats-export.md new file mode 100644 index 0000000..bc9e984 --- /dev/null +++ b/docs/api-stats-export.md @@ -0,0 +1,220 @@ +# API - Export des objets Stats + +## Endpoint + +``` +GET /api/v1/stats/export +``` + +## Description + +Cet endpoint permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes, similaire à la commande `app:export-stats`. + +## Paramètres de requête + +| Paramètre | Type | Défaut | Description | +|-----------|------|--------|-------------| +| `zone` | string | - | Code INSEE spécifique à exporter (optionnel) | +| `pretty` | boolean | false | Formater le JSON avec indentation | +| `include_followups` | boolean | true | Inclure les données de followup | +| `include_places` | boolean | false | Inclure les données des lieux (peut être volumineux) | + +## Exemples d'utilisation + +### Export de toutes les zones +```bash +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export" +``` + +### Export avec formatage JSON +```bash +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true" +``` + +### Export d'une zone spécifique +```bash +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056" +``` + +### Export d'une zone avec formatage et sans followups +```bash +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true&include_followups=false" +``` + +### Export complet avec lieux +```bash +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true&include_places=true" +``` + +## Réponse + +### Succès (200 OK) + +```json +[ + { + "id": 1, + "zone": "75056", + "name": "Paris", + "dateCreated": "2024-01-15 10:30:00", + "dateModified": "2024-01-20 14:45:00", + "population": 2161000, + "budgetAnnuel": "8500000000", + "siren": 200054781, + "codeEpci": 200054781, + "codesPostaux": "75001;75002;75003;...", + "decomptes": { + "placesCount": 1250, + "avecHoraires": 980, + "avecAdresse": 1200, + "avecSite": 850, + "avecAccessibilite": 450, + "avecNote": 320, + "completionPercent": 75, + "placesCountReal": 1250 + }, + "followups": [ + { + "name": "fire_hydrant_count", + "measure": 1250, + "date": "2024-01-20 14:45:00" + }, + { + "name": "fire_hydrant_completion", + "measure": 85.5, + "date": "2024-01-20 14:45:00" + } + ], + "places": [ + { + "id": 1, + "name": "Boulangerie du Centre", + "mainTag": "shop", + "osmId": 123456, + "osmKind": "node", + "email": "contact@boulangerie.fr", + "note": "Boulangerie artisanale", + "zipCode": "75001", + "siret": "12345678901234", + "lat": 48.8566, + "lon": 2.3522, + "hasOpeningHours": true, + "hasAddress": true, + "hasWebsite": true, + "hasWheelchair": false, + "hasNote": true, + "completionPercentage": 85 + } + ] + } +] +``` + +### Erreur - Zone non trouvée (404 Not Found) + +```json +{ + "error": "Aucun objet Stats trouvé", + "message": "Aucune zone trouvée pour le code INSEE: 99999" +} +``` + +### Erreur - Erreur serveur (500 Internal Server Error) + +```json +{ + "error": "Erreur lors de l'export", + "message": "Description de l'erreur" +} +``` + +## Headers de réponse + +| Header | Description | +|--------|-------------| +| `Content-Type` | `application/json` | +| `Content-Disposition` | `attachment; filename="stats_export.json"` | +| `X-Export-Count` | Nombre d'objets exportés | +| `X-Export-Generated` | Date/heure de génération (format ISO 8601) | +| `X-Export-Zone` | Code INSEE si export d'une zone spécifique | + +## Structure des données + +### Informations générales +- `id` : Identifiant unique de l'objet Stats +- `zone` : Code INSEE de la zone +- `name` : Nom de la ville/zone +- `dateCreated` : Date de création +- `dateModified` : Date de dernière modification + +### Données démographiques et administratives +- `population` : Population de la zone +- `budgetAnnuel` : Budget annuel de la collectivité +- `siren` : Code SIREN +- `codeEpci` : Code EPCI +- `codesPostaux` : Codes postaux de la zone + +### Décomptes +- `placesCount` : Nombre de lieux enregistrés +- `avecHoraires` : Nombre de lieux avec horaires d'ouverture +- `avecAdresse` : Nombre de lieux avec adresse complète +- `avecSite` : Nombre de lieux avec site web +- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR +- `avecNote` : Nombre de lieux avec note +- `completionPercent` : Pourcentage de complétion global +- `placesCountReal` : Nombre réel de lieux (comptage direct) + +### Followups (si `include_followups=true`) +- `followups` : Tableau des mesures de suivi (CityFollowUp) + - `name` : Nom de la mesure + - `measure` : Valeur de la mesure + - `date` : Date de la mesure + +### Places (si `include_places=true`) +- `places` : Tableau des lieux de la zone + - `id` : Identifiant du lieu + - `name` : Nom du lieu + - `mainTag` : Tag principal OSM + - `osmId` : ID OSM + - `osmKind` : Type OSM (node/way) + - `email` : Email de contact + - `note` : Note + - `zipCode` : Code postal + - `siret` : Numéro SIRET + - `lat` : Latitude + - `lon` : Longitude + - `hasOpeningHours` : A des horaires d'ouverture + - `hasAddress` : A une adresse complète + - `hasWebsite` : A un site web + - `hasWheelchair` : A des informations d'accessibilité + - `hasNote` : A une note + - `completionPercentage` : Pourcentage de complétion du lieu + +## Cas d'usage + +### Export pour analyse +```bash +# Export de toutes les villes avec formatage +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true" > analyse_villes.json + +# Export d'une ville spécifique +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true" > paris.json +``` + +### Export pour traitement automatisé +```bash +# Export compact pour traitement par script +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export" > stats_compact.json +``` + +### Export avec données complètes +```bash +# Export avec tous les lieux (attention: peut être volumineux) +curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?include_places=true&pretty=true" > stats_complet.json +``` + +## Limitations + +- L'option `include_places=true` peut générer des fichiers très volumineux pour les grandes villes +- Les requêtes avec `include_places=true` peuvent être plus lentes +- Le formatage JSON (`pretty=true`) augmente la taille de la réponse \ No newline at end of file diff --git a/docs/export-stats-command.md b/docs/export-stats-command.md new file mode 100644 index 0000000..c3ac200 --- /dev/null +++ b/docs/export-stats-command.md @@ -0,0 +1,142 @@ +# Commande d'export des objets Stats + +## Description + +La commande `app:export-stats` permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes. + +## Utilisation + +### Export de tous les objets Stats +```bash +php bin/console app:export-stats +``` + +### Export avec formatage JSON +```bash +php bin/console app:export-stats --pretty +``` + +### Export vers un fichier spécifique +```bash +php bin/console app:export-stats --output=mon_export.json +``` + +### Export d'une zone spécifique +```bash +php bin/console app:export-stats --zone=75056 +``` + +### Export avec toutes les options +```bash +php bin/console app:export-stats --output=paris_stats.json --zone=75056 --pretty +``` + +### Export avec mode verbeux +```bash +php bin/console app:export-stats -v +``` + +## Options disponibles + +- `--output, -o` : Fichier de sortie (défaut: `stats_export.json`) +- `--zone, -z` : Code INSEE spécifique à exporter (optionnel) +- `--pretty, -p` : Formater le JSON avec indentation +- `-v, --verbose` : Mode verbeux pour afficher un aperçu des données + +## Structure des données exportées + +Le fichier JSON contient un tableau d'objets avec la structure suivante : + +```json +[ + { + "id": 1, + "zone": "75056", + "name": "Paris", + "dateCreated": "2024-01-15 10:30:00", + "dateModified": "2024-01-20 14:45:00", + "population": 2161000, + "budgetAnnuel": "8500000000", + "siren": "200054781", + "codeEpci": "200054781", + "codesPostaux": "75001;75002;75003;...", + "decomptes": { + "placesCount": 1250, + "avecHoraires": 980, + "avecAdresse": 1200, + "avecSite": 850, + "avecAccessibilite": 450, + "avecNote": 320, + "completionPercent": 75, + "placesCountReal": 1250 + }, + "followups": [ + { + "name": "fire_hydrant_count", + "measure": 1250, + "date": "2024-01-20 14:45:00" + }, + { + "name": "fire_hydrant_completion", + "measure": 85.5, + "date": "2024-01-20 14:45:00" + } + ] + } +] +``` + +## Propriétés exportées + +### Informations générales +- `id` : Identifiant unique de l'objet Stats +- `zone` : Code INSEE de la zone +- `name` : Nom de la ville/zone +- `dateCreated` : Date de création +- `dateModified` : Date de dernière modification + +### Données démographiques et administratives +- `population` : Population de la zone +- `budgetAnnuel` : Budget annuel de la collectivité +- `siren` : Code SIREN +- `codeEpci` : Code EPCI +- `codesPostaux` : Codes postaux de la zone + +### Décomptes +- `placesCount` : Nombre de lieux enregistrés +- `avecHoraires` : Nombre de lieux avec horaires d'ouverture +- `avecAdresse` : Nombre de lieux avec adresse complète +- `avecSite` : Nombre de lieux avec site web +- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR +- `avecNote` : Nombre de lieux avec note +- `completionPercent` : Pourcentage de complétion global +- `placesCountReal` : Nombre réel de lieux (comptage direct) + +### Followups +- `followups` : Tableau des mesures de suivi (CityFollowUp) + - `name` : Nom de la mesure + - `measure` : Valeur de la mesure + - `date` : Date de la mesure + +## Exemples d'utilisation + +### Export pour analyse +```bash +# Export de toutes les villes avec formatage +php bin/console app:export-stats --pretty --output=analyse_villes.json + +# Export d'une ville spécifique +php bin/console app:export-stats --zone=75056 --pretty --output=paris.json +``` + +### Export pour traitement automatisé +```bash +# Export compact pour traitement par script +php bin/console app:export-stats --output=stats_compact.json +``` + +### Vérification des données +```bash +# Export avec aperçu des données +php bin/console app:export-stats --pretty -v +``` \ No newline at end of file diff --git a/src/Command/ExportStatsCommand.php b/src/Command/ExportStatsCommand.php new file mode 100644 index 0000000..eed68f5 --- /dev/null +++ b/src/Command/ExportStatsCommand.php @@ -0,0 +1,167 @@ +addOption( + 'output', + 'o', + InputOption::VALUE_REQUIRED, + 'Fichier de sortie (par défaut: stats_export.json)', + 'stats_export.json' + ) + ->addOption( + 'zone', + 'z', + InputOption::VALUE_REQUIRED, + 'Code INSEE spécifique à exporter (optionnel)' + ) + ->addOption( + 'pretty', + 'p', + InputOption::VALUE_NONE, + 'Formater le JSON avec indentation' + ) + ->setHelp('Cette commande exporte les objets Stats au format JSON avec leurs propriétés de nom et de décomptes.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $outputFile = $input->getOption('output'); + $zone = $input->getOption('zone'); + $pretty = $input->getOption('pretty'); + + $io->title('Export des objets Stats'); + + try { + // Construire la requête + $qb = $this->entityManager->getRepository(Stats::class)->createQueryBuilder('s'); + + if ($zone) { + $qb->where('s.zone = :zone') + ->setParameter('zone', $zone); + $io->note("Export pour la zone INSEE: $zone"); + } + + $stats = $qb->getQuery()->getResult(); + + if (empty($stats)) { + $io->warning('Aucun objet Stats trouvé.'); + return Command::SUCCESS; + } + + $io->info(sprintf('Export de %d objet(s) Stats...', count($stats))); + + // Préparer les données pour l'export + $exportData = []; + + foreach ($stats as $stat) { + $statData = [ + 'id' => $stat->getId(), + 'zone' => $stat->getZone(), + 'name' => $stat->getName(), + 'dateCreated' => $stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : null, + 'dateModified' => $stat->getDateModified() ? $stat->getDateModified()->format('Y-m-d H:i:s') : null, + 'population' => $stat->getPopulation(), + 'budgetAnnuel' => $stat->getBudgetAnnuel(), + 'siren' => $stat->getSiren(), + 'codeEpci' => $stat->getCodeEpci(), + 'codesPostaux' => $stat->getCodesPostaux(), + 'decomptes' => [ + 'placesCount' => $stat->getPlacesCount(), + 'avecHoraires' => $stat->getAvecHoraires(), + 'avecAdresse' => $stat->getAvecAdresse(), + 'avecSite' => $stat->getAvecSite(), + 'avecAccessibilite' => $stat->getAvecAccessibilite(), + 'avecNote' => $stat->getAvecNote(), + 'completionPercent' => $stat->getCompletionPercent(), + 'placesCountReal' => $stat->getPlaces()->count(), + ], + 'followups' => [] + ]; + + // Ajouter les followups si disponibles + foreach ($stat->getCityFollowUps() as $followup) { + $statData['followups'][] = [ + 'name' => $followup->getName(), + 'measure' => $followup->getMeasure(), + 'date' => $followup->getDate()->format('Y-m-d H:i:s') + ]; + } + + $exportData[] = $statData; + } + + // Préparer le JSON + $jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + if ($pretty) { + $jsonOptions |= JSON_PRETTY_PRINT; + } + + $jsonContent = json_encode($exportData, $jsonOptions); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Erreur lors de l\'encodage JSON: ' . json_last_error_msg()); + } + + // Écrire dans le fichier + $bytesWritten = file_put_contents($outputFile, $jsonContent); + + if ($bytesWritten === false) { + throw new \Exception("Impossible d'écrire dans le fichier: $outputFile"); + } + + $io->success(sprintf( + 'Export terminé avec succès ! %d objet(s) exporté(s) vers %s (%s octets)', + count($stats), + $outputFile, + number_format($bytesWritten, 0, ',', ' ') + )); + + // Afficher un aperçu des données + if ($io->isVerbose()) { + $io->section('Aperçu des données exportées'); + foreach ($exportData as $index => $data) { + $io->text(sprintf( + '%d. %s (%s) - %d lieux, %d%% complété', + $index + 1, + $data['name'], + $data['zone'], + $data['decomptes']['placesCountReal'], + $data['decomptes']['completionPercent'] + )); + } + } + + return Command::SUCCESS; + + } catch (\Exception $e) { + $io->error('Erreur lors de l\'export: ' . $e->getMessage()); + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php index 24ed9a9..654b83e 100644 --- a/src/Controller/ApiController.php +++ b/src/Controller/ApiController.php @@ -198,4 +198,138 @@ class ApiController extends AbstractController $response->headers->set('Content-Disposition', 'attachment; filename="places_'.$insee.'.csv"'); return $response; } + + #[Route('/api/v1/stats/export', name: 'api_stats_export', methods: ['GET'])] + public function statsExport( + StatsRepository $statsRepository, + \Symfony\Component\HttpFoundation\Request $request + ): JsonResponse { + // Récupérer les paramètres de requête + $zone = $request->query->get('zone'); + $pretty = $request->query->getBoolean('pretty', false); + $includeFollowups = $request->query->getBoolean('include_followups', true); + $includePlaces = $request->query->getBoolean('include_places', false); + + try { + // Construire la requête + $qb = $statsRepository->createQueryBuilder('s'); + + if ($zone) { + $qb->where('s.zone = :zone') + ->setParameter('zone', $zone); + } + + $stats = $qb->getQuery()->getResult(); + + if (empty($stats)) { + return new JsonResponse([ + 'error' => 'Aucun objet Stats trouvé', + 'message' => $zone ? "Aucune zone trouvée pour le code INSEE: $zone" : 'Aucune donnée disponible' + ], Response::HTTP_NOT_FOUND); + } + + // Préparer les données pour l'export + $exportData = []; + + foreach ($stats as $stat) { + $statData = [ + 'id' => $stat->getId(), + 'zone' => $stat->getZone(), + 'name' => $stat->getName(), + 'dateCreated' => $stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : null, + 'dateModified' => $stat->getDateModified() ? $stat->getDateModified()->format('Y-m-d H:i:s') : null, + 'population' => $stat->getPopulation(), + 'budgetAnnuel' => $stat->getBudgetAnnuel(), + 'siren' => $stat->getSiren(), + 'codeEpci' => $stat->getCodeEpci(), + 'codesPostaux' => $stat->getCodesPostaux(), + 'decomptes' => [ + 'placesCount' => $stat->getPlacesCount(), + 'avecHoraires' => $stat->getAvecHoraires(), + 'avecAdresse' => $stat->getAvecAdresse(), + 'avecSite' => $stat->getAvecSite(), + 'avecAccessibilite' => $stat->getAvecAccessibilite(), + 'avecNote' => $stat->getAvecNote(), + 'completionPercent' => $stat->getCompletionPercent(), + 'placesCountReal' => $stat->getPlaces()->count(), + ], + ]; + + // Ajouter les followups si demandé + if ($includeFollowups) { + $statData['followups'] = []; + foreach ($stat->getCityFollowUps() as $followup) { + $statData['followups'][] = [ + 'name' => $followup->getName(), + 'measure' => $followup->getMeasure(), + 'date' => $followup->getDate()->format('Y-m-d H:i:s') + ]; + } + } + + // Ajouter les lieux si demandé + if ($includePlaces) { + $statData['places'] = []; + foreach ($stat->getPlaces() as $place) { + $statData['places'][] = [ + 'id' => $place->getId(), + 'name' => $place->getName(), + 'mainTag' => $place->getMainTag(), + 'osmId' => $place->getOsmId(), + 'osmKind' => $place->getOsmKind(), + 'email' => $place->getEmail(), + 'note' => $place->getNote(), + 'zipCode' => $place->getZipCode(), + 'siret' => $place->getSiret(), + 'lat' => $place->getLat(), + 'lon' => $place->getLon(), + 'hasOpeningHours' => $place->hasOpeningHours(), + 'hasAddress' => $place->hasAddress(), + 'hasWebsite' => $place->hasWebsite(), + 'hasWheelchair' => $place->hasWheelchair(), + 'hasNote' => $place->hasNote(), + 'completionPercentage' => $place->getCompletionPercentage(), + ]; + } + } + + $exportData[] = $statData; + } + + // Préparer le JSON + $jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + if ($pretty) { + $jsonOptions |= JSON_PRETTY_PRINT; + } + + $jsonContent = json_encode($exportData, $jsonOptions); + + if (json_last_error() !== JSON_ERROR_NONE) { + return new JsonResponse([ + 'error' => 'Erreur lors de l\'encodage JSON', + 'message' => json_last_error_msg() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + // Retourner la réponse + $response = new JsonResponse($exportData, Response::HTTP_OK); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->set('Content-Disposition', 'attachment; filename="stats_export.json"'); + + // Ajouter des métadonnées dans les headers + $response->headers->set('X-Export-Count', count($stats)); + $response->headers->set('X-Export-Generated', (new \DateTime())->format('c')); + if ($zone) { + $response->headers->set('X-Export-Zone', $zone); + } + + return $response; + + } catch (\Exception $e) { + return new JsonResponse([ + 'error' => 'Erreur lors de l\'export', + 'message' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } } \ No newline at end of file