feat: ajouter l'envoi par e-mail du planning hebdomadaire global aux éducateurs
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
package com.astalange.core.service;
|
||||
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.CreneauEntrainementRepository;
|
||||
import com.astalange.core.repository.EducateurRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PlanningEmailService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlanningEmailService.class);
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final CreneauEntrainementRepository creneauRepository;
|
||||
private final EducateurRepository educateurRepository;
|
||||
|
||||
@Value("${app.mail.from:${spring.mail.username:astalange1933@gmail.com}}")
|
||||
private String fromEmail = "astalange1933@gmail.com";
|
||||
|
||||
public PlanningEmailService(@Autowired(required = false) JavaMailSender mailSender,
|
||||
SaisonRepository saisonRepository,
|
||||
CreneauEntrainementRepository creneauRepository,
|
||||
EducateurRepository educateurRepository) {
|
||||
this.mailSender = mailSender;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.creneauRepository = creneauRepository;
|
||||
this.educateurRepository = educateurRepository;
|
||||
}
|
||||
|
||||
private String getEffectiveFromEmail() {
|
||||
if (fromEmail != null && !fromEmail.trim().isEmpty() && fromEmail.contains("@")) {
|
||||
return fromEmail.trim();
|
||||
}
|
||||
return "astalange1933@gmail.com";
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie le planning global hebdomadaire à tous les éducateurs enregistrés possédant un e-mail.
|
||||
* @return Le nombre d'éducateurs à qui l'e-mail a été envoyé (ou planifié).
|
||||
*/
|
||||
public int sendPlanningGlobalToEducateurs() {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||
if (saisonActive == null) {
|
||||
log.warn("Impossible d'envoyer le planning : aucune saison active.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<Educateur> educateurs = educateurRepository.findAllWithCategorieAndEquipe();
|
||||
List<Educateur> recipientEducateurs = educateurs.stream()
|
||||
.filter(e -> e.getEmail() != null && !e.getEmail().trim().isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (recipientEducateurs.isEmpty()) {
|
||||
log.warn("Aucun éducateur avec adresse e-mail valide trouvé.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<CreneauEntrainement> creneaux = creneauRepository.findBySaisonFetch(saisonActive);
|
||||
|
||||
// Map creneau -> list of matching educateurs names
|
||||
Map<Long, String> creneauEducateursNames = new HashMap<>();
|
||||
for (CreneauEntrainement c : creneaux) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Educateur ed : educateurs) {
|
||||
boolean match = false;
|
||||
if (c.getEquipe() != null) {
|
||||
if (ed.getEquipe() != null && ed.getEquipe().getId().equals(c.getEquipe().getId())) {
|
||||
match = true;
|
||||
}
|
||||
} else if (c.getCategorie() != null) {
|
||||
if (ed.getCategorie() != null && ed.getCategorie().getId().equals(c.getCategorie().getId())) {
|
||||
match = true;
|
||||
} else if (ed.getEquipe() != null && ed.getEquipe().getCategorie() != null
|
||||
&& ed.getEquipe().getCategorie().getId().equals(c.getCategorie().getId())) {
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
names.add(ed.getPrenom() + " " + ed.getNom());
|
||||
}
|
||||
}
|
||||
creneauEducateursNames.put(c.getId(), names.isEmpty() ? "-" : String.join(", ", names));
|
||||
}
|
||||
|
||||
// Group creneaux by JourSemaine
|
||||
Map<JourSemaine, List<CreneauEntrainement>> creneauxParJour = new LinkedHashMap<>();
|
||||
for (JourSemaine jour : JourSemaine.values()) {
|
||||
List<CreneauEntrainement> listForJour = creneaux.stream()
|
||||
.filter(c -> c.getJourSemaine() == jour)
|
||||
.sorted(Comparator.comparing(CreneauEntrainement::getHeureDebut))
|
||||
.collect(Collectors.toList());
|
||||
creneauxParJour.put(jour, listForJour);
|
||||
}
|
||||
|
||||
String subject = "AS Talange - Planning Hebdomadaire des Entraînements (" + saisonActive.getNom() + ")";
|
||||
String htmlBody = buildPlanningGlobalHtml(saisonActive, creneauxParJour, creneauEducateursNames);
|
||||
|
||||
for (Educateur ed : recipientEducateurs) {
|
||||
String recipientEmail = ed.getEmail().trim();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (mailSender != null) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
helper.setFrom(getEffectiveFromEmail());
|
||||
helper.setTo(recipientEmail);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlBody, true);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("Planning global envoyé avec succès par e-mail à {} ({})", recipientEmail, ed.getPrenom() + " " + ed.getNom());
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur d'envoi du planning par e-mail à {}: {}. Simulation log.", recipientEmail, e.getMessage());
|
||||
logDevEmail(recipientEmail, subject, htmlBody);
|
||||
}
|
||||
} else {
|
||||
log.info("Mode DEV (MailSender non configuré) : simulation d'envoi du planning global à {}.", recipientEmail);
|
||||
logDevEmail(recipientEmail, subject, htmlBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return recipientEducateurs.size();
|
||||
}
|
||||
|
||||
private void logDevEmail(String recipient, String subject, String body) {
|
||||
log.info("==================== [SIMULATION E-MAIL PLANNING GLOBAL] ====================");
|
||||
log.info("Destinataire: {}", recipient);
|
||||
log.info("Sujet: {}", subject);
|
||||
log.info("Contenu:\n{}", body);
|
||||
log.info("=============================================================================");
|
||||
}
|
||||
|
||||
private String buildPlanningGlobalHtml(Saison saison, Map<JourSemaine, List<CreneauEntrainement>> creneauxParJour, Map<Long, String> creneauEducateursNames) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
|
||||
for (Map.Entry<JourSemaine, List<CreneauEntrainement>> entry : creneauxParJour.entrySet()) {
|
||||
JourSemaine jour = entry.getKey();
|
||||
List<CreneauEntrainement> creneaux = entry.getValue();
|
||||
|
||||
contentBuilder.append("<div class=\"day-section\">");
|
||||
contentBuilder.append("<h2 class=\"day-title\">").append(jour.getLibelle()).append("</h2>");
|
||||
|
||||
if (creneaux.isEmpty()) {
|
||||
contentBuilder.append("<p class=\"no-training\">Aucun entraînement planifié ce jour.</p>");
|
||||
} else {
|
||||
contentBuilder.append("<table class=\"planning-table\">");
|
||||
contentBuilder.append("<thead><tr>")
|
||||
.append("<th>Horaires</th>")
|
||||
.append("<th>Catégorie / Équipe</th>")
|
||||
.append("<th>Terrain & Zone</th>")
|
||||
.append("<th>Éducateurs référents</th>")
|
||||
.append("</tr></thead><tbody>");
|
||||
|
||||
for (CreneauEntrainement c : creneaux) {
|
||||
String horaire = (c.getHeureDebut() != null ? c.getHeureDebut().toString() : "") + " - " + (c.getHeureFin() != null ? c.getHeureFin().toString() : "");
|
||||
String catStr = c.getCategorie() != null ? c.getCategorie().getNom() : "";
|
||||
if (c.getEquipe() != null) {
|
||||
catStr += " (" + c.getEquipe().getNom() + ")";
|
||||
}
|
||||
String terrainZone = (c.getTerrain() != null ? c.getTerrain().getNom() : "") + " - " + (c.getZone() != null ? c.getZone().getLibelle() : "");
|
||||
String educateursStr = creneauEducateursNames.getOrDefault(c.getId(), "-");
|
||||
|
||||
contentBuilder.append("<tr>")
|
||||
.append("<td class=\"time-cell\">").append(horaire).append("</td>")
|
||||
.append("<td class=\"cat-cell\"><strong>").append(catStr).append("</strong></td>")
|
||||
.append("<td class=\"terrain-cell\">").append(terrainZone).append("</td>")
|
||||
.append("<td class=\"educ-cell\">").append(educateursStr).append("</td>")
|
||||
.append("</tr>");
|
||||
}
|
||||
|
||||
contentBuilder.append("</tbody></table>");
|
||||
}
|
||||
contentBuilder.append("</div>");
|
||||
}
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 20px; color: #1e293b; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.header { background-color: #1e3a8a; color: #ffffff; padding: 28px 24px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.5px; }
|
||||
.header p { margin: 8px 0 0 0; font-size: 14px; opacity: 0.9; }
|
||||
.content { padding: 24px; }
|
||||
.day-section { margin-bottom: 24px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
||||
.day-title { background: #f8fafc; border-bottom: 2px solid #1e3a8a; color: #1e3a8a; margin: 0; padding: 12px 18px; font-size: 16px; font-weight: 700; }
|
||||
.no-training { padding: 14px 18px; margin: 0; font-size: 13px; color: #64748b; font-style: italic; }
|
||||
.planning-table { width: 100%%; border-collapse: collapse; font-size: 13px; text-align: left; }
|
||||
.planning-table th { background: #f1f5f9; color: #475569; font-weight: 600; padding: 10px 14px; border-bottom: 1px solid #e2e8f0; }
|
||||
.planning-table td { padding: 10px 14px; border-bottom: 1px solid #f1f5f9; }
|
||||
.planning-table tr:last-child td { border-bottom: none; }
|
||||
.time-cell { font-weight: 600; color: #2563eb; white-space: nowrap; width: 120px; }
|
||||
.cat-cell { color: #0f172a; }
|
||||
.terrain-cell { color: #475569; }
|
||||
.educ-cell { color: #059669; font-weight: 500; }
|
||||
.footer { background: #f8fafc; border-top: 1px solid #e2e8f0; padding: 18px; text-align: center; font-size: 12px; color: #64748b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>AS TALANGE - Planning Global des Entraînements</h1>
|
||||
<p>Saison %s - Vue d'ensemble hebdomadaire pour le corps éducatif</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p style="font-size: 14px; line-height: 1.6; margin-bottom: 24px;">
|
||||
Chers éducateurs,<br>
|
||||
Voici le planning général d'occupation des terrains et des créneaux d'entraînement de l'<strong>AS Talange</strong> pour toute la semaine.
|
||||
</p>
|
||||
|
||||
%s
|
||||
|
||||
<p style="margin-top: 24px; font-size: 13px; color: #475569;">
|
||||
En cas de changement ou de modification sur les créneaux, merci d'en informer le responsable technique du club.<br><br>
|
||||
Sportivement,<br>
|
||||
<strong>Le Bureau & la Direction Technique - AS Talange</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Cet e-mail automatique est destiné à l'ensemble du corps éducatif de l'AS Talange.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(saison.getNom(), contentBuilder.toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user