retapage accueil, gestion de Demandes

This commit is contained in:
Tykayn 2025-07-16 17:00:09 +02:00 committed by tykayn
parent d777221d0d
commit f4c5e048ff
26 changed files with 2498 additions and 292 deletions

View file

@ -489,7 +489,7 @@ final class AdminController extends AbstractController
$progression7Days[$type] = \App\Service\FollowUpService::calculate7DayProgression($stats, $type);
}
$progression7Days['places'] = \App\Service\FollowUpService::calculate7DayProgression($stats, 'places');
// --- Ajout : mesures CTC CityFollowUp pour le graphique d'évolution ---
$ctc_completion_series = [];
foreach ($stats->getCityFollowUps() as $fu) {
@ -1908,4 +1908,164 @@ final class AdminController extends AbstractController
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
]);
}
#[Route('/admin/demandes', name: 'app_admin_demandes')]
public function listDemandes(Request $request): Response
{
$status = $request->query->get('status');
$repository = $this->entityManager->getRepository(\App\Entity\Demande::class);
if ($status) {
$demandes = $repository->findByStatus($status);
} else {
$demandes = $repository->findAllOrderedByCreatedAt();
}
// Get all possible statuses for the filter
$allStatuses = ['new', 'email_provided', 'ready', 'email_sent', 'email_failed', 'email_opened', 'edit_form_opened', 'place_modified', 'linked_to_place'];
// Count demandes for each status
$statusCounts = [];
foreach ($allStatuses as $statusValue) {
$statusCounts[$statusValue] = $repository->findByStatus($statusValue);
}
// Get total count
$totalCount = $repository->findAllOrderedByCreatedAt();
return $this->render('admin/demandes/list.html.twig', [
'demandes' => $demandes,
'current_status' => $status,
'all_statuses' => $allStatuses,
'status_counts' => $statusCounts,
'total_count' => count($totalCount)
]);
}
#[Route('/admin/demandes/{id}/edit', name: 'app_admin_demande_edit')]
public function editDemande(int $id, Request $request): Response
{
$demande = $this->entityManager->getRepository(\App\Entity\Demande::class)->find($id);
if (!$demande) {
$this->addFlash('error', 'Demande non trouvée');
return $this->redirectToRoute('app_admin_demandes');
}
if ($request->isMethod('POST')) {
$placeUuid = $request->request->get('placeUuid');
if ($placeUuid) {
// Check if the Place exists
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $placeUuid]);
if ($place) {
$demande->setPlaceUuid($placeUuid);
$demande->setPlace($place);
$demande->setStatus('linked_to_place');
// Set OSM object type and OSM ID from the Place
$demande->setOsmObjectType($place->getOsmKind());
$demande->setOsmId((int)$place->getOsmId());
$this->entityManager->persist($demande);
$this->entityManager->flush();
$this->addFlash('success', 'Demande mise à jour avec succès');
} else {
$this->addFlash('error', 'Place non trouvée avec cet UUID');
}
}
}
return $this->render('admin/demandes/edit.html.twig', [
'demande' => $demande
]);
}
#[Route('/admin/contacted-places', name: 'app_admin_contacted_places')]
public function listContactedPlaces(): Response
{
$demandes = $this->entityManager->getRepository(\App\Entity\Demande::class)->findPlacesWithContactAttempt();
return $this->render('admin/demandes/contacted_places.html.twig', [
'demandes' => $demandes
]);
}
#[Route('/admin/demandes/{id}/send-email', name: 'app_admin_demande_send_email')]
public function sendEmailToDemande(int $id, \Symfony\Component\Mailer\MailerInterface $mailer): Response
{
$demande = $this->entityManager->getRepository(\App\Entity\Demande::class)->find($id);
if (!$demande) {
$this->addFlash('error', 'Demande non trouvée');
return $this->redirectToRoute('app_admin_demandes');
}
$place = $demande->getPlace();
if (!$place) {
$this->addFlash('error', 'Aucune place associée à cette demande');
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
// Check if the place has an email
if (!$place->getEmail() && !$demande->getEmail()) {
$this->addFlash('error', 'Aucun email associé à cette place ou à cette demande');
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
// Use the email from the place if available, otherwise use the email from the demande
$email = $place->getEmail() ?: $demande->getEmail();
// Generate the email content
$emailContent = $this->renderView('admin/email_content.html.twig', [
'place' => $place
]);
// Only send the email in production environment
if ($this->getParameter('kernel.environment') === 'prod') {
$message = (new \Symfony\Component\Mime\Email())
->from('contact@osm-commerce.fr')
->to($email)
->subject('Votre lien de modification OpenStreetMap')
->html($emailContent);
try {
$mailer->send($message);
} catch (\Throwable $e) {
$this->actionLogger->log('ERROR_envoi_email', [
'demande_id' => $demande->getId(),
'place_id' => $place->getId(),
'message' => $e->getMessage(),
]);
$this->addFlash('error', 'Erreur lors de l\'envoi de l\'email : ' . $e->getMessage());
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
} else {
// In non-production environments, just log the attempt
$this->actionLogger->log('email_would_be_sent', [
'demande_id' => $demande->getId(),
'place_id' => $place->getId(),
'email' => $email,
'content' => $emailContent
]);
$this->addFlash('info', 'En environnement de production, un email serait envoyé à ' . $email);
}
// Update the last contact attempt date and set status to email_sent
$now = new \DateTime();
$demande->setLastContactAttempt($now);
$demande->setStatus('email_sent');
$place->setLastContactAttemptDate($now);
$this->entityManager->persist($demande);
$this->entityManager->persist($place);
$this->entityManager->flush();
$this->addFlash('success', 'Email envoyé avec succès');
return $this->redirectToRoute('app_admin_contacted_places');
}
}

View file

@ -21,6 +21,69 @@ class FollowUpController extends AbstractController
$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 = $request->request->getFloat('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, $measure_label, $oneHourAgo);
if ($recentFollowUp) {
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 {

View file

@ -4,16 +4,21 @@ namespace App\Controller;
use App\Entity\Stats;
use App\Entity\Place;
use App\Entity\CityFollowUp;
use App\Entity\Demande;
use App\Service\Motocultrice;
use App\Service\FollowUpService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\MailerInterface;
use App\Service\ActionLogger;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class PublicController extends AbstractController
{
@ -24,7 +29,8 @@ class PublicController extends AbstractController
private EntityManagerInterface $entityManager,
private Motocultrice $motocultrice,
private MailerInterface $mailer,
private ActionLogger $actionLogger
private ActionLogger $actionLogger,
private FollowUpService $followUpService
) {}
#[Route('/propose-email/{email}/{type}/{id}', name: 'app_public_propose_email')]
@ -115,6 +121,73 @@ class PublicController extends AbstractController
return $this->redirectToRoute('app_public_index');
}
#[Route('/api/demande/create', name: 'app_public_create_demande', methods: ['POST'])]
public function createDemande(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (!isset($data['businessName']) || empty($data['businessName'])) {
return new JsonResponse(['success' => false, 'message' => 'Le nom du commerce est requis'], 400);
}
// Create a new Demande
$demande = new Demande();
$demande->setQuery($data['businessName']);
$demande->setStatus('new');
$demande->setCreatedAt(new \DateTime());
// Save the INSEE code if provided
if (isset($data['insee']) && !empty($data['insee'])) {
$demande->setInsee((int)$data['insee']);
}
// Save the OSM object type if provided
if (isset($data['osmObjectType']) && !empty($data['osmObjectType'])) {
$demande->setOsmObjectType($data['osmObjectType']);
}
// Save the OSM ID if provided
if (isset($data['osmId']) && !empty($data['osmId'])) {
$demande->setOsmId((int)$data['osmId']);
}
$this->entityManager->persist($demande);
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'message' => 'Demande créée avec succès',
'id' => $demande->getId()
]);
}
#[Route('/api/demande/{id}/email', name: 'app_public_update_demande_email', methods: ['POST'])]
public function updateDemandeEmail(int $id, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (!isset($data['email']) || empty($data['email'])) {
return new JsonResponse(['success' => false, 'message' => 'L\'email est requis'], 400);
}
$demande = $this->entityManager->getRepository(Demande::class)->find($id);
if (!$demande) {
return new JsonResponse(['success' => false, 'message' => 'Demande non trouvée'], 404);
}
$demande->setEmail($data['email']);
$demande->setStatus('email_provided');
$this->entityManager->persist($demande);
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'message' => 'Email ajouté avec succès'
]);
}
#[Route('/', name: 'app_public_index')]
public function index(): Response
{
@ -122,11 +195,11 @@ class PublicController extends AbstractController
// Préparer les données pour la carte
$citiesForMap = [];
foreach ($stats as $stat) {
if ($stat->getZone() && $stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone()) && $stat->getZone() !== '00000') {
$cityName = $stat->getName() ?: $stat->getZone();
// Utiliser les coordonnées stockées si disponibles
if ($stat->getLat() && $stat->getLon()) {
$citiesForMap[] = [
@ -160,33 +233,33 @@ class PublicController extends AbstractController
{
// Cache simple pour éviter trop d'appels API
$cacheKey = 'city_coords_' . $inseeCode;
// Vérifier le cache (ici on utilise une approche simple)
// En production, vous pourriez utiliser le cache Symfony
$query = urlencode($cityName . ', France');
$url = "https://nominatim.openstreetmap.org/search?q={$query}&format=json&limit=1&countrycodes=fr";
try {
// Ajouter un délai pour respecter les limites de l'API Nominatim
usleep(100000); // 0.1 seconde entre les appels
$context = stream_context_create([
'http' => [
'timeout' => 5, // Timeout de 5 secondes
'user_agent' => 'OSM-Commerces/1.0'
]
]);
$response = file_get_contents($url, false, $context);
if ($response === false) {
error_log("DEBUG: Échec de récupération des coordonnées pour $cityName ($inseeCode)");
return null;
}
$data = json_decode($response, true);
if (!empty($data) && isset($data[0]['lat']) && isset($data[0]['lon'])) {
error_log("DEBUG: Coordonnées trouvées pour $cityName ($inseeCode): " . $data[0]['lat'] . ", " . $data[0]['lon']);
return [
@ -199,7 +272,7 @@ class PublicController extends AbstractController
} catch (\Exception $e) {
error_log("DEBUG: Exception lors de la récupération des coordonnées pour $cityName ($inseeCode): " . $e->getMessage());
}
return null;
}
@ -262,7 +335,7 @@ class PublicController extends AbstractController
{
$this->actionLogger->log('dashboard', []);
$stats_repo = $this->entityManager->getRepository(Stats::class)->findAll();
$stats_for_chart = [];
@ -291,6 +364,74 @@ class PublicController extends AbstractController
]);
}
#[Route('/api/dashboard/regression', name: 'api_dashboard_regression', methods: ['POST'])]
public function saveRegressionData(Request $request): JsonResponse
{
$this->actionLogger->log('save_regression_data', []);
// Récupérer les données de la requête
$data = json_decode($request->getContent(), true);
if (!isset($data['angle']) || !isset($data['slope']) || !isset($data['intercept'])) {
return new JsonResponse([
'success' => false,
'message' => 'Données de régression incomplètes'
], Response::HTTP_BAD_REQUEST);
}
// Récupérer les stats globales (zone 00000)
$statsGlobal = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => '00000']);
if (!$statsGlobal) {
// Créer les stats globales si elles n'existent pas
$statsGlobal = new Stats();
$statsGlobal->setZone('00000');
$statsGlobal->setName('toutes les villes');
$this->entityManager->persist($statsGlobal);
$this->entityManager->flush();
}
// Créer un nouveau followup pour la régression linéaire
$followup = new CityFollowUp();
$followup->setName('regression_angle');
$followup->setMeasure($data['angle']);
$followup->setDate(new \DateTime());
$followup->setStats($statsGlobal);
$this->entityManager->persist($followup);
// Créer un followup pour la pente
$followupSlope = new CityFollowUp();
$followupSlope->setName('regression_slope');
$followupSlope->setMeasure($data['slope']);
$followupSlope->setDate(new \DateTime());
$followupSlope->setStats($statsGlobal);
$this->entityManager->persist($followupSlope);
// Créer un followup pour l'ordonnée à l'origine
$followupIntercept = new CityFollowUp();
$followupIntercept->setName('regression_intercept');
$followupIntercept->setMeasure($data['intercept']);
$followupIntercept->setDate(new \DateTime());
$followupIntercept->setStats($statsGlobal);
$this->entityManager->persist($followupIntercept);
$this->entityManager->flush();
return new JsonResponse([
'success' => true,
'message' => 'Données de régression enregistrées avec succès',
'followup' => [
'id' => $followup->getId(),
'name' => $followup->getName(),
'measure' => $followup->getMeasure(),
'date' => $followup->getDate()->format('Y-m-d H:i:s')
]
]);
}
#[Route('/modify/{osm_object_id}/{version}/{changesetID}', name: 'app_public_submit')]
public function submit($osm_object_id, $version, $changesetID): Response
{
@ -666,7 +807,7 @@ class PublicController extends AbstractController
$followups = $stats->getCityFollowUps();
$countData = [];
$completionData = [];
foreach ($followups as $fu) {
if ($fu->getName() === $theme . '_count') {
$countData[] = [
@ -849,4 +990,47 @@ class PublicController extends AbstractController
'places_6mois' => $places_6mois,
]);
}
#[Route('/rss/demandes', name: 'app_public_rss_demandes')]
public function rssDemandes(): Response
{
$demandes = $this->entityManager->getRepository(Demande::class)->findAllOrderedByCreatedAt();
$content = $this->renderView('public/rss/demandes.xml.twig', [
'demandes' => $demandes,
'base_url' => $this->getParameter('router.request_context.host'),
]);
$response = new Response($content);
$response->headers->set('Content-Type', 'application/rss+xml');
return $response;
}
#[Route('/cities', name: 'app_public_cities')]
public function cities(): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findAll();
// Prepare data for the map
$citiesForMap = [];
foreach ($stats as $stat) {
if ($stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone())) {
$citiesForMap[] = [
'name' => $stat->getName(),
'zone' => $stat->getZone(),
'lat' => $stat->getLat(),
'lon' => $stat->getLon(),
'placesCount' => $stat->getPlacesCount(),
'completionPercent' => $stat->getCompletionPercent(),
];
}
}
return $this->render('public/cities.html.twig', [
'stats' => $stats,
'citiesForMap' => $citiesForMap,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
]);
}
}