feat(Service/Pdf): Crée le service PDF pour les fiches de candidature

📝 feat(Controller/Join): Génère et envoie la fiche candidat PDF
```
This commit is contained in:
Serreau Jovann
2025-12-25 11:48:42 +01:00
parent b532100003
commit 51d9d87784
7 changed files with 461 additions and 4 deletions

View File

@@ -82,6 +82,13 @@ vich_uploader:
inject_on_load: true inject_on_load: true
delete_on_update: true delete_on_update: true
delete_on_remove: true delete_on_remove: true
fiche:
uri_prefix: /fiche_candidat
upload_destination: '%kernel.project_dir%/public/storage/fiche_candidat'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
#mappings: #mappings:
# products: # products:
# uri_prefix: /images/products # uri_prefix: /images/products

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251225001531 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "join" ADD address VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD zip_code VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD city VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD pronom VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD sexe VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD insta_link VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD facebook_link VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "join" ADD tiktok_link VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD is_discord BOOLEAN NOT NULL');
$this->addSql('ALTER TABLE "join" ADD discord_account VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD role TEXT NOT NULL');
$this->addSql('COMMENT ON COLUMN "join".role IS \'(DC2Type:array)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "join" DROP address');
$this->addSql('ALTER TABLE "join" DROP zip_code');
$this->addSql('ALTER TABLE "join" DROP city');
$this->addSql('ALTER TABLE "join" DROP pronom');
$this->addSql('ALTER TABLE "join" DROP sexe');
$this->addSql('ALTER TABLE "join" DROP insta_link');
$this->addSql('ALTER TABLE "join" DROP facebook_link');
$this->addSql('ALTER TABLE "join" DROP tiktok_link');
$this->addSql('ALTER TABLE "join" DROP is_discord');
$this->addSql('ALTER TABLE "join" DROP discord_account');
$this->addSql('ALTER TABLE "join" DROP role');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251225104342 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "join" ADD fiche_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD fiche_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD fiche_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD fiche_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD fiche_original_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE "join" ADD update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN "join".update_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "join" DROP fiche_file_name');
$this->addSql('ALTER TABLE "join" DROP fiche_dimensions');
$this->addSql('ALTER TABLE "join" DROP fiche_size');
$this->addSql('ALTER TABLE "join" DROP fiche_mine_type');
$this->addSql('ALTER TABLE "join" DROP fiche_original_name');
$this->addSql('ALTER TABLE "join" DROP update_at');
}
}

View File

@@ -11,14 +11,18 @@ use App\Entity\Join;
use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType; use App\Form\RequestPasswordRequestType;
use App\Service\Mailer\Mailer; use App\Service\Mailer\Mailer;
use App\Service\Pdf\Candidat;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
@@ -28,13 +32,47 @@ class JoinController extends AbstractController
{ {
#[Route(path: '/join', name: 'app_recruit', options: ['sitemap' => false], methods: ['GET','POST'])] #[Route(path: '/join', name: 'app_recruit', options: ['sitemap' => false], methods: ['GET','POST'])]
public function join(Request $request,Mailer $mailer): Response public function join(Request $request,EntityManagerInterface $entityManager,Mailer $mailer,KernelInterface $kernel): Response
{ {
$j= new Join(); $j= new Join();
$form = $this->createForm(JoinType::class,$j); $form = $this->createForm(JoinType::class,$j);
$form->handleRequest($request); $form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){ if($form->isSubmitted() && $form->isValid()){
dd($j); $j->setState("create");
$j->setCreateAt(new \DateTimeImmutable("now"));
$cPdf = new Candidat();
$cPdf->setData($j,$entityManager,$kernel);
$cPdf->AddPage();
$cPdf->contentDetails();
$content = $cPdf->Output('S');
$fileName = 'fiche_candidat.pdf';
$tempFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName;
file_put_contents($tempFilePath, $content);
$file = new UploadedFile($tempFilePath,$fileName,"application/pdf",0,true);
$j->setFiche($file);
$entityManager->persist($j);
$entityManager->flush();
$mailer->send('contact@e-cosplay.fr',
'E-Cosplay',
'[E-Cosplay] - Nouvelle candidature',
'candidat/new.twig',
['joint'=>$j],
[new DataPart($content,'candidat.pdf','application/pdf')]
);
$mailer->send($j->getEmail(),
$j->getSurname()." ".$j->getName(),
"[E-Cosplay] - Confirmation de votre candidature",
'condidat/confirm.twig',
['joint'=>$j],
[new DataPart($content,'candidat.pdf','application/pdf')]
);
//send mail to contact@e-cosplay.fr
//send mail to candidat
} }
return $this->render('join.twig',[ return $this->render('join.twig',[
'form' => $form->createView(), 'form' => $form->createView(),

View File

@@ -7,6 +7,7 @@ use PixelOpen\CloudflareTurnstileBundle\Type\TurnstileType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@@ -36,9 +37,8 @@ class JoinType extends AbstractType
'label' => 'form.label.phone', 'label' => 'form.label.phone',
'required' => true, 'required' => true,
]) ])
->add('dateBirth', DateTimeType::class, [ ->add('dateBirth', DateType::class, [
'label' => 'form.label.birthdate', 'label' => 'form.label.birthdate',
'widget' => 'single_text',
'required' => true, 'required' => true,
]) ])
->add('address', TextType::class, [ ->add('address', TextType::class, [

View File

@@ -5,9 +5,12 @@ namespace App\Entity;
use App\Repository\JoinRepository; use App\Repository\JoinRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: JoinRepository::class)] #[ORM\Entity(repositoryClass: JoinRepository::class)]
#[ORM\Table(name: '`join`')] #[ORM\Table(name: '`join`')]
#[Vich\Uploadable()]
class Join class Join
{ {
#[ORM\Id] #[ORM\Id]
@@ -75,6 +78,143 @@ class Join
#[ORM\Column(type: Types::ARRAY)] #[ORM\Column(type: Types::ARRAY)]
private array $role = []; private array $role = [];
#[Vich\UploadableField(mapping: 'fiche',fileNameProperty: 'ficheFileName', size: 'ficheSize', mimeType: 'ficheMineType', originalName: 'ficheOriginalName',dimensions: 'ficheDimensions')]
private ?File $fiche = null;
#[ORM\Column(nullable: true)]
private ?string $ficheFileName = null;
#[ORM\Column(nullable: true)]
private ?array $ficheDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $ficheSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $ficheMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $ficheOriginalName = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
/**
* @return \DateTimeImmutable|null
*/
public function getUpdateAt(): ?\DateTimeImmutable
{
return $this->updateAt;
}
/**
* @return File|null
*/
public function getFiche(): ?File
{
return $this->fiche;
}
/**
* @return array|null
*/
public function getFicheDimensions(): ?array
{
return $this->ficheDimensions;
}
/**
* @return string|null
*/
public function getFicheFileName(): ?string
{
return $this->ficheFileName;
}
/**
* @return string|null
*/
public function getFicheMineType(): ?string
{
return $this->ficheMineType;
}
/**
* @return string|null
*/
public function getFicheOriginalName(): ?string
{
return $this->ficheOriginalName;
}
/**
* @return string|null
*/
public function getFicheSize(): ?string
{
return $this->ficheSize;
}
/**
* @return bool|null
*/
public function getIsDiscord(): ?bool
{
return $this->isDiscord;
}
/**
* @param \DateTimeImmutable|null $updateAt
*/
public function setUpdateAt(?\DateTimeImmutable $updateAt): void
{
$this->updateAt = $updateAt;
}
/**
* @param File|null $fiche
*/
public function setFiche(?File $fiche): void
{
$this->fiche = $fiche;
}
/**
* @param array|null $ficheDimensions
*/
public function setFicheDimensions(?array $ficheDimensions): void
{
$this->ficheDimensions = $ficheDimensions;
}
/**
* @param string|null $ficheFileName
*/
public function setFicheFileName(?string $ficheFileName): void
{
$this->ficheFileName = $ficheFileName;
}
/**
* @param string|null $ficheMineType
*/
public function setFicheMineType(?string $ficheMineType): void
{
$this->ficheMineType = $ficheMineType;
}
/**
* @param string|null $ficheOriginalName
*/
public function setFicheOriginalName(?string $ficheOriginalName): void
{
$this->ficheOriginalName = $ficheOriginalName;
}
/**
* @param string|null $ficheSize
*/
public function setFicheSize(?string $ficheSize): void
{
$this->ficheSize = $ficheSize;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;

View File

@@ -0,0 +1,176 @@
<?php
namespace App\Service\Pdf;
use App\Entity\Join;
use Doctrine\ORM\EntityManagerInterface;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
class Candidat extends Fpdf
{
private Join $join;
private EntityManagerInterface $em;
private KernelInterface $kernel;
public function __construct($orientation = 'P', $unit = 'mm', $size = 'A4')
{
parent::__construct($orientation, $unit, $size);
$this->AliasNbPages();
$this->SetAutoPageBreak(true, 20);
}
public function setData(Join $join, EntityManagerInterface $em, KernelInterface $kernel)
{
$this->join = $join;
$this->em = $em;
$this->kernel = $kernel;
}
public function Header()
{
$yPos = 10;
$xPos = 10;
$logoWidth = 25;
$logoHeight = 15;
$this->SetY($yPos);
$agDate = $this->join->getCreateAt() ? $this->join->getCreateAt()->format('d/m/Y') : date('d/m/Y');
$this->SetFont('Arial', 'B', 16);
$this->SetTextColor(0, 0, 0);
$this->Cell(0, 7, utf8_decode("FICHE CANDIDAT"), 0, 1, 'C');
$this->SetFont('Arial', '', 10);
$this->Cell(0, 6, utf8_decode("Générée le " . $agDate), 0, 1, 'C');
$logoPath = $this->kernel->getProjectDir() . '/public/assets/images/logo.jpg';
if (file_exists($logoPath)) {
$this->Image($logoPath, $xPos, $yPos, $logoWidth, $logoHeight);
}
$this->Ln(5);
if ($this->PageNo() == 1) {
$this->writeAgContextBlock();
} else {
$this->SetY(30);
}
}
public function writeAgContextBlock()
{
$this->SetFont('Arial', 'B', 9);
$this->Cell(0, 1, '', 'T', 1, 'L');
$this->Ln(2);
$this->Cell(0, 5, utf8_decode('INFORMATIONS LÉGALES'), 0, 1, 'C');
$this->SetFont('Arial', '', 9);
$this->Cell(35, 4, utf8_decode('Association :'), 0, 0);
$this->SetFont('Arial', 'B', 9);
$this->Cell(0, 4, utf8_decode('E-Cosplay Association loi 1901 RNA N°W022006988'), 0, 1);
$this->SetFont('Arial', '', 9);
$this->Cell(35, 4, utf8_decode('Siège social :'), 0, 0);
$this->Cell(0, 4, utf8_decode('42 rue de saint-quentin 02800 Beautor'), 0, 1);
$this->Ln(2);
$this->Cell(0, 1, '', 'T', 1, 'L');
$this->Ln(5);
}
public function contentDetails()
{
// --- SECTION 1 : ÉTAT CIVIL ---
$this->drawSectionTitle('IDENTITÉ DU CANDIDAT');
$this->infoRow('Nom / Prénom :', strtoupper($this->join->getSurname()) . ' ' . $this->join->getName(), true);
// --- CALCUL ET AFFICHAGE DE L'ÂGE ---
$birthDate = $this->join->getDateBirth();
$this->SetFont('Arial', '', 10);
$this->Cell(40, 7, utf8_decode('Date de naissance :'), 0, 0);
if ($birthDate) {
$age = $this->calculateAge($birthDate);
$dateStr = $birthDate->format('d/m/Y');
// Texte de la date
$this->SetFont('Arial', 'B', 10);
$this->Cell(30, 7, $dateStr, 0, 0);
// Affichage de l'âge avec couleur
if ($age >= 16) {
$this->SetTextColor(0, 150, 0); // Vert
} else {
$this->SetTextColor(200, 0, 0); // Rouge
}
$this->Cell(0, 7, utf8_decode(" (" . $age . " ans)"), 0, 1);
$this->SetTextColor(0, 0, 0); // Reset noir
} else {
$this->Cell(0, 7, 'N/C', 0, 1);
}
$genreInfo = sprintf("Sexe : %s | Pronom : %s",
$this->join->getSexe() ?? 'N/C',
$this->join->getPronom() ?? 'N/C'
);
$this->infoRow('Genre :', $genreInfo);
$this->Ln(3);
// --- SECTION 2 : CONTACT ---
$this->drawSectionTitle('COORDONNÉES');
$this->infoRow('Email :', $this->join->getEmail(), true);
$this->infoRow('Téléphone :', $this->join->getPhone());
$address = sprintf("%s, %s %s", $this->join->getAddress(), $this->join->getZipCode(), $this->join->getCity());
$this->infoRow('Adresse :', $address);
$this->Ln(3);
// --- SECTION 3 : PROFIL ---
$this->drawSectionTitle('PROFIL ET RÔLES');
$roles = $this->join->getRole();
$this->infoRow('Rôles visés :', is_array($roles) ? implode(', ', $roles) : ($roles ?? 'Aucun'), true);
$this->Ln(2);
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 6, utf8_decode("Présentation / Qui est-ce ?"), 0, 1);
$this->SetFont('Arial', '', 10);
$this->MultiCell(0, 5, utf8_decode($this->join->getWho() ?? "Aucune description fournie."), 1, 'L');
$this->Ln(5);
// --- SECTION 4 : RÉSEAUX SOCIAUX ---
$this->drawSectionTitle('RÉSEAUX SOCIAUX');
$this->infoRow('Discord :', $this->join->getDiscordAccount() ?? 'Non renseigné');
$this->infoRow('Instagram :', $this->join->getInstaLink() ?? 'N/A');
$this->infoRow('TikTok :', $this->join->getTiktokLink() ?? 'N/A');
$this->infoRow('Facebook :', $this->join->getFacebookLink() ?? 'N/A');
}
private function calculateAge(\DateTimeInterface $birthDate): int
{
$now = new \DateTime();
return $now->diff($birthDate)->y;
}
private function infoRow($label, $value, $important = false)
{
$this->SetFont('Arial', '', 10);
$this->Cell(40, 7, utf8_decode($label), 0, 0);
if ($important) $this->SetFont('Arial', 'B', 10);
$this->MultiCell(0, 7, utf8_decode($value), 0, 'L');
}
private function drawSectionTitle($title)
{
$this->SetFillColor(240, 240, 240);
$this->SetFont('Arial', 'B', 11);
$this->Cell(0, 8, utf8_decode(' ' . $title), 0, 1, 'L', true);
$this->Ln(2);
}
public function Footer()
{
$this->SetY(-15);
$this->SetFont('Arial', 'I', 8);
$this->SetTextColor(128, 128, 128);
$this->Cell(0, 10, 'Fiche Candidat E-Cosplay - Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
}