Initial commit - nettoyage et initialisation
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>as-talange-parent</artifactId>
|
||||
<groupId>com.astalange</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>as-talange-web</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.astalange</groupId>
|
||||
<artifactId>as-talange-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>com.astalange.web.AsTalangeApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.astalange.web;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.astalange")
|
||||
@EntityScan(basePackages = "com.astalange.core.entity")
|
||||
@EnableJpaRepositories(basePackages = "com.astalange.core.repository")
|
||||
public class AsTalangeApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AsTalangeApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.astalange.web.config;
|
||||
|
||||
import com.astalange.core.security.UserDetailsServiceImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessUrl("/login?logout")
|
||||
.permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/adherents")
|
||||
public class AdherentController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final com.astalange.core.repository.LicenceRepository licenceRepository;
|
||||
private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository;
|
||||
|
||||
public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listAdherents(Model model) {
|
||||
List<Adherent> adherents = adherentRepository.findAll();
|
||||
model.addAttribute("adherents", adherents);
|
||||
return "adherents/list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("adherent", new Adherent());
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.PostMapping
|
||||
public String saveAdherent(
|
||||
@jakarta.validation.Valid @org.springframework.web.bind.annotation.ModelAttribute("adherent") Adherent adherent,
|
||||
org.springframework.validation.BindingResult result,
|
||||
Model model) {
|
||||
|
||||
if (result.hasErrors()) {
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
adherentRepository.save(adherent);
|
||||
return "redirect:/adherents";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
|
||||
model.addAttribute("adherent", adherent);
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
model.addAttribute("licences", licenceRepository.findByAdherentId(id));
|
||||
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
|
||||
return "adherents/form";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.PostMapping("/{id}/delete")
|
||||
public String deleteAdherent(@org.springframework.web.bind.annotation.PathVariable Long id) {
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
|
||||
adherentRepository.delete(adherent);
|
||||
return "redirect:/adherents";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Categorie;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/categories")
|
||||
public class CategorieController {
|
||||
|
||||
private final CategorieRepository categorieRepository;
|
||||
|
||||
public CategorieController(CategorieRepository categorieRepository) {
|
||||
this.categorieRepository = categorieRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listCategories(Model model) {
|
||||
model.addAttribute("categories", categorieRepository.findAll());
|
||||
return "parametrage/categories_list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("categorie", new Categorie());
|
||||
return "parametrage/categories_form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
Categorie categorie = categorieRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
||||
model.addAttribute("categorie", categorie);
|
||||
return "parametrage/categories_form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) {
|
||||
categorieRepository.save(categorie);
|
||||
return "redirect:/categories";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteCategorie(@PathVariable Long id) {
|
||||
Categorie categorie = categorieRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
|
||||
categorieRepository.delete(categorie);
|
||||
return "redirect:/categories";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Adherent;
|
||||
import com.astalange.core.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.PaiementRepository;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final LicenceRepository licenceRepository;
|
||||
private final PaiementRepository paiementRepository;
|
||||
|
||||
public HomeController(AdherentRepository adherentRepository, LicenceRepository licenceRepository, PaiementRepository paiementRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.paiementRepository = paiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model, Authentication authentication) {
|
||||
if (authentication != null) {
|
||||
model.addAttribute("username", authentication.getName());
|
||||
}
|
||||
|
||||
long totalAdherents = adherentRepository.count();
|
||||
long licencesBrouillon = licenceRepository.countByEtat("Brouillon");
|
||||
long licencesValidees = licenceRepository.countByEtat("Validée");
|
||||
long licencesPayees = licenceRepository.countByEtat("Payée");
|
||||
|
||||
BigDecimal totalEncaisse = paiementRepository.sumMontant();
|
||||
if (totalEncaisse == null) totalEncaisse = BigDecimal.ZERO;
|
||||
|
||||
List<com.astalange.core.entity.Licence> allLicences = licenceRepository.findAll();
|
||||
BigDecimal totalAttendu = allLicences.stream()
|
||||
.map(lic -> {
|
||||
if (lic.getCategorie() == null) return BigDecimal.ZERO;
|
||||
return (lic.getAdherent() != null && lic.getAdherent().isResidentTalange())
|
||||
? lic.getCategorie().getTarifBase()
|
||||
: lic.getCategorie().getTarifExterieur();
|
||||
})
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal resteARecouvrer = totalAttendu.subtract(totalEncaisse);
|
||||
if (resteARecouvrer.compareTo(BigDecimal.ZERO) < 0) resteARecouvrer = BigDecimal.ZERO;
|
||||
|
||||
List<Adherent> derniersInscrits = adherentRepository.findTop5ByOrderByIdDesc();
|
||||
|
||||
model.addAttribute("totalAdherents", totalAdherents);
|
||||
model.addAttribute("licencesBrouillon", licencesBrouillon);
|
||||
model.addAttribute("licencesActives", licencesValidees + licencesPayees);
|
||||
model.addAttribute("totalEncaisse", totalEncaisse);
|
||||
model.addAttribute("resteARecouvrer", resteARecouvrer);
|
||||
model.addAttribute("derniersInscrits", derniersInscrits);
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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.repository.AdherentRepository;
|
||||
import com.astalange.core.repository.CategorieRepository;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class LicenceController {
|
||||
|
||||
private final AdherentRepository adherentRepository;
|
||||
private final CategorieRepository categorieRepository;
|
||||
private final LicenceRepository licenceRepository;
|
||||
|
||||
public LicenceController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, LicenceRepository licenceRepository) {
|
||||
this.adherentRepository = adherentRepository;
|
||||
this.categorieRepository = categorieRepository;
|
||||
this.licenceRepository = licenceRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/adherents/{id}/licences")
|
||||
public String addLicence(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long categorieId,
|
||||
@RequestParam String saison,
|
||||
@RequestParam String etat) {
|
||||
|
||||
Adherent adherent = adherentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Adherent ID"));
|
||||
Categorie categorie = categorieRepository.findById(categorieId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Categorie ID"));
|
||||
|
||||
Licence licence = new Licence();
|
||||
licence.setAdherent(adherent);
|
||||
licence.setCategorie(categorie);
|
||||
licence.setSaison(saison);
|
||||
licence.setEtat(etat);
|
||||
|
||||
licenceRepository.save(licence);
|
||||
|
||||
return "redirect:/adherents/" + id + "/edit";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.ModePaiement;
|
||||
import com.astalange.core.repository.ModePaiementRepository;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/modespaiement")
|
||||
public class ModePaiementController {
|
||||
|
||||
private final ModePaiementRepository modePaiementRepository;
|
||||
|
||||
public ModePaiementController(ModePaiementRepository modePaiementRepository) {
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listModes(Model model) {
|
||||
model.addAttribute("modes", modePaiementRepository.findAll());
|
||||
return "parametrage/modespaiement_list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("modePaiement", new ModePaiement());
|
||||
return "parametrage/modespaiement_form";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
ModePaiement mode = modePaiementRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ID: " + id));
|
||||
model.addAttribute("modePaiement", mode);
|
||||
return "parametrage/modespaiement_form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String saveMode(@ModelAttribute("modePaiement") ModePaiement modePaiement) {
|
||||
modePaiementRepository.save(modePaiement);
|
||||
return "redirect:/modespaiement";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteMode(@PathVariable Long id) {
|
||||
ModePaiement mode = modePaiementRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ID: " + id));
|
||||
modePaiementRepository.delete(mode);
|
||||
return "redirect:/modespaiement";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.astalange.web.controller;
|
||||
|
||||
import com.astalange.core.entity.Licence;
|
||||
import com.astalange.core.entity.ModePaiement;
|
||||
import com.astalange.core.entity.Paiement;
|
||||
import com.astalange.core.repository.LicenceRepository;
|
||||
import com.astalange.core.repository.ModePaiementRepository;
|
||||
import com.astalange.core.repository.PaiementRepository;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Controller
|
||||
public class PaiementController {
|
||||
|
||||
private final LicenceRepository licenceRepository;
|
||||
private final ModePaiementRepository modePaiementRepository;
|
||||
private final PaiementRepository paiementRepository;
|
||||
|
||||
public PaiementController(LicenceRepository licenceRepository, ModePaiementRepository modePaiementRepository, PaiementRepository paiementRepository) {
|
||||
this.licenceRepository = licenceRepository;
|
||||
this.modePaiementRepository = modePaiementRepository;
|
||||
this.paiementRepository = paiementRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/licences/{id}/paiements")
|
||||
public String addPaiement(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long adherentId,
|
||||
@RequestParam BigDecimal montant,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate datePaiement,
|
||||
@RequestParam Long modePaiementId) {
|
||||
|
||||
Licence licence = licenceRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid Licence ID"));
|
||||
ModePaiement mode = modePaiementRepository.findById(modePaiementId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid ModePaiement ID"));
|
||||
|
||||
// Validation basique pour ne pas payer plus que le reste à payer
|
||||
if (montant.compareTo(licence.getResteAPayer()) > 0) {
|
||||
montant = licence.getResteAPayer();
|
||||
}
|
||||
|
||||
if (montant.compareTo(BigDecimal.ZERO) > 0) {
|
||||
Paiement paiement = new Paiement();
|
||||
paiement.setLicence(licence);
|
||||
paiement.setModePaiement(mode);
|
||||
paiement.setMontant(montant);
|
||||
paiement.setDatePaiement(datePaiement);
|
||||
paiementRepository.save(paiement);
|
||||
}
|
||||
|
||||
return "redirect:/adherents/" + adherentId + "/edit";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
application:
|
||||
name: as-talange
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5433/astalange
|
||||
username: myuser
|
||||
password: mypassword
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@@ -0,0 +1,328 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nouvel Adhérent - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></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; }
|
||||
.hidden-block { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</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>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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" th:text="${adherent.id == null ? 'Ajouter un adhérent' : 'Modifier l''adhérent'}">Ajouter un adhérent</h2>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||
|
||||
<form th:action="@{/adherents}" th:object="${adherent}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div th:if="${#fields.hasErrors('global')}" class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg text-sm mb-6">
|
||||
<p th:each="err : ${#fields.errors('global')}" th:text="${err}"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Nom -->
|
||||
<div>
|
||||
<label for="nom" class="block text-sm font-medium text-gray-700 mb-1">Nom <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="nom" th:field="*{nom}" 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"
|
||||
th:classappend="${#fields.hasErrors('nom')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Prénom -->
|
||||
<div>
|
||||
<label for="prenom" class="block text-sm font-medium text-gray-700 mb-1">Prénom <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="prenom" th:field="*{prenom}" 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"
|
||||
th:classappend="${#fields.hasErrors('prenom')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('prenom')}" th:errors="*{prenom}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Date de Naissance -->
|
||||
<div>
|
||||
<label for="dateNaissance" class="block text-sm font-medium text-gray-700 mb-1">Date de naissance <span class="text-red-500">*</span></label>
|
||||
<input type="date" id="dateNaissance" th:field="*{dateNaissance}" 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"
|
||||
th:classappend="${#fields.hasErrors('dateNaissance')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('dateNaissance')}" th:errors="*{dateNaissance}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Téléphone -->
|
||||
<div>
|
||||
<label for="telephone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
|
||||
<input type="tel" id="telephone" th:field="*{telephone}"
|
||||
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"
|
||||
th:classappend="${#fields.hasErrors('telephone')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('telephone')}" th:errors="*{telephone}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Représentant Légal (Dynamique) -->
|
||||
<div id="representantBlock" class="hidden-block bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||
<label for="representantLegal" class="block text-sm font-medium text-blue-800 mb-1">
|
||||
Représentant légal <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p class="text-xs text-blue-600 mb-2">Obligatoire car l'adhérent a moins de 18 ans.</p>
|
||||
<input type="text" id="representantLegal" th:field="*{representantLegal}"
|
||||
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"
|
||||
th:classappend="${#fields.hasErrors('representantLegal')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('representantLegal')}" th:errors="*{representantLegal}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<input type="email" id="email" th:field="*{email}"
|
||||
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"
|
||||
th:classappend="${#fields.hasErrors('email')} ? 'border-red-500' : ''">
|
||||
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<!-- Résident Talange -->
|
||||
<div class="flex items-center space-x-3 mt-7">
|
||||
<input type="checkbox" id="residentTalange" th:field="*{residentTalange}" class="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||
<label for="residentTalange" class="text-sm font-medium text-gray-700">
|
||||
Résident de la commune de Talange
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adresse -->
|
||||
<div>
|
||||
<label for="adresse" class="block text-sm font-medium text-gray-700 mb-1">Adresse postale</label>
|
||||
<textarea id="adresse" th:field="*{adresse}" rows="3"
|
||||
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"
|
||||
th:classappend="${#fields.hasErrors('adresse')} ? 'border-red-500' : ''"></textarea>
|
||||
<p th:if="${#fields.hasErrors('adresse')}" th:errors="*{adresse}" class="mt-1 text-xs text-red-600"></p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/adherents}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">Annuler</a>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Licences Section (Only visible for existing adherents) -->
|
||||
<div th:if="${adherent.id != null}" class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 mt-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Licences de l'adhérent</h3>
|
||||
<button onclick="openLicenceModal()" class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
|
||||
+ Ajouter une Licence
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-4 font-medium">Saison</th>
|
||||
<th class="py-3 px-4 font-medium">Catégorie</th>
|
||||
<th class="py-3 px-4 font-medium">N° Licence</th>
|
||||
<th class="py-3 px-4 font-medium">État</th>
|
||||
<th class="py-3 px-4 font-medium">Reste à Payer</th>
|
||||
<th class="py-3 px-4 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(licences)}">
|
||||
<td colspan="6" class="py-4 px-4 text-center text-gray-500">Aucune licence enregistrée.</td>
|
||||
</tr>
|
||||
<th:block th:each="lic : ${licences}">
|
||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||
<td class="py-4 px-4 font-medium text-gray-900" th:text="${lic.saison}">2024-2025</td>
|
||||
<td class="py-4 px-4 text-gray-600" th:text="${lic.categorie.nom}">U15</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>
|
||||
<td class="py-4 px-4">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
th:classappend="${lic.etat == 'Validée' ? 'bg-blue-100 text-blue-800' : (lic.etat == 'Payée' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800')}"
|
||||
th:text="${lic.etat}">Brouillon</span>
|
||||
</td>
|
||||
<td class="py-4 px-4 text-gray-600 font-medium" th:text="${lic.getResteAPayer() + ' €'}">100.00 €</td>
|
||||
<td class="py-4 px-4 text-right">
|
||||
<button th:if="${lic.getResteAPayer() > 0}" th:onclick="'openPaiementModal(' + ${lic.id} + ', ' + ${lic.getResteAPayer()} + ')'"
|
||||
class="text-green-600 hover:text-green-800 font-medium bg-green-50 px-3 py-1 rounded-lg">Payer</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${!#lists.isEmpty(lic.paiements)}" class="bg-gray-50/50">
|
||||
<td colspan="5" class="py-2 px-4">
|
||||
<div class="text-xs text-gray-500 mb-1 font-medium uppercase tracking-wider">Historique des paiements</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span th:each="paiement : ${lic.paiements}" class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-white border border-gray-200 text-gray-700">
|
||||
<span th:text="${paiement.montant + ' €'}">50 €</span>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span th:text="${paiement.modePaiement.nom}">Chèque</span>
|
||||
<span class="mx-1 text-gray-300">|</span>
|
||||
<span th:text="${#temporals.format(paiement.datePaiement, 'dd/MM/yyyy')}">01/01/2026</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</th:block>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Licence Modal -->
|
||||
<div id="licenceModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white">
|
||||
<div class="mt-3 text-center">
|
||||
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Nouvelle Licence</h3>
|
||||
<form th:if="${adherent.id != null}" th:action="@{/adherents/{id}/licences(id=${adherent.id})}" method="post" class="space-y-4 text-left">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
|
||||
<select 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 outline-none">
|
||||
<option value="">Sélectionnez une catégorie...</option>
|
||||
<option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.nom} + ' (' + ${cat.tarifBase} + ' €)'"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Numéro de licence</label>
|
||||
<input 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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Saison</label>
|
||||
<input type="text" name="saison" required value="2024-2025" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">État</label>
|
||||
<select name="etat" required 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="Brouillon">Brouillon</option>
|
||||
<option value="Validée">Validée</option>
|
||||
<option value="Payée">Payée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="items-center px-4 py-3 flex justify-end space-x-2">
|
||||
<button type="button" onclick="closeLicenceModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paiement Modal -->
|
||||
<div id="paiementModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-xl bg-white">
|
||||
<div class="mt-3 text-center">
|
||||
<h3 class="text-lg leading-6 font-semibold text-gray-900 mb-4">Enregistrer un Paiement</h3>
|
||||
<form id="paiementForm" method="post" class="space-y-4 text-left">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<input type="hidden" name="adherentId" th:value="${adherent.id}">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Montant (€)</label>
|
||||
<input type="number" step="0.01" id="paiementMontant" name="montant" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
|
||||
<input type="date" id="paiementDate" name="datePaiement" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Mode de paiement</label>
|
||||
<select name="modePaiementId" required 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="">Sélectionnez un mode...</option>
|
||||
<option th:each="mode : ${modesPaiement}" th:value="${mode.id}" th:text="${mode.nom}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="items-center px-4 py-3 flex justify-end space-x-2">
|
||||
<button type="button" onclick="closePaiementModal()" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</button>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700">Payer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openLicenceModal() {
|
||||
document.getElementById('licenceModal').classList.remove('hidden');
|
||||
}
|
||||
function closeLicenceModal() {
|
||||
document.getElementById('licenceModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function openPaiementModal(licenceId, maxAmount) {
|
||||
document.getElementById('paiementModal').classList.remove('hidden');
|
||||
document.getElementById('paiementForm').action = '/licences/' + licenceId + '/paiements';
|
||||
document.getElementById('paiementMontant').value = maxAmount;
|
||||
document.getElementById('paiementMontant').max = maxAmount;
|
||||
document.getElementById('paiementDate').valueAsDate = new Date();
|
||||
}
|
||||
function closePaiementModal() {
|
||||
document.getElementById('paiementModal').classList.add('hidden');
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const dateInput = document.getElementById('dateNaissance');
|
||||
const repBlock = document.getElementById('representantBlock');
|
||||
|
||||
function calculateAge(birthday) {
|
||||
const ageDifMs = Date.now() - birthday.getTime();
|
||||
const ageDate = new Date(ageDifMs);
|
||||
return Math.abs(ageDate.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
function updateRepresentantBlock() {
|
||||
if (dateInput.value) {
|
||||
const dob = new Date(dateInput.value);
|
||||
const age = calculateAge(dob);
|
||||
if (age < 18) {
|
||||
repBlock.classList.remove('hidden-block');
|
||||
} else {
|
||||
repBlock.classList.add('hidden-block');
|
||||
}
|
||||
} else {
|
||||
repBlock.classList.add('hidden-block');
|
||||
}
|
||||
}
|
||||
|
||||
dateInput.addEventListener('change', updateRepresentantBlock);
|
||||
|
||||
// Initial check in case it's a validation error reload
|
||||
updateRepresentantBlock();
|
||||
// But if there's already an error on representantLegal, ensure it is shown
|
||||
const hasRepError = document.querySelector('p[th\\:errors="*{representantLegal}"]');
|
||||
if (hasRepError || document.getElementById('representantLegal').classList.contains('border-red-500')) {
|
||||
repBlock.classList.remove('hidden-block');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Adhérents - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</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>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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 Adhérents</h2>
|
||||
<button class="md:hidden text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Adhérents</h3>
|
||||
<a th:href="@{/adherents/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors inline-block">
|
||||
+ Nouvel Adhérent
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<input type="text" placeholder="Rechercher un adhérent..." class="border border-gray-300 rounded-lg px-4 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-6 font-medium text-left">Nom</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Prénom</th>
|
||||
<th class="py-3 px-6 font-medium text-center">N° Licence</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Email</th>
|
||||
<th class="py-3 px-6 font-medium text-left">Téléphone</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Paiement</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(adherents)}">
|
||||
<td colspan="7" class="py-8 text-center text-gray-500">Aucun adhérent enregistré.</td>
|
||||
</tr>
|
||||
<tr th:each="adherent : ${adherents}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.nom}">Dupont</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-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>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${adherent.telephone}">0600000000</td>
|
||||
<td class="py-4 px-6 text-center">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
||||
th:classappend="${adherent.getStatutPaiement() == 'À jour' ? 'bg-green-100 text-green-800' : (adherent.getStatutPaiement() == 'Aucune licence' ? 'bg-gray-100 text-gray-800' : 'bg-red-100 text-red-800')}"
|
||||
th:text="${adherent.getStatutPaiement()}">À jour</span>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/adherents/{id}/edit(id=${adherent.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Voir / Modifier</a>
|
||||
<form th:action="@{/adherents/{id}/delete(id=${adherent.id})}" method="post" onsubmit="return confirm('Supprimer cet adhérent ?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tableau de bord - AS Talange</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Adhérents</a>
|
||||
<a href="#" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Paiements</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="text-sm font-medium text-gray-900 mb-1" th:text="${username}">Admin</div>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-700 font-medium">Déconnexion</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 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 px-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Tableau de bord</h2>
|
||||
</header>
|
||||
|
||||
<!-- Main section -->
|
||||
<div class="flex-1 overflow-auto p-6 bg-gray-50/50">
|
||||
|
||||
<!-- KPIs -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Adhérents -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-blue-50 text-blue-600 rounded-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Total Adhérents</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${totalAdherents}">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Licences Actives -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-green-50 text-green-600 rounded-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Licences Actives</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${licencesActives}">0</p>
|
||||
<p class="text-xs text-gray-400 mt-1" th:text="${licencesBrouillon} + ' en brouillon'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Encaissé -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-purple-50 text-purple-600 rounded-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Total Encaissé</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${totalEncaisse} + ' €'">0 €</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reste à Recouvrer -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex items-center space-x-4">
|
||||
<div class="p-3 bg-red-50 text-red-600 rounded-lg">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Reste à Recouvrer</p>
|
||||
<p class="text-2xl font-bold text-gray-900" th:text="${resteARecouvrer} + ' €'">0 €</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Derniers Inscrits -->
|
||||
<div class="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">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Derniers adhérents inscrits</h3>
|
||||
<a href="/adherents" class="text-sm font-medium text-blue-600 hover:text-blue-800">Voir tout →</a>
|
||||
</div>
|
||||
<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 & Prénom</th>
|
||||
<th class="py-3 px-6 font-medium">Date de naissance</th>
|
||||
<th class="py-3 px-6 font-medium">Téléphone</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(derniersInscrits)}">
|
||||
<td colspan="4" class="py-4 px-6 text-center text-gray-500">Aucun adhérent récent.</td>
|
||||
</tr>
|
||||
<tr th:each="adh : ${derniersInscrits}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6">
|
||||
<div class="font-medium text-gray-900" th:text="${adh.nom + ' ' + adh.prenom}">Dupont Jean</div>
|
||||
<div class="text-xs text-gray-500" th:text="${adh.email}">jean@example.com</div>
|
||||
</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${#temporals.format(adh.dateNaissance, 'dd/MM/yyyy')}">01/01/2000</td>
|
||||
<td class="py-4 px-6 text-gray-600" th:text="${adh.telephone}">0600000000</td>
|
||||
<td class="py-4 px-6 text-right">
|
||||
<a th:href="@{/adherents/{id}/edit(id=${adh.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Voir profil</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Connexion - 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">AS Talange</h1>
|
||||
<p class="text-gray-500">Outil de gestion administrative</p>
|
||||
</div>
|
||||
|
||||
<div th:if="${param.error}" class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 text-sm">
|
||||
Identifiant ou mot de passe incorrect.
|
||||
</div>
|
||||
|
||||
<div th:if="${param.logout}" class="bg-green-50 text-green-600 p-4 rounded-lg mb-6 text-sm">
|
||||
Vous avez été déconnecté avec succès.
|
||||
</div>
|
||||
|
||||
<form th:action="@{/login}" 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" 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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" 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">
|
||||
Se connecter
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Catégorie - 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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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" th:text="${categorie.id == null ? 'Nouvelle Catégorie' : 'Modifier Catégorie'}">Catégorie</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||
<form th:action="@{/categories}" th:object="${categorie}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: U15)</label>
|
||||
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Âge Min</label>
|
||||
<input type="number" th:field="*{anneeMin}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Âge Max</label>
|
||||
<input type="number" th:field="*{anneeMax}" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="tarifBase" class="block text-sm font-medium text-gray-700 mb-1">Tarif de base (Talangeois) (€) <span class="text-red-500">*</span></label>
|
||||
<input type="number" step="0.01" id="tarifBase" th:field="*{tarifBase}" 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="tarifExterieur" class="block text-sm font-medium text-gray-700 mb-1">Tarif Extérieur (hors commune de Talange) (€) <span class="text-red-500">*</span></label>
|
||||
<input type="number" step="0.01" id="tarifExterieur" th:field="*{tarifExterieur}" 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>
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/categories}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Catégories</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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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">Catégories Sportives</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Catégories</h3>
|
||||
<a th:href="@{/categories/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouvelle Catégorie
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-6 font-medium text-left">Nom de la catégorie</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Année Min</th>
|
||||
<th class="py-3 px-6 font-medium text-center">Année Max</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Tarif (Talangeois)</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Tarif (Hors Commune)</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(categories)}">
|
||||
<td colspan="6" class="py-8 text-center text-gray-500">Aucune catégorie n'est paramétrée.</td>
|
||||
</tr>
|
||||
<tr th:each="cat : ${categories}" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${cat.nom}">U15</td>
|
||||
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMin}">2010</td>
|
||||
<td class="py-4 px-6 text-center text-gray-600" th:text="${cat.anneeMax}">2011</td>
|
||||
<td class="py-4 px-6 text-right font-medium text-blue-600" th:text="${cat.tarifBase + ' €'}">100.00 €</td>
|
||||
<td class="py-4 px-6 text-right font-medium text-purple-600" th:text="${cat.tarifExterieur != null ? cat.tarifExterieur + ' €' : '-'}">130.00 €</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/categories/{id}/edit(id=${cat.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Modifier</a>
|
||||
<form th:action="@{/categories/{id}/delete(id=${cat.id})}" method="post" onsubmit="return confirm('Supprimer cette catégorie ?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mode de Paiement - 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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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" th:text="${modePaiement.id == null ? 'Nouveau Mode' : 'Modifier Mode'}">Mode de Paiement</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="max-w-xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||
<form th:action="@{/modespaiement}" th:object="${modePaiement}" method="post" class="space-y-6">
|
||||
<input type="hidden" th:field="*{id}" />
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom (ex: Chèque)</label>
|
||||
<input type="text" th:field="*{nom}" required class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex justify-end space-x-3 border-t border-gray-100">
|
||||
<a th:href="@{/modespaiement}" class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50">Annuler</a>
|
||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Paramétrage - Modes de Paiement</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 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col hidden md:flex">
|
||||
<div class="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<h1 class="text-xl font-bold text-blue-600">AS Talange</h1>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Tableau de bord</a>
|
||||
<a href="/adherents" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Adhérents</a>
|
||||
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Paramétrage</div>
|
||||
<a href="/categories" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors">Catégories</a>
|
||||
<a href="/modespaiement" class="block px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Modes de Paiement</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<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">Modes de Paiement</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Liste des Modes de Paiement</h3>
|
||||
<a th:href="@{/modespaiement/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700">
|
||||
+ Nouveau Mode
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
||||
<th class="py-3 px-6 font-medium">ID</th>
|
||||
<th class="py-3 px-6 font-medium">Nom</th>
|
||||
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 text-sm">
|
||||
<tr th:if="${#lists.isEmpty(modes)}">
|
||||
<td colspan="3" class="py-4 px-6 text-center text-gray-500">Aucun mode de paiement trouvé.</td>
|
||||
</tr>
|
||||
<tr th:each="mode : ${modes}" class="hover:bg-gray-50">
|
||||
<td class="py-4 px-6 text-gray-500" th:text="${mode.id}">1</td>
|
||||
<td class="py-4 px-6 font-medium text-gray-900" th:text="${mode.nom}">Chèque</td>
|
||||
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
||||
<a th:href="@{/modespaiement/{id}/edit(id=${mode.id})}" class="text-blue-600 hover:text-blue-800 font-medium">Éditer</a>
|
||||
<form th:action="@{/modespaiement/{id}/delete(id=${mode.id})}" method="post" onsubmit="return confirm('Supprimer ce mode de paiement ?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user