<?php
namespace App\Controller;
use App\Entity\Job;
use App\Entity\Network;
use App\Entity\Suggestion;
use App\Form\PhoneType;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use App\Repository\SuggestionRepository;
use Doctrine\ORM\EntityManagerInterface;
use App\Message\UpdateCaptainDataCookie;
use App\Message\ProcessPendingMessagesJob;
use App\Entity\CaptaindataJob;
use Symfony\Component\Messenger\MessageBusInterface;
use DateTimeImmutable;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use App\Repository\MessageRepository;
use App\Entity\Message;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use App\Service\GeminiApiService;
class SuggestionController extends AbstractController
{
use TargetPathTrait;
private $httpClient;
private $logger;
private $geminiApiService;
private $kernel;
private $entityManager;
private $bus;
private $suggestionLoadingLogger;
private $params;
private const AI_TEMPERATURE = 0.2;
public function __construct(
HttpClientInterface $httpClient,
LoggerInterface $logger,
LoggerInterface $suggestionLoadingLogger,
GeminiApiService $geminiApiService,
KernelInterface $kernel,
EntityManagerInterface $entityManager,
MessageBusInterface $bus,
ParameterBagInterface $params
) {
$this->httpClient = $httpClient;
$this->logger = $logger;
$this->suggestionLoadingLogger = $suggestionLoadingLogger;
$this->geminiApiService = $geminiApiService;
$this->kernel = $kernel;
$this->entityManager = $entityManager;
$this->bus = $bus;
$this->params = $params;
}
/**
* @Route("/suggestions-ia", name="suggestion.index")
*/
public function index(
Request $request,
SuggestionRepository $suggestionRepository,
MessageRepository $messageRepository
): Response {
/** @var \App\Entity\User|null $user */
$user = $this->getUser();
// Important : le User stocké dans le token Symfony peut être obsolète (session),
// alors que la DB est à jour (identity_uid, flags LinkedIn, etc.).
// On force ici un refresh Doctrine pour garantir que le rendu Twig reflète la DB.
try {
if ($user instanceof \App\Entity\User) {
$this->entityManager->refresh($user);
}
} catch (\Exception $e) {
// Best-effort : ne pas bloquer la page si refresh impossible
}
// Sauvegarder le token en session si présent dans l'URL (pour préserver après déconnexion)
$token = $request->query->get('token', '');
if (!empty($token)) {
$request->getSession()->set('notification_token', $token);
}
if (!$user) {
// Récupérer le token depuis les paramètres de l'URL ou de la session
if (empty($token)) {
$token = $request->getSession()->get('notification_token', '');
}
if (!empty($token)) {
return $this->redirectToRoute('app_login', ['token' => $token]);
}
return $this->redirectToRoute('app_login');
}
// =========================================================================
// LOGIQUE DE MISE À JOUR DU COOKIE A ÉTÉ SUPPRIMÉE DE CETTE MÉTHODE
// Elle est maintenant gérée par ExtensionController::saveCookies
// =========================================================================
// --- Préparation des données pour le template ---
// 1. On vérifie s'il faut afficher l'alerte mobile
$failedMessage = $messageRepository->findOneBy([
'user' => $user,
'status' => Message::STATUS_SENT_FAILED
]);
$showMobileReconnectAlert = ($failedMessage !== null);
// 2. Vérifier si l'utilisateur a une intégration LinkedIn valide
// L'identity_uid est requis pour avoir une intégration LinkedIn fonctionnelle
$hasLinkedInIntegration = $user->getHasLinkedinIntegration() === true
&& $user->getLinkedinIntegrationStatus() === 'VALID'
&& !empty($user->getIdentityUid());
// 2.1. Déclencher le traitement des messages en attente si l'intégration est valide
// OU si l'utilisateur a un identity_uid (l'intégration peut être mise à jour via synchronisation cookie)
if ($hasLinkedInIntegration || (!empty($user->getIdentityUid()) && $user->getHasLinkedinIntegration())) {
$this->triggerPendingMessagesIfNeeded($user);
}
// 3. On génère l'URL de connexion LinkedIn pour le template
$linkedinUrl = 'https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id='
. $this->getParameter('app.linkedin_client_id')
. '&redirect_uri=' . $this->getParameter('app.root_url') . '/connect/linkedin&state='
. bin2hex(random_bytes(10))
. '&scope=openid%20profile%20email';
// 4. On sauvegarde la destination pour la redirection post-login
$this->saveTargetPath($request->getSession(), 'main', $this->generateUrl('suggestion.index'));
// --- Récupération des données des suggestions ---
if (!method_exists($user, 'getClient') || !($client = $user->getClient())) {
$this->addFlash('error', 'Aucun client associé à votre compte.');
return $this->render('suggestion/index.html.twig', [
'hasSuggestions' => 0,
'totalSuggestionsCount' => 0,
'suggestionsByJob' => [],
'linkedinUrl' => $linkedinUrl,
'showMobileReconnectAlert' => $showMobileReconnectAlert,
'chrome_extension_id' => $_ENV['CHROME_EXTENSION_ID'],
]);
}
$companyName = $client->getName() ?? '[NOM ENTREPRISE]';
$userId = $user->getId();
$clientId = $client->getId();
$this->suggestionLoadingLogger->info("Index page: User ID {$userId}, Client ID {$clientId}");
$userSuggestionsCheck = $suggestionRepository->hasSuggestions($userId);
$hasSuggestions = count($userSuggestionsCheck) > 0;
$this->suggestionLoadingLogger->info("Index page: hasSuggestions check returned " . ($hasSuggestions ? 'true' : 'false'));
$allFilteredSuggestions = $suggestionRepository->findByClientIdExcludingMessagedNetworks($clientId, $userId);
$totalSuggestionsCount = count($allFilteredSuggestions);
$this->suggestionLoadingLogger->info("Index page: findByClientIdExcludingMessagedNetworks found {$totalSuggestionsCount} suggestions.");
$allPendingSuggestionsCount = $suggestionRepository->countAllPendingSuggestions($userId, $clientId);
$this->suggestionLoadingLogger->info("Index page: countAllPendingSuggestions found {$allPendingSuggestionsCount} total pending suggestions.");
// A-t-il déjà traité au moins une suggestion ? (pour l'état vide)
$hasEverProcessed = $suggestionRepository->hasUserProcessedSuggestions($userId);
$suggestionsByJob = [];
foreach ($allFilteredSuggestions as $oneSuggestion) {
$job = $oneSuggestion->getJob();
if ($job) {
$jobId = $job->getId();
if (!isset($suggestionsByJob[$jobId])) {
$suggestionsByJob[$jobId] = ['job' => $job, 'suggestions' => []];
}
$suggestionsByJob[$jobId]['suggestions'][] = $oneSuggestion;
}
}
uasort($suggestionsByJob, function ($a, $b) {
return count($b['suggestions']) <=> count($a['suggestions']);
});
$this->suggestionLoadingLogger->info("Index page: suggestionsByJob constructed with " . count($suggestionsByJob) . " job(s).", [
'job_ids' => array_keys($suggestionsByJob),
'total_suggestions_in_array' => array_sum(array_map(function ($item) {
return count($item['suggestions']);
}, $suggestionsByJob))
]);
// --- Données "premier profil" (desktop) ---
// Objectif : éviter toute dépendance à des variables Twig implicites (strict_variables)
$firstJob = null;
$firstSuggestion = null;
$firstSuggestionCount = 0;
if (!empty($suggestionsByJob)) {
$firstJobData = reset($suggestionsByJob);
if (is_array($firstJobData)) {
$firstJob = $firstJobData['job'] ?? null;
$firstSuggestions = $firstJobData['suggestions'] ?? [];
if (is_array($firstSuggestions) && !empty($firstSuggestions)) {
$firstSuggestion = $firstSuggestions[0];
$firstSuggestionCount = count($firstSuggestions);
}
}
}
$phoneForm = null;
if (method_exists($user, 'getPhone') && !$user->getPhone()) {
$phoneForm = $this->createForm(PhoneType::class, $user, [
'action' => $this->generateUrl('ajax_user_update_phone'),
'method' => 'POST',
]);
}
$userHasSeenPreviewModal = false;
if ($user instanceof \App\Entity\User && method_exists($user, 'isHasSeenLinkedinPreviewModal')) {
$userHasSeenPreviewModal = $user->isHasSeenLinkedinPreviewModal();
}
return $this->render('suggestion/index.html.twig', [
'hasSuggestions' => $hasSuggestions,
'totalSuggestionsCount' => $totalSuggestionsCount,
'allPendingSuggestionsCount' => $allPendingSuggestionsCount,
'hasEverProcessed' => $hasEverProcessed,
'suggestionsByJob' => $suggestionsByJob,
'firstJob' => $firstJob,
'firstSuggestion' => $firstSuggestion,
'firstSuggestionCount' => $firstSuggestionCount,
'phoneForm' => $phoneForm ? $phoneForm->createView() : null,
'userHasSeenPreviewModal' => $userHasSeenPreviewModal,
'user_template_formal' => $user->getMessageTemplateFormal(),
'user_template_informal' => $user->getMessageTemplateInformal(),
'user_greeting_formal' => $user->getGreetingFormal(),
'user_greeting_informal' => $user->getGreetingInformal(),
'company_name' => $companyName,
'is_tour_hidden' => $user->isIsSuggestionTourHidden(),
'linkedinUrl' => $linkedinUrl,
'showMobileReconnectAlert' => $showMobileReconnectAlert, // Utilise la variable calculée au début
'chrome_extension_id' => $_ENV['CHROME_EXTENSION_ID'],
'hasLinkedInIntegration' => $hasLinkedInIntegration, // Nouveau
'hasExtension' => $user->getHasExtension() === 1, // Pour afficher le lien d'installation
'hasCredentials' => $user->hasValidLinkedinCredentials(), // Pour distinguer le parcours credentials
]);
}
/**
* Déclenche le traitement des messages en attente si l'intégration LinkedIn est VALID
*
* @param \App\Entity\User $user Utilisateur
* @return void
*/
private function triggerPendingMessagesIfNeeded(\App\Entity\User $user): void
{
// Vérifier s'il y a des messages en attente
$pendingMessages = $this->entityManager->getRepository(Message::class)->findBy([
'user' => $user,
'status' => Message::STATUS_PENDING,
]);
if (empty($pendingMessages)) {
$this->logger->debug('SUGGESTION: Aucun message en attente pour cet utilisateur', [
'user_id' => $user->getId(),
]);
return; // Pas de messages en attente
}
// Vérifier que l'intégration est VALID avant de déclencher
// Si elle est EXPIRED, on attend la synchronisation du cookie
if ($user->getLinkedinIntegrationStatus() !== 'VALID') {
$this->logger->info('SUGGESTION: Messages en attente détectés mais intégration non VALID, attente synchronisation cookie', [
'user_id' => $user->getId(),
'pending_count' => count($pendingMessages),
'integration_status' => $user->getLinkedinIntegrationStatus(),
]);
return;
}
$this->logger->info('SUGGESTION: Messages en attente détectés, déclenchement du job', [
'user_id' => $user->getId(),
'pending_count' => count($pendingMessages),
]);
// Vérifier s'il existe déjà un job en attente
$existingJob = $this->entityManager->getRepository(CaptaindataJob::class)->findOneBy([
'user' => $user,
'workflowType' => CaptaindataJob::WORKFLOW_TYPE_SEND_MESSAGES,
'status' => CaptaindataJob::STATUS_SEND_MESSAGES_PENDING,
]);
if ($existingJob) {
$this->logger->info('SUGGESTION: Job existant trouvé, re-dispatch', [
'user_id' => $user->getId(),
'job_id' => $existingJob->getId(),
]);
$this->bus->dispatch(new ProcessPendingMessagesJob($existingJob->getId()));
return;
}
// Créer un nouveau job
$workflowUid = $this->params->get('app.captaindata_workflow_send_message_uid');
$captainDataJob = new CaptaindataJob(
$user,
CaptaindataJob::WORKFLOW_TYPE_SEND_MESSAGES,
$workflowUid,
CaptaindataJob::STATUS_SEND_MESSAGES_PENDING
);
$this->entityManager->persist($captainDataJob);
$this->entityManager->flush();
$this->bus->dispatch(new ProcessPendingMessagesJob($captainDataJob->getId()));
$this->logger->info('SUGGESTION: Nouveau job créé et dispatché pour messages en attente', [
'user_id' => $user->getId(),
'job_id' => $captainDataJob->getId(),
'pending_count' => count($pendingMessages),
]);
}
/**
* @Route("/ajax/suggestion/update-refusal", name="ajax_suggestion_update_refusal", methods={"POST"})
*/
public function updateRefusalReason(
Request $request,
SuggestionRepository $suggestionRepository,
EntityManagerInterface $entityManager
): JsonResponse {
if (!$request->isXmlHttpRequest()) {
return new JsonResponse(['status' => 'error', 'message' => 'Requête non autorisée.'], Response::HTTP_FORBIDDEN);
}
if (!$this->getUser()) {
return new JsonResponse(['status' => 'error', 'message' => 'Utilisateur non authentifié.'], Response::HTTP_UNAUTHORIZED);
}
$data = $request->request->all();
$suggestionId = $data['suggestionId'] ?? null;
$refusalReason = $data['refusalReason'] ?? null;
if (!$suggestionId || $refusalReason === null || trim($refusalReason) === '') {
return new JsonResponse(['status' => 'error', 'message' => 'Données manquantes ou raison vide.'], Response::HTTP_BAD_REQUEST);
}
$suggestion = $suggestionRepository->find($suggestionId);
if (!$suggestion) {
return new JsonResponse(['status' => 'error', 'message' => 'Suggestion non trouvée.'], Response::HTTP_NOT_FOUND);
}
try {
$suggestion->setRefusal($refusalReason);
$suggestion->setSendStatus(0);
$suggestion->setProcessedAt(new \DateTimeImmutable());
$entityManager->flush();
return new JsonResponse(['status' => 'success', 'message' => 'Raison du refus enregistrée.']);
} catch (\Exception $e) {
$this->logger->error("Erreur MAJ refus suggestion ID {$suggestionId}: " . $e->getMessage());
return new JsonResponse(['status' => 'error', 'message' => 'Erreur lors de la mise à jour de la suggestion.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @Route("/api/suggestion/{id}/analyze", name="api_suggestion_analyze", methods={"POST"})
*/
public function api_suggestion_analyze(Suggestion $suggestion): JsonResponse
{
if (!$this->getUser()) {
return new JsonResponse(['success' => false, 'message' => 'Utilisateur non authentifié.'], Response::HTTP_UNAUTHORIZED);
}
$network = $suggestion->getNetwork();
$job = $suggestion->getJob();
if (!$network || !$job) {
$this->logger->error("Données Network ou Job manquantes pour l'analyse IA de la suggestion ID: " . $suggestion->getId());
return new JsonResponse(['success' => false, 'message' => 'Données de la suggestion incomplètes pour l\'analyse.'], Response::HTTP_BAD_REQUEST);
}
if ($suggestion->getExtendedComment() !== null) {
return new JsonResponse([
'success' => true,
'analysis' => $suggestion->getExtendedComment(),
'from_cache' => true
]);
}
// 1. Lire le template du prompt depuis le fichier
$promptTemplatePath = $this->kernel->getProjectDir() . '/config/prompts/gemini_2.5_flash.txt'; // Assurez-vous que ce chemin/nom est correct
if (!file_exists($promptTemplatePath)) {
$this->logger->error("Template de prompt non trouvé à: " . $promptTemplatePath);
return new JsonResponse(['success' => false, 'message' => 'Erreur de configuration interne (template de prompt manquant).'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$promptTemplate = file_get_contents($promptTemplatePath);
// 2. Préparer les valeurs pour les variables
$candidateFirstName = $network->getFirstname() ?? '[Prénom Candidat Manquant]';
// Utilisation directe du contenu de linkedin_data comme CV
$fullCVText = $this->extractCVText($network); // Cette méthode retourne maintenant le JSON brut ou un placeholder
$fullJobOfferText = $this->extractJobOfferText($job); // Assurez-vous que cette méthode est bien implémentée
$jobTitleFromOffer = $job->getTitle() ?? '[Titre Poste Manquant]';
// 3. Remplacer les variables dans le template
$replacements = [
'$Candidate_FirstName' => $candidateFirstName,
'$Full_CV_Text_Here' => $fullCVText,
'$Full_Job_Offer_Text_Here' => $fullJobOfferText,
'$Job_Title_From_Offer' => $jobTitleFromOffer,
];
$prompt = str_replace(array_keys($replacements), array_values($replacements), $promptTemplate);
$this->logger->info(
"Prompt préparé pour Gemini pour suggestion ID {$suggestion->getId()} (longueur: " . strlen($prompt) . " caractères)."
// Décommentez la ligne suivante pour logger le prompt complet dans le contexte si besoin pour le débogage
,
['full_prompt_for_debug' => $prompt]
);
// 4. Construire le payload pour Gemini (Google AI Studio)
$payload = [
"contents" => [
["role" => "user", "parts" => [["text" => $prompt]]]
],
"generationConfig" => [
"temperature" => self::AI_TEMPERATURE,
"topK" => 20,
"topP" => 0.95,
"maxOutputTokens" => 1000,
]
];
$this->logger->info("Appel API Gemini (Google AI Studio) pour suggestion ID {$suggestion->getId()}");
$decodedResponse = $this->geminiApiService->generateContent('gemini-2.5-pro', $payload);
if ($decodedResponse === null) {
$this->logger->error("Échec de l'appel Gemini API pour suggestion ID {$suggestion->getId()}");
return new JsonResponse([
'success' => false,
'isFriendlyError' => true,
'friendlyError' => [
'title' => 'Petite mise au point en cours !',
'message' => 'Nous affûtons nos algorithmes pour rendre nos suggestions encore plus pertinentes. Le service sera de retour dans quelques instants, plus performant que jamais.'
]
], Response::HTTP_SERVICE_UNAVAILABLE);
}
$this->logger->debug("Réponse brute Gemini (Google AI) pour suggestion ID {$suggestion->getId()}: ", $decodedResponse);
$analysisHtml = null;
if (isset($decodedResponse['candidates'][0]['content']['parts'][0]['text'])) {
$analysisText = $decodedResponse['candidates'][0]['content']['parts'][0]['text'];
// --- DÉBUT DE LA MODIFICATION ---
// Nettoyage initial du texte reçu de l'IA
$analysisText = preg_replace('/^```(html)?\s*?\n?/im', '', $analysisText);
$analysisText = preg_replace('/\n?```\s*?$/im', '', $analysisText);
$analysisText = trim($analysisText);
// Transformation en HTML avec le bon ordre de priorité
$lines = preg_split('/(\r\n|\n|\r)+/', $analysisText);
$htmlOutput = '';
$inList = false; // Pour savoir si nous sommes en train de construire une liste <ul>
$regexTitle = '/^\s*(?:\*\s*)?\*\*(.*?)\*\*\s*(.*)$/';
foreach ($lines as $line) {
$trimmedLine = trim($line);
if (empty($trimmedLine)) {
continue;
}
// CAS 1 (LE PLUS SPÉCIFIQUE) : La ligne est un "Titre : Paragraphe"
if (preg_match($regexTitle, $trimmedLine, $matches)) {
// Un titre signale la fin de toute liste précédente.
if ($inList) {
$htmlOutput .= '</ul>';
$inList = false;
}
$titleText = trim($matches[1]);
$paragraphText = trim($matches[2]);
$combinedHtml = '<strong>' . htmlspecialchars($titleText) . '</strong>';
if (!empty($paragraphText)) {
$combinedHtml .= '<br>' . htmlspecialchars($paragraphText);
}
$htmlOutput .= '<p>' . $combinedHtml . '</p>';
// CAS 2 : La ligne est un élément de liste simple
} elseif (strpos($trimmedLine, '* ') === 0) {
if (!$inList) {
// Si ce n'est pas déjà fait, on ouvre la balise <ul>
$htmlOutput .= '<ul>';
$inList = true;
}
// On retire le "* " du début et on crée le <li>
$listItemText = substr($trimmedLine, 2);
$htmlOutput .= '<li>' . htmlspecialchars(trim($listItemText)) . '</li>';
// CAS 3 : La ligne est un paragraphe simple
} else {
// Un paragraphe simple signale aussi la fin de toute liste.
if ($inList) {
$htmlOutput .= '</ul>';
$inList = false;
}
$htmlOutput .= '<p>' . htmlspecialchars($trimmedLine) . '</p>';
}
}
// Après la boucle, on s'assure de fermer la liste si le texte se terminait par un <li>
if ($inList) {
$htmlOutput .= '</ul>';
}
$analysisHtml = $htmlOutput;
} else {
$this->logger->warning("Réponse de Gemini inattendue pour suggestion ID {$suggestion->getId()}: ", $decodedResponse);
return new JsonResponse(['success' => false, 'message' => 'Réponse de l\'IA inattendue ou vide.'], Response::HTTP_OK);
}
if (empty(trim(strip_tags((string)$analysisHtml)))) { // Vérifier si l'analyse n'est pas vide après suppression des tags HTML
$this->logger->info("Analyse IA vide après nettoyage pour suggestion ID {$suggestion->getId()}");
return new JsonResponse(['success' => true, 'analysis' => '<p class="text-muted"><em>L\'IA n\'a pas fourni d\'analyse pour ce profil.</em></p>'], Response::HTTP_OK);
}
// Enregistrer l'analyse directement sur l'entité Suggestion
$suggestion->setExtendedComment($analysisHtml);
$suggestion->setExtendedCommentCreatedAt(new \DateTimeImmutable());
$this->entityManager->flush();
return new JsonResponse(['success' => true, 'analysis' => $analysisHtml]);
}
/**
* Méthode pour extraire le contenu du CV pour le prompt.
* Retourne directement le contenu de linkedin_data.
*/
private function extractCVText(Network $network): string
{
$linkedinDataJson = $network->getLinkedinData();
if (empty($linkedinDataJson)) {
$this->logger->warning("Le champ linkedin_data est vide pour Network ID: " . $network->getId() . ". Envoi d'un placeholder pour le CV.");
return "[Aucune donnée détaillée de CV disponible pour ce profil.]";
}
return $linkedinDataJson;
}
/**
* Méthode pour extraire/formater le texte de l'offre d'emploi pour le prompt.
*/
private function extractJobOfferText(Job $job): string
{
$description = $job->getClient()->getDescription();
$offerText = "Titre du poste: {$job->getTitle()}. ";
if ($description) {
$offerText .= "Description de l'entreprise: " . strip_tags($description) . ". ";
}
if (method_exists($job, 'getProfile') && ($profileText = $job->getProfile())) {
$offerText .= "Profil recherché: " . strip_tags($profileText) . ". ";
}
if (method_exists($job, 'getMission') && ($missionText = $job->getMission())) {
$offerText .= "Missions: " . strip_tags($missionText) . ". ";
}
// Ajoutez d'autres champs pertinents ici si nécessaire
return empty(trim($offerText)) ? "[Description de l'offre non disponible ou vide]" : $offerText;
}
/**
* @Route("/ajax/user/update-phone", name="ajax_user_update_phone", methods={"POST"})
*/
public function updateUserPhone(Request $request, EntityManagerInterface $entityManager, SuggestionRepository $suggestionRepository): Response
{
$isAjax = $request->isXmlHttpRequest();
/** @var \App\Entity\User|null $user */
$user = $this->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Utilisateur non authentifié.'], Response::HTTP_UNAUTHORIZED);
}
$form = $this->createForm(PhoneType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
// Mettre à jour l'étape d'onboarding
$user->setOnboardingStep('completed');
$entityManager->flush(); // User a été mis à jour par le form->handleRequest
// Après enregistrement, vérifier si des suggestions sont déjà disponibles pour cet utilisateur
$userId = $user->getId();
$userSuggestionsCheck = $suggestionRepository->hasSuggestions($userId);
$hasSuggestionsNow = count($userSuggestionsCheck) > 0;
// Préparer la réponse selon le type de requête (AJAX vs form classique)
if ($isAjax) {
$response = [
'success' => true,
'message' => 'Numéro de téléphone enregistré.',
'hasSuggestions' => $hasSuggestionsNow,
'redirect_url' => $this->generateUrl('suggestion.index')
];
return new JsonResponse($response);
}
// Requête non-AJAX (submit classique) : rediriger vers la page des suggestions
$this->addFlash('success', 'Numéro de téléphone enregistré.');
return $this->redirectToRoute('suggestion.index');
} catch (\Exception $e) {
$this->logger->error("Erreur MAJ téléphone utilisateur ID {$user->getId()}: " . $e->getMessage());
if ($isAjax) {
return new JsonResponse(['success' => false, 'message' => 'Erreur lors de l\'enregistrement du numéro.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$this->addFlash('error', 'Erreur lors de l\'enregistrement du numéro.');
return $this->redirectToRoute('suggestion.index');
}
}
// Récupérer les erreurs du formulaire pour les renvoyer si nécessaire
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[] = $error->getMessage();
}
// Pour les erreurs sur des champs spécifiques
foreach ($form as $child) {
if (!$child->isValid()) {
foreach ($child->getErrors(true) as $error) {
$errors[$child->getName()][] = $error->getMessage();
}
}
}
if ($isAjax) {
return new JsonResponse([
'success' => false,
'message' => 'Données invalides.',
'errors' => $errors
], Response::HTTP_BAD_REQUEST);
}
// Requête non-AJAX : afficher un message et rediriger
$this->addFlash('error', implode(', ', array_values($errors) ?: ['Données invalides.']));
return $this->redirectToRoute('suggestion.index');
}
/**
* @Route("/ajax/user/mark-linkedin-preview-modal-shown", name="ajax_user_mark_linkedin_preview_modal_shown", methods={"POST"})
*/
public function markLinkedInPreviewModalAsShown(Request $request, EntityManagerInterface $entityManager): JsonResponse
{
if (!$request->isXmlHttpRequest()) {
return new JsonResponse(['status' => 'error', 'message' => 'Requête non autorisée.'], Response::HTTP_FORBIDDEN);
}
/** @var \App\Entity\User|null $user */
$user = $this->getUser();
// Assurez-vous que $user est bien une instance de votre entité User
if (!$user instanceof \App\Entity\User) {
return new JsonResponse(['status' => 'error', 'message' => 'Utilisateur non authentifié ou type incorrect.'], Response::HTTP_UNAUTHORIZED);
}
// Vérifiez que la méthode existe pour éviter les erreurs si l'entité n'est pas à jour
if (method_exists($user, 'setHasSeenLinkedinPreviewModal') && method_exists($user, 'isHasSeenLinkedinPreviewModal')) {
if (!$user->isHasSeenLinkedinPreviewModal()) {
$user->setHasSeenLinkedinPreviewModal(true);
try {
$entityManager->flush();
return new JsonResponse(['status' => 'success', 'message' => 'Préférence enregistrée.']);
} catch (\Exception $e) {
$this->logger->error("Erreur MAJ isHasSeenLinkedinPreviewModal pour User ID {$user->getId()}: " . $e->getMessage(), ['exception' => $e]);
return new JsonResponse(['status' => 'error', 'message' => 'Erreur serveur lors de la sauvegarde.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
return new JsonResponse(['status' => 'info', 'message' => 'Préférence déjà enregistrée.']);
} else {
$this->logger->error("Méthode setHasSeenLinkedinPreviewModal ou isHasSeenLinkedinPreviewModal non trouvée pour User ID {$user->getId()}");
return new JsonResponse(['status' => 'error', 'message' => 'Erreur de configuration du modèle utilisateur.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @Route("/ajax/user/hide-suggestion-tour", name="ajax_user_hide_suggestion_tour", methods={"POST"})
*/
public function hideSuggestionTour(Request $request, EntityManagerInterface $entityManager): JsonResponse
{
if (!$request->isXmlHttpRequest()) {
return new JsonResponse(['status' => 'error', 'message' => 'Requête non autorisée.'], Response::HTTP_FORBIDDEN);
}
/** @var \App\Entity\User|null $user */
$user = $this->getUser();
if (!$user) {
return new JsonResponse(['status' => 'error', 'message' => 'Utilisateur non authentifié.'], Response::HTTP_UNAUTHORIZED);
}
try {
$user->setIsSuggestionTourHidden(true);
$entityManager->flush();
return new JsonResponse(['status' => 'success']);
} catch (\Exception $e) {
$this->logger->error("Erreur MAJ isSuggestionTourHidden pour User ID {$user->getId()}: " . $e->getMessage());
return new JsonResponse(['status' => 'error', 'message' => 'Erreur serveur.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* @Route("/ajax/suggestion/load-next", name="ajax_suggestion_load_next", methods={"POST"})
*/
public function loadNextSuggestion(SuggestionRepository $suggestionRepository, EntityManagerInterface $entityManager): JsonResponse
{
/** @var \App\Entity\User|null $user */
$user = $this->getUser();
if (!$user || !$user->getClient()) {
$this->suggestionLoadingLogger->warning('Tentative de chargement de suggestion par un utilisateur non authentifié ou sans client.');
return new JsonResponse(['status' => 'error', 'message' => 'Utilisateur non autorisé.'], Response::HTTP_UNAUTHORIZED);
}
$userId = $user->getId();
$clientId = $user->getClient()->getId();
$this->suggestionLoadingLogger->info("Recherche des prochaines suggestions pour l'utilisateur ID: {$userId}, Client ID: {$clientId}");
$nextSuggestions = $suggestionRepository->findNextHiddenSuggestions($userId, $clientId);
if (empty($nextSuggestions)) {
$this->suggestionLoadingLogger->info("Aucune suggestion cachée trouvée pour l'utilisateur ID: {$userId}");
return new JsonResponse(['status' => 'done', 'message' => 'Aucune autre suggestion disponible.']);
}
$suggestionsData = [];
foreach ($nextSuggestions as $nextSuggestion) {
$suggestionId = $nextSuggestion->getId();
$this->suggestionLoadingLogger->info("Suggestion ID: {$suggestionId} trouvée. Passage de is_hidden à false.");
$nextSuggestion->setIsHidden(false);
$job = $nextSuggestion->getJob();
$network = $nextSuggestion->getNetwork();
// Préparer les données pour cette suggestion
$suggestionsData[] = [
'id' => $nextSuggestion->getId(),
'comment' => $nextSuggestion->getComment(),
'network' => [
'id' => $network->getId(),
'firstname' => $network->getFirstname(),
'lastname' => $network->getLastname(),
'photo' => $network->getPhoto(),
'linkedin' => $network->getLinkedin(),
'job' => $network->getJob(),
],
'job' => [
'id' => $job->getId(),
'title' => $job->getTitle(),
]
];
}
$entityManager->flush();
$this->suggestionLoadingLogger->info(count($suggestionsData) . " suggestion(s) mise(s) à jour avec succès. Préparation de la réponse JSON.");
return new JsonResponse(['status' => 'success', 'suggestions' => $suggestionsData]);
}
}