mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
calcul progression
This commit is contained in:
parent
2bbc7153c6
commit
c81affd3e3
8 changed files with 275 additions and 50 deletions
|
@ -446,6 +446,13 @@ final class AdminController extends AbstractController
|
|||
if ($count) $latestFollowups['places']['count'] = $count;
|
||||
if ($completion) $latestFollowups['places']['completion'] = $completion;
|
||||
|
||||
// Calculer la progression sur 7 jours pour chaque type
|
||||
$progression7Days = [];
|
||||
foreach ($types as $type) {
|
||||
$progression7Days[$type] = \App\Service\FollowUpService::calculate7DayProgression($stats, $type);
|
||||
}
|
||||
$progression7Days['places'] = \App\Service\FollowUpService::calculate7DayProgression($stats, 'places');
|
||||
|
||||
return $this->render('admin/stats.html.twig', [
|
||||
'stats' => $stats,
|
||||
'commerces' => $commerces,
|
||||
|
@ -460,6 +467,7 @@ final class AdminController extends AbstractController
|
|||
'latestFollowups' => $latestFollowups,
|
||||
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
|
||||
'progression7Days' => $progression7Days,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -530,6 +538,7 @@ final class AdminController extends AbstractController
|
|||
public function labourer(Request $request, string $insee_code, bool $updateExisting = true): Response
|
||||
{
|
||||
$deleteMissing = $request->query->getBoolean('deleteMissing', true);
|
||||
$disableFollowUpCleanup = $request->query->getBoolean('disableFollowUpCleanup', false);
|
||||
|
||||
$this->actionLogger->log('labourer', ['insee_code' => $insee_code]);
|
||||
|
||||
|
@ -890,7 +899,7 @@ final class AdminController extends AbstractController
|
|||
$this->entityManager->flush();
|
||||
|
||||
// Générer les suivis (followups) après la mise à jour des Places
|
||||
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager);
|
||||
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, $disableFollowUpCleanup);
|
||||
|
||||
$message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.';
|
||||
if ($updateExisting) {
|
||||
|
|
|
@ -8,7 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
|
||||
class FollowUpService
|
||||
{
|
||||
public function generateCityFollowUps(Stats $stats, Motocultrice $motocultrice, EntityManagerInterface $em): void
|
||||
public function generateCityFollowUps(Stats $stats, Motocultrice $motocultrice, EntityManagerInterface $em, bool $disableCleanup = false): void
|
||||
{
|
||||
$insee_code = $stats->getZone();
|
||||
$elements = $motocultrice->followUpCity($insee_code) ?? [];
|
||||
|
@ -71,13 +71,13 @@ class FollowUpService
|
|||
} else {
|
||||
$objects = [];
|
||||
}
|
||||
$types[$type] = [
|
||||
'label' => $label,
|
||||
'objects' => $objects
|
||||
];
|
||||
$types[$type] = ['objects' => $objects, 'label' => $label];
|
||||
}
|
||||
$types['places'] = ['objects' => [], 'label' => 'Lieux'];
|
||||
|
||||
$now = new \DateTime();
|
||||
$persisted = 0;
|
||||
|
||||
foreach ($types as $type => $data) {
|
||||
// Suivi du nombre
|
||||
$followupCount = new CityFollowUp();
|
||||
|
@ -153,30 +153,27 @@ class FollowUpService
|
|||
});
|
||||
} elseif ($type === 'advertising_board') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
// On considère complet si le tag "operator" ou "ref" est présent
|
||||
return !empty($el['tags']['operator'] ?? null) || !empty($el['tags']['ref'] ?? null);
|
||||
return !empty($el['tags']['operator'] ?? null) || !empty($el['tags']['contact:phone'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'building') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
// Complet si ref:FR:RNB est rempli
|
||||
return !empty($el['tags']['ref:FR:RNB'] ?? null);
|
||||
return !empty($el['tags']['name'] ?? null) || !empty($el['tags']['ref'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'email') {
|
||||
$completed = $data['objects']; // Possède déjà un email ou contact:email
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['name'] ?? null) || !empty($el['tags']['phone'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'bench') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
// Complet si le tag "material" ou "backrest" est présent
|
||||
return !empty($el['tags']['material'] ?? null) || !empty($el['tags']['backrest'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'waste_basket') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
// Complet si le tag "material" ou "ref" est présent
|
||||
return !empty($el['tags']['material'] ?? null) || !empty($el['tags']['ref'] ?? null);
|
||||
return !empty($el['tags']['waste'] ?? null) || !empty($el['tags']['recycling_type'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'street_lamp') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
// Complet si le tag "lamp_type" ou "ref" est présent
|
||||
return !empty($el['tags']['lamp_type'] ?? null) || !empty($el['tags']['ref'] ?? null);
|
||||
return !empty($el['tags']['lamp_type'] ?? null) || !empty($el['tags']['height'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'drinking_water') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
|
@ -226,38 +223,40 @@ class FollowUpService
|
|||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
// Suppression des mesures redondantes (même valeur consécutive, sauf la dernière)
|
||||
$repo = $em->getRepository(CityFollowUp::class);
|
||||
foreach (array_keys(self::getFollowUpThemes()) as $type) {
|
||||
foreach (['_count', '_completion'] as $suffix) {
|
||||
$name = $type . $suffix;
|
||||
$followups = $repo->createQueryBuilder('f')
|
||||
->where('f.stats = :stats')
|
||||
->andWhere('f.name = :name')
|
||||
->setParameter('stats', $stats)
|
||||
->setParameter('name', $name)
|
||||
->orderBy('f.date', 'ASC')
|
||||
->getQuery()->getResult();
|
||||
$toDelete = [];
|
||||
$prev = null;
|
||||
$n = count($followups);
|
||||
foreach ($followups as $i => $fu) {
|
||||
// Si seulement 2 mesures, ne rien supprimer
|
||||
if ($n == 2) {
|
||||
break;
|
||||
// Suppression des mesures redondantes (même valeur consécutive, sauf la dernière) - désactivée si $disableCleanup est true
|
||||
if (!$disableCleanup) {
|
||||
$repo = $em->getRepository(CityFollowUp::class);
|
||||
foreach (array_keys(self::getFollowUpThemes()) as $type) {
|
||||
foreach (['_count', '_completion'] as $suffix) {
|
||||
$name = $type . $suffix;
|
||||
$followups = $repo->createQueryBuilder('f')
|
||||
->where('f.stats = :stats')
|
||||
->andWhere('f.name = :name')
|
||||
->setParameter('stats', $stats)
|
||||
->setParameter('name', $name)
|
||||
->orderBy('f.date', 'ASC')
|
||||
->getQuery()->getResult();
|
||||
$toDelete = [];
|
||||
$prev = null;
|
||||
$n = count($followups);
|
||||
foreach ($followups as $i => $fu) {
|
||||
// Si seulement 2 mesures, ne rien supprimer
|
||||
if ($n == 2) {
|
||||
break;
|
||||
}
|
||||
if ($prev && $fu->getMeasure() === $prev->getMeasure() && $i < $n - 1) {
|
||||
$toDelete[] = $prev;
|
||||
}
|
||||
$prev = $fu;
|
||||
}
|
||||
if ($prev && $fu->getMeasure() === $prev->getMeasure() && $i < $n - 1) {
|
||||
$toDelete[] = $prev;
|
||||
foreach ($toDelete as $del) {
|
||||
$em->remove($del);
|
||||
}
|
||||
$prev = $fu;
|
||||
}
|
||||
foreach ($toDelete as $del) {
|
||||
$em->remove($del);
|
||||
}
|
||||
}
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
}
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
}
|
||||
|
||||
public function generateGlobalFollowUps(EntityManagerInterface $em): void
|
||||
|
@ -428,7 +427,121 @@ class FollowUpService
|
|||
'street_lamp' => 'nwr["highway"="street_lamp"](area.searchArea);',
|
||||
'drinking_water' => 'nwr["amenity"="drinking_water"](area.searchArea);',
|
||||
'tree' => 'nwr["natural"="tree"](area.searchArea);',
|
||||
'places' => ''
|
||||
'power_pole' => 'nwr["power"="pole"](area.searchArea);',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la progression sur 7 jours pour un type donné
|
||||
*/
|
||||
public static function calculate7DayProgression(Stats $stats, string $type): array
|
||||
{
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$now = new \DateTime();
|
||||
$refDate = clone $now;
|
||||
$refDate->modify('-7 days');
|
||||
|
||||
// Récupérer toutes les mesures pour ce type
|
||||
$countData = [];
|
||||
$completionData = [];
|
||||
|
||||
foreach ($followups as $fu) {
|
||||
if ($fu->getName() === $type . '_count') {
|
||||
$countData[] = [
|
||||
'date' => $fu->getDate(),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
if ($fu->getName() === $type . '_completion') {
|
||||
$completionData[] = [
|
||||
'date' => $fu->getDate(),
|
||||
'value' => $fu->getMeasure()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date
|
||||
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
|
||||
$countDelta = self::calculateDelta($countData, $refDate);
|
||||
$completionDelta = self::calculateDelta($completionData, $refDate);
|
||||
|
||||
return [
|
||||
'count' => $countDelta,
|
||||
'completion' => $completionDelta
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le delta pour une série de données
|
||||
*/
|
||||
private static function calculateDelta(array $data, \DateTime $refDate): ?float
|
||||
{
|
||||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$last = end($data)['value'];
|
||||
|
||||
// Chercher la mesure exacte à la date de référence
|
||||
$exactRef = null;
|
||||
foreach (array_reverse($data) as $point) {
|
||||
if ($point['date'] <= $refDate) {
|
||||
$exactRef = $point['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si on a trouvé une mesure exacte, l'utiliser
|
||||
if ($exactRef !== null) {
|
||||
return $last - $exactRef;
|
||||
}
|
||||
|
||||
// Sinon, chercher les deux mesures les plus proches pour faire une interpolation
|
||||
$beforeRef = null;
|
||||
$afterRef = null;
|
||||
$beforeDate = null;
|
||||
$afterDate = null;
|
||||
|
||||
// Chercher la mesure juste avant la date de référence
|
||||
foreach (array_reverse($data) as $point) {
|
||||
if ($point['date'] < $refDate) {
|
||||
$beforeRef = $point['value'];
|
||||
$beforeDate = $point['date'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Chercher la mesure juste après la date de référence
|
||||
foreach ($data as $point) {
|
||||
if ($point['date'] > $refDate) {
|
||||
$afterRef = $point['value'];
|
||||
$afterDate = $point['date'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si on a les deux mesures, faire une interpolation linéaire
|
||||
if ($beforeRef !== null && $afterRef !== null && $beforeDate !== null && $afterDate !== null) {
|
||||
$timeDiff = $afterDate->getTimestamp() - $beforeDate->getTimestamp();
|
||||
$refTimeDiff = $refDate->getTimestamp() - $beforeDate->getTimestamp();
|
||||
$ratio = $refTimeDiff / $timeDiff;
|
||||
$interpolatedRef = $beforeRef + ($afterRef - $beforeRef) * $ratio;
|
||||
return $last - $interpolatedRef;
|
||||
}
|
||||
|
||||
// Si on n'a qu'une mesure avant, l'utiliser
|
||||
if ($beforeRef !== null) {
|
||||
return $last - $beforeRef;
|
||||
}
|
||||
|
||||
// Si on n'a qu'une mesure après, l'utiliser
|
||||
if ($afterRef !== null) {
|
||||
return $last - $afterRef;
|
||||
}
|
||||
|
||||
// Si aucune mesure n'est disponible, retourner null
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ class Motocultrice
|
|||
|
||||
public $overpass_base_places = '
|
||||
(
|
||||
nw["amenity"~"^(restaurant|fast_food|cafe|fuel|pharmacy|bank|bar|hospital|post_office|clinic|pub|car_wash|ice_cream|driving_school|cinema|car_rental|nightclub|bureau_de_change|studio|internet_cafe|money_transfer|casino|vehicle_inspection|frozen_food|boat_rental|coworking_space|workshop|personal_service|camping|dancing_school|training|ski_school|ski_rental|dive_centre|driver_training|nursing_home|funeral_hall|doctors|dentist|theatre|kindergarten|language_school|stripclub|veterinary|convenience|supermarket|clothes|hairdresser|car_repair|bakery|beauty|car|hardware|mobile_phone|butcher|furniture|car_parts|alcohol|florist|scooter|variety_store|electronics|shoes|optician|jewelry|mall|gift|doityourself|greengrocer|books|bicycle|chemist|department_store|laundry|travel_agency|stationery|pet|sports|confectionery|tyres|cosmetics|computer|tailor|tobacco|storage_rental|dry_cleaning|trade|copyshop|motorcycle|funeral_directors|beverages|newsagent|garden_centre|massage|pastry|interior_decoration|general|deli|toys|houseware|wine|seafood|pawnbroker|tattoo|paint|wholesale|photo|second_hand|bed|kitchen|outdoor|fabric|antiques|coffee|gas|e-cigarette|perfumery|craft|hearing_aids|money_lender|appliance|electrical|tea|motorcycle_repair|boutique|baby_goods|bag|musical_instrument|dairy|pet_grooming|music|carpet|rental|fashion_accessories|cheese|chocolate|medical_supply|leather|sewing|cannabis|locksmith|games|video_games|hifi|window_blind|caravan|tool_hire|household_linen|bathroom_furnishing|shoe_repair|watches|nutrition_supplements|fishing|erotic|frame|grocery|boat|repair|weapons|gold_buyer|lighting|pottery|security|groundskeeping|herbalist|curtain|health_food|flooring|printer_ink|anime|camera|scuba_diving|candles|printing|garden_furniture|food|estate_agent|insurance|it|accountant|employment_agency|tax_advisor|financial|advertising_agency|logistics|newspaper|financial_advisor|consulting|travel_agent|coworking|moving_company|lawyer|architect|construction_company|credit_broker|graphic_design|property_management|cleaning)$"](area.searchArea);
|
||||
nw["amenity"~"^(library|townhall|restaurant|fast_food|cafe|fuel|pharmacy|bank|bar|hospital|post_office|clinic|pub|car_wash|ice_cream|driving_school|cinema|car_rental|nightclub|bureau_de_change|studio|internet_cafe|money_transfer|casino|vehicle_inspection|frozen_food|boat_rental|coworking_space|workshop|personal_service|camping|dancing_school|training|ski_school|ski_rental|dive_centre|driver_training|nursing_home|funeral_hall|doctors|dentist|theatre|kindergarten|language_school|stripclub|veterinary|convenience|supermarket|clothes|hairdresser|car_repair|bakery|beauty|car|hardware|mobile_phone|butcher|furniture|car_parts|alcohol|florist|scooter|variety_store|electronics|shoes|optician|jewelry|mall|gift|doityourself|greengrocer|books|bicycle|chemist|department_store|laundry|travel_agency|stationery|pet|sports|confectionery|tyres|cosmetics|computer|tailor|tobacco|storage_rental|dry_cleaning|trade|copyshop|motorcycle|funeral_directors|beverages|newsagent|garden_centre|massage|pastry|interior_decoration|general|deli|toys|houseware|wine|seafood|pawnbroker|tattoo|paint|wholesale|photo|second_hand|bed|kitchen|outdoor|fabric|antiques|coffee|gas|e-cigarette|perfumery|craft|hearing_aids|money_lender|appliance|electrical|tea|motorcycle_repair|boutique|baby_goods|bag|musical_instrument|dairy|pet_grooming|music|carpet|rental|fashion_accessories|cheese|chocolate|medical_supply|leather|sewing|cannabis|locksmith|games|video_games|hifi|window_blind|caravan|tool_hire|household_linen|bathroom_furnishing|shoe_repair|watches|nutrition_supplements|fishing|erotic|frame|grocery|boat|repair|weapons|gold_buyer|lighting|pottery|security|groundskeeping|herbalist|curtain|health_food|flooring|printer_ink|anime|camera|scuba_diving|candles|printing|garden_furniture|food|estate_agent|insurance|it|accountant|employment_agency|tax_advisor|financial|advertising_agency|logistics|newspaper|financial_advisor|consulting|travel_agent|coworking|moving_company|lawyer|architect|construction_company|credit_broker|graphic_design|property_management|cleaning)$"](area.searchArea);
|
||||
nw["shop"]["shop"!~"vacant"](area.searchArea);
|
||||
nw["tourism"~"^(hotel|hostel|motel|wilderness_hut|yes|chalet|gallery|guest_house|museum|zoo|theme_park|aquarium|alpine_hut|apartment)$"](area.searchArea);
|
||||
nw["healthcare"](area.searchArea);
|
||||
|
|
|
@ -101,28 +101,99 @@ if (canvas) {
|
|||
// Affichage de la progression sur une semaine
|
||||
function getDelta(data, days) {
|
||||
if (!data.length) return null;
|
||||
|
||||
const now = new Date(data[data.length - 1].x);
|
||||
const refDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
let ref = null;
|
||||
const last = data[data.length - 1].y;
|
||||
|
||||
// Chercher la mesure exacte à la date de référence
|
||||
let exactRef = null;
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const d = new Date(data[i].x);
|
||||
if (d <= refDate) {
|
||||
ref = data[i].y;
|
||||
exactRef = data[i].y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const last = data[data.length - 1].y;
|
||||
return ref !== null ? last - ref : null;
|
||||
|
||||
// Si on a trouvé une mesure exacte, l'utiliser
|
||||
if (exactRef !== null) {
|
||||
return last - exactRef;
|
||||
}
|
||||
|
||||
// Sinon, chercher les deux mesures les plus proches pour faire une interpolation
|
||||
let beforeRef = null;
|
||||
let afterRef = null;
|
||||
let beforeDate = null;
|
||||
let afterDate = null;
|
||||
|
||||
// Chercher la mesure juste avant la date de référence
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const d = new Date(data[i].x);
|
||||
if (d < refDate) {
|
||||
beforeRef = data[i].y;
|
||||
beforeDate = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Chercher la mesure juste après la date de référence
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const d = new Date(data[i].x);
|
||||
if (d > refDate) {
|
||||
afterRef = data[i].y;
|
||||
afterDate = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si on a les deux mesures, faire une interpolation linéaire
|
||||
if (beforeRef !== null && afterRef !== null && beforeDate !== null && afterDate !== null) {
|
||||
const timeDiff = afterDate.getTime() - beforeDate.getTime();
|
||||
const refTimeDiff = refDate.getTime() - beforeDate.getTime();
|
||||
const ratio = refTimeDiff / timeDiff;
|
||||
const interpolatedRef = beforeRef + (afterRef - beforeRef) * ratio;
|
||||
return last - interpolatedRef;
|
||||
}
|
||||
|
||||
// Si on n'a qu'une mesure avant, l'utiliser
|
||||
if (beforeRef !== null) {
|
||||
return last - beforeRef;
|
||||
}
|
||||
|
||||
// Si on n'a qu'une mesure après, l'utiliser
|
||||
if (afterRef !== null) {
|
||||
return last - afterRef;
|
||||
}
|
||||
|
||||
// Si aucune mesure n'est disponible, retourner null
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatDelta(val) {
|
||||
if (val === null) return 'Pas de données';
|
||||
if (val === 0) return '0';
|
||||
return (val > 0 ? '+' : '') + val;
|
||||
}
|
||||
|
||||
const delta7dCount = getDelta(countData, 7);
|
||||
const delta7dCompletion = getDelta(completionData, 7);
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'mt-3 alert ' + (delta7dCount === null ? 'alert-secondary' : 'alert-info');
|
||||
infoDiv.innerHTML = `<strong>Progression sur 7 jours :</strong> ${delta7dCount === null ? '<span title="Données insuffisantes pour calculer la progression">Aucune donnée</span>' : (delta7dCount > 0 ? '+' + delta7dCount : delta7dCount === 0 ? '0' : delta7dCount)}`;
|
||||
|
||||
let progressionText = '';
|
||||
if (delta7dCount === null) {
|
||||
progressionText = '<span title="Données insuffisantes pour calculer la progression">Aucune donnée</span>';
|
||||
} else {
|
||||
const countText = delta7dCount > 0 ? '+' + delta7dCount : delta7dCount === 0 ? '0' : delta7dCount;
|
||||
const completionText = delta7dCompletion !== null ?
|
||||
(delta7dCompletion > 0 ? '+' + delta7dCompletion.toFixed(1) : delta7dCompletion === 0 ? '0' : delta7dCompletion.toFixed(1)) + '%' :
|
||||
'N/A';
|
||||
progressionText = `${countText} objets, ${completionText} complétion`;
|
||||
}
|
||||
|
||||
infoDiv.innerHTML = `<strong>Progression sur 7 jours :</strong> ${progressionText}`;
|
||||
canvas.parentNode.appendChild(infoDiv);
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -12,6 +12,9 @@
|
|||
<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
|
||||
</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">
|
||||
<i class="bi bi-shield-check"></i> Labourer (sans nettoyage)
|
||||
</a>
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-info">
|
||||
<i class="bi bi-bar-chart"></i> Voir les stats
|
||||
</a>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<div class="example-wrapper">
|
||||
<h1>Labourage fait sur la zone "{{ stats.zone }} {{stats.name}}" ✅</h1>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1, 'disableFollowUpCleanup': 1}) }}" class="btn btn-warning" id="labourer-no-cleanup" title="Labourer sans nettoyer les suivis OSM">Labourer (sans nettoyage)</a>
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Voir les résultats</a>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -76,6 +76,9 @@
|
|||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1, 'disableFollowUpCleanup': 1}) }}" class="btn btn-warning ms-2" id="labourer-no-cleanup" title="Labourer sans nettoyer les suivis OSM">
|
||||
<i class="bi bi-shield-check"></i> Labourer (sans nettoyage)
|
||||
</a>
|
||||
<a href="{{ path('admin_followup_graph', {'insee_code': stats.zone}) }}" class="btn btn-info ms-2" id="followup-graph-link">
|
||||
<i class="bi bi-graph-up"></i> Suivi OSM (graphes)
|
||||
</a>
|
||||
|
@ -169,6 +172,24 @@
|
|||
<a href="http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode }}" target="_blank" class="fw-bold text-decoration-underline text-dark" title="Charger dans JOSM">{{ followup_labels[type]|default(type|capitalize) }}</a><br>
|
||||
<span title="Nombre"> {{ data.count is defined ? data.count.getMeasure() : '?' }}</span><br>
|
||||
<span title="Complétion"> {{ completion is not null ? completion : '?' }}%</span>
|
||||
{% if progression7Days[type] is defined %}
|
||||
{% set countDelta = progression7Days[type].count %}
|
||||
{% set completionDelta = progression7Days[type].completion %}
|
||||
{% if countDelta is not null or completionDelta is not null %}
|
||||
<small class="text-muted">
|
||||
{% if countDelta is not null %}
|
||||
<span title="Progression sur 7 jours - Nombre d'objets">
|
||||
{{ countDelta > 0 ? '+' ~ countDelta : countDelta == 0 ? '0' : countDelta }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if completionDelta is not null %}
|
||||
<span title="Progression sur 7 jours - Complétion">
|
||||
{{ completionDelta > 0 ? '+' ~ completionDelta|round(1) : completionDelta == 0 ? '0' : completionDelta|round(1) }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -210,6 +210,13 @@
|
|||
>
|
||||
<i class="bi bi-recycle"></i>
|
||||
</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stat.zone, 'deleteMissing': 1, 'disableFollowUpCleanup': 1}) }}"
|
||||
class="btn btn-sm btn-warning btn-labourer"
|
||||
data-zip-code="{{ stat.zone }}"
|
||||
title="Labourer cette ville sans nettoyer les suivis OSM"
|
||||
>
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</a>
|
||||
|
||||
<a href="{{ path('app_admin_delete_by_zone', {'insee_code': stat.zone}) }}"
|
||||
class="btn btn-sm btn-danger"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue