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' ]); } }