From a3dc9f58018a7f087c45c5de68f77abbb6e45f8e Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Sat, 22 Nov 2025 20:36:20 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(templates/cota.twig):=20?= =?UTF-8?q?Ajoute=20template=20pour=20confirmation=20cotisation=20?= =?UTF-8?q?=E2=9C=A8=20feat(templates/admin/dashboard.twig):=20Affiche=20s?= =?UTF-8?q?tats=20membres=20et=20commandes=20=F0=9F=90=9B=20fix(src/Contro?= =?UTF-8?q?ller/WebhooksController.php):=20G=C3=A8re=20paiement=20et=20re?= =?UTF-8?q?=C3=A7u=20cotisation=20=E2=9C=A8=20feat(src/Service/Payments/Pa?= =?UTF-8?q?ymentClient.php):=20Ajoute=20paiement=20cotisation=20=E2=9C=A8?= =?UTF-8?q?=20feat(.env):=20Met=20=C3=A0=20jour=20URL=20de=20dev=20?= =?UTF-8?q?=E2=9C=A8=20feat(src/Controller/Admin/AdminController.php):=20A?= =?UTF-8?q?joute=20validation=20et=20lien=20paiement=20=E2=9C=A8=20feat(sr?= =?UTF-8?q?c/Controller/DonsController.php):=20Ajoute=20route=20validation?= =?UTF-8?q?=20cotisation=20=E2=9C=A8=20feat(assets/admin.js):=20Ajoute=20a?= =?UTF-8?q?ssets=20admin=20=E2=9C=A8=20feat(templates/form=5Fadmin.twig):?= =?UTF-8?q?=20Ajoute=20th=C3=A8me=20formulaire=20admin=20=E2=9C=A8=20feat(?= =?UTF-8?q?assets/admin.scss):=20Ajoute=20style=20admin=20=E2=9C=A8=20feat?= =?UTF-8?q?(src/Service/Pdf/CotaReceiptGenerator.php):=20G=C3=A9n=C3=A8re?= =?UTF-8?q?=20re=C3=A7u=20de=20cotisation=20=E2=9C=A8=20feat(src/Form/Memb?= =?UTF-8?q?ersType.php):=20Ajoute=20champs=20et=20options=20formulaire=20m?= =?UTF-8?q?embre=20=E2=9C=A8=20feat(templates/admin/base.twig):=20Ajoute?= =?UTF-8?q?=20base=20admin=20=E2=9C=A8=20feat(templates/admin/member/add.t?= =?UTF-8?q?wig):=20Ajoute=20template=20ajout/=C3=A9dition=20membre=20?= =?UTF-8?q?=E2=9C=A8=20feat(src/Entity/Members.php):=20Ajoute=20champs=20e?= =?UTF-8?q?t=20relations=20entit=C3=A9=20Membre=20=E2=9C=A8=20feat(templat?= =?UTF-8?q?es/admin/members.twig):=20Affiche=20liste=20membres=20=E2=9C=A8?= =?UTF-8?q?=20feat(templates/mails/coti=5Fpayment.twig):=20Ajoute=20templa?= =?UTF-8?q?te=20mail=20paiement=20cotisation=20=E2=9C=A8=20feat(src/Contro?= =?UTF-8?q?ller/MembersController.php):=20Filtre=20membres=20actifs=20?= =?UTF-8?q?=E2=9C=A8=20feat(templates/mails/cota=5Fvalidation.twig):=20Ajo?= =?UTF-8?q?ute=20template=20mail=20validation=20cota=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- assets/admin.js | 3 + assets/admin.scss | 1 + migrations/Version20251122180336.php | 35 +++ migrations/Version20251122185520.php | 37 +++ migrations/Version20251122185748.php | 32 ++ migrations/Version20251122190432.php | 32 ++ src/Controller/Admin/AdminController.php | 93 +++++- src/Controller/DonsController.php | 13 + src/Controller/MembersController.php | 4 +- src/Controller/WebhooksController.php | 97 ++++-- src/Entity/Members.php | 88 ++++++ src/Entity/MembersCotisations.php | 110 +++++++ src/Form/MembersType.php | 54 +++- .../MembersCotisationsRepository.php | 43 +++ src/Service/Payments/PaymentClient.php | 26 ++ src/Service/Pdf/CotaReceiptGenerator.php | 175 +++++++++++ templates/admin/base.twig | 210 ++++++------- templates/admin/dashboard.twig | 78 ++++- templates/admin/member/add.twig | 284 +++++++++++++----- templates/admin/members.twig | 103 +++++-- templates/cota.twig | 54 ++++ templates/form_admin.twig | 148 +++++++++ templates/mails/cota_validation.twig | 52 ++++ templates/mails/coti_payment.twig | 70 +++++ vite.config.js | 1 + 26 files changed, 1591 insertions(+), 254 deletions(-) create mode 100644 assets/admin.js create mode 100644 assets/admin.scss create mode 100644 migrations/Version20251122180336.php create mode 100644 migrations/Version20251122185520.php create mode 100644 migrations/Version20251122185748.php create mode 100644 migrations/Version20251122190432.php create mode 100644 src/Entity/MembersCotisations.php create mode 100644 src/Repository/MembersCotisationsRepository.php create mode 100644 src/Service/Pdf/CotaReceiptGenerator.php create mode 100644 templates/cota.twig create mode 100644 templates/form_admin.twig create mode 100644 templates/mails/cota_validation.twig create mode 100644 templates/mails/coti_payment.twig diff --git a/.env b/.env index 8fe3bd5..a7c775f 100644 --- a/.env +++ b/.env @@ -55,7 +55,7 @@ PATH_URL=https://esyweb.local STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE79Tr8treeHX9KMcZtvcQZ0X8VSm00Q6GQ365V STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR STRIPE_WEBHOOKS_SIGN=whsec_0DOZJAwgMwkcHl2RWXI8h8YItj9q7v3A -DEV_URL=https://3ea1cf1b1555.ngrok-free.app +DEV_URL=https://f584469e204f.ngrok-free.app VAPID_PK=DsOg7jToRSD-VpNSV1Gt3YAhSwz4l-nqeu7yFvzbSxg VAPID_PC=BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..bf73389 --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,3 @@ +import './admin.scss' + +import * as Turbo from "@hotwired/turbo" diff --git a/assets/admin.scss b/assets/admin.scss new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/assets/admin.scss @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/migrations/Version20251122180336.php b/migrations/Version20251122180336.php new file mode 100644 index 0000000..5b84484 --- /dev/null +++ b/migrations/Version20251122180336.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE members ADD joined_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE members ADD status VARCHAR(255) DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN members.joined_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 members DROP joined_at'); + $this->addSql('ALTER TABLE members DROP status'); + } +} diff --git a/migrations/Version20251122185520.php b/migrations/Version20251122185520.php new file mode 100644 index 0000000..2ea3e4b --- /dev/null +++ b/migrations/Version20251122185520.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE members_cotisations (id SERIAL NOT NULL, members_id INT DEFAULT NULL, startd_ate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, end_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, amount DOUBLE PRECISION DEFAULT NULL, is_paid BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C7D1EB64BD01F5ED ON members_cotisations (members_id)'); + $this->addSql('COMMENT ON COLUMN members_cotisations.startd_ate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN members_cotisations.end_date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE members_cotisations ADD CONSTRAINT FK_C7D1EB64BD01F5ED FOREIGN KEY (members_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 members_cotisations DROP CONSTRAINT FK_C7D1EB64BD01F5ED'); + $this->addSql('DROP TABLE members_cotisations'); + } +} diff --git a/migrations/Version20251122185748.php b/migrations/Version20251122185748.php new file mode 100644 index 0000000..05cc667 --- /dev/null +++ b/migrations/Version20251122185748.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE members ADD email 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 members DROP email'); + } +} diff --git a/migrations/Version20251122190432.php b/migrations/Version20251122190432.php new file mode 100644 index 0000000..09f4aec --- /dev/null +++ b/migrations/Version20251122190432.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE members_cotisations ADD payment_id 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 members_cotisations DROP payment_id'); + } +} diff --git a/src/Controller/Admin/AdminController.php b/src/Controller/Admin/AdminController.php index 8ac0120..78a252e 100644 --- a/src/Controller/Admin/AdminController.php +++ b/src/Controller/Admin/AdminController.php @@ -5,13 +5,18 @@ namespace App\Controller\Admin; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; use App\Entity\Members; +use App\Entity\MembersCotisations; use App\Entity\Products; use App\Form\MembersType; use App\Form\ProductsType; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; +use App\Repository\MembersCotisationsRepository; use App\Repository\MembersRepository; use App\Repository\ProductsRepository; +use App\Service\Mailer\Mailer; +use App\Service\Payments\PaymentClient; +use App\Service\Pdf\CotaReceiptGenerator; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; use Doctrine\ORM\EntityManagerInterface; @@ -20,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; @@ -29,9 +35,10 @@ class AdminController extends AbstractController { #[Route(path: '/admin', name: 'admin_dashboard', options: ['sitemap' => false], methods: ['GET'])] - public function adminDashboard(): Response + public function adminDashboard(MembersRepository $membersRepository): Response { return $this->render('admin/dashboard.twig', [ + 'memberCount' => $membersRepository->count(), ]); } #[Route(path: '/admin/products', name: 'admin_products', options: ['sitemap' => false], methods: ['GET'])] @@ -97,7 +104,7 @@ class AdminController extends AbstractController ]); } #[Route(path: '/admin/members/{id}', name: 'admin_member_edit', options: ['sitemap' => false], methods: ['GET','POST'])] - public function adminMembersEdit(?Members $members,Request $request,EntityManagerInterface $entityManager): Response + public function adminMembersEdit(Mailer $mailer,PaymentClient $paymentClient,MembersCotisationsRepository $membersCotisationsRepository,?Members $members,Request $request,EntityManagerInterface $entityManager): Response { $form = $this->createForm(MembersType::class, $members); $form->handleRequest($request); @@ -109,6 +116,59 @@ class AdminController extends AbstractController $entityManager->flush(); return $this->redirectToRoute('admin_member_edit',['id'=>$members->getId()]); } + + if($request->query->has('idValidateCota')) { + $cota = $membersCotisationsRepository->find($request->query->get('idValidateCota')); + $cota->setIsPaid(true); + $entityManager->persist($cota); + $entityManager->flush(); + + $pdfGenerator = new CotaReceiptGenerator(); + + $v = new \DateTime(); + $donationData = [ + 'pseudo' => $cota->getMembers()->getPseudo(), + 'email' => $cota->getMembers()->getEmail(), + 'amount' => $cota->getAmount(), + 'date' => $v->format('Y-m-d'), + 'period' => $cota->getStartdATE()->format('d/m/Y')." - ".$cota->getEnddate()->format('d/m/Y'), + ]; + $files = []; + $pdfContent = $pdfGenerator->generate( + $donationData, + 'recu_cotisation_E-Cosplay.pdf', + 'S' + ); + $files[] = new DataPart($pdfContent, 'recu_cotisation_E-Cosplay.pdf', 'application/pdf'); + + $mailer->send($members->getEmail(),$members->getPseudo(), '[E-Cosplay] - Confirmation de votre paiement de votre cotisation', "mails/cota_validation.twig", [ + 'pseudo' => $members->getPseudo(), + 'start_at'=> $cota->getStartdATE(), + 'end_at'=> $cota->getEnddate(), + 'amount' => $cota->getAmount(), + ], $files); + return $this->redirectToRoute('admin_member_edit',['id'=>$members->getId()]); + + } + if($request->query->has('idLinkCota')) { + $cota = $membersCotisationsRepository->find($request->query->get('idLinkCota')); + $link = $paymentClient->paymentCota($cota); + $cota->setPaymentId($link->id); + $entityManager->persist($cota); + $entityManager->flush(); + $paymentLink = $link->url; + + $mailer->send($members->getEmail(),$members->getPseudo(),"[E-Cosplay] - Lien de paiement de votre cotisation","mails/coti_payment.twig",[ + 'pseudo'=>$members->getPseudo(), + 'link'=>$paymentLink, + 'amount' => $cota->getAmount(), + 'start_at' => $cota->getStartdATE(), + 'end_at' => $cota->getEndDate(), + ]); + return $this->redirectToRoute('admin_member_edit',['id'=>$members->getId()]); + + } + return $this->render('admin/member/add.twig', [ 'form' => $form->createView(), 'member' => $members, @@ -118,24 +178,42 @@ class AdminController extends AbstractController public function adminMembersCreate(Request $request,EntityManagerInterface $entityManager): Response { $members = new Members(); + $members->setRole('Membre'); $members->setTrans(false); $members->setCrosscosplayer(false); $members->setCosplayer(false); $form = $this->createForm(MembersType::class, $members); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + + $dateTimeStart = \DateTimeImmutable::createFromFormat('d/m/Y', $members->getJoinedAt()); + $dateTimeEnd = \DateTimeImmutable::createFromFormat('d/m/Y', $members->getJoinedAt()); + $dateTimeEnd = $dateTimeEnd->modify("+1 year"); + $memberCota = new MembersCotisations(); + $memberCota->setMembers($members); + $memberCota->setStartdATE($dateTimeStart); + $memberCota->setEndDATE($dateTimeEnd); + $memberCota->setIsPaid(false); + $memberCota->setAmount(10); + $entityManager->persist($memberCota); $entityManager->persist($members); $entityManager->flush(); + return $this->redirectToRoute('admin_members'); } return $this->render('admin/member/add.twig', [ 'form' => $form->createView(), + 'member' => $members, ]); } #[Route(path: '/admin/members/delete/{id}', name: 'admin_member_delete', options: ['sitemap' => false], methods: ['GET'])] - public function adminMembersDelete(): Response + public function adminMembersDelete(?Members $members,EntityManagerInterface $entityManager): Response { - + if($members instanceof Members){ + $entityManager->remove($members); + $entityManager->flush(); + } + return $this->redirectToRoute('admin_members'); } #[Route(path: '/admin/events', name: 'admin_events', options: ['sitemap' => false], methods: ['GET'])] @@ -151,4 +229,11 @@ class AdminController extends AbstractController return $this->render('admin/dashboard.twig', [ ]); } + + #[Route(path: '/admin/ag', name: 'admin_ag', options: ['sitemap' => false], methods: ['GET'])] + public function adminAg(): Response + { + return $this->render('admin/ag.twig', [ + ]); + } } diff --git a/src/Controller/DonsController.php b/src/Controller/DonsController.php index dea6233..fe53134 100644 --- a/src/Controller/DonsController.php +++ b/src/Controller/DonsController.php @@ -6,8 +6,10 @@ use App\Dto\Contact\ContactType; use App\Dto\Contact\DtoContact; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; +use App\Entity\MembersCotisations; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; +use App\Repository\MembersCotisationsRepository; use App\Service\Mailer\Mailer; use App\Service\Payments\PaymentClient; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; @@ -27,6 +29,17 @@ use Twig\Environment; class DonsController extends AbstractController { + #[Route(path: '/cota/{id}/validation', name: 'app_cota_validate', options: ['sitemap' => false], methods: ['GET','POST'])] + public function cotaValidation(?PaymentClient $paymentClient,Request $request,?MembersCotisations $membersCotisations): Response + { + if(!$membersCotisations instanceof MembersCotisations){ + return $this->redirectToRoute('app_login'); + } + return $this->render('cota.twig',[ + 'payment' => $membersCotisations + ]); + } + #[Route(path: '/dons', name: 'app_dons', options: ['sitemap' => false], methods: ['GET','POST'])] public function index(Request $request,PaymentClient $paymentClient): Response { diff --git a/src/Controller/MembersController.php b/src/Controller/MembersController.php index 7d50c6f..900703b 100644 --- a/src/Controller/MembersController.php +++ b/src/Controller/MembersController.php @@ -30,7 +30,7 @@ class MembersController extends AbstractController $board_members =[]; $members =[]; - foreach ($membersRepository->findAll() as $member) { + foreach ($membersRepository->findBy(['status'=>'actif']) as $member) { if($member->getRole() == "Président(e)" || $member->getRole() == "Trésorier(e)" || $member->getRole() == "Secrétaire(e)" || @@ -47,7 +47,7 @@ class MembersController extends AbstractController 'orientation' => $member->getOrientation(), ]; } - foreach ($membersRepository->findAll() as $member) { + foreach ($membersRepository->findBy(['status'=>'actif']) as $member) { if($member->getRole() == "Membres(e)") $board_members[] = [ 'pseudo' => $member->getPseudo(), diff --git a/src/Controller/WebhooksController.php b/src/Controller/WebhooksController.php index 36c0a9d..80d4ced 100644 --- a/src/Controller/WebhooksController.php +++ b/src/Controller/WebhooksController.php @@ -7,10 +7,13 @@ use App\Dto\Contact\DtoContact; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; use App\Entity\Dons; +use App\Entity\MembersCotisations; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; +use App\Repository\MembersCotisationsRepository; use App\Service\Mailer\Mailer; use App\Service\Payments\PaymentClient; +use App\Service\Pdf\CotaReceiptGenerator; use App\Service\Pdf\DonReceiptGenerator; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; @@ -31,7 +34,7 @@ class WebhooksController extends AbstractController { #[Route(path: '/webhooks', name: 'app_webhooks', options: ['sitemap' => false], methods: ['POST'])] - public function index(DonReceiptGenerator $donReceiptGenerator,Request $request,PaymentClient $paymentClient,Mailer $mailer,EntityManagerInterface $entityManager,): Response + public function index(DonReceiptGenerator $donReceiptGenerator,MembersCotisationsRepository $membersCotisationsRepository,Request $request,PaymentClient $paymentClient,Mailer $mailer,EntityManagerInterface $entityManager,): Response { $content = $request->getContent(); if($paymentClient->validateWebhooks($content)) { @@ -39,39 +42,73 @@ class WebhooksController extends AbstractController $object = $content->data->object; $metadata = $object->metadata; if($object->status == "complete") { - $dons = new Dons(); - $dons->setName($metadata->name); - $dons->setEmail($metadata->email); - $dons->setAmount($metadata->amount); - $dons->setMessage($metadata->message); - $entityManager->persist($dons); - $pdfGenerator = new DonReceiptGenerator(); + if($metadata->id_con) { + $me= $membersCotisationsRepository->find($metadata->id_con); + if($me instanceof MembersCotisations) { + $me->setIsPaid(true); + $entityManager->persist($me); + $entityManager->flush(); - $v= new \DateTime(); - $donationData = [ - 'name' => $metadata->name, - 'email' => $metadata->email, - 'amount' => $metadata->amount, - 'date' => $v->format('Y-m-d'), - 'message' => $metadata->message ?? null, - ]; - $files = []; - $pdfContent = $pdfGenerator->generate( - $donationData, - 'recu_don_E-Cosplay.pdf', - 'S' - ); - $files[] = new DataPart($pdfContent,'recu_don_E-Cosplay.pdf','application/pdf'); + $pdfGenerator = new CotaReceiptGenerator(); - $mailer->send($metadata->email,$metadata->name,'[E-Cosplay] - Confirmation de votre don de '.$metadata->amount." €","mails/dons.twig",[ - 'don' => $dons, - ],$files); + $v = new \DateTime(); + $donationData = [ + 'pseudo' => $me->getMembers()->getPseudo(), + 'email' => $me->getMembers()->getEmail(), + 'amount' => $me->getAmount(), + 'date' => $v->format('Y-m-d'), + 'period' => $me->getStartdATE()->format('d/m/Y')." - ".$me->getEnddate()->format('d/m/Y'), + ]; + $files = []; + $pdfContent = $pdfGenerator->generate( + $donationData, + 'recu_cotisation_E-Cosplay.pdf', + 'S' + ); + $files[] = new DataPart($pdfContent, 'recu_cotisation_E-Cosplay.pdf', 'application/pdf'); - $mailer->send($metadata->email,$metadata->name,'[E-Cosplay] - Confirmation d\'un don de '.$metadata->amount." €","mails/dons_new.twig",[ - 'don' => $dons, - ]); + $mailer->send($me->getMembers()->getEmail(),$me->getMembers()->getPseudo(), '[E-Cosplay] - Confirmation de votre paiement de votre cotisation', "mails/cota_validation.twig", [ + 'pseudo' => $me->getMembers()->getPseudo(), + 'start_at'=> $me->getStartdATE(), + 'end_at'=> $me->getEnddate(), + 'amount' => $me->getAmount(), + ], $files); + } + } else { + $dons = new Dons(); + $dons->setName($metadata->name); + $dons->setEmail($metadata->email); + $dons->setAmount($metadata->amount); + $dons->setMessage($metadata->message); + $entityManager->persist($dons); + $pdfGenerator = new DonReceiptGenerator(); - $entityManager->flush(); + $v = new \DateTime(); + $donationData = [ + 'name' => $metadata->name, + 'email' => $metadata->email, + 'amount' => $metadata->amount, + 'date' => $v->format('Y-m-d'), + 'message' => $metadata->message ?? null, + ]; + $files = []; + $pdfContent = $pdfGenerator->generate( + $donationData, + 'recu_don_E-Cosplay.pdf', + 'S' + ); + $files[] = new DataPart($pdfContent, 'recu_don_E-Cosplay.pdf', 'application/pdf'); + + $mailer->send($metadata->email, $metadata->name, '[E-Cosplay] - Confirmation de votre don de ' . $metadata->amount . " €", "mails/dons.twig", [ + 'don' => $dons, + ], $files); + + $mailer->send($metadata->email, $metadata->name, '[E-Cosplay] - Confirmation d\'un don de ' . $metadata->amount . " €", "mails/dons_new.twig", [ + 'don' => $dons, + ]); + + $entityManager->flush(); + } } } return $this->json([]); diff --git a/src/Entity/Members.php b/src/Entity/Members.php index 7c8723d..119471e 100644 --- a/src/Entity/Members.php +++ b/src/Entity/Members.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\MembersRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; use Vich\UploaderBundle\Mapping\Annotation as Vich; @@ -50,6 +52,26 @@ class Members #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updateAt; + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $joinedAt = null; + + #[ORM\Column(length: 255,nullable: true)] + private ?string $status = null; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: MembersCotisations::class, mappedBy: 'members')] + private Collection $membersCotisations; + + #[ORM\Column(length: 255,nullable: true)] + private ?string $Email = null; + + public function __construct() + { + $this->membersCotisations = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -254,4 +276,70 @@ class Members { $this->memberSize = $memberSize; } + + public function getJoinedAt(): ?\DateTimeImmutable + { + return $this->joinedAt; + } + + public function setJoinedAt(?\DateTimeImmutable $joinedAt): static + { + $this->joinedAt = $joinedAt; + + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(string $status): static + { + $this->status = $status; + + return $this; + } + + /** + * @return Collection + */ + public function getMembersCotisations(): Collection + { + return $this->membersCotisations; + } + + public function addMembersCotisation(MembersCotisations $membersCotisation): static + { + if (!$this->membersCotisations->contains($membersCotisation)) { + $this->membersCotisations->add($membersCotisation); + $membersCotisation->setMembers($this); + } + + return $this; + } + + public function removeMembersCotisation(MembersCotisations $membersCotisation): static + { + if ($this->membersCotisations->removeElement($membersCotisation)) { + // set the owning side to null (unless already changed) + if ($membersCotisation->getMembers() === $this) { + $membersCotisation->setMembers(null); + } + } + + return $this; + } + + public function getEmail(): ?string + { + return $this->Email; + } + + public function setEmail(string $Email): static + { + $this->Email = $Email; + + return $this; + } } diff --git a/src/Entity/MembersCotisations.php b/src/Entity/MembersCotisations.php new file mode 100644 index 0000000..01fa05d --- /dev/null +++ b/src/Entity/MembersCotisations.php @@ -0,0 +1,110 @@ +id; + } + + public function getMembers(): ?Members + { + return $this->members; + } + + public function setMembers(?Members $members): static + { + $this->members = $members; + + return $this; + } + + public function getStartdATE(): ?\DateTimeImmutable + { + return $this->startdATE; + } + + public function setStartdATE(\DateTimeImmutable $startdATE): static + { + $this->startdATE = $startdATE; + + return $this; + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function setEndDate(\DateTimeImmutable $endDate): static + { + $this->endDate = $endDate; + + return $this; + } + + public function getAmount(): ?float + { + return $this->amount; + } + + public function setAmount(?float $amount): static + { + $this->amount = $amount; + + return $this; + } + + public function isPaid(): ?bool + { + return $this->isPaid; + } + + public function setIsPaid(bool $isPaid): static + { + $this->isPaid = $isPaid; + + return $this; + } + + public function getPaymentId(): ?string + { + return $this->paymentId; + } + + public function setPaymentId(?string $paymentId): static + { + $this->paymentId = $paymentId; + + return $this; + } +} diff --git a/src/Form/MembersType.php b/src/Form/MembersType.php index 71abb62..0e4a96b 100644 --- a/src/Form/MembersType.php +++ b/src/Form/MembersType.php @@ -5,6 +5,8 @@ namespace App\Form; use App\Entity\Members; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -15,10 +17,10 @@ class MembersType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('members', FileType::class, [ + ->add('members', FileType::class, [ // Renommé pour plus de clarté 'label' => 'Photo du membre (Max 2Mo)', - 'required' => false, // Rendre facultatif - 'mapped' => false, // Indiquer que ce champ n'est pas directement mappé à une propriété de l'entité + 'required' => false, + 'mapped' => false, 'attr' => [ 'placeholder' => 'Choisir un fichier...', ] @@ -27,16 +29,36 @@ class MembersType extends AbstractType 'label' => 'Pseudo du membre', 'required' => true, ]) + ->add('email', EmailType::class, [ + 'label' => 'Email du membre', + 'required' => true, + ]) + ->add('status',ChoiceType::class, [ + 'label' => 'Statut du membre', + 'required' => true, + 'choices' => [ + 'Actif' => 'actif', // Utiliser des valeurs minuscules pour la base de données + 'Inactif' => 'inactif', + 'Suspendu' => 'suspendu', + ], + 'preferred_choices' => ['actif'], + ]) + ->add('joinedAt',DateType::class,[ + 'label' => 'Date de rejoint', // Correction du libellé + 'required' => true, + 'widget' => 'single_text', // Affiche un champ de date simple au lieu d'une liste déroulante + 'input' => 'datetime', + ]) ->add('role', ChoiceType::class, [ 'label' => 'Rôle au sein de l\'association', 'choices' => [ - 'Président(e)' => 'Président(e)', - 'Trésorier(e)' => 'Trésorier(e)', - 'Secrétaire(e)' => 'Secrétaire(e)', - 'Vice-Président(e)' => 'Vice-Président(e)', - 'Trésorier(e) Adjoints' => 'Trésorier(e) Adjoints', - 'Secrétaire(e) Adjoints' => 'Secrétaire(e) Adjoints', - 'Membres(e)' => 'Membres(e)', + 'Président(e)' => 'President', + 'Trésorier(e)' => 'Tresorier', + 'Secrétaire(e)' => 'Secretaire', + 'Vice-Président(e)' => 'VicePresident', + 'Trésorier(e) Adjoints' => 'TresorierAdjoint', + 'Secrétaire(e) Adjoints' => 'SecretaireAdjoint', + 'Membre' => 'Membre', // Corrigé 'Membres(e)' en 'Membre' ], ]) ->add('orientation', ChoiceType::class, [ @@ -54,7 +76,6 @@ class MembersType extends AbstractType 'En questionnement' => 'questioning', 'Autre' => 'other', ], - // Optionnel : affichez-le comme une liste déroulante normale (pas expanded) ]) // Les champs booléens sont mieux affichés comme des boutons radio (expanded) @@ -64,7 +85,7 @@ class MembersType extends AbstractType 'Non' => false, 'Oui' => true, ], - 'expanded' => true, // Affiche comme boutons radio + 'required' => true, 'multiple' => false, ]) ->add('trans', ChoiceType::class, [ @@ -73,7 +94,7 @@ class MembersType extends AbstractType 'Non' => false, 'Oui' => true, ], - 'expanded' => true, + 'required' => true, 'multiple' => false, ]) ->add('cosplayer', ChoiceType::class, [ @@ -82,7 +103,8 @@ class MembersType extends AbstractType 'Non' => false, 'Oui' => true, ], - 'expanded' => true, + 'required' => true, + 'empty_data' => false, 'multiple' => false, ]) ; @@ -90,6 +112,8 @@ class MembersType extends AbstractType public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefault('data_class', Members::class); + $resolver->setDefaults([ + 'data_class' => Members::class, + ]); } } diff --git a/src/Repository/MembersCotisationsRepository.php b/src/Repository/MembersCotisationsRepository.php new file mode 100644 index 0000000..f1e6f2d --- /dev/null +++ b/src/Repository/MembersCotisationsRepository.php @@ -0,0 +1,43 @@ + + */ +class MembersCotisationsRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, MembersCotisations::class); + } + + // /** + // * @return MembersCotisations[] Returns an array of MembersCotisations 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): ?MembersCotisations + // { + // return $this->createQueryBuilder('m') + // ->andWhere('m.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/Payments/PaymentClient.php b/src/Service/Payments/PaymentClient.php index b95ac3c..b61a4ce 100644 --- a/src/Service/Payments/PaymentClient.php +++ b/src/Service/Payments/PaymentClient.php @@ -2,6 +2,7 @@ namespace App\Service\Payments; +use App\Entity\MembersCotisations; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -55,4 +56,29 @@ class PaymentClient return false; } } + + public function paymentCota(MembersCotisations $membersCotisations) { + $stripe = new \Stripe\StripeClient($_ENV['STRIPE_SK']); + $checkoutSession = $stripe->checkout->sessions->create([ + 'customer_email' => $membersCotisations->getMembers()->getEmail(), + 'line_items' => [[ + 'price_data' => [ + 'currency' => 'eur', + 'product_data' => [ + 'name' => "Cotisations", + 'description' => "Cotisations", + ], + 'unit_amount' => $membersCotisations->getAmount()*100, + ], + 'quantity' => 1, + ]], + 'metadata' => [ + 'id_con' => $membersCotisations->getId(), + ], + 'mode' => 'payment', + 'success_url' => $this->url.$this->urlGenerator->generate('app_cota_validate',['id'=>$membersCotisations->getId()]), + ]); + + return $checkoutSession; + } } diff --git a/src/Service/Pdf/CotaReceiptGenerator.php b/src/Service/Pdf/CotaReceiptGenerator.php new file mode 100644 index 0000000..c364c9e --- /dev/null +++ b/src/Service/Pdf/CotaReceiptGenerator.php @@ -0,0 +1,175 @@ +contributionData = $data; + } + + // --- Méthodes FPDF obligatoires ou recommandées --- + + /** + * Entête du document (Logo, Nom de l'association). + */ + public function Header(): void + { + // Logo de l'association E-Cosplay + $logoPath = $_SERVER['DOCUMENT_ROOT'] . '/assets/images/logo.jpg'; + + if (file_exists($logoPath)) { + $this->Image($logoPath, 10, 8, 30); // Position X=10, Y=8, Largeur=30mm (Hauteur auto) + } else { + $this->SetFont('Arial', 'B', 10); + $this->Cell(0, 10, utf8_decode('E-Cosplay Logo'), 0, 0, 'L'); + $this->Ln(5); + } + + // Police de caractères (ex: Arial gras 15) + $this->SetFont('Arial', 'B', 15); + $this->Cell(80); // Ajuster le décalage après le logo + + // --- MISE À JOUR : 'Reçu de Cotisation' --- + $this->Cell(30, 10, utf8_decode('Reçu de Cotisation'), 0, 1, 'C'); + + // Informations de l'association + $this->SetFont('Arial', '', 10); + $this->Cell(0, 5, utf8_decode('E-Cosplay'), 0, 1, 'C'); + $this->Cell(0, 5, utf8_decode('42 RUE DE SAINT-QUENTIN 02800 BEAUTOR'), 0, 1, 'C'); + $this->Cell(0, 5, utf8_decode('SIREN: 943121517 | Code NAF: 93.29Z'), 0, 1, 'C'); + + // Saut de ligne + $this->Ln(15); + } + + /** + * Pied de page du document. + */ + public function Footer(): void + { + $this->SetY(-15); + $this->SetFont('Arial', 'I', 8); + $this->Cell(0, 10, utf8_decode('Page ') . $this->PageNo() . '/{nb}', 0, 0, 'C'); + } + + // --- Méthode de génération spécifique au reçu --- + + /** + * Génère le corps principal du reçu de cotisation. + */ + public function generateReceiptBody(): void + { + // Utilisation de $this->contributionData + if (empty($this->contributionData)) { + $this->Cell(0, 10, utf8_decode('Erreur: Aucune donnée de cotisation n\'est définie.'), 0, 1); + return; + } + + $this->SetFont('Arial', '', 12); + + // Affichage des informations du Membre + $this->SetTextColor(0, 0, 0); // Noir + $this->Cell(0, 10, utf8_decode('Membre:'), 0, 1); // --- MISE À JOUR : 'Membre' --- + + // Récupération et décodage des données du membre + $name = utf8_decode($this->contributionData['name'] ?? 'Non spécifié'); + $email = utf8_decode($this->contributionData['email'] ?? 'Non spécifié'); + $period = utf8_decode($this->contributionData['period'] ?? 'Non spécifiée'); // Ajout potentiel + + $this->SetX(20); + $this->Cell(0, 7, utf8_decode('Nom/Pseudo: ') . $name, 0, 1); + $this->SetX(20); + $this->Cell(0, 7, utf8_decode('E-mail: ') . $email, 0, 1); + + // Affichage de la période d'adhésion si disponible + $this->SetX(20); + $this->Cell(0, 7, utf8_decode('Période d\'adhésion: ') . $period, 0, 1); + + $this->Ln(10); + + // Affichage des détails de la Cotisation + $this->SetFont('Arial', 'B', 14); + $this->SetFillColor(200, 220, 255); + // --- MISE À JOUR : 'Détails de la Cotisation' --- + $this->Cell(0, 10, utf8_decode('Détails de la Cotisation'), 0, 1, 'L', true); + + $this->SetFont('Arial', '', 12); + $this->Ln(3); + + // Montant + $amount = $this->contributionData['amount'] ?? 0; + $formattedAmount = number_format($amount, 2, ',', '.') . ' EUR'; + + $this->SetX(20); + $this->Cell(50, 8, utf8_decode('Montant de la Cotisation:'), 0, 0); // --- MISE À JOUR : 'Cotisation' --- + $this->SetFont('Arial', 'B', 12); + $this->Cell(0, 8, $formattedAmount, 0, 1); + $this->SetFont('Arial', '', 12); + + // Date + $date = $this->contributionData['date'] ?? date('Y-m-d'); + $this->SetX(20); + $this->Cell(50, 8, utf8_decode('Date de la Transaction:'), 0, 0); + $this->Cell(0, 8, $date, 0, 1); + + // Message (si présent) + if (!empty($this->contributionData['message'])) { + $message = utf8_decode($this->contributionData['message']); + $this->Ln(5); + $this->Cell(0, 8, utf8_decode('Note du Membre:'), 0, 1); // --- MISE À JOUR : 'Note du Membre' --- + $this->SetX(20); + $this->MultiCell(0, 6, $message); + } + + $this->Ln(15); + + // --- MISE À JOUR : Mention spécifique pour la Cotisation --- + $infoText = 'Ce document est un reçu attestant du paiement de votre cotisation d\'adhésion à l\'association E-Cosplay pour la période spécifiée. Il valide vos droits de membre.'; + + $this->SetFont('Arial', '', 10); + $this->MultiCell(0, 5, utf8_decode($infoText), 0, 'C'); + + $this->Ln(10); + + // Signature + $this->SetFont('Arial', 'I', 10); + $this->Cell(0, 5, utf8_decode('Fait à BEAUTOR, le ') . date('d/m/Y'), 0, 1, 'R'); + } + + /** + * Méthode publique pour générer et obtenir le PDF. + * @param array $data Les données de la cotisation. + * @param string $filename Le nom du fichier de sortie. + * @param string $dest La destination (I: navigateur, D: téléchargement, F: fichier, S: string). + */ + public function generate(array $data, string $filename = 'Reçu_Cotisation.pdf', string $dest = 'I') + { + // Utilisation de setContributionData + $this->setContributionData($data); + + // Initialisation du PDF + $this->AliasNbPages(); + $this->AddPage(); + + // Génération du contenu + $this->generateReceiptBody(); + + // Sortie du document + // Le nom de fichier est mis à jour + return $this->Output($dest, utf8_decode($filename)); + } +} diff --git a/templates/admin/base.twig b/templates/admin/base.twig index bf6f428..ef7c1c5 100644 --- a/templates/admin/base.twig +++ b/templates/admin/base.twig @@ -1,138 +1,144 @@ -{# Assurez-vous d'utiliser une version de Tailwind CSS qui supporte ces classes #} - + + + + {% block title %}Administration{% endblock %} - E-Cosplay - {# 🛑 NO INDEX DIRECTIVE #} - - {% block title %}Admin Dashboard{% endblock %} | Mon App + + {{ vite_asset('admin.js', []) }} - {# 🎨 Tailwind CSS Integration (Utilisation du CDN pour la démo) #} - {{ vite_asset('app.js',[]) }} + {% block stylesheets %}{% endblock %} + + + + + -{# Utiliser un layout en grille ou flex pour organiser la sidebar et le contenu #} - + -
+ - {# 1. BARRE LATÉRALE (SIDEBAR - FOND BLANC) #} - + - {# 2. CONTENU PRINCIPAL (Inclut la Topbar) #} -
+ + +
- {# 3. TOP BAR (BARRE SUPÉRIEURE) #} -
+ +

+ {{ block('page_title') }} +

- {# Remplacer 'current_user.name' par la variable Twig de votre session #} -
- Bienvenue, {{ app.user.username }} -
+ +
+ {% block body %}{% endblock %} +
+
- {# Vous pourriez ajouter ici un bouton de profil ou un dropdown #} - + + - {# ZONE DE CONTENU PRINCIPALE #} -
-

- {% block page_title %}{% endblock %} -

-
- {% block body %} - - {% endblock %} -
-
-
+ + + {% block javascripts %}{% endblock %} + diff --git a/templates/admin/dashboard.twig b/templates/admin/dashboard.twig index e5e814b..b66d487 100644 --- a/templates/admin/dashboard.twig +++ b/templates/admin/dashboard.twig @@ -1,3 +1,77 @@ {% extends 'admin/base.twig' %} -{% block title %}Tableau de bord{% endblock %} -{% block page_title %}Tableau de bord{% endblock %} + +{# Définition des titres pour le navigateur et l'affichage #} +{% block title 'Tableau de bord' %} +{% block page_title 'Tableau de bord' %} + +{# Variables de données simulées (MOCK DATA) - À REMPLACER par les vraies variables passées au template #} +{% set pendingOrdersCount = 0 %} + +{% block body %} +
+ {# Section des Cartes de Statistiques (KPIs) #} +
+ + {# Carte 1: Nombre de Membres #} +
+
+
+ +
+

+ Membres de l'association +

+
+
+

+ {{ memberCount | number_format(0, ',', ' ') }} +

+

+ Total des inscriptions actives. +

+
+
+ + {# Carte 2: Commandes en Attente #} +
+
+
+ +
+

+ Commandes en attente +

+
+
+

+ {{ pendingOrdersCount }} +

+

+ Nécessitent un traitement. +

+
+
+ + + {# Carte 4: Espace pour un autre KPI (ex: Nouveaux événements) #} +
+
+
+ +
+

+ Événements à venir +

+
+
+

+ 0 +

+

+ Planifiés pour les 3 prochains mois. +

+
+
+
+
+{% endblock %} diff --git a/templates/admin/member/add.twig b/templates/admin/member/add.twig index e0fcd52..3ba7cd8 100644 --- a/templates/admin/member/add.twig +++ b/templates/admin/member/add.twig @@ -1,99 +1,138 @@ {% extends 'admin/base.twig' %} + {% block title %}Ajouter/Éditer un Membre{% endblock %} {% block page_title %} - {{ form.vars.value.id ? 'Éditer le Membre' : 'Créer un nouveau Membre' }} + {{ form.vars.value.id ? 'Éditer le Membre: ' ~ form.vars.value.pseudo : 'Créer un nouveau Membre' }} {% endblock %} {% block body %} -
+ {# --- APPLICATION DU THÈME DE FORMULAIRE DEMANDÉ --- #} + {% form_theme form 'form_admin.twig' %} - {{ form_start(form, {'attr': {'class': 'space-y-6'}}) }} + {# Définir l'entité pour la rendre plus facile à référencer, notamment pour vich_uploader_asset #} + {% set member = form.vars.value %} - {# --- SECTION 1: Informations de base --- #} -

Informations Principales

+ {# Conteneur principal #} +
-
+ {{ form_start(form, {'attr': {'class': 'space-y-8'}}) }} - {# Pseudo #} - {{ form_row(form.pseudo, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }} + {# --- SECTION 1: Informations de base (Grille de 3 colonnes sur grand écran) --- #} +

+ Informations Principales +

- {# Rôle #} - {{ form_row(form.role, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }} + {# Grille de 3 colonnes sur les grands écrans (lg:grid-cols-3) #} +
+ + {# Pseudo - Utilise le style défini dans form_admin.twig #} +
+ {{ form_row(form.pseudo) }} +
+ {# Rôle - Utilise le style défini dans form_admin.twig #} +
+ {{ form_row(form.role) }} +
+ + {# Date de Rejoint - Utilise le style défini dans form_admin.twig #} +
+ {{ form_row(form.joinedAt) }} +
+ + {# Statut - Utilise le style défini dans form_admin.twig #} +
+ {{ form_row(form.status) }} +
+ + {# Orientation - Utilise le style défini dans form_admin.twig #} +
+ {{ form_row(form.orientation) }} +
+
+ {{ form_row(form.email) }} +
- {# L'élément LABEL est stylisé pour devenir le cercle cliquable #} -