Browse Source

ajout de la page de newsletter mensuelle

- pas de prise en charge du lieu
- page à générer à la main (/newsletter/(year)/(month)
- uniquement les événements public
- copier / coller pour un envoi externe pour l'instant !
garthh 1 month ago
parent
commit
fb58d26899

+ 30 - 10
src/Controller/MainController.php

@@ -59,16 +59,7 @@ final class MainController extends AbstractController
         return $this->redirectToRoute('app_main_booking', ['id' => $event->getId(), 'view' => 'pictures']);
     }
     
-    #[Route('/ics/{id}.ics', name: 'app_main_ics', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
-    public function ics(?Event $event, EventRepository $repository): Response
-    {
-        // Contrôler qu'un événement est bien ok
-        if (!$event) {
-            $this->addFlash('danger', 'Événement inconnu !');
-            $this->redirectToRoute('app_main_booking_main');
-        }
-        return $this->render('main/event.ics.twig', ['event' => $event]);
-    }
+
 
     #[Route('/booking/{id}/{view}', name: 'app_main_booking', requirements: ['id' => Requirement::UUID_V7, 'view' => '.*'], methods: ['GET', 'POST'])]
     public function booking(?Event $event, EventRepository $repository, string $view): Response
@@ -182,4 +173,33 @@ final class MainController extends AbstractController
             return $this->redirectToRoute('app_main');
         }
     }
+    
+    
+    // Gestion de la newsletter et des pièces jointes
+    // ORGASSO permet uniquement de générer un template de newsletter, il ne gère pas l'envoi de celle-ci pour l'instant
+    
+    // Génération de la newsletter /newsletter/(annee)/(mois)
+    #[Route('/newsletter/{year}/{month}', name: 'app_newsletter', requirements: ['year' => '\d{4}', 'month' => '0[1-9]|1[0-2]'], methods: ['GET'])]
+    public function newsletter(EventRepository $repository, int $year, int $month): Response
+    {
+ 
+        // Lister tous les événements du mois et de l'année
+        $events = $repository->findEventsOfMonth($month, $year, true);
+        
+        // Générer et retourner la newsletter
+        return $this->render('main/mailinglist.template.html.twig', ['events' => $events]);
+    }
+    
+    // Génération des ICS pour la newsletter et l'affiche des événements
+    #[Route('/ics/{id}.ics', name: 'app_main_ics', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function ics(?Event $event, EventRepository $repository): Response
+    {
+        // Contrôler qu'un événement est bien ok
+        if (!$event) {
+            $this->addFlash('danger', 'Événement inconnu !');
+            $this->redirectToRoute('app_main_booking_main');
+        }
+        return $this->render('main/event.ics.twig', ['event' => $event]);
+    }
+
 }

+ 2 - 2
src/Controller/PartyRequestController.php

@@ -36,14 +36,14 @@ final class PartyRequestController extends AbstractController
         // L'événement n'autorise pas les demandes ?
         if (!$event->isEveryoneCanAskForGame()) {
             $this->addFlash('danger', 'Cet événement n\'autorise pas les demandes.');
-            $this->redirectToRoute('app_main_booking', ['id' => $event->getId()]);
+            $this->redirectToRoute('app_main_booking', ['id' => $event->getId(), 'view' => 'pictures']);
         }
 
         // Pas d'utilisateur connecté ?
         $user = $this->getUser();
         if (!$user) {
             $this->addFlash('danger', 'Les demandes nécessitent d\'être connecté(e).');
-            $this->redirectToRoute('app_main_booking', ['id' => $event->getId()]);
+            $this->redirectToRoute('app_main_booking', ['id' => $event->getId(), 'view' => 'pictures']);
         }
 
         // On génère un formulaire

+ 33 - 0
src/Repository/EventRepository.php

@@ -45,6 +45,39 @@ class EventRepository extends ServiceEntityRepository
 
         return $query->getResult();
     }
+    
+    /*
+     * Extraire les événements d'une année / mois
+     * - published = true
+     * - startOn >= 01/mois/année
+     * - startOn < 01/mois+1/année
+     */
+    public function findEventsOfMonth(int $month, int $year, bool $public=true): array
+    {
+        // On génère les dates
+        $dateStartMonth = new \DateTime(sprintf('%04d-%02d-01 00:00:00', $year, $month));
+        $dateEndMonth = clone $dateStartMonth;
+        $dateEndMonth->modify('last day of this month')->setTime(23, 59, 59);
+
+        // Construction de la requête
+        $qb = $this->createQueryBuilder('e')
+        ->where('e.published = :published')
+        ->andWhere('e.startOn >= :dateStartMonth')
+        ->andWhere('e.endOn <= :dateEndMonth')
+        ->setParameter('published', true)
+        ->setParameter('dateStartMonth', $dateStartMonth)
+        ->setParameter('dateEndMonth', $dateEndMonth);
+        
+        // Prise en compte public / privé
+        if ($public) {
+            $qb->andWhere('(e.private = false OR e.private IS NULL)');
+        }
+        
+        $query = $qb->orderBy('e.startOn', 'ASC')
+        ->getQuery();
+        
+        return $query->getResult();
+    }
 
     /**
      * @return Event[]

+ 52 - 0
src/Twig/Base64Extension.php

@@ -0,0 +1,52 @@
+<?php
+
+# src/Twig/Base64Extension.php
+namespace App\Twig;
+
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+
+class Base64Extension extends AbstractExtension
+{
+    public function __construct(private ParameterBagInterface $params)
+    {
+        
+    }
+    
+    public function getFilters(): array
+    {
+        return [
+            new TwigFilter('base64img', [$this, 'convertToBase64']),
+        ];
+    }
+
+    public function convertToBase64(string $path): string
+    {
+        $absolutePath = $this->params->get('upload_images_directory') . $path;
+
+        if (!file_exists($absolutePath)) {
+            dump($absolutePath);
+            return '';
+        }
+
+        // convert WebP → JPEG avant d'encoder
+        $info = pathinfo($absolutePath);
+        $ext = strtolower($info['extension']);
+
+        if ($ext === 'webp') {
+            $image = imagecreatefromwebp($absolutePath);
+            ob_start();
+            imagejpeg($image, null, 85); // qualité 85%
+            $jpegData = ob_get_clean();
+            imagedestroy($image);
+            $data = $jpegData;
+            $mime = 'image/jpeg';
+        } else {
+            $data = file_get_contents($absolutePath);
+            $mime = mime_content_type($absolutePath);
+        }
+
+        return 'data:' . $mime . ';base64,' . base64_encode($data);
+    }
+}

+ 1 - 1
templates/main/event.ics.twig

@@ -8,7 +8,7 @@ DTSTART:{{ period.getStartOn|date('Ymd\\THis') }}Z
 DTEND:{{ period.getEndOn|date('Ymd\\THis') }}Z
 SUMMARY:{{ event.name }}
 DESCRIPTION:{% set description = event.description|replace({',':'\,',';':'\;','\\':'\\\\'}) %}{{ description }}
-URL:{{ url('app_main_booking', {id: event.id}) }}
+URL:{{ url('app_main_booking', {id: event.id, 'view': 'picture'}) }}
 END:VEVENT
 {% endfor %}
 END:VCALENDAR

+ 139 - 0
templates/main/mailinglist.template.html.twig

@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+  <meta charset="UTF-8">
+  <title>Nos prochains événements</title>
+</head>
+<body style="margin:0; padding:0; background-color:#f0f4f8;">
+  <center style="width:100%; background-color:#f0f4f8;">
+    <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">  
+      <tr>
+        <td align="center">
+          <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="600" style="background-color:#ffffff;">
+            
+            <!-- HEADER -->
+            <tr>
+              <td style="padding:20px; text-align:center; background-color:#0066cc; color:#ffffff; font-family:Arial, Helvetica, sans-serif;">
+                <h1 style="margin:0; font-size:24px;">✨ Nos événements du mois</h1>
+              </td>
+            </tr>
+
+            <!-- INTRO -->
+            <tr>
+              <td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:15px; color:#333333; line-height:1.6;">
+                <p>
+                  Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
+                  Sed feugiat, sapien ut pulvinar luctus, ex metus tincidunt ante, 
+                  non egestas mauris velit nec libero. Praesent at luctus nunc.
+                </p>
+              </td>
+            </tr>
+
+						{% for event in events %}
+            <!-- ÉVÉNEMENT -->
+            <tr>
+              <td style="border-bottom:1px solid #e0e0e0;">
+                <!-- Image -->
+                <img src="{{ ('/events/' ~ (event.picture ?: 'placeholder.webp'))|base64img }}" alt="{{ event.name }}" width="600" height="200"
+                     style="display:block; border:0; margin:0; padding:0;">
+                <!-- Contenu -->
+                <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
+                  <tr>
+                    <td style="padding:20px;">
+                      
+                      <!-- Panneau infos -->
+                      <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
+                        <tr>
+                          <td style="background:#e6f2ff; padding:15px; border-radius:6px;">
+                            <h2 style="margin:0; font-size:18px; font-family:Arial, Helvetica, sans-serif; color:#004080;">
+                              {{ event.name }}
+                            </h2>
+                            <p style="margin:8px 0 4px 0; font-size:14px; color:#004080; font-family:Arial, Helvetica, sans-serif;">
+                              <strong>Lieu :</strong> A COMPLETER
+                            </p>
+                            <p style="margin:4px 0; font-size:14px; color:#004080; font-family:Arial, Helvetica, sans-serif;">
+                              <strong>Date :</strong> du {{ event.startOn|date('d/m/Y \\à H:i') }} au {{ event.endOn|date('d/m/Y \\à H:i') }}
+                            </p>
+                          </td>
+                        </tr>
+                      </table>
+
+                      <!-- Texte -->
+                      <p style="margin:12px 0 16px 0; font-size:14px; color:#555555; line-height:1.5; font-family:Arial, Helvetica, sans-serif;">
+                        {{ event.description }}
+                      </p>
+                      
+                    	{% set authors = [] %}
+                    	{% for party in event.parties %}
+                          {% if party.gamemasterIsAuthor and party.gamemaster not in authors %}
+                              {% set authors = authors|merge([party.gamemaster]) %}
+                        	{% endif %}
+                    	{% endfor %}
+											{% if authors|length > 0 %}
+											{% for gamemaster in authors %}
+											<p style="margin:12px 0 16px 0; font-size:16px; color:#333333; line-height:1.5; font-family:Arial, Helvetica, sans-serif;"><strong>Les auteur(rice)s présent(e)s</strong></p>
+                      <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" 
+                             style="background:#f5f5f5; border-radius:6px; margin-bottom:16px;">
+                     		
+                        <tr>
+                          <td style="padding:12px;" width="128">
+                            <img src="{{ ('/gamemasters/' ~ (gamemaster.picture ?: 'placeholder.webp'))|base64img }}" alt="{{ gamemaster.preferedName }}" width="128" style="display:block; border-radius:6px;">
+                          </td>
+                          <td style="padding:12px; vertical-align:top;">
+                            <p style="margin:0; font-size:16px; color:#004080; font-family:Arial, Helvetica, sans-serif;">
+                              <strong>{{ gamemaster.preferedName }}</strong>
+                            </p>
+                            <p style="margin:6px 0 0 0; font-size:13px; color:#555555; line-height:1.4; font-family:Arial, Helvetica, sans-serif;">
+                              {{ gamemaster.description }}
+                            </p>
+                            <p style="margin:6px 0 0 0; font-size:13px; color:#555555; line-height:1.4; font-family:Arial, Helvetica, sans-serif;">
+                              Auteur(rice) de : {{ gamemaster.authorOfGames|map(game => game.name)|join(', ') }}
+                            </p>
+                          </td>
+                        </tr>
+                      </table>
+											{% endfor %}
+                      {% endif %}
+
+                      <p style="margin:12px 0 16px 0; font-size:14px; color:#555555; line-height:1.5; font-family:Arial, Helvetica, sans-serif;">
+                        Vous pouvez vous inscrire dès à présent à nos animations !
+                      </p>
+
+                      <!-- Boutons -->
+                      <table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center">
+                        <tr>
+                          <td align="center" style="border-radius:4px; background:#0066cc; padding:10px 18px;">
+                            <a href="{{ app_url ~ path('app_main_booking', {'id': event.id, 'view': 'pictures'})}}" style="font-size:14px; color:#ffffff; text-decoration:none; display:inline-block; font-family:Arial, Helvetica, sans-serif;">
+                              Voir le détail et m'inscrire
+                            </a>
+                          </td>
+                          <td style="width:10px;"></td>
+                          <td align="center" style="border-radius:4px; background:#0099cc; padding:10px 18px;">
+                 						<a href="{{ app_url|replace({'https://' : 'webcal://'}) ~ path('app_main_ics', {'id': event.id})  }}" style="font-size:14px; color:#ffffff; text-decoration:none; display:inline-block; font-family:Arial, Helvetica, sans-serif;">Ajouter à mon agenda</a>
+                          </td>
+                        </tr>
+                      </table>
+                      
+                      {%  endfor %}
+
+                    </td>
+                  </tr>
+                </table>
+              </td>
+            </tr>
+
+            <!-- FOOTER -->
+            <tr>
+              <td style="padding:20px; text-align:center; font-size:12px; font-family:Arial, Helvetica, sans-serif; color:#888888;">
+                Réalisé à partir de ORGASSO : une application pour tous vos événements rôlistiques !
+              </td>
+            </tr>
+
+          </table>
+        </td>
+      </tr>
+    </table>
+  </center>
+
+</body>
+</html>