src/Controller/CandidatureController.php line 115

  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Admis;
  4. use App\Entity\Candidature;
  5. use App\Entity\Metier;
  6. use App\Entity\Etablissement;
  7. use App\Entity\User;
  8. use App\Entity\EtablissementMetier;
  9. use App\Entity\Prospection;
  10. use App\Entity\ProspectionMetier;
  11. use App\Form\CandidatureType;
  12. use App\Service\FileUploader;
  13. use App\Service\PdfGenerator;
  14. use App\Service\SpreadsheetGenerator;
  15. use App\Service\Constant;
  16. use Doctrine\ORM\EntityManagerInterface;
  17. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\HttpFoundation\JsonResponse;
  21. use Symfony\Component\Routing\Annotation\Route;
  22. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  23. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  24. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  25. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  26. #[Route('/candidature')]
  27. class CandidatureController extends AbstractController
  28. {
  29.     private const STATUT_LABELS = [
  30.         => 'Accepté',
  31.         => 'Rejeté',
  32.         'default' => 'En attente'
  33.     ];
  34.     public function __construct(
  35.         private readonly EntityManagerInterface $entityManager,
  36.         private readonly UserPasswordHasherInterface $passwordHasher,
  37.         private readonly FileUploader $fileUploader,
  38.         private readonly PdfGenerator $pdfGenerator,
  39.         private readonly SpreadsheetGenerator $spreadsheetGenerator,
  40.         private readonly ParameterBagInterface $params,
  41.         private readonly Constant $constant
  42.     ) {}
  43.     #[Route('/'name'app_candidature_index'methods: ['GET'])]
  44.     #[IsGranted('ROLE_USER')]
  45.     public function index(): Response
  46.     {
  47.         $user $this->getUser();
  48.         $qb $this->entityManager->getRepository(Candidature::class)
  49.             ->createQueryBuilder('c')
  50.             ->leftJoin('c.etablissement''e')
  51.             ->leftJoin('c.metier''m')
  52.             ->addSelect('e''m');
  53.         // ADMIN : toutes les candidatures
  54.         if ($this->isGranted('ROLE_ADMIN')) {
  55.             $qb->leftJoin('c.user''u')->addSelect('u');
  56.             $is_admin true;
  57.             // ENT : mêmes colonnes/actions qu'admin mais filtre par établissement
  58.         } elseif ($this->isGranted('ROLE_ENT')) {
  59.             $qb->leftJoin('c.user''u')->addSelect('u')
  60.                 ->where('e.user = :user')  // Filtre sur l'établissement de l'utilisateur ENT
  61.                 ->setParameter('user'$user);
  62.             $is_admin true;
  63.             // JURY : mêmes droits que ENT pour l'affichage (voit les candidatures de son établissement)
  64.         } elseif ($this->isGranted('ROLE_JURY')) {
  65.             $qb->leftJoin('c.user''u')->addSelect('u')
  66.                 ->where('e.user = :user')
  67.                 ->setParameter('user'$user);
  68.             $is_admin true;
  69.             // CANDIDAT : ses propres candidatures
  70.         } else {
  71.             $qb->where('c.user = :user')
  72.                 ->setParameter('user'$user);
  73.             $is_admin false;
  74.         }
  75.         $candidatures $qb->orderBy('c.id''DESC')->getQuery()->getResult();
  76.         return $this->render('candidature/index.html.twig', [
  77.             'candidatures' => $candidatures,
  78.             'is_admin' => $is_admin,
  79.         ]);
  80.     }
  81.     #[Route('/new'name'app_candidature_new'methods: ['GET''POST'])]
  82.     public function new(Request $request): Response
  83.     {
  84.         // Bloquer les utilisateurs ENT
  85.         if ($this->isGranted('ROLE_ENT')) {
  86.             $this->addFlash(
  87.                 'error',
  88.                 'Vous êtes connecté comme établissement. <a href="' $this->generateUrl('app_logout') . '" class="underline font-semibold">Se déconnecter</a> pour créer une candidature.'
  89.             );
  90.             return $this->redirectToRoute('app_candidature_index');
  91.         }
  92.         if ($this->getUser() && !$this->canSubmitCandidature($this->getUser())) {
  93.             $this->addFlash('error''Vous avez déjà soumis une candidature.');
  94.             return $this->redirectToRoute('app_candidature_index');
  95.         }
  96.         $candidature $this->createCandidature($this->getUser());
  97.         $this->prefillFromRequest($candidature$request);
  98.         $form $this->createForm(CandidatureType::class, $candidature);
  99.         $form->handleRequest($request);
  100.         if ($form->isSubmitted() && $form->isValid()) {
  101.             if (!$this->canSubmitCandidature($this->getUser())) {
  102.                 $this->addFlash('error''Vous avez déjà soumis une candidature.');
  103.                 return $this->redirectToRoute('app_candidature_index');
  104.             }
  105.             // Validation : vérifier que le métier est autorisé pour l'établissement
  106.             $etab $candidature->getEtablissement();
  107.             $met $candidature->getMetier();
  108.             if ($etab && $met) {
  109.                 $emExists $this->entityManager->getRepository(EtablissementMetier::class)
  110.                     ->findOneBy(['etablissement' => $etab'metier' => $met]);
  111.                 if (!$emExists) {
  112.                     $this->addFlash('error''Le métier sélectionné n\'est pas ouvert dans cet établissement.');
  113.                     return $this->render('candidature/form.html.twig', [
  114.                         'candidature' => $candidature,
  115.                         'form' => $form,
  116.                         'document_labels' => $this->constant->document_labels,
  117.                         'candidaturesCount' => $this->getUser() ? $this->countUserCandidatures($this->getUser()) : 0
  118.                     ]);
  119.                 }
  120.             }
  121.             $candidature->setNumero($this->generateNumero());
  122.             if ($this->processCandidatureFiles($candidature$form)) {
  123.                 $this->entityManager->persist($candidature);
  124.                 $this->entityManager->flush();
  125.                 return $this->redirectToRoute('app_candidature_success', ['id' => $candidature->getId()]);
  126.             }
  127.         }
  128.         $this->displayFormErrors($form);
  129.         return $this->render('candidature/form.html.twig', [
  130.             'candidature' => $candidature,
  131.             'form' => $form,
  132.             'document_labels' => $this->constant->document_labels,
  133.             'candidaturesCount' => $this->getUser() ? $this->countUserCandidatures($this->getUser()) : 0
  134.         ]);
  135.     }
  136.     #[Route('/success/{id}'name'app_candidature_success'requirements: ['id' => '\\d+'])]
  137.     #[Security("is_granted('ROLE_CANDIDAT') and user === candidature.getUser() or is_granted('ROLE_ADMIN')")]
  138.     public function success(Candidature $candidature): Response
  139.     {
  140.         return $this->render('candidature/success.html.twig', ['candidature' => $candidature]);
  141.     }
  142.     #[Route('/{id}/edit'name'app_candidature_edit'methods: ['GET''POST'], requirements: ['id' => '\\d+'])]
  143.     #[Security("(is_granted('ROLE_ADMIN') or (is_granted('ROLE_ENT') and candidature.getEtablissement() and candidature.getEtablissement().getUser() == user))")]
  144.     public function edit(Request $requestCandidature $candidature): Response
  145.     {
  146.         $form $this->createForm(CandidatureType::class, $candidature, ['evaluation_only' => true]);
  147.         $form->handleRequest($request);
  148.         if ($form->isSubmitted() && $form->isValid()) {
  149.             $candidature->setModification(new \DateTime());
  150.             $this->entityManager->flush();
  151.             $this->addFlash('success''Évaluation mise à jour avec succès.');
  152.             return $this->redirectToRoute('app_candidature_index');
  153.         }
  154.         return $this->render('candidature/form.html.twig', [
  155.             'candidature' => $candidature,
  156.             'form' => $form,
  157.             'document_labels' => $this->constant->document_labels
  158.         ]);
  159.     }
  160.     #[Route('/{id}/update-info'name'app_candidature_update_info'methods: ['POST'], requirements: ['id' => '\\d+'])]
  161.     #[Security("(is_granted('ROLE_ADMIN') or (is_granted('ROLE_ENT') and candidature.getEtablissement() and candidature.getEtablissement().getUser() == user))")]
  162.     public function updateInfo(Request $requestCandidature $candidature): Response
  163.     {
  164.         if (!$this->isCsrfTokenValid('candidature_update_info_' $candidature->getId(), $request->request->get('_token'))) {
  165.             $this->addFlash('error''Token de sécurité invalide.');
  166.             return $this->redirectToRoute('app_candidature_edit', ['id' => $candidature->getId()]);
  167.         }
  168.         // Mettre à jour nom/prénoms/contact du candidat associé
  169.         $candidatUser $candidature->getUser();
  170.         if ($candidatUser) {
  171.             $nom trim($request->request->get('nom'''));
  172.             $prenoms trim($request->request->get('prenoms'''));
  173.             $contact trim($request->request->get('contact'''));
  174.             if ($nom !== '') {
  175.                 $candidatUser->setNom($nom);
  176.             }
  177.             if ($prenoms !== '') {
  178.                 $candidatUser->setPrenoms($prenoms);
  179.             }
  180.             $candidatUser->setContact($contact !== '' $contact null);
  181.         }
  182.         // Mettre à jour le métier
  183.         $metierId $request->request->get('metier_id');
  184.         if ($metierId) {
  185.             $metier $this->entityManager->getRepository(Metier::class)->find($metierId);
  186.             if ($metier) {
  187.                 $emExists $this->entityManager->getRepository(EtablissementMetier::class)
  188.                     ->findOneBy(['etablissement' => $candidature->getEtablissement(), 'metier' => $metier]);
  189.                 if ($emExists) {
  190.                     $candidature->setMetier($metier);
  191.                 } else {
  192.                     $this->addFlash('error'"Le métier sélectionné n'est pas ouvert dans cet établissement.");
  193.                     return $this->redirectToRoute('app_candidature_edit', ['id' => $candidature->getId()]);
  194.                 }
  195.             }
  196.         }
  197.         // Traiter les fichiers uploadés
  198.         $dir $this->params->get('dir_media') . $candidature->getNumero() . '/';
  199.         $this->fileUploader->mkdir($dir);
  200.         foreach (['fphoto''fpiece''fextrait''fniveau''fautre'] as $field) {
  201.             $file $request->files->get($field);
  202.             if ($file) {
  203.                 $getter 'get' ucfirst($field);
  204.                 $setter 'set' ucfirst($field);
  205.                 $oldFile $candidature->$getter();
  206.                 $fileName $this->fileUploader->upload($file$dir$oldFile);
  207.                 $candidature->$setter($fileName);
  208.             }
  209.         }
  210.         $candidature->setModification(new \DateTime());
  211.         $this->entityManager->flush();
  212.         $this->addFlash('success''Informations mises à jour avec succès.');
  213.         return $this->redirectToRoute('app_candidature_edit', ['id' => $candidature->getId()]);
  214.     }
  215.     #[Route('/{id}'name'app_candidature_delete'methods: ['POST'], requirements: ['id' => '\\d+'])]
  216.     #[Security("(is_granted('ROLE_CANDIDAT') and user === candidature.getUser()) or is_granted('ROLE_ADMIN')")]
  217.     public function delete(Request $requestCandidature $candidature): Response
  218.     {
  219.         if ($this->isCsrfTokenValid('delete' $candidature->getId(), $request->request->get('_token'))) {
  220.             $this->deleteCandidature($candidature);
  221.             $this->addFlash('success''Candidature supprimée avec succès.');
  222.         }
  223.         return $this->redirectToRoute('app_candidature_index');
  224.     }
  225.     // ==================== ROUTES D'IMPRESSION ET STATISTIQUES ====================
  226.     #[Route('/print'name'app_candidature_print'methods: ['GET'])]
  227.     public function print(Request $request): Response
  228.     {
  229.         $type $request->query->get('type');
  230.         $numero $request->query->get('numero');
  231.         if ($type === 'stat') {
  232.             return $this->printStatistics($request);
  233.         }
  234.         if (!$numero) {
  235.             $this->addFlash('error''Numéro de candidature manquant');
  236.             return $this->redirectToRoute('app_candidature_impressions');
  237.         }
  238.         $candidature $this->entityManager->getRepository(Candidature::class)
  239.             ->findOneBy(['numero' => trim($numero)]);
  240.         if (!$candidature) {
  241.             $this->addFlash('error''Candidature non trouvée');
  242.             return $this->redirectToRoute('app_candidature_impressions');
  243.         }
  244.         return match ($type) {
  245.             'fiche' => $this->generateFichePdf($candidature),
  246.             'convocation' => $candidature->getEntstatut() === 2
  247.                 $this->generateConvocationPdf($candidature)
  248.                 : $this->handleInvalidConvocation(),
  249.             default => $this->redirectToRoute('app_candidature_impressions')
  250.         };
  251.     }
  252.     #[Route('/stat'name'app_candidature_stat')]
  253.     #[Security("is_granted('ROLE_ADMIN') or is_granted('ROLE_ENT') or is_granted('ROLE_JURY') or is_granted('ROLE_COM')")]
  254.     public function stat(Request $request): Response
  255.     {
  256.         $user $this->getUser();
  257.         $etablissementId $request->query->get('etablissement');
  258.         $metierId $request->query->get('metier');
  259.         // --- SÉCURITÉ ET FILTRAGE AUTOMATIQUE ---
  260.         // Si c'est un ENT ou JURY, on force son établissement
  261.         if ($this->isGranted('ROLE_ENT') || $this->isGranted('ROLE_JURY')) {
  262.             $etabLie $this->entityManager->getRepository(Etablissement::class)->findOneBy(['user' => $user]);
  263.             if ($etabLie) {
  264.                 $etablissementId $etabLie->getId();
  265.             }
  266.         }
  267.         $etablissement $etablissementId $this->entityManager->getRepository(Etablissement::class)->find($etablissementId) : null;
  268.         $metier $metierId $this->entityManager->getRepository(Metier::class)->find($metierId) : null;
  269.         // Pour le filtre : Admin voit tout, l'ENT ne voit que lui-même dans la liste
  270.         if ($this->isGranted('ROLE_ADMIN') || $this->isGranted('ROLE_COM')) {
  271.             $etablissements $this->entityManager->getRepository(Etablissement::class)->findBy([], ['nom' => 'ASC']);
  272.         } else {
  273.             $etablissements $etablissement ? [$etablissement] : [];
  274.         }
  275.         // Récupérer les métiers de l'établissement sélectionné (pour le filtre)
  276.         $metiers = [];
  277.         if ($etablissement) {
  278.             $metiers $this->getMetiersForEtablissement($etablissement);
  279.         }
  280.         // Obtenir les statistiques (sans la contrainte EtablissementMetier)
  281.         $stats $this->getDetailedStatistics($etablissement$metier);
  282.         // Calculer les totaux pour les graphs
  283.         $totals $this->calculateTotals($stats);
  284.         if ($request->query->get('export') === 'true') {
  285.             return $this->exportDetailedStatistics($stats$etablissement$metier);
  286.         }
  287.         // --- Statistiques de prospection ---
  288.         $prospectionTotals $this->getProspectionTotals($etablissement$metier);
  289.         return $this->render('candidature/stat.html.twig', [
  290.             'etablissements' => $etablissements,
  291.             'etablissement_selectionne' => $etablissement,
  292.             'metiers' => $metiers,
  293.             'metier_selectionne' => $metier,
  294.             'stats' => $stats,
  295.             'totals' => $totals,
  296.             'prospection_totals' => $prospectionTotals
  297.         ]);
  298.     }
  299.     #[Route('/impressions'name'app_candidature_impressions')]
  300.     public function impressions(Request $request): Response
  301.     {
  302.         $user $this->getUser();
  303.         // Récupération des métiers et établissements selon le rôle
  304.         if ($this->isGranted('ROLE_ADMIN')) {
  305.             // ADMIN : tous les métiers et tous les établissements
  306.             $metiers $this->entityManager->getRepository(Metier::class)->findAll();
  307.             $etablissements $this->entityManager->getRepository(Etablissement::class)->findAll();
  308.         } elseif ($this->isGranted('ROLE_ENT') || $this->isGranted('ROLE_JURY')) {
  309.             // ENT/JURY : uniquement les métiers de leur établissement
  310.             $etablissement $this->entityManager->getRepository(Etablissement::class)
  311.                 ->findOneBy(['user' => $user]);
  312.             if ($etablissement) {
  313.                 $metiers $this->getMetiersForEtablissement($etablissement);
  314.                 $etablissements = [$etablissement];
  315.             } else {
  316.                 $metiers = [];
  317.                 $etablissements = [];
  318.                 $this->addFlash('warning''Votre compte n\'est pas associé à un établissement.');
  319.             }
  320.         } else {
  321.             // AUTRES (CANDIDAT, etc.)
  322.             $metiers = [];
  323.             $etablissements = [];
  324.         }
  325.         // Traitement des exports POST
  326.         if ($request->isMethod('POST')) {
  327.             $response $this->handleExportActions($request);
  328.             if ($response) return $response;
  329.         }
  330.         return $this->render('candidature/impressions.html.twig', [
  331.             'metiers' => $metiers,
  332.             'etablissements' => $etablissements,
  333.             'codes' => $request->get('codes')
  334.         ]);
  335.     }
  336.     #[Route('/etablissement/{id}/metiers'name'app_etablissement_metiers'methods: ['GET'])]
  337.     public function getMetiersByEtablissement(int $id): JsonResponse
  338.     {
  339.         $etablissement $this->entityManager->getRepository(Etablissement::class)->find($id);
  340.         return $this->json($etablissement $this->getMetiersWithDetails($etablissement) : []);
  341.     }
  342.     private function getMetiersForEtablissement(Etablissement $etablissement): array
  343.     {
  344.         return $this->entityManager->getRepository(Metier::class)
  345.             ->createQueryBuilder('m')
  346.             ->innerJoin('m.etablissementMetiers''em')
  347.             ->where('em.etablissement = :etablissement')
  348.             ->setParameter('etablissement'$etablissement)
  349.             ->orderBy('m.nom''ASC')
  350.             ->getQuery()
  351.             ->getResult();
  352.     }
  353.     private function getDetailedStatistics(?Etablissement $etablissement null, ?Metier $metier null): array
  354.     {
  355.         $qb $this->entityManager->getRepository(Candidature::class)
  356.             ->createQueryBuilder('c')
  357.             ->innerJoin('c.etablissement''e')
  358.             ->innerJoin('c.metier''m')
  359.             ->leftJoin('c.user''u')
  360.             ->select(
  361.                 'm.id as metier_id',
  362.                 'm.nom as metier_nom',
  363.                 'e.id as etablissement_id',
  364.                 'e.nom as etablissement_nom',
  365.                 'COUNT(c.id) as total',
  366.                 'SUM(CASE WHEN c.etustatut = 2 THEN 1 ELSE 0 END) as eligible',
  367.                 'SUM(CASE WHEN c.entstatut IS NOT NULL OR c.resultat IS NOT NULL THEN 1 ELSE 0 END) as evalue',
  368.                 'SUM(CASE WHEN c.entstatut = 2 THEN 1 ELSE 0 END) as admissible',
  369.                 'SUM(CASE WHEN c.resultat = 2 THEN 1 ELSE 0 END) as admis',
  370.                 'SUM(CASE WHEN u.sexe = :homme THEN 1 ELSE 0 END) as hommes',
  371.                 'SUM(CASE WHEN u.sexe = :femme THEN 1 ELSE 0 END) as femmes'
  372.             )
  373.             ->setParameter('homme''MASCULIN')
  374.             ->setParameter('femme''FEMININ')
  375.             ->groupBy('e.id, m.id')
  376.             ->orderBy('e.nom, m.nom');
  377.         if ($etablissement) {
  378.             $qb->andWhere('c.etablissement = :etablissement')
  379.                 ->setParameter('etablissement'$etablissement);
  380.         }
  381.         if ($metier) {
  382.             $qb->andWhere('c.metier = :metier')
  383.                 ->setParameter('metier'$metier);
  384.         }
  385.         return $qb->getQuery()->getResult();
  386.     }
  387.     private function calculateTotals(array $stats): array
  388.     {
  389.         $totals = [
  390.             'total' => 0,
  391.             'eligible' => 0,
  392.             'evalue' => 0,
  393.             'admissible' => 0,
  394.             'admis' => 0,
  395.             'hommes' => 0,
  396.             'femmes' => 0
  397.         ];
  398.         foreach ($stats as $stat) {
  399.             $totals['total'] += $stat['total'];
  400.             $totals['eligible'] += $stat['eligible'];
  401.             $totals['evalue'] += $stat['evalue'];
  402.             $totals['admissible'] += $stat['admissible'];
  403.             $totals['admis'] += $stat['admis'];
  404.             $totals['hommes'] += $stat['hommes'];
  405.             $totals['femmes'] += $stat['femmes'];
  406.         }
  407.         // Calculer les non-évalués
  408.         $totals['non_evalue'] = $totals['total'] - $totals['evalue'];
  409.         $totals['non_eligible'] = $totals['total'] - $totals['eligible'];
  410.         return $totals;
  411.     }
  412.     private function getProspectionTotals(?Etablissement $etablissement null, ?Metier $metier null): array
  413.     {
  414.         $totals = ['nb_entreprises' => 0'total_postes' => 0];
  415.         $entreprisesQb $this->entityManager->getRepository(Prospection::class)
  416.             ->createQueryBuilder('p')
  417.             ->select('COUNT(DISTINCT p.id)');
  418.         if ($etablissement) {
  419.             $entreprisesQb->andWhere('p.etablissement = :etab')->setParameter('etab'$etablissement);
  420.         }
  421.         $totals['nb_entreprises'] = (int) $entreprisesQb->getQuery()->getSingleScalarResult();
  422.         $postesQb $this->entityManager->getRepository(ProspectionMetier::class)
  423.             ->createQueryBuilder('pm')
  424.             ->innerJoin('pm.prospection''p')
  425.             ->select('SUM(pm.nombrePostes)');
  426.         if ($etablissement) {
  427.             $postesQb->andWhere('p.etablissement = :etab')->setParameter('etab'$etablissement);
  428.         }
  429.         if ($metier) {
  430.             $postesQb->andWhere('pm.metier = :metier')->setParameter('metier'$metier);
  431.         }
  432.         $totals['total_postes'] = (int) $postesQb->getQuery()->getSingleScalarResult();
  433.         return $totals;
  434.     }
  435.     private function exportDetailedStatistics(array $stats, ?Etablissement $etablissement, ?Metier $metier): Response
  436.     {
  437.         $headers = [
  438.             'Établissement',
  439.             'Métier',
  440.             'Total',
  441.             'Hommes',
  442.             'Femmes',
  443.             'Éligibles',
  444.             'Évalués',
  445.             'Admissibles',
  446.             'Admis'
  447.         ];
  448.         $datas = [];
  449.         foreach ($stats as $stat) {
  450.             $datas[] = [
  451.                 $stat['etablissement_nom'],
  452.                 $stat['metier_nom'],
  453.                 $stat['total'],
  454.                 $stat['hommes'],
  455.                 $stat['femmes'],
  456.                 $stat['eligible'],
  457.                 $stat['evalue'],
  458.                 $stat['admissible'],
  459.                 $stat['admis']
  460.             ];
  461.         }
  462.         $filename 'STATISTIQUES';
  463.         if ($etablissement$filename .= '_' $etablissement->getNom();
  464.         if ($metier$filename .= '_' $metier->getNom();
  465.         $filename .= '_' date('Ymd') . '.xlsx';
  466.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  467.     }
  468.     #[Route('/stat/etablissement/{id}/metiers'name'app_stat_etablissement_metiers'methods: ['GET'])]
  469.     public function getMetiersForStat(int $id): JsonResponse
  470.     {
  471.         $etablissement $this->entityManager->getRepository(Etablissement::class)->find($id);
  472.         if (!$etablissement) {
  473.             return $this->json([]);
  474.         }
  475.         $metiers $this->getMetiersForEtablissement($etablissement);
  476.         return $this->json(array_map(fn($m) => [
  477.             'id' => $m->getId(),
  478.             'nom' => $m->getNom()
  479.         ], $metiers));
  480.     }
  481.     // ==================== MÉTHODES PRIVÉES ====================
  482.     private function canSubmitCandidature(?User $user): bool
  483.     {
  484.         return $user && $this->countUserCandidatures($user) < 1;
  485.     }
  486.     private function countUserCandidatures(User $user): int
  487.     {
  488.         return $this->entityManager->getRepository(Candidature::class)->count(['user' => $user]);
  489.     }
  490.     private function createCandidature(?User $user): Candidature
  491.     {
  492.         $candidature = new Candidature();
  493.         if ($user$candidature->setUser($user);
  494.         $candidature->setCreation(new \DateTime());
  495.         return $candidature;
  496.     }
  497.     private function prefillFromRequest(Candidature $candidatureRequest $request): void
  498.     {
  499.         if ($metierId $request->query->get('metier')) {
  500.             $metier $this->entityManager->getRepository(Metier::class)->find($metierId);
  501.             if ($metier$candidature->setMetier($metier);
  502.         }
  503.         $etablissementId $request->query->get('etablissement') ?: HomeController::ETABLISSEMENT_ACTIF_ID;
  504.         $etablissement $this->entityManager->getRepository(Etablissement::class)->find($etablissementId);
  505.         if ($etablissement$candidature->setEtablissement($etablissement);
  506.     }
  507.     private function processCandidatureFiles(Candidature $candidature$form): bool
  508.     {
  509.         $missingFiles = [];
  510.         $fichiers = [];
  511.         foreach (array_keys($this->constant->document_labels) as $field) {
  512.             $file $form->get($field)->getData();
  513.             $fichiers[$field] = $file;
  514.             $getter 'get' ucfirst($field);
  515.             $existingFile method_exists($candidature$getter) ? $candidature->$getter() : null;
  516.             if (($this->constant->document_labels[$field]['required'] ?? false) && !$file && !$existingFile) {
  517.                 $missingFiles[] = $this->constant->document_labels[$field]['text'] ?? $field;
  518.             }
  519.         }
  520.         if ($missingFiles) {
  521.             $this->addFlash('error''Documents requis : ' implode(', '$missingFiles));
  522.             return false;
  523.         }
  524.         $this->handleUploadedFiles($candidature$fichiers);
  525.         return true;
  526.     }
  527.     private function validateRequiredDocuments(Candidature $candidature$form): bool
  528.     {
  529.         $missingFiles = [];
  530.         foreach (array_keys($this->constant->document_labels) as $field) {
  531.             $file $form->get($field)->getData();
  532.             $getter 'get' ucfirst($field);
  533.             $existingFile method_exists($candidature$getter) ? $candidature->$getter() : null;
  534.             if (($this->constant->document_labels[$field]['required'] ?? false) && !$file && !$existingFile) {
  535.                 $missingFiles[] = $this->constant->document_labels[$field]['text'] ?? $field;
  536.             }
  537.         }
  538.         if ($missingFiles) {
  539.             $this->addFlash('error''Documents requis : ' implode(', '$missingFiles));
  540.             return false;
  541.         }
  542.         return true;
  543.     }
  544.     private function displayFormErrors($form): void
  545.     {
  546.         if ($form->isSubmitted() && !$form->isValid()) {
  547.             foreach ($form->getErrors(true) as $error) {
  548.                 $this->addFlash('error'$error->getMessage());
  549.             }
  550.         }
  551.     }
  552.     private function handleUploadedFiles(Candidature $candidature, array $fichiers): void
  553.     {
  554.         $dir $this->params->get('dir_media') . $candidature->getNumero() . '/';
  555.         $this->fileUploader->mkdir($dir);
  556.         foreach (['fphoto''fpiece''fextrait''fniveau''fautre'] as $field) {
  557.             $file $fichiers[$field] ?? null;
  558.             if ($file) {
  559.                 $getter 'get' ucfirst($field);
  560.                 $setter 'set' ucfirst($field);
  561.                 $oldFile $candidature->$getter();
  562.                 $fileName $this->fileUploader->upload($file$dir$oldFile);
  563.                 $candidature->$setter($fileName);
  564.             }
  565.         }
  566.     }
  567.     private function generateNumero(): string
  568.     {
  569.         $last $this->entityManager->getRepository(Candidature::class)->findOneBy([], ['id' => 'DESC']);
  570.         $nextId $last $last->getId() + 1;
  571.         // Format: 2600001A (année sur 2 chiffres + numéro sur 5 chiffres + lettre aléatoire)
  572.         $year substr(date('Y'), -2); // 26 au lieu de 2026
  573.         $letters 'ABCDEFGHIJKLMNPQRSTUVWXYZ'// Sans O pour éviter confusion avec 0
  574.         $letter $letters[random_int(0strlen($letters) - 1)];
  575.         return $year str_pad($nextId5'0'STR_PAD_LEFT) . $letter;
  576.     }
  577.     private function deleteCandidature(Candidature $candidature): void
  578.     {
  579.         $this->fileUploader->remove($this->params->get('dir_media') . $candidature->getNumero());
  580.         $this->entityManager->remove($candidature);
  581.         $this->entityManager->flush();
  582.     }
  583.     private function getStatisticsByMetier(?Metier $metier null): array
  584.     {
  585.         $metiers $metier ? [$metier] : $this->entityManager->getRepository(Metier::class)->findAll();
  586.         $results = [];
  587.         foreach ($metiers as $m) {
  588.             $stats $this->calculateMetierStatistics($m);
  589.             $results[] = [
  590.                 'name' => $m->getNom(),
  591.                 'sum_h' => $stats['hommes'],
  592.                 'sum_f' => $stats['femmes'],
  593.                 'sum_pl' => $stats['places'],
  594.                 'sum' => $stats['total'],
  595.                 'sum_evalue' => $stats['evalue'],
  596.                 'sum_admissible' => $stats['admissible'],
  597.                 'sum_admis' => $stats['admis']
  598.             ];
  599.         }
  600.         return $results;
  601.     }
  602.     private function calculateMetierStatistics(Metier $metier): array
  603.     {
  604.         $repo $this->entityManager->getRepository(Candidature::class);
  605.         $candidatures $repo->createQueryBuilder('c')
  606.             ->innerJoin('c.etablissement''e')
  607.             ->leftJoin('c.user''u')
  608.             ->addSelect('u')
  609.             ->where('c.metier = :metier')
  610.             ->setParameter('metier'$metier)
  611.             ->getQuery()
  612.             ->getResult();
  613.         $countH $countF 0;
  614.         foreach ($candidatures as $c) {
  615.             $user $c->getUser();
  616.             if ($user) {
  617.                 $user->getSexe() === 'MASCULIN' $countH++ : $countF++;
  618.             }
  619.         }
  620.         // Évalués (candidatures ayant au moins une note)
  621.         $qb $repo->createQueryBuilder('c')
  622.             ->select('COUNT(c.id)')
  623.             ->where('c.metier = :metier');
  624.         $orConditions = [];
  625.         for ($i 1$i <= 13$i++) {
  626.             $orConditions[] = $qb->expr()->isNotNull('c.note' $i);
  627.         }
  628.         $evalue = (int) $qb->andWhere($qb->expr()->orX(...$orConditions))
  629.             ->setParameter('metier'$metier)
  630.             ->getQuery()
  631.             ->getSingleScalarResult();
  632.         // Places totales (somme des places dans tous les établissements pour ce métier)
  633.         $places array_sum(array_map(
  634.             fn($em) => $em->getNbrplace() ?? 0,
  635.             $this->entityManager->getRepository(EtablissementMetier::class)->findBy(['metier' => $metier])
  636.         ));
  637.         $admissible = (int) $repo->createQueryBuilder('c')
  638.             ->select('COUNT(c.id)')
  639.             ->where('c.metier = :metier')
  640.             ->andWhere('c.entstatut = 2')
  641.             ->setParameter('metier'$metier)
  642.             ->getQuery()
  643.             ->getSingleScalarResult();
  644.         $admis = (int) $repo->createQueryBuilder('c')
  645.             ->select('COUNT(c.id)')
  646.             ->where('c.metier = :metier')
  647.             ->andWhere('c.resultat = 2')
  648.             ->setParameter('metier'$metier)
  649.             ->getQuery()
  650.             ->getSingleScalarResult();
  651.         return [
  652.             'total' => $countH $countF,
  653.             'hommes' => $countH,
  654.             'femmes' => $countF,
  655.             'places' => $places,
  656.             'evalue' => $evalue,
  657.             'admissible' => $admissible,
  658.             'admis' => $admis
  659.         ];
  660.     }
  661.     private function exportStatistics(array $results): Response
  662.     {
  663.         $headers = ['N°''Métier''Postes''Hommes''Femmes''Total''Évalués''Non évalués''Admissibles''Admis'];
  664.         $datas = [];
  665.         foreach ($results as $i => $r) {
  666.             $datas[] = [
  667.                 $i 1,
  668.                 $r['name'],
  669.                 $r['sum_pl'],
  670.                 $r['sum_h'],
  671.                 $r['sum_f'],
  672.                 $r['sum'],
  673.                 $r['sum_evalue'],
  674.                 $r['sum'] - $r['sum_evalue'],
  675.                 $r['sum_admissible'],
  676.                 $r['sum_admis']
  677.             ];
  678.         }
  679.         return $this->spreadsheetGenerator->generate('STATISTIQUES_CANDIDATURES.xlsx'$headers$datas);
  680.     }
  681.     private function getMetiersWithDetails(Etablissement $etablissement): array
  682.     {
  683.         $metiers $this->entityManager->getRepository(Metier::class)
  684.             ->createQueryBuilder('m')
  685.             ->innerJoin('m.etablissementMetiers''em')
  686.             ->innerJoin('m.secteur''s')
  687.             ->where('em.etablissement = :etablissement')
  688.             ->setParameter('etablissement'$etablissement)
  689.             ->orderBy('s.nom''ASC')->addOrderBy('m.nom''ASC')
  690.             ->getQuery()->getResult();
  691.         return array_map(fn($metier) => [
  692.             'id' => $metier->getId(),
  693.             'nom' => $metier->getNom(),
  694.             'secteur_nom' => $metier->getSecteur()->getNom(),
  695.             'nbrplace' => ($em $this->entityManager->getRepository(EtablissementMetier::class)
  696.                 ->findOneBy(['etablissement' => $etablissement'metier' => $metier])) ? $em->getNbrplace() : null,
  697.             'niveau' => $em?->getNiveauRequis(),
  698.             'duree' => $em?->getDureeFormation()
  699.         ], $metiers);
  700.     }
  701.     private function handleExportActions(Request $request): ?Response
  702.     {
  703.         $action $request->request->get('action');
  704.         if ($action === 'export_codes') {
  705.             return $this->exportByCodes($request->request->get('codes'));
  706.         }
  707.         if ($action === 'export_filters') {
  708.             return $this->exportByFilters(
  709.                 $request->request->get('etablissement'),
  710.                 $request->request->get('statut''TOUS'),
  711.                 $request->request->get('metier')
  712.             );
  713.         }
  714.         if ($action === 'export_liste_candidats_inscrits') {
  715.             return $this->exportListeCandidatsInscrits(
  716.                 $request->request->get('etablissement_liste'),
  717.                 $request->request->get('metier_liste')
  718.             );
  719.         }
  720.         if ($action === 'export_liste_admis') {
  721.             return $this->exportListeAdmis(
  722.                 $request->request->get('etablissement_admis'),
  723.                 $request->request->get('metier_admis')
  724.             );
  725.         }
  726.         if ($action === 'export_liste_admis_definitifs') {
  727.             return $this->exportListeAdmisDefinitifs(
  728.                 $request->request->get('etablissement_admis_def'),
  729.                 $request->request->get('metier_admis_def')
  730.             );
  731.         }
  732.         if ($action === 'export_stats_secteur') {
  733.             return $this->exportStatsSecteur();
  734.         }
  735.         if ($action === 'export_stats_region') {
  736.             return $this->exportStatsRegion();
  737.         }
  738.         return null;
  739.     }
  740.     private function exportListeCandidatsInscrits(?string $etablissementId, ?string $metierId): ?Response
  741.     {
  742.         if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_ENT') && !$this->isGranted('ROLE_JURY')) {
  743.             throw $this->createAccessDeniedException('Accès non autorisé à cet export.');
  744.         }
  745.         $user $this->getUser();
  746.         $etablissement null;
  747.         if ($this->isGranted('ROLE_ADMIN')) {
  748.             if (!$etablissementId || $etablissementId === 'TOUS') {
  749.                 $this->addFlash('error''Veuillez sélectionner un établissement.');
  750.                 return $this->redirectToRoute('app_candidature_impressions');
  751.             }
  752.             $etablissement $this->entityManager->getRepository(Etablissement::class)->find((int) $etablissementId);
  753.             if (!$etablissement) {
  754.                 $this->addFlash('error''Établissement invalide.');
  755.                 return $this->redirectToRoute('app_candidature_impressions');
  756.             }
  757.         } else {
  758.             $etablissement $this->entityManager->getRepository(Etablissement::class)->findOneBy(['user' => $user]);
  759.             if (!$etablissement) {
  760.                 $this->addFlash('warning''Votre compte n\'est pas associé à un établissement.');
  761.                 return $this->redirectToRoute('app_candidature_impressions');
  762.             }
  763.             if ($etablissementId && (int) $etablissementId !== $etablissement->getId()) {
  764.                 $this->addFlash('error''Sélection d\'établissement non autorisée.');
  765.                 return $this->redirectToRoute('app_candidature_impressions');
  766.             }
  767.         }
  768.         $metiersAutorises $this->getMetiersForEtablissement($etablissement);
  769.         $metierIdsAutorises array_map(static fn(Metier $metier): int => $metier->getId(), $metiersAutorises);
  770.         $qb $this->entityManager->getRepository(Candidature::class)
  771.             ->createQueryBuilder('c')
  772.             ->leftJoin('c.user''u')
  773.             ->leftJoin('c.etablissement''e')
  774.             ->leftJoin('c.metier''m')
  775.             ->addSelect('u''e''m')
  776.             ->andWhere('c.etablissement = :etablissement')
  777.             ->setParameter('etablissement'$etablissement)
  778.             ->orderBy('u.nom''ASC')
  779.             ->addOrderBy('u.prenoms''ASC')
  780.             ->addOrderBy('c.numero''ASC');
  781.         $metierSelectionne null;
  782.         if ($metierId && $metierId !== 'TOUS') {
  783.             $metierSelectionneId = (int) $metierId;
  784.             if (!in_array($metierSelectionneId$metierIdsAutorisestrue)) {
  785.                 $this->addFlash('error''Sélection de métier non autorisée.');
  786.                 return $this->redirectToRoute('app_candidature_impressions');
  787.             }
  788.             $metierSelectionne $this->entityManager->getRepository(Metier::class)->find($metierSelectionneId);
  789.             if (!$metierSelectionne) {
  790.                 $this->addFlash('error''Métier invalide.');
  791.                 return $this->redirectToRoute('app_candidature_impressions');
  792.             }
  793.             $qb->andWhere('c.metier = :metier')->setParameter('metier'$metierSelectionne);
  794.         }
  795.         $candidatures $qb->getQuery()->getResult();
  796.         if (empty($candidatures)) {
  797.             $this->addFlash('warning''Aucune candidature trouvée pour les critères sélectionnés.');
  798.             return $this->redirectToRoute('app_candidature_impressions');
  799.         }
  800.         $headers = ['N°''Numéro''Nom''Prénoms''Contact''Établissement''Métier'];
  801.         $datas = [];
  802.         foreach ($candidatures as $index => $candidature) {
  803.             $candidat $candidature->getUser();
  804.             $datas[] = [
  805.                 $index 1,
  806.                 $candidature->getNumero(),
  807.                 $candidat?->getNom() ?? '',
  808.                 $candidat?->getPrenoms() ?? '',
  809.                 $candidat?->getContact() ?? '',
  810.                 $candidature->getEtablissement()?->getNom() ?? '',
  811.                 $candidature->getMetier()?->getNom() ?? '',
  812.             ];
  813.         }
  814.         $filename 'LISTE_CANDIDATS_INSCRITS_' date('Ymd_His') . '.xlsx';
  815.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  816.     }
  817.     private function exportListeAdmis(?string $etablissementId, ?string $metierId): ?Response
  818.     {
  819.         if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_ENT') && !$this->isGranted('ROLE_JURY')) {
  820.             throw $this->createAccessDeniedException('Accès non autorisé à cet export.');
  821.         }
  822.         $user $this->getUser();
  823.         $etablissement null;
  824.         if ($this->isGranted('ROLE_ADMIN')) {
  825.             if (!$etablissementId || $etablissementId === 'TOUS') {
  826.                 $this->addFlash('error''Veuillez sélectionner un établissement.');
  827.                 return $this->redirectToRoute('app_candidature_impressions');
  828.             }
  829.             $etablissement $this->entityManager->getRepository(Etablissement::class)->find((int) $etablissementId);
  830.             if (!$etablissement) {
  831.                 $this->addFlash('error''Établissement invalide.');
  832.                 return $this->redirectToRoute('app_candidature_impressions');
  833.             }
  834.         } else {
  835.             $etablissement $this->entityManager->getRepository(Etablissement::class)->findOneBy(['user' => $user]);
  836.             if (!$etablissement) {
  837.                 $this->addFlash('warning''Votre compte n\'est pas associé à un établissement.');
  838.                 return $this->redirectToRoute('app_candidature_impressions');
  839.             }
  840.         }
  841.         $metiersAutorises $this->getMetiersForEtablissement($etablissement);
  842.         $metierIdsAutorises array_map(static fn(Metier $m): int => $m->getId(), $metiersAutorises);
  843.         $qb $this->entityManager->getRepository(Candidature::class)
  844.             ->createQueryBuilder('c')
  845.             ->leftJoin('c.user''u')
  846.             ->leftJoin('c.etablissement''e')
  847.             ->leftJoin('c.metier''m')
  848.             ->addSelect('u''e''m')
  849.             ->andWhere('c.etablissement = :etablissement')
  850.             ->andWhere('c.resultat = 2')
  851.             ->setParameter('etablissement'$etablissement)
  852.             ->orderBy('u.nom''ASC')
  853.             ->addOrderBy('u.prenoms''ASC');
  854.         if ($metierId && $metierId !== 'TOUS') {
  855.             $metierSelectionneId = (int) $metierId;
  856.             if (!in_array($metierSelectionneId$metierIdsAutorisestrue)) {
  857.                 $this->addFlash('error''Sélection de métier non autorisée.');
  858.                 return $this->redirectToRoute('app_candidature_impressions');
  859.             }
  860.             $metierSelectionne $this->entityManager->getRepository(Metier::class)->find($metierSelectionneId);
  861.             if (!$metierSelectionne) {
  862.                 $this->addFlash('error''Métier invalide.');
  863.                 return $this->redirectToRoute('app_candidature_impressions');
  864.             }
  865.             $qb->andWhere('c.metier = :metier')->setParameter('metier'$metierSelectionne);
  866.         }
  867.         $candidatures $qb->getQuery()->getResult();
  868.         if (empty($candidatures)) {
  869.             $this->addFlash('warning''Aucun candidat admis trouvé pour les critères sélectionnés.');
  870.             return $this->redirectToRoute('app_candidature_impressions');
  871.         }
  872.         $headers = ['N°''Numéro''Nom''Prénoms''Contact''Date Naissance''Lieu Naissance''Établissement''Métier'];
  873.         $datas = [];
  874.         foreach ($candidatures as $index => $candidature) {
  875.             $candidat $candidature->getUser();
  876.             $datas[] = [
  877.                 $index 1,
  878.                 $candidature->getNumero(),
  879.                 $candidat?->getNom() ?? '',
  880.                 $candidat?->getPrenoms() ?? '',
  881.                 $candidat?->getContact() ?? '',
  882.                 $candidat?->getDatenaissance()?->format('d/m/Y') ?? '',
  883.                 $candidat?->getLieunaissance() ?? '',
  884.                 $candidature->getEtablissement()?->getNom() ?? '',
  885.                 $candidature->getMetier()?->getNom() ?? '',
  886.             ];
  887.         }
  888.         $filename 'LISTE_CANDIDATS_ADMIS_' date('Ymd_His') . '.xlsx';
  889.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  890.     }
  891.     private function exportListeAdmisDefinitifs(?string $etablissementId, ?string $metierId): ?Response
  892.     {
  893.         if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_ENT') && !$this->isGranted('ROLE_JURY')) {
  894.             throw $this->createAccessDeniedException('Accès non autorisé à cet export.');
  895.         }
  896.         $user $this->getUser();
  897.         $etablissement null;
  898.         if ($this->isGranted('ROLE_ADMIN')) {
  899.             if (!$etablissementId || $etablissementId === 'TOUS') {
  900.                 $this->addFlash('error''Veuillez sélectionner un établissement.');
  901.                 return $this->redirectToRoute('app_candidature_impressions');
  902.             }
  903.             $etablissement $this->entityManager->getRepository(Etablissement::class)->find((int) $etablissementId);
  904.             if (!$etablissement) {
  905.                 $this->addFlash('error''Établissement invalide.');
  906.                 return $this->redirectToRoute('app_candidature_impressions');
  907.             }
  908.         } else {
  909.             $etablissement $this->entityManager->getRepository(Etablissement::class)->findOneBy(['user' => $user]);
  910.             if (!$etablissement) {
  911.                 $this->addFlash('warning''Votre compte n\'est pas associé à un établissement.');
  912.                 return $this->redirectToRoute('app_candidature_impressions');
  913.             }
  914.         }
  915.         $metiersAutorises $this->getMetiersForEtablissement($etablissement);
  916.         $metierIdsAutorises array_map(static fn(Metier $m): int => $m->getId(), $metiersAutorises);
  917.         $qb $this->entityManager->getRepository(Admis::class)
  918.             ->createQueryBuilder('a')
  919.             ->leftJoin('a.user''u')
  920.             ->leftJoin('a.etablissement''e')
  921.             ->leftJoin('a.metier''m')
  922.             ->leftJoin('a.candidature''c')
  923.             ->addSelect('u''e''m''c')
  924.             ->andWhere('a.etablissement = :etablissement')
  925.             ->setParameter('etablissement'$etablissement)
  926.             ->orderBy('u.nom''ASC')
  927.             ->addOrderBy('u.prenoms''ASC');
  928.         if ($metierId && $metierId !== 'TOUS') {
  929.             $metierSelectionneId = (int) $metierId;
  930.             if (!in_array($metierSelectionneId$metierIdsAutorisestrue)) {
  931.                 $this->addFlash('error''Sélection de métier non autorisée.');
  932.                 return $this->redirectToRoute('app_candidature_impressions');
  933.             }
  934.             $metierSelectionne $this->entityManager->getRepository(Metier::class)->find($metierSelectionneId);
  935.             if (!$metierSelectionne) {
  936.                 $this->addFlash('error''Métier invalide.');
  937.                 return $this->redirectToRoute('app_candidature_impressions');
  938.             }
  939.             $qb->andWhere('a.metier = :metier')->setParameter('metier'$metierSelectionne);
  940.         }
  941.         $admisDefinitifs $qb->getQuery()->getResult();
  942.         if (empty($admisDefinitifs)) {
  943.             $this->addFlash('warning''Aucun candidat admis définitif trouvé pour les critères sélectionnés.');
  944.             return $this->redirectToRoute('app_candidature_impressions');
  945.         }
  946.         $headers = ['N°''N° Inscription''N° Candidature''Nom''Prénoms''Contact''Date Naissance''Lieu Naissance''Établissement''Métier''Date Inscription''Confirmé'];
  947.         $datas = [];
  948.         foreach ($admisDefinitifs as $index => $admis) {
  949.             $candidat $admis->getUser();
  950.             $datas[] = [
  951.                 $index 1,
  952.                 $admis->getNumeroInscription(),
  953.                 $admis->getCandidature()?->getNumero() ?? '',
  954.                 $candidat?->getNom() ?? '',
  955.                 $candidat?->getPrenoms() ?? '',
  956.                 $candidat?->getContact() ?? '',
  957.                 $candidat?->getDatenaissance()?->format('d/m/Y') ?? '',
  958.                 $candidat?->getLieunaissance() ?? '',
  959.                 $admis->getEtablissement()?->getNom() ?? '',
  960.                 $admis->getMetier()?->getNom() ?? '',
  961.                 $admis->getDateInscription()?->format('d/m/Y H:i') ?? '',
  962.                 $admis->getConfirme() ? 'Oui' 'Non',
  963.             ];
  964.         }
  965.         $filename 'LISTE_CANDIDATS_ADMIS_DEFINITIFS_' date('Ymd_His') . '.xlsx';
  966.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  967.     }
  968.     private function exportStatsSecteur(): ?Response
  969.     {
  970.         if (!$this->isGranted('ROLE_ADMIN')) {
  971.             throw $this->createAccessDeniedException('Accès non autorisé à cet export.');
  972.         }
  973.         $qb $this->entityManager->getRepository(Candidature::class)
  974.             ->createQueryBuilder('c')
  975.             ->innerJoin('c.metier''m')
  976.             ->innerJoin('m.secteur''s')
  977.             ->leftJoin('c.user''u')
  978.             ->select(
  979.                 's.nom as secteur_nom',
  980.                 'COUNT(c.id) as total',
  981.                 'SUM(CASE WHEN c.etustatut = 2 THEN 1 ELSE 0 END) as eligible',
  982.                 'SUM(CASE WHEN c.entstatut IS NOT NULL OR c.resultat IS NOT NULL THEN 1 ELSE 0 END) as evalue',
  983.                 'SUM(CASE WHEN c.entstatut = 2 THEN 1 ELSE 0 END) as admissible',
  984.                 'SUM(CASE WHEN c.resultat = 2 THEN 1 ELSE 0 END) as admis',
  985.                 'SUM(CASE WHEN u.sexe = :homme THEN 1 ELSE 0 END) as hommes',
  986.                 'SUM(CASE WHEN u.sexe = :femme THEN 1 ELSE 0 END) as femmes'
  987.             )
  988.             ->setParameter('homme''MASCULIN')
  989.             ->setParameter('femme''FEMININ')
  990.             ->groupBy('s.id')
  991.             ->orderBy('s.nom''ASC');
  992.         $stats $qb->getQuery()->getResult();
  993.         $headers = [
  994.             'Secteur d\'activité',
  995.             'Total',
  996.             'Hommes',
  997.             'Femmes',
  998.             'Éligibles',
  999.             'Évalués',
  1000.             'Admissibles',
  1001.             'Admis'
  1002.         ];
  1003.         $datas = [];
  1004.         foreach ($stats as $stat) {
  1005.             $datas[] = [
  1006.                 $stat['secteur_nom'],
  1007.                 $stat['total'],
  1008.                 $stat['hommes'],
  1009.                 $stat['femmes'],
  1010.                 $stat['eligible'],
  1011.                 $stat['evalue'],
  1012.                 $stat['admissible'],
  1013.                 $stat['admis']
  1014.             ];
  1015.         }
  1016.         $filename 'STATISTIQUES_PAR_SECTEUR_' date('Ymd_His') . '.xlsx';
  1017.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  1018.     }
  1019.     private function exportStatsRegion(): ?Response
  1020.     {
  1021.         if (!$this->isGranted('ROLE_ADMIN')) {
  1022.             throw $this->createAccessDeniedException('Accès non autorisé à cet export.');
  1023.         }
  1024.         $qb $this->entityManager->getRepository(Candidature::class)
  1025.             ->createQueryBuilder('c')
  1026.             ->innerJoin('c.etablissement''e')
  1027.             ->innerJoin('e.directionRegionale''dr')
  1028.             ->leftJoin('c.user''u')
  1029.             ->select(
  1030.                 'dr.nom as region_nom',
  1031.                 'COUNT(c.id) as total',
  1032.                 'SUM(CASE WHEN c.etustatut = 2 THEN 1 ELSE 0 END) as eligible',
  1033.                 'SUM(CASE WHEN c.entstatut IS NOT NULL OR c.resultat IS NOT NULL THEN 1 ELSE 0 END) as evalue',
  1034.                 'SUM(CASE WHEN c.entstatut = 2 THEN 1 ELSE 0 END) as admissible',
  1035.                 'SUM(CASE WHEN c.resultat = 2 THEN 1 ELSE 0 END) as admis',
  1036.                 'SUM(CASE WHEN u.sexe = :homme THEN 1 ELSE 0 END) as hommes',
  1037.                 'SUM(CASE WHEN u.sexe = :femme THEN 1 ELSE 0 END) as femmes'
  1038.             )
  1039.             ->setParameter('homme''MASCULIN')
  1040.             ->setParameter('femme''FEMININ')
  1041.             ->groupBy('dr.id')
  1042.             ->orderBy('dr.nom''ASC');
  1043.         $stats $qb->getQuery()->getResult();
  1044.         $headers = [
  1045.             'Direction Régionale',
  1046.             'Total',
  1047.             'Hommes',
  1048.             'Femmes',
  1049.             'Éligibles',
  1050.             'Évalués',
  1051.             'Admissibles',
  1052.             'Admis'
  1053.         ];
  1054.         $datas = [];
  1055.         foreach ($stats as $stat) {
  1056.             $datas[] = [
  1057.                 $stat['region_nom'],
  1058.                 $stat['total'],
  1059.                 $stat['hommes'],
  1060.                 $stat['femmes'],
  1061.                 $stat['eligible'],
  1062.                 $stat['evalue'],
  1063.                 $stat['admissible'],
  1064.                 $stat['admis']
  1065.             ];
  1066.         }
  1067.         $filename 'STATISTIQUES_PAR_REGION_' date('Ymd_His') . '.xlsx';
  1068.         return $this->spreadsheetGenerator->generate($filename$headers$datas);
  1069.     }
  1070.     private function exportByCodes(string $codes): ?Response
  1071.     {
  1072.         $numeros array_filter(array_map('trim'explode(';'str_replace(["\r""\n"], ';'$codes))));
  1073.         if (empty($numeros)) return null;
  1074.         $results $this->entityManager->getRepository(Candidature::class)->findBy(['numero' => $numeros]);
  1075.         if (empty($results)) return null;
  1076.         $headers = ['N°''Numéro''Nom''Prénoms''Date naissance''Lieu naissance''Sexe''Contact''Métier''Note'];
  1077.         $datas = [];
  1078.         foreach ($results as $i => $r) {
  1079.             $user $r->getUser();
  1080.             $datas[] = [
  1081.                 $i 1,
  1082.                 $r->getNumero(),
  1083.                 $user strtoupper($user->getNom()) : '',
  1084.                 $user strtoupper($user->getPrenoms()) : '',
  1085.                 $user?->getDatenaissance()?->format('d/m/Y') ?? '',
  1086.                 $user strtoupper($user->getLieunaissance() ?? '') : '',
  1087.                 $user && $user->getSexe() === 'MASCULIN' 'M' 'F',
  1088.                 $user?->getContact() ?? '',
  1089.                 $r->getMetier()?->getNom() ?? '',
  1090.                 $this->calculateTotalNote($r)
  1091.             ];
  1092.         }
  1093.         return $this->spreadsheetGenerator->generate('EXPORT_PAR_CODE.xlsx'$headers$datas);
  1094.     }
  1095.     private function exportByFilters($etablissement$statut$metierId): ?Response
  1096.     {
  1097.         $qb $this->entityManager->getRepository(Candidature::class)
  1098.             ->createQueryBuilder('c')
  1099.             ->innerJoin('c.metier''m')
  1100.             ->innerJoin('c.etablissement''e')
  1101.             ->leftJoin('c.user''u');
  1102.         if ($etablissement && $etablissement !== 'TOUS') {
  1103.             $qb->andWhere('c.etablissement = :etablissement')->setParameter('etablissement'$etablissement);
  1104.         }
  1105.         if ($metierId) {
  1106.             $qb->andWhere('c.metier = :metier')->setParameter('metier'$metierId);
  1107.         }
  1108.         if ($statut === '2'$qb->andWhere('c.entstatut = 2');
  1109.         elseif ($statut === '2|2'$qb->andWhere('c.resultat = 2');
  1110.         elseif ($statut === '1'$qb->andWhere('c.etustatut = 1');
  1111.         $candidatures $qb->getQuery()->getResult();
  1112.         if (empty($candidatures)) return null;
  1113.         $headers = ['N°''Numéro''Nom''Prénoms''Date naissance''Sexe''Contact''Métier''Établissement''Statut'];
  1114.         $datas = [];
  1115.         foreach ($candidatures as $i => $c) {
  1116.             $user $c->getUser();
  1117.             $datas[] = [
  1118.                 $i 1,
  1119.                 $c->getNumero(),
  1120.                 $user?->getNom() ?? '',
  1121.                 $user?->getPrenoms() ?? '',
  1122.                 $user?->getDatenaissance()?->format('d/m/Y') ?? '',
  1123.                 $user?->getSexe() ?? '',
  1124.                 $user?->getContact() ?? '',
  1125.                 $c->getMetier()?->getNom() ?? '',
  1126.                 $c->getEtablissement()?->getNom() ?? '',
  1127.                 self::STATUT_LABELS[$c->getEtustatut()] ?? self::STATUT_LABELS['default']
  1128.             ];
  1129.         }
  1130.         return $this->spreadsheetGenerator->generate('EXPORT_CANDIDATURES.xlsx'$headers$datas);
  1131.     }
  1132.     private function calculateTotalNote(Candidature $candidature): float
  1133.     {
  1134.         $total 0;
  1135.         for ($i 1$i <= 13$i++) {
  1136.             $method 'getNote' $i;
  1137.             $total += $candidature->$method() ?? 0;
  1138.         }
  1139.         return $total;
  1140.     }
  1141.     private function generateFichePdf(Candidature $candidature): Response
  1142.     {
  1143.         return $this->pdfGenerator->stream('candidature/print/fiche.html.twig', [
  1144.             'candidature' => $candidature,
  1145.             'image' => $this->prepareImagesForPdf($candidature)
  1146.         ]);
  1147.     }
  1148.     private function generateConvocationPdf(Candidature $candidature): Response
  1149.     {
  1150.         return $this->pdfGenerator->stream('candidature/print/convocation.html.twig', [
  1151.             'candidature' => $candidature,
  1152.             'image' => $this->prepareImagesForPdf($candidature)
  1153.         ]);
  1154.     }
  1155.     private function printStatistics(Request $request): Response
  1156.     {
  1157.         $metierId $request->query->get('metier');
  1158.         $metier $metierId $this->entityManager->getRepository(Metier::class)->find($metierId) : null;
  1159.         $results $this->getStatisticsByMetier($metier);
  1160.         $dirImage $this->params->get('dir_image');
  1161.         $images = ['entete' => $this->pdfGenerator->imageToBase64($dirImage 'entete_generique_e2c.png')];
  1162.         return $this->pdfGenerator->stream('candidature/print/stat.html.twig', [
  1163.             'results' => $results,
  1164.             'image' => $images,
  1165.             'date' => new \DateTime()
  1166.         ]);
  1167.     }
  1168.     private function prepareImagesForPdf(Candidature $candidature): array
  1169.     {
  1170.         $dirMedia $this->params->get('dir_media');
  1171.         $dirImage $this->params->get('dir_image');
  1172.         $photoPath $candidature->getFphoto()
  1173.             ? $dirMedia $candidature->getNumero() . '/' $candidature->getFphoto()
  1174.             : $dirImage 'user.svg';
  1175.         return [
  1176.             'entete' => $this->pdfGenerator->imageToBase64($dirImage 'entete_generique_e2c.png'),
  1177.             'photo' => $this->pdfGenerator->imageToBase64($photoPath)
  1178.         ];
  1179.     }
  1180.     private function handleInvalidConvocation(): Response
  1181.     {
  1182.         $this->addFlash('error''Ce candidat n\'est pas admissible, aucune convocation disponible');
  1183.         return $this->redirectToRoute('app_candidature_impressions');
  1184.     }
  1185. }