feat(doc): Ajoute la page de documents et les AGs au sitemap.
♻️ refactor(.env): Met à jour l'URL de développement Ngrok.
 feat(SitemapSubscriber): Ajoute les URLs contact et doc au sitemap.
 feat(AgGenerator): Ajoute l'option de largeur à la signature PDF.
 feat(Ag/Main): Ajoute le champ de signature de l'adhésion à l'AG.
 feat(AgTypeEdit): Crée le formulaire d'édition du type d'AG.
🌐 i18n: Ajoute des traductions chinoises pour la page documents.
 feat(Mailer): Ajoute le contenu texte aux e-mails.
 feat(DocumentController): Ajoute le contrôleur de documents.
 feat(txt-mails): Ajoute les templates de mails en texte.
 feat(AdController): Ajoute le contrôleur d'adhésion.
 feat(Service/Pdf): Crée le service PDF pour l'adhésion à l'AG.
 feat(AdminController): Ajoute la gestion de l'AG à l'admin.
🌐 i18n: Ajoute les traductions françaises pour la page documents.
 feat(Members): Ajoute la relation avec la signature de l'AG.
```
This commit is contained in:
Serreau Jovann
2025-11-23 22:57:58 +01:00
parent 359d7772a3
commit 5b4c06b470
38 changed files with 2078 additions and 63 deletions

2
.env
View File

@@ -51,7 +51,7 @@ PATH_URL=https://esyweb.local
STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE79Tr8treeHX9KMcZtvcQZ0X8VSm00Q6GQ365V
STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR
STRIPE_WEBHOOKS_SIGN=whsec_0DOZJAwgMwkcHl2RWXI8h8YItj9q7v3A
DEV_URL=https://265fcc9dd2a6.ngrok-free.app
DEV_URL=https://240fba7426df.ngrok-free.app
VAPID_PK=DsOg7jToRSD-VpNSV1Gt3YAhSwz4l-nqeu7yFvzbSxg
VAPID_PC=BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo

View File

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

View File

@@ -0,0 +1,40 @@
<?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 Version20251123201651 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 ag_main ADD ag_adh_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE ag_main ADD ag_adh_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE ag_main ADD ag_adh_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE ag_main ADD ag_adh_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE ag_main ADD ag_adh_original_name VARCHAR(255) DEFAULT NULL');
}
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 ag_main DROP ag_adh_file_name');
$this->addSql('ALTER TABLE ag_main DROP ag_adh_dimensions');
$this->addSql('ALTER TABLE ag_main DROP ag_adh_size');
$this->addSql('ALTER TABLE ag_main DROP ag_adh_mine_type');
$this->addSql('ALTER TABLE ag_main DROP ag_adh_original_name');
}
}

View File

@@ -0,0 +1,40 @@
<?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 Version20251123210026 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('CREATE TABLE ag_main_signed (id SERIAL NOT NULL, main_id INT DEFAULT NULL, members_id INT DEFAULT NULL, submiter_id VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_1726DFDA627EA78A ON ag_main_signed (main_id)');
$this->addSql('CREATE INDEX IDX_1726DFDABD01F5ED ON ag_main_signed (members_id)');
$this->addSql('ALTER TABLE ag_main_signed ADD CONSTRAINT FK_1726DFDA627EA78A FOREIGN KEY (main_id) REFERENCES ag_main (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main_signed ADD CONSTRAINT FK_1726DFDABD01F5ED FOREIGN KEY (members_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main ADD is_attestation BOOLEAN DEFAULT NULL');
}
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 ag_main_signed DROP CONSTRAINT FK_1726DFDA627EA78A');
$this->addSql('ALTER TABLE ag_main_signed DROP CONSTRAINT FK_1726DFDABD01F5ED');
$this->addSql('DROP TABLE ag_main_signed');
$this->addSql('ALTER TABLE ag_main DROP is_attestation');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Controller;
use App\Dto\Contact\ContactType;
use App\Dto\Contact\DtoContact;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Entity\Ag\Main;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Repository\Ag\MainRepository;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class AdController extends AbstractController
{
#[Route(path: '/adh/{id}', name: 'app_adh', options: ['sitemap' => false], methods: ['GET','POST'])]
public function adh(?Main $main,Request $request): Response
{
if(!$main instanceof Main){
return $this->redirectToRoute('app_home');
}
if($request->query->has('validateResult')) {
return $this->render('adh_validate.twig',[
'main' => $main,
]);
}
$signedList =[];
foreach ($main->getMainSigneds() as $signed) {
$docuseal = new \Docuseal\Api('pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe', 'https://signature.esy-web.dev/api');
$submiter = $docuseal->getSubmitter($signed->getSubmiterId());
$signed->sign = "https://signature.esy-web.dev/s/".$submiter['slug'];
$signedList[] = $signed;
}
return $this->render('adh.twig',[
'main' => $main,
'signed' => $signedList,
]);
}
}

View File

@@ -5,12 +5,14 @@ namespace App\Controller\Admin;
use App\Dto\Ag\AgMembersType;
use App\Dto\Ag\AgOrderType;
use App\Dto\Ag\AgType;
use App\Dto\Ag\AgTypeEdit;
use App\Dto\Ag\AgVoteType;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Entity\Ag\Main;
use App\Entity\Ag\MainMember;
use App\Entity\Ag\MainOrder;
use App\Entity\Ag\MainSigned;
use App\Entity\Ag\MainVote;
use App\Entity\Members;
use App\Entity\MembersCotisations;
@@ -25,11 +27,20 @@ use App\Repository\MembersRepository;
use App\Repository\ProductsRepository;
use App\Service\Mailer\Mailer;
use App\Service\Payments\PaymentClient;
use App\Service\Pdf\AgAdh;
use App\Service\Pdf\AgConvocation;
use App\Service\Pdf\AgGenerator;
use App\Service\Pdf\CotaReceiptGenerator;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -239,7 +250,7 @@ class AdminController extends AbstractController
#[Route(path: '/admin/ag', name: 'admin_ag', options: ['sitemap' => false], methods: ['GET'])]
public function adminAg(UploaderHelper $uploaderHelper,KernelInterface $kernel,EntityManagerInterface $entityManager,Request $request,MainRepository $mainRepository): Response
public function adminAg(Mailer $mailer,UploaderHelper $uploaderHelper,KernelInterface $kernel,EntityManagerInterface $entityManager,Request $request,MainRepository $mainRepository): Response
{
if($request->query->has('idValidateSign')) {
@@ -260,6 +271,31 @@ class AdminController extends AbstractController
$entityManager->flush();
return $this->redirectToRoute('admin_ag');
}
if($request->query->has('idQrCode')) {
/** @var Main $idQrCode */
$idQrCode = $mainRepository->find($request->query->get('idQrCode'));
if($_ENV['APP_ENV'] == "prod") {
$urlRoot = $request->getContentTypeFormat();
} else {
$urlRoot = $_ENV['DEV_URL'];
}
$builder = new Builder(
writer: new PngWriter(),
writerOptions: [],
validateResult: false,
data: $urlRoot.$this->generateUrl('app_adh',['id'=>$idQrCode->getId()]),
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::High,
size: 300,
margin: 10,
);
$result = $builder->build();
$response = new Response($result->getString(),Response::HTTP_OK,[
'Content-Type' => $result->getMimeType()
]);
return $response;
}
if($request->query->has('idSign')) {
/** @var Main $main */
$main = $mainRepository->find($request->query->get('id'));
@@ -298,6 +334,86 @@ class AdminController extends AbstractController
$r->setStatusCode(302);
return $r;
}
if($request->query->has('idConvocation')) {
/** @var Main $main */
$main = $mainRepository->find($request->query->get('idConvocation'));
$agAdh = new AgAdh();
$agAdh->setData($main,$kernel,$entityManager);
$agAdh->GeneratePdf();
$content = $agAdh->Output('S');
$fileName = 'adh_ecosplay_' . $main->getAgDateAt()->format('Ymd') . '.pdf';
$tempFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName;
file_put_contents($tempFilePath, $content);
$file = new UploadedFile($tempFilePath,$fileName,"application/pdf",0,true);
$main->setAgAdh($file);
$main->setUpdateAt(new \DateTimeImmutable());
$main->setIsAttestation(true);
$entityManager->persist($main);
$entityManager->flush();
$url = $uploaderHelper->asset($main,'agAdh');
if($_ENV['APP_ENV'] == "prod") {
$url = $request->getContentTypeFormat() . $url;
$urlRoot = $request->getContentTypeFormat();
} else {
$url = $_ENV['DEV_URL'] . $url;
$urlRoot = $_ENV['DEV_URL'];
}
$rList =[];
$submitters =[];
foreach ($entityManager->getRepository(Members::class)->findAll() as $member) {
$isSigned = $member->getMainSigneds()->filter(function (MainSigned $sign) use ($member) {
return $sign->getMembers()->getId() == $member->getId();
})->first();
if(!$isSigned instanceof MainSigned) {
$isSigned = new MainSigned();
$isSigned->setMembers($member);
$isSigned->setMain($main);
$isSigned->setSubmiterId(0);
$entityManager->persist($isSigned);
//$entityManager->flush();
$submitters[] = [
'role' => 'member_'.$member->getId(),
'email' => $member->getEmail(),
];
$rList[$member->getId()] = $isSigned;
}
$agConvocation = new AgConvocation();
$agConvocation->setData($main,$kernel,$member);
$agConvocation->generateConvocation();
$content = $agConvocation->Output('S');
$fileName = 'convocation_ag_ecosplay_' . $main->getAgDateAt()->format('Ymd') . '.pdf';
$mailer->send($member->getEmail(),$member->getCiv()." ".$member->getSurname().' '.$member->getName(),"[E-Cosplay] Convocation assemblée générale","mails/ag_convocation.twig",[
'main' => $main,
'member' => $member,
],[
new DataPart($content,$fileName,"application/pdf"),
]);
}
$data = [
'name' => 'ADH E-COSPLAY '.$main->getAgDateAt()->format('d/m/Y'),
'documents' => [
[
'name' => 'adh-ecosplay-'.$main->getAgDateAt()->format('d-m-Y'),
'file' => $url
]
],
'completed_redirect_url' => $urlRoot.$this->generateUrl('app_adh',['id'=>$main->getId(),'validateResult'=>true]),
'submitters' => $submitters
];
$docuseal = new \Docuseal\Api('pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe', 'https://signature.esy-web.dev/api');
$content = $docuseal->createSubmissionFromPdf($data);
foreach ($content['submitters'] as $submitter) {
/** @var MainSigned $role */
$role = $rList[str_replace("member_","",$submitter['role'])];
$role->setSubmiterId($submitter['id']);
$entityManager->persist($role);
}
$entityManager->flush();
return $this->redirectToRoute('admin_ag');
}
if($request->query->has('idGenerate')) {
/** @var Main $main */
$main = $mainRepository->find($request->query->get('idGenerate'));
@@ -317,6 +433,7 @@ class AdminController extends AbstractController
file_put_contents($tempFilePath, $content);
$file = new UploadedFile($tempFilePath,$fileName,"application/pdf",0,true);
$main->setAg($file);
$main->setUpdateAt(new \DateTimeImmutable());
$main->setIsClosed(true);
$entityManager->persist($main);
$entityManager->flush();
@@ -334,6 +451,7 @@ class AdminController extends AbstractController
$form = $this->createForm(AgType::class,$ag);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$entityManager->persist($ag);
$entityManager->flush();
return $this->redirectToRoute('admin_ag');
@@ -345,7 +463,7 @@ class AdminController extends AbstractController
#[Route(path: '/admin/ag/{id}', name: 'admin_ag_edit', options: ['sitemap' => false], methods: ['GET','POST'])]
public function adminAgeDIT(?Main $ag,Request $request,EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(AgType::class,$ag);
$form = $this->createForm(AgTypeEdit::class,$ag);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$entityManager->persist($ag);
@@ -413,6 +531,7 @@ class AdminController extends AbstractController
return $this->redirectToRoute('admin_ag_edit',['id'=>$ag->getId()]);
}
return $this->render('admin/ag/edit.twig', [
'ag' => $form->createView(),
'agMain' => $ag,

View File

@@ -25,7 +25,7 @@ use Twig\Environment;
class ContactController extends AbstractController
{
#[Route(path: '/contact', name: 'app_contact', options: ['sitemap' => true], methods: ['GET','POST'])]
#[Route(path: '/contact', name: 'app_contact', options: ['sitemap' => false], methods: ['GET','POST'])]
public function index(Request $request,Mailer $mailer): Response
{
$dto = new DtoContact();

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Controller;
use App\Dto\Contact\ContactType;
use App\Dto\Contact\DtoContact;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Repository\Ag\MainRepository;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class DocumentController extends AbstractController
{
#[Route(path: '/documents', name: 'app_doc', options: ['sitemap' => false], methods: ['GET','POST'])]
public function index(MainRepository $mainRepository): Response
{
return $this->render('doc.twig',[
'ag_ordinaire' => $mainRepository->findBy(['isSigned'=>true,'type'=>'Ordinaire'],['agDateAt'=>'DESC']),
'ag_extraordinaire' => $mainRepository->findBy(['isSigned'=>true,'type'=>'Extraordinaire'],['agDateAt'=>'DESC']),
]);
}
}

View File

@@ -56,10 +56,7 @@ class AgType extends AbstractType
return $member->getPseudo()." - ".$member->getRole();
}
])
->add('closedAt',DateTimeType::class,[
'label' => 'Date de cloture',
'required' => true,
])
;
}

70
src/Dto/Ag/AgTypeEdit.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace App\Dto\Ag;
use App\Entity\Ag\Main;
use App\Entity\Members;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AgTypeEdit extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('agDateAt',DateTimeType::class,[
'label' => 'Date de l\'Assemblée Générale',
'required' => true,
])
->add('type',ChoiceType::class,[
'label' => 'Type de l\'AG',
'required' => true,
'choices' => [
'Ordinaire' => 'Ordinaire',
'Extraordinaire' => 'Extraordinaire'
]
])
->add('locate',TextType::class,[
'label' => 'Adresse',
'required' => true,
])
->add('locateCity',TextType::class,[
'label' => 'Ville',
'required' => true,
])
->add('locateZipcode',TextType::class,[
'label' => 'Code postal',
'required' => true,
])
->add('president',EntityType::class,[
'label' => 'President de la scéance',
'class' => Members::class,
'choice_label' => function (Members $member) {
return $member->getPseudo()." - ".$member->getRole();
}
])
->add('secretaire',EntityType::class,[
'label' => 'Secretaire de la scéance',
'class' => Members::class,
'choice_label' => function (Members $member) {
return $member->getPseudo()." - ".$member->getRole();
}
])
->add('closedAt',DateTimeType::class,[
'label' => 'Date de cloture',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',Main::class);
}
}

View File

@@ -69,7 +69,6 @@ class Main
#[Vich\UploadableField(mapping: 'ag',fileNameProperty: 'agFileName', size: 'agSize', mimeType: 'agMineType', originalName: 'agOriginalName',dimensions: 'agDimensions')]
private ?File $ag = null;
#[ORM\Column(nullable: true)]
private ?string $agFileName = null;
#[ORM\Column(nullable: true)]
@@ -80,6 +79,21 @@ class Main
private ?string $agMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $agOriginalName = null;
#[Vich\UploadableField(mapping: 'ag_adh',fileNameProperty: 'agAdhFileName', size: 'agAdhSize', mimeType: 'agAdhMineType', originalName: 'agAdhOriginalName',dimensions: 'agAdhDimensions')]
private ?File $agAdh = null;
#[ORM\Column(nullable: true)]
private ?string $agAdhFileName = null;
#[ORM\Column(nullable: true)]
private ?array $agAdhDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $agAdhSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $agAdhMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $agAdhOriginalName = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
@@ -88,6 +102,15 @@ class Main
#[ORM\Column(nullable: true)]
private ?string $submiterId = null;
#[ORM\Column(nullable: true)]
private ?bool $isAttestation = null;
/**
* @var Collection<int, MainSigned>
*/
#[ORM\OneToMany(targetEntity: MainSigned::class, mappedBy: 'main')]
private Collection $mainSigneds;
/**
* @return File|null
*/
@@ -109,6 +132,7 @@ class Main
$this->mainMembers = new ArrayCollection();
$this->orders = new ArrayCollection();
$this->mainVote = new ArrayCollection();
$this->mainSigneds = new ArrayCollection();
}
public function getId(): ?int
@@ -441,4 +465,142 @@ class Main
return $this;
}
/**
* @return File|null
*/
public function getAgAdh(): ?File
{
return $this->agAdh;
}
/**
* @return array|null
*/
public function getAgAdhDimensions(): ?array
{
return $this->agAdhDimensions;
}
/**
* @return string|null
*/
public function getAgAdhFileName(): ?string
{
return $this->agAdhFileName;
}
/**
* @return string|null
*/
public function getAgAdhMineType(): ?string
{
return $this->agAdhMineType;
}
/**
* @return string|null
*/
public function getAgAdhOriginalName(): ?string
{
return $this->agAdhOriginalName;
}
/**
* @return string|null
*/
public function getAgAdhSize(): ?string
{
return $this->agAdhSize;
}
/**
* @param File|null $agAdh
*/
public function setAgAdh(?File $agAdh): void
{
$this->agAdh = $agAdh;
}
/**
* @param array|null $agAdhDimensions
*/
public function setAgAdhDimensions(?array $agAdhDimensions): void
{
$this->agAdhDimensions = $agAdhDimensions;
}
/**
* @param string|null $agAdhFileName
*/
public function setAgAdhFileName(?string $agAdhFileName): void
{
$this->agAdhFileName = $agAdhFileName;
}
/**
* @param string|null $agAdhMineType
*/
public function setAgAdhMineType(?string $agAdhMineType): void
{
$this->agAdhMineType = $agAdhMineType;
}
/**
* @param string|null $agAdhOriginalName
*/
public function setAgAdhOriginalName(?string $agAdhOriginalName): void
{
$this->agAdhOriginalName = $agAdhOriginalName;
}
/**
* @param string|null $agAdhSize
*/
public function setAgAdhSize(?string $agAdhSize): void
{
$this->agAdhSize = $agAdhSize;
}
public function isAttestation(): ?bool
{
return $this->isAttestation;
}
public function setIsAttestation(bool $isAttestation): static
{
$this->isAttestation = $isAttestation;
return $this;
}
/**
* @return Collection<int, MainSigned>
*/
public function getMainSigneds(): Collection
{
return $this->mainSigneds;
}
public function addMainSigned(MainSigned $mainSigned): static
{
if (!$this->mainSigneds->contains($mainSigned)) {
$this->mainSigneds->add($mainSigned);
$mainSigned->setMain($this);
}
return $this;
}
public function removeMainSigned(MainSigned $mainSigned): static
{
if ($this->mainSigneds->removeElement($mainSigned)) {
// set the owning side to null (unless already changed)
if ($mainSigned->getMain() === $this) {
$mainSigned->setMain(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Entity\Ag;
use App\Entity\Members;
use App\Repository\Ag\MainSignedRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MainSignedRepository::class)]
#[ORM\Table(name: 'ag_main_signed')]
class MainSigned
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'mainSigneds')]
private ?Main $main = null;
#[ORM\ManyToOne(inversedBy: 'mainSigneds')]
private ?Members $members = null;
#[ORM\Column(length: 255)]
private ?string $submiterId = null;
public function getId(): ?int
{
return $this->id;
}
public function getMain(): ?Main
{
return $this->main;
}
public function setMain(?Main $main): static
{
$this->main = $main;
return $this;
}
public function getMembers(): ?Members
{
return $this->members;
}
public function setMembers(?Members $members): static
{
$this->members = $members;
return $this;
}
public function getSubmiterId(): ?string
{
return $this->submiterId;
}
public function setSubmiterId(string $submiterId): static
{
$this->submiterId = $submiterId;
return $this;
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Entity;
use App\Entity\Ag\Main;
use App\Entity\Ag\MainMember;
use App\Entity\Ag\MainSigned;
use App\Entity\Ag\MainVote;
use App\Repository\MembersRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -103,6 +104,12 @@ class Members
#[ORM\Column(length: 255, nullable: true)]
private ?string $civ = null;
/**
* @var Collection<int, MainSigned>
*/
#[ORM\OneToMany(targetEntity: MainSigned::class, mappedBy: 'members')]
private Collection $mainSigneds;
public function __construct()
{
$this->membersCotisations = new ArrayCollection();
@@ -110,6 +117,7 @@ class Members
$this->secretaire = new ArrayCollection();
$this->mainMembers = new ArrayCollection();
$this->agVotes = new ArrayCollection();
$this->mainSigneds = new ArrayCollection();
}
public function getId(): ?int
@@ -538,4 +546,34 @@ class Members
return $this;
}
/**
* @return Collection<int, MainSigned>
*/
public function getMainSigneds(): Collection
{
return $this->mainSigneds;
}
public function addMainSigned(MainSigned $mainSigned): static
{
if (!$this->mainSigneds->contains($mainSigned)) {
$this->mainSigneds->add($mainSigned);
$mainSigned->setMembers($this);
}
return $this;
}
public function removeMainSigned(MainSigned $mainSigned): static
{
if ($this->mainSigneds->removeElement($mainSigned)) {
// set the owning side to null (unless already changed)
if ($mainSigned->getMembers() === $this) {
$mainSigned->setMembers(null);
}
}
return $this;
}
}

View File

@@ -86,6 +86,24 @@ class SitemapSubscriber
}
$urlContainer->addUrl($urlDons, 'default');
$urlContact = new UrlConcrete($urlGenerator->generate('app_contact', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlContact = new GoogleImageUrlDecorator($urlContact);
$urlContact->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlContact = new GoogleMultilangUrlDecorator($urlContact);
foreach ($langs as $lang) {
$urlContact->addLink($urlGenerator->generate('app_contact',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlContact, 'default');
$urlDoc = new UrlConcrete($urlGenerator->generate('app_doc', [], UrlGeneratorInterface::ABSOLUTE_URL));
$urlDoc = new GoogleImageUrlDecorator($urlDoc);
$urlDoc->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));
$urlDoc = new GoogleMultilangUrlDecorator($urlDoc);
foreach ($langs as $lang) {
$urlContact->addLink($urlGenerator->generate('app_doc',['lang'=>$lang], UrlGeneratorInterface::ABSOLUTE_URL), $lang);
}
$urlContainer->addUrl($urlDoc, 'default');
$urlAbout = new UrlConcrete($urlGenerator->generate('app_about', [], UrlGeneratorInterface::ABSOLUTE_URL));
$decoratedUrlAbout = new GoogleImageUrlDecorator($urlAbout);
$decoratedUrlAbout->addImage(new GoogleImage($this->cacheManager->resolve('assets/images/logo.jpg','webp')));

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Ag;
use App\Entity\Ag\MainSigned;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MainSigned>
*/
class MainSignedRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MainSigned::class);
}
// /**
// * @return MainSigned[] Returns an array of MainSigned objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('m.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?MainSigned
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -83,11 +83,18 @@ class Mailer
]);
$htmlContent = $this->convertMjmlToHtml($mjmlGenerator);
$txtContent = $this->environment->render('txt-'.$template,[
'system' => [
'subject' => $subject,
'path' => $_ENV['PATH_URL'],
],
'datas' => $data,
]);
foreach ($files as $file) {
$mail->addPart($file);
}
$mail->html($htmlContent);
$mail->text($txtContent);
$this->mailer->send($mail);
}

239
src/Service/Pdf/AgAdh.php Normal file
View File

@@ -0,0 +1,239 @@
<?php
namespace App\Service\Pdf;
use App\Entity\Ag\Main;
use App\Entity\Members;
use Doctrine\ORM\EntityManagerInterface;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
class AgAdh extends Fpdf
{
private Main $main;
private KernelInterface $kernel;
private EntityManagerInterface $em;
// Données d'association simulées (à remplacer par de vraies données si disponibles via Kernel ou autre)
private array $associationData = [
'name' => 'E-Cosplay Association',
'address' => '42 rue de saint-quentin',
'city' => '02800 Beautor',
'rna' => 'W022006988',
'type' => 'Association loi 1901 à but non lucratif',
];
public function setData(Main $main, KernelInterface $kernel,EntityManagerInterface $entityManager)
{
$this->main = $main;
$this->kernel = $kernel;
$this->em = $entityManager;
}
/**
* En-tête du document (Informations de l'association et titre - NOUVELLE IMPLÉMENTATION)
*/
public function Header()
{
// --- 1. Définition des constantes de position ---
$yPos = 10;
$xPos = 10;
$logoWidth = 20;
$logoHeight = 10;
// --- 2. Logo (Haut Gauche) ---
$logoPath = $this->kernel->getProjectDir() . '/public/assets/images/logo.jpg';
if (file_exists($logoPath)) {
$this->Image($logoPath, $xPos, $yPos, $logoWidth, $logoHeight);
}
// --- 3. Titre de l'Association (Haut Centré) ---
$this->SetY($yPos);
$this->SetFont('Arial', 'B', 14);
$this->SetTextColor(0, 0, 0);
$this->Cell(0, 7, utf8_decode('E-Cosplay Association'), 0, 1, 'C');
$this->SetFont('Arial', '', 10);
$this->Cell(0, 5, utf8_decode('Siège social : 42 rue de saint-quentin 02800 Beautor'), 0, 1, 'C');
// Ligne de séparation
$this->Ln(3);
$this->Cell(0, 1, '', 'B', 1, 'L', false);
$this->Ln(10);
}
/**
* Pied de page du document
*/
public function Footer()
{
// Positionnement à 15 mm du bas
$this->SetY(-15);
// Police Arial italique 8
$this->SetFont('Arial', 'I', 8);
$this->SetTextColor(120, 120, 120);
// Numéro de page
$this->Cell(0, 10, utf8_decode('Page ' . $this->PageNo() . '/{nb}'), 0, 0, 'C');
}
/**
* Bloc d'information contextuelle pour l'Assemblée Générale.
*/
public function writeAgContextBlock()
{
// Récupération des données nécessaires
$agDate = $this->main->getAgDateAt() ? $this->main->getAgDateAt()->format('d/m/Y') : 'Date non spécifiée';
$agTime = $this->main->getAgDateAt() ? $this->main->getAgDateAt()->format('H\hi') : 'Heure non spécifiée';
$this->SetFont('Arial', '', 10);
$this->SetTextColor(0, 0, 0);
// Largeur de la cellule de données (pour aligner les données)
$dataCellWidth = 100;
$labelCellWidth = 60;
// Message introductif pour la convocation
$text = utf8_decode("Cher(e) membre de l'Association E-Cosplay,");
$this->MultiCell(0, 6, $text);
$this->Ln(3);
$text = utf8_decode("Vous êtes cordialement convoqué(e) à l'Assemblée Générale de notre association dont les détails sont les suivants :");
$this->MultiCell(0, 6, $text);
$this->Ln(5);
// Ajout du cadre
$this->Cell(0, 1, '', 'T', 1, 'L', false); // Ligne supérieure légère
$this->Ln(2); // Petite marge après la ligne
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 5, utf8_decode('DÉTAILS DE L\'ASSEMBLÉE GÉNÉRALE'), 0, 1, 'C');
$this->Ln(1);
$this->SetFont('Arial', '', 10);
// Ligne 1: Association / RNA
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Association :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode('E-Cosplay Association loi 1901 RNA N°W022006988'), 0, 1, 'L');
// Ligne 2: Siège social
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Siège social :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode('42 rue de saint-quentin 02800 Beautor'), 0, 1, 'L');
// Ligne 3: Date de l'AG
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode("Date de l'Assemblée Générale :"), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($agDate), 0, 1, 'L');
// Ligne 4: Heure de début
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Heure de début de la séance :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($agTime), 0, 1, 'L');
// Ligne 5: Lieu (Dynamique)
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Lieu :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
// Utiliser l'opérateur de coalescence null si les getters peuvent retourner null
$location = $this->main->getLocate() . " " . $this->main->getLocateZipcode() . " " . $this->main->getLocateCity();
$this->Cell($dataCellWidth, 5, utf8_decode($location), 0, 1, 'L');
// Ligne 6: Nature de l'AG (Dynamique)
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode("Nature de l'AG :"), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($this->main->getType()), 0, 1, 'L');
// Fin de la mise en forme du bloc
$this->Ln(2); // Petite marge avant la ligne
$this->Cell(0, 1, '', 'T', 1, 'L', false); // Ligne inférieure légère
$this->Ln(5); // Espacement final avant le corps du texte
}
/**
* Méthode principale pour générer le PDF
*/
public function GeneratePdf()
{
$this->AliasNbPages(); // Active le comptage total des pages
$this->AddPage();
// ----------------------------------------------------
// 1. Informations Générales de l'AG (Utilisation du nouveau bloc)
// ----------------------------------------------------
$this->writeAgContextBlock();
// ----------------------------------------------------
// 2. Listing des Membres de l'AG
// ----------------------------------------------------
$membersCount = $this->em->getRepository(Members::class)->count([]);
$this->SetFont('Arial', 'B', 11);
$this->SetFillColor(230, 230, 230);
$this->Cell(0, 7, utf8_decode('Liste de tous les Membres de l\'association (' . $membersCount . ' membres)'), 0, 1, 'L', true);
// En-têtes du tableau (Hauteur 7)
$idCellWidth = 5;
$signatureCellWidth = 105;
// Nouvelle répartition des largeurs pour utiliser 190mm: 5 (id) + 60 (name) + 75 (email) + 50 (signature) = 190
$nameCellWidth = 40;
$emailCellWidth = 40;
$this->SetFont('Arial', 'B', 9);
$this->SetFillColor(200, 220, 255);
$this->Cell($idCellWidth, 7, utf8_decode('#'), 1, 0, 'C', true);
$this->Cell($nameCellWidth, 7, utf8_decode('Nom Prénom / Pseudo'), 1, 0, 'L', true);
$this->Cell($emailCellWidth, 7, utf8_decode('E-mail.'), 1, 0, 'L', true);
$this->Cell($signatureCellWidth, 7, utf8_decode('Signature de Présence'), 1, 1, 'C', true);
// Lignes du tableau
$this->SetFont('Arial', '', 9);
$i = 1;
$fill = false;
$cellHeight = 15; // Hauteur de cellule définie à 15
// Récupérer tous les membres
$members = $this->em->getRepository(Members::class)->findAll();
foreach ($members as $member) {
// Alternance des couleurs de fond
$this->SetFillColor($fill ? 240 : 255, 240, 240);
$fill = !$fill;
// Affichage des informations
$this->Cell($idCellWidth, $cellHeight, $i++, 1, 0, 'C', true);
$this->Cell($nameCellWidth, $cellHeight,
utf8_decode($member->getName() . ' ' . $member->getSurname() . ' (' . $member->getPseudo() . ')'),
1, 0, 'C', true
);
$this->Cell($emailCellWidth, $cellHeight,
utf8_decode($member->getEmail()),
1, 0, 'C' , true
);
// Espace pour la signature (avec le placeholder de signature) - Largeur de la cellule et du placeholder = 50. Alignement 'C'
$this->Cell($signatureCellWidth, $cellHeight, '{{Signature;width=250;height=25;type=signature;role=member_'.$member->getId().'}}', 1, 1, 'C', true);
// Gérer les sauts de page automatiques
if ($this->GetY() + $cellHeight > 280) { // Si la prochaine ligne dépasse la limite
$this->AddPage();
// Ré-afficher les en-têtes du tableau sur la nouvelle page (Hauteur 7)
$this->SetFont('Arial', 'B', 9);
$this->SetFillColor(200, 220, 255);
$this->Cell($idCellWidth, 7, utf8_decode('#'), 1, 0, 'C', true);
$this->Cell($nameCellWidth, 7, utf8_decode('Nom Prénom / Pseudo'), 1, 0, 'L', true);
$this->Cell($emailCellWidth, 7, utf8_decode('E-mail.'), 1, 0, 'L', true);
$this->Cell($signatureCellWidth, 7, utf8_decode('Signature de Présence'), 1, 1, 'C', true);
$this->SetFont('Arial', '', 9);
$this->SetFillColor(240, 240, 240);
}
}
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace App\Service\Pdf;
use App\Entity\Ag\Main;
use Fpdf\Fpdf;
use Symfony\Component\HttpKernel\KernelInterface;
class AgConvocation extends Fpdf
{
private Main $main;
private KernelInterface $kernel;
private \App\Entity\Members $member;
public function __construct($orientation = 'P', $unit = 'mm', $size = 'A4')
{
parent::__construct($orientation, $unit, $size);
$this->SetAutoPageBreak(true, 20); // Marge inférieure de 20mm
}
/**
* Définit les données de l'Assemblée Générale et le membre destinataire.
*/
public function setData(Main $main, KernelInterface $kernel, \App\Entity\Members $member)
{
$this->main = $main;
$this->kernel = $kernel;
$this->member = $member;
}
/**
* Définit l'en-tête de chaque page (logo et titre).
*/
public function Header()
{
// --- 1. Définition des constantes de position ---
$yPos = 10;
$xPos = 10;
$logoWidth = 20;
$logoHeight = 10;
// --- 2. Logo (Haut Gauche) ---
$logoPath = $this->kernel->getProjectDir() . '/public/assets/images/logo.jpg';
if (file_exists($logoPath)) {
$this->Image($logoPath, $xPos, $yPos, $logoWidth, $logoHeight);
}
// --- 3. Titre de l'Association (Haut Centré) ---
$this->SetY($yPos);
$this->SetFont('Arial', 'B', 14);
$this->SetTextColor(0, 0, 0);
$this->Cell(0, 7, utf8_decode('E-Cosplay Association'), 0, 1, 'C');
$this->SetFont('Arial', '', 10);
$this->Cell(0, 5, utf8_decode('Siège social : 42 rue de saint-quentin 02800 Beautor'), 0, 1, 'C');
// Ligne de séparation
$this->Ln(3);
$this->Cell(0, 1, '', 'B', 1, 'L', false);
$this->Ln(10);
}
/**
* Définit le pied de page (numéro de page si besoin).
*/
public function Footer()
{
// Positionnement à 15 mm du bas
$this->SetY(-15);
// Police Arial italique 8
$this->SetFont('Arial', 'I', 8);
// Numéro de page
$this->Cell(0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
/**
* Génère la convocation complète.
*/
public function generateConvocation()
{
$this->AddPage();
$this->writeRecipientBlock();
$this->writeConvocationTitle();
$this->writeAgContextBlock(); // Remplacement de writeDetailsBlock
$this->writeClosure();
}
/**
* Écrit le bloc destinataire (en haut à droite), en incluant l'adresse postale.
*/
private function writeRecipientBlock()
{
// Positionnement à droite (Marge - Largeur du bloc destinataire)
$this->SetX(120);
$this->SetFont('Arial', '', 10);
$civility = $this->member->getCiv();
$name = $this->member->getName();
$surname = $this->member->getSurname();
$this->Cell(0, 5, utf8_decode("À l'attention de :"), 0, 1, 'L');
$this->SetX(120);
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 5, utf8_decode("$civility $name $surname"), 0, 1, 'L');;
$this->Ln(10);
}
/**
* Écrit le titre principal de la convocation.
*/
private function writeConvocationTitle()
{
$this->SetFont('Arial', 'B', 16);
$this->Cell(0, 10, utf8_decode("CONVOCATION À L'ASSEMBLÉE GÉNÉRALE"), 0, 1, 'C');
$this->Ln(5);
}
/**
* Écrit le bloc d'informations légales et contextuelles de l'AG (copié depuis AgGenerator).
*/
public function writeAgContextBlock()
{
// Récupération des données nécessaires
$agDate = $this->main->getAgDateAt() ? $this->main->getAgDateAt()->format('d/m/Y') : 'Date non spécifiée';
$agTime = $this->main->getAgDateAt() ? $this->main->getAgDateAt()->format('H\hi') : 'Heure non spécifiée';
$this->SetFont('Arial', '', 10);
$this->SetTextColor(0, 0, 0);
// Largeur de la cellule de données (pour aligner les données)
$dataCellWidth = 100;
$labelCellWidth = 60;
// Message introductif pour la convocation
$text = utf8_decode("Cher(e) membre de l'Association E-Cosplay,");
$this->MultiCell(0, 6, $text);
$this->Ln(3);
$text = utf8_decode("Vous êtes cordialement convoqué(e) à l'Assemblée Générale de notre association dont les détails sont les suivants :");
$this->MultiCell(0, 6, $text);
$this->Ln(5);
// Ajout du cadre
$this->Cell(0, 1, '', 'T', 1, 'L', false); // Ligne supérieure légère
$this->Ln(2); // Petite marge après la ligne
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 5, utf8_decode('DÉTAILS DE L\'ASSEMBLÉE GÉNÉRALE'), 0, 1, 'C');
$this->Ln(1);
$this->SetFont('Arial', '', 10);
// Ligne 1: Association / RNA
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Association :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode('E-Cosplay Association loi 1901 RNA N°W022006988'), 0, 1, 'L');
// Ligne 2: Siège social
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Siège social :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode('42 rue de saint-quentin 02800 Beautor'), 0, 1, 'L');
// Ligne 3: Date de l'AG
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode("Date de l'Assemblée Générale :"), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($agDate), 0, 1, 'L');
// Ligne 4: Heure de début
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Heure de début de la séance :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($agTime), 0, 1, 'L');
// Ligne 5: Lieu (Dynamique)
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode('Lieu :'), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($this->main->getLocate() . " " . $this->main->getLocateZipcode() . " " . $this->main->getLocateCity()), 0, 1, 'L');
// Ligne 6: Nature de l'AG (Dynamique)
$this->SetFont('Arial', 'B', 10);
$this->Cell($labelCellWidth, 5, utf8_decode("Nature de l'AG :"), 0, 0, 'L');
$this->SetFont('Arial', '', 10);
$this->Cell($dataCellWidth, 5, utf8_decode($this->main->getType()), 0, 1, 'L');
// Fin de la mise en forme du bloc
$this->Ln(2); // Petite marge avant la ligne
$this->Cell(0, 1, '', 'T', 1, 'L', false); // Ligne inférieure légère
$this->Ln(5); // Espacement final avant le corps du texte
}
/**
* Écrit la formule de politesse et les informations de signature, y compris l'avertissement en rouge.
*/
private function writeClosure()
{
$agCity = $this->main->getLocateCity() ?? 'Lieu non défini';
$agDate = $this->main->getAgDateAt() ? $this->main->getAgDateAt()->format('d F Y') : 'Date non définie';
$presidentCiv = $this->main->getPresident()->getCiv();
$presidentTitle = ($presidentCiv === 'Mme') ? 'Présidente' : 'Président';
$presidentFullName = $this->main->getPresident()->getName() . ' ' . $this->main->getPresident()->getSurname();
$this->SetFont('Arial', '', 10);
$text = utf8_decode("Nous vous rappelons que votre présence est essentielle pour la vie démocratique de l'association. En cas d'impossibilité d'être présent, vous pouvez vous faire représenter en donnant une procuration à un autre membre de votre choix.");
$this->MultiCell(0, 6, $text);
$this->Ln(3);
// --- AJOUT DE LA MENTION ROUGE SUR LE VOTE ---
$this->SetTextColor(255, 0, 0); // Rouge
$this->SetFont('Arial', 'B', 10);
$warningText = utf8_decode("ATTENTION : Seuls les membres à jour de leurs adhésions pourront voter.");
$this->MultiCell(0, 6, $warningText, 0, 'C');
$this->SetTextColor(0, 0, 0); // Réinitialiser en Noir
$this->SetFont('Arial', '', 10); // Réinitialiser la police
$this->Ln(5);
$this->MultiCell(0, 6, utf8_decode("Dans l'attente de vous accueillir, veuillez agréer, cher(e) membre, l'expression de nos salutations distinguées."));
$this->Ln(10);
// Lieu et Date
$this->Cell(0, 5, utf8_decode("Fait à $agCity, le $agDate"), 0, 1, 'R');
$this->Ln(10);
// Signature
$this->Cell(0, 5, utf8_decode("Le $presidentTitle de l'Association,"), 0, 1, 'R');
$this->Ln(15); // Espace pour la signature
$this->SetFont('Arial', 'B', 10);
$this->Cell(0, 5, utf8_decode("$presidentCiv $presidentFullName"), 0, 1, 'R');
$this->SetFont('Arial', '', 10);
}
}

View File

@@ -361,7 +361,7 @@ class AgGenerator extends Fpdf
$this->Ln(5);
$this->SetFont('Arial', '', 10);
$this->Cell(95, 5, utf8_decode("{{Sign;type=signature}}"), 0, 0, 'C');
$this->Cell(95, 5, utf8_decode("{{Sign;type=signature,width=100}}"), 0, 0, 'C');
$this->Cell(95, 5, utf8_decode(""), 0, 1, 'C');
$this->Ln(5);
$this->Cell(95, 5, utf8_decode($this->main->getPresident()->getName()." ".$this->main->getPresident()->getSurname()), 0, 0, 'C');

132
templates/adh.twig Normal file
View File

@@ -0,0 +1,132 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA (Already using trans) --- #}
{% block title %}{{'adh_age.title'|trans}}{% endblock %}
{% block meta_description %}{{'adh_age.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_adh',{id:main.id}) }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ 'adh_page.breadcrumb'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{# --- BODY with Tailwind Styling and full i18n --- #}
{% block body %}
<div class="max-w-4xl mx-auto p-4 sm:p-6 lg:p-8">
<!-- Titre principal de la page -->
<h1 class="text-3xl font-extrabold text-gray-900 mb-8 text-center">
{{ 'adh_age.title'|trans }}
</h1>
<!-- Carte d'information de l'AG -->
<div class="bg-white shadow-xl rounded-2xl p-6 md:p-10 border border-gray-100">
<h2 class="text-2xl font-bold text-indigo-700 mb-6 border-b pb-2">
{{ 'ag.info.title'|trans }}
</h2>
<div class="space-y-4">
<!-- Ligne 1: Date & Heure -->
<div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold text-gray-600 w-full sm:w-1/4">
{{ 'ag.info.date_time'|trans }} :
</span>
<span class="text-lg font-medium text-gray-900 mt-1 sm:mt-0 sm:w-3/4">
{{ main.agDateAt | date('d/m/Y') }} {{ 'global.at'|trans }} {{ main.agDateAt | date('H:i') }}
</span>
</div>
<!-- Ligne 2: Lieu -->
<div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold text-gray-600 w-full sm:w-1/4">
{{ 'ag.info.location'|trans }} :
</span>
<span class="text-gray-800 mt-1 sm:mt-0 sm:w-3/4">
{{ main.locate }} {{ main.locateZipcode }} {{ main.locateCity }}
</span>
</div>
<!-- Ligne 3: Nature de l'AG -->
<div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold text-gray-600 w-full sm:w-1/4">
{{ 'ag.info.type'|trans }} :
</span>
<span class="text-gray-800 mt-1 sm:mt-0 sm:w-3/4">
{{ main.type }}
</span>
</div>
</div>
</div>
<!-- Espace pour le contenu spécifique aux adhérents : Liste des Signatures -->
<div class="mt-12">
<h2 class="text-xl font-semibold text-gray-900 border-b pb-2 mb-4">
{{ 'ag.content.signatures_list'|trans({'%count%': signed|length}) }}
</h2>
<div class="overflow-x-auto shadow-md rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<!-- En-tête du tableau -->
<thead class="bg-indigo-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">
{{ 'member.civility'|trans }}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/2">
{{ 'member.name_surname'|trans }}
</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-1/4">
{{ 'member.signature'|trans }}
</th>
</tr>
</thead>
<!-- Corps du tableau -->
<tbody class="bg-white divide-y divide-gray-200">
{% for sign in signed %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ sign.members.civ }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ sign.members.name }} {{ sign.members.surname }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
<a href="{{ sign.sign }}" target="_blank" class="text-indigo-600 hover:text-indigo-900 font-bold hover:underline transition duration-150">
{{ 'global.signed_link'|trans }}
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="px-6 py-4 text-center text-sm text-gray-500 italic">
{{ 'ag.content.no_signatures_yet'|trans }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA (Already using trans) --- #}
{% block title %}{{'adh_page_validate.title'|trans}}{% endblock %}
{% block meta_description %}{{'adh_page_validate.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_adh',{id:main.id,validateResult:1}) }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ 'adh_page_validate.breadcrumb'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{# --- BODY with Tailwind Styling and full i18n --- #}
{% block body %}
<div class="max-w-xl mx-auto p-4 sm:p-6 lg:p-8">
<div class="bg-white shadow-2xl rounded-2xl p-8 md:p-12 text-center border-t-4 border-green-500">
<!-- Icone de succès -->
<svg class="mx-auto h-16 w-16 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h1 class="mt-4 text-3xl font-extrabold text-gray-900">
{{ 'adh_page_validate.title'|trans }}
</h1>
<p class="mt-4 text-lg text-gray-600">
{{ 'adh_page_validate.success_message'|trans }}
</p>
<p class="mt-6 text-xl font-semibold text-indigo-600">
{{ 'adh_page_validate.thanks'|trans }}
</p>
</div>
</div>
{% endblock %}

View File

@@ -18,6 +18,11 @@
{% for ag in ags %}
<div class="mb-6 p-4 border rounded-lg shadow-md bg-gray-50 dark:bg-gray-700 dark:border-gray-600 hover:shadow-lg transition duration-300">
{# NOUVEAU CONTENEUR FLEX POUR DISPOSITION LATÉRALE #}
<div class="flex justify-between items-start">
{# BLOC GAUCHE: Détails de l'AG #}
<div class="flex-grow pr-4">
<div class="flex justify-between items-start mb-2">
<p class="text-lg font-semibold text-indigo-600 dark:text-indigo-400">
{{ ag.agDateAt|date('d/m/Y H:i') }}
@@ -50,14 +55,32 @@
<strong>Ordres du jour:</strong> <span class="font-medium">{{ ag.orders.count }}</span>
</p>
</div>
</div>
{# FIN BLOC GAUCHE #}
{# BLOC DROIT: QR CODE #}
<div class="flex-shrink-0 pt-1">
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mb-1">Détails AG</p>
<img src="{{ path('admin_ag',{idQrCode:ag.id}) }}" alt="QR Code pour {{ ag.type }} du {{ ag.agDateAt|date('d/m/Y') }}" class="w-54 h-54 border border-gray-300 dark:border-gray-600 rounded-md p-1"/>
</div>
{# FIN BLOC DROIT #}
</div>
{# BLOC ACTIONS (GARDÉ EN DESSOUS DES DÉTAILS) #}
{% if ag.closed == false or ag.closed == null %}
{# Bloc Actions: SUPPRIMER / MODIFIER #}
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600 flex space-x-3">
<a href="{{ path('admin_ag', {'idGenerate': ag.id}) }}"
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-cyan-600 hover:bg-cyan-700 dark:bg-cyan-500 dark:hover:bg-cyan-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Générer le pdf
</a>
{% if ag.attestation == null or ag.attestation == false %}
<a href="{{ path('admin_ag', {'idConvocation': ag.id}) }}"
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Envoyée les Convocation
</a>
{% endif %}
{# Bouton Modifier #}
<a href="{{ path('admin_ag_edit', {'id': ag.id}) }}"
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">

View File

@@ -24,7 +24,6 @@
<h2 class="col-span-full text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Détails de l'AG</h2>
{{ form_row(ag.agDateAt) }}
{{ form_row(ag.closedAt) }}
{{ form_row(ag.type) }}
{# Ce champ est seul sur la deuxième colonne pour l'exemple #}
<div class="md:col-span-1"></div>

View File

@@ -125,6 +125,7 @@
{ 'name': 'Nos membres'|trans, 'route': 'app_members' },
{ 'name': 'Nos événements'|trans, 'route': 'app_events' },
{ 'name': 'Boutiques'|trans, 'route': 'app_shop' },
{ 'name': 'Documents'|trans, 'route': 'app_doc' },
{ 'name': 'Dons'|trans, 'route': 'app_dons' },
{ 'name': 'Contact'|trans, 'route': 'app_contact' }
] %}

115
templates/doc.twig Normal file
View File

@@ -0,0 +1,115 @@
{% extends 'base.twig' %}
{# --- METADATA & SCHEMA (Already using trans) --- #}
{% block title %}{{'doc_page.title'|trans}}{% endblock %}
{% block meta_description %}{{'doc_page.description'|trans}}{% endblock %}
{% block canonical_url %}<link rel="canonical" href="{{ url('app_doc') }}" />{% endblock %}
{% block breadcrumb_schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "{{ 'breadcrumb.home'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ 'doc_page.breadcrumb'|trans }}",
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
}
]
}
</script>
{% endblock %}
{# --- BODY with Tailwind Styling and full i18n --- #}
{% block body %}
<div class="container mx-auto p-4 md:p-8">
{# Main Title #}
<h1 class="text-3xl font-bold text-gray-800 mb-6 border-b pb-2">
{{ 'doc_list.title'|trans }}
</h1>
{# --- Ordinary General Assembly (AG Ordinaire) --- #}
<section class="mb-10">
<h2 class="text-2xl font-semibold text-indigo-600 mb-4">
{{ 'doc_list.ag_ordinaire_title'|trans }}
</h2>
<div class="space-y-4">
{% for ag in ag_ordinaire %}
<div class="bg-white shadow-lg rounded-lg p-5 border border-gray-200 hover:shadow-xl transition duration-300">
<div class="flex flex-wrap justify-between items-center mb-2 border-b pb-2">
<span class="text-xl font-bold text-gray-700">
{{ 'ag.date_at_prefix'|trans }} {{ ag.agDateAt|date('d/m/Y') }}
</span>
<span class="text-sm font-medium text-gray-500">
{{ 'ag.main_members_label'|trans }} : <span class="font-semibold text-gray-700">{{ ag.mainMembers.count }}</span>
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600 mb-4">
<div>
<strong class="text-gray-800">{{ 'ag.president_label'|trans }} :</strong>
{{ ag.president.civ }} {{ ag.president.name }} {{ ag.president.surname }}
</div>
<div>
<strong class="text-gray-800">{{ 'ag.secretary_label'|trans }} :</strong>
{{ ag.secretaire.civ }} {{ ag.secretaire.name }} {{ ag.secretaire.surname }}
</div>
</div>
<a
download="ag_normal_ecosplay_{{ ag.agDateAt|date('d-m-Y') }}.pdf"
href="{{ vich_uploader_asset(ag,'ag') }}"
class="inline-block mt-2 px-4 py-2 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150"
>
<i class="fas fa-file-download mr-2"></i> {{ 'ag.view_document_link'|trans }}
</a>
</div>
{% endfor %}
</div>
</section>
{# --- Extraordinary General Assembly (AG Extraordinaire) --- #}
<section>
<h2 class="text-2xl font-semibold text-red-600 mb-4">
{{ 'doc_list.ag_extraordinaire_title'|trans }}
</h2>
<div class="space-y-4">
{% for ag in ag_extraordinaire %}
<div class="bg-white shadow-lg rounded-lg p-5 border border-gray-200 hover:shadow-xl transition duration-300">
<div class="flex flex-wrap justify-between items-center mb-2 border-b pb-2">
<span class="text-xl font-bold text-gray-700">
{{ 'ag.date_at_prefix'|trans }} {{ ag.agDateAt|date('d/m/Y') }}
</span>
<span class="text-sm font-medium text-gray-500">
{{ 'ag.main_members_label'|trans }} : <span class="font-semibold text-gray-700">{{ ag.mainMembers.count }}</span>
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600 mb-4">
<div>
<strong class="text-gray-800">{{ 'ag.president_label'|trans }} :</strong>
{{ ag.president.civ }} {{ ag.president.name }} {{ ag.president.surname }}
</div>
<div>
<strong class="text-gray-800">{{ 'ag.secretary_label'|trans }} :</strong>
{{ ag.secretaire.civ }} {{ ag.secretaire.name }} {{ ag.secretaire.surname }}
</div>
</div>
<a
download="ag_normal_ecosplay_{{ ag.agDateAt|date('d-m-Y') }}.pdf"
href="{{ vich_uploader_asset(ag,'ag') }}"
class="inline-block mt-2 px-4 py-2 bg-red-500 text-white font-semibold rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150"
>
<i class="fas fa-file-download mr-2"></i> {{ 'ag.view_document_link'|trans }}
</a>
</div>
{% endfor %}
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,111 @@
{% extends 'mails/base.twig' %}
{% block subject %}
Convocation à l'Assemblée Générale {{ datas.main.type|default('Ordinaire ou Extraordinaire') }} du {{ datas.main.ag_date_at_dmy|default('Date non spécifiée') }}
{% endblock %}
{% block content %}
<mj-section background-color="#ffffff">
<mj-column width="100%">
{# 1. En-tête (Logo et Titre) #}
<mj-text font-size="20px" font-weight="bold" align="center" padding-bottom="0">
CONVOCATION À L'ASSEMBLÉE GÉNÉRALE
</mj-text>
<mj-text font-size="14px" color="#555555" align="center" padding-top="5px">
E-Cosplay Association
</mj-text>
<mj-divider border-color="#e0e0e0" border-width="1px" padding="10px 0"></mj-divider>
</mj-column>
</mj-section>
{# 2. Bloc Destinataire (Style Courrier) #}
<mj-section background-color="#ffffff" padding-top="10px">
<mj-column width="60%"></mj-column>
<mj-column width="40%" border="1px solid #e0e0e0" padding="10px">
<mj-text font-size="12px" line-height="16px" padding="0">
À l'attention de :<br>
<span style="font-weight: bold;">{{ datas.member.civ|default('') }} {{ datas.member.name|default('') }} {{ datas.member.surname|default('Cher Membre') }}</span><br>
{# Ajout de l'adresse pour une référence complète (bien que ce soit un email) #}
{{ datas.member.locate|default('') }}<br>
{{ datas.member.locateZipcode|default('') }} {{ datas.member.locateCity|default('') }}
</mj-text>
</mj-column>
</mj-section>
{# 3. Introduction et Contexte #}
<mj-section background-color="#ffffff" padding-top="20px">
<mj-column>
<mj-text font-size="14px" line-height="22px">
Cher(e) membre de l'Association E-Cosplay,
</mj-text>
<mj-text font-size="14px" line-height="22px" padding-top="10px">
Vous êtes cordialement convoqué(e) à l'Assemblée Générale ({{ datas.main.type|default('Type non défini') }}) de notre association.
</mj-text>
</mj-column>
</mj-section>
{# 4. Bloc des Détails de l'AG (Mise en évidence) #}
<mj-section background-color="#f7f7f7" border-radius="5px" padding="20px">
<mj-column width="100%">
<mj-text font-size="16px" font-weight="bold" color="#333333" align="center" padding-bottom="10px">
DÉTAILS DE L'ASSEMBLÉE
</mj-text>
<mj-table>
<tr>
<td style="width: 40%; font-weight: bold; padding: 5px 0; color: #555555;">Date :</td>
<td style="width: 60%; padding: 5px 0;">{{ datas.main.agDateAt|date('d/m/Y') }}</td>
</tr>
<tr>
<td style="width: 40%; font-weight: bold; padding: 5px 0; color: #555555;">Heure de début :</td>
<td style="width: 60%; padding: 5px 0;">{{ datas.main.agDateAt|date('H:i') }}</td>
</tr>
<tr>
<td style="width: 40%; font-weight: bold; padding: 5px 0; color: #555555;">Lieu :</td>
<td style="width: 60%; padding: 5px 0;">{{ datas.main.locate }}, {{ datas.main.locateZipCode }} {{ datas.main.locateCity }}</td>
</tr>
<tr>
<td style="width: 40%; font-weight: bold; padding: 5px 0; color: #555555;">Nature de l'AG :</td>
<td style="width: 60%; padding: 5px 0;">{{ datas.main.type|default('Non spécifiée') }}</td>
</tr>
</mj-table>
</mj-column>
</mj-section>
{# 5. Avertissement sur les Droits de Vote (Nouveau bloc en rouge) #}
<mj-section background-color="#ffffff" padding-top="15px" padding-bottom="15px">
<mj-column>
<mj-text font-size="14px" font-weight="bold" color="#FF0000" align="center" padding="0">
ATTENTION : Seuls les membres à jour de leurs adhésions pourront voter.
</mj-text>
</mj-column>
</mj-section>
{# 6. Clôture et Signature #}
<mj-section background-color="#ffffff" padding-top="0">
<mj-column>
<mj-text font-size="14px" line-height="22px">
Nous vous rappelons que votre présence est essentielle pour la vie démocratique de l'association. En cas d'impossibilité, vous pouvez vous faire représenter par un autre membre de votre choix via procuration.
</mj-text>
<mj-text font-size="14px" line-height="22px" padding-top="10px">
Dans l'attente de vous accueillir, veuillez agréer, cher(e) membre, l'expression de nos salutations distinguées.
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" padding-top="20px">
<mj-column width="40%"></mj-column>
<mj-column width="60%">
<mj-text font-size="14px" align="right" padding="0">
Fait à {{ datas.main.locate_city|default('Lieu non défini') }}, le {{ datas.main.ag_date_at_dmy|default('Date non définie') }}
</mj-text>
<mj-spacer height="30px" />
<mj-text font-size="14px" align="right" padding="0">
Le {{ (datas.main.president.civ|default('M.') == 'Mme') ? 'Présidente' : 'Président' }} de l'Association,
</mj-text>
<mj-text font-size="14px" font-weight="bold" align="right" padding="0">
{{ datas.main.president.civ|default('M.') }} {{ datas.main.president.name|default('Nom') }} {{ datas.main.president.surname|default('Prénom') }}
</mj-text>
</mj-column>
</mj-section>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'txt-mails/base.twig' %}
{% block subject %}
Convocation à l'Assemblée Générale {{ datas.main.type|default('Ordinaire ou Extraordinaire') }} du {{ datas.main.ag_date_at_dmy|default('Date non spécifiée') }}
{% endblock %}
{% block content %}
CONVOCATION À L'ASSEMBLÉE GÉNÉRALE
===================================
E-Cosplay Association
--- Destinataire ---
À l'attention de :
{{ datas.member.civ|default('') }} {{ datas.member.name|default('') }} {{ datas.member.surname|default('Cher Membre') }}
{{ datas.member.locate|default('') }}
{{ datas.member.locateZipcode|default('') }} {{ datas.member.locateCity|default('') }}
--------------------
Cher(e) membre de l'Association E-Cosplay,
Vous êtes cordialement convoqué(e) à l'Assemblée Générale ({{ datas.main.type|default('Type non défini') }}) de notre association.
===================================
DÉTAILS DE L'ASSEMBLÉE
===================================
Nature de l'AG : {{ datas.main.type|default('Non spécifiée') }}
Date : {{ datas.main.agDateAt|date('d/m/Y') }}
Heure de début : {{ datas.main.agDateAt|date('H:i') }}
Lieu : {{ datas.main.locate }}, {{ datas.main.locateZipCode }} {{ datas.main.locateCity }}
===================================
ATTENTION : Seuls les membres à jour de leurs adhésions pourront voter.
-----------------------------------------------------------------------
Nous vous rappelons que votre présence est essentielle pour la vie démocratique de l'association. En cas d'impossibilité, vous pouvez vous faire représenter par un autre membre de votre choix via procuration.
Dans l'attente de vous accueillir, veuillez agréer, cher(e) membre, l'expression de nos salutations distinguées.
Fait à {{ datas.main.locate_city|default('Lieu non défini') }}, le {{ datas.main.ag_date_at_dmy|default('Date non définie') }}
Le {{ (datas.main.president.civ|default('M.') == 'Mme') ? 'Présidente' : 'Président' }} de l'Association,
{{ datas.main.president.civ|default('M.') }} {{ datas.main.president.name|default('Nom') }} {{ datas.main.president.surname|default('Prénom') }}
{% endblock %}

View File

@@ -0,0 +1,20 @@
[E-Cosplay] - {{ system.subject }}
==================================================
{% block content %}
{% endblock %}
==================================================
Si vous ne parvenez pas à cliquer sur un lien dans cet e-mail, veuillez copier et coller l'URL dans la barre d'adresse de votre navigateur.
---
© {{ "now"|date("Y") }} E-COSPLAY. Tous droits réservés.
E-COSPLAY, 42 rue de saint-quentin, 02800 Beautor, France
Association loi 1901 à but non lucratif. - RNA N°W022006988
Retrouvez-nous sur :
Facebook : https://www.facebook.com/assocationecosplay/
Instagram : https://www.instagram.com/asso_ecosplay/

View File

@@ -0,0 +1,26 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
Nouveau Message de Contact
=========================
Vous avez reçu un nouveau message via le formulaire de contact du site.
--- DÉTAILS DE L'EXPÉDITEUR ---
Expéditeur : {{ datas.dto.surname }} {{ datas.dto.name }}
E-mail : {{ datas.dto.email }}
{% if datas.dto.tel is not empty %}Tél : {{ datas.dto.tel }}{% endif %}
--------------------------------
Objet du Message : {{ datas.dto.subject }}
Contenu du Message :
--------------------------------------------------
{{ datas.dto.message | raw }}
--------------------------------------------------
Pour répondre à l'expéditeur, utilisez l'adresse e-mail ci-dessous :
Répondre à : mailto:{{ datas.dto.email }}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
✅ Paiement Confirmé
====================
Bonjour {{ datas.pseudo }},
Nous vous confirmons la bonne réception de votre paiement de cotisation. Merci beaucoup pour votre soutien ! Votre adhésion est maintenant active.
--- DÉTAILS DE VOTRE ADHÉSION ---
Période d'Adhésion : Du {{ datas.start_at|date('d/m/Y') }} au {{ datas.end_at|date('d/m/Y') }}
Montant Payé : {{ datas.amount|format_currency('EUR', locale='fr') }}
---------------------------------
Une question ? N'hésitez pas à nous contacter.
---
Cet e-mail est envoyé automatiquement.
{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
Votre Cotisation Annuelle
=========================
Bonjour {{ datas.pseudo }},
Nous vous remercions de votre engagement. Veuillez trouver ci-dessous les détails de votre cotisation pour la période à venir.
--- DÉTAILS DE LA COTISATION ---
Période : Du {{ datas.start_at|date('d/m/Y') }} au {{ datas.end_at|date('d/m/Y') }}
Montant total : {{ datas.amount|format_currency('EUR', locale='fr') }}
---------------------------------
Pour renouveler votre adhésion, veuillez procéder au paiement sécurisé en utilisant le lien ci-dessous :
Procéder au Paiement Sécurisé :
{{ datas.link }}
Si le lien ci-dessus ne fonctionne pas, veuillez copier-coller l'adresse suivante dans votre navigateur :
{{ datas.link }}
---
Cet e-mail est envoyé automatiquement. Merci de ne pas y répondre.
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
Confirmation de votre Don
Un immense merci pour votre générosité !
==================================================
Nous avons bien reçu votre soutien d'un montant de :
>>> {{ datas.don.amount|format_currency('EUR', locale='fr') }} <<<
Bonjour {% if datas.don.name %}{{ datas.don.name }}{% else %}Cher Donateur{% endif %},
Votre don a été confirmé avec succès. Votre contribution est essentielle pour l'organisation de nos événements, l'achat de matériel et le maintien de nos activités.
--- DÉTAILS DE VOTRE TRANSACTION ---
Nom/Pseudo : {% if datas.don.name %}{{ datas.don.name }}{% else %}Anonyme{% endif %}
Montant : {{ datas.don.amount|format_currency('EUR', locale='fr') }}
{% if datas.don.message %}
Message :
"{{ datas.don.message }}"
{% endif %}
--------------------------------------------------
==================================================
Votre reçu fiscal se trouve en pièce jointe de cet e-mail.
NOTE IMPORTANTE : Notre association ne vous permet pas de bénéficier d'une réduction d'impôt.
Si vous avez des questions, n'hésitez pas à nous contacter.
Cordialement,
L'équipe E-Cosplay
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
==================================================
🚨 NOUVEAU DON ARRIVÉ ! 🚨
==================================================
Un nouvel acte de générosité a été enregistré. Voici les détails :
Montant du Don : {{ datas.don.amount|format_currency('EUR', locale='fr') }}
--- DÉTAILS DU DONATEUR ---
Donateur : {% if datas.don.name %}{{ datas.don.name }}{% else %}Anonyme / Non spécifié{% endif %}
E-mail : {{ datas.don.email }}
---------------------------
{% if datas.don.message %}
Message laissé par le donateur :
--------------------------------------------------
"{{ datas.don.message }}"
--------------------------------------------------
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
Bonjour,
Nous avons le plaisir de vous informer que votre compte administrateur a été créé.
Voici vos identifiants de connexion temporaires :
--------------------------------------------------
Nom d'utilisateur : {{ datas.username }}
Mot de passe : {{ datas.password }}
--------------------------------------------------
Pour des raisons de sécurité, nous vous demandons de bien vouloir modifier votre mot de passe lors de votre première connexion.
Vous pouvez vous connecter à votre compte en utilisant le lien ci-dessous :
Lien de connexion : {{ system.path }}{{ datas.url }}
Si vous avez des questions ou rencontrez des difficultés, n'hésitez pas à nous contacter.
Cordialement,
L'équipe E-Cosplay
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'txt-mails/base.twig' %}
{% block content %}
Bonjour,
{% if 'ROLE_CUSTOMER' in datas.account.roles %}
Nous avons reçu une demande de réinitialisation de mot de passe pour votre espace client.
{% else %}
Nous avons reçu une demande de réinitialisation de mot de passe pour votre compte E-Cosplay.
{% endif %}
Pour réinitialiser votre mot de passe, veuillez cliquer sur le lien ci-dessous. Ce lien est valable pour une durée limitée.
--------------------------------------------------
Réinitialiser mon mot de passe :
{{ datas.resetLink }}
--------------------------------------------------
Ce lien expirera le {{ datas.request.expiresAt|date('d/m/Y à H:i') }}.
Veuillez l'utiliser avant cette date et heure.
Si vous n'avez pas demandé cette réinitialisation de mot de passe, veuillez ignorer cet e-mail. Votre mot de passe actuel restera inchangé.
Cordialement,
L'équipe E-Cosplay
{% endblock %}

View File

@@ -616,3 +616,54 @@ shop.sales_note_main: >
shop.sales_note_details: >
请注意,针对特定的 Cosplay 写真印品销售收益分配如下5% 用于支付协会的银行手续费95% 直接返还给制作该印品的会员。
shop_more: '了解更多'
# Used in the <head> section
doc_page:
title: "官方文件页面"
description: "协会的官方文件列表。"
breadcrumb: "文件"
breadcrumb:
home: "首页"
# Used in the <body> section
doc_list:
title: "文件列表"
ag_ordinaire_title: "普通会员大会"
ag_extraordinaire_title: "特别会员大会"
ag:
date_at_prefix: "于"
president_label: "主席"
secretary_label: "秘书"
main_members_label: "主要参会者"
view_document_link: "查看文件"
info:
title: "会员大会详情"
date_time: "日期和时间"
location: "地点"
type: "大会类型"
content:
signatures_list: "出席签到名单 (共%count%名成员)"
no_signatures_yet: "暂未记录任何出席签名。"
adh_age:
title: "会员大会"
description: "会员大会信息和签到跟踪。"
adh_page:
breadcrumb: "会员大会成员"
global:
at: "于" # Used for time, e.g., "at 10:00" -> "于 10:00"
signed_link: "已签署 (查看)"
member:
civility: "称谓"
name_surname: "姓名"
signature: "签到状态"
adh_page_validate:
title: "签名验证"
description: "确认您已签署会员大会文件。"
breadcrumb: "签名已验证"
success_message: "您的签名已成功记录并验证。您现在可以查看已签署的文件。"
thanks: "感谢您的参与!"

View File

@@ -684,3 +684,54 @@ shop.sales_note_main: >
shop.sales_note_details: >
Please note that for specific cosplay print sales, the distribution is as follows: 5% of the amount covers the association's banking fees, and 95% is directly remitted to the member who created the print.
shop_more: 'Learn more'
# Used in the <head> section
doc_page:
title: "Official Documents Page"
description: "List of the association's official documents."
breadcrumb: "Documents"
breadcrumb:
home: "Home"
# Used in the <body> section
doc_list:
title: "List of Documents"
ag_ordinaire_title: "Ordinary General Assemblies (OGA)"
ag_extraordinaire_title: "Extraordinary General Assemblies (EGA)"
ag:
date_at_prefix: "On" # e.g., "On 23/11/2025"
president_label: "President"
secretary_label: "Secretary"
main_members_label: "Main Participants"
view_document_link: "View Document"
info:
title: "General Assembly Details"
date_time: "Date & Time"
location: "Location"
type: "Type of GA"
content:
signatures_list: "Attendance Signatures List (%count% members)"
no_signatures_yet: "No attendance signatures recorded yet."
adh_age:
title: "General Assembly"
description: "Information and signature tracking for the General Assembly."
adh_page:
breadcrumb: "GA Members"
global:
at: "at"
signed_link: "Signed (View)"
member:
civility: "Title"
name_surname: "Name Surname"
signature: "Signature Status"
adh_page_validate:
title: "Signature Validation"
description: "Confirmation of your signature for the General Assembly."
breadcrumb: "Signature Validated"
success_message: "Your signature has been successfully recorded and validated for the General Assembly. You can now view the signed document."
thanks: "Thank you for your participation!"

View File

@@ -629,3 +629,51 @@ soissons: Soissons
gauchy: Gauchy
quessy: Quessy
guny: Guny
doc_page:
title: "Page des Documents"
description: "Liste des documents officiels de l'association."
breadcrumb: "Documents"
breadcrumb:
home: "Accueil"
doc_list:
title: "Liste des documents"
ag_ordinaire_title: "Assemblées Générales Ordinaires"
ag_extraordinaire_title: "Assemblées Générales Extraordinaires"
ag:
date_at_prefix: "Le" # e.g., "Le 23/11/2025"
president_label: "Président"
secretary_label: "Secrétaire"
main_members_label: "Participants principaux"
view_document_link: "Voir le document"
info:
title: "Détails de l'Assemblée Générale"
date_time: "Date & Heure"
location: "Lieu"
type: "Nature de l'AG"
content:
signatures_list: "Liste des Signatures de Présence (%count% membres)"
no_signatures_yet: "Aucune signature de présence enregistrée pour le moment."
adh_age:
title: "Assemblée Générale"
description: "Informations et suivi des signatures de l'Assemblée Générale."
adh_page:
breadcrumb: "AG Adhérents"
global:
at: "à"
signed_link: "Signée (Voir)"
member:
civility: "Civilité"
name_surname: "Nom Prénom"
signature: "Statut de Signature"
adh_page_validate:
title: "Validation de Signature"
description: "Confirmation de votre signature pour l'Assemblée Générale."
breadcrumb: "Signature validée"
success_message: "Votre signature a été enregistrée et validée avec succès pour l'Assemblée Générale. Vous pouvez consulter le document signé."
thanks: "Merci pour votre participation !"