centralisation des tags de complétion par thème
This commit is contained in:
parent
6cfb2f0958
commit
c7e4f4e6a2
7 changed files with 433 additions and 184 deletions
|
@ -476,6 +476,7 @@ final class AdminController extends AbstractController
|
|||
'progression7Days' => $progression7Days,
|
||||
'all_types' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
'getTagEmoji' => [self::class, 'getTagEmoji'],
|
||||
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -621,6 +622,7 @@ final class AdminController extends AbstractController
|
|||
'josm_url' => $josm_url,
|
||||
'center' => $center,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -1922,4 +1924,295 @@ final class AdminController extends AbstractController
|
|||
default => '🏷️',
|
||||
};
|
||||
}
|
||||
|
||||
public function followupEmbedGraph(Request $request, string $insee_code, string $theme): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
$this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
$themes = \App\Service\FollowUpService::getFollowUpThemes();
|
||||
if (!isset($themes[$theme])) {
|
||||
$this->addFlash('error', 'Thème non reconnu.');
|
||||
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
|
||||
}
|
||||
|
||||
// Récupérer toutes les données de followup pour ce thème
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$countData = [];
|
||||
$completionData = [];
|
||||
|
||||
foreach ($followups as $fu) {
|
||||
if ($fu->getName() === $theme . '_count') {
|
||||
$countData[] = [
|
||||
'date' => $fu->getDate()->format('Y-m-d'),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
if ($fu->getName() === $theme . '_completion') {
|
||||
$completionData[] = [
|
||||
'date' => $fu->getDate()->format('Y-m-d'),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date
|
||||
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
|
||||
// Récupérer les objets du thème (Place) pour la ville
|
||||
$places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
$motocultrice = $this->motocultrice;
|
||||
$objects = [];
|
||||
// Récupérer la correspondance thème <-> requête Overpass
|
||||
$themeQueries = \App\Service\FollowUpService::getFollowUpOverpassQueries();
|
||||
$overpass_type_query = $themeQueries[$theme] ?? '';
|
||||
if ($overpass_type_query) {
|
||||
$overpass_query = "[out:json][timeout:60];\narea[\"ref:INSEE\"=\"$insee_code\"]->.searchArea;\n($overpass_type_query);\n(._;>;);\nout meta;\n>;";
|
||||
$josm_url = 'http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data=' . urlencode($overpass_query);
|
||||
} else {
|
||||
$josm_url = null;
|
||||
}
|
||||
// Fonction utilitaire pour extraire clé/valeur de la requête Overpass
|
||||
$extractTag = function ($query) {
|
||||
if (preg_match('/\\[([a-zA-Z0-9:_-]+)\\]="([^"]+)"/', $query, $matches)) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
return [null, null];
|
||||
};
|
||||
list($tagKey, $tagValue) = $extractTag($themeQueries[$theme] ?? '');
|
||||
foreach ($places as $place) {
|
||||
$match = false;
|
||||
// Cas particulier healthcare
|
||||
if ($theme === 'healthcare') {
|
||||
$main_tag = $place->getMainTag();
|
||||
if ($main_tag && (
|
||||
str_starts_with($main_tag, 'healthcare=') ||
|
||||
in_array($main_tag, [
|
||||
'amenity=doctors',
|
||||
'amenity=pharmacy',
|
||||
'amenity=hospital',
|
||||
'amenity=clinic',
|
||||
'amenity=social_facility'
|
||||
])
|
||||
)) {
|
||||
$match = true;
|
||||
}
|
||||
} elseif ($tagKey && $tagValue) {
|
||||
// On tente de retrouver la valeur du tag dans les propriétés Place
|
||||
$main_tag = $place->getMainTag();
|
||||
if ($main_tag === "$tagKey=$tagValue") {
|
||||
$match = true;
|
||||
}
|
||||
// Cas particuliers pour certains tags stockés ailleurs
|
||||
if (!$match) {
|
||||
if ($tagKey === 'highway' && method_exists($place, 'getOsmKind') && $place->getOsmKind() === $tagValue) {
|
||||
$match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($match && $place->getLat() && $place->getLon()) {
|
||||
$objects[] = [
|
||||
'id' => $place->getOsmId(),
|
||||
'osm_kind' => $place->getOsmKind(),
|
||||
'lat' => $place->getLat(),
|
||||
'lon' => $place->getLon(),
|
||||
'name' => $place->getName(),
|
||||
'tags' => ['main_tag' => $place->getMainTag()],
|
||||
'is_complete' => !empty($place->getName()),
|
||||
'osm_url' => 'https://www.openstreetmap.org/' . $place->getOsmKind() . '/' . $place->getOsmId(),
|
||||
'uuid' => $place->getUuidForUrl(),
|
||||
'zip_code' => $place->getZipCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
$geojson = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => array_map(function ($obj) {
|
||||
return [
|
||||
'type' => 'Feature',
|
||||
'geometry' => [
|
||||
'type' => 'Point',
|
||||
'coordinates' => [$obj['lon'], $obj['lat']]
|
||||
],
|
||||
'properties' => $obj
|
||||
];
|
||||
}, $objects)
|
||||
];
|
||||
|
||||
// Centre de la carte : centre géographique des objets ou de la ville
|
||||
$center = null;
|
||||
if (count($objects) > 0) {
|
||||
$lat = array_sum(array_column($objects, 'lat')) / count($objects);
|
||||
$lon = array_sum(array_column($objects, 'lon')) / count($objects);
|
||||
$center = [$lon, $lat];
|
||||
} elseif ($stats->getPlaces()->count() > 0) {
|
||||
$first = $stats->getPlaces()->first();
|
||||
$center = [$first->getLon(), $first->getLat()];
|
||||
}
|
||||
|
||||
return $this->render('admin/followup_embed_graph.html.twig', [
|
||||
'stats' => $stats,
|
||||
'theme' => $theme,
|
||||
'theme_label' => $themes[$theme],
|
||||
'count_data' => json_encode($countData),
|
||||
'completion_data' => json_encode($completionData),
|
||||
'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
|
||||
'geojson' => json_encode($geojson),
|
||||
'overpass_query' => $overpass_query,
|
||||
'josm_url' => $josm_url,
|
||||
'center' => $center,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function followupGraph(Request $request, string $insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
$this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
$themes = \App\Service\FollowUpService::getFollowUpThemes();
|
||||
if (!isset($themes[$theme])) {
|
||||
$this->addFlash('error', 'Thème non reconnu.');
|
||||
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
|
||||
}
|
||||
|
||||
// Récupérer toutes les données de followup pour ce thème
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$countData = [];
|
||||
$completionData = [];
|
||||
|
||||
foreach ($followups as $fu) {
|
||||
if ($fu->getName() === $theme . '_count') {
|
||||
$countData[] = [
|
||||
'date' => $fu->getDate()->format('Y-m-d'),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
if ($fu->getName() === $theme . '_completion') {
|
||||
$completionData[] = [
|
||||
'date' => $fu->getDate()->format('Y-m-d'),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date
|
||||
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
|
||||
// Récupérer les objets du thème (Place) pour la ville
|
||||
$places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
$motocultrice = $this->motocultrice;
|
||||
$objects = [];
|
||||
// Récupérer la correspondance thème <-> requête Overpass
|
||||
$themeQueries = \App\Service\FollowUpService::getFollowUpOverpassQueries();
|
||||
$overpass_type_query = $themeQueries[$theme] ?? '';
|
||||
if ($overpass_type_query) {
|
||||
$overpass_query = "[out:json][timeout:60];\narea[\"ref:INSEE\"=\"$insee_code\"]->.searchArea;\n($overpass_type_query);\n(._;>;);\nout meta;\n>;";
|
||||
$josm_url = 'http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data=' . urlencode($overpass_query);
|
||||
} else {
|
||||
$josm_url = null;
|
||||
}
|
||||
// Fonction utilitaire pour extraire clé/valeur de la requête Overpass
|
||||
$extractTag = function ($query) {
|
||||
if (preg_match('/\\[([a-zA-Z0-9:_-]+)\\]="([^"]+)"/', $query, $matches)) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
return [null, null];
|
||||
};
|
||||
list($tagKey, $tagValue) = $extractTag($themeQueries[$theme] ?? '');
|
||||
foreach ($places as $place) {
|
||||
$match = false;
|
||||
// Cas particulier healthcare
|
||||
if ($theme === 'healthcare') {
|
||||
$main_tag = $place->getMainTag();
|
||||
if ($main_tag && (
|
||||
str_starts_with($main_tag, 'healthcare=') ||
|
||||
in_array($main_tag, [
|
||||
'amenity=doctors',
|
||||
'amenity=pharmacy',
|
||||
'amenity=hospital',
|
||||
'amenity=clinic',
|
||||
'amenity=social_facility'
|
||||
])
|
||||
)) {
|
||||
$match = true;
|
||||
}
|
||||
} elseif ($tagKey && $tagValue) {
|
||||
// On tente de retrouver la valeur du tag dans les propriétés Place
|
||||
$main_tag = $place->getMainTag();
|
||||
if ($main_tag === "$tagKey=$tagValue") {
|
||||
$match = true;
|
||||
}
|
||||
// Cas particuliers pour certains tags stockés ailleurs
|
||||
if (!$match) {
|
||||
if ($tagKey === 'highway' && method_exists($place, 'getOsmKind') && $place->getOsmKind() === $tagValue) {
|
||||
$match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($match && $place->getLat() && $place->getLon()) {
|
||||
$objects[] = [
|
||||
'id' => $place->getOsmId(),
|
||||
'osm_kind' => $place->getOsmKind(),
|
||||
'lat' => $place->getLat(),
|
||||
'lon' => $place->getLon(),
|
||||
'name' => $place->getName(),
|
||||
'tags' => ['main_tag' => $place->getMainTag()],
|
||||
'is_complete' => !empty($place->getName()),
|
||||
'osm_url' => 'https://www.openstreetmap.org/' . $place->getOsmKind() . '/' . $place->getOsmId(),
|
||||
'uuid' => $place->getUuidForUrl(),
|
||||
'zip_code' => $place->getZipCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
$geojson = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => array_map(function ($obj) {
|
||||
return [
|
||||
'type' => 'Feature',
|
||||
'geometry' => [
|
||||
'type' => 'Point',
|
||||
'coordinates' => [$obj['lon'], $obj['lat']]
|
||||
],
|
||||
'properties' => $obj
|
||||
];
|
||||
}, $objects)
|
||||
];
|
||||
|
||||
// Centre de la carte : centre géographique des objets ou de la ville
|
||||
$center = null;
|
||||
if (count($objects) > 0) {
|
||||
$lat = array_sum(array_column($objects, 'lat')) / count($objects);
|
||||
$lon = array_sum(array_column($objects, 'lon')) / count($objects);
|
||||
$center = [$lon, $lat];
|
||||
} elseif ($stats->getPlaces()->count() > 0) {
|
||||
$first = $stats->getPlaces()->first();
|
||||
$center = [$first->getLon(), $first->getLat()];
|
||||
}
|
||||
|
||||
return $this->render('admin/followup_graph.html.twig', [
|
||||
'stats' => $stats,
|
||||
'theme' => $theme,
|
||||
'theme_label' => $themes[$theme],
|
||||
'count_data' => json_encode($countData),
|
||||
'completion_data' => json_encode($completionData),
|
||||
'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
|
||||
'geojson' => json_encode($geojson),
|
||||
'overpass_query' => $overpass_query,
|
||||
'josm_url' => $josm_url,
|
||||
'center' => $center,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
|
||||
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ class FollowUpController extends AbstractController
|
|||
'followup_labels' => FollowUpService::getFollowUpThemes(),
|
||||
'followup_icons' => FollowUpService::getFollowUpIcons(),
|
||||
'followup_overpass' => FollowUpService::getFollowUpOverpassQueries(),
|
||||
'completion_tags' => FollowUpService::getFollowUpCompletionTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -92,196 +92,46 @@ class FollowUpService
|
|||
$em->clear();
|
||||
}
|
||||
|
||||
// Suivi de la complétion personnalisé (exemples)
|
||||
// Suivi de la complétion basé sur les tags attendus
|
||||
$completionTags = self::getFollowUpCompletionTags();
|
||||
$expectedTags = $completionTags[$type] ?? [];
|
||||
$completed = [];
|
||||
if ($type === 'fire_hydrant') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$partialCompletions = [];
|
||||
if (!empty($expectedTags)) {
|
||||
foreach ($data['objects'] as $el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
// Considérer comme complet si au moins un critère supplémentaire est rempli
|
||||
return !empty($tags['ref'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null)
|
||||
|| !empty($tags['colour'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'charging_station') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['charging_station:output'] ?? null)
|
||||
|| !empty($tags['capacity'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'toilets') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return ($tags['wheelchair'] ?? null) === 'yes'
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null)
|
||||
|| !empty($tags['access'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'bus_stop') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
// Considérer comme complet si au moins un de ces critères est rempli
|
||||
return !empty($tags['shelter'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null)
|
||||
|| !empty($tags['network'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'defibrillator') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
// Considérer comme complet si au moins un de ces critères est rempli
|
||||
return !empty($tags['indoor'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null)
|
||||
|| !empty($tags['access'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'camera') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['surveillance:type'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'recycling') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['recycling_type'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'substation') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['substation'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'laboratory') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['website'] ?? null)
|
||||
|| !empty($tags['contact:website'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'school') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
// Considérer comme complet si au moins un de ces critères est rempli
|
||||
return !empty($tags['ref:UAI'] ?? null)
|
||||
|| !empty($tags['isced:level'] ?? null)
|
||||
|| !empty($tags['school:FR'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'police') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['website'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'healthcare') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['contact:phone'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['email'] ?? null)
|
||||
|| !empty($tags['contact:email'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'bicycle_parking') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['capacity'] ?? null)
|
||||
|| !empty($tags['covered'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'advertising_board') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['operator'] ?? null)
|
||||
|| !empty($tags['contact:phone'] ?? null)
|
||||
|| !empty($tags['name'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'building') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['ref'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'email') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'bench') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['material'] ?? null)
|
||||
|| !empty($tags['backrest'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'waste_basket') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['waste'] ?? null)
|
||||
|| !empty($tags['recycling_type'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'street_lamp') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['lamp_type'] ?? null)
|
||||
|| !empty($tags['height'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'drinking_water') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['covered'] ?? null)
|
||||
|| !empty($tags['ref'] ?? null)
|
||||
|| !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'tree') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
$hasTaxonomy = !empty($tags['species'] ?? null)
|
||||
|| !empty($tags['genus'] ?? null)
|
||||
|| !empty($tags['taxon'] ?? null)
|
||||
|| !empty($tags['taxon:binomial'] ?? null);
|
||||
$hasLeaf = !empty($tags['leaf_type'] ?? null)
|
||||
|| !empty($tags['leaf_cycle'] ?? null)
|
||||
|| !empty($tags['leaf_shape'] ?? null)
|
||||
|| !empty($tags['leaf_color'] ?? null)
|
||||
|| !empty($tags['leaf_fall'] ?? null);
|
||||
return $hasTaxonomy && $hasLeaf;
|
||||
});
|
||||
} elseif ($type === 'power_pole') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['ref'] ?? null)
|
||||
|| !empty($tags['material'] ?? null)
|
||||
|| !empty($tags['height'] ?? null)
|
||||
|| !empty($tags['operator'] ?? null);
|
||||
});
|
||||
$filled = 0;
|
||||
foreach ($expectedTags as $tag) {
|
||||
if ($tag === 'phone') {
|
||||
if (!empty($tags['phone'] ?? null) || !empty($tags['contact:phone'] ?? null)) {
|
||||
$filled++;
|
||||
}
|
||||
} elseif ($tag === 'email') {
|
||||
if (!empty($tags['email'] ?? null) || !empty($tags['contact:email'] ?? null)) {
|
||||
$filled++;
|
||||
}
|
||||
} else {
|
||||
if (!empty($tags[$tag] ?? null)) {
|
||||
$filled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$percent = count($expectedTags) > 0 ? ($filled / count($expectedTags)) : 0;
|
||||
$partialCompletions[] = $percent;
|
||||
if ($percent === 1.0) {
|
||||
$completed[] = $el;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... fallback pour les types sans tags attendus
|
||||
else {
|
||||
$completed = [];
|
||||
$partialCompletions = array_fill(0, count($data['objects']), 0);
|
||||
}
|
||||
if ($type === 'places') {
|
||||
$completion = $stats->getCompletionPercent();
|
||||
} else {
|
||||
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||
$completion = count($partialCompletions) > 0 ? round(array_sum($partialCompletions) / count($partialCompletions) * 100) : 0;
|
||||
}
|
||||
$followupCompletion = new CityFollowUp();
|
||||
$followupCompletion->setName($type . '_completion')
|
||||
|
@ -619,4 +469,36 @@ class FollowUpService
|
|||
// Si aucune mesure n'est disponible, retourner null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne pour chaque thématique la liste des tags attendus pour la complétion
|
||||
*/
|
||||
public static function getFollowUpCompletionTags(): array
|
||||
{
|
||||
return [
|
||||
'fire_hydrant' => ['ref', 'colour'],
|
||||
'charging_station' => ['charging_station:output', 'capacity'],
|
||||
'toilets' => ['wheelchair', 'access'],
|
||||
'bus_stop' => ['shelter', 'network'],
|
||||
'defibrillator' => ['indoor', 'access'],
|
||||
'camera' => ['surveillance:type'],
|
||||
'recycling' => ['recycling_type'],
|
||||
'substation' => ['substation'],
|
||||
'laboratory' => ['website', 'contact:website', 'name', 'phone'],
|
||||
'school' => ['ref:UAI', 'isced:level', 'school:FR'],
|
||||
'police' => ['phone', 'website'],
|
||||
'healthcare' => ['name', 'contact:phone', 'phone', 'email', 'contact:email'],
|
||||
'bicycle_parking' => ['capacity', 'covered'],
|
||||
'advertising_board' => ['operator', 'contact:phone'],
|
||||
'building' => ['name', 'ref'],
|
||||
'email' => ['name', 'phone'],
|
||||
'bench' => ['material', 'backrest'],
|
||||
'waste_basket' => ['waste', 'recycling_type'],
|
||||
'street_lamp' => ['lamp_type', 'height'],
|
||||
'drinking_water' => ['covered', 'ref'],
|
||||
'tree' => ['species', 'leaf_type', 'leaf_cycle'],
|
||||
'power_pole' => ['ref', 'material'],
|
||||
'places' => ['name', 'address', 'opening_hours', 'website', 'phone', 'wheelchair', 'siret'],
|
||||
];
|
||||
}
|
||||
}
|
29
templates/admin/_followup_completion_tags.html.twig
Normal file
29
templates/admin/_followup_completion_tags.html.twig
Normal file
|
@ -0,0 +1,29 @@
|
|||
{#
|
||||
Template partiel pour afficher les critères de complétion par thématique
|
||||
Variables attendues :
|
||||
- completion_tags : tableau [type => [tags...]]
|
||||
- followup_labels : tableau [type => label]
|
||||
- all_types : liste des types (clés)
|
||||
#}
|
||||
<div class="row mb-4">
|
||||
{% for type in all_types %}
|
||||
<div class="col-md-4 col-12 mb-2">
|
||||
<span class="fw-bold">{{ followup_labels[type]|default(type|capitalize) }}</span>
|
||||
<button class="btn btn-link p-0 ms-1" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-tags-{{ type }}" aria-expanded="false" aria-controls="collapse-tags-{{ type }}" title="Voir les critères de complétion">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
<div class="collapse mt-2" id="collapse-tags-{{ type }}">
|
||||
<div class="card card-body p-2 small">
|
||||
<span class="fw-bold">Critères de complétion attendus :</span>
|
||||
<ul class="mb-0">
|
||||
{% for tag in completion_tags[type] ?? [] %}
|
||||
<li><code>{{ tag }}</code></li>
|
||||
{% else %}
|
||||
<li><span class="text-muted">Aucun critère défini</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -59,6 +59,11 @@
|
|||
</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>
|
||||
</div>
|
||||
{% include 'admin/_followup_completion_tags.html.twig' with {
|
||||
'completion_tags': completion_tags,
|
||||
'followup_labels': followup_labels,
|
||||
'all_types': followup_labels|keys
|
||||
} %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
|
|
|
@ -228,6 +228,23 @@
|
|||
<div class="chart-container">
|
||||
<canvas id="themeChart"></canvas>
|
||||
</div>
|
||||
|
||||
{% if completion_tags is defined and completion_tags[theme] is defined %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle"></i> Critères de complétion attendus pour ce thème
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<ul class="mb-0">
|
||||
{% for tag in completion_tags[theme] %}
|
||||
<li><code>{{ tag }}</code></li>
|
||||
{% else %}
|
||||
<li><span class="text-muted">Aucun critère défini</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -339,6 +339,28 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
{% for type in all_types %}
|
||||
<div class="col-md-4 col-12 mb-2">
|
||||
<span class="fw-bold">{{ followup_labels[type]|default(type|capitalize) }}</span>
|
||||
<button class="btn btn-link p-0 ms-1" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-tags-{{ type }}" aria-expanded="false" aria-controls="collapse-tags-{{ type }}" title="Voir les critères de complétion">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
<div class="collapse mt-2" id="collapse-tags-{{ type }}">
|
||||
<div class="card card-body p-2 small">
|
||||
<span class="fw-bold">Critères de complétion attendus :</span>
|
||||
<ul class="mb-0">
|
||||
{% for tag in completion_tags[type] ?? [] %}
|
||||
<li><code>{{ tag }}</code></li>
|
||||
{% else %}
|
||||
<li><span class="text-muted">Aucun critère défini</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue