Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb240ea58 | ||
|
|
5990b025ac | ||
|
|
48de522a26 | ||
|
|
03869841d5 | ||
|
|
6539e33f08 | ||
|
|
cb281aa3e0 |
@@ -33,3 +33,8 @@ Thumbs.db
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Database / Local Scripts
|
||||
anonymize_db.sql
|
||||
refresh_test_db.sh
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.7-SNAPSHOT</version>
|
||||
<version>1.7</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.7-SNAPSHOT</version>
|
||||
<version>1.7</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -42,6 +42,18 @@ public class Dotation {
|
||||
@Column(name = "date_commande")
|
||||
private java.time.LocalDateTime dateCommande;
|
||||
|
||||
@Column(name = "flocage_initiales_exporte", nullable = false)
|
||||
private Boolean flocageInitialesExporte = false;
|
||||
|
||||
@Column(name = "date_flocage_initiales")
|
||||
private java.time.LocalDateTime dateFlocageInitiales;
|
||||
|
||||
@Column(name = "flocage_prenom_numero_exporte", nullable = false)
|
||||
private Boolean flocagePrenomNumeroExporte = false;
|
||||
|
||||
@Column(name = "date_flocage_prenom_numero")
|
||||
private java.time.LocalDateTime dateFlocagePrenomNumero;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -77,6 +89,20 @@ public class Dotation {
|
||||
public java.time.LocalDateTime getDateCommande() { return dateCommande; }
|
||||
public void setDateCommande(java.time.LocalDateTime dateCommande) { this.dateCommande = dateCommande; }
|
||||
|
||||
public Boolean getFlocageInitialesExporte() { return flocageInitialesExporte; }
|
||||
public void setFlocageInitialesExporte(Boolean flocageInitialesExporte) { this.flocageInitialesExporte = flocageInitialesExporte; }
|
||||
|
||||
public java.time.LocalDateTime getDateFlocageInitiales() { return dateFlocageInitiales; }
|
||||
public void setDateFlocageInitiales(java.time.LocalDateTime dateFlocageInitiales) { this.dateFlocageInitiales = dateFlocageInitiales; }
|
||||
|
||||
public Boolean getFlocagePrenomNumeroExporte() { return flocagePrenomNumeroExporte; }
|
||||
public void setFlocagePrenomNumeroExporte(Boolean flocagePrenomNumeroExporte) { this.flocagePrenomNumeroExporte = flocagePrenomNumeroExporte; }
|
||||
|
||||
public java.time.LocalDateTime getDateFlocagePrenomNumero() { return dateFlocagePrenomNumero; }
|
||||
public void setDateFlocagePrenomNumero(java.time.LocalDateTime dateFlocagePrenomNumero) { this.dateFlocagePrenomNumero = dateFlocagePrenomNumero; }
|
||||
|
||||
|
||||
|
||||
public static boolean isSizeMatch(String option, String adherentSize) {
|
||||
if (option == null || adherentSize == null) return false;
|
||||
String opt = option.trim();
|
||||
|
||||
@@ -12,4 +12,8 @@ import com.astalange.core.entity.Saison;
|
||||
public interface DotationRepository extends JpaRepository<Dotation, Long>, JpaSpecificationExecutor<Dotation> {
|
||||
List<Dotation> findByLicence_SaisonAndChoisiTrueAndFourniFalse(Saison saison);
|
||||
List<Dotation> findByLicence_SaisonAndChoisiTrueAndCommandeeFalse(Saison saison);
|
||||
List<Dotation> findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocageInitialesExporteFalse(Saison saison);
|
||||
List<Dotation> findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocagePrenomNumeroExporteFalse(Saison saison);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,107 @@ public class DotationService {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public String genererCsvFlocageInitiales(Saison saisonActive) {
|
||||
List<Dotation> dotations = dotationRepository.findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocageInitialesExporteFalse(saisonActive);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String dateExportFormatted = now.format(DATE_FORMATTER);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('\ufeff'); // BOM UTF-8 for Excel
|
||||
sb.append("Saison;Catégorie;Nom;Prénom;Équipement;Référence;Taille;Initiales;Date Export Flocage\n");
|
||||
|
||||
for (Dotation d : dotations) {
|
||||
if (d.getEquipement() == null || !d.getEquipement().isHasFlocageInitiales()) {
|
||||
continue;
|
||||
}
|
||||
String initiales = d.getFlocage() != null ? d.getFlocage().trim() : "";
|
||||
if (initiales.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : "";
|
||||
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
|
||||
String nom = d.getLicence().getAdherent() != null ? d.getLicence().getAdherent().getNom() : "";
|
||||
String prenom = d.getLicence().getAdherent() != null ? d.getLicence().getAdherent().getPrenom() : "";
|
||||
String equipement = d.getEquipement().getNom();
|
||||
String reference = d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
|
||||
String taille = d.getTaille() != null ? d.getTaille() : "";
|
||||
|
||||
sb.append(escapeCsv(saison)).append(";")
|
||||
.append(escapeCsv(categorie)).append(";")
|
||||
.append(escapeCsv(nom)).append(";")
|
||||
.append(escapeCsv(prenom)).append(";")
|
||||
.append(escapeCsv(equipement)).append(";")
|
||||
.append(escapeCsv(reference)).append(";")
|
||||
.append(escapeCsv(taille)).append(";")
|
||||
.append(escapeCsv(initiales)).append(";")
|
||||
.append(escapeCsv(dateExportFormatted)).append("\n");
|
||||
|
||||
d.setFlocageInitialesExporte(true);
|
||||
d.setDateFlocageInitiales(now);
|
||||
dotationRepository.save(d);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public String genererCsvFlocagePrenomNumero(Saison saisonActive) {
|
||||
List<Dotation> dotations = dotationRepository.findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocagePrenomNumeroExporteFalse(saisonActive);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String dateExportFormatted = now.format(DATE_FORMATTER);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('\ufeff'); // BOM UTF-8 for Excel
|
||||
sb.append("Saison;Catégorie;Nom;Prénom;Équipement;Référence;Taille;Prénom Floqué;Numéro;Date Export Flocage\n");
|
||||
|
||||
for (Dotation d : dotations) {
|
||||
if (d.getEquipement() == null) {
|
||||
continue;
|
||||
}
|
||||
boolean handlesPrenom = d.getEquipement().isHasFlocagePrenom();
|
||||
boolean handlesNumero = d.getEquipement().isHasFlocageNumero();
|
||||
|
||||
if (!handlesPrenom && !handlesNumero) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String prenomFloque = d.getFlocage() != null ? d.getFlocage().trim() : "";
|
||||
String numero = d.getNumero() != null ? d.getNumero().trim() : "";
|
||||
|
||||
if (prenomFloque.isEmpty() && numero.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : "";
|
||||
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
|
||||
String nom = d.getLicence().getAdherent() != null ? d.getLicence().getAdherent().getNom() : "";
|
||||
String prenom = d.getLicence().getAdherent() != null ? d.getLicence().getAdherent().getPrenom() : "";
|
||||
String equipement = d.getEquipement().getNom();
|
||||
String reference = d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
|
||||
String taille = d.getTaille() != null ? d.getTaille() : "";
|
||||
|
||||
sb.append(escapeCsv(saison)).append(";")
|
||||
.append(escapeCsv(categorie)).append(";")
|
||||
.append(escapeCsv(nom)).append(";")
|
||||
.append(escapeCsv(prenom)).append(";")
|
||||
.append(escapeCsv(equipement)).append(";")
|
||||
.append(escapeCsv(reference)).append(";")
|
||||
.append(escapeCsv(taille)).append(";")
|
||||
.append(escapeCsv(prenomFloque)).append(";")
|
||||
.append(escapeCsv(numero)).append(";")
|
||||
.append(escapeCsv(dateExportFormatted)).append("\n");
|
||||
|
||||
d.setFlocagePrenomNumeroExporte(true);
|
||||
d.setDateFlocagePrenomNumero(now);
|
||||
dotationRepository.save(d);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
private String escapeCsv(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
@@ -130,3 +231,4 @@ public class DotationService {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE dotation ADD COLUMN flocage_exporte BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE dotation ADD COLUMN date_flocage TIMESTAMP NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE dotation DROP COLUMN IF EXISTS flocage_exporte;
|
||||
ALTER TABLE dotation DROP COLUMN IF EXISTS date_flocage;
|
||||
|
||||
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS flocage_initiales_exporte BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS date_flocage_initiales TIMESTAMP NULL;
|
||||
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS flocage_prenom_numero_exporte BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS date_flocage_prenom_numero TIMESTAMP NULL;
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.astalange.core.service;
|
||||
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.DotationRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DotationServiceTest {
|
||||
|
||||
@Mock
|
||||
private DotationRepository dotationRepository;
|
||||
|
||||
@InjectMocks
|
||||
private DotationService dotationService;
|
||||
|
||||
private Saison saison;
|
||||
private Categorie categorie;
|
||||
private Adherent adherent;
|
||||
private Licence licence;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
saison = new Saison();
|
||||
saison.setId(1L);
|
||||
saison.setNom("2026-2027");
|
||||
|
||||
categorie = new Categorie();
|
||||
categorie.setId(10L);
|
||||
categorie.setNom("U15");
|
||||
|
||||
adherent = new Adherent();
|
||||
adherent.setId(100L);
|
||||
adherent.setNom("Dupont");
|
||||
adherent.setPrenom("Jean");
|
||||
|
||||
licence = new Licence();
|
||||
licence.setId(500L);
|
||||
licence.setSaison(saison);
|
||||
licence.setCategorie(categorie);
|
||||
licence.setAdherent(adherent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Générer CSV Flocage Initiales - Uniquement équipements avec initiales et commandés")
|
||||
public void testGenererCsvFlocageInitiales() {
|
||||
Equipement eqVeste = new Equipement();
|
||||
eqVeste.setId(1L);
|
||||
eqVeste.setNom("Veste de sortie");
|
||||
eqVeste.setReference("REF-VESTE");
|
||||
eqVeste.setHasFlocageInitiales(true);
|
||||
|
||||
Dotation d1 = new Dotation();
|
||||
d1.setId(1L);
|
||||
d1.setLicence(licence);
|
||||
d1.setEquipement(eqVeste);
|
||||
d1.setTaille("M");
|
||||
d1.setFlocage("JD");
|
||||
d1.setChoisi(true);
|
||||
d1.setCommandee(true);
|
||||
d1.setFlocageInitialesExporte(false);
|
||||
|
||||
when(dotationRepository.findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocageInitialesExporteFalse(saison))
|
||||
.thenReturn(List.of(d1));
|
||||
|
||||
String csv = dotationService.genererCsvFlocageInitiales(saison);
|
||||
|
||||
assertNotNull(csv);
|
||||
assertTrue(csv.contains("JD"));
|
||||
assertTrue(csv.contains("Veste de sortie"));
|
||||
assertTrue(csv.contains("Dupont"));
|
||||
assertTrue(d1.getFlocageInitialesExporte());
|
||||
assertNotNull(d1.getDateFlocageInitiales());
|
||||
verify(dotationRepository, times(1)).save(d1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Générer CSV Flocage Prénom / Numéro - Uniquement équipements commandés avec prénom ou numéro")
|
||||
public void testGenererCsvFlocagePrenomNumero() {
|
||||
Equipement eqMaillot = new Equipement();
|
||||
eqMaillot.setId(2L);
|
||||
eqMaillot.setNom("Maillot de match");
|
||||
eqMaillot.setReference("REF-MAILLOT");
|
||||
eqMaillot.setHasFlocagePrenom(true);
|
||||
eqMaillot.setHasFlocageNumero(true);
|
||||
|
||||
Dotation d2 = new Dotation();
|
||||
d2.setId(2L);
|
||||
d2.setLicence(licence);
|
||||
d2.setEquipement(eqMaillot);
|
||||
d2.setTaille("L");
|
||||
d2.setFlocage("JEAN");
|
||||
d2.setNumero("10");
|
||||
d2.setChoisi(true);
|
||||
d2.setCommandee(true);
|
||||
d2.setFlocagePrenomNumeroExporte(false);
|
||||
|
||||
when(dotationRepository.findByLicence_SaisonAndChoisiTrueAndCommandeeTrueAndFlocagePrenomNumeroExporteFalse(saison))
|
||||
.thenReturn(List.of(d2));
|
||||
|
||||
String csv = dotationService.genererCsvFlocagePrenomNumero(saison);
|
||||
|
||||
assertNotNull(csv);
|
||||
assertTrue(csv.contains("JEAN"));
|
||||
assertTrue(csv.contains("10"));
|
||||
assertTrue(csv.contains("Maillot de match"));
|
||||
assertTrue(d2.getFlocagePrenomNumeroExporte());
|
||||
assertNotNull(d2.getDateFlocagePrenomNumero());
|
||||
verify(dotationRepository, times(1)).save(d2);
|
||||
}
|
||||
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
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.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PlanningEmailServiceTest {
|
||||
|
||||
private PlanningEmailService planningEmailService;
|
||||
private JavaMailSender mailSenderMock;
|
||||
private SaisonRepository saisonRepositoryMock;
|
||||
private CreneauEntrainementRepository creneauRepositoryMock;
|
||||
private EducateurRepository educateurRepositoryMock;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mailSenderMock = mock(JavaMailSender.class);
|
||||
MimeMessage mimeMessage = new MimeMessage((jakarta.mail.Session) null);
|
||||
when(mailSenderMock.createMimeMessage()).thenReturn(mimeMessage);
|
||||
|
||||
saisonRepositoryMock = mock(SaisonRepository.class);
|
||||
creneauRepositoryMock = mock(CreneauEntrainementRepository.class);
|
||||
educateurRepositoryMock = mock(EducateurRepository.class);
|
||||
|
||||
planningEmailService = new PlanningEmailService(
|
||||
mailSenderMock,
|
||||
saisonRepositoryMock,
|
||||
creneauRepositoryMock,
|
||||
educateurRepositoryMock
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPlanningGlobalToEducateurs_SansSaisonActive() {
|
||||
when(saisonRepositoryMock.findByEstActiveTrue()).thenReturn(Optional.empty());
|
||||
|
||||
int count = planningEmailService.sendPlanningGlobalToEducateurs();
|
||||
|
||||
assertEquals(0, count);
|
||||
verify(mailSenderMock, never()).send(any(MimeMessage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPlanningGlobalToEducateurs_SansEducateursAvecEmail() {
|
||||
Saison saison = new Saison();
|
||||
saison.setNom("2026/2027");
|
||||
when(saisonRepositoryMock.findByEstActiveTrue()).thenReturn(Optional.of(saison));
|
||||
|
||||
Educateur ed1 = new Educateur();
|
||||
ed1.setNom("DUPONT");
|
||||
ed1.setPrenom("Jean");
|
||||
ed1.setEmail(null);
|
||||
|
||||
Educateur ed2 = new Educateur();
|
||||
ed2.setNom("MARTIN");
|
||||
ed2.setPrenom("Paul");
|
||||
ed2.setEmail("");
|
||||
|
||||
when(educateurRepositoryMock.findAllWithCategorieAndEquipe()).thenReturn(List.of(ed1, ed2));
|
||||
|
||||
int count = planningEmailService.sendPlanningGlobalToEducateurs();
|
||||
|
||||
assertEquals(0, count);
|
||||
verify(mailSenderMock, never()).send(any(MimeMessage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPlanningGlobalToEducateurs_Succes() {
|
||||
Saison saison = new Saison();
|
||||
saison.setNom("2026/2027");
|
||||
when(saisonRepositoryMock.findByEstActiveTrue()).thenReturn(Optional.of(saison));
|
||||
|
||||
Educateur ed1 = new Educateur();
|
||||
ed1.setId(1L);
|
||||
ed1.setNom("DUPONT");
|
||||
ed1.setPrenom("Jean");
|
||||
ed1.setEmail("jean.dupont@example.com");
|
||||
|
||||
Educateur ed2 = new Educateur();
|
||||
ed2.setId(2L);
|
||||
ed2.setNom("BERNARD");
|
||||
ed2.setPrenom("Luc");
|
||||
ed2.setEmail("luc.bernard@example.com");
|
||||
|
||||
when(educateurRepositoryMock.findAllWithCategorieAndEquipe()).thenReturn(List.of(ed1, ed2));
|
||||
|
||||
Categorie cat = new Categorie();
|
||||
cat.setId(10L);
|
||||
cat.setNom("U13");
|
||||
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.setId(100L);
|
||||
terrain.setNom("Terrain Synthétique");
|
||||
|
||||
CreneauEntrainement creneau = new CreneauEntrainement();
|
||||
creneau.setId(50L);
|
||||
creneau.setSaison(saison);
|
||||
creneau.setCategorie(cat);
|
||||
creneau.setTerrain(terrain);
|
||||
creneau.setZone(ZoneTerrain.DEMI_A);
|
||||
creneau.setJourSemaine(JourSemaine.MERCREDI);
|
||||
creneau.setHeureDebut(LocalTime.of(14, 0));
|
||||
creneau.setHeureFin(LocalTime.of(16, 0));
|
||||
|
||||
when(creneauRepositoryMock.findBySaisonFetch(saison)).thenReturn(List.of(creneau));
|
||||
|
||||
int count = planningEmailService.sendPlanningGlobalToEducateurs();
|
||||
|
||||
assertEquals(2, count);
|
||||
verify(mailSenderMock, timeout(2000).times(2)).send(any(MimeMessage.class));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.7-SNAPSHOT</version>
|
||||
<version>1.7</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -149,6 +149,37 @@ public class DotationController {
|
||||
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/admin/equipements/export-flocage-initiales")
|
||||
public ResponseEntity<byte[]> exportFlocageInitiales() {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue()
|
||||
.orElseThrow(() -> new IllegalStateException("Aucune saison active"));
|
||||
|
||||
String csvContent = dotationService.genererCsvFlocageInitiales(saisonActive);
|
||||
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDispositionFormData("attachment", "flocage_initiales_" + LocalDate.now() + ".csv");
|
||||
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
|
||||
|
||||
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/admin/equipements/export-flocage-prenom-numero")
|
||||
public ResponseEntity<byte[]> exportFlocagePrenomNumero() {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue()
|
||||
.orElseThrow(() -> new IllegalStateException("Aucune saison active"));
|
||||
|
||||
String csvContent = dotationService.genererCsvFlocagePrenomNumero(saisonActive);
|
||||
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDispositionFormData("attachment", "flocage_prenom_numero_" + LocalDate.now() + ".csv");
|
||||
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
|
||||
|
||||
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/dotations/{id}")
|
||||
public String updateDotation(
|
||||
@PathVariable Long id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.astalange.core.repository.EducateurRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.repository.TerrainRepository;
|
||||
import com.astalange.core.service.PlanningEmailService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
@@ -25,19 +26,22 @@ public class PlanningController {
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
private final EducateurRepository educateurRepository;
|
||||
private final PlanningEmailService planningEmailService;
|
||||
|
||||
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
||||
TerrainRepository terrainRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository,
|
||||
EducateurRepository educateurRepository) {
|
||||
EducateurRepository educateurRepository,
|
||||
PlanningEmailService planningEmailService) {
|
||||
this.creneauRepository = creneauRepository;
|
||||
this.terrainRepository = terrainRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
this.educateurRepository = educateurRepository;
|
||||
this.planningEmailService = planningEmailService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -202,4 +206,16 @@ public class PlanningController {
|
||||
redirectAttributes.addFlashAttribute("success", "Créneau d'entraînement supprimé avec succès.");
|
||||
return "redirect:/planning?jour=" + jour.name();
|
||||
}
|
||||
|
||||
@PostMapping("/envoyer-educateurs")
|
||||
public String envoyerPlanningEducateurs(@RequestParam(required = false) JourSemaine jour, RedirectAttributes redirectAttributes) {
|
||||
int count = planningEmailService.sendPlanningGlobalToEducateurs();
|
||||
if (count > 0) {
|
||||
redirectAttributes.addFlashAttribute("success", "Le planning global de la semaine a été envoyé par e-mail à " + count + " éducateur(s).");
|
||||
} else {
|
||||
redirectAttributes.addFlashAttribute("error", "Aucun e-mail n'a pu être envoyé. Vérifiez qu'une saison est active et que des éducateurs possèdent un e-mail valide.");
|
||||
}
|
||||
String returnJour = (jour != null) ? jour.name() : "LUNDI";
|
||||
return "redirect:/planning?jour=" + returnJour;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ public class PublicInscriptionController {
|
||||
|
||||
TokenPreInscription token = new TokenPreInscription();
|
||||
token.setValeurUuid(UUID.randomUUID().toString());
|
||||
token.setDateExpiration(LocalDateTime.now().plusMinutes(15));
|
||||
token.setDateExpiration(LocalDateTime.now().plusMinutes(30));
|
||||
tokenRepository.save(token);
|
||||
|
||||
return "redirect:/inscription-public/formulaire?token=" + token.getValeurUuid();
|
||||
|
||||
@@ -53,20 +53,41 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2">
|
||||
<a th:href="@{/admin/equipements/export-commande}"
|
||||
onclick="return confirm('Exporter les équipements non encore commandés et les marquer comme commandés ?');"
|
||||
class="bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 flex items-center space-x-1 shadow-sm">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Exporter Commande Équipementier (Regroupé)
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 pt-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a th:href="@{/admin/equipements/export-commande}"
|
||||
onclick="return confirm('Exporter les équipements non encore commandés et les marquer comme commandés ?');"
|
||||
class="bg-indigo-600 text-white px-3.5 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 flex items-center space-x-1 shadow-sm transition-colors">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Exporter Commande Équipementier (Regroupé)
|
||||
</a>
|
||||
<a th:href="@{/admin/equipements/export-flocage-initiales}"
|
||||
onclick="return confirm('Exporter la liste des équipements commandés pour le flocage des initiales et les marquer comme exportés ?');"
|
||||
class="bg-purple-600 text-white px-3.5 py-2 rounded-lg text-sm font-medium hover:bg-purple-700 flex items-center space-x-1 shadow-sm transition-colors"
|
||||
title="Exporter les articles commandés nécessitant le flocage des initiales">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h10M7 12h10M7 17h10"/>
|
||||
</svg>
|
||||
Exporter Flocage Initiales
|
||||
</a>
|
||||
<a th:href="@{/admin/equipements/export-flocage-prenom-numero}"
|
||||
onclick="return confirm('Exporter la liste des équipements commandés pour le flocage du prénom/numéro et les marquer comme exportés ?');"
|
||||
class="bg-teal-600 text-white px-3.5 py-2 rounded-lg text-sm font-medium hover:bg-teal-700 flex items-center space-x-1 shadow-sm transition-colors"
|
||||
title="Exporter les articles commandés nécessitant le flocage prénom et/ou numéro">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
Exporter Flocage Prénom / N°
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">Rechercher</button>
|
||||
<a th:href="@{/admin/equipements/recherche/export(equipementId=${equipementId},categorieId=${categorieId},fourni=${fourni},commandee=${commandee})}" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700">Exporter Recherche</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,13 +48,26 @@
|
||||
<div th:if="${success}" class="bg-green-50 border-l-4 border-green-500 p-4 mb-6 rounded-lg">
|
||||
<p class="text-sm text-green-700 font-medium" th:text="${success}"></p>
|
||||
</div>
|
||||
<div th:if="${error}" class="bg-red-50 border-l-4 border-red-500 p-4 mb-6 rounded-lg">
|
||||
<p class="text-sm text-red-700 font-medium" th:text="${error}"></p>
|
||||
</div>
|
||||
|
||||
<!-- Page actions -->
|
||||
<div class="flex justify-between items-center mb-6" th:if="${saisonActive != null}">
|
||||
<h3 class="text-xl font-bold text-gray-900">Planning Hebdomadaire</h3>
|
||||
<a th:href="@{/planning/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 shadow transition-colors">
|
||||
+ Ajouter un Créneau
|
||||
</a>
|
||||
<div class="flex items-center space-x-3">
|
||||
<form th:action="@{/planning/envoyer-educateurs(jour=${selectedDay != null ? selectedDay.name() : 'LUNDI'})}" method="post" onsubmit="return confirm('Êtes-vous sûr de vouloir envoyer le planning global de la semaine par e-mail à tous les éducateurs ?');">
|
||||
<button type="submit" class="bg-emerald-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-emerald-700 shadow transition-colors flex items-center space-x-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span>Envoyer le planning aux éducateurs</span>
|
||||
</button>
|
||||
</form>
|
||||
<a th:href="@{/planning/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 shadow transition-colors">
|
||||
+ Ajouter un Créneau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Days of the week -->
|
||||
|
||||
Reference in New Issue
Block a user