feat: add weekly training schedule planning with Gantt chart and conflict detection

This commit is contained in:
2026-05-30 00:13:55 +02:00
parent a429e67133
commit 0baca4a334
12 changed files with 933 additions and 0 deletions
@@ -0,0 +1,205 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalTime;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "creneau_entrainement")
public class CreneauEntrainement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "terrain_id", nullable = false)
private Terrain terrain;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ZoneTerrain zone;
@Enumerated(EnumType.STRING)
@Column(name = "jour_semaine", nullable = false)
private JourSemaine jourSemaine;
@Column(name = "heure_debut", nullable = false)
private LocalTime heureDebut;
@Column(name = "heure_fin", nullable = false)
private LocalTime heureFin;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@Transient
private boolean enConflit = false;
@Transient
private Set<String> categoriesEnConflit = new HashSet<>();
// Logic for Gantt diagram grid placement
public int getStartColumn() {
if (heureDebut == null) return 3;
// Clip to working hours 08:00 to 22:00
LocalTime time = heureDebut;
if (time.isBefore(LocalTime.of(8, 0))) {
time = LocalTime.of(8, 0);
}
long minutes = Duration.between(LocalTime.of(8, 0), time).toMinutes();
return (int) (minutes / 15) + 3; // +3 because col 1 is Field name, col 2 is Zone name
}
public int getEndColumn() {
if (heureFin == null) return 3;
LocalTime time = heureFin;
if (time.isAfter(LocalTime.of(22, 0))) {
time = LocalTime.of(22, 0);
}
long minutes = Duration.between(LocalTime.of(8, 0), time).toMinutes();
return (int) (minutes / 15) + 3;
}
public int getStartRow(int fieldIndex) {
int baseRow = 3 + (fieldIndex * 5); // Row index matching HTML layout (base starts at 3, 5 rows per field)
if (zone == null) return baseRow;
return switch (zone) {
case COMPLET, DEMI_A, QUART_1 -> baseRow;
case QUART_2 -> baseRow + 1;
case DEMI_B, QUART_3 -> baseRow + 2;
case QUART_4 -> baseRow + 3;
};
}
public int getEndRow(int fieldIndex) {
int baseRow = 3 + (fieldIndex * 5);
if (zone == null) return baseRow + 4;
return switch (zone) {
case QUART_1 -> baseRow + 1;
case DEMI_A, QUART_2 -> baseRow + 2;
case QUART_3 -> baseRow + 3;
case DEMI_B, QUART_4, COMPLET -> baseRow + 4;
};
}
// Overlap and conflict checking logic
public boolean overlapsWith(CreneauEntrainement other) {
if (this.id != null && this.id.equals(other.id)) {
return false; // same slot
}
if (this.jourSemaine != other.jourSemaine) {
return false;
}
if (this.terrain == null || other.terrain == null || !Objects.equals(this.terrain.getId(), other.terrain.getId())) {
return false;
}
// Time overlap check: start1 < end2 and start2 < end1
boolean timeOverlap = this.heureDebut.isBefore(other.heureFin) && other.heureDebut.isBefore(this.heureFin);
if (!timeOverlap) {
return false;
}
// Spatial overlap check
if (this.zone == null || other.zone == null) {
return false;
}
Set<Integer> q1 = this.zone.getQuarters();
Set<Integer> q2 = other.zone.getQuarters();
return !Collections.disjoint(q1, q2);
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Categorie getCategorie() {
return categorie;
}
public void setCategorie(Categorie categorie) {
this.categorie = categorie;
}
public Terrain getTerrain() {
return terrain;
}
public void setTerrain(Terrain terrain) {
this.terrain = terrain;
}
public ZoneTerrain getZone() {
return zone;
}
public void setZone(ZoneTerrain zone) {
this.zone = zone;
}
public JourSemaine getJourSemaine() {
return jourSemaine;
}
public void setJourSemaine(JourSemaine jourSemaine) {
this.jourSemaine = jourSemaine;
}
public LocalTime getHeureDebut() {
return heureDebut;
}
public void setHeureDebut(LocalTime heureDebut) {
this.heureDebut = heureDebut;
}
public LocalTime getHeureFin() {
return heureFin;
}
public void setHeureFin(LocalTime heureFin) {
this.heureFin = heureFin;
}
public Saison getSaison() {
return saison;
}
public void setSaison(Saison saison) {
this.saison = saison;
}
public boolean isEnConflit() {
return enConflit;
}
public void setEnConflit(boolean enConflit) {
this.enConflit = enConflit;
}
public Set<String> getCategoriesEnConflit() {
return categoriesEnConflit;
}
public void setCategoriesEnConflit(Set<String> categoriesEnConflit) {
this.categoriesEnConflit = categoriesEnConflit;
}
}
@@ -0,0 +1,21 @@
package com.astalange.core.entity;
public enum JourSemaine {
LUNDI("Lundi"),
MARDI("Mardi"),
MERCREDI("Mercredi"),
JEUDI("Jeudi"),
VENDREDI("Vendredi"),
SAMEDI("Samedi"),
DIMANCHE("Dimanche");
private final String libelle;
JourSemaine(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
@@ -0,0 +1,31 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "terrain")
public class Terrain {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nom;
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;
}
}
@@ -0,0 +1,29 @@
package com.astalange.core.entity;
import java.util.Set;
public enum ZoneTerrain {
COMPLET("Terrain Complet", Set.of(1, 2, 3, 4)),
DEMI_A("Demi-terrain A", Set.of(1, 2)),
DEMI_B("Demi-terrain B", Set.of(3, 4)),
QUART_1("Quart 1", Set.of(1)),
QUART_2("Quart 2", Set.of(2)),
QUART_3("Quart 3", Set.of(3)),
QUART_4("Quart 4", Set.of(4));
private final String libelle;
private final Set<Integer> quarters;
ZoneTerrain(String libelle, Set<Integer> quarters) {
this.libelle = libelle;
this.quarters = quarters;
}
public String getLibelle() {
return libelle;
}
public Set<Integer> getQuarters() {
return quarters;
}
}
@@ -0,0 +1,21 @@
package com.astalange.core.repository;
import com.astalange.core.entity.CreneauEntrainement;
import com.astalange.core.entity.JourSemaine;
import com.astalange.core.entity.Saison;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CreneauEntrainementRepository extends JpaRepository<CreneauEntrainement, Long> {
@Query("SELECT c FROM CreneauEntrainement c JOIN FETCH c.categorie JOIN FETCH c.terrain WHERE c.saison = :saison")
List<CreneauEntrainement> findBySaisonFetch(@Param("saison") Saison saison);
@Query("SELECT c FROM CreneauEntrainement c JOIN FETCH c.categorie JOIN FETCH c.terrain WHERE c.saison = :saison AND c.jourSemaine = :jourSemaine")
List<CreneauEntrainement> findBySaisonAndJourSemaineFetch(@Param("saison") Saison saison, @Param("jourSemaine") JourSemaine jourSemaine);
}
@@ -0,0 +1,9 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Terrain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TerrainRepository extends JpaRepository<Terrain, Long> {
}
@@ -0,0 +1,26 @@
-- V10__add_terrains_and_schedules.sql
-- 1. Table des terrains
CREATE TABLE terrain (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL
);
-- Insertion des deux terrains par défaut
INSERT INTO terrain (nom) VALUES ('Terrain Vert');
INSERT INTO terrain (nom) VALUES ('Terrain Synthétique');
-- 2. Table des créneaux d'entraînement
CREATE TABLE creneau_entrainement (
id BIGSERIAL PRIMARY KEY,
categorie_id BIGINT NOT NULL,
terrain_id BIGINT NOT NULL,
zone VARCHAR(50) NOT NULL,
jour_semaine VARCHAR(50) NOT NULL,
heure_debut TIME NOT NULL,
heure_fin TIME NOT NULL,
saison_id BIGINT NOT NULL,
CONSTRAINT fk_creneau_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
CONSTRAINT fk_creneau_terrain FOREIGN KEY (terrain_id) REFERENCES terrain(id),
CONSTRAINT fk_creneau_saison FOREIGN KEY (saison_id) REFERENCES saison(id) ON DELETE CASCADE
);