src/Controller/ThemesWebsite/Whileresume/Website/CandidatesController.php line 145

Open in your IDE?
  1. <?php
  2. namespace App\Controller\ThemesWebsite\Whileresume\Website;
  3. use App\Entity\Articles\Articles;
  4. use App\Entity\Core\Users;
  5. use App\Entity\Cvs\Candidates;
  6. use App\Entity\Cvs\Jobs;
  7. use App\Entity\Cvs\JobsHasLikes;
  8. use App\Entity\Cvs\KeywordsLandingJobs;
  9. use App\Entity\Cvs\LandingJobs;
  10. use App\Entity\Pages\Pages;
  11. use App\Form\Core\UsersEmailForm;
  12. use App\Security\LoginFormAuthenticator;
  13. use App\Services\Core\RequestData;
  14. use Doctrine\ORM\EntityManagerInterface;
  15. use Knp\Component\Pager\PaginatorInterface;
  16. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
  17. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  18. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  19. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  20. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  21. use Symfony\Component\HttpFoundation\Cookie;
  22. use Symfony\Component\HttpFoundation\JsonResponse;
  23. use Symfony\Component\HttpFoundation\RedirectResponse;
  24. use Symfony\Component\HttpFoundation\Request;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\Routing\Annotation\Route;
  27. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  28. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  29. use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
  30. use Symfony\Component\Security\Core\User\UserInterface;
  31. use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
  32. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  33. use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
  34. use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
  35. use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
  36. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordCredentialsBadge;
  37. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  38. use Twig\Environment;
  39. class CandidatesController extends AbstractController
  40. {
  41.     private $rd;
  42.     private $em;
  43.     private $passwordEncoder;
  44.     private $ms;
  45.     private $us;
  46.     private $authenticator;
  47.     private $userAuthenticator;
  48.     private $paginator;
  49.     private $twig;
  50.     public function __construct(RequestData                  $rd,
  51.                                 EntityManagerInterface       $em,
  52.                                 UserPasswordEncoderInterface $passwordEncoder,
  53.                                 \App\Services\Mails          $ms,
  54.                                 \App\Services\Core\Users     $us,
  55.                                 UserAuthenticatorInterface   $userAuthenticator,
  56.                                 LoginFormAuthenticator       $authenticator,
  57.                                 PaginatorInterface           $paginator,
  58.                                 Environment $twig,
  59.     ) {
  60.         $this->rd $rd;
  61.         $this->em $em;
  62.         $this->passwordEncoder $passwordEncoder;
  63.         $this->ms $ms;
  64.         $this->authenticator $authenticator;
  65.         $this->userAuthenticator $userAuthenticator;
  66.         $this->us $us;
  67.         $this->paginator $paginator;
  68.         $this->twig $twig;
  69.     }
  70.     /**
  71.      * Homepage principal
  72.      * @param Request $request
  73.      * @return Response
  74.      */
  75.     public function homepage(Request $request): Response
  76.     {
  77.         $locale $request->getLocale();
  78.         // Page meta (title, description, robots, etc.)
  79.         $page $this->em->getRepository(Pages::class)->findOneBy(['name' => 'homepage''locale' => $locale]);
  80.         // Articles : 6 derniers pour la section "Conseils carrière"
  81.         $articles $this->em->getRepository(Articles::class)->getArticles(6$locale);
  82.         // Stat hero : nombre total d'offres en ligne
  83.         $totalJobs $this->em->getRepository(Jobs::class)->countAllOnlineJobs();
  84.         // Jobs aléatoires affichés par défaut dans le hero (filtrés par locale de navigation)
  85.         // On régénère le seed à chaque chargement de la homepage : ainsi l'utilisateur
  86.         // voit des offres différentes à chaque visite. Le seed est ensuite stocké en
  87.         // session pour que la pagination AJAX (searchJobsApi sans query) garde le
  88.         // même ordre que celui rendu côté serveur.
  89.         $seed random_int(1PHP_INT_MAX);
  90.         $request->getSession()->set('jobs_random_seed'$seed);
  91.         $randomJobs $this->em->getRepository(Jobs::class)->findRandomJobs($locale06$seed);
  92.         // Mots-clés populaires (categories) — affichés en pills + sections d'offres associées
  93.         $keywordsEntities $this->em->getRepository(KeywordsLandingJobs::class)->findByLocaleForHomepage($locale);
  94.         $popularKeywords = [];
  95.         $popularJobs = [];
  96.         $jobsRepo $this->em->getRepository(Jobs::class);
  97.         foreach ($keywordsEntities as $kw) {
  98.             $label   $kw->getLabel() ?? $kw->getSearchKeyword();
  99.             $icon    $kw->getIcon() ?? '';
  100.             $keyword $kw->getSearchKeyword() ?? $kw->getLabel();
  101.             // Liste utilisée pour la barre de pills "Recherchez par catégorie"
  102.             $popularKeywords[] = [
  103.                 'label'   => $label,
  104.                 'icon'    => $icon,
  105.                 'keyword' => $keyword,
  106.             ];
  107.             // Pour chaque keyword, on ramène 4 offres pour la section "Offres populaires"
  108.             $popularJobs[] = [
  109.                 'label' => $label,
  110.                 'icon'  => $icon,
  111.                 'jobs'  => $jobsRepo->findByKeyword($keyword$locale4),
  112.             ];
  113.         }
  114.         // Form d'inscription pour la section signup intégrée à la homepage.
  115.         // Le form POST vers la page inscription qui contient déjà toute la logique
  116.         // (validation, reCAPTCHA, envoi mail de bienvenue, auth automatique).
  117.         $form $this->createForm(UsersEmailForm::class, null, [
  118.             'action' => $this->generateUrl('whileresume_resume_' $locale),
  119.             'method' => 'POST',
  120.         ]);
  121.         // Incrémenter compteur de vues
  122.         if ($page !== null) {
  123.             $page->setViews((int)$page->getViews() + 1);
  124.             $this->em->persist($page);
  125.             $this->em->flush();
  126.         }
  127.         return $this->render('application/whileresume/website/candidates/homepage.html.twig', [
  128.             'page'            => $page,
  129.             'articles'        => $articles,
  130.             'totalJobs'       => $totalJobs,
  131.             'randomJobs'      => $randomJobs,
  132.             'popularKeywords' => $popularKeywords,
  133.             'popularJobs'     => $popularJobs,
  134.             'form'            => $form->createView(),
  135.         ]);
  136.     }
  137.     private function jobsKeyword($keyword)
  138.     {
  139.         return $this->em->getRepository(Jobs::class)->searchJobs($keyword,'en',0,10);
  140.     }
  141.     /**
  142.      * API : Recherche d'offres (AJAX)
  143.      *
  144.      * Si aucun query n'est fourni : on retourne des offres aléatoires
  145.      * toutes langues confondues, et le total est le nombre total d'offres
  146.      * en ligne sur tout le site.
  147.      *
  148.      * Si un query est fourni : recherche classique filtrée par locale.
  149.      */
  150.     public function searchJobsApi(Request $request): JsonResponse
  151.     {
  152.         $locale $request->getLocale();
  153.         $query trim($request->query->get('q'''));
  154.         $offset max(0, (int) $request->query->get('offset'0));
  155.         $limit min(20max(1, (int) $request->query->get('limit'10)));
  156.         $jobsRepo $this->em->getRepository(Jobs::class);
  157.         if ($query === '') {
  158.             // Mode "default" : jobs aléatoires filtrés par locale de navigation.
  159.             // On utilise un seed basé sur la session pour conserver le même ordre
  160.             // aléatoire entre les paginations successives.
  161.             $session $request->getSession();
  162.             $seed $session->get('jobs_random_seed');
  163.             if ($seed === null) {
  164.                 $seed random_int(1PHP_INT_MAX);
  165.                 $session->set('jobs_random_seed'$seed);
  166.             }
  167.             $jobs $jobsRepo->findRandomJobs($locale$offset$limit$seed);
  168.             // Compteur "toutes langues confondues" pour rester cohérent avec
  169.             // l'affichage initial côté Twig (totalJobs = countAllOnlineJobs).
  170.             $total $jobsRepo->countAllOnlineJobs();
  171.         } else {
  172.             // Recherche classique : on reset le seed pour la prochaine fois
  173.             // qu'on revient en mode default
  174.             $request->getSession()->remove('jobs_random_seed');
  175.             $jobs $jobsRepo->searchJobs($query$locale$offset$limit);
  176.             $total $jobsRepo->countSearchJobs($query$locale);
  177.         }
  178.         // Récupérer les likes du candidat connecté
  179.         $likedJobIds = [];
  180.         $user $this->getUser();
  181.         if ($user !== null && method_exists($user'getCandidate') && $user->getCandidate() !== null) {
  182.             $likes $this->em->getRepository(JobsHasLikes::class)->findBy(['candidate' => $user->getCandidate()]);
  183.             foreach ($likes as $like) {
  184.                 if ($like->getJob() !== null) {
  185.                     $likedJobIds[] = $like->getJob()->getId();
  186.                 }
  187.             }
  188.         }
  189.         $results = [];
  190.         foreach ($jobs as $job) {
  191.             $results[] = $this->serializeJob($job$likedJobIds);
  192.         }
  193.         return new JsonResponse([
  194.             'jobs' => $results,
  195.             'total' => $total,
  196.             'offset' => $offset,
  197.             'limit' => $limit,
  198.             'hasMore' => ($offset $limit) < $total,
  199.         ]);
  200.     }
  201.     /**
  202.      * API : Like / Unlike une offre (AJAX)
  203.      */
  204.     public function toggleJobLikeApi(Request $request): JsonResponse
  205.     {
  206.         $user $this->getUser();
  207.         if ($user === null) {
  208.             return new JsonResponse(['error' => 'Vous devez être connecté.'], 401);
  209.         }
  210.         $candidate $user->getCandidate();
  211.         if ($candidate === null) {
  212.             return new JsonResponse(['error' => 'Vous devez être candidat pour liker une offre.'], 403);
  213.         }
  214.         $data json_decode($request->getContent(), true);
  215.         $jobId $data['jobId'] ?? null;
  216.         if ($jobId === null) {
  217.             return new JsonResponse(['error' => 'ID de l\'offre manquant.'], 400);
  218.         }
  219.         $job $this->em->getRepository(Jobs::class)->find($jobId);
  220.         if ($job === null) {
  221.             return new JsonResponse(['error' => 'Offre introuvable.'], 404);
  222.         }
  223.         // Vérifier si le like existe déjà
  224.         $existingLike $this->em->getRepository(JobsHasLikes::class)->findOneBy([
  225.             'job' => $job,
  226.             'candidate' => $candidate,
  227.         ]);
  228.         if ($existingLike !== null) {
  229.             // Unlike
  230.             $this->em->remove($existingLike);
  231.             $this->em->flush();
  232.             return new JsonResponse(['liked' => false'jobId' => $jobId]);
  233.         }
  234.         // Like
  235.         $like = new JobsHasLikes();
  236.         $like->setJob($job);
  237.         $like->setCandidate($candidate);
  238.         $this->em->persist($like);
  239.         $this->em->flush();
  240.         return new JsonResponse(['liked' => true'jobId' => $jobId]);
  241.     }
  242.     // ======================================================================
  243.     // HELPERS
  244.     // ======================================================================
  245.     /**
  246.      * Sérialise un Job en tableau pour la réponse JSON
  247.      */
  248.     private function serializeJob(Jobs $job, array $likedJobIds): array
  249.     {
  250.         $now = new \DateTime();
  251.         $diffDays $job->getCreatedAt() ? $now->diff($job->getCreatedAt())->days 999;
  252.         // Image du job (Vich uploader) — adapter le chemin si nécessaire
  253.         $imageUrl null;
  254.         if ($job->getImage() && $job->getImage()->getName()) {
  255.             $imageUrl '/uploads/cv_files/' $job->getImage()->getName();
  256.         }
  257.         return [
  258.             'id' => $job->getId(),
  259.             'jobTitle' => $job->getJobTitle(),
  260.             'companyName' => $job->getCompanyName() ?? 'Entreprise',
  261.             'city' => $job->getCity() ?? '',
  262.             'employmentType' => $job->getEmploymentType() ?? 'CDI',
  263.             'experienceLevel' => $job->getExperienceLevel() ?? '',
  264.             'remoteWork' => $job->getRemoteWork() ?? '',
  265.             'salary' => $this->formatSalary($job->getSalaryMin(), $job->getSalaryMax(), $job->getDevise()),
  266.             'slug' => $job->getSlug(),
  267.             'isNew' => $diffDays <= 7,
  268.             'isLiked' => in_array($job->getId(), $likedJobIds),
  269.             'image' => $imageUrl,
  270.             'logo' => $job->getCompanyName() ? mb_strtoupper(mb_substr($job->getCompanyName(), 01)) : '?',
  271.             'category' => $job->getCategory() ?? '',
  272.         ];
  273.     }
  274.     /**
  275.      * Formate le salaire en string lisible : "55-70k€"
  276.      */
  277.     private function formatSalary(?int $min, ?int $max, ?string $devise): string
  278.     {
  279.         $symbol = ($devise === 'USD' || $devise === '$') ? '$' '€';
  280.         if ($min && $max) {
  281.             return floor($min 1000) . '-' floor($max 1000) . 'k' $symbol;
  282.         }
  283.         if ($min) {
  284.             return floor($min 1000) . 'k' $symbol '+';
  285.         }
  286.         if ($max) {
  287.             return '< ' floor($max 1000) . 'k' $symbol;
  288.         }
  289.         return '';
  290.     }
  291.     // ======================================================================
  292.     // MÉTHODES EXISTANTES (inchangées)
  293.     // ======================================================================
  294.     public function homepageLanding(Request $request$slug): Response
  295.     {
  296.         $locale $request->getLocale();
  297.         $user $this->getUser();
  298.         $landing $this->em->getRepository(LandingJobs::class)->findOneBy(['type' => 'candidates''slug' => $slug'locale' => $locale]);
  299.         if($landing === null) {
  300.             throw new \RuntimeException('No exist');
  301.         }
  302.         $articles $this->em->getRepository(Articles::class)->getArticles(6,$locale);
  303.         // Nombre total d'offres en ligne sur tout le site
  304.         $totalJobs $this->em->getRepository(Jobs::class)->countAllOnlineJobs();
  305.         // Données pour utilisateur connecté
  306.         $likedJobIds = [];
  307.         if ($user !== null) {
  308.             if (method_exists($user'getCandidate') && $user->getCandidate() !== null) {
  309.                 $likes $this->em->getRepository(JobsHasLikes::class)->findBy(['candidate' => $user->getCandidate()]);
  310.                 foreach ($likes as $like) {
  311.                     if ($like->getJob() !== null) {
  312.                         $likedJobIds[] = $like->getJob()->getId();
  313.                     }
  314.                 }
  315.             }
  316.         }
  317.         // Charger les keywords populaires associés à cette landing page
  318.         $keywordsEntities $this->em->getRepository(KeywordsLandingJobs::class)->findByLanding($landing);
  319.         $popularKeywords = [];
  320.         foreach ($keywordsEntities as $kw) {
  321.             $popularKeywords[] = [
  322.                 'label' => $kw->getLabel() ?? $kw->getSearchKeyword(),
  323.                 'icon' => $kw->getIcon() ?? '',
  324.                 'keyword' => $kw->getSearchKeyword() ?? $kw->getLabel(),
  325.                 'searchKeyword' => $kw->getSearchKeyword() ?? $kw->getLabel(),
  326.             ];
  327.         }
  328.         // Offres populaires par keyword (pour l'animation visiteur, comme la homepage)
  329.         $jobsRepo $this->em->getRepository(Jobs::class);
  330.         $popularJobsForVisitor = [];
  331.         foreach ($popularKeywords as $pk) {
  332.             $jobs $jobsRepo->findByKeyword($pk['keyword'], $locale4);
  333.             $serializedJobs = [];
  334.             foreach ($jobs as $job) {
  335.                 $now = new \DateTime();
  336.                 $diffDays $job->getCreatedAt() ? $now->diff($job->getCreatedAt())->days 999;
  337.                 $daysUntilExpiry 30 $diffDays;
  338.                 $serializedJobs[] = [
  339.                     'jobTitle' => $job->getJobTitle() ?? 'Poste',
  340.                     'city' => $job->getCity() ?? 'Non précisé',
  341.                     'employmentType' => $job->getEmploymentType() ?? 'CDI',
  342.                     'createdAt' => $job->getCreatedAt() ? $job->getCreatedAt()->format('d/m/Y') : '',
  343.                     'diffDays' => $diffDays,
  344.                     'daysUntilExpiry' => $daysUntilExpiry,
  345.                     'slug' => $job->getSlug(),
  346.                 ];
  347.             }
  348.             $popularJobsForVisitor[] = [
  349.                 'text' => $pk['label'],
  350.                 'icon' => $pk['icon'],
  351.                 'searchKeyword' => $pk['keyword'],
  352.                 'jobs' => $serializedJobs,
  353.             ];
  354.         }
  355.         return $this->render('application/whileresume/website/candidates/landing.html.twig',[
  356.             'landing' => $landing,
  357.             'articles' => $articles,
  358.             'totalJobs' => $totalJobs,
  359.             'popularKeywords' => $popularKeywords,
  360.             'likedJobIds' => $likedJobIds,
  361.             'popularJobsForVisitor' => $popularJobsForVisitor,
  362.         ]);
  363.     }
  364.     /**
  365.      * Déposer un CV
  366.      * @param Request $request
  367.      * @return Response
  368.      */
  369.     public function resume(Request $request): Response
  370.     {
  371.         $session $request->getSession();
  372.         $locale $request->getLocale();
  373.         $user $this->getUser();
  374.         $errorMessage "";
  375.         $page $this->em->getRepository(Pages::class)->findOneBy(['name' => 'resume''locale' => $locale]);
  376.         $form $this->createForm(UsersEmailForm::class);
  377.         $form->handleRequest($request);
  378.         if ($form->isSubmitted() && $form->isValid()) {
  379.             $data $request->request->all();
  380.             $data $data['users_email_form'];
  381.             if($data['acceptTerm'] == "1") {
  382.                 if($data['password']['first'] != $data['password']['second']) {
  383.                     if($locale == "fr") {
  384.                         $session->getFlashBag()->add('danger''Votre second mot de passe n\'est pas identique');
  385.                         return $this->redirectToRoute('whileresume_resume_fr');
  386.                     }
  387.                     $session->getFlashBag()->add('danger''Your second password is not identical');
  388.                     return $this->redirectToRoute('whileresume_resume_en');
  389.                 }
  390.                 $verificationUser $this->em->getRepository(Users::class)->findOneBy(['email' => $data['email']]);
  391.                 if ($verificationUser == null) {
  392.                     $candidate = new Candidates();
  393.                     $candidate->setEmail($data['email']);
  394.                     $candidate->setUpdatedAt(new \DateTime("now"));
  395.                     $candidate->setCreatedAt(new \DateTime("now"));
  396.                     $candidate->setOnline(false);
  397.                     $candidate->setAvailability("offline");
  398.                     $this->em->persist($candidate);
  399.                     $this->em->flush();
  400.                     $us1 $this->generateUniqueSlug($candidate->getId(),10);
  401.                     $us2 $this->generateUniqueSlug($candidate->getId(),10);
  402.                     $candidate->setSlug($us1);
  403.                     $candidate->setSlugAnonyme($us2);
  404.                     $this->em->persist($candidate);
  405.                     $this->em->flush();
  406.                     $newUser = new Users();
  407.                     $newUser->setEmail($data['email']);
  408.                     $newUser->setUsername("");
  409.                     $newUser->setVerification(false);
  410.                     $newUser->setNotificationsMessages(true);
  411.                     $newUser->setNotificationsSuivis(true);
  412.                     $newUser->setPremium(false);
  413.                     $newUser->setFirst(true);
  414.                     $newUser->setEnabled(true);
  415.                     $newUser->setPassword($this->passwordEncoder->encodePassword($newUser,$data['password']['first']));
  416.                     $newUser->setRoles(['ROLE_USER']);
  417.                     $newUser->setTypeAccount("candidate");
  418.                     $newUser->setUpdatedAt(new \DateTime("now"));
  419.                     $newUser->setCreatedAt(new \DateTime("now"));
  420.                     $newUser->setCandidate($candidate);
  421.                     $this->em->persist($newUser);
  422.                     $this->em->flush();
  423.                     $title "Find your next tech talents in just a few minutes!";
  424.                     if($locale == "fr") {
  425.                         $title "🎉 Bienvenue ! Votre profil peut déjà attirer des recruteurs !";
  426.                     }
  427.                     $descriptionHTML $this->twig->render("application/whileresume/gestion/emails/"$locale ."/register_candidate.html.twig",[
  428.                         'title' => $title,
  429.                         'email' => $data['email']
  430.                     ]);
  431.                     $this->ms->webhook($title,$descriptionHTMLnull$data['email'], nullnull);
  432.                     $this->userAuthenticator->authenticateUser($newUser$this->authenticator$request);
  433.                     if($locale == "en") {
  434.                         return $this->redirectToRoute('customer_homepage');
  435.                     }
  436.                     return $this->redirectToRoute('locale_customer_homepage',['_locale' => $locale]);
  437.                 }
  438.                 if($locale == "fr") {
  439.                     $session->getFlashBag()->add('danger'$errorMessage);
  440.                     return $this->redirectToRoute('whileresume_resume_fr');
  441.                 }
  442.                 $errorMessage "You are already registered with this email";
  443.                 $session->getFlashBag()->add('danger'$errorMessage);
  444.                 return $this->redirectToRoute('whileresume_resume_en');
  445.             }
  446.         }
  447.         return $this->render('application/whileresume/website/candidates/resume.html.twig',[
  448.             'form' => $form->createView(),
  449.             'page' => $page
  450.         ]);
  451.     }
  452.     private function generateUniqueSlug(int $userIdint $length 8): string
  453.     {
  454.         $characters '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  455.         $slug '';
  456.         for ($i 0$i $length$i++) {
  457.             $slug .= $characters[random_int(0strlen($characters) - 1)];
  458.         }
  459.         return $userId '-' $slug;
  460.     }
  461. }