feat(security): Ajoute la route de déconnexion et configure la redirection.
 feat(Dto/Ag): Crée les DTOs AgType, AgMembersType et AgOrderType.
 feat(Controller/Admin): Implémente la gestion des AG (CRUD complet).
 feat(templates/admin): Ajoute les templates pour la gestion des AG.
```
This commit is contained in:
Serreau Jovann
2025-11-23 17:06:10 +01:00
parent c4c8ad92be
commit bfc2370d2e
29 changed files with 1653 additions and 22 deletions

View File

@@ -27,7 +27,8 @@ security:
entry_point: App\Security\AuthenticationEntryPoint
custom_authenticator:
- App\Security\LoginFormAuthenticator
logout:
target: app_logout
# Configuration des algorithmes de hachage des mots de passe.
# Symfony choisira automatiquement le meilleur algorithme par défaut si non spécifié,

View File

@@ -0,0 +1,39 @@
<?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 Version20251123150147 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 main (id SERIAL NOT NULL, president_id INT DEFAULT NULL, secretaire_id INT DEFAULT NULL, ag_date_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, locate VARCHAR(255) NOT NULL, locate_zipcode VARCHAR(255) NOT NULL, locate_city VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BF28CD64B40A33C7 ON main (president_id)');
$this->addSql('CREATE INDEX IDX_BF28CD64A90F02B2 ON main (secretaire_id)');
$this->addSql('COMMENT ON COLUMN main.ag_date_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE main ADD CONSTRAINT FK_BF28CD64B40A33C7 FOREIGN KEY (president_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE main ADD CONSTRAINT FK_BF28CD64A90F02B2 FOREIGN KEY (secretaire_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 main DROP CONSTRAINT FK_BF28CD64B40A33C7');
$this->addSql('ALTER TABLE main DROP CONSTRAINT FK_BF28CD64A90F02B2');
$this->addSql('DROP TABLE main');
}
}

View File

@@ -0,0 +1,58 @@
<?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 Version20251123150400 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('DROP SEQUENCE main_id_seq CASCADE');
$this->addSql('CREATE TABLE ag_main (id SERIAL NOT NULL, president_id INT DEFAULT NULL, secretaire_id INT DEFAULT NULL, ag_date_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, locate VARCHAR(255) NOT NULL, locate_zipcode VARCHAR(255) NOT NULL, locate_city VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8173EDAFB40A33C7 ON ag_main (president_id)');
$this->addSql('CREATE INDEX IDX_8173EDAFA90F02B2 ON ag_main (secretaire_id)');
$this->addSql('COMMENT ON COLUMN ag_main.ag_date_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE TABLE ag_main_member (id SERIAL NOT NULL, member_id INT DEFAULT NULL, main_id INT DEFAULT NULL, is_voted_allow BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_7979FD617597D3FE ON ag_main_member (member_id)');
$this->addSql('CREATE INDEX IDX_7979FD61627EA78A ON ag_main_member (main_id)');
$this->addSql('ALTER TABLE ag_main ADD CONSTRAINT FK_8173EDAFB40A33C7 FOREIGN KEY (president_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main ADD CONSTRAINT FK_8173EDAFA90F02B2 FOREIGN KEY (secretaire_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main_member ADD CONSTRAINT FK_7979FD617597D3FE FOREIGN KEY (member_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main_member ADD CONSTRAINT FK_7979FD61627EA78A FOREIGN KEY (main_id) REFERENCES ag_main (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE main DROP CONSTRAINT fk_bf28cd64a90f02b2');
$this->addSql('ALTER TABLE main DROP CONSTRAINT fk_bf28cd64b40a33c7');
$this->addSql('DROP TABLE main');
}
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('CREATE SEQUENCE main_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE main (id SERIAL NOT NULL, president_id INT DEFAULT NULL, secretaire_id INT DEFAULT NULL, ag_date_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, locate VARCHAR(255) NOT NULL, locate_zipcode VARCHAR(255) NOT NULL, locate_city VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX idx_bf28cd64a90f02b2 ON main (secretaire_id)');
$this->addSql('CREATE INDEX idx_bf28cd64b40a33c7 ON main (president_id)');
$this->addSql('COMMENT ON COLUMN main.ag_date_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE main ADD CONSTRAINT fk_bf28cd64a90f02b2 FOREIGN KEY (secretaire_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE main ADD CONSTRAINT fk_bf28cd64b40a33c7 FOREIGN KEY (president_id) REFERENCES members (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ag_main DROP CONSTRAINT FK_8173EDAFB40A33C7');
$this->addSql('ALTER TABLE ag_main DROP CONSTRAINT FK_8173EDAFA90F02B2');
$this->addSql('ALTER TABLE ag_main_member DROP CONSTRAINT FK_7979FD617597D3FE');
$this->addSql('ALTER TABLE ag_main_member DROP CONSTRAINT FK_7979FD61627EA78A');
$this->addSql('DROP TABLE ag_main');
$this->addSql('DROP TABLE ag_main_member');
}
}

View File

@@ -0,0 +1,35 @@
<?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 Version20251123150802 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 main_order (id SERIAL NOT NULL, title VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE ag_main ADD closed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN ag_main.closed_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('DROP TABLE main_order');
$this->addSql('ALTER TABLE ag_main DROP closed_at');
}
}

View File

@@ -0,0 +1,36 @@
<?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 Version20251123150833 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('DROP SEQUENCE main_order_id_seq CASCADE');
$this->addSql('CREATE TABLE ag_order (id SERIAL NOT NULL, title VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('DROP TABLE main_order');
}
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('CREATE SEQUENCE main_order_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE main_order (id SERIAL NOT NULL, title VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('DROP TABLE ag_order');
}
}

View File

@@ -0,0 +1,36 @@
<?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 Version20251123151054 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_order ADD main_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE ag_order ADD CONSTRAINT FK_F9A1D380627EA78A FOREIGN KEY (main_id) REFERENCES ag_main (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_F9A1D380627EA78A ON ag_order (main_id)');
}
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_order DROP CONSTRAINT FK_F9A1D380627EA78A');
$this->addSql('DROP INDEX IDX_F9A1D380627EA78A');
$this->addSql('ALTER TABLE ag_order DROP main_id');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20251123152416 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 is_closed 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 DROP is_closed');
}
}

View File

@@ -0,0 +1,35 @@
<?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 Version20251123160153 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_vote (id SERIAL NOT NULL, member_id INT DEFAULT NULL, role VARCHAR(255) NOT NULL, pour INT NOT NULL, contre INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_A5781BDE7597D3FE ON ag_main_vote (member_id)');
$this->addSql('ALTER TABLE ag_main_vote ADD CONSTRAINT FK_A5781BDE7597D3FE FOREIGN KEY (member_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 ag_main_vote DROP CONSTRAINT FK_A5781BDE7597D3FE');
$this->addSql('DROP TABLE ag_main_vote');
}
}

View File

@@ -2,8 +2,14 @@
namespace App\Controller\Admin;
use App\Dto\Ag\AgMembersType;
use App\Dto\Ag\AgOrderType;
use App\Dto\Ag\AgType;
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\Members;
use App\Entity\MembersCotisations;
use App\Entity\Products;
@@ -11,6 +17,7 @@ use App\Form\MembersType;
use App\Form\ProductsType;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Repository\Ag\MainRepository;
use App\Repository\MembersCotisationsRepository;
use App\Repository\MembersRepository;
use App\Repository\ProductsRepository;
@@ -223,17 +230,99 @@ class AdminController extends AbstractController
]);
}
#[Route(path: '/admin/account', name: 'admin_accounts_list', options: ['sitemap' => false], methods: ['GET'])]
public function adminAccount(): Response
#[Route(path: '/admin/ag', name: 'admin_ag', options: ['sitemap' => false], methods: ['GET'])]
public function adminAg(MainRepository $mainRepository): Response
{
return $this->render('admin/dashboard.twig', [
return $this->render('admin/ag.twig', [
'ags' => $mainRepository->findBy([],['agDateAt'=>'DESC']),
]);
}
#[Route(path: '/admin/ag', name: 'admin_ag', options: ['sitemap' => false], methods: ['GET'])]
public function adminAg(): Response
#[Route(path: '/admin/ag/new', name: 'admin_ag_new', options: ['sitemap' => false], methods: ['GET','POST'])]
public function adminAgNew(Request $request,EntityManagerInterface $entityManager): Response
{
return $this->render('admin/ag.twig', [
$ag = new Main();
$form = $this->createForm(AgType::class,$ag);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$entityManager->persist($ag);
$entityManager->flush();
return $this->redirectToRoute('admin_ag');
}
return $this->render('admin/ag/new.twig', [
'ag' => $form->createView(),
]);
}
#[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->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$entityManager->persist($ag);
$entityManager->flush();
return $this->redirectToRoute('admin_ag_edit',['id'=>$ag->getId()]);
}
$agOrder = new MainOrder();
$agOrder->setMain($ag);
$formOrder = $this->createForm(AgOrderType::class,$agOrder);
$formOrder->handleRequest($request);
if($formOrder->isSubmitted() && $formOrder->isValid()){
$entityManager->persist($agOrder);
$entityManager->flush();
return $this->redirectToRoute('admin_ag_edit',['id'=>$agOrder->getId()]);
}
$agMembers = new MainMember();
$agMembers->setMain($ag);
$formMember = $this->createForm(AgMembersType::class,$agMembers);
$formMember->handleRequest($request);
if($formMember->isSubmitted() && $formMember->isValid()){
$rf = $entityManager->getRepository(Members::class)->find($request->request->all()['ag_members']['member']);
$agMembers->setMember($rf);
$t = new \DateTime();
$tz = new \DateTime();
$tz->modify('+1 year');
$isValid = $agMembers->getMember()->getMembersCotisations()->filter(function (MembersCotisations $membersCotisations) use ($t,$tz) {
return ($membersCotisations->getStartdATE()->format('Y') == $t->format('Y')) && ($membersCotisations->getEndDate()->format('Y') == $tz->format('Y'));
})->first();
$agMembers->setIsVotedAllow($isValid instanceof MembersCotisations);
$entityManager->persist($agMembers);
$entityManager->flush();
return $this->redirectToRoute('admin_ag_edit',['id'=>$ag->getId()]);
}
if($request->query->has('idMember')) {
$agMembers = $entityManager->getRepository(MainMember::class)->find($request->query->get('idMember'));
$entityManager->remove($agMembers);
$entityManager->flush();
return $this->redirectToRoute('admin_ag_edit',['id'=>$ag->getId()]);
}
if($request->query->has('orderId')) {
$agOders = $entityManager->getRepository(MainOrder::class)->find($request->query->get('orderId'));
$entityManager->remove($agOders);
$entityManager->flush();
return $this->redirectToRoute('admin_ag_edit',['id'=>$ag->getId()]);
}
return $this->render('admin/ag/edit.twig', [
'ag' => $form->createView(),
'agMain' => $ag,
'agMembers' => $formMember->createView(),
'agOrder' => $formOrder->createView(),
]);
}
#[Route(path: '/admin/ag/delete/{id}', name: 'admin_ag_delete', options: ['sitemap' => false], methods: ['GET','POST'])]
public function adminAgDelete(?Main $ag,Request $request,EntityManagerInterface $entityManager): Response
{
foreach ($ag->getMainMembers() as $member)
$entityManager->remove($member);
foreach ($ag->getOrders() as $order)
$entityManager->remove($order);
$entityManager->remove($ag);
$entityManager->flush();
return $this->redirectToRoute('admin_ag');
}
}

View File

@@ -50,6 +50,11 @@ class SecurityController extends AbstractController
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
#[Route(path: '/logout', name: 'app_logout', options: ['sitemap' => false], methods: ['GET','POST'])]
public function logout(): Response
{
}
#[Route(path: '/mot-de-passe-oublie', name: 'app_forgot_password', options: ['sitemap' => false], methods: ['GET','POST'])]
public function forgotPassword(Request $request,EventDispatcherInterface $eventDispatcher): Response

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Dto\Ag;
use App\Entity\Ag\Main;
use App\Entity\Ag\MainMember;
use App\Entity\Members;
use App\Repository\MembersRepository;
use Doctrine\ORM\EntityRepository;
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 AgMembersType extends AbstractType
{
public function __construct(private MembersRepository $membersRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var Main $main */
$main = $options['data']->getMain();
$choices = [];
foreach ($this->membersRepository->findAll() as $member) {
$present = $main->getMainMembers()->filter(function (MainMember $mainMember) use ($member) {
return $member->getId() == $mainMember->getMember()->getId();
})->first();
if(!$present instanceof MainMember) {
$choices[$member->getPseudo()] = $member->getId();
}
}
$builder
->add('member', ChoiceType::class, [
'label' => 'Membre',
'choices' => $choices,
'mapped' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',MainMember::class);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Dto\Ag;
use App\Entity\Ag\Main;
use App\Entity\Ag\MainMember;
use App\Entity\Ag\MainOrder;
use App\Entity\Members;
use App\Repository\MembersRepository;
use Doctrine\ORM\EntityRepository;
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\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AgOrderType extends AbstractType
{
public function __construct(private MembersRepository $membersRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'Titre',
'required' => true,
])
->add('description', TextareaType::class, [
'label' => 'Description',
'required' => false
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',MainOrder::class);
}
}

70
src/Dto/Ag/AgType.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 AgType 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' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',Main::class);
}
}

237
src/Entity/Ag/Main.php Normal file
View File

@@ -0,0 +1,237 @@
<?php
namespace App\Entity\Ag;
use App\Entity\Members;
use App\Repository\Ag\MainRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MainRepository::class)]
#[ORM\Table(name: 'ag_main')]
class Main
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?\DateTimeImmutable $agDateAt = null;
#[ORM\ManyToOne(inversedBy: 'ag_president')]
private ?Members $president = null;
#[ORM\ManyToOne(inversedBy: 'secretaire')]
private ?Members $secretaire = null;
#[ORM\Column(length: 255)]
private ?string $locate = null;
#[ORM\Column(length: 255)]
private ?string $locateZipcode = null;
#[ORM\Column(length: 255)]
private ?string $locateCity = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
/**
* @var Collection<int, MainMember>
*/
#[ORM\OneToMany(targetEntity: MainMember::class, mappedBy: 'main')]
private Collection $mainMembers;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $closedAt = null;
/**
* @var Collection<int, MainOrder>
*/
#[ORM\OneToMany(targetEntity: MainOrder::class, mappedBy: 'main')]
private Collection $orders;
#[ORM\Column(nullable: true)]
private ?bool $isClosed = null;
public function __construct()
{
$this->mainMembers = new ArrayCollection();
$this->orders = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getAgDateAt(): ?\DateTimeImmutable
{
return $this->agDateAt;
}
public function setAgDateAt(\DateTimeImmutable $agDateAt): static
{
$this->agDateAt = $agDateAt;
return $this;
}
public function getPresident(): ?Members
{
return $this->president;
}
public function setPresident(?Members $president): static
{
$this->president = $president;
return $this;
}
public function getSecretaire(): ?Members
{
return $this->secretaire;
}
public function setSecretaire(?Members $secretaire): static
{
$this->secretaire = $secretaire;
return $this;
}
public function getLocate(): ?string
{
return $this->locate;
}
public function setLocate(string $locate): static
{
$this->locate = $locate;
return $this;
}
public function getLocateZipcode(): ?string
{
return $this->locateZipcode;
}
public function setLocateZipcode(string $locateZipcode): static
{
$this->locateZipcode = $locateZipcode;
return $this;
}
public function getLocateCity(): ?string
{
return $this->locateCity;
}
public function setLocateCity(string $locateCity): static
{
$this->locateCity = $locateCity;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
/**
* @return Collection<int, MainMember>
*/
public function getMainMembers(): Collection
{
return $this->mainMembers;
}
public function addMainMember(MainMember $mainMember): static
{
if (!$this->mainMembers->contains($mainMember)) {
$this->mainMembers->add($mainMember);
$mainMember->setMain($this);
}
return $this;
}
public function removeMainMember(MainMember $mainMember): static
{
if ($this->mainMembers->removeElement($mainMember)) {
// set the owning side to null (unless already changed)
if ($mainMember->getMain() === $this) {
$mainMember->setMain(null);
}
}
return $this;
}
public function getClosedAt(): ?\DateTimeImmutable
{
return $this->closedAt;
}
public function setClosedAt(?\DateTimeImmutable $closedAt): static
{
$this->closedAt = $closedAt;
return $this;
}
/**
* @return Collection<int, MainOrder>
*/
public function getOrders(): Collection
{
return $this->orders;
}
public function addOrder(MainOrder $order): static
{
if (!$this->orders->contains($order)) {
$this->orders->add($order);
$order->setMain($this);
}
return $this;
}
public function removeOrder(MainOrder $order): static
{
if ($this->orders->removeElement($order)) {
// set the owning side to null (unless already changed)
if ($order->getMain() === $this) {
$order->setMain(null);
}
}
return $this;
}
public function isClosed(): ?bool
{
return $this->isClosed;
}
public function setIsClosed(?bool $isClosed): static
{
$this->isClosed = $isClosed;
return $this;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Entity\Ag;
use App\Entity\Members;
use App\Repository\Ag\MainMemberRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MainMemberRepository::class)]
#[ORM\Table(name: 'ag_main_member')]
class MainMember
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'mainMembers')]
private ?Members $member = null;
#[ORM\Column]
private ?bool $isVotedAllow = null;
#[ORM\ManyToOne(inversedBy: 'mainMembers')]
private ?Main $main = null;
public function getId(): ?int
{
return $this->id;
}
public function getMember(): ?Members
{
return $this->member;
}
public function setMember(?Members $member): static
{
$this->member = $member;
return $this;
}
public function isVotedAllow(): ?bool
{
return $this->isVotedAllow;
}
public function setIsVotedAllow(bool $isVotedAllow): static
{
$this->isVotedAllow = $isVotedAllow;
return $this;
}
public function getMain(): ?Main
{
return $this->main;
}
public function setMain(?Main $main): static
{
$this->main = $main;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Entity\Ag;
use App\Repository\Ag\MainOrderRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MainOrderRepository::class)]
#[ORM\Table(name: 'ag_order')]
class MainOrder
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
#[ORM\ManyToOne(inversedBy: 'orders')]
private ?Main $main = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getMain(): ?Main
{
return $this->main;
}
public function setMain(?Main $main): static
{
$this->main = $main;
return $this;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Entity\Ag;
use App\Entity\Members;
use App\Repository\Ag\MainVoteRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MainVoteRepository::class)]
#[ORM\Table(name: 'ag_main_vote')]
class MainVote
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'agVotes')]
private ?Members $member = null;
#[ORM\Column(length: 255)]
private ?string $role = null;
#[ORM\Column]
private ?int $pour = null;
#[ORM\Column]
private ?int $contre = null;
public function getId(): ?int
{
return $this->id;
}
public function getMember(): ?Members
{
return $this->member;
}
public function setMember(?Members $member): static
{
$this->member = $member;
return $this;
}
public function getRole(): ?string
{
return $this->role;
}
public function setRole(string $role): static
{
$this->role = $role;
return $this;
}
public function getPour(): ?int
{
return $this->pour;
}
public function setPour(int $pour): static
{
$this->pour = $pour;
return $this;
}
public function getContre(): ?int
{
return $this->contre;
}
public function setContre(int $contre): static
{
$this->contre = $contre;
return $this;
}
}

View File

@@ -2,6 +2,9 @@
namespace App\Entity;
use App\Entity\Ag\Main;
use App\Entity\Ag\MainMember;
use App\Entity\Ag\MainVote;
use App\Repository\MembersRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -67,9 +70,37 @@ class Members
#[ORM\Column(length: 255,nullable: true)]
private ?string $Email = null;
/**
* @var Collection<int, Main>
*/
#[ORM\OneToMany(targetEntity: Main::class, mappedBy: 'president')]
private Collection $ag_president;
/**
* @var Collection<int, Main>
*/
#[ORM\OneToMany(targetEntity: Main::class, mappedBy: 'secretaire')]
private Collection $secretaire;
/**
* @var Collection<int, MainMember>
*/
#[ORM\OneToMany(targetEntity: MainMember::class, mappedBy: 'member')]
private Collection $mainMembers;
/**
* @var Collection<int, MainVote>
*/
#[ORM\OneToMany(targetEntity: MainVote::class, mappedBy: 'member')]
private Collection $agVotes;
public function __construct()
{
$this->membersCotisations = new ArrayCollection();
$this->ag_president = new ArrayCollection();
$this->secretaire = new ArrayCollection();
$this->mainMembers = new ArrayCollection();
$this->agVotes = new ArrayCollection();
}
public function getId(): ?int
@@ -342,4 +373,124 @@ class Members
return $this;
}
/**
* @return Collection<int, Main>
*/
public function getAgPresident(): Collection
{
return $this->ag_president;
}
public function addAgPresident(Main $agPresident): static
{
if (!$this->ag_president->contains($agPresident)) {
$this->ag_president->add($agPresident);
$agPresident->setPresident($this);
}
return $this;
}
public function removeAgPresident(Main $agPresident): static
{
if ($this->ag_president->removeElement($agPresident)) {
// set the owning side to null (unless already changed)
if ($agPresident->getPresident() === $this) {
$agPresident->setPresident(null);
}
}
return $this;
}
/**
* @return Collection<int, Main>
*/
public function getSecretaire(): Collection
{
return $this->secretaire;
}
public function addSecretaire(Main $secretaire): static
{
if (!$this->secretaire->contains($secretaire)) {
$this->secretaire->add($secretaire);
$secretaire->setSecretaire($this);
}
return $this;
}
public function removeSecretaire(Main $secretaire): static
{
if ($this->secretaire->removeElement($secretaire)) {
// set the owning side to null (unless already changed)
if ($secretaire->getSecretaire() === $this) {
$secretaire->setSecretaire(null);
}
}
return $this;
}
/**
* @return Collection<int, MainMember>
*/
public function getMainMembers(): Collection
{
return $this->mainMembers;
}
public function addMainMember(MainMember $mainMember): static
{
if (!$this->mainMembers->contains($mainMember)) {
$this->mainMembers->add($mainMember);
$mainMember->setMember($this);
}
return $this;
}
public function removeMainMember(MainMember $mainMember): static
{
if ($this->mainMembers->removeElement($mainMember)) {
// set the owning side to null (unless already changed)
if ($mainMember->getMember() === $this) {
$mainMember->setMember(null);
}
}
return $this;
}
/**
* @return Collection<int, MainVote>
*/
public function getAgVotes(): Collection
{
return $this->agVotes;
}
public function addAgVote(MainVote $agVote): static
{
if (!$this->agVotes->contains($agVote)) {
$this->agVotes->add($agVote);
$agVote->setMember($this);
}
return $this;
}
public function removeAgVote(MainVote $agVote): static
{
if ($this->agVotes->removeElement($agVote)) {
// set the owning side to null (unless already changed)
if ($agVote->getMember() === $this) {
$agVote->setMember(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Ag;
use App\Entity\Ag\MainMember;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MainMember>
*/
class MainMemberRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MainMember::class);
}
// /**
// * @return MainMember[] Returns an array of MainMember 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): ?MainMember
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Ag;
use App\Entity\Ag\MainOrder;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MainOrder>
*/
class MainOrderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MainOrder::class);
}
// /**
// * @return MainOrder[] Returns an array of MainOrder 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): ?MainOrder
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Ag;
use App\Entity\Ag\Main;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Main>
*/
class MainRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Main::class);
}
// /**
// * @return Main[] Returns an array of Main 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): ?Main
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Ag;
use App\Entity\Ag\MainVote;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MainVote>
*/
class MainVoteRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MainVote::class);
}
// /**
// * @return MainVote[] Returns an array of MainVote 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): ?MainVote
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -51,6 +51,6 @@ class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
);
}
return new RedirectResponse($this->urlGenerator->generate('frontend_login'));
return new RedirectResponse($this->urlGenerator->generate('app_login'));
}
}

79
templates/admin/ag.twig Normal file
View File

@@ -0,0 +1,79 @@
{% extends 'admin/base.twig' %}
{% block title %}AG (Assemblée Générale){% endblock %}
{% block page_title %}AG (Assemblée Générale){% endblock %}
{% block body %}
<div class="p-4 sm:p-6 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 min-h-screen">
<div class="flex justify-between items-center mb-6 border-b pb-2 border-gray-200 dark:border-gray-700">
<h1 class="text-2xl font-bold">Liste des AG</h1>
{# NOUVEAU BOUTON: CRÉER UNE AG #}
<a href="{{ path('admin_ag_new') }}"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
+ Créer une AG
</a>
</div>
{% 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">
<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') }}
<span class="ml-2 px-3 py-1 text-xs font-medium rounded-full
{% if ag.type == 'Extraordinaire' %}
bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300
{% else %}
bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300
{% endif %}">
{{ ag.type }}
</span>
</p>
</div>
<p class="text-sm">
<strong>Président:</strong> {{ ag.president.pseudo }} /
<strong>Secrétaire:</strong> {{ ag.secretaire.pseudo }}
</p>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
<strong>Lieu:</strong> {{ ag.locate }}
{{ ag.locateZipcode }} {{ ag.locateCity }}
</div>
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 text-sm">
<p>
<strong>Membres principaux:</strong> <span class="font-medium">{{ ag.mainMembers.count }}</span>
</p>
<p>
<strong>Ordres du jour:</strong> <span class="font-medium">{{ ag.orders.count }}</span>
</p>
</div>
{% 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">
{# 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">
Modifier
</a>
{# Bouton Supprimer #}
<form method="POST" action="{{ path('admin_ag_delete', {'id': ag.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette Assemblée Générale ?');">
<input type="hidden" name="_method" value="DELETE">
<button type="submit"
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Supprimer
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,176 @@
{% extends 'admin/base.twig' %}
{% block title %}AG (Assemblée Générale){% endblock %}
{% block page_title %}AG (Assemblée Générale){% endblock %}
{% block body %}
<div class="p-4 sm:p-6 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 min-h-screen">
{# Titre Principal (Créer / Modifier) #}
<h1 class="text-2xl font-bold mb-6 border-b pb-2 border-gray-200 dark:border-gray-700">
{% if ag.vars.value.id is defined and ag.vars.value.id is not null %}
Modifier l'Assemblée Générale
{% else %}
Créer une nouvelle Assemblée Générale
{% endif %}
</h1>
{% form_theme ag 'form_admin.twig' %}
{# ============================================== #}
{# FORMULAIRE 1: DÉTAILS DE L'AG (Non répété ici) #}
{# ============================================== #}
{{ form_start(ag, {'attr': {'class': 'space-y-6 mb-10'}}) }}
{# Section 1: Détails de l'AG #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<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) }}
<div class="md:col-span-1"></div>
</div>
{# Section 2: Localisation #}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<h2 class="col-span-full text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Lieu</h2>
{{ form_row(ag.locate) }}
{{ form_row(ag.locateZipcode) }}
{{ form_row(ag.locateCity) }}
</div>
{# Section 3: Rôles #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<h2 class="col-span-full text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Rôles</h2>
{{ form_row(ag.president) }}
{{ form_row(ag.secretaire) }}
</div>
{# Boutons d'action pour le formulaire AG #}
<div class="flex justify-end space-x-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<a href="{{ path('admin_ag') }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Annuler
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 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">
Enregistrer l'AG
</button>
</div>
{{ form_end(ag) }}
{# ============================================== #}
{# BLOC GESTION DES MEMBRES (Non répété ici) #}
{# ============================================== #}
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Ajouter/Gérer les membres</h2>
{% form_theme agMembers 'form_admin.twig' %}
{{ form_start(agMembers, {'attr': {'class': 'space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700 mb-6'}}) }}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div class="md:col-span-2">{{ form_row(agMembers.member) }}</div>
<div class="md:col-span-1">
<button type="submit" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Ajouter le membre
</button>
</div>
</div>
{{ form_end(agMembers) }}
{# LISTE DES MEMBRES ACTUELS #}
{% if ag.vars.value.mainMembers is defined and ag.vars.value.mainMembers|length > 0 %}
<h3 class="text-lg font-semibold mt-8 mb-4 border-b pb-2 text-gray-700 dark:text-gray-300">
Liste des Membres Principaux ({{ ag.vars.value.mainMembers|length }})
</h3>
<ul class="space-y-3">
{% for memberAg in ag.vars.value.mainMembers %}
<li class="flex justify-between items-center p-3 rounded-md bg-white dark:bg-gray-800 shadow border border-gray-200 dark:border-gray-700">
<span class="text-gray-900 dark:text-gray-100 font-medium">
{{ memberAg.member.pseudo }} - <span class="text-sm font-normal text-gray-600 dark:text-gray-400">{{ memberAg.member.role }}</span>
</span>
<form method="POST" action="{{ path('admin_ag_edit',{id:ag.vars.value.id,idMember:memberAg.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir retirer {{ memberAg.member.pseudo }} de cette AG ?');">
<input type="hidden" name="_method" value="DELETE">
<button type="submit"
class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-md text-white bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150">
Supprimer
</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="mt-4 p-4 text-center text-gray-500 dark:text-gray-400 border border-dashed rounded-lg">
Aucun membre principal n'est encore rattaché à cette Assemblée Générale.
</p>
{% endif %}
</div>
{# ============================================== #}
{# BLOC ORDRES DU JOUR (ODJ) #}
{# ============================================== #}
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Ajouter un Ordre du Jour</h2>
{% form_theme agOrder 'form_admin.twig' %}
{# FORMULAIRE D'AJOUT D'ODJ #}
{{ form_start(agOrder, {'attr': {'class': 'space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700 mb-6'}}) }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{ form_row(agOrder.title) }}
<div class="md:col-span-1 hidden md:block"></div>
</div>
{{ form_row(agOrder.description) }}
<div class="flex justify-end pt-2">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-teal-600 hover:bg-teal-700 dark:bg-teal-500 dark:hover:bg-teal-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500">
Ajouter l'Ordre du Jour
</button>
</div>
{{ form_end(agOrder) }}
{# LISTE DES ORDRES DU JOUR ACTUELS #}
{% if ag.vars.value.orders is defined and ag.vars.value.orders|length > 0 %}
<h3 class="text-lg font-semibold mt-8 mb-4 border-b pb-2 text-gray-700 dark:text-gray-300">
Ordres du Jour ({{ ag.vars.value.orders|length }})
</h3>
<ul class="space-y-3">
{% for order in ag.vars.value.orders %}
<li class="p-4 rounded-md bg-white dark:bg-gray-800 shadow border border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-start">
<span class="text-gray-900 dark:text-gray-100 font-medium text-base">
{{ loop.index }}. {{ order.title }}
</span>
{# Formulaire de Suppression de l'ODJ #}
<form method="POST" action="{{ path('admin_ag_edit', {'id': agMain.id, 'orderId': order.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet Ordre du Jour : {{ order.title }} ?');">
<input type="hidden" name="_method" value="DELETE">
<button type="submit"
class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-md text-white bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ml-4">
Supprimer
</button>
</form>
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 whitespace-pre-line">
{{ order.description|default('Aucune description fournie.') }}
</p>
</li>
{% endfor %}
</ul>
{% else %}
<p class="mt-4 p-4 text-center text-gray-500 dark:text-gray-400 border border-dashed rounded-lg">
Aucun Ordre du Jour n'a encore été défini pour cette Assemblée Générale.
</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends 'admin/base.twig' %}
{% block title %}AG (Assemblée Générale){% endblock %}
{% block page_title %}AG (Assemblée Générale){% endblock %}
{% block body %}
<div class="p-4 sm:p-6 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 min-h-screen">
<h1 class="text-2xl font-bold mb-6 border-b pb-2 border-gray-200 dark:border-gray-700">
{% if ag.vars.value.id is defined and ag.vars.value.id is not null %}
Modifier l'Assemblée Générale
{% else %}
Créer une nouvelle Assemblée Générale
{% endif %}
</h1>
{# Le formulaire doit utiliser le thème 'form_admin.twig' pour le style #}
{% form_theme ag 'form_admin.twig' %}
{{ form_start(ag, {'attr': {'class': 'space-y-6'}}) }}
{# Section 1: Informations principales et dates (Grid Layout) #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<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>
</div>
{# Section 2: Localisation (Grid Layout) #}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<h2 class="col-span-full text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Lieu</h2>
{{ form_row(ag.locate) }}
{{ form_row(ag.locateZipcode) }}
{{ form_row(ag.locateCity) }}
</div>
{# Section 3: Rôles (Simple Layout) #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 border rounded-lg bg-gray-50 dark:bg-gray-700">
<h2 class="col-span-full text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">Rôles</h2>
{{ form_row(ag.president) }}
{{ form_row(ag.secretaire) }}
</div>
{# Boutons d'action #}
<div class="flex justify-end space-x-4 pt-6 border-t border-gray-200 dark:border-gray-700">
{# Bouton Annuler/Retour #}
<a href="{{ path('admin_ag') }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Annuler
</a>
{# Bouton Soumettre/Enregistrer #}
<button type="submit"
class="inline-flex items-center px-4 py-2 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">
Enregistrer l'AG
</button>
</div>
{{ form_end(ag) }}
</div>
{% endblock %}

View File

@@ -26,9 +26,9 @@ Elle défilera donc avec le reste du contenu de la page.
L'overflow-y-auto n'est plus nécessaire ici car c'est le <body> qui gère le scroll. -->
<aside id="sidebar" class="w-64 bg-gray-800 text-white flex-shrink-0 transition-transform duration-300 transform -translate-x-full md:translate-x-0 z-40">
<div class="p-6">
<a href="{{ path('app_home') }}" target="_blank" class="p-6 block">
<h1 class="text-3xl font-bold tracking-wider">E-Cosplay</h1>
</div>
</a>
<nav class="mt-8">
<!-- Dashboard -->
@@ -60,19 +60,9 @@ L'overflow-y-auto n'est plus nécessaire ici car c'est le <body> qui gère le sc
<svg class="w-5 h-5 mr-3" 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="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.523 5.754 18 7.5 18s3.332.477 4.5 1.247m0-13C13.168 5.477 14.754 5 16.5 5s3.332.477 4.5 1.253v13C19.832 18.523 18.246 18 16.5 18s-3.332.477-4.5 1.247"></path></svg>
AG (Assemblée Générale)
</a>
{# Le lien Paramètres a été supprimé #}
{# Ajoutez d'autres liens ici #}
</nav>
</aside>
<!-- 2. Contenu Principal et Navigation Supérieure -->
<!-- Le conteneur principal reste flex-col et prend l'espace restant -->
<div id="main-content" class="flex flex-col flex-1">
<!-- 2.1. Navbar (Barre de Navigation Supérieure) - bg-gray-800 -->

View File

@@ -223,7 +223,11 @@
tabindex="-1">
{{ 'logged_admin'|trans }}
</a>
<a href="{{ path('app_logout') }}"
class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100" role="menuitem"
tabindex="-1">
{{ 'logout_link'|trans }}
</a>
{% else %}
{# Afficher la connexion si non connecté #}
<a href="{{ path('app_login') }}"

View File

@@ -11,7 +11,7 @@
{# ---------- ROW ---------- #}
{% block form_row %}
{# La ROW est le conteneur du label, du widget et des erreurs. #}
<div class="mb-5">
<div class="mb-0">
{{ form_label(form) }}
<div class="mt-1">
{{ form_widget(form) }}