<?phpnamespace App\Controller\ThemesWebsite\Whileresume\Website;use App\Entity\Articles\Articles;use App\Entity\Core\Users;use App\Entity\Cvs\Candidates;use App\Entity\Cvs\Jobs;use App\Entity\Cvs\JobsHasLikes;use App\Entity\Cvs\LandingJobs;use App\Entity\Pages\Pages;use App\Form\Core\UsersEmailForm;use App\Security\LoginFormAuthenticator;use App\Services\Core\RequestData;use Doctrine\ORM\EntityManagerInterface;use Knp\Component\Pager\PaginatorInterface;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\EventDispatcher\EventDispatcherInterface;use Symfony\Component\HttpFoundation\Cookie;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\HttpFoundation\RedirectResponse;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Annotation\Route;use Symfony\Component\Routing\Generator\UrlGeneratorInterface;use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;use Symfony\Component\Security\Core\User\UserInterface;use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordCredentialsBadge;use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;use Twig\Environment;class CandidatesController extends AbstractController{ private $rd; private $em; private $passwordEncoder; private $ms; private $us; private $authenticator; private $userAuthenticator; private $paginator; private $twig; public function __construct(RequestData $rd, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, \App\Services\Mails $ms, \App\Services\Core\Users $us, UserAuthenticatorInterface $userAuthenticator, LoginFormAuthenticator $authenticator, PaginatorInterface $paginator, Environment $twig, ) { $this->rd = $rd; $this->em = $em; $this->passwordEncoder = $passwordEncoder; $this->ms = $ms; $this->authenticator = $authenticator; $this->userAuthenticator = $userAuthenticator; $this->us = $us; $this->paginator = $paginator; $this->twig = $twig; } /** * Homepage principal * @param Request $request * @return Response */ public function homepage(Request $request): Response { $locale = $request->getLocale(); $user = $this->getUser(); $page = $this->em->getRepository(Pages::class)->findOneBy(['name' => 'homepage', 'locale' => $locale]); $articles = $this->em->getRepository(Articles::class)->getArticles(6, $locale); // Données pour utilisateur connecté $likedJobIds = []; $totalJobs = 0; if ($user !== null) { $totalJobs = $this->em->getRepository(Jobs::class)->countSearchJobs('', $locale); // Récupérer les IDs des offres likées par le candidat if (method_exists($user, 'getCandidate') && $user->getCandidate() !== null) { $likes = $this->em->getRepository(JobsHasLikes::class)->findBy(['candidate' => $user->getCandidate()]); foreach ($likes as $like) { if ($like->getJob() !== null) { $likedJobIds[] = $like->getJob()->getId(); } } } } return $this->render('application/whileresume/website/candidates/homepage_' . $locale . '.html.twig', [ 'page' => $page, 'articles' => $articles, 'likedJobIds' => $likedJobIds, 'totalJobs' => $totalJobs, ]); } /** * API : Recherche d'offres (AJAX) */ public function searchJobsApi(Request $request): JsonResponse { $locale = $request->getLocale(); $query = trim($request->query->get('q', '')); $offset = max(0, (int) $request->query->get('offset', 0)); $limit = min(20, max(1, (int) $request->query->get('limit', 10))); $jobsRepo = $this->em->getRepository(Jobs::class); $jobs = $jobsRepo->searchJobs($query, $locale, $offset, $limit); $total = $jobsRepo->countSearchJobs($query, $locale); // Récupérer les likes du candidat connecté $likedJobIds = []; $user = $this->getUser(); if ($user !== null && method_exists($user, 'getCandidate') && $user->getCandidate() !== null) { $likes = $this->em->getRepository(JobsHasLikes::class)->findBy(['candidate' => $user->getCandidate()]); foreach ($likes as $like) { if ($like->getJob() !== null) { $likedJobIds[] = $like->getJob()->getId(); } } } $results = []; foreach ($jobs as $job) { $results[] = $this->serializeJob($job, $likedJobIds); } return new JsonResponse([ 'jobs' => $results, 'total' => $total, 'offset' => $offset, 'limit' => $limit, 'hasMore' => ($offset + $limit) < $total, ]); } /** * API : Like / Unlike une offre (AJAX) */ public function toggleJobLikeApi(Request $request): JsonResponse { $user = $this->getUser(); if ($user === null) { return new JsonResponse(['error' => 'Vous devez être connecté.'], 401); } $candidate = $user->getCandidate(); if ($candidate === null) { return new JsonResponse(['error' => 'Vous devez être candidat pour liker une offre.'], 403); } $data = json_decode($request->getContent(), true); $jobId = $data['jobId'] ?? null; if ($jobId === null) { return new JsonResponse(['error' => 'ID de l\'offre manquant.'], 400); } $job = $this->em->getRepository(Jobs::class)->find($jobId); if ($job === null) { return new JsonResponse(['error' => 'Offre introuvable.'], 404); } // Vérifier si le like existe déjà $existingLike = $this->em->getRepository(JobsHasLikes::class)->findOneBy([ 'job' => $job, 'candidate' => $candidate, ]); if ($existingLike !== null) { // Unlike $this->em->remove($existingLike); $this->em->flush(); return new JsonResponse(['liked' => false, 'jobId' => $jobId]); } // Like $like = new JobsHasLikes(); $like->setJob($job); $like->setCandidate($candidate); $this->em->persist($like); $this->em->flush(); return new JsonResponse(['liked' => true, 'jobId' => $jobId]); } // ====================================================================== // HELPERS // ====================================================================== /** * Sérialise un Job en tableau pour la réponse JSON */ private function serializeJob(Jobs $job, array $likedJobIds): array { $now = new \DateTime(); $diffDays = $job->getCreatedAt() ? $now->diff($job->getCreatedAt())->days : 999; // Image du job (Vich uploader) — adapter le chemin si nécessaire $imageUrl = null; if ($job->getImage() && $job->getImage()->getName()) { $imageUrl = '/uploads/cv_files/' . $job->getImage()->getName(); } return [ 'id' => $job->getId(), 'jobTitle' => $job->getJobTitle(), 'companyName' => $job->getCompanyName() ?? 'Entreprise', 'city' => $job->getCity() ?? '', 'employmentType' => $job->getEmploymentType() ?? 'CDI', 'experienceLevel' => $job->getExperienceLevel() ?? '', 'remoteWork' => $job->getRemoteWork() ?? '', 'salary' => $this->formatSalary($job->getSalaryMin(), $job->getSalaryMax(), $job->getDevise()), 'slug' => $job->getSlug(), 'isNew' => $diffDays <= 7, 'isLiked' => in_array($job->getId(), $likedJobIds), 'image' => $imageUrl, 'logo' => $job->getCompanyName() ? mb_strtoupper(mb_substr($job->getCompanyName(), 0, 1)) : '?', 'category' => $job->getCategory() ?? '', ]; } /** * Formate le salaire en string lisible : "55-70k€" */ private function formatSalary(?int $min, ?int $max, ?string $devise): string { $symbol = ($devise === 'USD' || $devise === '$') ? '$' : '€'; if ($min && $max) { return floor($min / 1000) . '-' . floor($max / 1000) . 'k' . $symbol; } if ($min) { return floor($min / 1000) . 'k' . $symbol . '+'; } if ($max) { return '< ' . floor($max / 1000) . 'k' . $symbol; } return ''; } // ====================================================================== // MÉTHODES EXISTANTES (inchangées) // ====================================================================== public function homepageLanding(Request $request, $slug): Response { $locale = $request->getLocale(); $landing = $this->em->getRepository(LandingJobs::class)->findOneBy(['type' => 'candidates', 'locale' => $locale]); if($landing === null) { throw new \RuntimeException('No exist'); } $articles = $this->em->getRepository(Articles::class)->getArticles(6,$locale); return $this->render('application/whileresume/website/candidates/landing_'.$locale.'.html.twig',[ 'landing' => $landing, 'articles' => $articles ]); } /** * Déposer un CV * @param Request $request * @return Response */ public function resume(Request $request): Response { $session = $request->getSession(); $locale = $request->getLocale(); $user = $this->getUser(); $page = $this->em->getRepository(Pages::class)->findOneBy(['name' => 'resume', 'locale' => $locale]); $form = $this->createForm(UsersEmailForm::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $data = $request->request->all(); $data = $data['users_email_form']; if($data['acceptTerm'] == "1") { if($data['password']['first'] != $data['password']['second']) { if($locale == "fr") { $session->getFlashBag()->add('danger', 'Votre second mot de passe n\'est pas identique'); return $this->redirectToRoute('whileresume_resume_fr'); } $session->getFlashBag()->add('danger', 'Your second password is not identical'); return $this->redirectToRoute('whileresume_resume_en'); } $verificationUser = $this->em->getRepository(Users::class)->findOneBy(['email' => $data['email']]); if ($verificationUser == null) { $candidate = new Candidates(); $candidate->setEmail($data['email']); $candidate->setUpdatedAt(new \DateTime("now")); $candidate->setCreatedAt(new \DateTime("now")); $candidate->setOnline(false); $candidate->setAvailability("offline"); $this->em->persist($candidate); $this->em->flush(); $us1 = $this->generateUniqueSlug($candidate->getId(),10); $us2 = $this->generateUniqueSlug($candidate->getId(),10); $candidate->setSlug($us1); $candidate->setSlugAnonyme($us2); $this->em->persist($candidate); $this->em->flush(); $newUser = new Users(); $newUser->setEmail($data['email']); $newUser->setUsername(""); $newUser->setVerification(false); $newUser->setNotificationsMessages(true); $newUser->setNotificationsSuivis(true); $newUser->setPremium(false); $newUser->setFirst(true); $newUser->setEnabled(true); $newUser->setPassword($this->passwordEncoder->encodePassword($newUser,$data['password']['first'])); $newUser->setRoles(['ROLE_USER']); $newUser->setTypeAccount("candidate"); $newUser->setUpdatedAt(new \DateTime("now")); $newUser->setCreatedAt(new \DateTime("now")); $newUser->setCandidate($candidate); $this->em->persist($newUser); $this->em->flush(); $title = "Find your next tech talents in just a few minutes!"; if($locale == "fr") { $title = "🎉 Bienvenue ! Votre profil peut déjà attirer des recruteurs !"; } $descriptionHTML = $this->twig->render("application/whileresume/gestion/emails/". $locale ."/register_candidate.html.twig",[ 'title' => $title, 'email' => $data['email'] ]); $this->ms->webhook($title,$descriptionHTML, null, $data['email'], null, null); $this->userAuthenticator->authenticateUser($newUser, $this->authenticator, $request); if($locale == "en") { return $this->redirectToRoute('customer_homepage'); } return $this->redirectToRoute('locale_customer_homepage',['_locale' => $locale]); } if($locale == "fr") { return $this->redirectToRoute('whileresume_resume_fr'); } return $this->redirectToRoute('whileresume_resume_en'); } } return $this->render('application/whileresume/website/candidates/resume_'.$locale.'.html.twig',[ 'form' => $form->createView(), 'page' => $page ]); } private function generateUniqueSlug(int $userId, int $length = 8): string { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $slug = ''; for ($i = 0; $i < $length; $i++) { $slug .= $characters[random_int(0, strlen($characters) - 1)]; } return $userId . '-' . $slug; }}