mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
371 lines
16 KiB
PHP
371 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Entity\CityFollowUp;
|
|
use App\Entity\Stats;
|
|
use App\Service\FollowUpService;
|
|
use App\Service\Motocultrice;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Annotation\Route;
|
|
|
|
class FollowUpController extends AbstractController
|
|
{
|
|
private FollowUpService $followUpService;
|
|
|
|
public function __construct(FollowUpService $followUpService)
|
|
{
|
|
$this->followUpService = $followUpService;
|
|
}
|
|
|
|
#[Route('/api/city-followup', name: 'api_city_followup', methods: ['POST'])]
|
|
public function recordCityFollowUp(
|
|
EntityManagerInterface $em,
|
|
\Symfony\Component\HttpFoundation\Request $request
|
|
): Response
|
|
{
|
|
$insee_code = $request->request->get('insee_code');
|
|
$measure_label = $request->request->get('measure_label');
|
|
$measure_value = (float)$request->request->get('measure_value');
|
|
|
|
if (!$insee_code || !$measure_label || $measure_value === null) {
|
|
return $this->json([
|
|
'success' => false,
|
|
'message' => 'Missing required parameters: insee_code, measure_label, measure_value'
|
|
], Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
|
if (!$stats) {
|
|
return $this->json([
|
|
'success' => false,
|
|
'message' => 'No stats found for this INSEE code'
|
|
], Response::HTTP_NOT_FOUND);
|
|
}
|
|
|
|
// Check if the same measure was recorded less than an hour ago
|
|
$oneHourAgo = new \DateTime('-1 hour');
|
|
$recentFollowUp = $em->getRepository(CityFollowUp::class)
|
|
->findRecentByStatsAndName($stats->getId(), $measure_label, $oneHourAgo);
|
|
|
|
if ($recentFollowUp && $recentFollowUp->getMeasure() == $measure_value) {
|
|
return $this->json([
|
|
'success' => false,
|
|
'message' => 'A measure with the same label was recorded less than an hour ago',
|
|
'existing_measure' => [
|
|
'id' => $recentFollowUp->getId(),
|
|
'date' => $recentFollowUp->getDate()->format('Y-m-d H:i:s'),
|
|
'value' => $recentFollowUp->getMeasure()
|
|
]
|
|
], Response::HTTP_TOO_MANY_REQUESTS);
|
|
}
|
|
|
|
// Create and save the new follow-up
|
|
$followUp = new CityFollowUp();
|
|
$followUp->setName($measure_label);
|
|
$followUp->setMeasure($measure_value);
|
|
$followUp->setDate(new \DateTime());
|
|
$followUp->setStats($stats);
|
|
|
|
$em->persist($followUp);
|
|
$em->flush();
|
|
|
|
return $this->json([
|
|
'success' => true,
|
|
'message' => 'City follow-up recorded successfully',
|
|
'follow_up' => [
|
|
'id' => $followUp->getId(),
|
|
'name' => $followUp->getName(),
|
|
'measure' => $followUp->getMeasure(),
|
|
'date' => $followUp->getDate()->format('Y-m-d H:i:s')
|
|
]
|
|
]);
|
|
}
|
|
|
|
|
|
#[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete', requirements: ['insee_code' => '\\d+'])]
|
|
public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response
|
|
{
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
|
if (!$stats) {
|
|
$this->addFlash('error', '9 Aucune stats trouvée pour ce code INSEE.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
$followups = $stats->getCityFollowUps();
|
|
foreach ($followups as $fu) {
|
|
$em->remove($fu);
|
|
}
|
|
$em->flush();
|
|
$this->addFlash('success', 'Tous les suivis ont été supprimés pour cette ville.');
|
|
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
|
}
|
|
|
|
#[Route('/admin/followup/{insee_code}', name: 'admin_followup', requirements: ['insee_code' => '\\d+'])]
|
|
public function followup(
|
|
string $insee_code,
|
|
Motocultrice $motocultrice,
|
|
EntityManagerInterface $em
|
|
): Response
|
|
{
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
|
if (!$stats) {
|
|
$this->addFlash('error', '10 Aucune stats trouvée pour ce code INSEE.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
$this->followUpService->generateCityFollowUps($stats, $motocultrice, $em);
|
|
$this->addFlash('success', 'Suivi enregistré pour la ville.');
|
|
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
|
}
|
|
|
|
#[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph', requirements: ['insee_code' => '\\d+'])]
|
|
public function followupGraph(
|
|
string $insee_code,
|
|
EntityManagerInterface $em,
|
|
Motocultrice $motocultrice
|
|
)
|
|
{
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
|
if (!$stats) {
|
|
$this->addFlash('error', '11 Aucune stats trouvée pour ce code INSEE.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
$followups = $stats->getCityFollowUps();
|
|
$refresh = false;
|
|
if (!$followups->isEmpty()) {
|
|
$latest = null;
|
|
foreach ($followups as $fu) {
|
|
if ($latest === null || $fu->getDate() > $latest->getDate()) {
|
|
$latest = $fu;
|
|
}
|
|
}
|
|
if ($latest && $latest->getDate() < (new \DateTime('-1 day'))) {
|
|
$refresh = true;
|
|
}
|
|
} else {
|
|
$refresh = true;
|
|
}
|
|
if ($refresh) {
|
|
$this->followUpService->generateCityFollowUps($stats, $motocultrice, $em);
|
|
$followups = $stats->getCityFollowUps();
|
|
}
|
|
$followups = $followups->toArray();
|
|
usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate());
|
|
$series = [];
|
|
$all_points = [];
|
|
foreach ($followups as $fu) {
|
|
$series[$fu->getName()][] = [
|
|
'date' => $fu->getDate()->format('Y-m-d'),
|
|
'value' => $fu->getMeasure(),
|
|
'name' => $fu->getName(),
|
|
];
|
|
$all_points[] = [
|
|
'date' => $fu->getDate()->format('Y-m-d'),
|
|
'type' => $fu->getName(),
|
|
'name' => $fu->getName(),
|
|
'value' => $fu->getMeasure(),
|
|
];
|
|
}
|
|
usort($all_points, fn($a, $b) => strcmp($b['date'], $a['date']));
|
|
$all_points = array_slice($all_points, 0, 20);
|
|
// Tri par date dans chaque série
|
|
foreach ($series as &$points) {
|
|
usort($points, function ($a, $b) {
|
|
return strtotime($a['date']) <=> strtotime($b['date']);
|
|
});
|
|
}
|
|
unset($points);
|
|
// Ajout du calcul all_completion_data
|
|
$themes = \App\Service\FollowUpService::getFollowUpThemes();
|
|
$all_completion_data = [];
|
|
$latest_diffs = [];
|
|
// Définir les bornes de période
|
|
$now = new \DateTime();
|
|
$periods = [
|
|
'7j' => (clone $now)->modify('-7 days'),
|
|
'30j' => (clone $now)->modify('-30 days'),
|
|
'6mois' => (clone $now)->modify('-6 months'),
|
|
];
|
|
foreach ($themes as $type => $label) {
|
|
$all_completion_data[$type] = $series[$type . '_completion'] ?? [];
|
|
$count_series = $series[$type . '_count'] ?? [];
|
|
$completion_series = $series[$type . '_completion'] ?? [];
|
|
// Fonction utilitaire pour trouver la valeur la plus proche avant ou égale à une date
|
|
$findValueAtOrBefore = function ($series, \DateTime $date) {
|
|
$val = null;
|
|
foreach (array_reverse($series) as $point) {
|
|
$ptDate = \DateTime::createFromFormat('Y-m-d', $point['date']);
|
|
if ($ptDate && $ptDate <= $date) {
|
|
$val = $point['value'];
|
|
break;
|
|
}
|
|
}
|
|
return $val;
|
|
};
|
|
// Valeurs aux bornes
|
|
$val_now = count($count_series) ? $count_series[count($count_series) - 1]['value'] : null;
|
|
$val_7j = $findValueAtOrBefore($count_series, $periods['7j']);
|
|
$val_30j = $findValueAtOrBefore($count_series, $periods['30j']);
|
|
$val_6mois = $findValueAtOrBefore($count_series, $periods['6mois']);
|
|
// Différences exclusives
|
|
$diff_7j = ($val_now !== null && $val_7j !== null) ? $val_now - $val_7j : null;
|
|
$diff_30j = ($val_7j !== null && $val_30j !== null) ? $val_7j - $val_30j : null;
|
|
$diff_6mois = ($val_30j !== null && $val_6mois !== null) ? $val_30j - $val_6mois : null;
|
|
// Idem pour la complétion
|
|
$comp_now = count($completion_series) ? $completion_series[count($completion_series) - 1]['value'] : null;
|
|
$comp_7j = $findValueAtOrBefore($completion_series, $periods['7j']);
|
|
$comp_30j = $findValueAtOrBefore($completion_series, $periods['30j']);
|
|
$comp_6mois = $findValueAtOrBefore($completion_series, $periods['6mois']);
|
|
$comp_diff_7j = ($comp_now !== null && $comp_7j !== null) ? $comp_now - $comp_7j : null;
|
|
$comp_diff_30j = ($comp_7j !== null && $comp_30j !== null) ? $comp_7j - $comp_30j : null;
|
|
$comp_diff_6mois = ($comp_30j !== null && $comp_6mois !== null) ? $comp_30j - $comp_6mois : null;
|
|
$latest_diffs[$type] = [
|
|
'count_now' => $val_now,
|
|
'count_7j' => $val_7j,
|
|
'count_30j' => $val_30j,
|
|
'count_6mois' => $val_6mois,
|
|
'count_diff_7j' => $diff_7j,
|
|
'count_diff_30j' => $diff_30j,
|
|
'count_diff_6mois' => $diff_6mois,
|
|
'completion_now' => $comp_now,
|
|
'completion_7j' => $comp_7j,
|
|
'completion_30j' => $comp_30j,
|
|
'completion_6mois' => $comp_6mois,
|
|
'completion_diff_7j' => $comp_diff_7j,
|
|
'completion_diff_30j' => $comp_diff_30j,
|
|
'completion_diff_6mois' => $comp_diff_6mois,
|
|
'label' => $label,
|
|
];
|
|
}
|
|
return $this->render('admin/followup_graph.html.twig', [
|
|
'stats' => $stats,
|
|
'series' => $series,
|
|
'all_points' => $all_points,
|
|
'followup_labels' => FollowUpService::getFollowUpThemes(),
|
|
'followup_icons' => FollowUpService::getFollowUpIcons(),
|
|
'followup_overpass' => FollowUpService::getFollowUpOverpassQueries(),
|
|
'completion_tags' => FollowUpService::getFollowUpCompletionTags(),
|
|
'all_completion_data' => $all_completion_data,
|
|
'latest_diffs' => $latest_diffs,
|
|
]);
|
|
}
|
|
|
|
#[Route('/admin/followup/all', name: 'admin_followup_all')]
|
|
public function followupAll(
|
|
EntityManagerInterface $em,
|
|
Motocultrice $motocultrice
|
|
)
|
|
{
|
|
$statsList = $em->getRepository(Stats::class)->findAll();
|
|
foreach ($statsList as $stats) {
|
|
$this->followUpService->generateCityFollowUps($stats, $motocultrice, $em);
|
|
}
|
|
$this->addFlash('success', 'Suivi généré pour toutes les villes.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
|
|
#[Route('/admin/followup/global', name: 'admin_followup_global')]
|
|
public function followupGlobal(EntityManagerInterface $em)
|
|
{
|
|
$this->followUpService->generateGlobalFollowUps($em);
|
|
$this->addFlash('success', 'Suivi global généré pour toutes les villes.');
|
|
return $this->redirectToRoute('admin_followup_global_graph');
|
|
}
|
|
|
|
#[Route('/admin/followup/global/graph', name: 'admin_followup_global_graph')]
|
|
public function followupGlobalGraph(EntityManagerInterface $em)
|
|
{
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']);
|
|
if (!$stats) {
|
|
$this->addFlash('error', 'Aucun suivi global trouvé.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
$followups = $stats->getCityFollowUps();
|
|
$followups = $followups->toArray();
|
|
usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate());
|
|
$series = [];
|
|
foreach ($followups as $fu) {
|
|
$series[$fu->getName()][] = [
|
|
'date' => $fu->getDate()->format('c'),
|
|
'value' => $fu->getMeasure(),
|
|
'name' => $fu->getName(),
|
|
];
|
|
}
|
|
// Tri par date dans chaque série
|
|
foreach ($series as &$points) {
|
|
usort($points, function ($a, $b) {
|
|
return strtotime($a['date']) <=> strtotime($b['date']);
|
|
});
|
|
}
|
|
unset($points);
|
|
return $this->render('admin/followup_global_graph.html.twig', [
|
|
'stats' => $stats,
|
|
'series' => $series,
|
|
'followup_labels' => FollowUpService::getFollowUpThemes(),
|
|
'followup_icons' => FollowUpService::getFollowUpIcons(),
|
|
'followup_overpass' => FollowUpService::getFollowUpOverpassQueries(),
|
|
]);
|
|
}
|
|
|
|
#[Route('/admin/followup/{insee_code}/embed/{theme}', name: 'admin_followup_embed_graph', requirements: ['insee_code' => '\\d+', 'theme' => '[a-zA-Z0-9_]+'])]
|
|
public function followupEmbedGraph(
|
|
string $insee_code,
|
|
string $theme,
|
|
EntityManagerInterface $em,
|
|
Motocultrice $motocultrice
|
|
)
|
|
{
|
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
|
if (!$stats) {
|
|
$this->addFlash('error', '12 Aucune stats trouvée pour ce code INSEE.');
|
|
return $this->redirectToRoute('app_admin');
|
|
}
|
|
$followups = $stats->getCityFollowUps();
|
|
$refresh = false;
|
|
if (!$followups->isEmpty()) {
|
|
$latest = null;
|
|
foreach ($followups as $fu) {
|
|
if ($latest === null || $fu->getDate() > $latest->getDate()) {
|
|
$latest = $fu;
|
|
}
|
|
}
|
|
if ($latest && $latest->getDate() < (new \DateTime('-1 day'))) {
|
|
$refresh = true;
|
|
}
|
|
} else {
|
|
$refresh = true;
|
|
}
|
|
if ($refresh) {
|
|
$this->followUpService->generateCityFollowUps($stats, $motocultrice, $em);
|
|
$followups = $stats->getCityFollowUps();
|
|
}
|
|
$followups = $followups->toArray();
|
|
usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate());
|
|
$series = [];
|
|
foreach ($followups as $fu) {
|
|
if (str_starts_with($fu->getName(), $theme)) {
|
|
$series[$fu->getName()][] = [
|
|
'date' => $fu->getDate()->format('c'),
|
|
'value' => $fu->getMeasure(),
|
|
'name' => $fu->getName(),
|
|
];
|
|
}
|
|
}
|
|
return $this->render('admin/followup_embed_graph.html.twig', [
|
|
'stats' => $stats,
|
|
'series' => $series,
|
|
'theme' => $theme,
|
|
'label' => FollowUpService::getFollowUpThemes()[$theme] ?? $theme,
|
|
'icon' => FollowUpService::getFollowUpIcons()[$theme] ?? 'bi-question-circle',
|
|
]);
|
|
}
|
|
|
|
#[Route('/admin/followup/unused-stores', name: 'admin_followup_unused_stores')]
|
|
public function unusedStores(): Response
|
|
{
|
|
return $this->render('admin/followup_unused_stores.html.twig', [
|
|
'json_url' => 'https://complete-tes-commerces.fr/13/13001-aix-en-provence/json/aix-en-provence_last_stats.json'
|
|
]);
|
|
}
|
|
}
|