harmoniser les décomptes de completion sur la page stats

This commit is contained in:
Tykayn 2025-08-18 12:41:31 +02:00 committed by tykayn
parent b9f57e48b5
commit 060b23f87e
3 changed files with 399 additions and 345 deletions

View file

@ -303,11 +303,11 @@ final class AdminController extends AbstractController
return $this->redirectToRoute('app_admin_import_stats'); return $this->redirectToRoute('app_admin_import_stats');
} }
$completion = $stats->getCompletionPercent(); // $completion = $stats->getCompletionPercent();
if (!$completion) { // if (!$completion) {
$stats->computeCompletionPercent(); // $stats->computeCompletionPercent();
$completion = $stats->getCompletionPercent(); // $completion = $stats->getCompletionPercent();
} // }
$followups = $stats->getCityFollowUps(); $followups = $stats->getCityFollowUps();
$refresh = false; $refresh = false;
if (!$followups->isEmpty()) { if (!$followups->isEmpty()) {
@ -378,7 +378,7 @@ final class AdminController extends AbstractController
// Update the places_count property // Update the places_count property
$stats->setPlacesCount($stats->getPlaces()->count()); $stats->setPlacesCount($stats->getPlaces()->count());
$this->entityManager->persist($stats); // $this->entityManager->persist($stats);
} }
$this->entityManager->flush(); $this->entityManager->flush();
@ -433,6 +433,15 @@ final class AdminController extends AbstractController
if (isset($commerces) && is_iterable($commerces)) { if (isset($commerces) && is_iterable($commerces)) {
foreach ($commerces as $commerce) { foreach ($commerces as $commerce) {
if ($commerce->getLat() && $commerce->getLon()) { if ($commerce->getLat() && $commerce->getLon()) {
// Collect missing tags
$missingTags = [];
if (!$commerce->getName()) $missingTags[] = 'name';
if (!$commerce->hasAddress()) $missingTags[] = 'address';
if (!$commerce->hasOpeningHours()) $missingTags[] = 'opening_hours';
if (!$commerce->hasWebsite()) $missingTags[] = 'website';
if (!$commerce->hasWheelchair()) $missingTags[] = 'wheelchair';
if (!$commerce->getSiret()) $missingTags[] = 'siret';
$geojson['features'][] = [ $geojson['features'][] = [
'type' => 'Feature', 'type' => 'Feature',
'geometry' => [ 'geometry' => [
@ -445,7 +454,9 @@ final class AdminController extends AbstractController
'main_tag' => $commerce->getMainTag(), 'main_tag' => $commerce->getMainTag(),
'address' => $commerce->getStreet() . ' ' . $commerce->getHousenumber(), 'address' => $commerce->getStreet() . ' ' . $commerce->getHousenumber(),
'note' => $commerce->getNoteContent(), 'note' => $commerce->getNoteContent(),
'osm_url' => 'https://www.openstreetmap.org/' . $commerce->getOsmKind() . '/' . $commerce->getOsmId() 'osm_url' => 'https://www.openstreetmap.org/' . $commerce->getOsmKind() . '/' . $commerce->getOsmId(),
'completion' => $commerce->getCompletionPercentage(),
'missing_tags' => $missingTags
] ]
]; ];
} }
@ -1955,8 +1966,24 @@ final class AdminController extends AbstractController
} }
unset($points); unset($points);
$now = new \DateTime();
$last_week = new \DateTime();
$last_week->sub(new \DateInterval('P7D'));
$adiff_query = '[timeout:60]
[adiff:"' . $now->format('Y-m-d') . 'T' . $now->format('H:i:s') . 'Z","2025-08-05T00:00:00Z"][out:xml];
area["ref:INSEE"="91111"]->.searchArea;
(
nwr(area.searchArea);
);
out meta;';
return $this->render('admin/followup_graph.html.twig', [ return $this->render('admin/followup_graph.html.twig', [
'adiff_query' => $adiff_query,
'stats' => $stats, 'stats' => $stats,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(), 'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(), 'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(), 'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),

View file

@ -23,10 +23,12 @@
<a href="{{ path('admin_followup', {'insee_code': stats.zone}) }}" class="btn btn-warning"> <a href="{{ path('admin_followup', {'insee_code': stats.zone}) }}" class="btn btn-warning">
<i class="bi bi-arrow-repeat"></i> Mettre à jour les suivis (followup) <i class="bi bi-arrow-repeat"></i> Mettre à jour les suivis (followup)
</a> </a>
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}" class="btn btn-primary"> <a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}"
class="btn btn-primary">
<i class="bi bi-shovel"></i> Labourer la zone <i class="bi bi-shovel"></i> Labourer la zone
</a> </a>
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1, 'disableFollowUpCleanup': 1}) }}" class="btn btn-warning" title="Labourer sans nettoyer les suivis OSM"> <a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1, 'disableFollowUpCleanup': 1}) }}"
class="btn btn-warning" title="Labourer sans nettoyer les suivis OSM">
<i class="bi bi-shield-check"></i> Labourer (sans nettoyage) <i class="bi bi-shield-check"></i> Labourer (sans nettoyage)
</a> </a>
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-info"> <a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-info">
@ -40,7 +42,15 @@
<thead> <thead>
<tr> <tr>
<th>Thème</th> <th>Thème</th>
<th>Évolution 7j</th> <th>
{# <pre> #}
{# {{ dump(adiff_query ) }} #}
{# </pre> #}
{# <a href="https://overpass-turbo.eu/?Q={{ adiff_query|url_encode }}" target="_blank" #}
{# title="Voir les changements des 7 derniers jours sur Overpass Turbo">Évolution 7j #}
{# <i class="bi bi-box-arrow-up-right"></i></a> #}
</th>
<th>Évolution 30j</th> <th>Évolution 30j</th>
<th>Évolution 6 mois</th> <th>Évolution 6 mois</th>
</tr> </tr>
@ -52,7 +62,8 @@
{% set has_change = true %} {% set has_change = true %}
<tr> <tr>
<td> <td>
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': type}) }}" class="fw-bold text-decoration-none"> <a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': type}) }}"
class="fw-bold text-decoration-none">
{{ tag_emoji(type) }} {{ diff.label }} {{ tag_emoji(type) }} {{ diff.label }}
</a> </a>
</td> </td>
@ -64,7 +75,8 @@
{% else %} {% else %}
<i class="bi bi-arrow-right text-secondary"></i> <i class="bi bi-arrow-right text-secondary"></i>
{% endif %} {% endif %}
{{ diff.count_diff_7j > 0 ? '+' ~ diff.count_diff_7j : diff.count_diff_7j }} objets {{ diff.count_diff_7j > 0 ? '+' ~ diff.count_diff_7j : diff.count_diff_7j }}
objets
<br> <br>
<span class="small text-muted">Complétion : <span class="small text-muted">Complétion :
{% if diff.completion_diff_7j > 0 %} {% if diff.completion_diff_7j > 0 %}
@ -85,7 +97,8 @@
{% else %} {% else %}
<i class="bi bi-arrow-right text-secondary"></i> <i class="bi bi-arrow-right text-secondary"></i>
{% endif %} {% endif %}
{{ diff.count_diff_30j > 0 ? '+' ~ diff.count_diff_30j : diff.count_diff_30j }} objets {{ diff.count_diff_30j > 0 ? '+' ~ diff.count_diff_30j : diff.count_diff_30j }}
objets
<br> <br>
<span class="small text-muted">Complétion : <span class="small text-muted">Complétion :
{% if diff.completion_diff_30j > 0 %} {% if diff.completion_diff_30j > 0 %}
@ -106,7 +119,8 @@
{% else %} {% else %}
<i class="bi bi-arrow-right text-secondary"></i> <i class="bi bi-arrow-right text-secondary"></i>
{% endif %} {% endif %}
{{ diff.count_diff_6mois > 0 ? '+' ~ diff.count_diff_6mois : diff.count_diff_6mois }} objets {{ diff.count_diff_6mois > 0 ? '+' ~ diff.count_diff_6mois : diff.count_diff_6mois }}
objets
<br> <br>
<span class="small text-muted">Complétion : <span class="small text-muted">Complétion :
{% if diff.completion_diff_6mois > 0 %} {% if diff.completion_diff_6mois > 0 %}
@ -123,26 +137,37 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if not has_change %} {% if not has_change %}
<tr><td colspan="4" class="text-muted">Aucun changement significatif cette semaine.</td></tr> <tr>
<td colspan="4" class="text-muted">Aucun changement significatif cette semaine.</td>
</tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% for type, label in followup_labels %} {% for type, label in followup_labels %}
<h2 id="title-{{ type }}"><i class="bi {{ followup_icons[type]|default('bi-question-circle') }} fs-2"></i> {{ label }}</h2> <h2 id="title-{{ type }}"><i
class="bi {{ followup_icons[type]|default('bi-question-circle') }} fs-2"></i> {{ label }}
</h2>
<canvas id="{{ type }}Chart" width="600" height="400"></canvas> <canvas id="{{ type }}Chart" width="600" height="400"></canvas>
<div class="mb-3"> <div class="mb-3">
{% set overpass_query = '[out:json][timeout:60];\narea["ref:INSEE"="' ~ stats.zone ~ '"]->.searchArea;\n(' ~ followup_overpass[type]|default('') ~ ');\n\n(._;>;);\n\nout meta;\n>;' %} {% set overpass_query = '[out:json][timeout:60];\narea["ref:INSEE"="' ~ stats.zone ~ '"]->.searchArea;\n(' ~ followup_overpass[type]|default('') ~ ');\n\n(._;>;);\n\nout meta;\n>;' %}
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-sm btn-outline-primary me-2"> <a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank"
class="btn btn-sm btn-outline-primary me-2">
<i class="bi bi-geo"></i> Voir sur Overpass Turbo <i class="bi bi-geo"></i> Voir sur Overpass Turbo
</a> </a>
<a href="http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode }}" target="_blank" class="btn btn-sm btn-outline-success"> <a href="http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode }}"
target="_blank" class="btn btn-sm btn-outline-success">
<i class="bi bi-box-arrow-in-up-right"></i> Ouvrir dans JOSM <i class="bi bi-box-arrow-in-up-right"></i> Ouvrir dans JOSM
</a> </a>
<a href="{{ path('admin_followup_embed_graph', {'insee_code': stats.zone, 'theme': type}) }}" target="_blank" class="btn btn-sm btn-outline-secondary ms-2"> <a href="{{ path('admin_followup_embed_graph', {'insee_code': stats.zone, 'theme': type}) }}"
target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
<i class="bi bi-code-slash"></i> Version embarquée <i class="bi bi-code-slash"></i> Version embarquée
</a> </a>
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': type}) }}"
target="_blank" class="btn btn-sm btn-outline-secondary ms-2">
<i class="bi bi-graph-up-arrow"></i> Détails
</a>
</div> </div>
{% include 'admin/_followup_completion_tags.html.twig' with { {% include 'admin/_followup_completion_tags.html.twig' with {
'completion_tags': completion_tags, 'completion_tags': completion_tags,
@ -175,7 +200,8 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-secondary mt-3"><i class="bi bi-arrow-left"></i> Retour à la fiche ville</a> <a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}"
class="btn btn-secondary mt-3"><i class="bi bi-arrow-left"></i> Retour à la fiche ville</a>
</div> </div>
</div> </div>
</div> </div>
@ -227,7 +253,9 @@
align: 'top', align: 'top',
anchor: 'end', anchor: 'end',
display: true, display: true,
formatter: function(value) { return value.y; }, formatter: function (value) {
return value.y;
},
font: {weight: 'bold'} font: {weight: 'bold'}
} }
}, },
@ -242,7 +270,9 @@
align: 'bottom', align: 'bottom',
anchor: 'end', anchor: 'end',
display: true, display: true,
formatter: function(value) { return value.y + '%'; }, formatter: function (value) {
return value.y + '%';
},
font: {weight: 'bold'} font: {weight: 'bold'}
} }
} }
@ -274,7 +304,14 @@
scales: { scales: {
x: {type: 'time', time: {unit: 'day'}, title: {display: true, text: 'Date'}}, x: {type: 'time', time: {unit: 'day'}, title: {display: true, text: 'Date'}},
y: {beginAtZero: true, title: {display: true, text: 'Nombre'}}, y: {beginAtZero: true, title: {display: true, text: 'Nombre'}},
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false }, min: 0, max: 100 } y1: {
beginAtZero: true,
position: 'right',
title: {display: true, text: 'Complétion (%)'},
grid: {drawOnChartArea: false},
min: 0,
max: 100
}
} }
}, },
plugins: [ChartDataLabels] plugins: [ChartDataLabels]

View file

@ -150,6 +150,60 @@
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1> {{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
</div> </div>
</div> </div>
<div id="carte" class="section-anchor">
<h2>Carte</h2>
<div id="maploader">
<div class="spinner-border" role="status">
<i class="bi bi-load bi-spin"></i>
<span class="visually-hidden">Chargement de la carte...</span>
</div>
</div>
<div class="d-flex justify-content-end mb-2">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary" id="circleMarkersBtn">
<i class="bi bi-circle"></i> Cercles
</button>
<button type="button" class="btn btn-outline-primary active" id="dropMarkersBtn">
<i class="bi bi-geo-alt"></i> Gouttes
</button>
</div>
<button id="btn-geolocate" class="btn btn-outline-primary btn-sm">
<i class="bi bi-geo-alt"></i> Me localiser
</button>
</div>
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
<div id="graphiques" class="section-anchor">
<h2>Graphiques</h2>
<div class="row">
<div class="col-md-6 col-12">
<canvas id="repartition_tags" width="600" height="400"
style="max-width:100%; margin: 20px 0;"></canvas>
</div>
<div class="col-md-6 col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour par
trimestre pour {{ stats.name }}
</div>
<div class="card-body">
<canvas id="modificationsByQuarterChart"
style="min-height: 250px; width: 100%;"></canvas>
</div>
</div>
</div>
</div>
</div>
<div id="attribution">
<a href="https://www.openstreetmap.org/copyright">Données OpenStreetMap</a>
</div>
</div>
{% if stats.population %} {% if stats.population %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-4 col-12"> <div class="col-md-4 col-12">
@ -466,57 +520,6 @@
</div> </div>
<div id="carte" class="section-anchor">
<h2>Carte</h2>
<div id="maploader">
<div class="spinner-border" role="status">
<i class="bi bi-load bi-spin"></i>
<span class="visually-hidden">Chargement de la carte...</span>
</div>
</div>
<div class="d-flex justify-content-end mb-2">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary" id="circleMarkersBtn">
<i class="bi bi-circle"></i> Cercles
</button>
<button type="button" class="btn btn-outline-primary active" id="dropMarkersBtn">
<i class="bi bi-geo-alt"></i> Gouttes
</button>
</div>
<button id="btn-geolocate" class="btn btn-outline-primary btn-sm">
<i class="bi bi-geo-alt"></i> Me localiser
</button>
</div>
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
<div id="graphiques" class="section-anchor">
<h2>Graphiques</h2>
<div class="row">
<div class="col-md-6 col-12">
<canvas id="repartition_tags" width="600" height="400"
style="max-width:100%; margin: 20px 0;"></canvas>
</div>
<div class="col-md-6 col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour par
trimestre pour {{ stats.name }}
</div>
<div class="card-body">
<canvas id="modificationsByQuarterChart"
style="min-height: 250px; width: 100%;"></canvas>
</div>
</div>
</div>
</div>
</div>
<div id="attribution">
<a href="https://www.openstreetmap.org/copyright">Données OpenStreetMap</a>
</div>
</div>
<div class="card mt-4"> <div class="card mt-4">
{% include 'admin/stats_history.html.twig' with {stat: stats} %} {% include 'admin/stats_history.html.twig' with {stat: stats} %}
<canvas id="distribution_completion" class="mt-4 mb-4" height="400"></canvas> <canvas id="distribution_completion" class="mt-4 mb-4" height="400"></canvas>
@ -718,23 +721,10 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const geojsonData = {{ geojson|raw }}; const geojsonData = {{ geojson|raw }};
const map_token = "{{ maptiler_token }}"; const map_token = "{{ maptiler_token }}";
// Liste des tags attendus pour la complétion des lieux // Les valeurs de complétion et tags manquants sont maintenant calculées côté serveur
const completionTags = {{ completion_tags['places']|json_encode|raw }}; // et incluses directement dans les données GeoJSON
// Calcul de la complétion et des tags manquants pour chaque lieu
geojsonData.features.forEach(f => { geojsonData.features.forEach(f => {
let filled = 0; // Assurons-nous que missing_tags est toujours un tableau
let missing = [];
if (completionTags && completionTags.length > 0) {
completionTags.forEach(tag => {
if (f.properties && typeof f.properties[tag] !== 'undefined' && f.properties[tag] !== null && f.properties[tag] !== '') {
filled++;
} else {
missing.push(tag);
}
});
}
f.properties.completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null;
// Correction : toujours un tableau
f.properties.missing_tags = Array.isArray(f.properties.missing_tags) ? f.properties.missing_tags : (f.properties.missing_tags ? [f.properties.missing_tags] : []); f.properties.missing_tags = Array.isArray(f.properties.missing_tags) ? f.properties.missing_tags : (f.properties.missing_tags ? [f.properties.missing_tags] : []);
}); });