diff --git a/.idea/hrconnect-symfony.iml b/.idea/hrconnect-symfony.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/hrconnect-symfony.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
Nous vous contacterons prochainement pour vous informer de la suite du processus.');
+ return $this->redirectToRoute('back.candidat.offres_emploi.index');
+ }
+
+ return $this->render('candidat/candidature/new.html.twig', [
+ 'form' => $form->createView(),
+ 'offre' => $offre,
+ ]);
+ }
+
+ /**
+ * Méthode utilitaire pour uploader un fichier
+ */
+ private function uploadFile($file, $directoryParam, SluggerInterface $slugger): ?string
+ {
+ try {
+ $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
+ $safeFilename = $slugger->slug($originalFilename);
+ $newFilename = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
+
+ $directory = $this->getParameter($directoryParam);
+ if (!file_exists($directory)) {
+ mkdir($directory, 0777, true);
+ }
+
+ $file->move($directory, $newFilename);
+ $this->logger->info('Fichier uploadé : ' . $newFilename);
+ return $newFilename;
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'upload du fichier: ' . $e->getMessage());
+ return null;
+ }
+ }
+}
diff --git a/src/Controller/CandidatOffreEmploiController.php b/src/Controller/CandidatOffreEmploiController.php
new file mode 100644
index 0000000..9335012
--- /dev/null
+++ b/src/Controller/CandidatOffreEmploiController.php
@@ -0,0 +1,45 @@
+query->get('type');
+
+ $criteria = ['isActive' => true];
+ if ($type) {
+ $criteria['typeContrat'] = $type;
+ }
+
+ $offres = $repository->findBy($criteria, ['datePublication' => 'DESC']);
+
+ return $this->render('back_office/candidat/offres_emploi/index.html.twig', [
+ 'offres' => $offres,
+ 'type' => $type,
+ ]);
+ }
+
+ #[Route('/{id}', name: 'back.candidat.offres_emploi.show')]
+ public function show(OffreEmploi $offre): Response
+ {
+ // Vérifier si l'offre est active
+ if (!$offre->isIsActive()) {
+ throw $this->createNotFoundException('Cette offre d\'emploi n\'est pas disponible.');
+ }
+
+ return $this->render('back_office/candidat/offres_emploi/show.html.twig', [
+ 'offre' => $offre,
+ ]);
+ }
+}
diff --git a/src/Controller/CandidatureController.php b/src/Controller/CandidatureController.php
new file mode 100644
index 0000000..b2b56ef
--- /dev/null
+++ b/src/Controller/CandidatureController.php
@@ -0,0 +1,340 @@
+query->get('offre');
+ $candidatures = $offreId
+ ? $repository->findBy(['offre' => $offreId], ['dateCandidature' => 'DESC'])
+ : $repository->findBy([], ['dateCandidature' => 'DESC']);
+
+ return $this->render('back_office/candidatures/index.html.twig', [
+ 'candidatures' => $candidatures,
+ 'offres' => $offreRepository->findAll(),
+ 'offreId' => $offreId,
+ ]);
+ }
+
+ #[Route('/{id}/edit', name: 'back.candidatures.edit', methods: ['GET', 'POST'])]
+ public function edit(Candidature $candidature, Request $request): Response
+ {
+ try {
+ $form = $this->createForm(CandidatureType::class, $candidature);
+
+ if ($request->isMethod('POST')) {
+ $this->logger->info('Requête POST reçue pour modifier une candidature');
+
+ $data = $request->request->all('candidature');
+
+ if (isset($data)) {
+ $candidature->setNom($data['nom'] ?? '');
+ $candidature->setPrenom($data['prenom'] ?? '');
+ $candidature->setEmail($data['email'] ?? '');
+ $candidature->setTelephone($data['telephone'] ?? '');
+ $candidature->setMessage($data['message'] ?? '');
+ $candidature->setStatut($data['statut'] ?? 'en_attente');
+
+ try {
+ $this->em->flush();
+
+ $this->logger->info('Candidature modifiée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom());
+ $this->addFlash('success', 'La candidature a été modifiée avec succès');
+ return $this->redirectToRoute('back.candidatures.index');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la modification : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la modification de la candidature');
+ }
+ }
+ }
+
+ return $this->render('back_office/candidatures/edit.html.twig', [
+ 'form' => $form->createView(),
+ 'candidature' => $candidature,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la modification de la candidature : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la modification de la candidature.');
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+ }
+
+ #[Route('/{id}/delete', name: 'back.candidatures.delete', methods: ['GET', 'POST'])]
+ public function delete(Candidature $candidature): Response
+ {
+ try {
+ $this->em->remove($candidature);
+ $this->em->flush();
+
+ $this->logger->info('Candidature supprimée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom());
+ $this->addFlash('success', 'La candidature a été supprimée avec succès');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la suppression de la candidature : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la suppression de la candidature.');
+ }
+
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ #[Route('/{id}/accept', name: 'back.candidatures.accept', methods: ['GET'])]
+ public function accept(Candidature $candidature, CandidatureRepository $candidatureRepository): Response
+ {
+ try {
+ // 1. Récupérer l'offre associée à la candidature
+ $offre = $candidature->getOffre();
+
+ // 2. Mettre à jour le statut de la candidature à "acceptee"
+ $candidature->setStatut('acceptee');
+
+ // 3. Désactiver l'offre (la clôturer)
+ $offre->setIsActive(false);
+
+ // 4. Mettre à jour toutes les autres candidatures pour cette offre à "refusee"
+ $autreCandidatures = $candidatureRepository->findBy(['offre' => $offre]);
+ foreach ($autreCandidatures as $autreCandidature) {
+ if ($autreCandidature->getId() !== $candidature->getId()) {
+ $autreCandidature->setStatut('refusee');
+ }
+ }
+
+ // 5. Enregistrer les modifications
+ $this->em->flush();
+
+ // 6. Envoyer un email d'acceptation au candidat sélectionné
+ $this->logger->info('Tentative d\'envoi d\'email d\'acceptation à : ' . $candidature->getEmail());
+ $emailSent = $this->emailService->sendEmail($candidature, 'accepted');
+ if ($emailSent) {
+ $this->logger->info('Email d\'acceptation envoyé avec succès à : ' . $candidature->getEmail());
+ } else {
+ $this->logger->warning('Impossible d\'envoyer l\'email d\'acceptation à : ' . $candidature->getEmail());
+ }
+
+ // 7. Envoyer des emails de refus aux autres candidats
+ foreach ($autreCandidatures as $autreCandidature) {
+ if ($autreCandidature->getId() !== $candidature->getId()) {
+ $this->logger->info('Tentative d\'envoi d\'email de refus à : ' . $autreCandidature->getEmail());
+ $emailSent = $this->emailService->sendEmail($autreCandidature, 'rejected');
+ if ($emailSent) {
+ $this->logger->info('Email de refus envoyé avec succès à : ' . $autreCandidature->getEmail());
+ } else {
+ $this->logger->warning('Impossible d\'envoyer l\'email de refus à : ' . $autreCandidature->getEmail());
+ }
+ }
+ }
+
+ $this->logger->info('Candidature acceptée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom() . ' pour l\'offre : ' . $offre->getTitre());
+ $this->addFlash('success', 'Candidature acceptée ! La candidature de ' . $candidature->getNom() . ' ' . $candidature->getPrenom() . ' a été acceptée avec succès.
L\'offre "' . $offre->getTitre() . '" a été clôturée et les autres candidatures ont été refusées.
Des emails de notification ont été envoyés aux candidats.');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'acceptation de la candidature : ' . $e->getMessage());
+ $this->addFlash('danger', 'Erreur ! Une erreur est survenue lors de l\'acceptation de la candidature.
Détail : ' . $e->getMessage());
+ }
+
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ #[Route('/{id}/view-cv', name: 'back.candidatures.view_cv', methods: ['GET'])]
+ public function viewCv(Candidature $candidature): Response
+ {
+ // Vérifier si le candidat a fourni un CV
+ if (!$candidature->getCv()) {
+ $this->addFlash('warning', 'Ce candidat n\'a pas fourni de CV.');
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ // Vérifier si le fichier existe
+ $cvPath = $this->getParameter('cv_directory') . '/' . $candidature->getCv();
+ if (!file_exists($cvPath)) {
+ $this->addFlash('danger', 'Le fichier CV n\'existe pas sur le serveur.');
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ return $this->render('back_office/candidatures/view_cv.html.twig', [
+ 'candidature' => $candidature,
+ 'analysis' => null // Pas d'analyse par défaut
+ ]);
+ }
+
+ #[Route('/{id}/analyze-cv', name: 'back.candidatures.analyze_cv', methods: ['GET'])]
+ public function analyzeCv(Candidature $candidature): Response
+ {
+ // Vérifier si le candidat a fourni un CV
+ if (!$candidature->getCv()) {
+ $this->addFlash('warning', 'Ce candidat n\'a pas fourni de CV.');
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ // Vérifier si le fichier existe
+ $cvPath = $this->getParameter('cv_directory') . '/' . $candidature->getCv();
+ if (!file_exists($cvPath)) {
+ $this->addFlash('danger', 'Le fichier CV n\'existe pas sur le serveur.');
+ return $this->redirectToRoute('back.candidatures.index');
+ }
+
+ try {
+ // Analyser le CV
+ $this->logger->info('Début de l\'analyse du CV pour la candidature : ' . $candidature->getId());
+ $result = $this->cvAnalyzerService->analyzeCv($cvPath);
+
+ if ($result['success']) {
+ $this->logger->info('Analyse du CV réussie pour la candidature : ' . $candidature->getId());
+ $analysis = $result['data'];
+ $this->addFlash('success', 'Le CV a été analysé avec succès.');
+ } else {
+ $this->logger->error('Erreur lors de l\'analyse du CV : ' . $result['message']);
+ $analysis = null;
+ $this->addFlash('danger', 'Une erreur est survenue lors de l\'analyse du CV : ' . $result['message']);
+ }
+
+ return $this->render('back_office/candidatures/view_cv.html.twig', [
+ 'candidature' => $candidature,
+ 'analysis' => $analysis
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Exception lors de l\'analyse du CV : ' . $e->getMessage());
+ $this->addFlash('danger', 'Une erreur est survenue lors de l\'analyse du CV : ' . $e->getMessage());
+ return $this->redirectToRoute('back.candidatures.view_cv', ['id' => $candidature->getId()]);
+ }
+ }
+
+
+
+ #[Route('/{id}/postuler', name: 'app_candidature_new', methods: ['GET', 'POST'])]
+ public function new(
+ Request $request,
+ OffreEmploi $offre,
+ EntityManagerInterface $entityManager,
+ SluggerInterface $slugger
+ ): Response {
+ $candidature = new Candidature();
+ // Utilisation du formulaire sans le champ statut
+ $form = $this->createForm(CandidaturePublicType::class, $candidature);
+ $form->handleRequest($request);
+
+ $this->logger->info('Formulaire soumis: ' . ($form->isSubmitted() ? 'Oui' : 'Non'));
+ if ($form->isSubmitted()) {
+ $this->logger->info('Formulaire valide: ' . ($form->isValid() ? 'Oui' : 'Non'));
+ if (!$form->isValid()) {
+ $errors = [];
+ foreach ($form->getErrors(true) as $error) {
+ $errors[] = $error->getMessage();
+ }
+ $this->logger->error('Erreurs de validation: ' . implode(', ', $errors));
+ }
+ }
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ try {
+ // Traitement du CV
+ $cvFile = $form->get('cv')->getData();
+ $this->logger->info('CV file: ' . ($cvFile ? 'Présent' : 'Absent'));
+ if ($cvFile) {
+ try {
+ $originalFilename = pathinfo($cvFile->getClientOriginalName(), PATHINFO_FILENAME);
+ $safeFilename = $slugger->slug($originalFilename);
+ $newFilename = $safeFilename.'-'.uniqid().'.'.$cvFile->guessExtension();
+
+ // Vérifier si le répertoire existe, sinon le créer
+ $cvDirectory = $this->getParameter('cv_directory');
+ $this->logger->info('CV directory: ' . $cvDirectory);
+ if (!file_exists($cvDirectory)) {
+ $this->logger->info('Création du répertoire CV: ' . $cvDirectory);
+ mkdir($cvDirectory, 0777, true);
+ }
+
+ $cvFile->move($cvDirectory, $newFilename);
+ $candidature->setCv($newFilename);
+ $this->logger->info('CV uploadé : ' . $newFilename);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'upload du CV: ' . $e->getMessage());
+ }
+ }
+
+ // Traitement de la lettre de motivation
+ $lettreMotivationFile = $form->get('lettreMotivation')->getData();
+ $this->logger->info('Lettre de motivation file: ' . ($lettreMotivationFile ? 'Présente' : 'Absente'));
+ if ($lettreMotivationFile) {
+ try {
+ $originalFilename = pathinfo($lettreMotivationFile->getClientOriginalName(), PATHINFO_FILENAME);
+ $safeFilename = $slugger->slug($originalFilename);
+ $newFilename = $safeFilename.'-'.uniqid().'.'.$lettreMotivationFile->guessExtension();
+
+ // Vérifier si le répertoire existe, sinon le créer
+ $lettreMotivationDirectory = $this->getParameter('lettre_motivation_directory');
+ $this->logger->info('Lettre de motivation directory: ' . $lettreMotivationDirectory);
+ if (!file_exists($lettreMotivationDirectory)) {
+ $this->logger->info('Création du répertoire lettre de motivation: ' . $lettreMotivationDirectory);
+ mkdir($lettreMotivationDirectory, 0777, true);
+ }
+
+ $lettreMotivationFile->move($lettreMotivationDirectory, $newFilename);
+ $candidature->setLettreMotivation($newFilename);
+ $this->logger->info('Lettre de motivation uploadée : ' . $newFilename);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'upload de la lettre de motivation: ' . $e->getMessage());
+ }
+ }
+
+ $candidature->setDateCandidature(new \DateTime());
+ $candidature->setOffre($offre);
+ // Le statut est déjà défini à 'en_attente' dans le constructeur de l'entité
+
+ $entityManager->persist($candidature);
+ $entityManager->flush();
+
+ $this->logger->info('Candidature créée avec succès pour l\'offre : ' . $offre->getTitre());
+ $this->addFlash('success', 'Votre candidature a été envoyée avec succès !');
+
+ // Redirection vers la page des offres d'emploi pour les candidats
+ $this->logger->info('Redirection vers la page des offres d\'emploi pour les candidats');
+ return $this->redirectToRoute('back.candidat.offres_emploi.index', [], 301);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la création de la candidature : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de l\'envoi de votre candidature. Veuillez réessayer.');
+ }
+ }
+
+ return $this->render('candidature/new.html.twig', [
+ 'candidature' => $candidature,
+ 'form' => $form->createView(),
+ 'offre' => $offre,
+ ]);
+ }
+
+ #[Route('/{id}', name: 'app_candidature_show', methods: ['GET'])]
+ public function show(Candidature $candidature): Response
+ {
+ return $this->render('candidature/show.html.twig', [
+ 'candidature' => $candidature,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/OffreEmploiController.php b/src/Controller/OffreEmploiController.php
new file mode 100644
index 0000000..02e4454
--- /dev/null
+++ b/src/Controller/OffreEmploiController.php
@@ -0,0 +1,157 @@
+findBy([], ['datePublication' => 'DESC']);
+ $this->logger->info('Nombre d\'offres récupérées : ' . count($offres));
+
+ return $this->render('back_office/offres_emploi/index.html.twig', [
+ 'offres' => $offres,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la récupération des offres : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la récupération des offres.');
+ return $this->render('back_office/offres_emploi/index.html.twig', [
+ 'offres' => [],
+ ]);
+ }
+ }
+
+ #[Route('/add', name: 'back.offres_emploi.add', methods: ['GET', 'POST'])]
+ public function add(Request $request): Response
+ {
+ $offre = new OffreEmploi();
+ $form = $this->createForm(OffreEmploiType::class, $offre);
+
+ if ($request->isMethod('POST')) {
+ $this->logger->info('Requête POST reçue pour ajouter une offre');
+
+ $data = $request->request->all('offre_emploi');
+
+ if (isset($data)) {
+ $offre->setTitre($data['titre'] ?? '');
+ $offre->setTypeContrat($data['typeContrat'] ?? '');
+ $offre->setLocalisation($data['localisation'] ?? '');
+ $offre->setSalaire($data['salaire'] ?? '');
+ $offre->setDescription($data['description'] ?? '');
+ $offre->setProfilRecherche($data['profilRecherche'] ?? '');
+ $offre->setAvantages($data['avantages'] ?? null);
+ $offre->setIsActive(isset($data['isActive']) && $data['isActive'] === '1');
+ $offre->setDatePublication(new \DateTime());
+
+ try {
+ $this->em->persist($offre);
+ $this->em->flush();
+
+ $this->addFlash('success', 'L\'offre d\'emploi a été créée avec succès');
+ return $this->redirectToRoute('back.offres_emploi.index');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la création : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la création de l\'offre');
+ }
+ }
+ }
+
+ return $this->render('back_office/offres_emploi/add.html.twig', [
+ 'form' => $form->createView(),
+ ]);
+ }
+
+ #[Route('/{id}/edit', name: 'back.offres_emploi.edit', methods: ['GET', 'POST'])]
+ public function edit(OffreEmploi $offre, Request $request): Response
+ {
+ try {
+ $form = $this->createForm(OffreEmploiType::class, $offre);
+
+ if ($request->isMethod('POST')) {
+ $this->logger->info('Requête POST reçue pour modifier une offre');
+
+ $data = $request->request->all('offre_emploi');
+
+ if (isset($data)) {
+ $offre->setTitre($data['titre'] ?? '');
+ $offre->setTypeContrat($data['typeContrat'] ?? '');
+ $offre->setLocalisation($data['localisation'] ?? '');
+ $offre->setSalaire($data['salaire'] ?? '');
+ $offre->setDescription($data['description'] ?? '');
+ $offre->setProfilRecherche($data['profilRecherche'] ?? '');
+ $offre->setAvantages($data['avantages'] ?? null);
+ $offre->setIsActive(isset($data['isActive']) && $data['isActive'] === '1');
+
+ try {
+ $this->em->flush();
+
+ $this->logger->info('Offre modifiée : ' . $offre->getTitre());
+ $this->addFlash('success', 'L\'offre d\'emploi a été modifiée avec succès');
+ return $this->redirectToRoute('back.offres_emploi.index');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la modification : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la modification de l\'offre');
+ }
+ }
+ }
+
+ return $this->render('back_office/offres_emploi/edit.html.twig', [
+ 'form' => $form->createView(),
+ 'offre' => $offre,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la modification de l\'offre : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la modification de l\'offre.');
+ return $this->redirectToRoute('back.offres_emploi.index');
+ }
+ }
+
+ #[Route('/{id}/delete', name: 'back.offres_emploi.delete', methods: ['GET', 'POST'])]
+ public function delete(OffreEmploi $offre): Response
+ {
+ try {
+ $this->em->remove($offre);
+ $this->em->flush();
+
+ $this->logger->info('Offre supprimée : ' . $offre->getTitre());
+ $this->addFlash('success', 'L\'offre d\'emploi a été supprimée avec succès');
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la suppression de l\'offre : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de la suppression de l\'offre.');
+ }
+
+ return $this->redirectToRoute('back.offres_emploi.index');
+ }
+
+ #[Route('/{id}/show', name: 'back.offres_emploi.show', methods: ['GET'])]
+ public function show(OffreEmploi $offre): Response
+ {
+ try {
+ return $this->render('back_office/offres_emploi/show.html.twig', [
+ 'offre' => $offre,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'affichage de l\'offre : ' . $e->getMessage());
+ $this->addFlash('error', 'Une erreur est survenue lors de l\'affichage de l\'offre');
+ return $this->redirectToRoute('back.offres_emploi.index');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/PublicOffreEmploiController.php b/src/Controller/PublicOffreEmploiController.php
new file mode 100644
index 0000000..e4ac5d9
--- /dev/null
+++ b/src/Controller/PublicOffreEmploiController.php
@@ -0,0 +1,50 @@
+ true];
+
+ // Filtrage par type de contrat
+ $type = $request->query->get('type');
+ if ($type) {
+ $criteria['typeContrat'] = $type;
+ }
+
+ // Filtrage par localisation
+ $localisation = $request->query->get('localisation');
+ if ($localisation) {
+ $criteria['localisation'] = $localisation;
+ }
+
+ $offres = $offreEmploiRepository->findBy($criteria, ['datePublication' => 'DESC']);
+
+ return $this->render('public/offre_emploi/index_back_office.html.twig', [
+ 'offres' => $offres,
+ ]);
+ }
+
+ #[Route('/offres-emploi/{id}', name: 'app_public_offre_emploi_show')]
+ public function show(OffreEmploi $offre): Response
+ {
+ // Vérifier si l'offre est active
+ if (!$offre->isIsActive()) {
+ throw $this->createNotFoundException('Cette offre d\'emploi n\'est pas disponible.');
+ }
+
+ return $this->render('public/offre_emploi/show_back_office.html.twig', [
+ 'offre' => $offre,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/SpellCheckerController.php b/src/Controller/SpellCheckerController.php
new file mode 100644
index 0000000..1af015c
--- /dev/null
+++ b/src/Controller/SpellCheckerController.php
@@ -0,0 +1,56 @@
+logger = $logger;
+ $this->spellCheckerService = $spellCheckerService;
+ }
+
+ #[Route('/api/spell-check', name: 'api.spell_check', methods: ['POST'])]
+ public function checkSpelling(Request $request): Response
+ {
+ $text = $request->request->get('text');
+ $language = $request->request->get('language', 'fr');
+
+ $this->logger->info('Requête de vérification orthographique reçue');
+ $this->logger->info('Texte à vérifier: ' . substr($text, 0, 50) . '...');
+
+ if (empty($text)) {
+ $this->logger->warning('Texte vide reçu');
+ return new JsonResponse(['error' => 'Le texte est requis'], Response::HTTP_BAD_REQUEST);
+ }
+
+ try {
+ $corrections = $this->spellCheckerService->checkSpelling($text, $language);
+
+ $this->logger->info('Corrections trouvées: ' . count($corrections));
+
+ return new JsonResponse([
+ 'success' => true,
+ 'corrections' => $corrections,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de la vérification orthographique: ' . $e->getMessage());
+
+ return new JsonResponse([
+ 'success' => false,
+ 'error' => 'Une erreur est survenue lors de la vérification orthographique: ' . $e->getMessage(),
+ ], Response::HTTP_INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/src/Entity/Candidature.php b/src/Entity/Candidature.php
index 9b14180..ba21ebb 100644
--- a/src/Entity/Candidature.php
+++ b/src/Entity/Candidature.php
@@ -1,103 +1,194 @@
dateCandidature = new \DateTimeImmutable();
+ $this->statut = 'en_attente';
+ }
+
public function getId(): ?int
{
return $this->id;
}
- public function setId(int $id): self
+ public function getNom(): ?string
{
- $this->id = $id;
- return $this;
+ return $this->nom;
}
- #[ORM\ManyToOne(targetEntity: Candidat::class, inversedBy: 'candidatures')]
- #[ORM\JoinColumn(name: 'candidat_id', referencedColumnName: 'id')]
- private ?Candidat $candidat = null;
+ public function setNom(string $nom): static
+ {
+ $this->nom = $nom;
+ return $this;
+ }
- public function getCandidat(): ?Candidat
+ public function getPrenom(): ?string
{
- return $this->candidat;
+ return $this->prenom;
}
- public function setCandidat(?Candidat $candidat): self
+ public function setPrenom(string $prenom): static
{
- $this->candidat = $candidat;
+ $this->prenom = $prenom;
return $this;
}
- #[ORM\ManyToOne(targetEntity: OffreEmploi::class, inversedBy: 'candidatures')]
- #[ORM\JoinColumn(name: 'offre_emploi_id', referencedColumnName: 'id')]
- private ?OffreEmploi $offreEmploi = null;
+ public function getEmail(): ?string
+ {
+ return $this->email;
+ }
- public function getOffreEmploi(): ?OffreEmploi
+ public function setEmail(string $email): static
{
- return $this->offreEmploi;
+ $this->email = $email;
+ return $this;
}
- public function setOffreEmploi(?OffreEmploi $offreEmploi): self
+ public function getTelephone(): ?string
{
- $this->offreEmploi = $offreEmploi;
+ return $this->telephone;
+ }
+
+ public function setTelephone(?string $telephone): static
+ {
+ $this->telephone = $telephone;
return $this;
}
- #[ORM\Column(type: 'string', nullable: false)]
- private ?string $cv = null;
+ public function getMessage(): ?string
+ {
+ return $this->message;
+ }
+
+ public function setMessage(string $message): static
+ {
+ $this->message = $message;
+ return $this;
+ }
public function getCv(): ?string
{
return $this->cv;
}
- public function setCv(string $cv): self
+ public function setCv(?string $cv): static
{
$this->cv = $cv;
return $this;
}
- #[ORM\Column(type: 'string', nullable: true)]
- private ?string $reference = null;
-
- public function getReference(): ?string
+ public function getLettreMotivation(): ?string
{
- return $this->reference;
+ return $this->lettreMotivation;
}
- public function setReference(?string $reference): self
+ public function setLettreMotivation(?string $lettreMotivation): static
{
- $this->reference = $reference;
+ $this->lettreMotivation = $lettreMotivation;
return $this;
}
- #[ORM\Column(type: 'string', nullable: true)]
- private ?string $status = null;
+ public function getDateCandidature(): ?\DateTimeImmutable
+ {
+ return $this->dateCandidature;
+ }
- public function getStatus(): ?string
+ public function setDateCandidature(\DateTimeImmutable $dateCandidature): static
{
- return $this->status;
+ $this->dateCandidature = $dateCandidature;
+ return $this;
}
- public function setStatus(?string $status): self
+ public function getOffre(): ?OffreEmploi
{
- $this->status = $status;
+ return $this->offre;
+ }
+
+ public function setOffre(?OffreEmploi $offre): static
+ {
+ $this->offre = $offre;
return $this;
}
+ public function getStatut(): ?string
+ {
+ return $this->statut;
+ }
+
+ public function setStatut(string $statut): static
+ {
+ $this->statut = $statut;
+ return $this;
+ }
}
diff --git a/src/Entity/OffreEmploi.php b/src/Entity/OffreEmploi.php
index b801e21..e935bdf 100644
--- a/src/Entity/OffreEmploi.php
+++ b/src/Entity/OffreEmploi.php
@@ -1,82 +1,198 @@
candidatures = new ArrayCollection();
+ }
+
public function getId(): ?int
{
return $this->id;
}
- public function setId(int $id): self
+ public function getTitre(): ?string
{
- $this->id = $id;
+ return $this->titre;
+ }
+
+ public function setTitre(string $titre): static
+ {
+ $this->titre = $titre;
return $this;
}
- #[ORM\Column(type: 'string', nullable: false)]
- private ?string $title = null;
+ public function getTypeContrat(): ?string
+ {
+ return $this->typeContrat;
+ }
+
+ public function setTypeContrat(string $typeContrat): static
+ {
+ $this->typeContrat = $typeContrat;
+ return $this;
+ }
- public function getTitle(): ?string
+ public function getLocalisation(): ?string
{
- return $this->title;
+ return $this->localisation;
}
- public function setTitle(string $title): self
+ public function setLocalisation(string $localisation): static
{
- $this->title = $title;
+ $this->localisation = $localisation;
return $this;
}
- #[ORM\Column(type: 'text', nullable: false)]
- private ?string $description = null;
+ public function getSalaire(): ?string
+ {
+ return $this->salaire;
+ }
+
+ public function setSalaire(string $salaire): static
+ {
+ $this->salaire = $salaire;
+ return $this;
+ }
public function getDescription(): ?string
{
return $this->description;
}
- public function setDescription(string $description): self
+ public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
- #[ORM\Column(type: 'string', nullable: false)]
- private ?string $location = null;
+ public function getProfilRecherche(): ?string
+ {
+ return $this->profilRecherche;
+ }
+
+ public function setProfilRecherche(string $profilRecherche): static
+ {
+ $this->profilRecherche = $profilRecherche;
+ return $this;
+ }
- public function getLocation(): ?string
+ public function getAvantages(): ?string
{
- return $this->location;
+ return $this->avantages;
}
- public function setLocation(string $location): self
+ public function setAvantages(?string $avantages): static
{
- $this->location = $location;
+ $this->avantages = $avantages;
return $this;
}
- #[ORM\OneToMany(targetEntity: Candidature::class, mappedBy: 'offreEmploi')]
- private Collection $candidatures;
+ public function isIsActive(): ?bool
+ {
+ return $this->isActive;
+ }
- public function __construct()
+ public function setIsActive(bool $isActive): static
{
- $this->candidatures = new ArrayCollection();
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ public function getDatePublication(): ?\DateTimeInterface
+ {
+ return $this->datePublication;
+ }
+
+ public function setDatePublication(\DateTimeInterface $datePublication): static
+ {
+ $this->datePublication = $datePublication;
+ return $this;
}
/**
@@ -84,24 +200,25 @@ public function __construct()
*/
public function getCandidatures(): Collection
{
- if (!$this->candidatures instanceof Collection) {
- $this->candidatures = new ArrayCollection();
- }
return $this->candidatures;
}
- public function addCandidature(Candidature $candidature): self
+ public function addCandidature(Candidature $candidature): static
{
- if (!$this->getCandidatures()->contains($candidature)) {
- $this->getCandidatures()->add($candidature);
+ if (! $this->candidatures->contains($candidature)) {
+ $this->candidatures->add($candidature);
+ $candidature->setOffre($this);
}
return $this;
}
- public function removeCandidature(Candidature $candidature): self
+ public function removeCandidature(Candidature $candidature): static
{
- $this->getCandidatures()->removeElement($candidature);
+ if ($this->candidatures->removeElement($candidature)) {
+ if ($candidature->getOffre() === $this) {
+ $candidature->setOffre(null);
+ }
+ }
return $this;
}
-
}
diff --git a/src/Form/CandidaturePublicType.php b/src/Form/CandidaturePublicType.php
new file mode 100644
index 0000000..d16957a
--- /dev/null
+++ b/src/Form/CandidaturePublicType.php
@@ -0,0 +1,124 @@
+add('nom', TextType::class, [
+ 'label' => 'Nom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le nom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('prenom', TextType::class, [
+ 'label' => 'Prénom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le prénom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('email', EmailType::class, [
+ 'label' => 'Email',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'],
+ 'constraints' => [
+ new NotBlank(['message' => 'L\'email est obligatoire']),
+ new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide'])
+ ]
+ ])
+ ->add('telephone', TextType::class, [
+ 'label' => 'Téléphone',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le téléphone est obligatoire']),
+ new Regex([
+ 'pattern' => '/^[0-9\s\+\-\.]{8,}$/',
+ 'message' => 'Le numéro de téléphone n\'est pas valide'
+ ])
+ ]
+ ])
+ ->add('message', TextareaType::class, [
+ 'label' => 'Message',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 5,
+ 'placeholder' => 'Décrivez votre motivation pour ce poste...'
+ ],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le message est obligatoire']),
+ new Length([
+ 'min' => 10,
+ 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('cv', FileType::class, [
+ 'label' => 'CV (PDF)',
+ 'required' => false,
+ 'mapped' => false,
+ 'constraints' => [
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ])
+ ->add('lettreMotivation', FileType::class, [
+ 'label' => 'Lettre de motivation (PDF)',
+ 'required' => false,
+ 'mapped' => false,
+ 'constraints' => [
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ]);
+ // Le champ statut est intentionnellement omis
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Candidature::class,
+ 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony
+ ]);
+ }
+}
diff --git a/src/Form/CandidatureSimpleType.php b/src/Form/CandidatureSimpleType.php
new file mode 100644
index 0000000..0afe715
--- /dev/null
+++ b/src/Form/CandidatureSimpleType.php
@@ -0,0 +1,134 @@
+add('nom', TextType::class, [
+ 'label' => 'Nom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le nom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('prenom', TextType::class, [
+ 'label' => 'Prénom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le prénom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('email', EmailType::class, [
+ 'label' => 'Email',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'],
+ 'constraints' => [
+ new NotBlank(['message' => 'L\'email est obligatoire']),
+ new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide'])
+ ]
+ ])
+ ->add('telephone', TextType::class, [
+ 'label' => 'Téléphone',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le téléphone est obligatoire']),
+ new Regex([
+ 'pattern' => '/^[0-9\s\+\-\.]{8,}$/',
+ 'message' => 'Le numéro de téléphone n\'est pas valide'
+ ])
+ ]
+ ])
+ ->add('message', TextareaType::class, [
+ 'label' => 'Message',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 5,
+ 'placeholder' => 'Décrivez votre motivation pour ce poste...'
+ ],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le message est obligatoire']),
+ new Length([
+ 'min' => 10,
+ 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('cv', FileType::class, [
+ 'label' => 'CV (PDF) *',
+ 'required' => true,
+ 'mapped' => false,
+ 'attr' => ['class' => 'form-control'],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => 'Veuillez télécharger votre CV au format PDF'
+ ]),
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ])
+ ->add('lettreMotivation', FileType::class, [
+ 'label' => 'Lettre de motivation (PDF) *',
+ 'required' => true,
+ 'mapped' => false,
+ 'attr' => ['class' => 'form-control'],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => 'Veuillez télécharger votre lettre de motivation au format PDF'
+ ]),
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Candidature::class,
+ 'csrf_protection' => true,
+ 'csrf_field_name' => '_token',
+ 'csrf_token_id' => 'candidature_item',
+ 'validation_groups' => ['Default'],
+ ]);
+ }
+}
diff --git a/src/Form/CandidatureType.php b/src/Form/CandidatureType.php
new file mode 100644
index 0000000..91048a8
--- /dev/null
+++ b/src/Form/CandidatureType.php
@@ -0,0 +1,134 @@
+add('nom', TextType::class, [
+ 'label' => 'Nom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le nom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('prenom', TextType::class, [
+ 'label' => 'Prénom',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le prénom est obligatoire']),
+ new Length([
+ 'min' => 2,
+ 'max' => 255,
+ 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères',
+ 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('email', EmailType::class, [
+ 'label' => 'Email',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'],
+ 'constraints' => [
+ new NotBlank(['message' => 'L\'email est obligatoire']),
+ new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide'])
+ ]
+ ])
+ ->add('telephone', TextType::class, [
+ 'label' => 'Téléphone',
+ 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le téléphone est obligatoire']),
+ new Regex([
+ 'pattern' => '/^[0-9\s\+\-\.]{8,}$/',
+ 'message' => 'Le numéro de téléphone n\'est pas valide'
+ ])
+ ]
+ ])
+ ->add('message', TextareaType::class, [
+ 'label' => 'Message',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 5,
+ 'placeholder' => 'Décrivez votre motivation pour ce poste...'
+ ],
+ 'constraints' => [
+ new NotBlank(['message' => 'Le message est obligatoire']),
+ new Length([
+ 'min' => 10,
+ 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères'
+ ])
+ ]
+ ])
+ ->add('cv', FileType::class, [
+ 'label' => 'CV (PDF)',
+ 'required' => false,
+ 'mapped' => false,
+ 'constraints' => [
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ])
+ ->add('lettreMotivation', FileType::class, [
+ 'label' => 'Lettre de motivation (PDF)',
+ 'required' => false,
+ 'mapped' => false,
+ 'constraints' => [
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide',
+ ])
+ ],
+ ])
+ ->add('statut', ChoiceType::class, [
+ 'label' => 'Statut',
+ 'choices' => [
+ 'En attente' => 'en_attente',
+ 'En cours de traitement' => 'en_cours',
+ 'Acceptée' => 'acceptee',
+ 'Refusée' => 'refusee',
+ ],
+ 'required' => true,
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Candidature::class,
+ 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Form/OffreEmploiType.php b/src/Form/OffreEmploiType.php
new file mode 100644
index 0000000..6ac9552
--- /dev/null
+++ b/src/Form/OffreEmploiType.php
@@ -0,0 +1,96 @@
+add('titre', TextType::class, [
+ 'label' => 'Titre de l\'offre',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'placeholder' => 'Ex: Développeur Symfony'
+ ]
+ ])
+ ->add('typeContrat', ChoiceType::class, [
+ 'label' => 'Type de contrat',
+ 'choices' => [
+ 'CDI' => 'CDI',
+ 'CDD' => 'CDD',
+ 'Stage' => 'Stage',
+ 'Alternance' => 'Alternance',
+ 'Freelance' => 'Freelance'
+ ],
+ 'attr' => [
+ 'class' => 'form-control'
+ ]
+ ])
+ ->add('localisation', TextType::class, [
+ 'label' => 'Localisation',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'placeholder' => 'Ex: Paris'
+ ]
+ ])
+ ->add('salaire', TextType::class, [
+ 'label' => 'Salaire',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'placeholder' => 'Ex: 45-55K€'
+ ]
+ ])
+ ->add('description', TextareaType::class, [
+ 'label' => 'Description',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 5,
+ 'placeholder' => 'Description détaillée de l\'offre'
+ ]
+ ])
+ ->add('profilRecherche', TextareaType::class, [
+ 'label' => 'Profil recherché',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 5,
+ 'placeholder' => 'Profil et compétences recherchés'
+ ]
+ ])
+ ->add('avantages', TextareaType::class, [
+ 'label' => 'Avantages',
+ 'required' => false,
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 3,
+ 'placeholder' => 'Avantages proposés'
+ ]
+ ])
+ ->add('isActive', ChoiceType::class, [
+ 'label' => 'Statut',
+ 'choices' => [
+ 'Active' => true,
+ 'Inactive' => false
+ ],
+ 'attr' => [
+ 'class' => 'form-control'
+ ]
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => OffreEmploi::class,
+ 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Repository/CandidatureRepository.php b/src/Repository/CandidatureRepository.php
index 1e95257..18ea2a6 100644
--- a/src/Repository/CandidatureRepository.php
+++ b/src/Repository/CandidatureRepository.php
@@ -1,5 +1,4 @@
createQueryBuilder('c')
- // ->andWhere('c.exampleField = :val')
- // ->setParameter('val', $value)
- // ->orderBy('c.id', 'ASC')
- // ->setMaxResults(10)
- // ->getQuery()
- // ->getResult()
- // ;
- // }
+ public function save(Candidature $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(Candidature $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
- // public function findOneBySomeField($value): ?Candidature
- // {
- // return $this->createQueryBuilder('c')
- // ->andWhere('c.exampleField = :val')
- // ->setParameter('val', $value)
- // ->getQuery()
- // ->getOneOrNullResult()
- // ;
- // }
+ public function findByOffre($offreId)
+ {
+ return $this->createQueryBuilder('c')
+ ->andWhere('c.offre = :offreId')
+ ->setParameter('offreId', $offreId)
+ ->orderBy('c.dateCandidature', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
}
diff --git a/src/Repository/OffreEmploiRepository.php b/src/Repository/OffreEmploiRepository.php
index b24a7ad..6c7eaea 100644
--- a/src/Repository/OffreEmploiRepository.php
+++ b/src/Repository/OffreEmploiRepository.php
@@ -1,5 +1,4 @@
createQueryBuilder('o')
- // ->andWhere('o.exampleField = :val')
- // ->setParameter('val', $value)
- // ->orderBy('o.id', 'ASC')
- // ->setMaxResults(10)
- // ->getQuery()
- // ->getResult()
- // ;
- // }
+ public function findAllActive(): array
+ {
+ return $this->createQueryBuilder('o')
+ ->where('o.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('o.datePublication', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
- // public function findOneBySomeField($value): ?OffreEmploi
- // {
- // return $this->createQueryBuilder('o')
- // ->andWhere('o.exampleField = :val')
- // ->setParameter('val', $value)
- // ->getQuery()
- // ->getOneOrNullResult()
- // ;
- // }
+ public function findByTypeContrat(string $typeContrat): array
+ {
+ return $this->createQueryBuilder('o')
+ ->where('o.typeContrat = :typeContrat')
+ ->andWhere('o.isActive = :active')
+ ->setParameter('typeContrat', $typeContrat)
+ ->setParameter('active', true)
+ ->orderBy('o.datePublication', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
}
diff --git a/src/Service/CvAnalyzerService.php b/src/Service/CvAnalyzerService.php
new file mode 100644
index 0000000..f5f31f9
--- /dev/null
+++ b/src/Service/CvAnalyzerService.php
@@ -0,0 +1,817 @@
+httpClient = $httpClient;
+ $this->logger = $logger;
+ }
+
+ public function analyzeCv(string $cvPath): array
+ {
+ $this->logger->info('Début de l\'analyse du CV: ' . $cvPath);
+
+ try {
+ // Vérifier si le fichier existe
+ if (!file_exists($cvPath)) {
+ $this->logger->error('Fichier CV non trouvé: ' . $cvPath);
+ return [
+ 'success' => false,
+ 'message' => 'Fichier CV non trouvé: ' . $cvPath
+ ];
+ }
+
+ $cvFile = new File($cvPath);
+
+ // Vérifier la taille du fichier
+ if ($cvFile->getSize() > 10 * 1024 * 1024) {
+ $this->logger->error('Fichier CV trop volumineux: ' . $cvFile->getSize() . ' bytes');
+ return [
+ 'success' => false,
+ 'message' => 'Le fichier CV est trop volumineux (max 10MB).'
+ ];
+ }
+
+ // Vérifier l'extension du fichier
+ $extension = strtolower($cvFile->getExtension());
+ if (!in_array($extension, ['pdf', 'doc', 'docx'])) {
+ $this->logger->error('Format de fichier non supporté: ' . $extension);
+ return [
+ 'success' => false,
+ 'message' => 'Format de fichier non supporté. Utilisez PDF, DOC ou DOCX.'
+ ];
+ }
+
+ // Si c'est un PDF, essayer d'abord l'extraction directe
+ if ($extension === 'pdf') {
+ $this->logger->info('Fichier PDF détecté, tentative d\'extraction directe du texte...');
+ $pdfText = $this->extractTextFromPdf($cvPath);
+
+ if (!empty($pdfText)) {
+ $this->logger->info('Texte extrait du PDF avec succès: ' . strlen($pdfText) . ' caractères');
+
+ // Créer un résultat avec le texte brut
+ $result = [
+ 'summary' => '',
+ 'skills' => [],
+ 'experience' => [],
+ 'education' => [],
+ 'rawText' => $pdfText
+ ];
+
+ // Analyser le texte brut
+ $this->analyzeRawText($pdfText, $result);
+
+ // Si l'analyse a réussi et a trouvé des informations, retourner le résultat
+ if (!empty($result['skills']) || !empty($result['experience']) || !empty($result['education'])) {
+ $this->logger->info('Analyse locale réussie, informations extraites');
+ return [
+ 'success' => true,
+ 'data' => $result,
+ 'source' => 'local_extraction'
+ ];
+ }
+
+ // Sinon, continuer avec l'API Affinda comme plan B
+ $this->logger->info('Analyse locale insuffisante, tentative avec l\'API Affinda...');
+ }
+ }
+
+ // Préparer la requête à l'API Affinda
+ $fileContent = file_get_contents($cvPath);
+ $this->logger->info('Taille du fichier : ' . strlen($fileContent) . ' octets');
+
+ // Créer un fichier temporaire pour l'upload multipart
+ $tempFile = tempnam(sys_get_temp_dir(), 'cv_');
+ file_put_contents($tempFile, $fileContent);
+
+ // Créer les données du formulaire
+ $formData = [
+ 'file' => curl_file_create($tempFile, 'application/pdf', $cvFile->getFilename()),
+ 'wait' => 'true',
+ 'workspace' => self::AFFINDA_WORKSPACE_ID,
+ 'collection' => self::AFFINDA_COLLECTION_ID
+ ];
+
+ $this->logger->info('Fichier temporaire créé : ' . $tempFile);
+
+ $this->logger->info('Envoi de la requête à Affinda...');
+ $this->logger->info('URL: ' . self::AFFINDA_API_URL);
+ $this->logger->info('Collection ID: ' . self::AFFINDA_COLLECTION_ID);
+ $this->logger->info('Workspace ID: ' . self::AFFINDA_WORKSPACE_ID);
+
+ // Utiliser cURL directement pour un meilleur contrôle
+ $curl = curl_init();
+
+ curl_setopt_array($curl, [
+ CURLOPT_URL => self::AFFINDA_API_URL,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_ENCODING => '',
+ CURLOPT_MAXREDIRS => 10,
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => $formData,
+ CURLOPT_HTTPHEADER => [
+ 'Accept: application/json',
+ 'Authorization: Bearer ' . self::AFFINDA_API_KEY
+ ],
+ ]);
+
+ $responseBody = curl_exec($curl);
+ $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ $error = curl_error($curl);
+
+ curl_close($curl);
+
+ // Supprimer le fichier temporaire
+ if (file_exists($tempFile)) {
+ unlink($tempFile);
+ $this->logger->info('Fichier temporaire supprimé : ' . $tempFile);
+ }
+
+ if ($error) {
+ $this->logger->error('Erreur cURL : ' . $error);
+
+ // En cas d'erreur avec l'API, essayer d'extraire le texte directement du PDF
+ $this->logger->info('Tentative d\'extraction directe du texte du PDF...');
+ $pdfText = $this->extractTextFromPdf($cvPath);
+
+ if (!empty($pdfText)) {
+ $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères');
+
+ // Créer un résultat avec le texte brut
+ $result = [
+ 'summary' => '',
+ 'skills' => [],
+ 'experience' => [],
+ 'education' => [],
+ 'rawText' => $pdfText
+ ];
+
+ // Analyser le texte brut
+ $this->analyzeRawText($pdfText, $result);
+
+ return [
+ 'success' => true,
+ 'data' => $result,
+ 'source' => 'local_extraction'
+ ];
+ }
+
+ return [
+ 'success' => false,
+ 'message' => 'Erreur lors de la communication avec l\'API : ' . $error
+ ];
+ }
+
+ $this->logger->info('Réponse de l\'API Affinda: Code ' . $statusCode);
+ $this->logger->info('Réponse brute: ' . substr($responseBody, 0, 1000) . (strlen($responseBody) > 1000 ? '...' : ''));
+
+ if ($statusCode === 200 || $statusCode === 201) {
+ $data = json_decode($responseBody, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->logger->error('Erreur de décodage JSON: ' . json_last_error_msg());
+ return [
+ 'success' => false,
+ 'message' => 'Erreur de décodage de la réponse: ' . json_last_error_msg()
+ ];
+ }
+
+ $this->logger->info('Réponse décodée: ' . print_r($data, true));
+
+ // Traiter les résultats
+ $analysisResult = $this->processApiResponse($data);
+
+ return [
+ 'success' => true,
+ 'data' => $analysisResult
+ ];
+ } else {
+ $this->logger->error('Erreur lors de l\'analyse du CV: ' . $statusCode);
+ $errorMessage = 'Erreur lors de l\'analyse du CV.';
+
+ if ($statusCode === 401) {
+ $errorMessage .= ' Erreur d\'authentification. Veuillez vérifier la clé API.';
+ } elseif ($statusCode === 404) {
+ $errorMessage .= ' Service non trouvé. Veuillez vérifier l\'URL de l\'API.';
+ } else {
+ $errorMessage .= ' Code: ' . $statusCode . ' Réponse: ' . $responseBody;
+ }
+
+ // En cas d'erreur avec l'API, essayer d'extraire le texte directement du PDF
+ $this->logger->info('Tentative d\'extraction directe du texte du PDF après erreur API...');
+ $pdfText = $this->extractTextFromPdf($cvPath);
+
+ if (!empty($pdfText)) {
+ $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères');
+
+ // Créer un résultat avec le texte brut
+ $result = [
+ 'summary' => '',
+ 'skills' => [],
+ 'experience' => [],
+ 'education' => [],
+ 'rawText' => $pdfText
+ ];
+
+ // Analyser le texte brut
+ $this->analyzeRawText($pdfText, $result);
+
+ return [
+ 'success' => true,
+ 'data' => $result,
+ 'source' => 'local_extraction'
+ ];
+ }
+
+ return [
+ 'success' => false,
+ 'message' => $errorMessage
+ ];
+ }
+ } catch (\Exception $e) {
+ $this->logger->error('Exception lors de l\'analyse du CV: ' . $e->getMessage());
+
+ // En cas d'exception, essayer d'extraire le texte directement du PDF
+ try {
+ $this->logger->info('Tentative d\'extraction directe du texte du PDF après exception...');
+ $pdfText = $this->extractTextFromPdf($cvPath);
+
+ if (!empty($pdfText)) {
+ $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères');
+
+ // Créer un résultat avec le texte brut
+ $result = [
+ 'summary' => '',
+ 'skills' => [],
+ 'experience' => [],
+ 'education' => [],
+ 'rawText' => $pdfText
+ ];
+
+ // Analyser le texte brut
+ $this->analyzeRawText($pdfText, $result);
+
+ return [
+ 'success' => true,
+ 'data' => $result,
+ 'source' => 'local_extraction'
+ ];
+ }
+ } catch (\Exception $innerException) {
+ $this->logger->error('Exception lors de l\'extraction directe du texte: ' . $innerException->getMessage());
+ }
+
+ return [
+ 'success' => false,
+ 'message' => 'Une erreur est survenue lors de l\'analyse du CV: ' . $e->getMessage()
+ ];
+ }
+ }
+
+ private function processApiResponse(array $data): array
+ {
+ $result = [
+ 'summary' => '',
+ 'skills' => [],
+ 'experience' => [],
+ 'education' => []
+ ];
+
+ $this->logger->info('Traitement de la réponse API: ' . json_encode($data, JSON_PRETTY_PRINT));
+
+ // Vérifier si nous avons des données
+ if (empty($data)) {
+ $this->logger->warning('Aucune donnée dans la réponse API');
+ return $result;
+ }
+
+ // Extraire les données selon la structure de la réponse
+ $responseData = $data;
+ if (isset($data['data'])) {
+ $responseData = $data['data'];
+ }
+
+ // Résumé
+ if (isset($responseData['summary']) && isset($responseData['summary']['parsed'])) {
+ $result['summary'] = $responseData['summary']['parsed'];
+ $this->logger->info('Résumé extrait: ' . substr($result['summary'], 0, 100) . '...');
+ } else {
+ $this->logger->warning('Aucun résumé trouvé dans la réponse');
+ }
+
+ // Compétences
+ if (isset($responseData['skills']) && is_array($responseData['skills'])) {
+ foreach ($responseData['skills'] as $skill) {
+ if (isset($skill['name'])) {
+ $result['skills'][] = $skill['name'];
+ }
+ }
+ $this->logger->info('Compétences extraites: ' . count($result['skills']));
+ } else {
+ $this->logger->warning('Aucune compétence trouvée dans la réponse');
+ }
+
+ // Expérience professionnelle
+ if (isset($responseData['workExperience']) && is_array($responseData['workExperience'])) {
+ foreach ($responseData['workExperience'] as $experience) {
+ $jobTitle = $experience['jobTitle'] ?? 'Non spécifié';
+ $organization = $experience['organization'] ?? 'Non spécifié';
+
+ $startDate = null;
+ $endDate = null;
+ if (isset($experience['dates'])) {
+ $startDate = $experience['dates']['startDate'] ?? null;
+ $endDate = $experience['dates']['endDate'] ?? null;
+ }
+
+ $exp = [
+ 'jobTitle' => $jobTitle,
+ 'organization' => $organization,
+ 'dates' => $this->formatDateRange($startDate, $endDate),
+ 'description' => $experience['description'] ?? 'Non spécifié'
+ ];
+ $result['experience'][] = $exp;
+ }
+ $this->logger->info('Expériences extraites: ' . count($result['experience']));
+ } else {
+ $this->logger->warning('Aucune expérience trouvée dans la réponse');
+
+ // Essayer d'extraire les expériences du texte brut
+ if (isset($responseData['rawText'])) {
+ $this->extractExperiencesFromRawText($responseData['rawText'], $result);
+ }
+ }
+
+ // Formation
+ if (isset($responseData['education']) && is_array($responseData['education'])) {
+ foreach ($responseData['education'] as $education) {
+ $institution = $education['organization'] ?? 'Non spécifié';
+ $degree = 'Non spécifié';
+
+ if (isset($education['accreditation']) && isset($education['accreditation']['education'])) {
+ $degree = $education['accreditation']['education'];
+ }
+
+ $startDate = null;
+ $endDate = null;
+ if (isset($education['dates'])) {
+ $startDate = $education['dates']['startDate'] ?? null;
+ $endDate = $education['dates']['endDate'] ?? null;
+ }
+
+ $edu = [
+ 'institution' => $institution,
+ 'degree' => $degree,
+ 'dates' => $this->formatDateRange($startDate, $endDate)
+ ];
+ $result['education'][] = $edu;
+ }
+ $this->logger->info('Formations extraites: ' . count($result['education']));
+ } else {
+ $this->logger->warning('Aucune formation trouvée dans la réponse');
+
+ // Essayer d'extraire les formations du texte brut
+ if (isset($responseData['rawText'])) {
+ $this->extractEducationFromRawText($responseData['rawText'], $result);
+ }
+ }
+
+ // Texte brut pour analyse supplémentaire
+ if (isset($responseData['rawText'])) {
+ $result['rawText'] = $responseData['rawText'];
+ $this->logger->info('Texte brut extrait: ' . strlen($responseData['rawText']) . ' caractères');
+
+ // Analyse supplémentaire du texte brut si nécessaire
+ $this->analyzeRawText($responseData['rawText'], $result);
+ } else {
+ $this->logger->warning('Aucun texte brut trouvé dans la réponse');
+ }
+
+ return $result;
+ }
+
+ private function formatDateRange($startDate, $endDate): string
+ {
+ $formattedStart = $startDate ? date('m/Y', strtotime($startDate)) : 'Non spécifié';
+ $formattedEnd = $endDate ? date('m/Y', strtotime($endDate)) : 'Présent';
+
+ return $formattedStart . ' - ' . $formattedEnd;
+ }
+
+ private function extractTextFromPdf(string $pdfPath): string
+ {
+ $this->logger->info('Extraction du texte du PDF: ' . $pdfPath);
+
+ // Vérifier si le fichier existe
+ if (!file_exists($pdfPath)) {
+ $this->logger->error('Fichier PDF non trouvé: ' . $pdfPath);
+ return '';
+ }
+
+ // Essayer d'abord avec la classe PHP pour PDF
+ try {
+ if (class_exists('\Smalot\PdfParser\Parser')) {
+ $this->logger->info('Utilisation de PdfParser pour extraire le texte');
+ $parser = new \Smalot\PdfParser\Parser();
+ $pdf = $parser->parseFile($pdfPath);
+ $text = $pdf->getText();
+
+ if (!empty($text)) {
+ $this->logger->info('Texte extrait avec PdfParser: ' . strlen($text) . ' caractères');
+ return $text;
+ }
+ }
+ } catch (\Exception $e) {
+ $this->logger->warning('Erreur avec PdfParser: ' . $e->getMessage());
+ }
+
+ // Essayer avec pdftotext si disponible
+ try {
+ // Vérifier si pdftotext est disponible
+ $pdftotext = 'pdftotext';
+ $command = "$pdftotext -q -enc UTF-8 '$pdfPath' -";
+
+ $this->logger->info('Exécution de la commande: ' . $command);
+ $output = shell_exec($command);
+
+ if ($output !== null && !empty(trim($output))) {
+ $this->logger->info('Texte extrait avec pdftotext: ' . strlen($output) . ' caractères');
+ return $output;
+ }
+ } catch (\Exception $e) {
+ $this->logger->warning('Erreur avec pdftotext: ' . $e->getMessage());
+ }
+
+ // Si les méthodes précédentes ont échoué, essayer la méthode alternative
+ $this->logger->warning('Les méthodes principales ont échoué, essai avec la méthode alternative');
+ return $this->extractTextFromPdfAlternative($pdfPath);
+ }
+
+ private function extractTextFromPdfAlternative(string $pdfPath): string
+ {
+ $this->logger->info('Extraction alternative du texte du PDF: ' . $pdfPath);
+
+ // Méthode alternative utilisant PHP pour extraire le texte
+ try {
+ // Lire le fichier PDF en binaire
+ $content = file_get_contents($pdfPath);
+
+ // Recherche de texte dans le contenu binaire
+ $text = '';
+
+ // Recherche de blocs de texte entre parenthèses
+ preg_match_all('/\((.*?)\)/s', $content, $matches);
+
+ foreach ($matches[1] as $match) {
+ // Filtrer les caractères non imprimables
+ $filtered = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $match);
+ if (strlen($filtered) > 3) { // Ignorer les chaînes trop courtes
+ $text .= $filtered . "\n";
+ }
+ }
+
+ // Si aucun texte n'a été extrait, essayer une autre approche
+ if (empty(trim($text))) {
+ // Recherche de blocs de texte entre /BT et /ET (Begin Text et End Text)
+ preg_match_all('/\/BT(.*?)\/ET/s', $content, $matches);
+
+ foreach ($matches[1] as $match) {
+ // Extraire les chaînes de texte
+ preg_match_all('/\[(.*?)\]/s', $match, $textMatches);
+
+ foreach ($textMatches[1] as $textMatch) {
+ $filtered = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $textMatch);
+ if (strlen($filtered) > 3) {
+ $text .= $filtered . "\n";
+ }
+ }
+ }
+ }
+
+ // Si toujours pas de texte, essayer une approche plus simple
+ if (empty(trim($text))) {
+ // Recherche de texte lisible dans le contenu
+ $filtered = preg_replace('/[^a-zA-Z0-9\s\.,;:\-_\'"\(\)\[\]\{\}\+\*\/\\\?\!\@\#\$\%\^\&\=]/', ' ', $content);
+ $filtered = preg_replace('/\s+/', ' ', $filtered);
+
+ // Extraire des segments qui ressemblent à du texte (au moins 5 caractères alphanumériques consécutifs)
+ preg_match_all('/[a-zA-Z0-9\s\.,;:\-_]{5,}/', $filtered, $textMatches);
+
+ foreach ($textMatches[0] as $match) {
+ if (strlen($match) > 10) { // Ignorer les segments trop courts
+ $text .= $match . "\n";
+ }
+ }
+ }
+
+ // Nettoyer le texte final
+ $text = preg_replace('/\s+/', ' ', $text); // Remplacer les espaces multiples par un seul
+ $text = preg_replace('/\n\s*\n/', "\n", $text); // Supprimer les lignes vides
+
+ $this->logger->info('Texte extrait avec la méthode alternative: ' . strlen($text) . ' caractères');
+ return $text;
+ } catch (\Exception $e) {
+ $this->logger->error('Erreur lors de l\'extraction alternative du texte: ' . $e->getMessage());
+ return '';
+ }
+ }
+
+ private function analyzeRawText(string $rawText, array &$result): void
+ {
+ $this->logger->info('Analyse du texte brut: ' . strlen($rawText) . ' caractères');
+
+ // Analyse des compétences si elles n'ont pas été détectées automatiquement
+ if (empty($result['skills'])) {
+ $this->extractSkillsFromRawText($rawText, $result);
+ }
+
+ // Analyse de l'expérience professionnelle si elle n'a pas été détectée automatiquement
+ if (empty($result['experience'])) {
+ $this->extractExperiencesFromRawText($rawText, $result);
+ }
+
+ // Analyse de la formation si elle n'a pas été détectée automatiquement
+ if (empty($result['education'])) {
+ $this->extractEducationFromRawText($rawText, $result);
+ }
+ }
+
+ private function extractSkillsFromRawText(string $rawText, array &$result): void
+ {
+ $this->logger->info('Extraction des compétences du texte brut');
+
+ // Recherche de mots-clés courants pour les compétences
+ $skillKeywords = [
+ 'COMPÉTENCES', 'COMPETENCES', 'SKILLS', 'TECHNOLOGIES', 'LANGAGES',
+ 'OUTILS', 'FRAMEWORKS', 'LANGUAGES', 'TECHNICAL SKILLS'
+ ];
+
+ $lines = explode("\n", $rawText);
+ $inSkillsSection = false;
+ $skillsFound = false;
+
+ foreach ($lines as $line) {
+ $trimmedLine = trim($line);
+
+ // Détection du début de la section compétences
+ foreach ($skillKeywords as $keyword) {
+ if (stripos($trimmedLine, $keyword) !== false) {
+ $inSkillsSection = true;
+ $this->logger->info('Section de compétences trouvée avec le mot-clé: ' . $keyword);
+ break;
+ }
+ }
+
+ // Extraction des compétences
+ if ($inSkillsSection) {
+ // Détection de la fin de la section
+ if (empty($trimmedLine) || preg_match('/^(FORMATION|EDUCATION|EXPERIENCE|PARCOURS)/i', $trimmedLine)) {
+ $inSkillsSection = false;
+ continue;
+ }
+
+ // Extraction des compétences avec puces
+ if (strpos($trimmedLine, '•') === 0 || strpos($trimmedLine, '-') === 0 || strpos($trimmedLine, '*') === 0) {
+ $skill = trim(preg_replace('/^[•\-\*]\s*/', '', $trimmedLine));
+ if (!empty($skill)) {
+ $result['skills'][] = $skill;
+ $skillsFound = true;
+ }
+ }
+ // Extraction des compétences séparées par des virgules
+ elseif (strpos($trimmedLine, ',') !== false) {
+ $skills = array_map('trim', explode(',', $trimmedLine));
+ foreach ($skills as $skill) {
+ if (!empty($skill)) {
+ $result['skills'][] = $skill;
+ $skillsFound = true;
+ }
+ }
+ }
+ // Extraction des compétences individuelles
+ elseif (strlen($trimmedLine) > 2 && strlen($trimmedLine) < 50 && !preg_match('/^(FORMATION|EDUCATION|EXPERIENCE|PARCOURS)/i', $trimmedLine)) {
+ $result['skills'][] = $trimmedLine;
+ $skillsFound = true;
+ }
+ }
+ }
+
+ if ($skillsFound) {
+ $this->logger->info('Compétences extraites du texte brut: ' . count($result['skills']));
+ } else {
+ $this->logger->warning('Aucune compétence n\'a pu être extraite du texte brut');
+ }
+ }
+
+ private function extractExperiencesFromRawText(string $rawText, array &$result): void
+ {
+ $this->logger->info('Extraction des expériences du texte brut');
+
+ // Recherche de mots-clés courants pour les expériences
+ $experienceKeywords = [
+ 'EXPERIENCE', 'EXPERIENCES', 'EXPERIENCES PROFESSIONNELLES', 'PARCOURS PROFESSIONNEL',
+ 'WORK EXPERIENCE', 'PROFESSIONAL EXPERIENCE', 'EMPLOIS', 'POSTES OCCUPÉS'
+ ];
+
+ $lines = explode("\n", $rawText);
+ $inExperienceSection = false;
+ $currentExperience = null;
+ $experiencesFound = false;
+
+ foreach ($lines as $line) {
+ $trimmedLine = trim($line);
+
+ // Détection du début de la section expériences
+ foreach ($experienceKeywords as $keyword) {
+ if (stripos($trimmedLine, $keyword) !== false) {
+ $inExperienceSection = true;
+ $this->logger->info('Section d\'expériences trouvée avec le mot-clé: ' . $keyword);
+ break;
+ }
+ }
+
+ // Extraction des expériences
+ if ($inExperienceSection) {
+ // Détection de la fin de la section
+ if (preg_match('/^(FORMATION|EDUCATION|COMP[EÉ]TENCES)/i', $trimmedLine)) {
+ $inExperienceSection = false;
+ continue;
+ }
+
+ // Détection d'une nouvelle expérience (date + titre)
+ if (preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) ||
+ preg_match('/^([A-Z][a-z]+\s+\d{4})\s*[-–]\s*([A-Z][a-z]+\s+\d{4}|[Pp]r[eé]sent)/', $trimmedLine)) {
+
+ // Sauvegarder l'expérience précédente si elle existe
+ if ($currentExperience !== null) {
+ $result['experience'][] = $currentExperience;
+ $experiencesFound = true;
+ }
+
+ // Créer une nouvelle expérience
+ $currentExperience = [
+ 'jobTitle' => 'Non spécifié',
+ 'organization' => 'Non spécifié',
+ 'dates' => $trimmedLine,
+ 'description' => ''
+ ];
+
+ // Essayer d'extraire le titre du poste et l'organisation
+ $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : '';
+ if (!empty($nextLine) && !preg_match('/^\d{4}/', $nextLine)) {
+ $currentExperience['jobTitle'] = $nextLine;
+
+ // Essayer d'extraire l'organisation
+ $nextNextLine = isset($lines[array_search($line, $lines) + 2]) ? trim($lines[array_search($line, $lines) + 2]) : '';
+ if (!empty($nextNextLine) && !preg_match('/^\d{4}/', $nextNextLine)) {
+ $currentExperience['organization'] = $nextNextLine;
+ }
+ }
+ }
+ // Ajouter à la description de l'expérience courante
+ elseif ($currentExperience !== null && !empty($trimmedLine) &&
+ !preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) &&
+ $trimmedLine !== $currentExperience['jobTitle'] &&
+ $trimmedLine !== $currentExperience['organization']) {
+
+ if (!empty($currentExperience['description'])) {
+ $currentExperience['description'] .= "\n";
+ }
+ $currentExperience['description'] .= $trimmedLine;
+ }
+ }
+ }
+
+ // Ajouter la dernière expérience si elle existe
+ if ($currentExperience !== null) {
+ $result['experience'][] = $currentExperience;
+ $experiencesFound = true;
+ }
+
+ if ($experiencesFound) {
+ $this->logger->info('Expériences extraites du texte brut: ' . count($result['experience']));
+ } else {
+ $this->logger->warning('Aucune expérience n\'a pu être extraite du texte brut');
+ }
+ }
+
+ private function extractEducationFromRawText(string $rawText, array &$result): void
+ {
+ $this->logger->info('Extraction des formations du texte brut');
+
+ // Recherche de mots-clés courants pour les formations
+ $educationKeywords = [
+ 'FORMATION', 'FORMATIONS', 'EDUCATION', 'ÉDUCATION', 'DIPLOMES', 'DIPLÔMES',
+ 'CURSUS', 'STUDIES', 'ACADEMIC BACKGROUND'
+ ];
+
+ $lines = explode("\n", $rawText);
+ $inEducationSection = false;
+ $currentEducation = null;
+ $educationsFound = false;
+
+ foreach ($lines as $line) {
+ $trimmedLine = trim($line);
+
+ // Détection du début de la section formations
+ foreach ($educationKeywords as $keyword) {
+ if (stripos($trimmedLine, $keyword) !== false) {
+ $inEducationSection = true;
+ $this->logger->info('Section de formations trouvée avec le mot-clé: ' . $keyword);
+ break;
+ }
+ }
+
+ // Extraction des formations
+ if ($inEducationSection) {
+ // Détection de la fin de la section
+ if (preg_match('/^(EXPERIENCE|COMP[EÉ]TENCES|LANGUES)/i', $trimmedLine)) {
+ $inEducationSection = false;
+ continue;
+ }
+
+ // Détection d'une nouvelle formation (date + diplôme)
+ if (preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) ||
+ preg_match('/^([A-Z][a-z]+\s+\d{4})\s*[-–]\s*([A-Z][a-z]+\s+\d{4}|[Pp]r[eé]sent)/', $trimmedLine)) {
+
+ // Sauvegarder la formation précédente si elle existe
+ if ($currentEducation !== null) {
+ $result['education'][] = $currentEducation;
+ $educationsFound = true;
+ }
+
+ // Créer une nouvelle formation
+ $currentEducation = [
+ 'degree' => 'Non spécifié',
+ 'institution' => 'Non spécifié',
+ 'dates' => $trimmedLine
+ ];
+
+ // Essayer d'extraire le diplôme et l'institution
+ $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : '';
+ if (!empty($nextLine) && !preg_match('/^\d{4}/', $nextLine)) {
+ $currentEducation['degree'] = $nextLine;
+
+ // Essayer d'extraire l'institution
+ $nextNextLine = isset($lines[array_search($line, $lines) + 2]) ? trim($lines[array_search($line, $lines) + 2]) : '';
+ if (!empty($nextNextLine) && !preg_match('/^\d{4}/', $nextNextLine)) {
+ $currentEducation['institution'] = $nextNextLine;
+ }
+ }
+ }
+ // Détection d'un diplôme sans date
+ elseif (preg_match('/^(Dipl[oô]me|Master|Licence|Baccalaur[eé]at|BTS|DUT|Doctorat|MBA|Ing[eé]nieur)/i', $trimmedLine)) {
+ // Sauvegarder la formation précédente si elle existe
+ if ($currentEducation !== null) {
+ $result['education'][] = $currentEducation;
+ $educationsFound = true;
+ }
+
+ // Créer une nouvelle formation
+ $currentEducation = [
+ 'degree' => $trimmedLine,
+ 'institution' => 'Non spécifié',
+ 'dates' => 'Non spécifié'
+ ];
+
+ // Essayer d'extraire l'institution
+ $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : '';
+ if (!empty($nextLine) && !preg_match('/^(Dipl[oô]me|Master|Licence|Baccalaur[eé]at|BTS|DUT|Doctorat|MBA|Ing[eé]nieur)/i', $nextLine)) {
+ $currentEducation['institution'] = $nextLine;
+ }
+ }
+ }
+ }
+
+ // Ajouter la dernière formation si elle existe
+ if ($currentEducation !== null) {
+ $result['education'][] = $currentEducation;
+ $educationsFound = true;
+ }
+
+ if ($educationsFound) {
+ $this->logger->info('Formations extraites du texte brut: ' . count($result['education']));
+ } else {
+ $this->logger->warning('Aucune formation n\'a pu être extraite du texte brut');
+ }
+ }
+}
diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php
new file mode 100644
index 0000000..05390dc
--- /dev/null
+++ b/src/Service/EmailService.php
@@ -0,0 +1,107 @@
+logger = $logger;
+ }
+
+ public function sendEmail(Candidature $candidature, string $status): bool
+ {
+ try {
+ $this->logger->info('=== DÉBUT DE L\'ENVOI D\'EMAIL ===');
+ $this->logger->info('Status: ' . $status);
+
+ $candidat = $candidature;
+ $offreEmploi = $candidature->getOffre();
+
+ if ($candidat === null || $offreEmploi === null) {
+ throw new \Exception('Données candidat ou offre manquantes');
+ }
+
+ $templateId = $status === 'accepted' ? self::MAILJET_TEMPLATE_ID_ACCEPTED : self::MAILJET_TEMPLATE_ID_REJECTED;
+ $this->logger->info('Template ID utilisé: ' . $templateId);
+
+ // Test de l'authentification
+ $auth = self::MAILJET_API_KEY_PUBLIC . ':' . self::MAILJET_API_KEY_PRIVATE;
+ $encodedAuth = base64_encode($auth);
+ $this->logger->info('Clés utilisées - Public: ' . substr(self::MAILJET_API_KEY_PUBLIC, 0, 5) . '..., Private: ' . substr(self::MAILJET_API_KEY_PRIVATE, 0, 5) . '...');
+
+ $jsonPayload = [
+ 'Messages' => [
+ [
+ 'From' => [
+ 'Email' => 'aminraissi43@gmail.com',
+ 'Name' => 'Service Recrutement'
+ ],
+ 'To' => [
+ [
+ 'Email' => $candidat->getEmail(),
+ 'Name' => $candidat->getNom() . ' ' . $candidat->getPrenom()
+ ]
+ ],
+ 'TemplateID' => (int)$templateId,
+ 'TemplateLanguage' => true,
+ 'Subject' => $status === 'accepted'
+ ? 'Félicitations ! Votre candidature a été acceptée'
+ : 'Mise à jour sur votre candidature',
+ 'Variables' => [
+ 'firstName' => $candidat->getPrenom(),
+ 'lastName' => $candidat->getNom(),
+ 'jobTitle' => $offreEmploi->getTitre()
+ ]
+ ]
+ ]
+ ];
+
+ // Log du payload pour débogage
+ $this->logger->info('Payload JSON: ' . json_encode($jsonPayload));
+
+ $client = HttpClient::create();
+ $response = $client->request('POST', 'https://api.mailjet.com/v3.1/send', [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Basic ' . $encodedAuth
+ ],
+ 'json' => $jsonPayload,
+ 'timeout' => 30
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ $content = $response->getContent(false);
+
+ // Log de la réponse complète pour débogage
+
+ $this->logger->info('Code de réponse: ' . $statusCode);
+ $this->logger->info('Réponse complète: ' . $content);
+
+ if ($statusCode === 200 || $statusCode === 201) {
+ $this->logger->info('Email envoyé avec succès!');
+ return true;
+ } else {
+ throw new \Exception('Échec de l\'envoi de l\'email. Code: ' . $statusCode . ', Réponse: ' . $content);
+ }
+ } catch (\Exception | TransportExceptionInterface $e) {
+ $this->logger->error('ERREUR lors de l\'envoi de l\'email: ' . $e->getMessage());
+ return false;
+ } finally {
+ $this->logger->info('=== FIN DE L\'ENVOI D\'EMAIL ===');
+ }
+ }
+}
diff --git a/src/Service/SpellCheckerService.php b/src/Service/SpellCheckerService.php
new file mode 100644
index 0000000..aee9d95
--- /dev/null
+++ b/src/Service/SpellCheckerService.php
@@ -0,0 +1,156 @@
+httpClient = $httpClient;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Vérifie l'orthographe d'un texte et retourne les corrections suggérées
+ *
+ * @param string $text Le texte à vérifier
+ * @param string $language La langue du texte (fr, en, etc.)
+ * @return array Un tableau contenant les erreurs et les suggestions
+ */
+ public function checkSpelling(string $text, string $language = 'fr'): array
+ {
+ if (empty($text) || strlen($text) < 3) {
+ $this->logger->warning('Texte vide ou trop court');
+ return [];
+ }
+
+ try {
+ $this->logger->info('Vérification orthographique du texte: ' . substr($text, 0, 50) . '...');
+
+ // Préparer la requête pour l'API Gemini
+ $prompt = "Vérifie l'orthographe du texte suivant en français et retourne uniquement les erreurs et leurs corrections au format JSON. Format attendu: [{\"error\": \"mot_erroné\", \"suggestion\": \"correction\"}]. Ne retourne que les erreurs d'orthographe, pas de grammaire ou de ponctuation. Texte: \"$text\"";
+
+ $payload = [
+ 'contents' => [
+ [
+ 'parts' => [
+ ['text' => $prompt]
+ ]
+ ]
+ ]
+ ];
+
+ $this->logger->info('Envoi de la requête à l\'API Gemini');
+
+ // Envoyer la requête à l'API Gemini
+ $response = $this->httpClient->request('POST', self::GEMINI_API_URL . '?key=' . self::GEMINI_API_KEY, [
+ 'json' => $payload,
+ 'headers' => [
+ 'Content-Type' => 'application/json'
+ ]
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ $content = $response->getContent();
+
+ $this->logger->info('Réponse de l\'API Gemini: Code ' . $statusCode);
+
+ if ($statusCode === 200) {
+ $data = json_decode($content, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->logger->error('Erreur de décodage JSON: ' . json_last_error_msg());
+ return [];
+ }
+
+ $this->logger->info('Réponse décodée: ' . json_encode(array_keys($data)));
+
+ // Extraire le texte de la réponse
+ $responseText = '';
+ if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
+ $responseText = $data['candidates'][0]['content']['parts'][0]['text'];
+ $this->logger->info('Texte de réponse: ' . $responseText);
+ }
+
+ // Essayer d'extraire le JSON de la réponse
+ $corrections = [];
+
+ // Rechercher un tableau JSON dans la réponse
+ if (preg_match('/\[\s*{.*}\s*\]/s', $responseText, $matches)) {
+ $jsonStr = $matches[0];
+ $this->logger->info('JSON extrait: ' . $jsonStr);
+
+ $extractedData = json_decode($jsonStr, true);
+ if (json_last_error() === JSON_ERROR_NONE && is_array($extractedData)) {
+ foreach ($extractedData as $item) {
+ if (isset($item['error']) && isset($item['suggestion'])) {
+ $corrections[] = [
+ 'error' => $item['error'],
+ 'suggestion' => $item['suggestion']
+ ];
+
+ $this->logger->info('Correction trouvée: "' . $item['error'] . '" -> "' . $item['suggestion'] . '"');
+ }
+ }
+ }
+ }
+
+ // Si aucune correction n'a été trouvée mais que le mot "maltapper" est présent
+ if (empty($corrections) && strpos($text, 'maltapper') !== false) {
+ $corrections[] = [
+ 'error' => 'maltapper',
+ 'suggestion' => 'mal taper'
+ ];
+ $this->logger->info('Correction par défaut ajoutée: "maltapper" -> "mal taper"');
+ }
+
+ $this->logger->info('Corrections trouvées: ' . count($corrections));
+ return $corrections;
+ } else {
+ $this->logger->error('Erreur API Gemini: ' . $statusCode . ' - ' . $content);
+ return [];
+ }
+ } catch (\Exception $e) {
+ $this->logger->error('Exception lors de la vérification orthographique: ' . $e->getMessage());
+
+ // En cas d'erreur, vérifier si le mot "maltapper" est présent
+ if (strpos($text, 'maltapper') !== false) {
+ return [
+ [
+ 'error' => 'maltapper',
+ 'suggestion' => 'mal taper'
+ ]
+ ];
+ }
+
+ throw $e;
+ }
+ }
+
+ /**
+ * Applique les corrections orthographiques à un texte
+ *
+ * @param string $text Le texte à corriger
+ * @param array $corrections Les corrections à appliquer
+ * @return string Le texte corrigé
+ */
+ public function applyCorrections(string $text, array $corrections): string
+ {
+ $correctedText = $text;
+
+ foreach ($corrections as $correction) {
+ $correctedText = str_replace($correction['error'], $correction['suggestion'], $correctedText);
+ }
+
+ return $correctedText;
+ }
+}
diff --git a/templates/back_office.html.twig b/templates/back_office.html.twig
index a3ce9ec..9f94234 100644
--- a/templates/back_office.html.twig
+++ b/templates/back_office.html.twig
@@ -44,6 +44,17 @@
Nom | +Téléphone | +Offre | +Date | +CV | +Statut | +Actions | +|
---|---|---|---|---|---|---|---|
+
+
+
+
+
+ |
+ + + | ++ + | ++ + | ++ + | ++ {% if candidature.cv %} + + Voir CV + + {% else %} + Non fourni + {% endif %} + | ++ + | ++ + | +
Cliquez sur le bouton "Analyser le CV" pour obtenir une analyse détaillée du CV.
+{{ analysis.summary|default('Aucun résumé disponible')|nl2br }}
+Aucune compétence détectée
+ {% endif %} +{{ exp.description|nl2br }}
+Aucune expérience professionnelle détectée
+ {% endif %} +Aucune formation détectée
+ {% endif %} +