Task 1.2 - Entités JPA fondamentales
- Création de BaseEntity avec audit trail et soft delete - Création de l'entité User avec Quarkus Security JPA - Création de l'entité Client avec informations d'entreprise - Création de l'entité Coach avec informations professionnelles - Relations JPA one-to-one entre User-Client et User-Coach - Migrations Flyway V1, V2, V3 pour les tables - Données de test dans import.sql - Compilation réussie du module d'implémentation
This commit is contained in:
@@ -1,180 +0,0 @@
|
||||
package com.gbcm.server.entities;
|
||||
|
||||
import com.gbcm.server.api.enums.UserRole;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
||||
import io.quarkus.security.jpa.Password;
|
||||
import io.quarkus.security.jpa.Roles;
|
||||
import io.quarkus.security.jpa.UserDefinition;
|
||||
import io.quarkus.security.jpa.Username;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Entité utilisateur pour l'authentification et la gestion des profils
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_user_email", columnList = "email", unique = true),
|
||||
@Index(name = "idx_user_role", columnList = "role"),
|
||||
@Index(name = "idx_user_active", columnList = "active")
|
||||
})
|
||||
@UserDefinition
|
||||
public class User extends PanacheEntityBase {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
public Long id;
|
||||
|
||||
@Column(name = "first_name", nullable = false, length = 100)
|
||||
@NotBlank(message = "Le prénom est obligatoire")
|
||||
public String firstName;
|
||||
|
||||
@Column(name = "last_name", nullable = false, length = 100)
|
||||
@NotBlank(message = "Le nom est obligatoire")
|
||||
public String lastName;
|
||||
|
||||
@Column(name = "email", nullable = false, unique = true, length = 255)
|
||||
@NotBlank(message = "L'email est obligatoire")
|
||||
@Email(message = "Format d'email invalide")
|
||||
@Username
|
||||
public String email;
|
||||
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
@NotBlank(message = "Le mot de passe est obligatoire")
|
||||
@Password
|
||||
public String passwordHash;
|
||||
|
||||
@Column(name = "phone", length = 20)
|
||||
public String phone;
|
||||
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@NotNull(message = "Le rôle est obligatoire")
|
||||
@Roles
|
||||
public UserRole role;
|
||||
|
||||
@Column(name = "active", nullable = false)
|
||||
public boolean active = true;
|
||||
|
||||
@Column(name = "email_verified", nullable = false)
|
||||
public boolean emailVerified = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
public LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
public LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "last_login_at")
|
||||
public LocalDateTime lastLoginAt;
|
||||
|
||||
@Column(name = "password_reset_token")
|
||||
public String passwordResetToken;
|
||||
|
||||
@Column(name = "password_reset_expires_at")
|
||||
public LocalDateTime passwordResetExpiresAt;
|
||||
|
||||
@Column(name = "email_verification_token")
|
||||
public String emailVerificationToken;
|
||||
|
||||
// Constructeurs
|
||||
public User() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public User(String firstName, String lastName, String email, String passwordHash, UserRole role) {
|
||||
this();
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.passwordHash = passwordHash;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// Méthodes utilitaires
|
||||
public String getFullName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
public String getInitials() {
|
||||
String firstInitial = firstName != null && !firstName.isEmpty() ?
|
||||
firstName.substring(0, 1).toUpperCase() : "";
|
||||
String lastInitial = lastName != null && !lastName.isEmpty() ?
|
||||
lastName.substring(0, 1).toUpperCase() : "";
|
||||
return firstInitial + lastInitial;
|
||||
}
|
||||
|
||||
public boolean isPasswordResetTokenValid() {
|
||||
return passwordResetToken != null &&
|
||||
passwordResetExpiresAt != null &&
|
||||
passwordResetExpiresAt.isAfter(LocalDateTime.now());
|
||||
}
|
||||
|
||||
public void updateLastLogin() {
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Callbacks JPA
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Méthodes de recherche Panache
|
||||
public static User findByEmail(String email) {
|
||||
return find("email", email).firstResult();
|
||||
}
|
||||
|
||||
public static User findByEmailAndActive(String email, boolean active) {
|
||||
return find("email = ?1 and active = ?2", email, active).firstResult();
|
||||
}
|
||||
|
||||
public static User findByPasswordResetToken(String token) {
|
||||
return find("passwordResetToken", token).firstResult();
|
||||
}
|
||||
|
||||
public static User findByEmailVerificationToken(String token) {
|
||||
return find("emailVerificationToken", token).firstResult();
|
||||
}
|
||||
|
||||
public static long countByRole(UserRole role) {
|
||||
return count("role", role);
|
||||
}
|
||||
|
||||
public static long countActiveUsers() {
|
||||
return count("active", true);
|
||||
}
|
||||
|
||||
// equals et hashCode
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
User user = (User) o;
|
||||
return Objects.equals(id, user.id) && Objects.equals(email, user.email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", role=" + role +
|
||||
", active=" + active +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
271
src/main/java/com/gbcm/server/impl/entity/BaseEntity.java
Normal file
271
src/main/java/com/gbcm/server/impl/entity/BaseEntity.java
Normal file
@@ -0,0 +1,271 @@
|
||||
package com.gbcm.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité de base pour toutes les entités GBCM.
|
||||
* Fournit l'audit trail (createdAt, updatedAt, createdBy, updatedBy) et le soft delete.
|
||||
*
|
||||
* @author GBCM Development Team
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
@MappedSuperclass
|
||||
public abstract class BaseEntity extends PanacheEntityBase {
|
||||
|
||||
/**
|
||||
* Date et heure de création de l'enregistrement.
|
||||
* Automatiquement définie lors de la persistance.
|
||||
*/
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Date et heure de dernière mise à jour de l'enregistrement.
|
||||
* Automatiquement mise à jour lors des modifications.
|
||||
*/
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a créé l'enregistrement.
|
||||
* Doit être défini manuellement lors de la création.
|
||||
*/
|
||||
@Column(name = "created_by", length = 100)
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a effectué la dernière mise à jour.
|
||||
* Doit être défini manuellement lors des modifications.
|
||||
*/
|
||||
@Column(name = "updated_by", length = 100)
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* Indicateur de suppression logique (soft delete).
|
||||
* true = supprimé, false = actif.
|
||||
*/
|
||||
@Column(name = "deleted", nullable = false)
|
||||
private boolean deleted = false;
|
||||
|
||||
/**
|
||||
* Date et heure de suppression logique.
|
||||
* Définie lors du soft delete.
|
||||
*/
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a effectué la suppression logique.
|
||||
*/
|
||||
@Column(name = "deleted_by", length = 100)
|
||||
private String deletedBy;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
*/
|
||||
protected BaseEntity() {}
|
||||
|
||||
/**
|
||||
* Méthode appelée avant la persistance pour définir les champs d'audit.
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (updatedAt == null) {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode appelée avant la mise à jour pour mettre à jour les champs d'audit.
|
||||
*/
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectue une suppression logique de l'entité.
|
||||
*
|
||||
* @param deletedBy l'identifiant de l'utilisateur effectuant la suppression
|
||||
*/
|
||||
public void softDelete(String deletedBy) {
|
||||
this.deleted = true;
|
||||
this.deletedAt = LocalDateTime.now();
|
||||
this.deletedBy = deletedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaure une entité supprimée logiquement.
|
||||
*/
|
||||
public void restore() {
|
||||
this.deleted = false;
|
||||
this.deletedAt = null;
|
||||
this.deletedBy = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'entité est supprimée logiquement.
|
||||
*
|
||||
* @return true si l'entité est supprimée, false sinon
|
||||
*/
|
||||
public boolean isDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'entité est active (non supprimée).
|
||||
*
|
||||
* @return true si l'entité est active, false sinon
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return !deleted;
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
|
||||
/**
|
||||
* Retourne la date de création.
|
||||
*
|
||||
* @return la date de création
|
||||
*/
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la date de création.
|
||||
*
|
||||
* @param createdAt la date de création
|
||||
*/
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la date de dernière mise à jour.
|
||||
*
|
||||
* @return la date de dernière mise à jour
|
||||
*/
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la date de dernière mise à jour.
|
||||
*
|
||||
* @param updatedAt la date de dernière mise à jour
|
||||
*/
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'identifiant de l'utilisateur créateur.
|
||||
*
|
||||
* @return l'identifiant du créateur
|
||||
*/
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit l'identifiant de l'utilisateur créateur.
|
||||
*
|
||||
* @param createdBy l'identifiant du créateur
|
||||
*/
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'identifiant de l'utilisateur ayant effectué la dernière mise à jour.
|
||||
*
|
||||
* @return l'identifiant du modificateur
|
||||
*/
|
||||
public String getUpdatedBy() {
|
||||
return updatedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit l'identifiant de l'utilisateur ayant effectué la dernière mise à jour.
|
||||
*
|
||||
* @param updatedBy l'identifiant du modificateur
|
||||
*/
|
||||
public void setUpdatedBy(String updatedBy) {
|
||||
this.updatedBy = updatedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le statut de suppression logique.
|
||||
*
|
||||
* @param deleted true pour marquer comme supprimé, false sinon
|
||||
*/
|
||||
public void setDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la date de suppression logique.
|
||||
*
|
||||
* @return la date de suppression
|
||||
*/
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la date de suppression logique.
|
||||
*
|
||||
* @param deletedAt la date de suppression
|
||||
*/
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'identifiant de l'utilisateur ayant effectué la suppression.
|
||||
*
|
||||
* @return l'identifiant du suppresseur
|
||||
*/
|
||||
public String getDeletedBy() {
|
||||
return deletedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit l'identifiant de l'utilisateur ayant effectué la suppression.
|
||||
*
|
||||
* @param deletedBy l'identifiant du suppresseur
|
||||
*/
|
||||
public void setDeletedBy(String deletedBy) {
|
||||
this.deletedBy = deletedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Représentation textuelle de l'entité de base.
|
||||
*
|
||||
* @return une chaîne représentant l'entité
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BaseEntity{" +
|
||||
"createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
", createdBy='" + createdBy + '\'' +
|
||||
", updatedBy='" + updatedBy + '\'' +
|
||||
", deleted=" + deleted +
|
||||
", deletedAt=" + deletedAt +
|
||||
", deletedBy='" + deletedBy + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
531
src/main/java/com/gbcm/server/impl/entity/Client.java
Normal file
531
src/main/java/com/gbcm/server/impl/entity/Client.java
Normal file
@@ -0,0 +1,531 @@
|
||||
package com.gbcm.server.impl.entity;
|
||||
|
||||
import com.gbcm.server.api.enums.ServiceType;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Entité représentant un client de la plateforme GBCM.
|
||||
* Un client est associé à un utilisateur et peut avoir plusieurs services.
|
||||
*
|
||||
* @author GBCM Development Team
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "clients", indexes = {
|
||||
@Index(name = "idx_clients_user_id", columnList = "user_id", unique = true),
|
||||
@Index(name = "idx_clients_company", columnList = "company_name"),
|
||||
@Index(name = "idx_clients_status", columnList = "status"),
|
||||
@Index(name = "idx_clients_deleted", columnList = "deleted")
|
||||
})
|
||||
@NamedQueries({
|
||||
@NamedQuery(name = "Client.findByUserId",
|
||||
query = "SELECT c FROM Client c WHERE c.user.id = :userId AND c.deleted = false"),
|
||||
@NamedQuery(name = "Client.findByStatus",
|
||||
query = "SELECT c FROM Client c WHERE c.status = :status AND c.deleted = false"),
|
||||
@NamedQuery(name = "Client.findByCompanyName",
|
||||
query = "SELECT c FROM Client c WHERE LOWER(c.companyName) LIKE LOWER(:companyName) AND c.deleted = false"),
|
||||
@NamedQuery(name = "Client.findActiveClients",
|
||||
query = "SELECT c FROM Client c WHERE c.status = 'ACTIVE' AND c.deleted = false")
|
||||
})
|
||||
public class Client extends BaseEntity {
|
||||
|
||||
/**
|
||||
* Identifiant unique du client.
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* Utilisateur associé à ce client.
|
||||
* Relation one-to-one obligatoire.
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id", nullable = false, unique = true,
|
||||
foreignKey = @ForeignKey(name = "fk_clients_user_id"))
|
||||
@NotNull(message = "L'utilisateur associé est obligatoire")
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* Nom de l'entreprise du client.
|
||||
*/
|
||||
@Column(name = "company_name", nullable = false, length = 200)
|
||||
@NotBlank(message = "Le nom de l'entreprise est obligatoire")
|
||||
@Size(max = 200, message = "Le nom de l'entreprise ne peut pas dépasser 200 caractères")
|
||||
private String companyName;
|
||||
|
||||
/**
|
||||
* Secteur d'activité de l'entreprise.
|
||||
*/
|
||||
@Column(name = "industry", length = 100)
|
||||
@Size(max = 100, message = "Le secteur d'activité ne peut pas dépasser 100 caractères")
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* Taille de l'entreprise (nombre d'employés).
|
||||
*/
|
||||
@Column(name = "company_size")
|
||||
private Integer companySize;
|
||||
|
||||
/**
|
||||
* Chiffre d'affaires annuel de l'entreprise.
|
||||
*/
|
||||
@Column(name = "annual_revenue", precision = 15, scale = 2)
|
||||
private BigDecimal annualRevenue;
|
||||
|
||||
/**
|
||||
* Adresse de l'entreprise - ligne 1.
|
||||
*/
|
||||
@Column(name = "address_line1", length = 255)
|
||||
@Size(max = 255, message = "L'adresse ligne 1 ne peut pas dépasser 255 caractères")
|
||||
private String addressLine1;
|
||||
|
||||
/**
|
||||
* Adresse de l'entreprise - ligne 2.
|
||||
*/
|
||||
@Column(name = "address_line2", length = 255)
|
||||
@Size(max = 255, message = "L'adresse ligne 2 ne peut pas dépasser 255 caractères")
|
||||
private String addressLine2;
|
||||
|
||||
/**
|
||||
* Ville de l'entreprise.
|
||||
*/
|
||||
@Column(name = "city", length = 100)
|
||||
@Size(max = 100, message = "La ville ne peut pas dépasser 100 caractères")
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* État/Province de l'entreprise.
|
||||
*/
|
||||
@Column(name = "state", length = 100)
|
||||
@Size(max = 100, message = "L'état ne peut pas dépasser 100 caractères")
|
||||
private String state;
|
||||
|
||||
/**
|
||||
* Code postal de l'entreprise.
|
||||
*/
|
||||
@Column(name = "postal_code", length = 20)
|
||||
@Size(max = 20, message = "Le code postal ne peut pas dépasser 20 caractères")
|
||||
private String postalCode;
|
||||
|
||||
/**
|
||||
* Pays de l'entreprise.
|
||||
*/
|
||||
@Column(name = "country", length = 100)
|
||||
@Size(max = 100, message = "Le pays ne peut pas dépasser 100 caractères")
|
||||
private String country;
|
||||
|
||||
/**
|
||||
* Site web de l'entreprise.
|
||||
*/
|
||||
@Column(name = "website", length = 255)
|
||||
@Size(max = 255, message = "Le site web ne peut pas dépasser 255 caractères")
|
||||
private String website;
|
||||
|
||||
/**
|
||||
* Statut du client.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
@NotNull(message = "Le statut est obligatoire")
|
||||
private ClientStatus status = ClientStatus.PROSPECT;
|
||||
|
||||
/**
|
||||
* Date de conversion de prospect à client.
|
||||
*/
|
||||
@Column(name = "converted_at")
|
||||
private LocalDateTime convertedAt;
|
||||
|
||||
/**
|
||||
* Type de service principal du client.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "primary_service_type", length = 30)
|
||||
private ServiceType primaryServiceType;
|
||||
|
||||
/**
|
||||
* Valeur totale des contrats du client.
|
||||
*/
|
||||
@Column(name = "total_contract_value", precision = 15, scale = 2)
|
||||
private BigDecimal totalContractValue = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* Date de début de la relation client.
|
||||
*/
|
||||
@Column(name = "relationship_start_date")
|
||||
private LocalDate relationshipStartDate;
|
||||
|
||||
/**
|
||||
* Notes internes sur le client.
|
||||
*/
|
||||
@Column(name = "notes", columnDefinition = "TEXT")
|
||||
private String notes;
|
||||
|
||||
/**
|
||||
* Source d'acquisition du client.
|
||||
*/
|
||||
@Column(name = "acquisition_source", length = 100)
|
||||
@Size(max = 100, message = "La source d'acquisition ne peut pas dépasser 100 caractères")
|
||||
private String acquisitionSource;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
*/
|
||||
public Client() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur avec les champs obligatoires.
|
||||
*
|
||||
* @param user l'utilisateur associé
|
||||
* @param companyName le nom de l'entreprise
|
||||
*/
|
||||
public Client(User user, String companyName) {
|
||||
this();
|
||||
this.user = user;
|
||||
this.companyName = companyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un prospect en client.
|
||||
*/
|
||||
public void convertToClient() {
|
||||
if (this.status == ClientStatus.PROSPECT) {
|
||||
this.status = ClientStatus.ACTIVE;
|
||||
this.convertedAt = LocalDateTime.now();
|
||||
this.relationshipStartDate = LocalDate.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Désactive le client.
|
||||
*/
|
||||
public void deactivate() {
|
||||
this.status = ClientStatus.INACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Réactive le client.
|
||||
*/
|
||||
public void reactivate() {
|
||||
this.status = ClientStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une valeur au contrat total.
|
||||
*
|
||||
* @param amount le montant à ajouter
|
||||
*/
|
||||
public void addContractValue(BigDecimal amount) {
|
||||
if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
this.totalContractValue = this.totalContractValue.add(amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'adresse complète formatée.
|
||||
*
|
||||
* @return l'adresse complète
|
||||
*/
|
||||
public String getFullAddress() {
|
||||
StringBuilder address = new StringBuilder();
|
||||
|
||||
if (addressLine1 != null && !addressLine1.trim().isEmpty()) {
|
||||
address.append(addressLine1);
|
||||
}
|
||||
|
||||
if (addressLine2 != null && !addressLine2.trim().isEmpty()) {
|
||||
if (address.length() > 0) address.append(", ");
|
||||
address.append(addressLine2);
|
||||
}
|
||||
|
||||
if (city != null && !city.trim().isEmpty()) {
|
||||
if (address.length() > 0) address.append(", ");
|
||||
address.append(city);
|
||||
}
|
||||
|
||||
if (state != null && !state.trim().isEmpty()) {
|
||||
if (address.length() > 0) address.append(", ");
|
||||
address.append(state);
|
||||
}
|
||||
|
||||
if (postalCode != null && !postalCode.trim().isEmpty()) {
|
||||
if (address.length() > 0) address.append(" ");
|
||||
address.append(postalCode);
|
||||
}
|
||||
|
||||
if (country != null && !country.trim().isEmpty()) {
|
||||
if (address.length() > 0) address.append(", ");
|
||||
address.append(country);
|
||||
}
|
||||
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le client est actif.
|
||||
*
|
||||
* @return true si le client est actif, false sinon
|
||||
*/
|
||||
public boolean isActiveClient() {
|
||||
return status == ClientStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le client est un prospect.
|
||||
*
|
||||
* @return true si c'est un prospect, false sinon
|
||||
*/
|
||||
public boolean isProspect() {
|
||||
return status == ClientStatus.PROSPECT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par ID utilisateur.
|
||||
*
|
||||
* @param userId l'ID de l'utilisateur
|
||||
* @return le client trouvé ou null
|
||||
*/
|
||||
public static Client findByUserId(Long userId) {
|
||||
return find("#Client.findByUserId", userId).firstResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par statut.
|
||||
*
|
||||
* @param status le statut à rechercher
|
||||
* @return la liste des clients avec ce statut
|
||||
*/
|
||||
public static List<Client> findByStatus(ClientStatus status) {
|
||||
return find("#Client.findByStatus", status).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par nom d'entreprise.
|
||||
*
|
||||
* @param companyName le nom d'entreprise à rechercher
|
||||
* @return la liste des clients correspondants
|
||||
*/
|
||||
public static List<Client> findByCompanyName(String companyName) {
|
||||
String searchPattern = "%" + companyName + "%";
|
||||
return find("#Client.findByCompanyName", searchPattern).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche des clients actifs.
|
||||
*
|
||||
* @return la liste des clients actifs
|
||||
*/
|
||||
public static List<Client> findActiveClients() {
|
||||
return find("#Client.findActiveClients").list();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getCompanyName() {
|
||||
return companyName;
|
||||
}
|
||||
|
||||
public void setCompanyName(String companyName) {
|
||||
this.companyName = companyName;
|
||||
}
|
||||
|
||||
public String getIndustry() {
|
||||
return industry;
|
||||
}
|
||||
|
||||
public void setIndustry(String industry) {
|
||||
this.industry = industry;
|
||||
}
|
||||
|
||||
public Integer getCompanySize() {
|
||||
return companySize;
|
||||
}
|
||||
|
||||
public void setCompanySize(Integer companySize) {
|
||||
this.companySize = companySize;
|
||||
}
|
||||
|
||||
public BigDecimal getAnnualRevenue() {
|
||||
return annualRevenue;
|
||||
}
|
||||
|
||||
public void setAnnualRevenue(BigDecimal annualRevenue) {
|
||||
this.annualRevenue = annualRevenue;
|
||||
}
|
||||
|
||||
public String getAddressLine1() {
|
||||
return addressLine1;
|
||||
}
|
||||
|
||||
public void setAddressLine1(String addressLine1) {
|
||||
this.addressLine1 = addressLine1;
|
||||
}
|
||||
|
||||
public String getAddressLine2() {
|
||||
return addressLine2;
|
||||
}
|
||||
|
||||
public void setAddressLine2(String addressLine2) {
|
||||
this.addressLine2 = addressLine2;
|
||||
}
|
||||
|
||||
public String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
public void setCity(String city) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getPostalCode() {
|
||||
return postalCode;
|
||||
}
|
||||
|
||||
public void setPostalCode(String postalCode) {
|
||||
this.postalCode = postalCode;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public String getWebsite() {
|
||||
return website;
|
||||
}
|
||||
|
||||
public void setWebsite(String website) {
|
||||
this.website = website;
|
||||
}
|
||||
|
||||
public ClientStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ClientStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getConvertedAt() {
|
||||
return convertedAt;
|
||||
}
|
||||
|
||||
public void setConvertedAt(LocalDateTime convertedAt) {
|
||||
this.convertedAt = convertedAt;
|
||||
}
|
||||
|
||||
public ServiceType getPrimaryServiceType() {
|
||||
return primaryServiceType;
|
||||
}
|
||||
|
||||
public void setPrimaryServiceType(ServiceType primaryServiceType) {
|
||||
this.primaryServiceType = primaryServiceType;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalContractValue() {
|
||||
return totalContractValue;
|
||||
}
|
||||
|
||||
public void setTotalContractValue(BigDecimal totalContractValue) {
|
||||
this.totalContractValue = totalContractValue;
|
||||
}
|
||||
|
||||
public LocalDate getRelationshipStartDate() {
|
||||
return relationshipStartDate;
|
||||
}
|
||||
|
||||
public void setRelationshipStartDate(LocalDate relationshipStartDate) {
|
||||
this.relationshipStartDate = relationshipStartDate;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public String getAcquisitionSource() {
|
||||
return acquisitionSource;
|
||||
}
|
||||
|
||||
public void setAcquisitionSource(String acquisitionSource) {
|
||||
this.acquisitionSource = acquisitionSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Client{" +
|
||||
"id=" + id +
|
||||
", companyName='" + companyName + '\'' +
|
||||
", industry='" + industry + '\'' +
|
||||
", status=" + status +
|
||||
", totalContractValue=" + totalContractValue +
|
||||
", relationshipStartDate=" + relationshipStartDate +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Énumération des statuts de client.
|
||||
*/
|
||||
public enum ClientStatus {
|
||||
/**
|
||||
* Prospect - pas encore client.
|
||||
*/
|
||||
PROSPECT,
|
||||
|
||||
/**
|
||||
* Client actif.
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* Client inactif.
|
||||
*/
|
||||
INACTIVE,
|
||||
|
||||
/**
|
||||
* Ancien client.
|
||||
*/
|
||||
FORMER
|
||||
}
|
||||
}
|
||||
550
src/main/java/com/gbcm/server/impl/entity/Coach.java
Normal file
550
src/main/java/com/gbcm/server/impl/entity/Coach.java
Normal file
@@ -0,0 +1,550 @@
|
||||
package com.gbcm.server.impl.entity;
|
||||
|
||||
import com.gbcm.server.api.enums.ServiceType;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Entité représentant un coach de la plateforme GBCM.
|
||||
* Un coach est associé à un utilisateur et peut offrir différents services.
|
||||
*
|
||||
* @author GBCM Development Team
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "coaches", indexes = {
|
||||
@Index(name = "idx_coaches_user_id", columnList = "user_id", unique = true),
|
||||
@Index(name = "idx_coaches_status", columnList = "status"),
|
||||
@Index(name = "idx_coaches_specialization", columnList = "specialization"),
|
||||
@Index(name = "idx_coaches_deleted", columnList = "deleted")
|
||||
})
|
||||
@NamedQueries({
|
||||
@NamedQuery(name = "Coach.findByUserId",
|
||||
query = "SELECT c FROM Coach c WHERE c.user.id = :userId AND c.deleted = false"),
|
||||
@NamedQuery(name = "Coach.findByStatus",
|
||||
query = "SELECT c FROM Coach c WHERE c.status = :status AND c.deleted = false"),
|
||||
@NamedQuery(name = "Coach.findBySpecialization",
|
||||
query = "SELECT c FROM Coach c WHERE c.specialization = :specialization AND c.deleted = false"),
|
||||
@NamedQuery(name = "Coach.findAvailableCoaches",
|
||||
query = "SELECT c FROM Coach c WHERE c.status = 'ACTIVE' AND c.availableForBooking = true AND c.deleted = false")
|
||||
})
|
||||
public class Coach extends BaseEntity {
|
||||
|
||||
/**
|
||||
* Identifiant unique du coach.
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* Utilisateur associé à ce coach.
|
||||
* Relation one-to-one obligatoire.
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id", nullable = false, unique = true,
|
||||
foreignKey = @ForeignKey(name = "fk_coaches_user_id"))
|
||||
@NotNull(message = "L'utilisateur associé est obligatoire")
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* Spécialisation principale du coach.
|
||||
*/
|
||||
@Column(name = "specialization", nullable = false, length = 100)
|
||||
@NotBlank(message = "La spécialisation est obligatoire")
|
||||
@Size(max = 100, message = "La spécialisation ne peut pas dépasser 100 caractères")
|
||||
private String specialization;
|
||||
|
||||
/**
|
||||
* Biographie professionnelle du coach.
|
||||
*/
|
||||
@Column(name = "bio", columnDefinition = "TEXT")
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* Années d'expérience du coach.
|
||||
*/
|
||||
@Column(name = "years_of_experience")
|
||||
private Integer yearsOfExperience;
|
||||
|
||||
/**
|
||||
* Certifications du coach.
|
||||
*/
|
||||
@Column(name = "certifications", columnDefinition = "TEXT")
|
||||
private String certifications;
|
||||
|
||||
/**
|
||||
* Langues parlées par le coach.
|
||||
*/
|
||||
@Column(name = "languages", length = 255)
|
||||
@Size(max = 255, message = "Les langues ne peuvent pas dépasser 255 caractères")
|
||||
private String languages;
|
||||
|
||||
/**
|
||||
* Tarif horaire du coach pour les sessions individuelles.
|
||||
*/
|
||||
@Column(name = "hourly_rate", precision = 10, scale = 2)
|
||||
private BigDecimal hourlyRate;
|
||||
|
||||
/**
|
||||
* Statut du coach.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
@NotNull(message = "Le statut est obligatoire")
|
||||
private CoachStatus status = CoachStatus.ACTIVE;
|
||||
|
||||
/**
|
||||
* Disponibilité pour les réservations.
|
||||
*/
|
||||
@Column(name = "available_for_booking", nullable = false)
|
||||
private boolean availableForBooking = true;
|
||||
|
||||
/**
|
||||
* Types de services offerts par le coach.
|
||||
*/
|
||||
@ElementCollection(targetClass = ServiceType.class)
|
||||
@CollectionTable(name = "coach_service_types",
|
||||
joinColumns = @JoinColumn(name = "coach_id"),
|
||||
foreignKey = @ForeignKey(name = "fk_coach_service_types_coach_id"))
|
||||
@Column(name = "service_type", length = 30)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Set<ServiceType> serviceTypes;
|
||||
|
||||
/**
|
||||
* Heures de travail - début.
|
||||
*/
|
||||
@Column(name = "working_hours_start")
|
||||
private LocalTime workingHoursStart;
|
||||
|
||||
/**
|
||||
* Heures de travail - fin.
|
||||
*/
|
||||
@Column(name = "working_hours_end")
|
||||
private LocalTime workingHoursEnd;
|
||||
|
||||
/**
|
||||
* Jours de travail (format: MONDAY,TUESDAY,WEDNESDAY...).
|
||||
*/
|
||||
@Column(name = "working_days", length = 100)
|
||||
@Size(max = 100, message = "Les jours de travail ne peuvent pas dépasser 100 caractères")
|
||||
private String workingDays;
|
||||
|
||||
/**
|
||||
* Fuseau horaire du coach.
|
||||
*/
|
||||
@Column(name = "timezone", length = 50)
|
||||
@Size(max = 50, message = "Le fuseau horaire ne peut pas dépasser 50 caractères")
|
||||
private String timezone;
|
||||
|
||||
/**
|
||||
* Date de début d'activité comme coach.
|
||||
*/
|
||||
@Column(name = "start_date")
|
||||
private LocalDate startDate;
|
||||
|
||||
/**
|
||||
* Date de fin d'activité comme coach.
|
||||
*/
|
||||
@Column(name = "end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
/**
|
||||
* Note moyenne du coach (calculée à partir des évaluations).
|
||||
*/
|
||||
@Column(name = "average_rating", precision = 3, scale = 2)
|
||||
private BigDecimal averageRating;
|
||||
|
||||
/**
|
||||
* Nombre total d'évaluations reçues.
|
||||
*/
|
||||
@Column(name = "total_ratings", nullable = false)
|
||||
private int totalRatings = 0;
|
||||
|
||||
/**
|
||||
* Nombre total de sessions effectuées.
|
||||
*/
|
||||
@Column(name = "total_sessions", nullable = false)
|
||||
private int totalSessions = 0;
|
||||
|
||||
/**
|
||||
* Revenus totaux générés.
|
||||
*/
|
||||
@Column(name = "total_revenue", precision = 15, scale = 2)
|
||||
private BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* Notes internes sur le coach.
|
||||
*/
|
||||
@Column(name = "notes", columnDefinition = "TEXT")
|
||||
private String notes;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
*/
|
||||
public Coach() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur avec les champs obligatoires.
|
||||
*
|
||||
* @param user l'utilisateur associé
|
||||
* @param specialization la spécialisation du coach
|
||||
*/
|
||||
public Coach(User user, String specialization) {
|
||||
this();
|
||||
this.user = user;
|
||||
this.specialization = specialization;
|
||||
this.startDate = LocalDate.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Active le coach.
|
||||
*/
|
||||
public void activate() {
|
||||
this.status = CoachStatus.ACTIVE;
|
||||
this.availableForBooking = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Désactive le coach.
|
||||
*/
|
||||
public void deactivate() {
|
||||
this.status = CoachStatus.INACTIVE;
|
||||
this.availableForBooking = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met le coach en congé.
|
||||
*/
|
||||
public void setOnLeave() {
|
||||
this.status = CoachStatus.ON_LEAVE;
|
||||
this.availableForBooking = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Termine l'activité du coach.
|
||||
*/
|
||||
public void terminate() {
|
||||
this.status = CoachStatus.TERMINATED;
|
||||
this.availableForBooking = false;
|
||||
this.endDate = LocalDate.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la note moyenne après une nouvelle évaluation.
|
||||
*
|
||||
* @param newRating la nouvelle note reçue
|
||||
*/
|
||||
public void updateRating(BigDecimal newRating) {
|
||||
if (newRating != null && newRating.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (averageRating == null) {
|
||||
averageRating = newRating;
|
||||
totalRatings = 1;
|
||||
} else {
|
||||
BigDecimal totalScore = averageRating.multiply(new BigDecimal(totalRatings));
|
||||
totalScore = totalScore.add(newRating);
|
||||
totalRatings++;
|
||||
averageRating = totalScore.divide(new BigDecimal(totalRatings), 2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrémente le nombre de sessions effectuées.
|
||||
*/
|
||||
public void incrementSessionCount() {
|
||||
this.totalSessions++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute des revenus au total.
|
||||
*
|
||||
* @param amount le montant à ajouter
|
||||
*/
|
||||
public void addRevenue(BigDecimal amount) {
|
||||
if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
this.totalRevenue = this.totalRevenue.add(amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le coach est disponible pour les réservations.
|
||||
*
|
||||
* @return true si disponible, false sinon
|
||||
*/
|
||||
public boolean isAvailableForBooking() {
|
||||
return availableForBooking && status == CoachStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le coach offre un type de service spécifique.
|
||||
*
|
||||
* @param serviceType le type de service à vérifier
|
||||
* @return true si le coach offre ce service, false sinon
|
||||
*/
|
||||
public boolean offersService(ServiceType serviceType) {
|
||||
return serviceTypes != null && serviceTypes.contains(serviceType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par ID utilisateur.
|
||||
*
|
||||
* @param userId l'ID de l'utilisateur
|
||||
* @return le coach trouvé ou null
|
||||
*/
|
||||
public static Coach findByUserId(Long userId) {
|
||||
return find("#Coach.findByUserId", userId).firstResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par statut.
|
||||
*
|
||||
* @param status le statut à rechercher
|
||||
* @return la liste des coaches avec ce statut
|
||||
*/
|
||||
public static List<Coach> findByStatus(CoachStatus status) {
|
||||
return find("#Coach.findByStatus", status).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par spécialisation.
|
||||
*
|
||||
* @param specialization la spécialisation à rechercher
|
||||
* @return la liste des coaches avec cette spécialisation
|
||||
*/
|
||||
public static List<Coach> findBySpecialization(String specialization) {
|
||||
return find("#Coach.findBySpecialization", specialization).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche des coaches disponibles.
|
||||
*
|
||||
* @return la liste des coaches disponibles pour réservation
|
||||
*/
|
||||
public static List<Coach> findAvailableCoaches() {
|
||||
return find("#Coach.findAvailableCoaches").list();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getSpecialization() {
|
||||
return specialization;
|
||||
}
|
||||
|
||||
public void setSpecialization(String specialization) {
|
||||
this.specialization = specialization;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public Integer getYearsOfExperience() {
|
||||
return yearsOfExperience;
|
||||
}
|
||||
|
||||
public void setYearsOfExperience(Integer yearsOfExperience) {
|
||||
this.yearsOfExperience = yearsOfExperience;
|
||||
}
|
||||
|
||||
public String getCertifications() {
|
||||
return certifications;
|
||||
}
|
||||
|
||||
public void setCertifications(String certifications) {
|
||||
this.certifications = certifications;
|
||||
}
|
||||
|
||||
public String getLanguages() {
|
||||
return languages;
|
||||
}
|
||||
|
||||
public void setLanguages(String languages) {
|
||||
this.languages = languages;
|
||||
}
|
||||
|
||||
public BigDecimal getHourlyRate() {
|
||||
return hourlyRate;
|
||||
}
|
||||
|
||||
public void setHourlyRate(BigDecimal hourlyRate) {
|
||||
this.hourlyRate = hourlyRate;
|
||||
}
|
||||
|
||||
public CoachStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(CoachStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public void setAvailableForBooking(boolean availableForBooking) {
|
||||
this.availableForBooking = availableForBooking;
|
||||
}
|
||||
|
||||
public Set<ServiceType> getServiceTypes() {
|
||||
return serviceTypes;
|
||||
}
|
||||
|
||||
public void setServiceTypes(Set<ServiceType> serviceTypes) {
|
||||
this.serviceTypes = serviceTypes;
|
||||
}
|
||||
|
||||
public LocalTime getWorkingHoursStart() {
|
||||
return workingHoursStart;
|
||||
}
|
||||
|
||||
public void setWorkingHoursStart(LocalTime workingHoursStart) {
|
||||
this.workingHoursStart = workingHoursStart;
|
||||
}
|
||||
|
||||
public LocalTime getWorkingHoursEnd() {
|
||||
return workingHoursEnd;
|
||||
}
|
||||
|
||||
public void setWorkingHoursEnd(LocalTime workingHoursEnd) {
|
||||
this.workingHoursEnd = workingHoursEnd;
|
||||
}
|
||||
|
||||
public String getWorkingDays() {
|
||||
return workingDays;
|
||||
}
|
||||
|
||||
public void setWorkingDays(String workingDays) {
|
||||
this.workingDays = workingDays;
|
||||
}
|
||||
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDate startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDate getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDate endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public BigDecimal getAverageRating() {
|
||||
return averageRating;
|
||||
}
|
||||
|
||||
public void setAverageRating(BigDecimal averageRating) {
|
||||
this.averageRating = averageRating;
|
||||
}
|
||||
|
||||
public int getTotalRatings() {
|
||||
return totalRatings;
|
||||
}
|
||||
|
||||
public void setTotalRatings(int totalRatings) {
|
||||
this.totalRatings = totalRatings;
|
||||
}
|
||||
|
||||
public int getTotalSessions() {
|
||||
return totalSessions;
|
||||
}
|
||||
|
||||
public void setTotalSessions(int totalSessions) {
|
||||
this.totalSessions = totalSessions;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalRevenue() {
|
||||
return totalRevenue;
|
||||
}
|
||||
|
||||
public void setTotalRevenue(BigDecimal totalRevenue) {
|
||||
this.totalRevenue = totalRevenue;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Coach{" +
|
||||
"id=" + id +
|
||||
", specialization='" + specialization + '\'' +
|
||||
", status=" + status +
|
||||
", availableForBooking=" + availableForBooking +
|
||||
", averageRating=" + averageRating +
|
||||
", totalSessions=" + totalSessions +
|
||||
", totalRevenue=" + totalRevenue +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Énumération des statuts de coach.
|
||||
*/
|
||||
public enum CoachStatus {
|
||||
/**
|
||||
* Coach actif et disponible.
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* Coach inactif temporairement.
|
||||
*/
|
||||
INACTIVE,
|
||||
|
||||
/**
|
||||
* Coach en congé.
|
||||
*/
|
||||
ON_LEAVE,
|
||||
|
||||
/**
|
||||
* Coach dont le contrat est terminé.
|
||||
*/
|
||||
TERMINATED
|
||||
}
|
||||
}
|
||||
432
src/main/java/com/gbcm/server/impl/entity/User.java
Normal file
432
src/main/java/com/gbcm/server/impl/entity/User.java
Normal file
@@ -0,0 +1,432 @@
|
||||
package com.gbcm.server.impl.entity;
|
||||
|
||||
import com.gbcm.server.api.enums.UserRole;
|
||||
import io.quarkus.security.jpa.Password;
|
||||
import io.quarkus.security.jpa.Roles;
|
||||
import io.quarkus.security.jpa.UserDefinition;
|
||||
import io.quarkus.security.jpa.Username;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Entité représentant un utilisateur de la plateforme GBCM.
|
||||
* Utilisée pour l'authentification et l'autorisation avec Quarkus Security.
|
||||
*
|
||||
* @author GBCM Development Team
|
||||
* @version 1.0
|
||||
* @since 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_users_email", columnList = "email", unique = true),
|
||||
@Index(name = "idx_users_role", columnList = "role"),
|
||||
@Index(name = "idx_users_active", columnList = "active"),
|
||||
@Index(name = "idx_users_deleted", columnList = "deleted")
|
||||
})
|
||||
@UserDefinition
|
||||
@NamedQueries({
|
||||
@NamedQuery(name = "User.findByEmail",
|
||||
query = "SELECT u FROM User u WHERE u.email = :email AND u.deleted = false"),
|
||||
@NamedQuery(name = "User.findActiveUsers",
|
||||
query = "SELECT u FROM User u WHERE u.active = true AND u.deleted = false"),
|
||||
@NamedQuery(name = "User.findByRole",
|
||||
query = "SELECT u FROM User u WHERE u.role = :role AND u.deleted = false"),
|
||||
@NamedQuery(name = "User.searchByNameOrEmail",
|
||||
query = "SELECT u FROM User u WHERE (LOWER(u.firstName) LIKE LOWER(:search) OR LOWER(u.lastName) LIKE LOWER(:search) OR LOWER(u.email) LIKE LOWER(:search)) AND u.deleted = false")
|
||||
})
|
||||
public class User extends BaseEntity {
|
||||
|
||||
/**
|
||||
* Identifiant unique de l'utilisateur.
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* Prénom de l'utilisateur.
|
||||
*/
|
||||
@Column(name = "first_name", nullable = false, length = 50)
|
||||
@NotBlank(message = "Le prénom est obligatoire")
|
||||
@Size(max = 50, message = "Le prénom ne peut pas dépasser 50 caractères")
|
||||
private String firstName;
|
||||
|
||||
/**
|
||||
* Nom de famille de l'utilisateur.
|
||||
*/
|
||||
@Column(name = "last_name", nullable = false, length = 50)
|
||||
@NotBlank(message = "Le nom est obligatoire")
|
||||
@Size(max = 50, message = "Le nom ne peut pas dépasser 50 caractères")
|
||||
private String lastName;
|
||||
|
||||
/**
|
||||
* Adresse email unique de l'utilisateur.
|
||||
* Utilisée comme nom d'utilisateur pour l'authentification.
|
||||
*/
|
||||
@Column(name = "email", nullable = false, unique = true, length = 255)
|
||||
@NotBlank(message = "L'email est obligatoire")
|
||||
@Email(message = "Format d'email invalide")
|
||||
@Size(max = 255, message = "L'email ne peut pas dépasser 255 caractères")
|
||||
@Username
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* Mot de passe haché de l'utilisateur.
|
||||
* Utilisé pour l'authentification avec Quarkus Security.
|
||||
*/
|
||||
@Column(name = "password_hash", nullable = false, length = 255)
|
||||
@NotBlank(message = "Le mot de passe est obligatoire")
|
||||
@Size(max = 255, message = "Le hash du mot de passe ne peut pas dépasser 255 caractères")
|
||||
@Password
|
||||
private String passwordHash;
|
||||
|
||||
/**
|
||||
* Numéro de téléphone de l'utilisateur.
|
||||
*/
|
||||
@Column(name = "phone", length = 20)
|
||||
@Size(max = 20, message = "Le téléphone ne peut pas dépasser 20 caractères")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* Rôle de l'utilisateur dans le système.
|
||||
* Détermine les permissions et l'accès aux fonctionnalités.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
@NotNull(message = "Le rôle est obligatoire")
|
||||
@Roles
|
||||
private UserRole role;
|
||||
|
||||
/**
|
||||
* Statut d'activation du compte utilisateur.
|
||||
* true = compte actif, false = compte désactivé.
|
||||
*/
|
||||
@Column(name = "active", nullable = false)
|
||||
private boolean active = true;
|
||||
|
||||
/**
|
||||
* Date de dernière connexion de l'utilisateur.
|
||||
*/
|
||||
@Column(name = "last_login_at")
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
/**
|
||||
* Adresse IP de la dernière connexion.
|
||||
*/
|
||||
@Column(name = "last_login_ip", length = 45)
|
||||
@Size(max = 45, message = "L'adresse IP ne peut pas dépasser 45 caractères")
|
||||
private String lastLoginIp;
|
||||
|
||||
/**
|
||||
* Nombre de tentatives de connexion échouées consécutives.
|
||||
*/
|
||||
@Column(name = "failed_login_attempts", nullable = false)
|
||||
private int failedLoginAttempts = 0;
|
||||
|
||||
/**
|
||||
* Date de verrouillage du compte (après trop de tentatives échouées).
|
||||
*/
|
||||
@Column(name = "locked_until")
|
||||
private LocalDateTime lockedUntil;
|
||||
|
||||
/**
|
||||
* Token de réinitialisation de mot de passe.
|
||||
*/
|
||||
@Column(name = "password_reset_token", length = 255)
|
||||
@Size(max = 255, message = "Le token de réinitialisation ne peut pas dépasser 255 caractères")
|
||||
private String passwordResetToken;
|
||||
|
||||
/**
|
||||
* Date d'expiration du token de réinitialisation.
|
||||
*/
|
||||
@Column(name = "password_reset_expires_at")
|
||||
private LocalDateTime passwordResetExpiresAt;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
*/
|
||||
public User() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur avec les champs obligatoires.
|
||||
*
|
||||
* @param firstName le prénom
|
||||
* @param lastName le nom de famille
|
||||
* @param email l'adresse email
|
||||
* @param passwordHash le mot de passe haché
|
||||
* @param role le rôle de l'utilisateur
|
||||
*/
|
||||
public User(String firstName, String lastName, String email, String passwordHash, UserRole role) {
|
||||
this();
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.passwordHash = passwordHash;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom complet de l'utilisateur.
|
||||
*
|
||||
* @return le nom complet (prénom + nom)
|
||||
*/
|
||||
public String getFullName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le compte est verrouillé.
|
||||
*
|
||||
* @return true si le compte est verrouillé, false sinon
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
return lockedUntil != null && LocalDateTime.now().isBefore(lockedUntil);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verrouille le compte pour une durée spécifiée.
|
||||
*
|
||||
* @param lockDurationMinutes durée de verrouillage en minutes
|
||||
*/
|
||||
public void lockAccount(int lockDurationMinutes) {
|
||||
this.lockedUntil = LocalDateTime.now().plusMinutes(lockDurationMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Déverrouille le compte et remet à zéro les tentatives échouées.
|
||||
*/
|
||||
public void unlockAccount() {
|
||||
this.lockedUntil = null;
|
||||
this.failedLoginAttempts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrémente le nombre de tentatives de connexion échouées.
|
||||
*/
|
||||
public void incrementFailedLoginAttempts() {
|
||||
this.failedLoginAttempts++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remet à zéro les tentatives de connexion échouées.
|
||||
*/
|
||||
public void resetFailedLoginAttempts() {
|
||||
this.failedLoginAttempts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les informations de dernière connexion.
|
||||
*
|
||||
* @param ipAddress l'adresse IP de connexion
|
||||
*/
|
||||
public void updateLastLogin(String ipAddress) {
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
this.lastLoginIp = ipAddress;
|
||||
resetFailedLoginAttempts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token de réinitialisation de mot de passe.
|
||||
*
|
||||
* @param token le token généré
|
||||
* @param expirationHours durée de validité en heures
|
||||
*/
|
||||
public void setPasswordResetToken(String token, int expirationHours) {
|
||||
this.passwordResetToken = token;
|
||||
this.passwordResetExpiresAt = LocalDateTime.now().plusHours(expirationHours);
|
||||
}
|
||||
|
||||
/**
|
||||
* Efface le token de réinitialisation de mot de passe.
|
||||
*/
|
||||
public void clearPasswordResetToken() {
|
||||
this.passwordResetToken = null;
|
||||
this.passwordResetExpiresAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le token de réinitialisation est valide.
|
||||
*
|
||||
* @param token le token à vérifier
|
||||
* @return true si le token est valide, false sinon
|
||||
*/
|
||||
public boolean isPasswordResetTokenValid(String token) {
|
||||
return passwordResetToken != null &&
|
||||
passwordResetToken.equals(token) &&
|
||||
passwordResetExpiresAt != null &&
|
||||
LocalDateTime.now().isBefore(passwordResetExpiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par email.
|
||||
*
|
||||
* @param email l'adresse email à rechercher
|
||||
* @return l'utilisateur trouvé ou null
|
||||
*/
|
||||
public static User findByEmail(String email) {
|
||||
return find("#User.findByEmail", email).firstResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche des utilisateurs actifs.
|
||||
*
|
||||
* @return la liste des utilisateurs actifs
|
||||
*/
|
||||
public static List<User> findActiveUsers() {
|
||||
return find("#User.findActiveUsers").list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par rôle.
|
||||
*
|
||||
* @param role le rôle à rechercher
|
||||
* @return la liste des utilisateurs avec ce rôle
|
||||
*/
|
||||
public static List<User> findByRole(UserRole role) {
|
||||
return find("#User.findByRole", role).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de recherche par nom ou email.
|
||||
*
|
||||
* @param search le terme de recherche
|
||||
* @return la liste des utilisateurs correspondants
|
||||
*/
|
||||
public static List<User> searchByNameOrEmail(String search) {
|
||||
String searchPattern = "%" + search + "%";
|
||||
return find("#User.searchByNameOrEmail", searchPattern).list();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public UserRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(UserRole role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLoginAt() {
|
||||
return lastLoginAt;
|
||||
}
|
||||
|
||||
public void setLastLoginAt(LocalDateTime lastLoginAt) {
|
||||
this.lastLoginAt = lastLoginAt;
|
||||
}
|
||||
|
||||
public String getLastLoginIp() {
|
||||
return lastLoginIp;
|
||||
}
|
||||
|
||||
public void setLastLoginIp(String lastLoginIp) {
|
||||
this.lastLoginIp = lastLoginIp;
|
||||
}
|
||||
|
||||
public int getFailedLoginAttempts() {
|
||||
return failedLoginAttempts;
|
||||
}
|
||||
|
||||
public void setFailedLoginAttempts(int failedLoginAttempts) {
|
||||
this.failedLoginAttempts = failedLoginAttempts;
|
||||
}
|
||||
|
||||
public LocalDateTime getLockedUntil() {
|
||||
return lockedUntil;
|
||||
}
|
||||
|
||||
public void setLockedUntil(LocalDateTime lockedUntil) {
|
||||
this.lockedUntil = lockedUntil;
|
||||
}
|
||||
|
||||
public String getPasswordResetToken() {
|
||||
return passwordResetToken;
|
||||
}
|
||||
|
||||
public LocalDateTime getPasswordResetExpiresAt() {
|
||||
return passwordResetExpiresAt;
|
||||
}
|
||||
|
||||
public void setPasswordResetExpiresAt(LocalDateTime passwordResetExpiresAt) {
|
||||
this.passwordResetExpiresAt = passwordResetExpiresAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", role=" + role +
|
||||
", active=" + active +
|
||||
", lastLoginAt=" + lastLoginAt +
|
||||
", failedLoginAttempts=" + failedLoginAttempts +
|
||||
", locked=" + isLocked() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package com.gbcm.server.services.impl;
|
||||
|
||||
import com.gbcm.server.api.dto.auth.LoginRequestDTO;
|
||||
import com.gbcm.server.api.dto.auth.LoginResponseDTO;
|
||||
import com.gbcm.server.api.dto.user.UserDTO;
|
||||
import com.gbcm.server.api.exceptions.AuthenticationException;
|
||||
import com.gbcm.server.api.exceptions.GBCMException;
|
||||
import com.gbcm.server.api.interfaces.AuthService;
|
||||
import com.gbcm.server.entities.User;
|
||||
import com.gbcm.server.services.security.JwtService;
|
||||
import com.gbcm.server.utils.EmailUtils;
|
||||
|
||||
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||
import io.quarkus.logging.Log;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implémentation du service d'authentification
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
@Inject
|
||||
JwtService jwtService;
|
||||
|
||||
@Inject
|
||||
EmailUtils emailUtils;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public LoginResponseDTO login(@Valid LoginRequestDTO loginRequest)
|
||||
throws AuthenticationException, GBCMException {
|
||||
|
||||
Log.infof("Tentative de connexion pour: %s", loginRequest.getEmail());
|
||||
|
||||
try {
|
||||
// Recherche de l'utilisateur
|
||||
User user = User.findByEmailAndActive(loginRequest.getEmail(), true);
|
||||
if (user == null) {
|
||||
Log.warnf("Utilisateur non trouvé ou inactif: %s", loginRequest.getEmail());
|
||||
throw new AuthenticationException("Identifiants invalides");
|
||||
}
|
||||
|
||||
// Vérification du mot de passe
|
||||
if (!BcryptUtil.matches(loginRequest.getPassword(), user.passwordHash)) {
|
||||
Log.warnf("Mot de passe incorrect pour: %s", loginRequest.getEmail());
|
||||
throw new AuthenticationException("Identifiants invalides");
|
||||
}
|
||||
|
||||
// Mise à jour de la dernière connexion
|
||||
user.updateLastLogin();
|
||||
user.persist();
|
||||
|
||||
// Génération du token JWT
|
||||
String token = jwtService.generateToken(user);
|
||||
LocalDateTime expiresAt = LocalDateTime.now().plusHours(1);
|
||||
|
||||
// Création de la réponse
|
||||
UserDTO userDTO = mapToUserDTO(user);
|
||||
LoginResponseDTO response = LoginResponseDTO.success(token, expiresAt, userDTO);
|
||||
|
||||
if (loginRequest.isRememberMe()) {
|
||||
String refreshToken = jwtService.generateRefreshToken(user);
|
||||
response.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
Log.infof("Connexion réussie pour: %s", loginRequest.getEmail());
|
||||
return response;
|
||||
|
||||
} catch (AuthenticationException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors de la connexion pour: %s", loginRequest.getEmail());
|
||||
throw new GBCMException("Erreur système lors de la connexion");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void logout(String authToken) throws AuthenticationException {
|
||||
try {
|
||||
// Extraction du token (suppression du préfixe "Bearer ")
|
||||
String token = extractToken(authToken);
|
||||
|
||||
// Validation et extraction des informations utilisateur
|
||||
UserDTO userDTO = jwtService.validateToken(token);
|
||||
|
||||
// Ajout du token à la blacklist
|
||||
jwtService.blacklistToken(token);
|
||||
|
||||
Log.infof("Déconnexion réussie pour l'utilisateur: %s", userDTO.getEmail());
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors de la déconnexion");
|
||||
throw new AuthenticationException("Erreur lors de la déconnexion");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginResponseDTO refreshToken(String refreshToken) throws AuthenticationException {
|
||||
try {
|
||||
// Validation du refresh token
|
||||
UserDTO userDTO = jwtService.validateRefreshToken(refreshToken);
|
||||
|
||||
// Recherche de l'utilisateur
|
||||
User user = User.findByEmail(userDTO.getEmail());
|
||||
if (user == null || !user.active) {
|
||||
throw new AuthenticationException("Utilisateur non trouvé ou inactif");
|
||||
}
|
||||
|
||||
// Génération d'un nouveau token
|
||||
String newToken = jwtService.generateToken(user);
|
||||
LocalDateTime expiresAt = LocalDateTime.now().plusHours(1);
|
||||
|
||||
return LoginResponseDTO.success(newToken, expiresAt, userDTO);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors du rafraîchissement du token");
|
||||
throw new AuthenticationException("Token de rafraîchissement invalide");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDTO validateToken(String authToken) throws AuthenticationException {
|
||||
try {
|
||||
String token = extractToken(authToken);
|
||||
return jwtService.validateToken(token);
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors de la validation du token");
|
||||
throw new AuthenticationException("Token invalide");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void forgotPassword(String email) throws GBCMException {
|
||||
try {
|
||||
User user = User.findByEmailAndActive(email, true);
|
||||
if (user == null) {
|
||||
// Pour des raisons de sécurité, on ne révèle pas si l'email existe
|
||||
Log.warnf("Demande de réinitialisation pour email inexistant: %s", email);
|
||||
return;
|
||||
}
|
||||
|
||||
// Génération du token de réinitialisation
|
||||
String resetToken = UUID.randomUUID().toString();
|
||||
user.passwordResetToken = resetToken;
|
||||
user.passwordResetExpiresAt = LocalDateTime.now().plusHours(24);
|
||||
user.persist();
|
||||
|
||||
// Envoi de l'email
|
||||
emailUtils.sendPasswordResetEmail(user.email, user.getFullName(), resetToken);
|
||||
|
||||
Log.infof("Email de réinitialisation envoyé à: %s", email);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors de l'envoi de l'email de réinitialisation");
|
||||
throw new GBCMException("Erreur lors de l'envoi de l'email");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void resetPassword(String resetToken, String newPassword) throws GBCMException {
|
||||
try {
|
||||
User user = User.findByPasswordResetToken(resetToken);
|
||||
if (user == null || !user.isPasswordResetTokenValid()) {
|
||||
throw new GBCMException("Token de réinitialisation invalide ou expiré");
|
||||
}
|
||||
|
||||
// Mise à jour du mot de passe
|
||||
user.passwordHash = BcryptUtil.bcryptHash(newPassword);
|
||||
user.passwordResetToken = null;
|
||||
user.passwordResetExpiresAt = null;
|
||||
user.updatedAt = LocalDateTime.now();
|
||||
user.persist();
|
||||
|
||||
Log.infof("Mot de passe réinitialisé pour: %s", user.email);
|
||||
|
||||
} catch (GBCMException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors de la réinitialisation du mot de passe");
|
||||
throw new GBCMException("Erreur lors de la réinitialisation");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void changePassword(String authToken, String oldPassword, String newPassword)
|
||||
throws AuthenticationException, GBCMException {
|
||||
try {
|
||||
// Validation du token et récupération de l'utilisateur
|
||||
UserDTO userDTO = validateToken(authToken);
|
||||
User user = User.findByEmail(userDTO.getEmail());
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationException("Utilisateur non trouvé");
|
||||
}
|
||||
|
||||
// Vérification de l'ancien mot de passe
|
||||
if (!BcryptUtil.matches(oldPassword, user.passwordHash)) {
|
||||
throw new GBCMException("Ancien mot de passe incorrect");
|
||||
}
|
||||
|
||||
// Mise à jour du mot de passe
|
||||
user.passwordHash = BcryptUtil.bcryptHash(newPassword);
|
||||
user.updatedAt = LocalDateTime.now();
|
||||
user.persist();
|
||||
|
||||
Log.infof("Mot de passe changé pour: %s", user.email);
|
||||
|
||||
} catch (AuthenticationException | GBCMException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Erreur lors du changement de mot de passe");
|
||||
throw new GBCMException("Erreur lors du changement de mot de passe");
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes utilitaires privées
|
||||
private String extractToken(String authToken) throws AuthenticationException {
|
||||
if (authToken == null || !authToken.startsWith("Bearer ")) {
|
||||
throw new AuthenticationException("Format de token invalide");
|
||||
}
|
||||
return authToken.substring(7);
|
||||
}
|
||||
|
||||
private UserDTO mapToUserDTO(User user) {
|
||||
UserDTO dto = new UserDTO();
|
||||
dto.setId(user.id);
|
||||
dto.setFirstName(user.firstName);
|
||||
dto.setLastName(user.lastName);
|
||||
dto.setEmail(user.email);
|
||||
dto.setPhone(user.phone);
|
||||
dto.setRole(user.role);
|
||||
dto.setActive(user.active);
|
||||
dto.setCreatedAt(user.createdAt);
|
||||
dto.setUpdatedAt(user.updatedAt);
|
||||
dto.setLastLoginAt(user.lastLoginAt);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
86
src/main/resources/db/migration/V1__Create_users_table.sql
Normal file
86
src/main/resources/db/migration/V1__Create_users_table.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
-- Migration V1: Création de la table users
|
||||
-- Auteur: GBCM Development Team
|
||||
-- Date: 2024-01-25
|
||||
-- Description: Table principale des utilisateurs avec audit trail et soft delete
|
||||
|
||||
CREATE TABLE users (
|
||||
-- Identifiant unique
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Informations personnelles
|
||||
first_name VARCHAR(50) NOT NULL,
|
||||
last_name VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
|
||||
-- Authentification
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL CHECK (role IN ('ADMIN', 'MANAGER', 'COACH', 'CLIENT', 'PROSPECT')),
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Sécurité et connexion
|
||||
last_login_at TIMESTAMP,
|
||||
last_login_ip VARCHAR(45),
|
||||
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
locked_until TIMESTAMP,
|
||||
|
||||
-- Réinitialisation de mot de passe
|
||||
password_reset_token VARCHAR(255),
|
||||
password_reset_expires_at TIMESTAMP,
|
||||
|
||||
-- Audit trail
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100),
|
||||
updated_by VARCHAR(100),
|
||||
|
||||
-- Soft delete
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
deleted_at TIMESTAMP,
|
||||
deleted_by VARCHAR(100)
|
||||
);
|
||||
|
||||
-- Index pour les performances
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
CREATE INDEX idx_users_active ON users(active);
|
||||
CREATE INDEX idx_users_deleted ON users(deleted);
|
||||
CREATE INDEX idx_users_password_reset_token ON users(password_reset_token);
|
||||
|
||||
-- Trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Commentaires sur la table et les colonnes
|
||||
COMMENT ON TABLE users IS 'Table des utilisateurs de la plateforme GBCM';
|
||||
COMMENT ON COLUMN users.id IS 'Identifiant unique de l''utilisateur';
|
||||
COMMENT ON COLUMN users.first_name IS 'Prénom de l''utilisateur';
|
||||
COMMENT ON COLUMN users.last_name IS 'Nom de famille de l''utilisateur';
|
||||
COMMENT ON COLUMN users.email IS 'Adresse email unique utilisée pour l''authentification';
|
||||
COMMENT ON COLUMN users.phone IS 'Numéro de téléphone de l''utilisateur';
|
||||
COMMENT ON COLUMN users.password_hash IS 'Hash du mot de passe pour l''authentification';
|
||||
COMMENT ON COLUMN users.role IS 'Rôle de l''utilisateur dans le système';
|
||||
COMMENT ON COLUMN users.active IS 'Statut d''activation du compte';
|
||||
COMMENT ON COLUMN users.last_login_at IS 'Date et heure de la dernière connexion';
|
||||
COMMENT ON COLUMN users.last_login_ip IS 'Adresse IP de la dernière connexion';
|
||||
COMMENT ON COLUMN users.failed_login_attempts IS 'Nombre de tentatives de connexion échouées consécutives';
|
||||
COMMENT ON COLUMN users.locked_until IS 'Date jusqu''à laquelle le compte est verrouillé';
|
||||
COMMENT ON COLUMN users.password_reset_token IS 'Token pour la réinitialisation du mot de passe';
|
||||
COMMENT ON COLUMN users.password_reset_expires_at IS 'Date d''expiration du token de réinitialisation';
|
||||
COMMENT ON COLUMN users.created_at IS 'Date et heure de création de l''enregistrement';
|
||||
COMMENT ON COLUMN users.updated_at IS 'Date et heure de dernière mise à jour';
|
||||
COMMENT ON COLUMN users.created_by IS 'Utilisateur qui a créé l''enregistrement';
|
||||
COMMENT ON COLUMN users.updated_by IS 'Utilisateur qui a effectué la dernière mise à jour';
|
||||
COMMENT ON COLUMN users.deleted IS 'Indicateur de suppression logique';
|
||||
COMMENT ON COLUMN users.deleted_at IS 'Date et heure de suppression logique';
|
||||
COMMENT ON COLUMN users.deleted_by IS 'Utilisateur qui a effectué la suppression logique';
|
||||
98
src/main/resources/db/migration/V2__Create_clients_table.sql
Normal file
98
src/main/resources/db/migration/V2__Create_clients_table.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- Migration V2: Création de la table clients
|
||||
-- Auteur: GBCM Development Team
|
||||
-- Date: 2024-01-25
|
||||
-- Description: Table des clients avec informations d'entreprise et relation avec users
|
||||
|
||||
CREATE TABLE clients (
|
||||
-- Identifiant unique
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Relation avec l'utilisateur (one-to-one)
|
||||
user_id BIGINT NOT NULL UNIQUE,
|
||||
|
||||
-- Informations de l'entreprise
|
||||
company_name VARCHAR(200) NOT NULL,
|
||||
industry VARCHAR(100),
|
||||
company_size INTEGER,
|
||||
annual_revenue DECIMAL(15,2),
|
||||
|
||||
-- Adresse de l'entreprise
|
||||
address_line1 VARCHAR(255),
|
||||
address_line2 VARCHAR(255),
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
country VARCHAR(100),
|
||||
website VARCHAR(255),
|
||||
|
||||
-- Statut et informations client
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PROSPECT' CHECK (status IN ('PROSPECT', 'ACTIVE', 'INACTIVE', 'FORMER')),
|
||||
converted_at TIMESTAMP,
|
||||
primary_service_type VARCHAR(30) CHECK (primary_service_type IN ('strategic_workshop', 'one_on_one_coaching', 'on_demand_coaching', 'special_project')),
|
||||
|
||||
-- Informations financières
|
||||
total_contract_value DECIMAL(15,2) NOT NULL DEFAULT 0.00,
|
||||
relationship_start_date DATE,
|
||||
|
||||
-- Informations additionnelles
|
||||
notes TEXT,
|
||||
acquisition_source VARCHAR(100),
|
||||
|
||||
-- Audit trail
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100),
|
||||
updated_by VARCHAR(100),
|
||||
|
||||
-- Soft delete
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
deleted_at TIMESTAMP,
|
||||
deleted_by VARCHAR(100),
|
||||
|
||||
-- Contraintes
|
||||
CONSTRAINT fk_clients_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
-- Index pour les performances
|
||||
CREATE INDEX idx_clients_user_id ON clients(user_id);
|
||||
CREATE INDEX idx_clients_company ON clients(company_name);
|
||||
CREATE INDEX idx_clients_status ON clients(status);
|
||||
CREATE INDEX idx_clients_deleted ON clients(deleted);
|
||||
CREATE INDEX idx_clients_industry ON clients(industry);
|
||||
CREATE INDEX idx_clients_primary_service_type ON clients(primary_service_type);
|
||||
|
||||
-- Trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE TRIGGER update_clients_updated_at
|
||||
BEFORE UPDATE ON clients
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Commentaires sur la table et les colonnes
|
||||
COMMENT ON TABLE clients IS 'Table des clients de la plateforme GBCM';
|
||||
COMMENT ON COLUMN clients.id IS 'Identifiant unique du client';
|
||||
COMMENT ON COLUMN clients.user_id IS 'Référence vers l''utilisateur associé';
|
||||
COMMENT ON COLUMN clients.company_name IS 'Nom de l''entreprise du client';
|
||||
COMMENT ON COLUMN clients.industry IS 'Secteur d''activité de l''entreprise';
|
||||
COMMENT ON COLUMN clients.company_size IS 'Nombre d''employés de l''entreprise';
|
||||
COMMENT ON COLUMN clients.annual_revenue IS 'Chiffre d''affaires annuel de l''entreprise';
|
||||
COMMENT ON COLUMN clients.address_line1 IS 'Première ligne de l''adresse';
|
||||
COMMENT ON COLUMN clients.address_line2 IS 'Deuxième ligne de l''adresse';
|
||||
COMMENT ON COLUMN clients.city IS 'Ville de l''entreprise';
|
||||
COMMENT ON COLUMN clients.state IS 'État/Province de l''entreprise';
|
||||
COMMENT ON COLUMN clients.postal_code IS 'Code postal de l''entreprise';
|
||||
COMMENT ON COLUMN clients.country IS 'Pays de l''entreprise';
|
||||
COMMENT ON COLUMN clients.website IS 'Site web de l''entreprise';
|
||||
COMMENT ON COLUMN clients.status IS 'Statut du client (PROSPECT, ACTIVE, INACTIVE, FORMER)';
|
||||
COMMENT ON COLUMN clients.converted_at IS 'Date de conversion de prospect à client';
|
||||
COMMENT ON COLUMN clients.primary_service_type IS 'Type de service principal du client';
|
||||
COMMENT ON COLUMN clients.total_contract_value IS 'Valeur totale des contrats du client';
|
||||
COMMENT ON COLUMN clients.relationship_start_date IS 'Date de début de la relation client';
|
||||
COMMENT ON COLUMN clients.notes IS 'Notes internes sur le client';
|
||||
COMMENT ON COLUMN clients.acquisition_source IS 'Source d''acquisition du client';
|
||||
COMMENT ON COLUMN clients.created_at IS 'Date et heure de création de l''enregistrement';
|
||||
COMMENT ON COLUMN clients.updated_at IS 'Date et heure de dernière mise à jour';
|
||||
COMMENT ON COLUMN clients.created_by IS 'Utilisateur qui a créé l''enregistrement';
|
||||
COMMENT ON COLUMN clients.updated_by IS 'Utilisateur qui a effectué la dernière mise à jour';
|
||||
COMMENT ON COLUMN clients.deleted IS 'Indicateur de suppression logique';
|
||||
COMMENT ON COLUMN clients.deleted_at IS 'Date et heure de suppression logique';
|
||||
COMMENT ON COLUMN clients.deleted_by IS 'Utilisateur qui a effectué la suppression logique';
|
||||
121
src/main/resources/db/migration/V3__Create_coaches_table.sql
Normal file
121
src/main/resources/db/migration/V3__Create_coaches_table.sql
Normal file
@@ -0,0 +1,121 @@
|
||||
-- Migration V3: Création de la table coaches et coach_service_types
|
||||
-- Auteur: GBCM Development Team
|
||||
-- Date: 2024-01-25
|
||||
-- Description: Table des coaches avec informations professionnelles et types de services
|
||||
|
||||
CREATE TABLE coaches (
|
||||
-- Identifiant unique
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Relation avec l'utilisateur (one-to-one)
|
||||
user_id BIGINT NOT NULL UNIQUE,
|
||||
|
||||
-- Informations professionnelles
|
||||
specialization VARCHAR(100) NOT NULL,
|
||||
bio TEXT,
|
||||
years_of_experience INTEGER,
|
||||
certifications TEXT,
|
||||
languages VARCHAR(255),
|
||||
hourly_rate DECIMAL(10,2),
|
||||
|
||||
-- Statut et disponibilité
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'ON_LEAVE', 'TERMINATED')),
|
||||
available_for_booking BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Horaires de travail
|
||||
working_hours_start TIME,
|
||||
working_hours_end TIME,
|
||||
working_days VARCHAR(100),
|
||||
timezone VARCHAR(50),
|
||||
|
||||
-- Dates d'activité
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
|
||||
-- Statistiques
|
||||
average_rating DECIMAL(3,2),
|
||||
total_ratings INTEGER NOT NULL DEFAULT 0,
|
||||
total_sessions INTEGER NOT NULL DEFAULT 0,
|
||||
total_revenue DECIMAL(15,2) NOT NULL DEFAULT 0.00,
|
||||
|
||||
-- Notes internes
|
||||
notes TEXT,
|
||||
|
||||
-- Audit trail
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100),
|
||||
updated_by VARCHAR(100),
|
||||
|
||||
-- Soft delete
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
deleted_at TIMESTAMP,
|
||||
deleted_by VARCHAR(100),
|
||||
|
||||
-- Contraintes
|
||||
CONSTRAINT fk_coaches_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_coaches_rating_range CHECK (average_rating IS NULL OR (average_rating >= 0 AND average_rating <= 5))
|
||||
);
|
||||
|
||||
-- Table de liaison pour les types de services offerts par le coach
|
||||
CREATE TABLE coach_service_types (
|
||||
coach_id BIGINT NOT NULL,
|
||||
service_type VARCHAR(30) NOT NULL CHECK (service_type IN ('strategic_workshop', 'one_on_one_coaching', 'on_demand_coaching', 'special_project')),
|
||||
|
||||
PRIMARY KEY (coach_id, service_type),
|
||||
CONSTRAINT fk_coach_service_types_coach_id FOREIGN KEY (coach_id) REFERENCES coaches(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Index pour les performances
|
||||
CREATE INDEX idx_coaches_user_id ON coaches(user_id);
|
||||
CREATE INDEX idx_coaches_status ON coaches(status);
|
||||
CREATE INDEX idx_coaches_specialization ON coaches(specialization);
|
||||
CREATE INDEX idx_coaches_deleted ON coaches(deleted);
|
||||
CREATE INDEX idx_coaches_available_for_booking ON coaches(available_for_booking);
|
||||
CREATE INDEX idx_coaches_average_rating ON coaches(average_rating);
|
||||
CREATE INDEX idx_coaches_hourly_rate ON coaches(hourly_rate);
|
||||
|
||||
-- Index pour la table de liaison
|
||||
CREATE INDEX idx_coach_service_types_service_type ON coach_service_types(service_type);
|
||||
|
||||
-- Trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE TRIGGER update_coaches_updated_at
|
||||
BEFORE UPDATE ON coaches
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Commentaires sur la table coaches
|
||||
COMMENT ON TABLE coaches IS 'Table des coaches de la plateforme GBCM';
|
||||
COMMENT ON COLUMN coaches.id IS 'Identifiant unique du coach';
|
||||
COMMENT ON COLUMN coaches.user_id IS 'Référence vers l''utilisateur associé';
|
||||
COMMENT ON COLUMN coaches.specialization IS 'Spécialisation principale du coach';
|
||||
COMMENT ON COLUMN coaches.bio IS 'Biographie professionnelle du coach';
|
||||
COMMENT ON COLUMN coaches.years_of_experience IS 'Années d''expérience du coach';
|
||||
COMMENT ON COLUMN coaches.certifications IS 'Certifications du coach';
|
||||
COMMENT ON COLUMN coaches.languages IS 'Langues parlées par le coach';
|
||||
COMMENT ON COLUMN coaches.hourly_rate IS 'Tarif horaire du coach';
|
||||
COMMENT ON COLUMN coaches.status IS 'Statut du coach (ACTIVE, INACTIVE, ON_LEAVE, TERMINATED)';
|
||||
COMMENT ON COLUMN coaches.available_for_booking IS 'Disponibilité pour les réservations';
|
||||
COMMENT ON COLUMN coaches.working_hours_start IS 'Heure de début des heures de travail';
|
||||
COMMENT ON COLUMN coaches.working_hours_end IS 'Heure de fin des heures de travail';
|
||||
COMMENT ON COLUMN coaches.working_days IS 'Jours de travail du coach';
|
||||
COMMENT ON COLUMN coaches.timezone IS 'Fuseau horaire du coach';
|
||||
COMMENT ON COLUMN coaches.start_date IS 'Date de début d''activité comme coach';
|
||||
COMMENT ON COLUMN coaches.end_date IS 'Date de fin d''activité comme coach';
|
||||
COMMENT ON COLUMN coaches.average_rating IS 'Note moyenne du coach';
|
||||
COMMENT ON COLUMN coaches.total_ratings IS 'Nombre total d''évaluations reçues';
|
||||
COMMENT ON COLUMN coaches.total_sessions IS 'Nombre total de sessions effectuées';
|
||||
COMMENT ON COLUMN coaches.total_revenue IS 'Revenus totaux générés';
|
||||
COMMENT ON COLUMN coaches.notes IS 'Notes internes sur le coach';
|
||||
COMMENT ON COLUMN coaches.created_at IS 'Date et heure de création de l''enregistrement';
|
||||
COMMENT ON COLUMN coaches.updated_at IS 'Date et heure de dernière mise à jour';
|
||||
COMMENT ON COLUMN coaches.created_by IS 'Utilisateur qui a créé l''enregistrement';
|
||||
COMMENT ON COLUMN coaches.updated_by IS 'Utilisateur qui a effectué la dernière mise à jour';
|
||||
COMMENT ON COLUMN coaches.deleted IS 'Indicateur de suppression logique';
|
||||
COMMENT ON COLUMN coaches.deleted_at IS 'Date et heure de suppression logique';
|
||||
COMMENT ON COLUMN coaches.deleted_by IS 'Utilisateur qui a effectué la suppression logique';
|
||||
|
||||
-- Commentaires sur la table coach_service_types
|
||||
COMMENT ON TABLE coach_service_types IS 'Table de liaison entre coaches et types de services';
|
||||
COMMENT ON COLUMN coach_service_types.coach_id IS 'Référence vers le coach';
|
||||
COMMENT ON COLUMN coach_service_types.service_type IS 'Type de service offert par le coach';
|
||||
38
src/main/resources/import.sql
Normal file
38
src/main/resources/import.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- Données de test pour le développement GBCM
|
||||
-- Ce fichier est chargé automatiquement en mode développement
|
||||
|
||||
-- Insertion des utilisateurs de test
|
||||
-- Mot de passe pour tous: "password123" (hash BCrypt)
|
||||
INSERT INTO users (first_name, last_name, email, password_hash, role, active, created_by) VALUES
|
||||
('Admin', 'System', 'admin@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'ADMIN', true, 'system'),
|
||||
('John', 'Manager', 'manager@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'MANAGER', true, 'system'),
|
||||
('Sarah', 'Coach', 'sarah.coach@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'COACH', true, 'system'),
|
||||
('Michael', 'Expert', 'michael.expert@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'COACH', true, 'system'),
|
||||
('Emily', 'Johnson', 'emily.johnson@techcorp.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'CLIENT', true, 'system'),
|
||||
('David', 'Smith', 'david.smith@innovate.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'CLIENT', true, 'system'),
|
||||
('Lisa', 'Brown', 'lisa.brown@startup.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'PROSPECT', true, 'system'),
|
||||
('Robert', 'Wilson', 'robert.wilson@enterprise.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'PROSPECT', true, 'system');
|
||||
|
||||
-- Insertion des coaches
|
||||
INSERT INTO coaches (user_id, specialization, bio, years_of_experience, certifications, languages, hourly_rate, status, available_for_booking, working_hours_start, working_hours_end, working_days, timezone, start_date, average_rating, total_ratings, total_sessions, total_revenue, created_by) VALUES
|
||||
(3, 'Strategic Planning', 'Expert en planification stratégique avec plus de 10 ans d''expérience dans le conseil aux PME. Spécialisée dans la transformation digitale et l''optimisation des processus.', 10, 'Certified Management Consultant (CMC), PMP, Lean Six Sigma Black Belt', 'English, French, Spanish', 125.00, 'ACTIVE', true, '09:00:00', '17:00:00', 'MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY', 'America/New_York', '2020-01-15', 4.8, 45, 120, 15000.00, 'system'),
|
||||
(4, 'Business Development', 'Coach expérimenté en développement commercial et leadership. Aide les entrepreneurs à développer leurs compétences managériales et à faire croître leur entreprise.', 8, 'Certified Business Coach (CBC), MBA, Dale Carnegie Leadership Training', 'English, French', 100.00, 'ACTIVE', true, '08:00:00', '18:00:00', 'MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY', 'America/New_York', '2021-03-01', 4.9, 38, 95, 9500.00, 'system');
|
||||
|
||||
-- Insertion des types de services pour les coaches
|
||||
INSERT INTO coach_service_types (coach_id, service_type) VALUES
|
||||
(1, 'strategic_workshop'),
|
||||
(1, 'one_on_one_coaching'),
|
||||
(1, 'special_project'),
|
||||
(2, 'one_on_one_coaching'),
|
||||
(2, 'on_demand_coaching'),
|
||||
(2, 'special_project');
|
||||
|
||||
-- Insertion des clients
|
||||
INSERT INTO clients (user_id, company_name, industry, company_size, annual_revenue, address_line1, city, state, postal_code, country, website, status, converted_at, primary_service_type, total_contract_value, relationship_start_date, acquisition_source, created_by) VALUES
|
||||
(5, 'TechCorp Solutions', 'Technology', 50, 2500000.00, '123 Tech Street', 'Atlanta', 'GA', '30309', 'USA', 'https://techcorp.com', 'ACTIVE', '2023-06-15 10:30:00', 'strategic_workshop', 4000.00, '2023-06-15', 'Website', 'system'),
|
||||
(6, 'Innovate Inc', 'Consulting', 25, 1200000.00, '456 Innovation Ave', 'Atlanta', 'GA', '30308', 'USA', 'https://innovate.com', 'ACTIVE', '2023-08-20 14:15:00', 'one_on_one_coaching', 2500.00, '2023-08-20', 'Referral', 'system');
|
||||
|
||||
-- Insertion des prospects
|
||||
INSERT INTO clients (user_id, company_name, industry, company_size, annual_revenue, address_line1, city, state, postal_code, country, website, status, acquisition_source, created_by) VALUES
|
||||
(7, 'StartupCo', 'E-commerce', 10, 500000.00, '789 Startup Blvd', 'Atlanta', 'GA', '30307', 'USA', 'https://startupco.com', 'PROSPECT', 'LinkedIn', 'system'),
|
||||
(8, 'Enterprise Corp', 'Manufacturing', 200, 15000000.00, '321 Enterprise Way', 'Atlanta', 'GA', '30306', 'USA', 'https://enterprise.com', 'PROSPECT', 'Trade Show', 'system');
|
||||
Reference in New Issue
Block a user