feat: gestion des utilisateurs, rôles et équipes
- Ajout de la section de gestion des utilisateurs avec rôles (Admin, Trésorerie, Agent de Saisie). - Restriction des accès par rôle (Admin a tout, Trésorerie a adhérents/paiements, Agent de saisie a adhérents/planning). - Flux de modification obligatoire de mot de passe temporaire au premier login (par défaut AsTalange123). - Intégration de la configuration des équipes du club.
This commit is contained in:
@@ -26,6 +26,9 @@ public class AppUser {
|
||||
@Column(nullable = false)
|
||||
private boolean enabled = true;
|
||||
|
||||
@Column(name = "must_change_password", nullable = false)
|
||||
private boolean mustChangePassword = false;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "user_role",
|
||||
|
||||
@@ -42,6 +42,10 @@ public class CreneauEntrainement {
|
||||
@JoinColumn(name = "saison_id", nullable = false)
|
||||
private Saison saison;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "equipe_id")
|
||||
private Equipe equipe;
|
||||
|
||||
@Transient
|
||||
private boolean enConflit = false;
|
||||
|
||||
@@ -187,6 +191,14 @@ public class CreneauEntrainement {
|
||||
this.saison = saison;
|
||||
}
|
||||
|
||||
public Equipe getEquipe() {
|
||||
return equipe;
|
||||
}
|
||||
|
||||
public void setEquipe(Equipe equipe) {
|
||||
this.equipe = equipe;
|
||||
}
|
||||
|
||||
public boolean isEnConflit() {
|
||||
return enConflit;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.astalange.core.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "equipe")
|
||||
public class Equipe {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String nom;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "categorie_id", nullable = false)
|
||||
private Categorie categorie;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "saison_id", nullable = false)
|
||||
private Saison saison;
|
||||
|
||||
// Constructors
|
||||
public Equipe() {}
|
||||
|
||||
public Equipe(String nom, Categorie categorie, Saison saison) {
|
||||
this.nom = nom;
|
||||
this.categorie = categorie;
|
||||
this.saison = saison;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getNom() { return nom; }
|
||||
public void setNom(String nom) { this.nom = nom; }
|
||||
|
||||
public Categorie getCategorie() { return categorie; }
|
||||
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
|
||||
|
||||
public Saison getSaison() { return saison; }
|
||||
public void setSaison(Saison saison) { this.saison = saison; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Equipe equipe = (Equipe) o;
|
||||
return Objects.equals(id, equipe.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,10 @@ public class Licence {
|
||||
@JoinColumn(name = "saison_id", nullable = false)
|
||||
private Saison saison;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "equipe_id")
|
||||
private Equipe equipe;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
private String etat;
|
||||
|
||||
@@ -93,6 +97,9 @@ public class Licence {
|
||||
public Saison getSaison() { return saison; }
|
||||
public void setSaison(Saison saison) { this.saison = saison; }
|
||||
|
||||
public Equipe getEquipe() { return equipe; }
|
||||
public void setEquipe(Equipe equipe) { this.equipe = equipe; }
|
||||
|
||||
public String getEtat() { return etat; }
|
||||
public void setEtat(String etat) { this.etat = etat; }
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Equipe;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface EquipeRepository extends JpaRepository<Equipe, Long> {
|
||||
List<Equipe> findByCategorieId(Long categorieId);
|
||||
List<Equipe> findBySaison(Saison saison);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.astalange.core.repository;
|
||||
|
||||
import com.astalange.core.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||
Optional<Role> findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.astalange.core.security;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import java.util.Collection;
|
||||
|
||||
public class CustomUserDetails extends User {
|
||||
private final Long id;
|
||||
private final boolean mustChangePassword;
|
||||
|
||||
public CustomUserDetails(Long id, String username, String password, boolean enabled, boolean mustChangePassword,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(username, password, enabled, true, true, true, authorities);
|
||||
this.id = id;
|
||||
this.mustChangePassword = mustChangePassword;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isMustChangePassword() {
|
||||
return mustChangePassword;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -25,11 +25,12 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
AppUser appUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
return new User(
|
||||
return new CustomUserDetails(
|
||||
appUser.getId(),
|
||||
appUser.getUsername(),
|
||||
appUser.getPassword(),
|
||||
appUser.isEnabled(),
|
||||
true, true, true,
|
||||
appUser.isMustChangePassword(),
|
||||
appUser.getRoles().stream()
|
||||
.map(role -> new SimpleGrantedAuthority(role.getName()))
|
||||
.collect(Collectors.toList())
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- V11__add_equipes.sql
|
||||
|
||||
-- 1. Table des équipes
|
||||
CREATE TABLE equipe (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nom VARCHAR(255) NOT NULL,
|
||||
categorie_id BIGINT NOT NULL,
|
||||
saison_id BIGINT NOT NULL,
|
||||
CONSTRAINT fk_equipe_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_equipe_saison FOREIGN KEY (saison_id) REFERENCES saison(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 2. Ajouter equipe_id à la table licence (nullable, facultative)
|
||||
ALTER TABLE licence ADD COLUMN equipe_id BIGINT;
|
||||
ALTER TABLE licence ADD CONSTRAINT fk_licence_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
|
||||
|
||||
-- 3. Ajouter equipe_id à la table creneau_entrainement (nullable, facultative)
|
||||
ALTER TABLE creneau_entrainement ADD COLUMN equipe_id BIGINT;
|
||||
ALTER TABLE creneau_entrainement ADD CONSTRAINT fk_creneau_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE app_user ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
INSERT INTO role (name) VALUES ('ROLE_TRESORERIE'), ('ROLE_AGENT_SAISIE') ON CONFLICT (name) DO NOTHING;
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.astalange.core;
|
||||
|
||||
import com.astalange.core.entity.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class EquipeTest {
|
||||
|
||||
@Test
|
||||
public void testEquipeEntityAndAssociations() {
|
||||
Categorie U15 = new Categorie();
|
||||
U15.setNom("U15");
|
||||
|
||||
Equipe equipeA = new Equipe();
|
||||
equipeA.setNom("Équipe 1");
|
||||
equipeA.setCategorie(U15);
|
||||
|
||||
assertEquals("Équipe 1", equipeA.getNom());
|
||||
assertEquals(U15, equipeA.getCategorie());
|
||||
|
||||
Licence licence = new Licence();
|
||||
licence.setCategorie(U15);
|
||||
licence.setEquipe(equipeA);
|
||||
|
||||
assertEquals(equipeA, licence.getEquipe());
|
||||
|
||||
CreneauEntrainement creneau = new CreneauEntrainement();
|
||||
creneau.setCategorie(U15);
|
||||
creneau.setEquipe(equipeA);
|
||||
|
||||
assertEquals(equipeA, creneau.getEquipe());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import com.astalange.core.security.CustomUserDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class PasswordChangeInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomUserDetails) {
|
||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
||||
if (userDetails.isMustChangePassword()) {
|
||||
String uri = request.getRequestURI();
|
||||
// Exclude /change-password, /logout, static assets, error pages
|
||||
if (!uri.equals("/change-password") &&
|
||||
!uri.equals("/logout") &&
|
||||
!uri.startsWith("/css/") &&
|
||||
!uri.startsWith("/js/") &&
|
||||
!uri.startsWith("/images/") &&
|
||||
!uri.equals("/error")) {
|
||||
response.sendRedirect("/change-password");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ public class SecurityConfig {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
|
||||
.requestMatchers("/change-password").authenticated()
|
||||
.requestMatchers("/utilisateurs/**", "/saisons/**", "/categories/**", "/equipes/**", "/modespaiement/**", "/equipements/**").hasRole("ADMIN")
|
||||
.requestMatchers("/paiements/**", "/paiement/**").hasAnyRole("ADMIN", "TRESORERIE")
|
||||
.requestMatchers("/planning/**").hasAnyRole("ADMIN", "AGENT_SAISIE")
|
||||
.requestMatchers("/adherents/**", "/dotations/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final PasswordChangeInterceptor passwordChangeInterceptor;
|
||||
|
||||
public WebConfig(PasswordChangeInterceptor passwordChangeInterceptor) {
|
||||
this.passwordChangeInterceptor = passwordChangeInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(passwordChangeInterceptor);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,11 @@ public class AdherentController {
|
||||
this.saisonRepository = saisonRepository;
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.ModelAttribute("equipes")
|
||||
public List<com.astalange.core.entity.Equipe> populateEquipes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listAdherents(
|
||||
@org.springframework.web.bind.annotation.RequestParam(required = false) String query,
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import com.astalange.core.repository.AppUserRepository;
|
||||
import com.astalange.core.security.CustomUserDetails;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class ChangePasswordController {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public ChangePasswordController(AppUserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@GetMapping("/change-password")
|
||||
public String showChangePasswordForm() {
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public String changePassword(
|
||||
@RequestParam String newPassword,
|
||||
@RequestParam String confirmPassword,
|
||||
Model model) {
|
||||
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails)) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
|
||||
|
||||
if (!newPassword.equals(confirmPassword)) {
|
||||
model.addAttribute("error", "Les mots de passe ne correspondent pas.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
if (newPassword.equals("AsTalange123")) {
|
||||
model.addAttribute("error", "Le nouveau mot de passe doit être différent de AsTalange123.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
if (newPassword.length() < 6) {
|
||||
model.addAttribute("error", "Le mot de passe doit contenir au moins 6 caractères.");
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
AppUser appUser = userRepository.findById(userDetails.getId())
|
||||
.orElseThrow(() -> new IllegalStateException("Utilisateur non trouvé"));
|
||||
|
||||
appUser.setPassword(passwordEncoder.encode(newPassword));
|
||||
appUser.setMustChangePassword(false);
|
||||
userRepository.save(appUser);
|
||||
|
||||
// Update Authentication in Security Context
|
||||
CustomUserDetails newPrincipal = new CustomUserDetails(
|
||||
appUser.getId(),
|
||||
appUser.getUsername(),
|
||||
appUser.getPassword(),
|
||||
appUser.isEnabled(),
|
||||
false,
|
||||
auth.getAuthorities()
|
||||
);
|
||||
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
|
||||
newPrincipal, auth.getCredentials(), auth.getAuthorities()
|
||||
);
|
||||
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||
|
||||
return "redirect:/?passwordChanged=true";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Equipe;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class EquipeController {
|
||||
|
||||
private final EquipeRepository equipeRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
|
||||
public EquipeController(EquipeRepository equipeRepository, CategorieRepository categorieRepository, SaisonRepository saisonRepository) {
|
||||
this.equipeRepository = equipeRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/categories/equipes")
|
||||
public String getEquipesByCategorie(@RequestParam(required = false) Long categorieId, Model model) {
|
||||
if (categorieId != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(categorieId));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
return "adherents/form :: equipe-options";
|
||||
}
|
||||
|
||||
@GetMapping("/equipes")
|
||||
public String listEquipes(Model model) {
|
||||
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
|
||||
if (saisonActive != null) {
|
||||
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
|
||||
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
|
||||
model.addAttribute("saisonActive", saisonActive);
|
||||
} else {
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
model.addAttribute("equipes", equipeRepository.findAll());
|
||||
}
|
||||
return "parametrage/equipes_list";
|
||||
}
|
||||
|
||||
@PostMapping("/equipes")
|
||||
public String saveEquipe(@RequestParam String nom, @RequestParam Long categorieId) {
|
||||
Categorie categorie = categorieRepository.findById(categorieId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Catégorie invalide : " + categorieId));
|
||||
Saison saison = categorie.getSaison();
|
||||
if (saison == null) {
|
||||
saison = saisonRepository.findByEstActiveTrue()
|
||||
.orElseThrow(() -> new IllegalStateException("Aucune saison active configurée"));
|
||||
}
|
||||
Equipe equipe = new Equipe(nom, categorie, saison);
|
||||
equipeRepository.save(equipe);
|
||||
return "redirect:/equipes";
|
||||
}
|
||||
|
||||
@PostMapping("/equipes/{id}/delete")
|
||||
public String deleteEquipe(@PathVariable Long id) {
|
||||
Equipe equipe = equipeRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Équipe invalide : " + id));
|
||||
equipeRepository.delete(equipe);
|
||||
return "redirect:/equipes";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,7 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.entity.Licence;
|
||||
import com.astalange.core.entity.Dotation;
|
||||
import com.astalange.core.entity.Equipement;
|
||||
import com.astalange.core.entity.Saison;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.DotationRepository;
|
||||
import com.astalange.core.repository.EquipementRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.*;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -28,19 +18,22 @@ public class LicenceController {
|
||||
private final EquipementRepository equipementRepository;
|
||||
private final DotationRepository dotationRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
|
||||
public LicenceController(AdherentRepository adherentRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
LicenceRepository licenceRepository,
|
||||
EquipementRepository equipementRepository,
|
||||
DotationRepository dotationRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.equipementRepository = equipementRepository;
|
||||
this.dotationRepository = dotationRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/adherents/{id}/licences")
|
||||
@@ -48,7 +41,8 @@ public class LicenceController {
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String etat,
|
||||
@RequestParam(required = false) String numeroLicence) {
|
||||
@RequestParam(required = false) String numeroLicence,
|
||||
@RequestParam(required = false) Long equipeId) {
|
||||
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Adherent ID"));
|
||||
@@ -71,6 +65,12 @@ public class LicenceController {
|
||||
licence.setEtat(etat);
|
||||
licence.setNumeroLicence(numeroLicence);
|
||||
|
||||
if (equipeId != null) {
|
||||
Equipe equipe = equipeRepository.findById(equipeId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Equipe ID"));
|
||||
licence.setEquipe(equipe);
|
||||
}
|
||||
|
||||
licence = licenceRepository.save(licence);
|
||||
|
||||
// Auto-initialize dotations for all configured equipments of the active season
|
||||
@@ -96,7 +96,8 @@ public class LicenceController {
|
||||
@PathVariable Long licenceId,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String etat,
|
||||
@RequestParam(required = false) String numeroLicence) {
|
||||
@RequestParam(required = false) String numeroLicence,
|
||||
@RequestParam(required = false) Long equipeId) {
|
||||
|
||||
Licence licence = licenceRepository.findById(licenceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
|
||||
@@ -108,6 +109,14 @@ public class LicenceController {
|
||||
licence.setEtat(etat);
|
||||
licence.setNumeroLicence(numeroLicence);
|
||||
|
||||
if (equipeId != null) {
|
||||
Equipe equipe = equipeRepository.findById(equipeId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Equipe ID"));
|
||||
licence.setEquipe(equipe);
|
||||
} else {
|
||||
licence.setEquipe(null);
|
||||
}
|
||||
|
||||
licenceRepository.save(licence);
|
||||
|
||||
return "redirect:/adherents/" + adherentId + "/edit";
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.astalange.web.controller;
|
||||
import com.astalange.core.entity.*;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.CreneauEntrainementRepository;
|
||||
import com.astalange.core.repository.EquipeRepository;
|
||||
import com.astalange.core.repository.SaisonRepository;
|
||||
import com.astalange.core.repository.TerrainRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -21,15 +22,18 @@ public class PlanningController {
|
||||
private final TerrainRepository terrainRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final SaisonRepository saisonRepository;
|
||||
private final EquipeRepository equipeRepository;
|
||||
|
||||
public PlanningController(CreneauEntrainementRepository creneauRepository,
|
||||
TerrainRepository terrainRepository,
|
||||
CategorieRepository categorieRepository,
|
||||
SaisonRepository saisonRepository) {
|
||||
SaisonRepository saisonRepository,
|
||||
EquipeRepository equipeRepository) {
|
||||
this.creneauRepository = creneauRepository;
|
||||
this.terrainRepository = terrainRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.saisonRepository = saisonRepository;
|
||||
this.equipeRepository = equipeRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -92,6 +96,7 @@ public class PlanningController {
|
||||
model.addAttribute("terrains", terrainRepository.findAll());
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
model.addAttribute("equipes", List.of());
|
||||
|
||||
return "planning/form";
|
||||
}
|
||||
@@ -107,6 +112,12 @@ public class PlanningController {
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
|
||||
if (creneau.getCategorie() != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(creneau.getCategorie().getId()));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
|
||||
return "planning/form";
|
||||
}
|
||||
|
||||
@@ -122,6 +133,10 @@ public class PlanningController {
|
||||
}
|
||||
}
|
||||
|
||||
if (creneau.getEquipe() != null && creneau.getEquipe().getId() == null) {
|
||||
creneau.setEquipe(null);
|
||||
}
|
||||
|
||||
if (result.hasErrors()) {
|
||||
Saison saison = creneau.getSaison();
|
||||
if (saison == null) {
|
||||
@@ -132,6 +147,11 @@ public class PlanningController {
|
||||
model.addAttribute("terrains", terrainRepository.findAll());
|
||||
model.addAttribute("zones", ZoneTerrain.values());
|
||||
model.addAttribute("jours", JourSemaine.values());
|
||||
if (creneau.getCategorie() != null && creneau.getCategorie().getId() != null) {
|
||||
model.addAttribute("equipes", equipeRepository.findByCategorieId(creneau.getCategorie().getId()));
|
||||
} else {
|
||||
model.addAttribute("equipes", List.of());
|
||||
}
|
||||
return "planning/form";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.AppUser;
|
||||
import com.astalange.core.entity.Role;
|
||||
import com.astalange.core.repository.AppUserRepository;
|
||||
import com.astalange.core.repository.RoleRepository;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/utilisateurs")
|
||||
public class UtilisateurController {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UtilisateurController(AppUserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listUsers(Model model) {
|
||||
List<AppUser> users = userRepository.findAll();
|
||||
model.addAttribute("users", users);
|
||||
return "utilisateurs/list";
|
||||
}
|
||||
|
||||
@GetMapping("/ajouter")
|
||||
public String showAddForm(Model model) {
|
||||
model.addAttribute("username", "");
|
||||
model.addAttribute("defaultPassword", "AsTalange123");
|
||||
model.addAttribute("roles", roleRepository.findAll());
|
||||
return "utilisateurs/form";
|
||||
}
|
||||
|
||||
@PostMapping("/ajouter")
|
||||
public String addUser(
|
||||
@RequestParam String username,
|
||||
@RequestParam String password,
|
||||
@RequestParam Long roleId,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model) {
|
||||
|
||||
if (userRepository.findByUsername(username).isPresent()) {
|
||||
model.addAttribute("error", "Nom d'utilisateur déjà utilisé.");
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("defaultPassword", password);
|
||||
model.addAttribute("roles", roleRepository.findAll());
|
||||
return "utilisateurs/form";
|
||||
}
|
||||
|
||||
Role role = roleRepository.findById(roleId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Rôle invalide: " + roleId));
|
||||
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUsername(username);
|
||||
appUser.setPassword(passwordEncoder.encode(password));
|
||||
appUser.setEnabled(true);
|
||||
appUser.setMustChangePassword(true);
|
||||
appUser.setRoles(Set.of(role));
|
||||
|
||||
userRepository.save(appUser);
|
||||
|
||||
redirectAttributes.addFlashAttribute("success", "Utilisateur créé avec succès !");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteUser(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
AppUser appUser = userRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Utilisateur non trouvé"));
|
||||
|
||||
org.springframework.security.core.Authentication auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getName().equals(appUser.getUsername())) {
|
||||
redirectAttributes.addFlashAttribute("error", "Vous ne pouvez pas supprimer votre propre compte.");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
|
||||
userRepository.delete(appUser);
|
||||
redirectAttributes.addFlashAttribute("success", "Utilisateur supprimé avec succès.");
|
||||
return "redirect:/utilisateurs";
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@
|
||||
th:data-total-paye="${lic.getPrixTotal().subtract(lic.getResteAPayer())}">
|
||||
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison.nom}">2024-2025</td>
|
||||
<td class="py-4 px-4 text-gray-600">
|
||||
<span th:text="${lic.categorie.nom}">U15</span>
|
||||
<span th:text="${lic.categorie.nom + (lic.equipe != null ? ' (' + lic.equipe.nom + ')' : '')}">U15</span>
|
||||
<span class="text-[10px] text-gray-400 block cell-tarif-categorie" th:text="${(lic.adherent != null && lic.adherent.residentTalange ? lic.categorie.tarifBase : lic.categorie.tarifExterieur) + ' € (Catégorie)'}">100.00 € (Catégorie)</span>
|
||||
</td>
|
||||
<td class="py-4 px-4 text-gray-600 font-mono text-xs" th:text="${lic.numeroLicence != null and !lic.numeroLicence.isEmpty() ? lic.numeroLicence : '-'}">-</td>
|
||||
@@ -193,7 +193,8 @@
|
||||
th:data-categorie-id="${lic.categorie.id}"
|
||||
th:data-numero-licence="${lic.numeroLicence}"
|
||||
th:data-etat="${lic.etat}"
|
||||
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'))"
|
||||
th:data-equipe-id="${lic.equipe != null ? lic.equipe.id : ''}"
|
||||
onclick="openLicenceModal(this.getAttribute('data-licence-id'), this.getAttribute('data-categorie-id'), this.getAttribute('data-numero-licence'), this.getAttribute('data-etat'), this.getAttribute('data-equipe-id'))"
|
||||
class="text-blue-600 hover:text-blue-800 font-medium bg-blue-50 px-3 py-1 rounded-lg">Modifier</button>
|
||||
<button th:if="${lic.getResteAPayer() > 0}"
|
||||
type="button"
|
||||
@@ -330,6 +331,15 @@
|
||||
th:text="${cat.nom} + ' (Talange: ' + ${cat.tarifBase} + ' € / Ext: ' + ${cat.tarifExterieur} + ' €)'"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Équipe</label>
|
||||
<select id="licenceEquipe" name="equipeId" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<th:block th:fragment="equipe-options">
|
||||
<option value="">Aucune équipe</option>
|
||||
<option th:each="team : ${equipes}" th:value="${team.id}" th:text="${team.nom}"></option>
|
||||
</th:block>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
|
||||
<input id="numeroLicenceInput" type="text" name="numeroLicence" placeholder="Ex: FFF-123456" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
@@ -388,18 +398,33 @@
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon') {
|
||||
function openLicenceModal(licenceId = null, categorieId = '', numeroLicence = '', etat = 'Brouillon', equipeId = '') {
|
||||
document.getElementById('licenceModal').classList.remove('hidden');
|
||||
const form = document.getElementById('licenceForm');
|
||||
const title = document.getElementById('licenceModalTitle');
|
||||
const adherentId = [[${adherent.id}]];
|
||||
const catSelect = document.getElementById('categorieSelect');
|
||||
const equipeSelect = document.getElementById('licenceEquipe');
|
||||
|
||||
// Clean up team options first
|
||||
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
|
||||
|
||||
if (licenceId) {
|
||||
title.textContent = 'Modifier la Licence';
|
||||
form.action = '/adherents/' + adherentId + '/licences/' + licenceId + '/update';
|
||||
document.getElementById('categorieSelect').value = categorieId;
|
||||
catSelect.value = categorieId;
|
||||
document.getElementById('numeroLicenceInput').value = numeroLicence;
|
||||
document.getElementById('etatSelect').value = etat;
|
||||
|
||||
// Fetch and populate teams
|
||||
if (categorieId) {
|
||||
fetch('/categories/equipes?categorieId=' + categorieId)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
equipeSelect.innerHTML = html;
|
||||
equipeSelect.value = equipeId;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
title.textContent = 'Nouvelle Licence';
|
||||
form.action = '/adherents/' + adherentId + '/licences';
|
||||
@@ -408,8 +433,8 @@
|
||||
|
||||
// Pré-sélection de la catégorie selon l'année de naissance
|
||||
const dateNaissanceStr = document.getElementById('dateNaissance').value;
|
||||
const catSelect = document.getElementById('categorieSelect');
|
||||
let foundCat = false;
|
||||
let preSelectedCatId = '';
|
||||
|
||||
if (dateNaissanceStr) {
|
||||
const birthYear = new Date(dateNaissanceStr).getFullYear();
|
||||
@@ -419,6 +444,7 @@
|
||||
const max = parseInt(opt.getAttribute('data-annee-max'));
|
||||
if (!isNaN(min) && !isNaN(max) && birthYear >= min && birthYear <= max) {
|
||||
catSelect.value = opt.value;
|
||||
preSelectedCatId = opt.value;
|
||||
foundCat = true;
|
||||
}
|
||||
});
|
||||
@@ -427,6 +453,15 @@
|
||||
if (!foundCat) {
|
||||
catSelect.value = '';
|
||||
}
|
||||
|
||||
// Fetch and populate teams for the pre-selected category
|
||||
if (preSelectedCatId) {
|
||||
fetch('/categories/equipes?categorieId=' + preSelectedCatId)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
equipeSelect.innerHTML = html;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function closeLicenceModal() {
|
||||
@@ -448,6 +483,22 @@
|
||||
const repBlock = document.getElementById('representantBlock');
|
||||
const residentCheckbox = document.getElementById('residentTalange');
|
||||
|
||||
const catSelect = document.getElementById('categorieSelect');
|
||||
if (catSelect) {
|
||||
catSelect.addEventListener('change', function() {
|
||||
const catId = this.value;
|
||||
const equipeSelect = document.getElementById('licenceEquipe');
|
||||
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
|
||||
if (catId) {
|
||||
fetch('/categories/equipes?categorieId=' + catId)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
equipeSelect.innerHTML = html;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function calculateAge(birthday) {
|
||||
const ageDifMs = Date.now() - birthday.getTime();
|
||||
const ageDate = new Date(ageDifMs);
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
|
||||
<td class="py-4 px-6 text-gray-600 font-medium" th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() : '-'}">-</td>
|
||||
<td class="py-4 px-6 text-gray-600 font-medium"
|
||||
th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() + (adherent.getLicenceActuelle().getEquipe() != null ? ' (' + adherent.getLicenceActuelle().getEquipe().getNom() + ')' : '') : '-'}">-</td>
|
||||
<td class="py-4 px-6 text-center text-gray-600 font-mono text-xs"
|
||||
th:text="${adherent.getLicenceActuelle() != null and adherent.getLicenceActuelle().numeroLicence != null and !adherent.getLicenceActuelle().numeroLicence.isEmpty() ? adherent.getLicenceActuelle().numeroLicence : '-'}">-</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${adherent.email}">jean@example.com</td>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Changement de mot de passe - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
|
||||
|
||||
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-gray-100">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Première connexion</h1>
|
||||
<p class="text-gray-500">Pour des raisons de sécurité, veuillez modifier votre mot de passe temporaire.</p>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm" th:text="${error}">
|
||||
Une erreur est survenue.
|
||||
</div>
|
||||
|
||||
<form th:action="@{/change-password}" method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="newPassword" class="block text-sm font-medium text-gray-700 mb-1">Nouveau mot de passe</label>
|
||||
<input type="password" id="newPassword" name="newPassword" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required autofocus>
|
||||
<p class="text-xs text-gray-400 mt-1">Le mot de passe doit être différent de AsTalange123.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">Confirmer le nouveau mot de passe</label>
|
||||
<input type="password" id="confirmPassword" name="confirmPassword" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200">
|
||||
Enregistrer le mot de passe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
@@ -11,14 +11,25 @@
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" th:classappend="${requestURI == '/' ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" th:classappend="${requestURI != null and requestURI.startsWith('/adherents') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Adhérents</a>
|
||||
<a href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div>
|
||||
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Entraînements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
|
||||
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Modes de Paiement</a>
|
||||
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipements</a>
|
||||
|
||||
<a sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_TRESORERIE')" href="/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Paiements</a>
|
||||
|
||||
<th:block sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_AGENT_SAISIE')">
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Planning</div>
|
||||
<a href="/planning" th:classappend="${requestURI != null and requestURI.startsWith('/planning') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Entraînements</a>
|
||||
</th:block>
|
||||
|
||||
<th:block sec:authorize="hasRole('ROLE_ADMIN')">
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/saisons" th:classappend="${requestURI != null and requestURI.startsWith('/saisons') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Saisons</a>
|
||||
<a href="/categories" th:classappend="${requestURI != null and requestURI.startsWith('/categories') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Catégories</a>
|
||||
<a href="/equipes" th:classappend="${requestURI != null and requestURI.startsWith('/equipes') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipes</a>
|
||||
<a href="/modespaiement" th:classappend="${requestURI != null and requestURI.startsWith('/modespaiement') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Modes de Paiement</a>
|
||||
<a href="/equipements" th:classappend="${requestURI != null and requestURI.startsWith('/equipements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Équipements</a>
|
||||
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div>
|
||||
<a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a>
|
||||
</th:block>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username != null ? username : 'Admin'}">Admin</div>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Équipes</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style> body { font-family: 'Inter', sans-serif; } </style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
|
||||
|
||||
<main class="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Configuration des Équipes</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Left: Add Team Form Card -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 h-fit">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">Créer une Équipe</h3>
|
||||
<form th:action="@{/equipes}" method="post" class="space-y-4">
|
||||
<div>
|
||||
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom de l'équipe</label>
|
||||
<input type="text" id="nom" name="nom" placeholder="Ex: Équipe A, Équipe 1" required
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label for="categorie" class="block text-sm font-medium text-gray-700 mb-1">Catégorie associée</label>
|
||||
<select id="categorie" name="categorieId" required
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
|
||||
<option value="">Choisir une catégorie...</option>
|
||||
<option th:each="cat : ${categories}" th:value="${cat.id}"
|
||||
th:text="${cat.nom + ' (' + cat.saison.nom + ')'}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-semibold transition-colors shadow-sm">
|
||||
+ Ajouter l'équipe
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right: Teams List Card -->
|
||||
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<h3 class="font-bold text-gray-900">Équipes Paramétrées</h3>
|
||||
<span class="text-xs font-semibold text-gray-500 bg-gray-200 px-2.5 py-1 rounded-full"
|
||||
th:text="${saisonActive != null ? 'Saison active : ' + saisonActive.nom : 'Toutes les Saisons'}">Saison Actuelle</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
|
||||
<th class="py-3 px-6 font-medium">Nom de l'équipe</th>
|
||||
<th class="py-3 px-6 font-medium">Catégorie</th>
|
||||
<th class="py-3 px-6 font-medium">Saison</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(equipes)}">
|
||||
<td colspan="4" class="py-8 text-center text-gray-500">Aucune équipe configurée pour le moment. Usez du formulaire à gauche pour en créer une.</td>
|
||||
</tr>
|
||||
<tr th:each="team : ${equipes}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${team.nom}">Équipe 1</td>
|
||||
<td class="py-4 px-6 text-gray-600">
|
||||
<span class="bg-blue-50 text-blue-700 px-2.5 py-1 rounded-full text-xs font-medium" th:text="${team.categorie.nom}">U15</span>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-gray-500" th:text="${team.saison.nom}">2024-2025</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end items-center">
|
||||
<form th:action="@{/equipes/{id}/delete(id=${team.id})}" method="post" onsubmit="return confirm('Supprimer cette équipe ? Elle sera dissociée de toutes ses licences.');">
|
||||
<button type="submit" class="text-red-500 hover:text-red-700 font-medium text-xs bg-red-50 hover:bg-red-100 px-2.5 py-1.5 rounded-lg transition-colors">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -33,6 +33,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Équipe (Optionnelle) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Équipe</label>
|
||||
<select id="planningEquipe" name="equipe.id" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option value="">Aucune équipe</option>
|
||||
<option th:each="team : ${equipes}" th:value="${team.id}" th:text="${team.nom}" th:selected="${creneau.equipe != null && creneau.equipe.id == team.id}"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Terrain -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Terrain <span class="text-red-500">*</span></label>
|
||||
@@ -95,5 +104,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const catSelect = document.getElementsByName('categorie.id')[0];
|
||||
if (catSelect) {
|
||||
catSelect.addEventListener('change', function() {
|
||||
const catId = this.value;
|
||||
const equipeSelect = document.getElementById('planningEquipe');
|
||||
equipeSelect.innerHTML = '<option value="">Aucune équipe</option>';
|
||||
if (catId) {
|
||||
fetch('/categories/equipes?categorieId=' + catId)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
equipeSelect.innerHTML = html;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
class="border-l-4 p-2 rounded shadow-sm flex flex-col justify-between overflow-hidden cursor-pointer hover:shadow transition-all relative z-10 m-0.5">
|
||||
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-bold text-xs" th:text="${c.categorie.nom}">U15</span>
|
||||
<span class="font-bold text-xs" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U15</span>
|
||||
<!-- Delete button or info -->
|
||||
<div class="flex items-center space-x-1">
|
||||
<a th:href="@{/planning/{id}/edit(id=${c.id})}" class="text-[10px] text-gray-500 hover:underline">Modifier</a>
|
||||
@@ -192,7 +192,7 @@
|
||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucun entraînement planifié pour ce jour.</td>
|
||||
</tr>
|
||||
<tr th:each="c : ${creneaux}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom}">U11</td>
|
||||
<td class="py-4 px-6 font-semibold text-gray-900" th:text="${c.categorie.nom + (c.equipe != null ? ' (' + c.equipe.nom + ')' : '')}">U11</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${c.terrain.nom}">Terrain Synthétique</td>
|
||||
<td class="py-4 px-6 text-gray-600">
|
||||
<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs" th:text="${c.zone.libelle}">Demi A</span>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ajouter un Utilisateur - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Ajouter un Utilisateur</h2>
|
||||
<a href="/utilisateurs" class="text-sm font-medium text-gray-600 hover:text-gray-900">
|
||||
← Retour à la liste
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-50/50 flex justify-center items-start">
|
||||
|
||||
<div class="w-full max-w-lg bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-4">
|
||||
<div th:if="${error}" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/utilisateurs/ajouter}" method="post" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Nom d'utilisateur</label>
|
||||
<input type="text" id="username" name="username" th:value="${username}" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required placeholder="Ex: JeanDupont" autofocus>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="roleId" class="block text-sm font-medium text-gray-700 mb-1">Rôle</label>
|
||||
<select id="roleId" name="roleId" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors bg-white" required>
|
||||
<option value="">Sélectionnez un rôle</option>
|
||||
<option th:each="r : ${roles}" th:value="${r.id}"
|
||||
th:text="${r.name == 'ROLE_ADMIN' ? 'Administrateur' : (r.name == 'ROLE_TRESORERIE' ? 'Trésorerie' : (r.name == 'ROLE_AGENT_SAISIE' ? 'Agent de Saisie' : r.name))}">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Mot de passe temporaire</label>
|
||||
<input type="text" id="password" name="password" th:value="${defaultPassword}" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" required>
|
||||
<p class="text-xs text-gray-400 mt-1">Le mot de passe par défaut est AsTalange123. L'utilisateur devra le modifier lors de sa première connexion.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200">
|
||||
Créer l'utilisateur
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Gestion des Utilisateurs - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Gestion des Utilisateurs</h2>
|
||||
<a href="/utilisateurs/ajouter" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors text-sm">
|
||||
Ajouter un utilisateur
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-50/50">
|
||||
|
||||
<!-- Success / Error messages -->
|
||||
<div th:if="${success}" class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
<span th:text="${success}"></span>
|
||||
</div>
|
||||
<div th:if="${error}" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50/50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-100">
|
||||
<th class="py-3 px-6 font-medium">Nom d'utilisateur</th>
|
||||
<th class="py-3 px-6 font-medium">Rôle</th>
|
||||
<th class="py-3 px-6 font-medium">Statut</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm">
|
||||
<tr th:each="user : ${users}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${user.username}">
|
||||
Ucef
|
||||
</td>
|
||||
<td class="py-4 px-6 text-gray-600">
|
||||
<span th:each="role : ${user.roles}"
|
||||
th:text="${role.name == 'ROLE_ADMIN' ? 'Administrateur' : (role.name == 'ROLE_TRESORERIE' ? 'Trésorerie' : (role.name == 'ROLE_AGENT_SAISIE' ? 'Agent de Saisie' : role.name))}"
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mr-1">
|
||||
Admin
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 px-6">
|
||||
<span th:if="${user.mustChangePassword}" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
Mot de passe temporaire
|
||||
</span>
|
||||
<span th:unless="${user.mustChangePassword}" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Actif
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-right">
|
||||
<!-- Don't show delete button for the current user -->
|
||||
<form th:if="${#authentication.name != user.username}"
|
||||
th:action="@{/utilisateurs/{id}/delete(id=${user.id})}"
|
||||
method="post"
|
||||
class="inline"
|
||||
onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
<span th:if="${#authentication.name == user.username}" class="text-gray-400 italic text-xs">
|
||||
Connecté
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user