add measure on checking advanced graph

This commit is contained in:
Tykayn 2025-08-12 11:58:11 +02:00 committed by tykayn
parent 46f8b3f6ab
commit 1659864efb
3 changed files with 133 additions and 32 deletions

View file

@ -2,13 +2,12 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Stats;
use App\Entity\CityFollowUp; use App\Entity\CityFollowUp;
use App\Service\Motocultrice; use App\Entity\Stats;
use App\Service\FollowUpService; use App\Service\FollowUpService;
use App\Service\Motocultrice;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@ -23,12 +22,13 @@ class FollowUpController extends AbstractController
#[Route('/api/city-followup', name: 'api_city_followup', methods: ['POST'])] #[Route('/api/city-followup', name: 'api_city_followup', methods: ['POST'])]
public function recordCityFollowUp( public function recordCityFollowUp(
EntityManagerInterface $em, EntityManagerInterface $em,
\Symfony\Component\HttpFoundation\Request $request \Symfony\Component\HttpFoundation\Request $request
): Response { ): Response
{
$insee_code = $request->request->get('insee_code'); $insee_code = $request->request->get('insee_code');
$measure_label = $request->request->get('measure_label'); $measure_label = $request->request->get('measure_label');
$measure_value = $request->request->getFloat('measure_value'); $measure_value = (float)$request->request->get('measure_value');
if (!$insee_code || !$measure_label || $measure_value === null) { if (!$insee_code || !$measure_label || $measure_value === null) {
return $this->json([ return $this->json([
@ -48,9 +48,9 @@ class FollowUpController extends AbstractController
// Check if the same measure was recorded less than an hour ago // Check if the same measure was recorded less than an hour ago
$oneHourAgo = new \DateTime('-1 hour'); $oneHourAgo = new \DateTime('-1 hour');
$recentFollowUp = $em->getRepository(CityFollowUp::class) $recentFollowUp = $em->getRepository(CityFollowUp::class)
->findRecentByStatsAndName($stats, $measure_label, $oneHourAgo); ->findRecentByStatsAndName($stats->getId(), $measure_label, $oneHourAgo);
if ($recentFollowUp) { if ($recentFollowUp && $recentFollowUp->getMeasure() == $measure_value) {
return $this->json([ return $this->json([
'success' => false, 'success' => false,
'message' => 'A measure with the same label was recorded less than an hour ago', 'message' => 'A measure with the same label was recorded less than an hour ago',
@ -86,7 +86,8 @@ class FollowUpController extends AbstractController
#[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete', requirements: ['insee_code' => '\\d+'])] #[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete', requirements: ['insee_code' => '\\d+'])]
public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response { public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response
{
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) { if (!$stats) {
$this->addFlash('error', '9 Aucune stats trouvée pour ce code INSEE.'); $this->addFlash('error', '9 Aucune stats trouvée pour ce code INSEE.');
@ -103,10 +104,11 @@ class FollowUpController extends AbstractController
#[Route('/admin/followup/{insee_code}', name: 'admin_followup', requirements: ['insee_code' => '\\d+'])] #[Route('/admin/followup/{insee_code}', name: 'admin_followup', requirements: ['insee_code' => '\\d+'])]
public function followup( public function followup(
string $insee_code, string $insee_code,
Motocultrice $motocultrice, Motocultrice $motocultrice,
EntityManagerInterface $em EntityManagerInterface $em
): Response { ): Response
{
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) { if (!$stats) {
$this->addFlash('error', '10 Aucune stats trouvée pour ce code INSEE.'); $this->addFlash('error', '10 Aucune stats trouvée pour ce code INSEE.');
@ -119,10 +121,11 @@ class FollowUpController extends AbstractController
#[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph', requirements: ['insee_code' => '\\d+'])] #[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph', requirements: ['insee_code' => '\\d+'])]
public function followupGraph( public function followupGraph(
string $insee_code, string $insee_code,
EntityManagerInterface $em, EntityManagerInterface $em,
Motocultrice $motocultrice Motocultrice $motocultrice
) { )
{
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) { if (!$stats) {
$this->addFlash('error', '11 Aucune stats trouvée pour ce code INSEE.'); $this->addFlash('error', '11 Aucune stats trouvée pour ce code INSEE.');
@ -168,7 +171,7 @@ class FollowUpController extends AbstractController
$all_points = array_slice($all_points, 0, 20); $all_points = array_slice($all_points, 0, 20);
// Tri par date dans chaque série // Tri par date dans chaque série
foreach ($series as &$points) { foreach ($series as &$points) {
usort($points, function($a, $b) { usort($points, function ($a, $b) {
return strtotime($a['date']) <=> strtotime($b['date']); return strtotime($a['date']) <=> strtotime($b['date']);
}); });
} }
@ -189,7 +192,7 @@ class FollowUpController extends AbstractController
$count_series = $series[$type . '_count'] ?? []; $count_series = $series[$type . '_count'] ?? [];
$completion_series = $series[$type . '_completion'] ?? []; $completion_series = $series[$type . '_completion'] ?? [];
// Fonction utilitaire pour trouver la valeur la plus proche avant ou égale à une date // Fonction utilitaire pour trouver la valeur la plus proche avant ou égale à une date
$findValueAtOrBefore = function($series, \DateTime $date) { $findValueAtOrBefore = function ($series, \DateTime $date) {
$val = null; $val = null;
foreach (array_reverse($series) as $point) { foreach (array_reverse($series) as $point) {
$ptDate = \DateTime::createFromFormat('Y-m-d', $point['date']); $ptDate = \DateTime::createFromFormat('Y-m-d', $point['date']);
@ -201,7 +204,7 @@ class FollowUpController extends AbstractController
return $val; return $val;
}; };
// Valeurs aux bornes // Valeurs aux bornes
$val_now = count($count_series) ? $count_series[count($count_series)-1]['value'] : null; $val_now = count($count_series) ? $count_series[count($count_series) - 1]['value'] : null;
$val_7j = $findValueAtOrBefore($count_series, $periods['7j']); $val_7j = $findValueAtOrBefore($count_series, $periods['7j']);
$val_30j = $findValueAtOrBefore($count_series, $periods['30j']); $val_30j = $findValueAtOrBefore($count_series, $periods['30j']);
$val_6mois = $findValueAtOrBefore($count_series, $periods['6mois']); $val_6mois = $findValueAtOrBefore($count_series, $periods['6mois']);
@ -210,7 +213,7 @@ class FollowUpController extends AbstractController
$diff_30j = ($val_7j !== null && $val_30j !== null) ? $val_7j - $val_30j : 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; $diff_6mois = ($val_30j !== null && $val_6mois !== null) ? $val_30j - $val_6mois : null;
// Idem pour la complétion // Idem pour la complétion
$comp_now = count($completion_series) ? $completion_series[count($completion_series)-1]['value'] : null; $comp_now = count($completion_series) ? $completion_series[count($completion_series) - 1]['value'] : null;
$comp_7j = $findValueAtOrBefore($completion_series, $periods['7j']); $comp_7j = $findValueAtOrBefore($completion_series, $periods['7j']);
$comp_30j = $findValueAtOrBefore($completion_series, $periods['30j']); $comp_30j = $findValueAtOrBefore($completion_series, $periods['30j']);
$comp_6mois = $findValueAtOrBefore($completion_series, $periods['6mois']); $comp_6mois = $findValueAtOrBefore($completion_series, $periods['6mois']);
@ -251,8 +254,9 @@ class FollowUpController extends AbstractController
#[Route('/admin/followup/all', name: 'admin_followup_all')] #[Route('/admin/followup/all', name: 'admin_followup_all')]
public function followupAll( public function followupAll(
EntityManagerInterface $em, EntityManagerInterface $em,
Motocultrice $motocultrice Motocultrice $motocultrice
) { )
{
$statsList = $em->getRepository(Stats::class)->findAll(); $statsList = $em->getRepository(Stats::class)->findAll();
foreach ($statsList as $stats) { foreach ($statsList as $stats) {
$this->followUpService->generateCityFollowUps($stats, $motocultrice, $em); $this->followUpService->generateCityFollowUps($stats, $motocultrice, $em);
@ -262,14 +266,16 @@ class FollowUpController extends AbstractController
} }
#[Route('/admin/followup/global', name: 'admin_followup_global')] #[Route('/admin/followup/global', name: 'admin_followup_global')]
public function followupGlobal(EntityManagerInterface $em) { public function followupGlobal(EntityManagerInterface $em)
{
$this->followUpService->generateGlobalFollowUps($em); $this->followUpService->generateGlobalFollowUps($em);
$this->addFlash('success', 'Suivi global généré pour toutes les villes.'); $this->addFlash('success', 'Suivi global généré pour toutes les villes.');
return $this->redirectToRoute('admin_followup_global_graph'); return $this->redirectToRoute('admin_followup_global_graph');
} }
#[Route('/admin/followup/global/graph', name: 'admin_followup_global_graph')] #[Route('/admin/followup/global/graph', name: 'admin_followup_global_graph')]
public function followupGlobalGraph(EntityManagerInterface $em) { public function followupGlobalGraph(EntityManagerInterface $em)
{
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']); $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']);
if (!$stats) { if (!$stats) {
$this->addFlash('error', 'Aucun suivi global trouvé.'); $this->addFlash('error', 'Aucun suivi global trouvé.');
@ -288,7 +294,7 @@ class FollowUpController extends AbstractController
} }
// Tri par date dans chaque série // Tri par date dans chaque série
foreach ($series as &$points) { foreach ($series as &$points) {
usort($points, function($a, $b) { usort($points, function ($a, $b) {
return strtotime($a['date']) <=> strtotime($b['date']); return strtotime($a['date']) <=> strtotime($b['date']);
}); });
} }
@ -304,11 +310,12 @@ class FollowUpController extends AbstractController
#[Route('/admin/followup/{insee_code}/embed/{theme}', name: 'admin_followup_embed_graph', requirements: ['insee_code' => '\\d+', 'theme' => '[a-zA-Z0-9_]+'])] #[Route('/admin/followup/{insee_code}/embed/{theme}', name: 'admin_followup_embed_graph', requirements: ['insee_code' => '\\d+', 'theme' => '[a-zA-Z0-9_]+'])]
public function followupEmbedGraph( public function followupEmbedGraph(
string $insee_code, string $insee_code,
string $theme, string $theme,
EntityManagerInterface $em, EntityManagerInterface $em,
Motocultrice $motocultrice Motocultrice $motocultrice
) { )
{
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) { if (!$stats) {
$this->addFlash('error', '12 Aucune stats trouvée pour ce code INSEE.'); $this->addFlash('error', '12 Aucune stats trouvée pour ce code INSEE.');
@ -353,6 +360,7 @@ class FollowUpController extends AbstractController
'icon' => FollowUpService::getFollowUpIcons()[$theme] ?? 'bi-question-circle', 'icon' => FollowUpService::getFollowUpIcons()[$theme] ?? 'bi-question-circle',
]); ]);
} }
#[Route('/admin/followup/unused-stores', name: 'admin_followup_unused_stores')] #[Route('/admin/followup/unused-stores', name: 'admin_followup_unused_stores')]
public function unusedStores(): Response public function unusedStores(): Response
{ {

View file

@ -17,7 +17,7 @@ class CityFollowUpRepository extends ServiceEntityRepository
} }
public function findRecentByStatsAndName(Stats $stats, string $name, \DateTime $since): ?CityFollowUp public function findRecentByStatsAndName(string $stats, string $name, \DateTime $since): ?CityFollowUp
{ {
return $this->createQueryBuilder('c') return $this->createQueryBuilder('c')
->andWhere('c.stats = :stats') ->andWhere('c.stats = :stats')
@ -29,7 +29,6 @@ class CityFollowUpRepository extends ServiceEntityRepository
->orderBy('c.date', 'DESC') ->orderBy('c.date', 'DESC')
->setMaxResults(1) ->setMaxResults(1)
->getQuery() ->getQuery()
->getOneOrNullResult() ->getOneOrNullResult();
;
} }
} }

View file

@ -527,6 +527,77 @@
if (average_completion && current_completion) { if (average_completion && current_completion) {
current_completion.textContent = average_completion.toFixed(2) + ' %'; current_completion.textContent = average_completion.toFixed(2) + ' %';
} }
// Send measurement to /api/city-followup
const insee_code = '{{ stats.zone }}';
const theme = '{{ theme }}';
// Prepare data for the API request
const measureData = new FormData();
measureData.append('insee_code', insee_code);
measureData.append('measure_label', theme + '_count');
measureData.append('measure_value', count_objects);
// Send count measurement
fetch('/api/city-followup', {
method: 'POST',
body: measureData
})
.then(response => response.json())
.then(result => {
console.log('Count measurement saved:', result);
if (result.success) {
// Add the new measurement to the chart data
const newMeasurement = {
date: result.follow_up.date,
value: result.follow_up.measure
};
// Add to the global countData array
if (Array.isArray(window.countData)) {
window.countData.push(newMeasurement);
// Update the chart
updateChart();
}
}
})
.catch(error => {
console.error('Error saving count measurement:', error);
});
// Send completion measurement
if (average_completion) {
const completionData = new FormData();
completionData.append('insee_code', insee_code);
completionData.append('measure_label', theme + '_completion');
completionData.append('measure_value', average_completion);
fetch('/api/city-followup', {
method: 'POST',
body: completionData
})
.then(response => response.json())
.then(result => {
console.log('Completion measurement saved:', result);
if (result.success) {
// Add the new measurement to the chart data
const newMeasurement = {
date: result.follow_up.date,
value: result.follow_up.measure
};
// Add to the global completionData array
if (Array.isArray(window.completionData)) {
window.completionData.push(newMeasurement);
// Update the chart
updateChart();
}
}
})
.catch(error => {
console.error('Error saving completion measurement:', error);
});
}
const tbody = document.querySelector('#tags-stats-table tbody'); const tbody = document.querySelector('#tags-stats-table tbody');
if (Object.keys(tagCounts).length === 0) { if (Object.keys(tagCounts).length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="text-muted">Aucun tag trouvé</td></tr>'; tbody.innerHTML = '<tr><td colspan="2" class="text-muted">Aucun tag trouvé</td></tr>';
@ -551,8 +622,9 @@
const countData = {{ count_data|json_encode|raw }}; const countData = {{ count_data|json_encode|raw }};
console.log('Count data:', countData); console.log('Count data:', countData);
window.countData = countData window.countData = countData;
const completionData = {{ completion_data|json_encode|raw }}; const completionData = {{ completion_data|json_encode|raw }};
window.completionData = completionData;
console.log('Completion data:', completionData); console.log('Completion data:', completionData);
// Current metrics from server // Current metrics from server
@ -694,6 +766,28 @@
} }
}); });
// Function to update the chart with new data
function updateChart() {
// Get the chart instance
const chartInstance = Chart.getChart('themeChart');
if (!chartInstance) return;
// Update the datasets
chartInstance.data.datasets[0].data = Array.isArray(window.countData)
? window.countData.map(d => ({x: new Date(d.date), y: d.value}))
: [];
if (Array.isArray(window.completionData)) {
chartInstance.data.datasets[1].data = window.completionData.map(d => ({
x: new Date(d.date),
y: d.value
}));
}
// Update the chart
chartInstance.update();
}
// Initialiser les statistiques // Initialiser les statistiques
updateStats(); updateStats();