diff --git a/pom.xml b/pom.xml index 8355737d653563472d927e315c1cf88fc1632d44..d235935d1b46ab22085f41f9e8a5e2b468133372 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ <artifactId>spring-boot-starter-web</artifactId> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> @@ -58,6 +59,18 @@ <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + + <!-- Spring Boot Starter Security --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..f5fb1abf9a3f69580397d2860554e1ff0eb931b3 --- /dev/null +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.java @@ -0,0 +1,44 @@ +package fr.atlantique.imt.inf211.jobmngt.config; + +import fr.atlantique.imt.inf211.jobmngt.dao.AppUserDao; +import fr.atlantique.imt.inf211.jobmngt.entity.AppUser; +import fr.atlantique.imt.inf211.jobmngt.entity.Company; +import fr.atlantique.imt.inf211.jobmngt.entity.RoleType; +import fr.atlantique.imt.inf211.jobmngt.service.CompanyService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AppUserDetailsService implements UserDetailsService { + + @Autowired + private AppUserDao appUserDao; + private static final Logger logger = LoggerFactory.getLogger(AppUserDetailsService.class); + + + @Override + public UserDetails loadUserByUsername(String mail) throws UsernameNotFoundException { + logger.info("Tentative de connexion avec l'email : {}", mail); + + AppUser user = appUserDao.findByMail(mail) + .orElseThrow(() -> { + logger.error("Utilisateur non trouvé : {}", mail); + return new UsernameNotFoundException("User not found"); + }); + + logger.info("Utilisateur trouvé : {}", user.getMail()); + logger.info("Rôle de l'utilisateur : {}", user.getUsertype()); + + return User.builder() + .username(user.getMail()) + .password(user.getPassword()) + .roles("ROLE_" + user.getUsertype()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.java new file mode 100644 index 0000000000000000000000000000000000000000..4bdf6f44bc4a9a566a6a7d5cf2c5c197b271ffba --- /dev/null +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.java @@ -0,0 +1,16 @@ +package fr.atlantique.imt.inf211.jobmngt.config; + +import fr.atlantique.imt.inf211.jobmngt.entity.AppUser; +import jakarta.servlet.http.HttpSession; + +public class CheckAuth { + public static boolean isUserAuthenticated(HttpSession session) { + return session != null && session.getAttribute("loggedInUser") != null; + } + + public static boolean isUserAuthenticated(HttpSession session, int id) { + AppUser user =session != null && session.getAttribute("loggedInUser") != null? + (AppUser) session.getAttribute("loggedInUser"):null; + return user != null && user.getId() == id; + } +} diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..b490a6d2fc4bf5d9d0e8cbef4f8bbcf5d5082f1e --- /dev/null +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.java @@ -0,0 +1,76 @@ +package fr.atlantique.imt.inf211.jobmngt.config; + +import fr.atlantique.imt.inf211.jobmngt.dao.QualificationLevelDao; +import fr.atlantique.imt.inf211.jobmngt.dao.SectorDao; +import fr.atlantique.imt.inf211.jobmngt.entity.QualificationLevel; +import fr.atlantique.imt.inf211.jobmngt.entity.Sector; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class DataInitializer { + + final SectorDao sectorDao; + final QualificationLevelDao qualificationLevelDao; + + + @PostConstruct + public void init() { + initSectors(); + initQualificationLevels(); + } + + public void initSectors() { + List<String> sectorNames = List.of( + "Purchase/Logistic", + "Administration", + "Agriculture", + "Agrofood", + "Insurance", + "Audit/Advise/Expertise", + "Public works/Real estate", + "Trade", + "Communication/Art/Media/Fashion", + "Industry/Engineering/Production", + "Computer science", + "Juridique/Fiscal/Droit", + "Marketing", + "Public/Parapublic", + "Human resources", + "Information Technology", + "Software Engineering", + "Telecommunications", + "Health/Social/Biology/Humanitarian", + "Telecom/Networking" + ); + + sectorNames.forEach(name -> { + if (!sectorDao.existsByLabel(name)) { + Sector sector = new Sector(); + sector.setLabel(name); + sectorDao.persist(sector); + } + }); + } + public void initQualificationLevels() { + List<String> levels = List.of( + "Professional level", + "A-diploma", + "Licence", + "Master", + "PhD" + ); + + levels.forEach(level -> { + if (!qualificationLevelDao.existsByLabel(level)) { + QualificationLevel ql = new QualificationLevel(); + ql.setLabel(level); + qualificationLevelDao.persist(ql); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..d95e57e4cc992e65f704efd51ded39dde65e8882 --- /dev/null +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.java @@ -0,0 +1,50 @@ +package fr.atlantique.imt.inf211.jobmngt.config; + +import fr.atlantique.imt.inf211.jobmngt.entity.RoleType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/", + "/login", + "/logout", + "/companies/**", + "/candidates/**", + "/jobs/**", + "/jobs/update", + "/applications/**", + "/qualificationLevels/**", + "/sectors/**", + "/auth/**", + "/index.html", + "/static/**", + "/css/**", + "/js/**", + "/img/**" + ).permitAll() + .anyRequest().authenticated() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.java index 6a1d93b906df3626ab4ab6fbdc8b92dce723aedc..d9b0233676bb7486871940ab1792c8330bb63e41 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.java @@ -1,72 +1,65 @@ package fr.atlantique.imt.inf211.jobmngt.controller; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.logging.Logger; - +import java.util.stream.Collectors; + +import fr.atlantique.imt.inf211.jobmngt.config.CheckAuth; +import fr.atlantique.imt.inf211.jobmngt.entity.*; +import fr.atlantique.imt.inf211.jobmngt.service.*; +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.hibernate.Hibernate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import fr.atlantique.imt.inf211.jobmngt.entity.Application; -import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; -import fr.atlantique.imt.inf211.jobmngt.entity.QualificationLevel; -import fr.atlantique.imt.inf211.jobmngt.entity.Sector; -import fr.atlantique.imt.inf211.jobmngt.service.ApplicationService; -import fr.atlantique.imt.inf211.jobmngt.service.CandidateService; -import fr.atlantique.imt.inf211.jobmngt.service.QualificationLevelService; -import fr.atlantique.imt.inf211.jobmngt.service.SectorService; import jakarta.servlet.http.HttpSession; @Controller @RequestMapping("/applications") +@RequiredArgsConstructor public class ApplicationController { private static final Logger logger = Logger.getLogger(ApplicationController.class.getName()); - @Autowired - private ApplicationService applicationService; + final ApplicationService applicationService; + + final CandidateService candidateService; - @Autowired - private CandidateService candidateService; + final QualificationLevelService qualificationLevelService; + + final SectorService sectorService; + final MatchingIndexService matchingIndexService; - @Autowired - private QualificationLevelService qualificationLevelService; - @Autowired - private SectorService sectorService; /** * Affiche le formulaire de candidature. */ @GetMapping("/apply") - public String showApplicationForm(HttpSession session, Model model) { - Integer userId = (Integer) session.getAttribute("uid"); - - if (userId == null) { - model.addAttribute("error", "Vous devez être connecté pour postuler."); + public String showApplicationForm(Model model, HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession())) { return "redirect:/login"; } - Optional<Candidate> candidateOpt = candidateService.findById(userId); - if (candidateOpt.isEmpty()) { - model.addAttribute("error", "Utilisateur non trouvé."); - return "error"; - } - - Candidate candidate = candidateOpt.get(); List<QualificationLevel> qualifications = qualificationLevelService.getAllQualificationLevels(); - List<Sector> sectors = sectorService.getAllSectors(); + Collection<Sector> sectors = sectorService.getAllSectors(); model.addAttribute("application", new Application()); - model.addAttribute("candidate", candidate); model.addAttribute("qualifications", qualifications); model.addAttribute("sectors", sectors); @@ -79,21 +72,22 @@ public class ApplicationController { @PostMapping("/apply") public String submitApplication( @RequestParam("cv") String cv, + @RequestParam("publicationDate") LocalDate date, @RequestParam("qualificationLevel") int qualificationLevelId, @RequestParam("sectors") List<Integer> sectorIds, - HttpSession session, + HttpServletRequest request, RedirectAttributes redirectAttributes) { - Integer userId = (Integer) session.getAttribute("uid"); - if (userId == null) { - return "redirect:/login?error=Vous devez être connecté pour postuler."; + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; } - Optional<Candidate> candidateOpt = candidateService.findById(userId); + Optional<Candidate> candidateOpt = candidateService.findById((Integer) request.getSession().getAttribute("userId")); if (candidateOpt.isEmpty()) { redirectAttributes.addFlashAttribute("error", "Utilisateur non trouvé."); return "redirect:/error"; } + Application application = new Application(); Candidate candidate = candidateOpt.get(); Optional<QualificationLevel> qualificationLevelOpt = qualificationLevelService.findById(qualificationLevelId); @@ -104,24 +98,24 @@ public class ApplicationController { } QualificationLevel qualificationLevel = qualificationLevelOpt.get(); - List<Sector> sectors = sectorService.getSectorsByIds(sectorIds); + Collection<Sector> sectors = sectorService.getSectorsByIds(sectorIds); if (sectors.isEmpty()) { redirectAttributes.addFlashAttribute("error", "Vous devez sélectionner au moins un secteur."); return "redirect:/error"; } - Application application = new Application(); + application.setCandidate(candidate); application.setCv(cv); application.setQualificationlevel(qualificationLevel); application.setSectors(sectors); - application.setAppdate(LocalDateTime.now()); + application.setAppdate(date); Application savedApplication = applicationService.save(application); // Stocke uniquement l'ID en session - session.setAttribute("lastApplicationId", savedApplication.getId()); + request.getSession().setAttribute("lastApplicationId", savedApplication.getId()); return "redirect:/applications/confirmation"; } @@ -130,30 +124,25 @@ public class ApplicationController { * Affiche la page de confirmation avec les détails de la candidature. */ @GetMapping("/confirmation") - public String showConfirmationPage(Model model, HttpSession session) { - Integer lastApplicationId = (Integer) session.getAttribute("lastApplicationId"); - - if (lastApplicationId == null) { - System.out.println(" Aucun ID de candidature enregistré."); - return "redirect:/error"; - } - - Optional<Application> applicationOpt = applicationService.findById(lastApplicationId); - if (applicationOpt.isEmpty()) { - System.out.println(" Aucune candidature trouvée en base de données !"); - return "redirect:/error"; + public String showConfirmationPage(Model model, HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; } + Integer lastApplicationId = (Integer) request.getSession().getAttribute("lastApplicationId"); + Application application = applicationService.findById(lastApplicationId).orElseThrow(() -> new EntityNotFoundException("Application not found"));; // Méthode custom - Application application = applicationOpt.get(); + // Force l'initialisation avant de fermer la session + Hibernate.initialize(application.getQualificationlevel()); + Hibernate.initialize(application.getSectors()); - // Vérifier si l'objet a bien été récupéré - System.out.println(" Candidature trouvée : " + application.getId()); - System.out.println(" CV : " + application.getCv()); - System.out.println(" Qualification : " + (application.getQualificationlevel() != null ? application.getQualificationlevel().getLabel() : "NULL")); - System.out.println(" Secteurs : " + (application.getSectors() != null ? application.getSectors().size() : "NULL")); - System.out.println(" Date : " + application.getAppdate()); + // Ajoutez explicitement tous les attributs nécessaires + model.addAttribute("appId", application.getId()); + model.addAttribute("appCv", application.getCv()); + model.addAttribute("appDate", application.getAppdate()); + model.addAttribute("qualification", application.getQualificationlevel()); + model.addAttribute("sectors", application.getSectors()); + model.addAttribute("appD", application.getAppdate()); - model.addAttribute("application", application); return "application/application-confirmation"; } @@ -162,7 +151,9 @@ public class ApplicationController { List<Application> applications = applicationService.getAllApplications(); if (applications.isEmpty()) { - model.addAttribute("error", "Aucune candidature trouvée."); + model.addAttribute("error", "No applications found"); + return "redirect:/list"; + } model.addAttribute("applicationsList", applications); // Renommage de la variable @@ -173,14 +164,17 @@ public class ApplicationController { */ @GetMapping("/details/{id}") - public String showApplicationDetails(@PathVariable int id, Model model) { - Optional<Application> applicationOpt = applicationService.findById(id); - if (applicationOpt.isEmpty()) { - model.addAttribute("error", "Candidature non trouvée."); - return "error"; // Page d'erreur si l'application n'est pas trouvée - } - Application application = applicationOpt.get(); - model.addAttribute("application", application); + public String showApplicationDetails(@PathVariable int id, Model model, HttpServletRequest request) { + + Application application = applicationService.findById(id).orElseThrow(); + + // Test avec des valeurs directes + model.addAttribute("cv", application.getCv()); + model.addAttribute("sectors", application.getSectors()); + model.addAttribute("appdate", application.getAppdate()); + model.addAttribute("qualification", application.getQualificationlevel()); + model.addAttribute("candidate", application.getCandidate()); + return "application/application-details"; } @@ -189,48 +183,55 @@ public class ApplicationController { @GetMapping("/edit/{id}") public String showEditForm(@PathVariable int id, Model model, HttpSession session) { - Integer userId = (Integer) session.getAttribute("uid"); - - if (userId == null) { - return "redirect:/login"; // Rediriger vers login si non connecté + if (!CheckAuth.isUserAuthenticated(session)) { + return "redirect:/login"; } - Optional<Application> applicationOpt = applicationService.findById(id); - if (applicationOpt.isEmpty()) { - model.addAttribute("error", "Candidature non trouvée."); - return "error"; - } + // Récupération avec vérification robuste + Application application = applicationService.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Invalid application id: " + id)); - Application application = applicationOpt.get(); + // LOG important + logger.info("Editing application ID: " + application.getId()); - // Vérification si l'utilisateur est bien le propriétaire - if (application.getCandidate().getId() != userId) { - model.addAttribute("error", "Vous ne pouvez modifier que vos propres candidatures."); - return "error"; - } + // Initialisation des relations + Hibernate.initialize(application.getQualificationlevel()); + Hibernate.initialize(application.getSectors()); - List<QualificationLevel> qualifications = qualificationLevelService.getAllQualificationLevels(); - List<Sector> sectors = sectorService.getAllSectors(); + // Vérification null + if(application.getSectors() == null) { + application.setSectors(new HashSet<>()); + } - model.addAttribute("application", application); - model.addAttribute("qualifications", qualifications); - model.addAttribute("sectors", sectors); + model.addAttribute("id", application.getId()); + model.addAttribute("cv", application.getCv()); + model.addAttribute("candidate", application.getCandidate()); + model.addAttribute("sectorApp", application.getSectors()); + model.addAttribute("qualApp", application.getQualificationlevel()); + model.addAttribute("appDate", application.getAppdate()); + model.addAttribute("qualifications", qualificationLevelService.getAllQualificationLevels()); + model.addAttribute("sectors", sectorService.listOfSectors()); - return "application/application-edit"; + return "application/application-edit"; // Chemin exact } - /** - * Traite la modification d'une candidature - */ @PostMapping("/update") - public String updateApplication( + public String updateJobOffer( @RequestParam("id") int id, - @RequestParam("cv") String cv, - @RequestParam("qualificationLevel") int qualificationLevelId, - @RequestParam("sectors") List<Integer> sectorIds, - RedirectAttributes redirectAttributes) { + @RequestParam(value = "cv", required = false) String newCv, + @RequestParam(value = "publicationDate", required = false) LocalDate newAppDate, + @RequestParam(value = "qualificationLevel", required = false) Integer qualificationLevelId, + @RequestParam(value = "sectorIds", required = false) List<Integer> sectorIds, + RedirectAttributes redirectAttributes, + HttpServletRequest request) { + + // Vérification d'authentification + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; + } + // Récupération de l'offre existante Optional<Application> applicationOpt = applicationService.findById(id); if (applicationOpt.isEmpty()) { redirectAttributes.addFlashAttribute("error", "Candidature non trouvée."); @@ -238,20 +239,51 @@ public class ApplicationController { } Application application = applicationOpt.get(); - application.setCv(cv); - Optional<QualificationLevel> qualificationLevelOpt = qualificationLevelService.findById(qualificationLevelId); - if (qualificationLevelOpt.isPresent()) { - application.setQualificationlevel(qualificationLevelOpt.get()); + // Mise à jour conditionnelle des champs + if (newCv != null && !newCv.isEmpty()) { + application.setCv(newCv); } + // Si newCv est null ou vide, on conserve l'ancienne valeur - List<Sector> sectors = sectorService.getSectorsByIds(sectorIds); - application.setSectors(sectors); - - applicationService.save(application); - redirectAttributes.addFlashAttribute("success", "Candidature mise à jour avec succès."); + if (newAppDate != null) { + application.setAppdate(newAppDate); + } + // Si newAppDate est null, on conserve l'ancienne date - return "redirect:/applications/list"; + // Mise à jour conditionnelle du niveau de qualification + if (qualificationLevelId != null) { + Optional<QualificationLevel> qualificationLevel = qualificationLevelService.findById(qualificationLevelId); + qualificationLevel.ifPresent(application::setQualificationlevel); + } + // Si qualificationLevelId est null, on conserve l'ancienne qualification + + // Mise à jour conditionnelle des secteurs + if (sectorIds != null) { + if (!sectorIds.isEmpty()) { + Collection<Sector> selectedSectors = sectorService.getSectorsByIds(sectorIds); + application.setSectors(selectedSectors); + } else { + application.setSectors(new HashSet<>()); + } + } + // Si sectorIds est null, on conserve les anciens secteurs + + try { + // Sauvegarde de l'offre mise à jour + applicationService.save(application); + + // Mise à jour de l'index de matching si nécessaire + matchingIndexService.updateIndex(application, true); + + // Stocke uniquement l'ID en session + request.getSession().setAttribute("lastApplicationId", application.getId()); + redirectAttributes.addFlashAttribute("successMessage", "Candidature mise à jour avec succès !"); + return "redirect:/applications/confirmation"; + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Erreur lors de la mise à jour: " + e.getMessage()); + return "redirect:/applications/edit/" + id; + } } /** @@ -259,7 +291,10 @@ public class ApplicationController { */ @GetMapping("/delete/{id}") public String deleteApplication(@PathVariable int id, RedirectAttributes redirectAttributes, HttpSession session) { - Integer userId = (Integer) session.getAttribute("uid"); + if (!CheckAuth.isUserAuthenticated(session)) { + return "redirect:/login"; + } + Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { return "redirect:/login"; diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.java index 24996e030a90ed8c2f8c6bbf1e21169725d47eef..0e14059e421f381778fbd3246ec4344ca757972b 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.java @@ -2,6 +2,10 @@ package fr.atlantique.imt.inf211.jobmngt.controller; import java.util.List; +import fr.atlantique.imt.inf211.jobmngt.config.CheckAuth; +import fr.atlantique.imt.inf211.jobmngt.entity.AppUser; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -14,6 +18,7 @@ import org.springframework.web.servlet.ModelAndView; import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; import fr.atlantique.imt.inf211.jobmngt.service.CandidateService; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @RequestMapping("/candidates") @@ -29,29 +34,32 @@ public class CandidateController { } @PostMapping("/signup") - public ModelAndView registerCandidate(@RequestParam String mail, + public String registerCandidate(@RequestParam String mail, @RequestParam String password, @RequestParam String lastname, @RequestParam String firstname, - @RequestParam String city) { + @RequestParam String city, RedirectAttributes redirectAttributes + ) { + boolean isRegistered = candidateService.registerCandidate(new Candidate(mail, password, city, lastname, firstname)); if (!isRegistered) { - ModelAndView mav = new ModelAndView("candidate/signupCandidate"); - mav.addObject("error", "This email is already registered. Please use another one."); - return mav; + redirectAttributes.addFlashAttribute("error", "This email is already registered. Please use another one."); + return "candidate/signupCandidate"; } - // Redirection vers confirmation avec un message spécifique - ModelAndView mav = new ModelAndView("candidate/confirmation"); - mav.addObject("message", "Your account has been successfully created!"); - return mav; + redirectAttributes.addFlashAttribute("success", "Entreprise créée avec succès !"); + return "redirect:/candidates/list"; // Redirige vers la liste des entreprises } @GetMapping("/list") public String listCandidates(Model model) { List<Candidate> candidates = candidateService.getAllCandidates(); + if (candidates.isEmpty()) { + model.addAttribute("error", "Aucune candidat trouvée."); + } + model.addAttribute("candidates", candidates); return "candidate/candidates-list"; } @@ -69,11 +77,17 @@ public class CandidateController { return "error"; } model.addAttribute("candidate", candidate); + System.out.println(candidateService.getApplicationByCandidate(id)); + // Change this line: + model.addAttribute("candidateApplications", candidate.getCandidatApplications()); return "candidate/details"; } @GetMapping("/edit/{id}") - public String showEditForm(@PathVariable int id, Model model) { + public String showEditForm(@PathVariable int id, Model model,HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } Candidate candidate = candidateService.getCandidateById(id); if (candidate == null) { model.addAttribute("error", "Le candidat n'existe pas."); @@ -89,7 +103,10 @@ public class CandidateController { @RequestParam String lastname, @RequestParam String firstname, @RequestParam String city, - Model model) { + Model model,HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } Candidate candidate = candidateService.getCandidateById(id); if (candidate == null) { model.addAttribute("error", "Le candidat n'existe pas."); @@ -110,19 +127,35 @@ public class CandidateController { } @GetMapping("/delete/{id}") - public String deleteCandidate(@PathVariable int id, Model model) { - Candidate candidate = candidateService.getCandidateById(id); + public String deleteCandidate( + @PathVariable int id, + HttpServletRequest request, + RedirectAttributes redirectAttributes) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } + Candidate candidate = candidateService.getCandidateById(id); if (candidate == null) { - model.addAttribute("error", "Le candidat avec l'ID " + id + " n'existe pas."); - return "error"; // Affiche une page d'erreur si le candidat n'existe pas + redirectAttributes.addFlashAttribute("error", + "Candidate with ID " + id + " not found"); + return "redirect:/candidates/list"; } candidateService.deleteCandidate(id); - // Rediriger vers la liste avec un message de confirmation - model.addAttribute("message", "Le candidat " + candidate.getFirstname() + " " + candidate.getLastname() + " a été supprimé avec succès !"); - return "candidate/confirmationSupp"; // Affichage de la confirmation après suppression + // Invalidation de session si nécessaire + HttpSession session = request.getSession(false); + if (session != null) { + AppUser loggedInUser = (AppUser) session.getAttribute("loggedInUser"); + if (loggedInUser != null && loggedInUser.getId() == id) { + session.invalidate(); + return "redirect:/login?logout"; + } + } + + redirectAttributes.addFlashAttribute("deletedCandidate", candidate); + return "redirect:/candidates/delete/confirmation"; } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.java index cdd39045bf5b7982195429abdc7658ea142bbea5..5a5913cd3ff21b88c6edf9f49f6d60a411dfb3bb 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.java @@ -4,92 +4,87 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; +import fr.atlantique.imt.inf211.jobmngt.config.CheckAuth; +import fr.atlantique.imt.inf211.jobmngt.entity.*; import fr.atlantique.imt.inf211.jobmngt.service.JobOfferService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import fr.atlantique.imt.inf211.jobmngt.entity.Company; -import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; import fr.atlantique.imt.inf211.jobmngt.service.CompanyService; @Controller +@RequiredArgsConstructor public class CompanyController { - @Autowired - private CompanyService companyService; - private final JobOfferService jobOfferService; + final CompanyService companyService; + final JobOfferService jobOfferService; + final PasswordEncoder passwordEncoder; - public CompanyController(CompanyService companyService, JobOfferService jobOfferService) { - this.jobOfferService = jobOfferService; - this.companyService = companyService; - } - @GetMapping("/jobOffers/view/{id}") - public String viewJobOffer(@PathVariable("id") Long id, Model model) { - Optional<JobOffer> offerOpt = jobOfferService.findById(id); - if (offerOpt.isPresent()) { - model.addAttribute("offer", offerOpt.get()); - return "jobOffer/jobOfferView"; - } - return "redirect:/companies"; // Redirige si l'offre n'est pas trouvée + // Affiche le formulaire d'inscription + @GetMapping("/companies/create") + public String showCreateForm(Model model) { + // On passe un objet 'Company' vide pour lier les champs du formulaire + model.addAttribute("company", new Company()); + return "company/companyForm"; } + // Enregistre une entreprise avec vérification des doublons et message de succès + @PostMapping("/companies/create") + public String registerCompany(@ModelAttribute("company") Company company, + BindingResult bindingResult, + RedirectAttributes redirectAttributes) { - - - - // // // Afficher les offres d'emploi d'une entreprise spécifique - @GetMapping("/companies/{id}/jobOffers") - public String listCompanyJobOffers(@PathVariable("id") Long companyId, Model model) { - Optional<Company> companyOpt = companyService.findById(companyId); - if (companyOpt.isPresent()) { - Company company = companyOpt.get(); - List<JobOffer> jobOffers = jobOfferService.findByCompany(company); - model.addAttribute("company", company); - model.addAttribute("jobOffers", jobOffers); - return "company/jobOffers"; + if (bindingResult.hasErrors()) { + return "company/companyForm"; } - model.addAttribute("errorMessage", "Entreprise introuvable !"); - return "error"; - - - } - - - - + Optional<Company> existingCompany = companyService.findByMail(company.getMail()); + if (existingCompany.isPresent()) { + redirectAttributes.addFlashAttribute("error", "This user already exist "); + return "redirect:/companies/create"; + } + try { + company.setPassword(passwordEncoder.encode(company.getPassword())); + company.setUsertype(RoleType.Company.name()); + companyService.saveCompany(company); + redirectAttributes.addFlashAttribute("success", "Entreprise créée avec succès !"); + return "redirect:/companies"; // Redirige vers la liste des entreprises - // Affiche le formulaire d'inscription - @GetMapping("/companies/create") - public String showCreateForm(Model model) { - // On passe un objet 'Company' vide pour lier les champs du formulaire - model.addAttribute("company", new Company()); - return "company/companyForm"; + } catch (Exception e) { + bindingResult.reject("error.global", "Erreur technique : " + e.getMessage()); + return "company/companyForm"; + } } - // Affiche la liste des entreprises @GetMapping("/companies") public String listCompanies(Model model) { - model.addAttribute("companies", companyService.getAllCompanies()); + List<Company> companies = companyService.getAllCompanies(); + if (companies.isEmpty()) { + model.addAttribute("error", "Aucune entreprise trouvée."); + } + + model.addAttribute("companies",companies); return "company/companyList"; } // Affiche les détails d'une entreprise @GetMapping("/companies/view/{id}") - public String viewCompany(@PathVariable("id") Long id, Model model) { + public String viewCompany(@PathVariable("id") int id, Model model,HttpServletRequest request) { Optional<Company> companyOpt = companyService.findById(id); if (companyOpt.isPresent()) { Company company = companyOpt.get(); @@ -102,33 +97,13 @@ public class CompanyController { return "redirect:/companies"; } - - // Enregistre une entreprise avec vérification des doublons et message de succès - @PostMapping("/companies/create") - public String registerCompany(Company company, RedirectAttributes redirectAttributes, Model model) { - Optional<Company> existingCompany = companyService.findByMail(company.getMail()); - - if (existingCompany.isPresent()) { - model.addAttribute("errorMessage", "❌ Une entreprise avec cet e-mail existe déjà !"); - model.addAttribute("company", company); - return "company/companyForm"; // 🔹 Rester sur le formulaire en cas d'erreur - } - - try { - companyService.saveCompany(company); - redirectAttributes.addFlashAttribute("successMessage", "✅ L'entreprise a été ajoutée avec succès !"); - return "redirect:/companies"; - } catch (Exception e) { - model.addAttribute("errorMessage", "❌ Erreur lors de l'inscription !"); - model.addAttribute("company", company); - return "company/companyForm"; // Rester sur le formulaire en cas d'erreur - } - } - // Affiche le formulaire de modification d'une entreprise // Affiche le formulaire de modification d'une entreprise - @GetMapping("/companies/{id}/edit") - public String showEditForm(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) { + @GetMapping("/companies/edit/{id}") + public String showEditForm(@PathVariable("id") int id, Model model, RedirectAttributes redirectAttributes, HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } Optional<Company> companyOpt = companyService.findById(id); if (companyOpt.isPresent()) { @@ -143,10 +118,14 @@ public class CompanyController { // // Met à jour les informations d'une entreprise avec message de succès // Met à jour les informations d'une entreprise avec message de succès @PostMapping("/companies/update") - public String updateCompany(@ModelAttribute Company company, RedirectAttributes redirectAttributes) { + public String updateCompany(@ModelAttribute Company company, RedirectAttributes redirectAttributes,HttpServletRequest request) { + // check user auth + if (!CheckAuth.isUserAuthenticated(request.getSession(),company.getId())) { + return "redirect:/login"; + } try { // Vérifier si l'entreprise existe avant la mise à jour - Optional<Company> existingCompany = companyService.findById(Long.valueOf(company.getId())); + Optional<Company> existingCompany = companyService.findById(company.getId()); if (existingCompany.isPresent()) { // Mettre à jour les informations de l'entreprise @@ -159,7 +138,7 @@ public class CompanyController { // Enregistrer la mise à jour companyService.saveCompany(companyToUpdate); redirectAttributes.addFlashAttribute("successMessage", "✅ L'entreprise a été mise à jour avec succès !"); - return "redirect:/companies/view/" + company.getId(); + return "redirect:/companies" ; } else { redirectAttributes.addFlashAttribute("errorMessage", "❌ L'entreprise n'existe pas !"); return "redirect:/companies"; @@ -177,15 +156,65 @@ public class CompanyController { // Supprime une entreprise // Supprime une entreprise @GetMapping("/companies/delete/{id}") - public String deleteCompany(@PathVariable("id") Long id, RedirectAttributes redirectAttributes) { + public String deleteCompany(@PathVariable("id") int id, RedirectAttributes redirectAttributes,HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } try { companyService.deleteCompany(id); + // Invalidation de session si nécessaire + HttpSession session = request.getSession(false); + if (session != null) { + AppUser loggedInUser = (AppUser) session.getAttribute("loggedInUser"); + if (loggedInUser != null && loggedInUser.getId() == id) { + session.invalidate(); + return "redirect:/login?logout"; + } + } redirectAttributes.addFlashAttribute("successMessage", "✅ Entreprise supprimée avec succès !"); } catch (Exception e) { redirectAttributes.addFlashAttribute("errorMessage", "❌ Erreur lors de la suppression !"); } return "redirect:/companies"; } + + @GetMapping("/jobOffers/view/{id}") + public String viewJobOffer(@PathVariable("id") int id, Model model,HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),id)) { + return "redirect:/login"; + } + Optional<JobOffer> offerOpt = jobOfferService.findById(id); + if (offerOpt.isPresent()) { + model.addAttribute("job", offerOpt.get()); + return "jobOffer/jobOfferView"; + } + return "redirect:/companies"; // Redirige si l'offre n'est pas trouvée + } + + + + + + // // // Afficher les offres d'emploi d'une entreprise spécifique + @GetMapping("/companies/{id}/jobOffers") + public String listCompanyJobOffers(@PathVariable("id") int companyId, Model model,HttpServletRequest request) { + if (!CheckAuth.isUserAuthenticated(request.getSession(),companyId)) { + return "redirect:/login"; + } + Optional<Company> companyOpt = companyService.findById(companyId); + if (companyOpt.isPresent()) { + Company company = companyOpt.get(); + List<JobOffer> jobOffers = jobOfferService.findByCompany(company); + model.addAttribute("company", company); + model.addAttribute("jobOffers", jobOffers); + return "company/jobOffers"; + } + + model.addAttribute("errorMessage", "Entreprise introuvable !"); + return "error"; + + + } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.java index b20d0869f7f091bb39974dd28037ad2aa9e30e78..4a1f016c20455a2b946a4a6cb50c08356907d2cd 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.java @@ -1,11 +1,13 @@ package fr.atlantique.imt.inf211.jobmngt.controller; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.time.LocalDate; +import java.util.*; import java.util.stream.Collectors; +import fr.atlantique.imt.inf211.jobmngt.config.CheckAuth; +import fr.atlantique.imt.inf211.jobmngt.entity.*; +import fr.atlantique.imt.inf211.jobmngt.service.*; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -17,54 +19,33 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import fr.atlantique.imt.inf211.jobmngt.entity.AppUser; -import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; -import fr.atlantique.imt.inf211.jobmngt.entity.Sector; -import fr.atlantique.imt.inf211.jobmngt.service.CompanyService; -import fr.atlantique.imt.inf211.jobmngt.service.JobOfferService; -import fr.atlantique.imt.inf211.jobmngt.service.QualificationLevelService; -import fr.atlantique.imt.inf211.jobmngt.service.SectorService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; @Controller @RequestMapping("/jobs") +@RequiredArgsConstructor public class JobOfferController { - private final JobOfferService jobOfferService; - private final CompanyService companyService; - private final QualificationLevelService qualificationLevelService; - private final SectorService sectorService; - - @Autowired - public JobOfferController(JobOfferService jobOfferService, - CompanyService companyService, - QualificationLevelService qualificationLevelService, - SectorService sectorService) { - this.jobOfferService = jobOfferService; - this.companyService = companyService; - this.qualificationLevelService = qualificationLevelService; - this.sectorService = sectorService; - } + final JobOfferService jobOfferService; + final CompanyService companyService; + final QualificationLevelService qualificationLevelService; + final SectorService sectorService; + final MatchingIndexService matchingIndexService; + + // Affiche la liste des offres d'emploi avec log @GetMapping public String listJobOffers(Model model, HttpServletRequest request) { - HttpSession session = request.getSession(); - AppUser loggedInUser = (AppUser) session.getAttribute("user"); - - // Vérification de l'utilisateur - if (loggedInUser != null) { - model.addAttribute("userType", loggedInUser.getUsertype()); - model.addAttribute("userEmail", loggedInUser.getMail()); - System.out.println("Utilisateur connecté : " + loggedInUser.getMail() + " | Type: " + loggedInUser.getUsertype()); - } else { - model.addAttribute("userType", null); - model.addAttribute("userEmail", null); - System.out.println("Aucun utilisateur connecté."); + + List<JobOffer> jobOffers = jobOfferService.getAllJobOffers(); + if (jobOffers.isEmpty()) { + model.addAttribute("errorMessage", "Aucune offre trouvée."); } - model.addAttribute("jobOffers", jobOfferService.getAllJobOffers()); + + model.addAttribute("jobOffers", jobOffers); return "jobOffer/jobOfferList"; // Correspond au fichier "jobOfferList.html" dans /templates/ } @@ -73,12 +54,13 @@ public class JobOfferController { @GetMapping("/view/{id}") - public String viewJobOffer(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) { + public String viewJobOffer(@PathVariable("id") int id, Model model, RedirectAttributes redirectAttributes, + HttpServletRequest request) { System.out.println("🔍 Recherche de l'offre avec ID : " + id); Optional<JobOffer> jobOfferOpt = jobOfferService.findById(id); if (jobOfferOpt.isPresent()) { - model.addAttribute("jobOffer", jobOfferOpt.get()); + model.addAttribute("job", jobOfferOpt.get()); return "jobOffer/jobOfferView"; } else { System.out.println("❌ L'offre avec ID " + id + " est introuvable en base de données !"); @@ -88,8 +70,9 @@ public class JobOfferController { } - @GetMapping("/company/job/view/{id}") - public String viewCompanyJob(@PathVariable("id") Long jobId, Model model) { + /* @GetMapping("/company/job/view/{id}") + public String viewCompanyJob(@PathVariable("id") int jobId, Model model) { + Optional<JobOffer> jobOffer = jobOfferService.findById(jobId); if (jobOffer.isEmpty()) { @@ -98,7 +81,7 @@ public class JobOfferController { model.addAttribute("jobOffer", jobOffer.get()); return "jobOffer/companyJobOfferView"; // Nouveau template dédié - } + }*/ @@ -107,102 +90,73 @@ public class JobOfferController { // Affiche le formulaire de création avec sélection des secteurs @GetMapping("/create") public String showCreateForm(Model model, HttpServletRequest request) { - -// // Vérifier si l'utilisateur est une entreprise -// HttpSession session = request.getSession(false); -// if (session == null || !"company".equals(session.getAttribute("usertype"))) { -// return "redirect:/login"; // Redirige vers la connexion si ce n'est pas une entreprise -// } - + // Vérification authentification + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; + } JobOffer jobOffer = new JobOffer(); - jobOffer.setPublicationDate(new Date()); + jobOffer.setPublicationDate(LocalDate.now()); model.addAttribute("jobOffer", jobOffer); - model.addAttribute("companies", companyService.getAllCompanies()); model.addAttribute("qualificationLevels", qualificationLevelService.getAllQualificationLevels()); model.addAttribute("sectors", sectorService.listOfSectors()); return "jobOffer/jobOfferForm"; } - // Enregistre une offre avec gestion des secteurs et des erreurs @PostMapping("/save") public String saveJobOffer(@ModelAttribute JobOffer jobOffer, @RequestParam(value = "sectorIds", required = false) List<Integer> sectorIds, - RedirectAttributes redirectAttributes) { - try { + RedirectAttributes redirectAttributes,HttpServletRequest request) { + // check user auth + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; + } + Optional<Company> companyOpt = companyService.findById((Integer) request.getSession().getAttribute("userId")); + if (companyOpt.isEmpty()) { + return "redirect:/login"; + } System.out.println("🔹 Tentative d'enregistrement: " + jobOffer); // Associer les secteurs sélectionnés à l'offre d'emploi if (sectorIds != null && !sectorIds.isEmpty()) { - Set<Sector> selectedSectors = sectorIds.stream() - .map(id -> sectorService.findById(id.longValue())) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toSet()); // Convertir la liste en Set + Collection<Sector> selectedSectors =sectorService.getSectorsByIds(sectorIds); // Convertir la liste en Set jobOffer.setSectors(selectedSectors); // Affectation corrigée } - - jobOfferService.saveJobOffer(jobOffer); - System.out.println(" Enregistrement réussi !"); - redirectAttributes.addFlashAttribute("successMessage", " Offre d'emploi créée avec succès !"); - return "redirect:/jobs"; - } catch (Exception e) { + jobOffer.setCompany(companyOpt.get()); + try { + jobOfferService.saveJobOffer(jobOffer); + matchingIndexService.indexJobOffer(jobOffer); // Indexation après sauvegarde + + redirectAttributes.addFlashAttribute("successMessage", "Offre d'emploi créée avec succès !"); + return "redirect:/jobs"; + } catch (Exception e) { System.err.println(" Erreur lors de l'enregistrement: " + e.getMessage()); redirectAttributes.addFlashAttribute("errorMessage", " Erreur lors de la création de l'offre !"); return "redirect:/jobs/create"; } + } - /* @PostMapping("/save") - public String saveJobOffer(@ModelAttribute JobOffer jobOffer, - @RequestParam(value = "sectorIds", required = false) List<Long> sectorIds, - RedirectAttributes redirectAttributes, - HttpServletRequest request ) { - - // Vérifier si l'utilisateur est une entreprise - // HttpSession session = request.getSession(false); - // if (session == null || !"company".equals(session.getAttribute("usertype"))) { - // return "redirect:/login"; - // } - try { - if (sectorIds != null && !sectorIds.isEmpty()) { - Set<Sector> selectedSectors = sectorIds.stream() - .map(id -> sectorService.findById(id.intValue())) // - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toSet()); - jobOffer.setSectors(selectedSectors); - } - jobOfferService.saveJobOffer(jobOffer); - redirectAttributes.addFlashAttribute("successMessage", "✅ Offre créée avec succès !"); - return "redirect:/jobs"; - } catch (Exception e) { - redirectAttributes.addFlashAttribute("errorMessage", "❌ Erreur lors de la création de l'offre !"); - return "redirect:/jobs/create"; - } - }*/ @GetMapping("/delete/{id}") - public String deleteJobOffer(@PathVariable("id") Long id, HttpServletRequest request, RedirectAttributes redirectAttributes) { + public String deleteJobOffer(@PathVariable("id") int id, HttpServletRequest request, RedirectAttributes redirectAttributes) { + -// Supprime une offre d'emploi -/*@GetMapping("/delete/{id}") -public String deleteJobOffer(@PathVariable("id") int id, RedirectAttributes redirectAttributes) {*/ + // check user auth + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; + } Optional<JobOffer> jobOfferOpt = jobOfferService.findById(id); if (jobOfferOpt.isPresent()) { - HttpSession session = request.getSession(false); - String userType = (session != null) ? (String) session.getAttribute("usertype") : null; - String userEmail = (session != null) ? (String) session.getAttribute("useremail") : null; JobOffer jobOffer = jobOfferOpt.get(); // Vérifie si l'utilisateur est bien une entreprise et le propriétaire de l'offre - if (userType != null && userType.equals("company") && jobOffer.getCompany().getMail().equals(userEmail)) { try { jobOfferService.deleteJobOffer(id); redirectAttributes.addFlashAttribute("successMessage", "✅ Offre supprimée avec succès !"); @@ -212,9 +166,6 @@ public String deleteJobOffer(@PathVariable("id") int id, RedirectAttributes redi } else { redirectAttributes.addFlashAttribute("errorMessage", "❌ Vous n'avez pas l'autorisation de supprimer cette offre !"); } - } else { - redirectAttributes.addFlashAttribute("errorMessage", "❌ L'offre avec ID " + id + " n'existe pas !"); - } return "redirect:/jobs"; } /* try { @@ -232,20 +183,83 @@ public String deleteJobOffer(@PathVariable("id") int id, RedirectAttributes redi // Affiche le formulaire de modification - @GetMapping("/{id}/edit") - public String showEditForm(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) { + @GetMapping("/edit/{id}") + public String showEditForm(@PathVariable int id, Model model, HttpSession session) { + if (!CheckAuth.isUserAuthenticated(session)) { + return "redirect:/login"; + } + + Optional<JobOffer> jobOfferOpt = jobOfferService.findById(id); - if (jobOfferOpt.isPresent()) { - model.addAttribute("jobOffer", jobOfferOpt.get()); - model.addAttribute("companies", companyService.getAllCompanies()); - model.addAttribute("qualificationLevels", qualificationLevelService.getAllQualificationLevels()); - model.addAttribute("sectors", sectorService.listOfSectors()); - return "jobOffer/jobOfferForm"; + if (jobOfferOpt.isEmpty()) { + model.addAttribute("error", "Candidature non trouvée."); + return "error"; + } + + model.addAttribute("job", jobOfferOpt.get()); + model.addAttribute("qualificationLevels", qualificationLevelService.getAllQualificationLevels()); + model.addAttribute("sectors", sectorService.listOfSectors()); + + return "jobOffer/jobOfferEdit.html"; + } + + @PostMapping("/update") + public String updateJobOffer( + @RequestParam("id") int id, + @RequestParam("title") String title, + @RequestParam("publicationDate") LocalDate date, + @RequestParam("taskDescription") String description, + @RequestParam("qualificationLevel") int qualificationLevelId, + @RequestParam(value = "sectorIds", required = false) List<Integer> sectorIds, + RedirectAttributes redirectAttributes, + HttpServletRequest request) { + + // Vérification d'authentification + if (!CheckAuth.isUserAuthenticated(request.getSession())) { + return "redirect:/login"; + } + + // Récupération de l'offre existante + Optional<JobOffer> jobOfferOpt = jobOfferService.findById(id); + if (jobOfferOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "Offre d'emploi non trouvée."); + return "redirect:/jobs"; + } + + JobOffer jobOffer = jobOfferOpt.get(); + + // Mise à jour des champs de base + jobOffer.setTitle(title); + jobOffer.setTaskDescription(description); + jobOffer.setPublicationDate(date); + + // Mise à jour du niveau de qualification + Optional<QualificationLevel> qualificationLevel = qualificationLevelService.findById(qualificationLevelId); + qualificationLevel.ifPresent(jobOffer::setQualificationLevel); + + // Mise à jour des secteurs + if (sectorIds != null && !sectorIds.isEmpty()) { + Collection<Sector> selectedSectors = sectorService.getSectorsByIds(sectorIds); + jobOffer.setSectors(selectedSectors); } else { - redirectAttributes.addFlashAttribute("errorMessage", "L'offre d'emploi avec ID " + id + " n'existe pas !"); + jobOffer.setSectors(new HashSet<>()); // Effacer les secteurs si aucun n'est sélectionné + } + + try { + // Sauvegarde de l'offre mise à jour + jobOfferService.saveJobOffer(jobOffer); + + // Mise à jour de l'index de matching si nécessaire + matchingIndexService.updateIndex(jobOffer,true); + + redirectAttributes.addFlashAttribute("successMessage", "Offre mise à jour avec succès !"); return "redirect:/jobs"; + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Erreur lors de la mise à jour de l'offre: " + e.getMessage()); + return "redirect:/jobs/edit/" + id; } } + } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.java index d18397b067d011ef65cae51fe54578e2b4fa2dee..dc3e029ea7fdc3b998ba3d54a764edd9b194e7e9 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.java @@ -2,71 +2,110 @@ package fr.atlantique.imt.inf211.jobmngt.controller; import java.util.Optional; +import fr.atlantique.imt.inf211.jobmngt.dao.AppUserDao; +import fr.atlantique.imt.inf211.jobmngt.dao.CandidateDao; +import fr.atlantique.imt.inf211.jobmngt.dao.CompanyDao; +import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; +import fr.atlantique.imt.inf211.jobmngt.entity.Company; +import fr.atlantique.imt.inf211.jobmngt.entity.RoleType; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import fr.atlantique.imt.inf211.jobmngt.entity.AppUser; import fr.atlantique.imt.inf211.jobmngt.service.AppUserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller +@RequiredArgsConstructor public class LoginController { - // Value injected from the application.properties file - @Value("${jobmngt.admin}") - private String adminLogin; - @Autowired - private AppUserService userAppService; + final AppUserDao appUserDao; + final CompanyDao companyDao; + final CandidateDao candidateDao; - // Login form Get Method - @RequestMapping(value = "/login", method = RequestMethod.GET) - public String login() { + final PasswordEncoder passwordEncoder; + + // GET /login - Affiche le formulaire + @GetMapping("/login") + public String showLoginForm( + @RequestParam(required = false) String error, + @RequestParam(required = false) String logout, + Model model) { + + if (error != null) { + model.addAttribute("error", "Email ou mot de passe incorrect"); + } + if (logout != null) { + model.addAttribute("message", "Vous avez été déconnecté avec succès"); + } return "login"; } - - - // Login form post method - @RequestMapping(value = "/login", method = RequestMethod.POST) - public ModelAndView checkLog(@RequestParam("mail") String mail, @RequestParam("password") String pwd, - HttpServletRequest request) { - HttpSession session = request.getSession(); - AppUser appUser = new AppUser(); - appUser.setMail(mail); - appUser.setPassword(pwd); - Optional<AppUser> user = userAppService.checkLogin(appUser); - if (user.isPresent()) { - appUser = user.get(); - System.out.println("User found uid: " + appUser.getId()); - session.setAttribute("loggedInUser", appUser); - if (appUser.getUsertype().equals("candidate")) - session.setAttribute("usertype", "candidate"); - else - session.setAttribute("usertype", "company"); - - session.setAttribute("uid", appUser.getId()); - return new ModelAndView("redirect:/"); - } else { - ModelAndView mav = new ModelAndView("login"); - mav.addObject("error", "Password or username incorrect."); - return mav; + // POST /login - Traite la connexion + @PostMapping("/login") + public String processLogin( + @RequestParam String mail, + @RequestParam String password, + HttpServletRequest request, + RedirectAttributes redirectAttributes) { + + Optional<AppUser> userOpt = appUserDao.findByMail(mail); + if (userOpt.isEmpty()){ + redirectAttributes.addFlashAttribute("error", "User not found"); + return "redirect:/login"; + + } + // Vérification des identifiants + if (!passwordEncoder.matches(password, userOpt.get().getPassword())) { + redirectAttributes.addFlashAttribute("error", "Email ou mot de passe incorrect"); + return "redirect:/login"; + } + + // Création de la session + AppUser user = userOpt.get(); + HttpSession session = request.getSession(true); + session.setAttribute("userId", user.getId()); + session.setAttribute("userMail", user.getMail()); + session.setAttribute("userType", user.getUsertype()); + if(user.getUsertype().equals(RoleType.Company.name())){ + Company company = companyDao.findByMail(user.getMail()).get(); + if(company == null){ + redirectAttributes.addFlashAttribute("error", "company not found"); + + } + session.setAttribute("loggedInUser", company); + } + else { + Candidate candidate = candidateDao.findById(user.getId()); + if (candidate==null){ + redirectAttributes.addFlashAttribute("error", "candidate not found"); + + } + session.setAttribute("loggedInUser", candidate); } - } - @RequestMapping(value = "/logout", method = RequestMethod.GET) - public String logout(HttpServletRequest request) { - HttpSession session = request.getSession(); - session.setAttribute("uid", null); - session.setAttribute("user", null); return "redirect:/"; } + // GET /logout - Déconnexion + @GetMapping("/logout") + public String logout(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + return "redirect:/login?logout"; + } } \ No newline at end of file diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.java index 9dd7810dde161a8288008d1a61e44efa871557ce..1a812201c0739e4e6a0de08c8087b6d2e7c60610 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.java @@ -20,7 +20,7 @@ public class CompanyConverter implements Converter<String, Company> { if (source == null || source.isEmpty()) { return null; } - return companyService.findById(Long.parseLong(source)).orElse(null); + return companyService.findById(Integer.parseInt(source)).orElse(null); } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.java index 297c3a929854d702a842e4c8d5b3453db9fe2305..7afa498c1cc884e8b7d346df1e8397a0afab3161 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.java @@ -68,9 +68,9 @@ public class AppUserDao { } @Transactional(readOnly = true) - public Long count() { + public int count() { String query = "SELECT COUNT(u) FROM AppUser u"; - return entityManager.createQuery(query, Long.class).getSingleResult(); + return entityManager.createQuery(query, int.class).getSingleResult(); } @Transactional(readOnly = true) diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.java index c9353c60b29ace81689658db64c511516dc58f73..9a5edb22980227589355cac9f7ac225f841e6825 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.java @@ -1,5 +1,6 @@ package fr.atlantique.imt.inf211.jobmngt.dao; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Level; @@ -12,6 +13,7 @@ import fr.atlantique.imt.inf211.jobmngt.entity.Application; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.TypedQuery; +import org.thymeleaf.expression.Lists; @Repository public class ApplicationDao { @@ -52,6 +54,34 @@ public class ApplicationDao { TypedQuery<Application> query = entityManager.createQuery("SELECT a FROM Application a ORDER BY a.appdate DESC", Application.class); return query.getResultList(); } + @Transactional(readOnly = true) + public List<Application> findByCandidateId(int candidateId) { + logger.log(Level.INFO, "Fetching applications for candidate ID: {0}", candidateId); + TypedQuery<Application> query = entityManager.createQuery( + "SELECT a FROM Application a WHERE a.candidate.id = :candidateId ORDER BY a.appdate DESC", + Application.class); + query.setParameter("candidateId", candidateId); + return query.getResultList(); + } + @Transactional(readOnly = true) + public boolean existsByCandidateAndJobOffer(Integer candidateId, Integer jobOfferId) { + TypedQuery<Long> query = entityManager.createQuery( + "SELECT COUNT(a) FROM Application a WHERE a.candidate.id = :candidateId AND a.jobOffer.id = :jobOfferId", + Long.class); + query.setParameter("candidateId", candidateId); + query.setParameter("jobOfferId", jobOfferId); + return query.getSingleResult() > 0; + } + + @Transactional(readOnly = true) + public List<Application> findByJobOfferId(Integer jobOfferId) { + /* TypedQuery<Application> query = entityManager.createQuery( + "SELECT a FROM Application a WHERE a.jobOffer.id = :jobOfferId ORDER BY a.appDate DESC", + Application.class); + query.setParameter("jobOfferId", jobOfferId); + return query.getResultList();*/ + return Collections.emptyList(); + } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.java index 99a93f1d5d21fac7e126b59773dacabb9e627fe1..49da5f96e184c2d375927485484cab4f495438fa 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.java @@ -1,6 +1,7 @@ package fr.atlantique.imt.inf211.jobmngt.dao; import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; +import fr.atlantique.imt.inf211.jobmngt.entity.Company; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.TypedQuery; @@ -17,8 +18,13 @@ public class CandidateDao { private EntityManager entityManager; @Transactional - public void persist(Candidate candidate) { - entityManager.persist(candidate); + public Candidate save(Candidate candidate) { + if (candidate.getId() == 0) { + entityManager.persist(candidate); + return candidate; + } else { + return entityManager.merge(candidate); + } } @Transactional diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.java index f08c8b2e2453cc83173a2a2f546445247ed383fc..d82258a13e4dc4ed767f59d71740760405deeffd 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.java @@ -32,13 +32,13 @@ public class CompanyDao { } @Transactional(readOnly = true) - public Optional<Company> findById(Long id) { // ✅ Changer int en Long + public Optional<Company> findById(int id) { // ✅ Changer int en Long return Optional.ofNullable(entityManager.find(Company.class, id)); } @Transactional -public void remove(Long id) { +public void remove(int id) { Company company = findById(id).orElseThrow(() -> new RuntimeException("Impossible de supprimer : l'entreprise avec ID " + id + " n'existe pas.")); entityManager.remove(company); diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.java index 43676b78d15847fa0d98578c35f0e37581e6a137..be01c5efab19b7b2225da3d8dca59f39ebd3087b 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.java @@ -11,7 +11,7 @@ import fr.atlantique.imt.inf211.jobmngt.entity.Company; import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; @Repository -public interface JobOfferDao extends JpaRepository<JobOffer, Long> { // ✅ Hérite maintenant de JpaRepository +public interface JobOfferDao extends JpaRepository<JobOffer, Integer> { // ✅ Hérite maintenant de JpaRepository @Query("SELECT DISTINCT j FROM JobOffer j " + "JOIN j.sectors s " + @@ -19,13 +19,13 @@ public interface JobOfferDao extends JpaRepository<JobOffer, Long> { // ✅ Hér "WHERE LOWER(s.label) LIKE LOWER(:sectorLabel) " + "AND LOWER(q.label) LIKE LOWER(:qualificationLevel)") List<JobOffer> findBySectorAndQualification(String sectorLabel, String qualificationLevel); -int countByCompanyId(Long companyId); -List<JobOffer> findByCompanyId(Long companyId); - List<JobOffer> findByCompany(Optional<Company> company); +Integer countByCompanyId(int companyId); +List<JobOffer> findByCompanyId(int companyId); +List<JobOffer> findByCompany(Optional<Company> company); + - } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.java index 3e0ec38d43d7c846f149f5ac1aeaa3a7963e637f..c0e0c52e23c5cb94312f66ffec6c615329fa7078 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.java @@ -47,10 +47,10 @@ public class QualificationLevelDao { } @Transactional(readOnly = true) - public Long count() { + public int count() { logger.log(Level.INFO, "Counting all qualification levels"); String query = "SELECT COUNT(q) FROM QualificationLevel q"; - return entityManager.createQuery(query, Long.class).getSingleResult(); + return entityManager.createQuery(query, int.class).getSingleResult(); } @Transactional(readOnly = true) @@ -61,11 +61,11 @@ public class QualificationLevelDao { } @Transactional(readOnly = true) - public List<QualificationLevel> findByLabel(String label) { - logger.log(Level.INFO, "Fetching qualification levels with label: " + label); - String jpql = "SELECT q FROM QualificationLevel q WHERE q.level = :label"; - TypedQuery<QualificationLevel> query = entityManager.createQuery(jpql, QualificationLevel.class); - query.setParameter("label", label); - return query.getResultList(); + public boolean existsByLabel(String label) { + String query = "SELECT COUNT(q) FROM QualificationLevel q WHERE q.label = :label"; + Long count = entityManager.createQuery(query, Long.class) + .setParameter("label", label) + .getSingleResult(); + return count > 0; } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.java index 9cc968d0e0ff8b7cf8e5cca090329a94bc50cc49..6fde34b7e0b3ad980254c14d752b03f13c25245f 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.java @@ -1,6 +1,8 @@ package fr.atlantique.imt.inf211.jobmngt.dao; +import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,13 +47,13 @@ public class SectorDao { } @Transactional(readOnly = true) - public Long count() { + public int count() { logger.log(Level.INFO, "Counting all sectors"); String query = "SELECT COUNT(s) FROM Sector s"; - return entityManager.createQuery(query, Long.class).getSingleResult(); + return entityManager.createQuery(query, Integer.class).getSingleResult(); } - public List<Sector> findAll(String sort, String order) { + public Collection<Sector> findAll(String sort, String order) { if (order == null || (!order.equalsIgnoreCase("ASC") && !order.equalsIgnoreCase("DESC"))) { order = "ASC"; // Définit une valeur par défaut si `order` est null } @@ -72,16 +74,17 @@ public class SectorDao { * ✅ Nouvelle surcharge de findAll() sans paramètres. */ @Transactional(readOnly = true) - public List<Sector> findAll() { + public Collection<Sector> findAll() { logger.log(Level.INFO, "Fetching all sectors"); return entityManager.createQuery("SELECT s FROM Sector s", Sector.class).getResultList(); } + /** * ✅ Nouvelle méthode pour récupérer plusieurs secteurs par ID. */ @Transactional(readOnly = true) - public List<Sector> findAllById(List<Integer> ids) { + public Collection<Sector> findAllById(List<Integer> ids) { logger.log(Level.INFO, "Fetching sectors with IDs: " + ids); return entityManager.createQuery("SELECT s FROM Sector s WHERE s.id IN :ids", Sector.class) .setParameter("ids", ids) @@ -119,7 +122,7 @@ public class SectorDao { } @Transactional(readOnly = true) - public List<Sector> findAllByIds(List<Integer> ids) { + public Collection<Sector> findAllByIds(List<Integer> ids) { logger.log(Level.INFO, "Fetching sectors with IDs: " + ids); String jpql = "SELECT s FROM Sector s WHERE s.id IN :ids"; @@ -127,8 +130,15 @@ public class SectorDao { TypedQuery<Sector> query = entityManager.createQuery(jpql, Sector.class); query.setParameter("ids", ids); - return query.getResultList(); + return query.getResultList(); + } + @Transactional(readOnly = true) + public boolean existsByLabel(String label) { + String query = "SELECT COUNT(s) FROM Sector s WHERE s.label = :label"; + Long count = entityManager.createQuery(query, Long.class) + .setParameter("label", label) + .getSingleResult(); + return count > 0; } - } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.java index 7c0503d4e71cf50043c85ad2131a71c0cc813902..e4b458c8cd6f34b9a9080aca63ed2561df4eed31 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.java @@ -1,18 +1,23 @@ package fr.atlantique.imt.inf211.jobmngt.entity; import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; @Entity @Table(name = "appuser", schema = "public", uniqueConstraints = @UniqueConstraint(columnNames = "mail")) @Inheritance(strategy = InheritanceType.JOINED) // Stratégie d'héritage @DiscriminatorColumn(name = "usertype", discriminatorType = DiscriminatorType.STRING) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") -public class AppUser implements Serializable { +public class AppUser implements Serializable, UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-incrémentation @@ -27,9 +32,8 @@ public class AppUser implements Serializable { @Column(name = "city", length = 30) private String city; - @Enumerated(EnumType.STRING) @Column(name = "usertype", length = 50, insertable = false, updatable = false) - private Role usertype; // Permet d'identifier le type d'utilisateur + private String usertype; // Permet d'identifier le type d'utilisateur // Constructeurs public AppUser() {} @@ -58,10 +62,40 @@ public class AppUser implements Serializable { this.mail = mail; } + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("role" + this.usertype.toUpperCase())); + } + public String getPassword() { return password; } + @Override + public String getUsername() { + return this.mail; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + public void setPassword(String password) { this.password = password; } @@ -74,11 +108,11 @@ public class AppUser implements Serializable { this.city = city; } - public Role getUsertype() { + public String getUsertype() { return usertype; } - public void setUsertype(Role usertype) { + public void setUsertype(String usertype) { this.usertype = usertype; } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Application.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Application.java index 7f585796b1143b76187f43ea020dd4124b0e6720..11e97bbfae4f2ab45cdce4c2c7cf11b875e5302a 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Application.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Application.java @@ -1,8 +1,12 @@ package fr.atlantique.imt.inf211.jobmngt.entity; import java.io.Serializable; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -35,7 +39,6 @@ public class Application implements Serializable { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "candidateid", nullable = false) - @JsonIgnoreProperties({"applications"}) // ✅ Évite la surcharge JSON circulaire private Candidate candidate; @ManyToOne(fetch = FetchType.LAZY) @@ -47,7 +50,7 @@ public class Application implements Serializable { private String cv; @Column(name = "appdate") - private LocalDateTime appdate; + private LocalDate appdate; @ManyToMany(fetch = FetchType.LAZY) @JoinTable( @@ -57,7 +60,7 @@ public class Application implements Serializable { inverseJoinColumns = { @JoinColumn(name = "sectorid", nullable = false, updatable = false) } ) @JsonIgnoreProperties({"applications"}) // ✅ Empêche la surcharge JSON circulaire - private List<Sector> sectors; + private Collection<Sector> sectors = new HashSet<>();; // Constructeurs public Application() {} @@ -82,9 +85,10 @@ public class Application implements Serializable { public String getCv() { return cv; } public void setCv(String cv) { this.cv = cv; } - public LocalDateTime getAppdate() { return appdate; } - public void setAppdate(LocalDateTime appdate) { this.appdate = appdate; } + public LocalDate getAppdate() { return appdate; } + public void setAppdate(LocalDate appdate) { this.appdate = appdate; } + + public Collection<Sector> getSectors() { return sectors; } + public void setSectors(Collection<Sector> sectors) { this.sectors = sectors; } - public List<Sector> getSectors() { return sectors; } - public void setSectors(List<Sector> sectors) { this.sectors = sectors; } } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.java index 8a200c299e2ef52fc2e8c11c611260ffe85f06e0..54464ac0528346c3b9c538c6de6922cf7c536038 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.java @@ -1,11 +1,12 @@ package fr.atlantique.imt.inf211.jobmngt.entity; import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.PrimaryKeyJoinColumn; -import jakarta.persistence.Table; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; @Entity @Table(name = "candidate", schema = "public") @@ -18,6 +19,10 @@ public class Candidate extends AppUser implements Serializable { @Column(name = "firstname", nullable = false, length = 50) private String firstname; + @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @JsonIgnore // Évite la surcharge JSON circulaire + private Collection<Application> applications; + // Constructeurs public Candidate() {} @@ -25,6 +30,7 @@ public class Candidate extends AppUser implements Serializable { super(mail, password, city); this.lastname = lastname; this.firstname = firstname; + this.applications=new HashSet<>(); } // Getters et Setters @@ -43,4 +49,15 @@ public class Candidate extends AppUser implements Serializable { public void setFirstname(String firstname) { this.firstname = firstname; } + public Integer getCandidatAppCount() { + return Math.toIntExact((applications != null) ? (Integer) applications.size() : 0L); + } + public Collection<Application> getCandidatApplications() { + return applications; + } + + public void setCandidatApplications(Set<Application> candidatApplications) { + this.applications = candidatApplications; + } + } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Company.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Company.java index 468813807ed65ce8a8a4e4631c7fd5eaa6bba6cd..0e4593f7a09110efcb97594aa4d55165016da810 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Company.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Company.java @@ -69,8 +69,8 @@ public class Company extends AppUser implements Serializable { this.jobOffers = jobOffers; } - public long getJobOfferCount() { - return (jobOffers != null) ? (long) jobOffers.size() : 0L; + public Integer getJobOfferCount() { + return Math.toIntExact((jobOffers != null) ? (Integer) jobOffers.size() : 0L); } // public int getJobOfferCount() { diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.java index ac444ecdf9401762c558711e5b29cfd34d02b5cb..46123ebdd865ab4bd8d1932c3d5615fd0ff7acd0 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.java @@ -1,8 +1,7 @@ package fr.atlantique.imt.inf211.jobmngt.entity; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; +import java.time.LocalDate; +import java.util.*; import org.springframework.format.annotation.DateTimeFormat; @@ -33,7 +32,7 @@ public class JobOffer implements java.io.Serializable { @SequenceGenerator(name = "JOBOFFER_ID_GENERATOR", sequenceName = "JOBOFFER_ID_SEQ", allocationSize = 1) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "JOBOFFER_ID_GENERATOR") @Column(name="id", unique = true, nullable = false) - private Long id; // ✅ Correct + private int id; // ✅ Correct @ManyToOne(fetch = FetchType.LAZY) @@ -53,7 +52,7 @@ public class JobOffer implements java.io.Serializable { @Temporal(TemporalType.DATE) @DateTimeFormat(pattern = "yyyy-MM-dd") @Column(name="publicationdate") - private Date publicationDate; + private LocalDate publicationDate; @ManyToMany(fetch = FetchType.LAZY) // @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @@ -61,7 +60,7 @@ public class JobOffer implements java.io.Serializable { joinColumns = { @JoinColumn(name="jobofferid", nullable = false, updatable = false) }, inverseJoinColumns = { @JoinColumn(name="sectorid", nullable = false, updatable = false) }) @JsonIgnoreProperties({"applications", "jobOffers"}) // Empêche les boucles JSON infinies - private Set<Sector> sectors = new HashSet<>(); + private Collection<Sector> sectors = new HashSet<>(); // @OneToMany(fetch = FetchType.LAZY, mappedBy = "jobOffer", cascade = CascadeType.ALL) @OneToMany(fetch = FetchType.LAZY, mappedBy = "jobOffer") // Correction du mappedBy @@ -69,15 +68,15 @@ public class JobOffer implements java.io.Serializable { public JobOffer() {} - public JobOffer(Long id, Company company, QualificationLevel qualificationLevel, String title) { + public JobOffer(int id, Company company, QualificationLevel qualificationLevel, String title) { this.id = id; this.company = company; this.qualificationLevel = qualificationLevel; this.title = title; } - public JobOffer(Long id, Company company, QualificationLevel qualificationLevel, String title, - String taskDescription, Date publicationDate, Set<Sector> sectors, Set<OfferMessage> offerMessages) { + public JobOffer(int id, Company company, QualificationLevel qualificationLevel, String title, + String taskDescription, LocalDate publicationDate, Set<Sector> sectors, Set<OfferMessage> offerMessages) { this.id = id; this.company = company; this.qualificationLevel = qualificationLevel; @@ -88,8 +87,8 @@ public class JobOffer implements java.io.Serializable { this.offerMessages = offerMessages; } - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } + public int getId() { return id; } + public void setId(int id) { this.id = id; } public Company getCompany() { return company; } @@ -104,11 +103,11 @@ public class JobOffer implements java.io.Serializable { public String getTaskDescription() { return taskDescription; } public void setTaskDescription(String taskDescription) { this.taskDescription = taskDescription; } - public Date getPublicationDate() { return publicationDate; } - public void setPublicationDate(Date publicationDate) { this.publicationDate = publicationDate; } + public LocalDate getPublicationDate() { return publicationDate; } + public void setPublicationDate(LocalDate publicationDate) { this.publicationDate = publicationDate; } - public Set<Sector> getSectors() { return sectors; } - public void setSectors(Set<Sector> sectors) { this.sectors = sectors; } + public Collection<Sector> getSectors() { return sectors; } + public void setSectors(Collection<Sector> sectors) { this.sectors = sectors; } public Set<OfferMessage> getOfferMessages() { return offerMessages; } public void setOfferMessages(Set<OfferMessage> offerMessages) { this.offerMessages = offerMessages; } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Role.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.java similarity index 50% rename from src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Role.java rename to src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.java index 2b3e13d630a3a66d722dd42481310f986a11789f..85ec68105e718ae23b0106b2d64d7ec0836dcb7b 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/Role.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.java @@ -1,6 +1,6 @@ package fr.atlantique.imt.inf211.jobmngt.entity; -public enum Role { - COMPANY, - CONDIDATE +public enum RoleType { + Company, + Candidate } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.java index 0c810c4612732571164002e79abb585ae4c7218d..6c7af14e44c56f8463637fbc345a0f9942185ffe 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.java @@ -10,7 +10,7 @@ public interface AppUserService { public AppUser getUserapp(Integer id); - public Long nbUsers(); + public int nbUsers(); public Optional<AppUser> checkLogin(AppUser u); } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.java index c498d20925155c6e2bb6528bedafa118fab1458d..5c6cb62399e31cfc81eb81bbdf79dad888cecc20 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.java @@ -23,7 +23,7 @@ public class AppUserServiceImpl implements AppUserService { return appUserDao.findById(id); } - public Long nbUsers() { + public int nbUsers() { return appUserDao.count(); } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.java index 5ce638cd8b49c3e7357cb27ff8636755a139dd53..67312ebb06eadae6649ec797a665a41d94f0289c 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.java @@ -1,6 +1,9 @@ package fr.atlantique.imt.inf211.jobmngt.service; +import fr.atlantique.imt.inf211.jobmngt.entity.Application; import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; +import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; + import java.util.List; import java.util.Optional; @@ -14,4 +17,6 @@ public interface CandidateService { void updateCandidate(Candidate candidate); void deleteCandidate(int id); Optional<Candidate> findById(int id); + List<Application> getApplicationByCandidate(int candidateId); + } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.java index 56b28220200bfc82f0c78fa14bca64d30de6756c..a2c1247eaf4a0788cb917086c4e41bbfac8efca4 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.java @@ -1,8 +1,14 @@ package fr.atlantique.imt.inf211.jobmngt.service; +import fr.atlantique.imt.inf211.jobmngt.dao.ApplicationDao; import fr.atlantique.imt.inf211.jobmngt.dao.CandidateDao; +import fr.atlantique.imt.inf211.jobmngt.entity.Application; import fr.atlantique.imt.inf211.jobmngt.entity.Candidate; +import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; +import fr.atlantique.imt.inf211.jobmngt.entity.RoleType; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,10 +16,12 @@ import java.util.List; import java.util.Optional; @Service +@RequiredArgsConstructor public class CandidateServiceImpl implements CandidateService { - @Autowired - private CandidateDao candidateDao; + final CandidateDao candidateDao; + final ApplicationDao applicationDao; + final PasswordEncoder passwordEncoder; @Override public List<Candidate> getAllCandidates() { @@ -27,7 +35,7 @@ public class CandidateServiceImpl implements CandidateService { @Override public void createCandidate(Candidate candidate) { - candidateDao.persist(candidate); + candidateDao.save(candidate); } @Override @@ -38,8 +46,9 @@ public class CandidateServiceImpl implements CandidateService { if (existingCandidate.isPresent()) { return false; } - - candidateDao.persist(candidate); + candidate.setPassword(passwordEncoder.encode(candidate.getPassword())); + candidate.setUsertype(RoleType.Candidate.name()); + candidateDao.save(candidate); return true; } @@ -70,5 +79,10 @@ public class CandidateServiceImpl implements CandidateService { return Optional.ofNullable(candidate); } + @Override + public List<Application> getApplicationByCandidate(int candidateId) { + return applicationDao.findByCandidateId(candidateId); + } + } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.java index 7a6bd598deb7415cfc018152b314e0161fe32d23..550e70a68c1804c4cd809a54918f234a09db862f 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.java @@ -9,12 +9,12 @@ import java.util.Optional; public interface CompanyService { void saveCompany(Company company); List<Company> getAllCompanies(); - Optional<Company> findById(Long id); + Optional<Company> findById(int id); Optional<Company> findByMail(String mail); // Ajout pour vérifier les doublons void updateCompany(Company company); // Mise à jour d'une entreprise - void deleteCompany(Long id); // Supprimer une entreprise - int countJobOffersByCompany(Long companyId); - List<JobOffer> getJobOffersByCompany(Long companyId); + void deleteCompany(int id); // Supprimer une entreprise + int countJobOffersByCompany(int companyId); + List<JobOffer> getJobOffersByCompany(int companyId); diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.java index 0d6cae8235130f657b019c0a37105fff3d3bd7f9..380915d2c61446625372a06c95471af01b4cd5fc 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.java @@ -3,7 +3,8 @@ package fr.atlantique.imt.inf211.jobmngt.service; import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import fr.atlantique.imt.inf211.jobmngt.dao.CompanyDao; @@ -12,17 +13,13 @@ import fr.atlantique.imt.inf211.jobmngt.entity.Company; import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; @Service +@RequiredArgsConstructor public class CompanyServiceImpl implements CompanyService { - private CompanyDao companyDao; - private final JobOfferDao jobOfferDao; + final CompanyDao companyDao; + final JobOfferDao jobOfferDao; - @Autowired - public CompanyServiceImpl(CompanyDao companyDao, JobOfferDao jobOfferDao) { - this.companyDao = companyDao; - this.jobOfferDao = jobOfferDao; - } @Override public void saveCompany(Company company) { @@ -35,7 +32,7 @@ public class CompanyServiceImpl implements CompanyService { } @Override - public Optional<Company> findById(Long id) { + public Optional<Company> findById(int id) { return companyDao.findById(id); // Assure-toi que companyRepository retourne Optional<Company> } @@ -45,7 +42,7 @@ public class CompanyServiceImpl implements CompanyService { } @Override - public void deleteCompany(Long id) { + public void deleteCompany(int id) { companyDao.remove(id); } @@ -56,16 +53,16 @@ public class CompanyServiceImpl implements CompanyService { } @Override - public int countJobOffersByCompany(Long companyId) { + public int countJobOffersByCompany(int companyId) { return jobOfferDao.countByCompanyId(companyId); } @Override - public List<JobOffer> getJobOffersByCompany(Long companyId) { + public List<JobOffer> getJobOffersByCompany(int companyId) { return jobOfferDao.findByCompanyId(companyId); } - public Company getCompanyById(Long id) { + public Company getCompanyById(int id) { return companyDao.findById(id).orElse(null); } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.java index 06319c3dd2d632e021585cf20d870b194c3f9e7f..0e16e09328ad4a95c3bab00eee0e4b1aca7afc18 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.java @@ -8,10 +8,10 @@ import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; public interface JobOfferService { List<JobOffer> getAllJobOffers(); - List<JobOffer> getJobOffersByCompanyId(Long companyId); // 🔹 Nouvelle méthode - Optional<JobOffer> findById(Long id); // ✅ Correction int → Long + List<JobOffer> getJobOffersByCompanyId(int companyId); // 🔹 Nouvelle méthode + Optional<JobOffer> findById(int id); // ✅ Correction int → Long void saveJobOffer(JobOffer jobOffer); // void updateJobOffer(JobOffer jobOffer); - void deleteJobOffer(Long id); // ✅ Correction int → Long + void deleteJobOffer(int id); // ✅ Correction int → Long List<JobOffer> findByCompany(Company company); } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.java index bfe86f422af4b46639af2bbfb90e2f97be4c0f2e..35e35c2acc864d074740dc4d58d726ec3fb2f489 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.java @@ -26,7 +26,7 @@ public class JobOfferServiceImpl implements JobOfferService { return jobOfferDao.findAll(); } - public Optional<JobOffer> findById(Long id) { + public Optional<JobOffer> findById(int id) { Optional<JobOffer> jobOfferOpt = jobOfferDao.findById(id); System.out.println("🔍 Vérification BDD pour l'offre ID " + id + " : " + (jobOfferOpt.isPresent() ? "Trouvée ✅" : "Non trouvée ❌")); return jobOfferOpt; @@ -36,8 +36,7 @@ public class JobOfferServiceImpl implements JobOfferService { @Override public void saveJobOffer(JobOffer jobOffer) { if (jobOffer.getPublicationDate() == null) { - jobOffer.setPublicationDate(new Date()); - jobOffer.setPublicationDate(new Date()); + jobOffer.setPublicationDate(jobOffer.getPublicationDate()); } jobOfferDao.save(jobOffer); System.out.println(" Enregistrement réussi !"); @@ -52,12 +51,12 @@ public class JobOfferServiceImpl implements JobOfferService { // } @Override - public void deleteJobOffer(Long id) { // ✅ Correction int → Long + public void deleteJobOffer(int id) { // ✅ Correction int → Long jobOfferDao.deleteById(id); } @Override - public List<JobOffer> getJobOffersByCompanyId(Long companyId) { + public List<JobOffer> getJobOffersByCompanyId(int companyId) { return jobOfferDao.findByCompanyId(companyId); // 🔹 Récupère les offres par entreprise } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.java new file mode 100644 index 0000000000000000000000000000000000000000..5a5bffadfe1507fa27a4891a7a59ac6492115d16 --- /dev/null +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.java @@ -0,0 +1,134 @@ +package fr.atlantique.imt.inf211.jobmngt.service; + +import fr.atlantique.imt.inf211.jobmngt.dao.AppUserDao; +import fr.atlantique.imt.inf211.jobmngt.dao.ApplicationDao; +import fr.atlantique.imt.inf211.jobmngt.dao.JobOfferDao; +import fr.atlantique.imt.inf211.jobmngt.dao.SectorDao; +import fr.atlantique.imt.inf211.jobmngt.entity.Application; +import fr.atlantique.imt.inf211.jobmngt.entity.JobOffer; +import fr.atlantique.imt.inf211.jobmngt.entity.QualificationLevel; +import fr.atlantique.imt.inf211.jobmngt.entity.Sector; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MatchingIndexService { + final JobOfferDao jobOfferDao; + final ApplicationDao applicationRepository; + final SectorDao sectorDao; + final QualificationLevelService qualificationLevelService; + private static final Logger logger = LoggerFactory.getLogger(MatchingIndexService.class.getName()); + + @Transactional + public void indexJobOffer(JobOffer jobOffer) { + // Mise à jour des index pour une offre existante + if (jobOffer.getId() != 0) { + JobOffer existing = jobOfferDao.findById(jobOffer.getId()).orElse(null); + if (existing != null) { + updateIndex(existing, false); // Supprime l'ancien index + } + } + updateIndex(jobOffer, true); // Crée le nouvel index + } + + @Transactional + public void indexApplication(Application application) { + // Logique similaire pour les candidatures + if (application.getId() != 0) { + Application existing = applicationRepository.findById(application.getId()).orElse(null); + if (existing != null) { + updateIndex(existing, false); + } + } + updateIndex(application, true); + } + + @Transactional + public void updateIndex(JobOffer jobOffer, boolean add) { + if (add) { + // Sauvegarde des associations existantes (automatique avec CascadeType.MERGE/PERSIST) + // Alternative explicite : + Collection<Sector> sectors = jobOffer.getSectors(); + if (sectors != null) { + sectors.forEach(sector -> { + sector.getJobOffers().add(jobOffer); + sectorDao.persist(sector); + }); + } + } else { + // Nettoyage des anciennes associations + Collection<Sector> oldSectors = jobOffer.getSectors(); + if (oldSectors != null) { + oldSectors.forEach(sector -> { + sector.getJobOffers().remove(jobOffer); + sectorDao.persist(sector); + }); + } + } + } + + @Transactional + public void updateIndex(Application application, boolean add) { + Collection<Sector> sectors = application.getSectors(); + if (sectors == null || sectors.isEmpty()) return; + + for (Sector sector : sectors) { + // 1. Initialiser la collection si nécessaire + if (sector.getApplications() == null) { + sector.setApplications(new HashSet<>()); + } + + // 2. Mettre à jour la relation en mémoire + if (add) { + sector.getApplications().add(application); + } else { + sector.getApplications().remove(application); + } + + // 3. Sauvegarder le secteur via votre DAO + sectorDao.update(sector); // Méthode existante dans votre SectorDao + } + } + @Transactional(readOnly = true) + public List<JobOffer> findMatchingJobOffers(Application application) { + logger.info("Matching application ID {} with sectors: {}", + application.getId(), + application.getSectors().stream().map(Sector::getLabel).collect(Collectors.toList()) + ); + QualificationLevel appQl = application.getQualificationlevel(); + Collection<Sector> appSectors = application.getSectors(); + + return jobOfferDao.findAll().stream() + .filter(offer -> offer.getQualificationLevel().equals(appQl)) + .filter(offer -> !Collections.disjoint(offer.getSectors(), appSectors)) + .sorted(Comparator.comparingInt(offer -> + -countCommonSectors(offer.getSectors(), appSectors))) // Tri par nombre de secteurs communs + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public Collection<Application> findMatchingApplications(JobOffer jobOffer) { + QualificationLevel offerQl = jobOffer.getQualificationLevel(); + Collection<Sector> offerSectors = jobOffer.getSectors(); + + return applicationRepository.findAll().stream() + .filter(app -> app.getQualificationlevel().equals(offerQl)) + .filter(app -> !Collections.disjoint(app.getSectors(), offerSectors)) + .sorted(Comparator.comparingInt(app -> + -countCommonSectors(app.getSectors(), offerSectors))) + .collect(Collectors.toList()); + } + + private int countCommonSectors(Collection<Sector> set1, Collection<Sector> set2) { + return (int) set1.stream() + .filter(set2::contains) + .count(); + } +} diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorService.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorService.java index 185a275c66c1d134704915d23a160d3d353b85d3..5bbc1b911d3aeeea9b18c4132c956afae7c16605 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorService.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorService.java @@ -1,20 +1,21 @@ package fr.atlantique.imt.inf211.jobmngt.service; +import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import fr.atlantique.imt.inf211.jobmngt.entity.Sector; public interface SectorService { - List<Sector> listOfSectors(); // ✅ Ajout de cette méthode + Collection<Sector> listOfSectors(); // ✅ Ajout de cette méthode Optional<Sector> findById(int id); // ✅ Changer int en Long void saveSector(Sector sector); void updateSector(Sector sector); void deleteSector(int id); - List<Sector> getAllSectors(); - List<Sector> getSectorsByIds(List<Integer> sectorIds); - Optional<Sector> findById(long id); + Collection<Sector> getAllSectors(); + Collection<Sector> getSectorsByIds(List<Integer> sectorIds); - public long countSectors(); + public Integer countSectors(); } diff --git a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.java b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.java index b4d86f07622a96caa0f64aad1c5a6d8522cbd416..c42834e870df39d2206ea67d514c83bc99067837 100644 --- a/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.java +++ b/src/main/java/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.java @@ -1,7 +1,9 @@ package fr.atlantique.imt.inf211.jobmngt.service; +import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -16,7 +18,7 @@ public class SectorServiceImpl implements SectorService { private SectorDao sectorDao; @Override - public List<Sector> listOfSectors() { + public Collection<Sector> listOfSectors() { return sectorDao.findAll(); // ✅ Appel de la nouvelle méthode sans arguments } @@ -26,25 +28,20 @@ public class SectorServiceImpl implements SectorService { } @Override - public List<Sector> getAllSectors() { + public Collection<Sector> getAllSectors() { return sectorDao.findAll(null, null); } - public List<Sector> getSectorsByIds(List<Integer> sectorIds) { + public Collection<Sector> getSectorsByIds(List<Integer> sectorIds) { return sectorDao.findAllByIds(sectorIds); } @Override - public long countSectors() { + public Integer countSectors() { return sectorDao.count(); } - @Override - public Optional<Sector> findById(long id) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'findById'"); - } public void saveSector(Sector sector) { sectorDao.save(sector); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a5b3aeddcb74787f0c0ef818477f2760c026df6a..c970441d4741c52eabaf7b4f2dbe76f1cf66c380 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,7 +18,7 @@ spring.jpa.show-sql=true logging.level.root=info logging.level.org.springframework.web=debug spring.jackson.serialization.indent_output=true - +spring.thymeleaf.expose-authentication-attributes=true # Session timeout in seconds server.servlet.session.timeout=10m @@ -29,4 +29,11 @@ jobmngt.admin=007 spring.thymeleaf.cache=false -spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false \ No newline at end of file +spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false + +temp.upload.dir=./temp_uploads + +# Taille max des fichiers +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true diff --git a/src/main/resources/static/css/gyj_imt.css b/src/main/resources/static/css/gyj_imt.css index fae8405c753b39e5943feb2ea79137246e36ac93..4cc3cb88e6e4d3e34484948d256df7d7b40b7287 100644 --- a/src/main/resources/static/css/gyj_imt.css +++ b/src/main/resources/static/css/gyj_imt.css @@ -7,6 +7,9 @@ #header_div{ color: white; background-color: rgb(179, 199, 71); + padding: 10px; + + } #tiny_text{ @@ -20,7 +23,7 @@ #content_div{ background-color: white; - + } #central_div{ padding: 10px; @@ -32,6 +35,15 @@ color: white; background-color: rgb(0, 32, 65); } +.favorite_color { + color: rgb(0, 184, 222); +} +.favorite_back { + background-color: rgb(0, 184, 222); +} +.favorite_outline { + border-color: rgb(0, 184, 222); +} .logo{ width:30px; @@ -148,4 +160,82 @@ .card-black h2 { color: white !important; } +/* Ajoutez ces styles pour améliorer l'interface */ +.navbar { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + + +.alert-info { + background-color: #f8f9fa; + border-left: 4px solid #072262; + color: #072262; +} + +.nav-link { + font-weight: 500; +} + +.btn-outline-light:hover { + color: #072262 !important; +} +/* Dashboard */ +.card { + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: transform 0.3s; +} + +.card:hover { + transform: translateY(-3px); +} + +.card-header { + border-radius: 10px 10px 0 0 !important; +} + +/* Profil */ +dl.row dt { + font-weight: normal; + color: #6c757d; +} + +.list-group-item { + border-left: 3px solid transparent; + transition: all 0.3s; +} + +.list-group-item:hover { + border-left-color: #072262; + background-color: #f8f9fa; +} +/* Style cohérent pour l'interface anglaise */ +.navbar-brand { + font-weight: 600; +} + + +.btn-outline-danger { + border-color: #dc3545; + color: #dc3545; +} +/* Style pour la bannière de bienvenue */ +.welcome-banner { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.welcome-banner:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.welcome-banner i { + font-size: 1.5rem; +} +/* Style pour le nom utilisateur */ +.text-white { + font-size: 1rem; + vertical-align: middle; +} diff --git a/src/main/resources/templates/application/application-confirmation.html b/src/main/resources/templates/application/application-confirmation.html index 49c8ab4c0d24fd87c8419c8f3120fa7036c754d8..aa8fd92be082199e7a0ef77c5be8231d4a35f426 100644 --- a/src/main/resources/templates/application/application-confirmation.html +++ b/src/main/resources/templates/application/application-confirmation.html @@ -1,36 +1,130 @@ -<!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Confirmation de Candidature</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <div class="alert alert-success mt-5 text-center"> - <h2> Votre candidature a été soumise avec succès !</h2> - </div> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Application Confirmation</title> +<section> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <style> + .confirmation-card { + border-radius: 12px; + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08); + border: none; + overflow: hidden; + } + .confirmation-header { + background:#00B8DEFF; + color: white; + padding: 1.8rem; + text-align: center; + } + .confirmation-icon { + font-size: 2.5rem; + margin-bottom: 1rem; + } + .detail-card { + border-left: 4px solid #00B8DEFF; + border-radius: 8px; + margin-bottom: 1.5rem; + } + .detail-item { + padding: 1rem 1.5rem; + border-bottom: 1px solid #f0f0f0; + } + .detail-item:last-child { + border-bottom: none; + } + .sector-badge { + background-color: #e8f0fe; + color: #00B8DEFF; + padding: 6px 12px; + border-radius: 18px; + margin-right: 8px; + margin-bottom: 8px; + display: inline-block; + font-size: 0.85rem; + } + .cv-link i { + margin-right: 8px; + } + .timestamp { + color: #5f6368; + font-size: 0.9rem; + } + </style> + </head> + <body> + <div class="container py-5"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <!-- Confirmation Card --> + <div class="card confirmation-card"> + <div class="confirmation-header"> + <div class="confirmation-icon"> + <i class="fas fa-check-circle"></i> + </div> + <h2>Application Submitted Successfully</h2> + <p class="mb-0">Thank you for your application</p> + </div> - <h3 class="mt-4">📄 Détails de la candidature :</h3> + <div class="card-body p-4"> + <!-- Application Details --> + <div class="card detail-card mb-4"> + <div class="card-body p-0"> + <div class="detail-item"> + <h5 class="text-muted mb-3">Application Details</h5> + <dl class="row"> + <dt class="col-sm-4">Application Number</dt> + <dd class="col-sm-8 font-weight-bold" th:text="${appId}"></dd> - <div th:if="${application != null}"> - <table class="table table-bordered"> - <tr><th>ID</th><td th:text="${application.id != null ? application.id : 'Non spécifié'}"></td></tr> - <tr><th> CV</th><td th:text="${application.cv != null ? application.cv : 'Non spécifié'}"></td></tr> - <tr><th> Niveau de qualification</th> - <td th:text="${application.qualificationlevel != null ? application.qualificationlevel.label : 'Non spécifié'}"></td> - </tr> - <tr><th> Secteurs d'activité</th> - <td th:each="sector : ${application.sectors}" th:text="${sector.label}"></td> - </tr> - <tr><th> Date de dépôt</th> - <td th:text="${application.appdate != null ? application.appdate : 'Non spécifié'}"></td> - </tr> - - </table> - </div> + <dt class="col-sm-4">Submission Date</dt> + <dd class="col-sm-8"> + <span th:text="${#temporals.format(appDate, 'dd MMMM yyyy - HH:mm')}" class="timestamp"></span> + </dd> + </dl> + </div> + </div> + </div> + + <!-- Candidate Information --> + <div class="card detail-card"> + <div class="card-body p-0"> + <div class="detail-item"> + <h5 class="text-muted mb-3">Your Information</h5> + <dl class="row"> + <dt class="col-sm-4">CV Document</dt> + <dd class="col-sm-8"> + <span th:text="${appCv} ?: 'Not specified'" + class="badge favorite_back text-white"></span> + </dd> + + <dt class="col-sm-4">Qualification Level</dt> + <dd class="col-sm-8"> + <span th:text="${qualification?.label} ?: 'Not specified'" + class="badge favorite_back text-white"></span> + </dd> - <a href="/" class="btn btn-primary mt-3"> Retour à l'accueil</a> + <dt class="col-sm-4">Activity Sectors</dt> + <dd class="col-sm-8"> + <div th:each="sector : ${sectors}"> + <span class="sector-badge" th:text="${sector.label}"></span> + </div> + <div th:if="${#lists.isEmpty(sectors)}" class="text-muted"> + No sectors specified + </div> + </dd> + </dl> + </div> + </div> + </div> + + </div> + </div> + </div> + </div> </div> -</body> -</html> + + <!-- Font Awesome for icons --> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/application/application-details.html b/src/main/resources/templates/application/application-details.html index 4a53d5b0d38cedcd9c3949f8e676ea2f5cfa13eb..1f060334a0d5b279a5cef6e7f9dddf3f66760672 100644 --- a/src/main/resources/templates/application/application-details.html +++ b/src/main/resources/templates/application/application-details.html @@ -1,49 +1,113 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <head> - <meta charset="UTF-8"> - <title>Détails de la Candidature</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <title>Applications</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + :root { + --primary: #072262; + --accent: #1ABC9C; + --text-primary: #333333; + --text-secondary: #666666; + --bg-light: #F5F5F5; + --border-light: #E0E0E0; + } + + .favorite_color { + color: var(--primary); + } + + .favorite_back { + background-color: var(--accent); + color: white; + border: none; + } + + .favorite_outline { + border-color: var(--accent); + color: var(--accent); + } + + .favorite_outline:hover { + background-color: var(--accent); + color: white; + } + </style> </head> <body> - <div class="container mt-5"> - <h2 class="mb-4"> Détails de la Candidature</h2> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Application Details</h2> + </header> - <div th:if="${error}" class="alert alert-warning"> - <p th:text="${error}"></p> - </div> + <!-- Alert Messages --> + <div th:if="${error}" class="alert alert-warning alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${error}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-person-badge"></i> Candidate Information + </h4> - <div class="card"> - <div class="card-body"> - <h5 class="card-title"> - Nom: - <span th:if="${application.candidate != null}" th:text="${application.candidate.firstname + ' ' + application.candidate.lastname}"></span> - <span th:unless="${application.candidate != null}">Non disponible</span> - </h5> - - <p class="card-text"> - <span th:if="${application.cv != null}" th:text="'CV: ' + ${application.cv}"></span> - <span th:unless="${application.cv != null}">Non disponible</span> - </p> - - <p class="card-text"> - <span th:if="${application.qualificationlevel != null}" th:text="'Niveau de Qualification: ' + ${application.qualificationlevel.label}"></span> - <span th:unless="${application.qualificationlevel != null}">Non disponible</span> - </p> - - <p class="card-text"> - <span th:each="sector : ${application.sectors}" th:text="'Secteur: ' + ${sector.label}"></span> - <span th:if="${#lists.isEmpty(application.sectors)}">Secteurs non disponibles</span> - </p> - - <p class="card-text"> - <span th:if="${application.appdate != null}" th:text="'Date de dépôt: ' + ${application.appdate}"></span> - <span th:unless="${application.appdate != null}">Non disponible</span> - </p> + <div class="mb-3"> + <label class="form-label fw-semibold">Full Name</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${candidate.firstname + ' ' + candidate.lastname}"></span> + </div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">CV</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${cv}"></span> + </div> + </div> + <div class="mb-3"> + <label class="form-label fw-semibold">Application Date</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${#temporals.format(appdate, 'MMMM dd, yyyy')}"></span> + </div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-award"></i> Qualifications + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Qualification Level</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${qualification.label}"></span> + </div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Sectors</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:each="sector : ${sectors}" class="badge favorite_back me-1 mb-1" + th:text="${sector.label}"></span> + </div> + </div> + </div> </div> </div> - - <a th:href="@{/applications/list}" class="btn btn-primary mt-3">Retour à la liste des candidatures</a> </div> + + <footer class="d-flex justify-content-between mt-4"> + <a th:href="@{/applications/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to Applications List + </a> + </footer> +</section> </body> </html> \ No newline at end of file diff --git a/src/main/resources/templates/application/application-edit.html b/src/main/resources/templates/application/application-edit.html index f68c9eca601803bc0fefcd04d0de283c53478c41..6a3bb686d472ca1cce48b3a2c36144cdab9926e8 100644 --- a/src/main/resources/templates/application/application-edit.html +++ b/src/main/resources/templates/application/application-edit.html @@ -1,17 +1,240 @@ -<form th:action="@{/applications/update}" method="post"> - <input type="hidden" name="id" th:value="${application.id}"> - <label>CV :</label> - <input type="text" name="cv" th:value="${application.cv}" required> - - <label>Niveau de qualification :</label> - <select name="qualificationLevel"> - <option th:each="q : ${qualifications}" th:value="${q.id}" th:text="${q.label}"></option> - </select> - - <label>Secteurs :</label> - <select name="sectors" multiple> - <option th:each="s : ${sectors}" th:value="${s.id}" th:text="${s.label}"></option> - </select> - - <button type="submit">Modifier</button> -</form> + <!DOCTYPE html> + <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> + <title>Edit Job Application</title> + <section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: #00B8DEFF; + color: white; + border-color: #00B8DEFF; + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .file-upload { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s; + } + .file-upload:hover { + border-color: #00B8DEFF; + background-color: #f8f9fa; + } + .file-upload i { + font-size: 2rem; + color: #6c757d; + } + .file-name { + margin-top: 10px; + font-weight: bold; + color: #00B8DEFF; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + Edit Job Application + </h2> + <form th:action="@{/applications/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${id}" /> + <!-- Improved CV Field --> + <div class="mb-3"> + <label class="form-label">Resume*</label> + <div class="file-upload" onclick="document.getElementById('cvFile').click()"> + <i class="bi bi-upload"></i> + <div id="cvText">Click to upload your resume</div> + <div id="fileName" class="file-name d-none"></div> + <input type="file" id="cvFile" class="d-none bold" accept=".pdf,.doc,.docx"> + <input type="hidden" id="cv" name="cv" th:text="${cv}" required> + </div> + <small class="text-muted">Accepted formats: PDF, DOC, DOCX</small> + <div class="invalid-feedback">Please upload your resume</div> + </div> + + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${appDate != null} ? ${#temporals.format(appDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + + <!-- Qualification Level --> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualifications}" + th:value="${level.id}" + th:text="${level.label}" + th:selected="${qualApp?.id == level.id}"> + </option> + </select> + </div> + + <!-- Industry Sectors - Version corrigée --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + + <!-- Liste complète des secteurs --> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:classappend="${#lists.contains(sectorApp.![id], sector.id)} ? 'selected' : ''" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + + <!-- Champ affichant les secteurs sélectionnés --> + <div id="selectedSectorsDisplay" class="mb-2"> + <span th:if="${sectorApp.empty}">No sectors selected</span> + <th:block th:each="sector : ${sectorApp}"> + <div th:data-id="${sector.id}"> + <span th:text="${sector.label}"></span> + </div> + </th:block> + </div> + + <input type="hidden" id="sectorIds" name="sectorIds" + th:value="${#strings.listJoin(sectorApp.![id], ',')}" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> + + <!-- Buttons --> + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/applications/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Save Changes + </button> + </div> + </form> + </div> + </div> + + <script> + // Initialize with already selected sectors from the application object + const selectedSectors = new Set( + document.getElementById('sectorIds').value.split(',').filter(Boolean) + ); + + // Mark selected sectors on page load + document.addEventListener('DOMContentLoaded', function() { + updateSelectedDisplay(); + + // Mark initially selected sectors in the list + selectedSectors.forEach(sectorId => { + const element = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (element) { + element.classList.add('selected'); + } + }); + }); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + const sectorLabel = element.textContent; + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectorsDisplay'); + const hiddenInput = document.getElementById('sectorIds'); + + // Clear the display + displayDiv.innerHTML = ''; + + if (selectedSectors.size === 0) { + displayDiv.innerHTML = '<span>No sectors selected</span>'; + } else { + // Add selected sectors as tags + selectedSectors.forEach(sectorId => { + const sectorElement = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (sectorElement) { + const tag = document.createElement('div'); + tag.setAttribute('data-id', sectorId); + tag.innerHTML = `<span>${sectorElement.textContent}</span>`; + displayDiv.appendChild(tag); + } + }); + } + + // Update hidden input with selected sector IDs + hiddenInput.value = Array.from(selectedSectors).join(','); + } + + + + // File upload handling + document.getElementById('cvFile').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + document.getElementById('cvText').classList.add('d-none'); + document.getElementById('fileName').textContent = file.name; + document.getElementById('fileName').classList.remove('d-none'); + document.getElementById('cv').value = file.name; + } + }); + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> + </section> + </html> \ No newline at end of file diff --git a/src/main/resources/templates/application/application-list.html b/src/main/resources/templates/application/application-list.html index 2200f3182c91b13442415c476cc118d9565b44c9..2bdac216bc0d8f9774dcfe73ddbbbcc0bf3a6e37 100644 --- a/src/main/resources/templates/application/application-list.html +++ b/src/main/resources/templates/application/application-list.html @@ -1,75 +1,133 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Liste des Candidatures</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container mt-5"> - <h2 class="mb-4">Liste des Candidatures</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Applications</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .application-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .application-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .sector-badge { + margin-right: 4px; + margin-bottom: 4px; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> + </head> + <body class="bg-light"> - <div th:if="${error}" class="alert alert-warning"> - <p th:text="${error}"></p> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-file-earmark-text me-2"></i> Job Applications + </h2> </div> - <table class="table table-bordered table-striped" th:if="${applicationsList}"> - <thead> - <tr> - <th>ID</th> - <th>Nom</th> - <th>Prénom</th> - <th>CV</th> - <th>Niveau de Qualification</th> - <th>Secteurs d'activité</th> - <th>Date de dépôt</th> - <th>Détails</th> - <th>Modifier</th> - <th>Supprimer</th> - </tr> - </thead> - <tbody> - <tr th:each="app : ${applicationsList}"> - <td th:text="${app.id}"></td> - <td th:text="${app.candidate.firstname}"></td> - <td th:text="${app.candidate.lastname}"></td> - <td><a th:href="@{${app.cv}}" th:text="${app.cv}"></a></td> - <td th:text="${app.qualificationlevel.label}"></td> - <td> - <ul> - <li th:each="sector : ${app.sectors}" th:text="${sector.label}"></li> - </ul> - </td> - <td th:text="${app.appdate}"></td> - <td> - <a th:href="@{/applications/details/{id}(id=${app.id})}" class="btn btn-info">Détails</a> - </td> - <!-- Vérification que l'utilisateur est connecté et est le propriétaire --> - <td th:if="${session.uid != null && session.uid == app.candidate.id}"> - <a th:href="@{/applications/edit/{id}(id=${app.id})}" class="btn btn-warning">Modifier</a> - </td> - <td th:if="${session.uid != null && session.uid == app.candidate.id}"> - <a th:href="@{/applications/delete/{id}(id=${app.id})}" class="btn btn-danger" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette candidature ?');"> - Supprimer - </a> - </td> - <!-- Affichage d'un message si l'utilisateur n'est pas connecté --> - <td th:if="${session.uid == null}" colspan="2"> - <a th:href="@{/login}" class="btn btn-secondary">Se connecter</a> - </td> - </tr> - </tbody> - </table> + <!-- Applications Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(applicationsList)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">First Name</th> + <th>Last Name</th> + <th>Qualification</th> + <th>Sectors</th> + <th>Applied Date</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="app : ${applicationsList}" class="application-card"> + <td class="ps-4 fw-semibold" th:text="${app.candidate.firstname}"></td> + <td th:text="${app.candidate.lastname}"></td> + <td th:text="${app.qualificationlevel?.label} ?: 'N/A'"></td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${app.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${#temporals.format(app.appdate, 'yyyy-MM-dd')}"></td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/applications/details/' + ${app.id}}" + class="btn btn-sm favorite_outline"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == app.candidate.id}"> + <a th:href="@{'/applications/edit/' + ${app.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/applications/delete/' + ${app.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this application?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> - <a th:href="@{/}" class="btn btn-primary mt-3">Retour à l'accueil</a> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(applicationsList)}" class="text-center py-5"> + <i class="bi bi-file-earmark-x empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No applications found</h4> + </div> + </div> + </div> + </div> </div> -</body> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> </html> +<!-- +</td> +<td> + <a th:href="@{/applications/details/{id}(id=${app.id})}" class="btn btn-info">Détails</a> +</td> + +<td th:if="${session.uid != null && session.uid == app.candidate.id}"> + <a th:href="@{/applications/edit/{id}(id=${app.id})}" class="btn btn-warning">Modifier</a> +</td> +<td th:if="${session.uid != null && session.uid == app.candidate.id}"> + <a th:href="@{/applications/delete/{id}(id=${app.id})}" class="btn btn-danger" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette candidature ?');"> + Supprimer + </a> +</td> +<td th:if="${session.uid == null}" colspan="2"> + <a th:href="@{/login}" class="btn btn-secondary">Se connecter</a> +</td> +!--> diff --git a/src/main/resources/templates/application/application-update-form.html b/src/main/resources/templates/application/application-update-form.html index 26266c1eb803c2a61e4fa4d794429159bb970af7..3eefb8c3042e099ffd27c19e822afd349f987ff4 100644 --- a/src/main/resources/templates/application/application-update-form.html +++ b/src/main/resources/templates/application/application-update-form.html @@ -1,6 +1,8 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Applications</title> +<section> + <head> <meta charset="UTF-8"> <title>Mettre à jour la Candidature</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> @@ -12,7 +14,7 @@ <form th:action="@{/applications/update/{id}(id=${application.id})}" method="post"> <div class="form-group"> <label for="cv">CV</label> - <input type="text" id="cv" name="cv" class="form-control" th:value="${application.cv}" required> + <input type="text" id="cv" name="cvPath" class="form-control" th:value="${application.cv}" required> </div> <div class="form-group"> @@ -37,10 +39,11 @@ </select> </div> - <button type="submit" class="btn btn-primary mt-3">Mettre à jour</button> + <button type="submit" class="btn favorite_back mt-3">Mettre à jour</button> </form> <a th:href="@{/applications/list}" class="btn btn-secondary mt-3">Retour à la liste des candidatures</a> </div> </body> +</section> </html> diff --git a/src/main/resources/templates/application/apply.html b/src/main/resources/templates/application/apply.html index 4615526e200b1aff678ccab99012d1ea5eb414a7..66f297c302e5475ecda82aa0793b4ead4a4db82e 100644 --- a/src/main/resources/templates/application/apply.html +++ b/src/main/resources/templates/application/apply.html @@ -1,74 +1,192 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Postuler à une offre</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> - <style> - body { - background-color: #f8f9fa; /* Couleur de fond */ - } - .container { - max-width: 600px; - margin-top: 50px; - background: #ffffff; - padding: 20px; - border-radius: 10px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); - } - h2 { - text-align: center; - color: #007bff; /* Bleu Bootstrap */ - } - label { - font-weight: bold; - color: #343a40; - } - .form-control { - margin-bottom: 10px; - } - .btn-submit { - background-color: #007bff; - color: white; - width: 100%; - } - .btn-submit:hover { - background-color: #0056b3; - } - </style> -</head> -<body> - <div class="container"> - <h2> Postuler à une offre</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Job Application</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: #0d6efd; + color: white; + border-color: #0d6efd; + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .file-upload { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s; + } + .file-upload:hover { + border-color: #0d6efd; + background-color: #f8f9fa; + } + .file-upload i { + font-size: 2rem; + color: #6c757d; + } + .file-name { + margin-top: 10px; + font-weight: bold; + color: #0d6efd; + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-person-plus me-2"></i> + Apply for Job + </h2> - <form th:action="@{/applications/apply}" method="post"> - <!-- Champ CV --> - <div class="form-group"> - <label for="cv">CV :</label> - <input type="text" id="cv" name="cv" class="form-control" placeholder="Lien vers votre CV" required> - </div> + <form th:action="@{/applications/apply}" method="post" class="needs-validation" novalidate> + <!-- Improved CV Field --> + <div class="mb-3"> + <label class="form-label">Resume*</label> + <div class="file-upload" onclick="document.getElementById('cvFile').click()"> + <i class="bi bi-upload"></i> + <div id="cvText">Click to upload your resume</div> + <div id="fileName" class="file-name d-none"></div> + <input type="file" id="cvFile" class="d-none" accept=".pdf,.doc,.docx"> + <input type="hidden" id="cv" name="cv" required> + </div> + <small class="text-muted">Accepted formats: PDF, DOC, DOCX</small> + <div class="invalid-feedback">Please upload your resume</div> + </div> - <!-- Niveau de qualification --> - <div class="form-group"> - <label for="qualificationLevel">Niveau de qualification :</label> - <select id="qualificationLevel" name="qualificationLevel" class="form-control" required> - <option value="">Choisir un niveau</option> - <option th:each="qualification : ${qualifications}" th:value="${qualification.id}" th:text="${qualification.label}"></option> - </select> - </div> + <!-- Qualification Level --> + <div class="mb-3"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="qualification : ${qualifications}" + th:value="${qualification.id}" + th:text="${qualification.label}"></option> + </select> + </div> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${application.appdate != null} ? ${#temporals.format(jobOffer.appdate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> - <!-- Secteurs d'activité --> - <div class="form-group"> - <label for="sectors">Secteurs d'activité :</label> - <select id="sectors" name="sectors" class="form-control" multiple required> - <option th:each="sector : ${sectors}" th:value="${sector.id}" th:text="${sector.label}"></option> - </select> - <small class="form-text text-muted">Maintenez la touche Ctrl (Cmd sur Mac) pour sélectionner plusieurs options.</small> - </div> + <!-- Improved Industry Sectors Selection --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + <div id="selectedSectors" class="mb-2">No sectors selected</div> + <input type="hidden" id="sectorIds" name="sectors" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> - <!-- Bouton de soumission --> - <button type="submit" class="btn btn-submit"> Soumettre</button> - </form> + <!-- Buttons --> + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/jobs}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn favorite_back"> + <i class="bi bi-send me-1"></i> Submit Application + </button> + </div> + </form> + </div> </div> -</body> -</html> + + <script> + // Sector selection management + const selectedSectors = new Set(); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectors'); + const hiddenInput = document.getElementById('sectorIds'); + + if (selectedSectors.size === 0) { + displayDiv.textContent = 'No sectors selected'; + hiddenInput.value = ''; + } else { + displayDiv.textContent = Array.from(selectedSectors).map(id => { + return document.querySelector(`.sector-item[data-id="${id}"]`).textContent; + }).join(', '); + + hiddenInput.value = Array.from(selectedSectors).join(','); + } + } + + // File upload handling + document.getElementById('cvFile').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + document.getElementById('cvText').classList.add('d-none'); + document.getElementById('fileName').textContent = file.name; + document.getElementById('fileName').classList.remove('d-none'); + document.getElementById('cv').value = file.name; + } + }); + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/baseTemplate/base.html b/src/main/resources/templates/baseTemplate/base.html index f480ca2cf07d454514c49b5be5e8b86d5a3d49f8..264b500da1438921058df091ac1acc1c00b07dc7 100644 --- a/src/main/resources/templates/baseTemplate/base.html +++ b/src/main/resources/templates/baseTemplate/base.html @@ -7,44 +7,64 @@ <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <link th:href="@{/css/gyj_imt.css}" rel="stylesheet"> <link th:href="@{/css/bootstrap-icons.min.css}" rel="stylesheet"> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> - + <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script th:src="@{/js/bootstrap.bundle.min.js}"></script> <script th:src="@{/js/gyj_imt.js}"></script> <script src="http://localhost:35729/livereload.js"></script> - <link rel="icon" th:href="@{/img/favicon-32x32.png}" sizes="32x32" type="image/png"> <title th:replace="${title}">IMT Atlantique: Get Your Job</title> + <style> + html, body { + height: 100%; + } + + body { + display: flex; + flex-direction: column; + } + + main { + flex: 1 0 auto; + padding-bottom: 2rem; /* Espace avant le footer */ + } + footer { + flex-shrink: 0; + background-color: #f8f9fa; + padding: 1rem 0; + } + </style> </head> -<body> - <nav th:insert="~{/baseTemplate/nav :: fheader}" /> - <main class="container"> - <th:block th:replace="${content}"> - Empty Page - </th:block> - <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="exampleModalLabel">Confirmation of deletion</h5> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="modal-body"> - <p class="alert alert-danger">Are you sure you want to <span id="delRecord"> </span> ?</p> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> - <a href="" class="btn btn-danger" id="delRef">Delete</a> - </div> +<body class="d-flex flex-column min-vh-100"> +<nav th:insert="~{/baseTemplate/nav :: fheader}" /> + +<main class="container flex-grow-1"> + <th:block th:replace="${content}"> + Empty Page + </th:block> + <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">Confirmation of deletion</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <p class="alert alert-danger">Are you sure you want to <span id="delRecord"> </span> ?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <a href="" class="btn btn-danger" id="delRef">Delete</a> </div> </div> </div> - </main> - <footer class="mt-4" th:replace="~{/baseTemplate/footer:: footer}" /> + </div> +</main> + +<footer class="mt-auto py-3 bg-light" th:replace="~{/baseTemplate/footer:: footer}" /> </body> </html> \ No newline at end of file diff --git a/src/main/resources/templates/baseTemplate/footer.html b/src/main/resources/templates/baseTemplate/footer.html index bfe41e52f59372904d60a452cc5858c9c2b2a4c5..a6172ccda86d50607e66223d1862ec362fddb8d4 100644 --- a/src/main/resources/templates/baseTemplate/footer.html +++ b/src/main/resources/templates/baseTemplate/footer.html @@ -1,5 +1,5 @@ <footer th:fragment="ffooter"> - <div id="footer_div" class="row"> + <div id="footer_div" class="row"> <div class="col"> IMT Atlantique - FIP </div> diff --git a/src/main/resources/templates/baseTemplate/head.html b/src/main/resources/templates/baseTemplate/head.html index 28f9bec37142655cc47cfd20d30a8197ef211892..e60a2d4d353c7526f7d35531bbc583c83a9a9aa3 100644 --- a/src/main/resources/templates/baseTemplate/head.html +++ b/src/main/resources/templates/baseTemplate/head.html @@ -18,4 +18,6 @@ <title>IMT Atlantique: Get Your Job</title> </head> <body> + </body> + </html> </div> \ No newline at end of file diff --git a/src/main/resources/templates/baseTemplate/nav.html b/src/main/resources/templates/baseTemplate/nav.html index 36532658f8e5cad9ec42eda3dd4815dcf37db6ef..08e5b977f01fce4b3bc72bc6a5f93630b013565a 100644 --- a/src/main/resources/templates/baseTemplate/nav.html +++ b/src/main/resources/templates/baseTemplate/nav.html @@ -1,31 +1,20 @@ -//header.html <div th:fragment="fheader"> <div id="header_div" class="row h-10"> <div class="col-2 align-self-start"> <img th:src="@{/img/logo_imt.png}" /> </div> - <div class="col-6 align-self-center"></div> - <div class="col-2 align-self-end"> - <p th:if="${#ctx.session.uid} != null"> - <i class="bi bi-" style="font-size: 2rem; color: white;" th:title="${#ctx.session.usertype}" - th:attrappend="class=${#ctx.session.usertype=='company'?'buildings':'person'}"></i> - <span th:text="${#ctx.session.user.mail}" class="tiny_text" /> - </p> - </div> - - <div class="col-2 align-self-end"> - <th:block th:if="${#ctx.session.uid} != null"> - <a th:if="${#ctx.session.hasMessages == true}" th:href="@{/messages}" title="access to your message"><i - class="bi bi-envelope-at" style="font-size: 2rem; color: white;"></i></a> - <a href="/logout" title="logout from the webapp"><i class="bi bi-box-arrow-in-up" - style="font-size: 2rem; color: white;"></i></a> - </th:block> - <a th:if="${#ctx.session.uid} == null" title="Click to login" href="/login"> - <i class="bi bi-box-arrow-in-down" style="font-size: 2rem; color: white;" - title="login to access your data"></i> + <div class="col-8"></div> + <div class="col-2 text-end "> + <!-- Bouton de connexion/déconnexion seulement --> + <div th:if="${session.loggedInUser != null}"> + <a href="/logout" class="btn text-white fw-bold "> + <i class="bi bi-box-arrow-right"></i> Logout + </a> + </div> + <a th:unless="${session.loggedInUser != null}" href="/login" class="btn text-white fw-bold"> + <i class="bi bi-box-arrow-right"></i> Login </a> </div> - </div> <nav class="navbar navbar-expand-md navbar-light"> <div class="container-fluid"> @@ -41,24 +30,22 @@ <a class="nav-link" th:href="@{/companies}">Companies</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/jobs}">Jobs</a> + <a class="nav-link" th:href="@{/candidates/list}">Candidates</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/candidates/list}">Candidates</a> + <a class="nav-link" th:href="@{/jobs}">Job Offers</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/applications/list}">Applications</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/qualificationLevels}">Qualification levels</a> + <a class="nav-link" th:href="@{/sectors}">Sectors</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/sectors}">Sectors</a> + <a class="nav-link" th:href="@{/qualificationLevels}">Qualification Levels</a> </li> </ul> - </div> </div> </nav> - </div> \ No newline at end of file diff --git a/src/main/resources/templates/candidate/candidates-list.html b/src/main/resources/templates/candidate/candidates-list.html index 039447c90e40d3dca0a29cd98e7432b9eb005b83..283fddec8b427e79d231b5d2412e12ba63ea177e 100644 --- a/src/main/resources/templates/candidate/candidates-list.html +++ b/src/main/resources/templates/candidate/candidates-list.html @@ -1,44 +1,106 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Liste des Candidats</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <h2 class="mt-5">Liste des Candidats</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <head> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .candidate-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .candidate-card:hover { + transform: translateY(-2px); + border-left-color: #3a7bd5; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .actions-cell { + white-space: nowrap; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> + </head> + <body class="bg-light"> - <!-- Message d'erreur --> - <div th:if="${error}" class="alert alert-danger" th:text="${error}"></div> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-people me-2"></i> Candidates List + </h2> - <table class="table table-striped mt-3"> - <thead> - <tr> - <th>ID</th> - <th>Nom</th> - <th>Prénom</th> - <th>Ville</th> - <th>Email</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="candidate : ${candidates}"> - <td th:text="${candidate.id}"></td> - <td th:text="${candidate.lastname}"></td> - <td th:text="${candidate.firstname}"></td> - <td th:text="${candidate.city}"></td> - <td th:text="${candidate.mail}"></td> - <td> - <a th:href="@{/candidates/details/{id}(id=${candidate.id})}" class="btn btn-info btn-sm">Détails</a> - <span th:if="${#ctx.session.uid} != null and ${#ctx.session.uid} == ${candidate.id} ">PEUT SUPPRIMER</span> - </td> - </tr> - </tbody> - </table> + </div> - <a th:href="@{/}" class="btn btn-secondary mt-3">Retour</a> + + <!-- Candidates Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(candidates)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">Last Name</th> + <th>First Name</th> + <th>City</th> + <th>Applications Submitted</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="candidate : ${candidates}" class="candidate-card"> + <td class="ps-4 fw-semibold" th:text="${candidate.lastname}"></td> + <td th:text="${candidate.firstname}"></td> + <td> + <i class="bi bi-geo-alt text-secondary me-1"></i> + <span th:text="${candidate.city}"></span> + </td> + <td> + <span class="badge favorite_back rounded-pill" + th:text="${candidate.getCandidatAppCount()}"></span> + </td> + <td class="text-end pe-4 actions-cell"> + <div > + <a th:href="@{/candidates/details/{id}(id=${candidate.id})}" + class="btn btn-sm favorite_outline" + aria-label="View candidate details"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == candidate.id}"> + <a th:href="@{'/candidates/edit/' + ${candidate.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/candidates/delete/' + ${candidate.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this candidate?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(candidates)}" class="text-center py-5"> + <i class="bi bi bi-people empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No candidates registered</h4> + </div> + </div> + </div> + </div> </div> -</body> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> </html> diff --git a/src/main/resources/templates/candidate/confirmation.html b/src/main/resources/templates/candidate/confirmation.html index 8a22d725d2dd69e9120247d20355b72a469b08a1..bd630bfca8b700513c24eaf217c1be4aa1dda57c 100644 --- a/src/main/resources/templates/candidate/confirmation.html +++ b/src/main/resources/templates/candidate/confirmation.html @@ -1,34 +1,97 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Confirmation</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Confirmation</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .confirmation-card { + max-width: 800px; + margin: 2rem auto; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); + border: none; + } + .confirmation-header { + background: rgb(0, 184, 222); + color: white; + padding: 2rem; + text-align: center; + } + .confirmation-icon { + font-size: 3.5rem; + margin-bottom: 1rem; + } + .info-card { + border-left: 4px solid #00B8DEFF; + background-color: #f8f9fa; + } + .btn-custom { + background: rgb(0, 184, 222); + border: none; + padding: 10px 25px; + font-weight: 500; + transition: all 0.3s; + } + .btn-custom:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4); + } + </style> + </head> + <body class="bg-light"> <div class="container"> - <div class="alert alert-success mt-5 text-center"> - <h2>Operation Successful</h2> - <p th:text="${message}"></p> - </div> + <div class="card confirmation-card"> + <div class="confirmation-header"> + <i class="bi bi-check-circle-fill confirmation-icon"></i> + <h2 class="mb-3">Opération réussie</h2> + <p class="lead mb-0" th:text="${message}"></p> + </div> - <!-- Affichage des infos du candidat uniquement si c'est une modification --> - <div th:if="${candidate != null}"> - <h3>Informations mises à jour :</h3> - <table class="table table-bordered"> - <tr><th>ID</th><td th:text="${candidate.id}"></td></tr> - <tr><th>Nom</th><td th:text="${candidate.lastname}"></td></tr> - <tr><th>Prénom</th><td th:text="${candidate.firstname}"></td></tr> - <tr><th>Email</th><td th:text="${candidate.mail}"></td></tr> - <tr><th>Ville</th><td th:text="${candidate.city}"></td></tr> - </table> - </div> + <div class="card-body p-4"> + <!-- Affichage des infos du candidat --> + <div th:if="${candidate != null}" class="mb-4"> + <h4 class="mb-3 text-primary"> + <i class="bi bi-person-badge me-2"></i>Informations mises à jour + </h4> + + <div class="row"> + <div class="col-md-6 mb-3"> + <div class="p-3 info-card"> + <h6 class="text-muted">Identité</h6> + <p class="mb-1"><strong th:text="${candidate.firstname + ' ' + candidate.lastname}"></strong></p> + <small class="text-muted" th:text="'ID: ' + ${candidate.id}"></small> + </div> + </div> + + <div class="col-md-6 mb-3"> + <div class="p-3 info-card"> + <h6 class="text-muted">Coordonnées</h6> + <p class="mb-1" th:text="${candidate.mail}"></p> + <p class="mb-0" th:text="${candidate.city}"></p> + </div> + </div> + </div> + </div> - <!-- Bouton différent selon la situation --> - <div class="text-center"> - <a th:if="${candidate != null}" th:href="@{/candidates/list}" class="btn btn-primary">Retour à la liste des candidats</a> - <a th:if="${candidate == null}" th:href="@{/login}" class="btn btn-primary">Go to Login</a> + <!-- Bouton d'action --> + <div class="text-center mt-4"> + <a th:if="${candidate != null}" + th:href="@{/candidates/list}" + class="btn btn-custom text-white"> + <i class="bi bi-arrow-left me-2"></i>Retour à la liste + </a> + <a th:if="${candidate == null}" + th:href="@{/login}" + class="btn btn-custom text-white"> + <i class="bi bi-box-arrow-in-right me-2"></i>Se connecter + </a> + </div> + </div> </div> </div> -</body> -</html> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/candidate/confirmationSupp.html b/src/main/resources/templates/candidate/confirmationSupp.html index a8ebbf083b54a7d5ec3e83107bcd51533c418614..9563393fc887f28959c4e4fdc444423113b14733 100644 --- a/src/main/resources/templates/candidate/confirmationSupp.html +++ b/src/main/resources/templates/candidate/confirmationSupp.html @@ -12,7 +12,7 @@ <p th:text="${message}"></p> </div> - <a th:href="@{/candidates/list}" class="btn btn-primary">Retour à la liste des candidats</a> + <a th:href="@{/candidates/list}" class="btn favorite_back">Retour à la liste des candidats</a> </div> </body> </html> diff --git a/src/main/resources/templates/candidate/details.html b/src/main/resources/templates/candidate/details.html index 7e2ea97e99c78a87d1b2d7395335abc4398d9579..9e7c5948f0ea08debc6319fc54d49fac92d82420 100644 --- a/src/main/resources/templates/candidate/details.html +++ b/src/main/resources/templates/candidate/details.html @@ -1,25 +1,114 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Détails du Candidat</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <h2 class="mt-5">Détails du Candidat</h2> - - <table class="table table-bordered mt-3"> - <tr><th>ID</th><td th:text="${candidate.id}"></td></tr> - <tr><th>Nom</th><td th:text="${candidate.lastname}"></td></tr> - <tr><th>Prénom</th><td th:text="${candidate.firstname}"></td></tr> - <tr><th>Email</th><td th:text="${candidate.mail}"></td></tr> - <tr><th>Ville</th><td th:text="${candidate.city}"></td></tr> - </table> - - <a th:href="@{/candidates/edit/{id}(id=${candidate.id})}" class="btn btn-warning">Modifier</a> - <a th:href="@{/candidates/delete/{id}(id=${candidate.id})}" class="btn btn-danger" onclick="return confirm('Voulez-vous vraiment supprimer ce candidat ?')">Supprimer</a> - <a th:href="@{/candidates/list}" class="btn btn-secondary">Retour</a> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Candidate Profile</h2> + </header> + + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> -</body> -</html> + + <div th:if="${candidate != null}" class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-person-badge"></i> Personal Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">First Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.firstname}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Last Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.lastname}"></div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.mail}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.city}"></div> + </div> + </div> + </div> + + <!-- Job Applications Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-file-earmark-text"></i> Job Applications + </h4> + + <div th:if="${#lists.isEmpty(candidateApplications)}" class="alert alert-info"> + <i class="bi bi-info-circle"></i> No job applications found + </div> + + <div th:unless="${#lists.isEmpty(candidateApplications)}" class="table-responsive"> + <table class="table table-hover align-middle"> + <thead class="table-light"> + <tr> + <th>Application ID</th> + <th>CV Reference</th> + <th>Qualification</th> + <th>Sectors</th> + <th>Applied Date</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="app : ${candidateApplications}"> <!-- Changed from application to app --> + <td th:text="${app.id}"></td> + <td th:text="${app.cv}"></td> + <td th:text="${app.qualificationlevel.label}"></td> + <td> + <div th:each="sector : ${app.sectors}" class="badge favorite_back me-1 mb-1" + th:text="${sector.label}"></div> + </td> + <td th:text="${#temporals.format(app.appdate, 'yyyy-MM-dd')}"></td> + <td class="text-end"> + <span th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == candidate.id}" > + <a th:href="@{'/applications/details/' + ${app.id}}" + class="btn btn-sm btn-outline-secondary me-1"> + <i class="bi bi-eye"></i> View + </a> + <a th:href="@{'/applications/edit/' + ${app.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/applications/delete/' + ${app.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this application?');"> + <i class="bi bi-trash"></i> Delete + </a> + </span> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/candidate/editCandidate.html b/src/main/resources/templates/candidate/editCandidate.html index 8bc1e65f63dff81577a04e2e1c90e51ec3c3d776..4bb63f8a15d9adffa99531d422bca86ecb9dc4ff 100644 --- a/src/main/resources/templates/candidate/editCandidate.html +++ b/src/main/resources/templates/candidate/editCandidate.html @@ -1,46 +1,176 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> <head> <meta charset="UTF-8"> - <title>Modifier un Candidat</title> + <title>Edit Candidate</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" th:href="@{/css/bootstrap-icons.css}"> + <style> + .profile-card { + max-width: 700px; + margin: 0 auto; + border-radius: 10px; + overflow: hidden; + } + .profile-header { + background: #00B8DEFF; + color: white; + } + .form-control:read-only { + background-color: #f8f9fa; + border-color: #dee2e6; + } + .password-container { + position: relative; + } + .password-toggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + z-index: 5; + background: none; + border: none; + color: #6c757d; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> </head> -<body> - <div class="container"> - <h2 class="mt-5">Modifier un Candidat</h2> - - <form th:action="@{/candidates/edit}" method="post"> - <input type="hidden" name="id" th:value="${candidate.id}" /> - - <div class="form-group"> - <label>Email :</label> - <!-- <input type="email" name="mail" class="form-control" th:value="${candidate.mail}" required> --> - <input type="email" class="form-control" name="mail" required th:value="${candidate.mail}" readonly /> +<body class="bg-light"> +<div class="container py-4"> + <div class="card profile-card shadow-lg"> + <div class="card-header profile-header py-3"> + <div class="d-flex justify-content-between align-items-center"> + <h2 class="h4 mb-0"> + <i class="bi bi-person-gear me-2"></i> Edit Candidate Profile + </h2> </div> + </div> - <div class="form-group"> - <label>Mot de passe :</label> - <input type="password" name="password" class="form-control" required> + <div class="card-body p-4"> + <!-- Success Message --> + <div th:if="${successMessage != null}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div class="form-group"> - <label>Nom :</label> - <input type="text" name="lastname" class="form-control" th:value="${candidate.lastname}" required> + <!-- Error Messages (simplified to avoid Thymeleaf errors) --> + <div th:if="${errorMessage != null}" class="alert alert-danger alert-dismissible fade show"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div class="form-group"> - <label>Prénom :</label> - <input type="text" name="firstname" class="form-control" th:value="${candidate.firstname}" required> - </div> + <form th:action="@{/candidates/edit}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${candidate.id}" /> - <div class="form-group"> - <label>Ville :</label> - <input type="text" name="city" class="form-control" th:value="${candidate.city}" required> - </div> + <div class="row g-4"> + <!-- Left Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <input type="email" class="form-control" name="mail" + th:value="${candidate.mail}" readonly> + <small class="text-muted">Email cannot be changed</small> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Password</label> + <div class="password-container"> + <input type="password" id="passwordField" name="password" + class="form-control" + placeholder="Leave blank to keep current password"> + <button type="button" class="password-toggle" onclick="togglePassword()"> + <i class="bi bi-eye"></i> + </button> + </div> + <small class="text-muted">Minimum 8 characters</small> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">First Name</label> + <input type="text" name="firstname" class="form-control" + th:value="${candidate.firstname}" required> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Last Name</label> + <input type="text" name="lastname" class="form-control" + th:value="${candidate.lastname}" required> + </div> - <button type="submit" class="btn btn-success mt-3">Enregistrer</button> - <a th:href="@{/candidates/list}" class="btn btn-secondary mt-3">Annuler</a> - </form> + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <input type="text" name="city" class="form-control" + th:value="${candidate.city}" required> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <div> + <a th:href="@{/candidates/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to list + </a> + </div> + + <div > + <button type="submit" class="btn btn-primary px-4"> + <i class="bi bi-save me-2"></i> Save Changes + </button> + </div> + </div> + </form> + </div> </div> +</div> + +<script th:src="@{/js/bootstrap.bundle.min.js}"></script> +<script> + // Password toggle function + function togglePassword() { + const passwordField = document.getElementById('passwordField'); + const toggleIcon = document.querySelector('.password-toggle i'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + toggleIcon.classList.replace('bi-eye', 'bi-eye-slash'); + } else { + passwordField.type = 'password'; + toggleIcon.classList.replace('bi-eye-slash', 'bi-eye'); + } + } + + // Form validation + document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('.needs-validation'); + + form.addEventListener('submit', function(event) { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + + // Add input validation on blur + form.querySelectorAll('input[required]').forEach(input => { + input.addEventListener('blur', () => { + input.classList.toggle('is-invalid', !input.checkValidity()); + }); + }); + }); +</script> </body> -</html> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/candidate/signupCandidate.html b/src/main/resources/templates/candidate/signupCandidate.html index f8d74e5e6e033c038888c4f69ec112a65775d012..dd6a1008514dc6e994abc142f91a370a1cbd1761 100644 --- a/src/main/resources/templates/candidate/signupCandidate.html +++ b/src/main/resources/templates/candidate/signupCandidate.html @@ -1,17 +1,26 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <head> <meta charset="UTF-8"> <title>Candidate Sign Up</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <style> + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> </head> <body> <div class="container"> <h2 class="mt-5">Sign Up as Candidate</h2> - <!-- Affichage des erreurs --> - <div th:if="${error}" class="alert alert-danger" th:text="${error}"></div> - + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> + </div> <!-- Formulaire d'inscription --> <form th:action="@{/candidates/signup}" method="post"> <div class="mb-3"> @@ -35,13 +44,15 @@ <input type="text" class="form-control" id="city" name="city" th:value="${city}" required> </div> - <button type="submit" class="btn btn-primary">Sign Up</button> - </form> + <button type="submit" class="btn btn-primary ">Sign Up</button> + <a th:href="@{/login}" class="btn btn-light text-black " >Annuler</a> + </form> <!-- Lien de retour à la page de connexion --> <p class="mt-3"> Already have an account? <a th:href="@{/login}">Sign in here</a> </p> </div> </body> +</section> </html> diff --git a/src/main/resources/templates/company/companyBase.html b/src/main/resources/templates/company/companyBase.html deleted file mode 100644 index eed3128d24fc488ded44f3d635d2629e4a8066f5..0000000000000000000000000000000000000000 --- a/src/main/resources/templates/company/companyBase.html +++ /dev/null @@ -1,13 +0,0 @@ -<!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:fragment="article(subcontent)" - th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> -<title>Companies</title> -<section> - <header> - <h1>Companies Management</h1> - </header> - <th:block th:insert="${subcontent}"> - </th:block> -</section> - -</html> \ No newline at end of file diff --git a/src/main/resources/templates/company/companyEdit.html b/src/main/resources/templates/company/companyEdit.html index 82ea247f8dfe4eba39fd8cfb5b8d39b3175ab559..dea5445fb50b3fb18534cf0945ead527f6c40e9d 100644 --- a/src/main/resources/templates/company/companyEdit.html +++ b/src/main/resources/templates/company/companyEdit.html @@ -1,50 +1,143 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> - <header> - <h2>Modifier l'entreprise</h2> - </header> - - <!-- Message d'erreur ou de succès --> - <div th:if="${successMessage}" class="alert alert-success"> - <strong>Succès :</strong> <span th:text="${successMessage}"></span> - </div> - - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" th:href="@{/css/bootstrap-icons.css}"> + <style> + .profile-card { + max-width: 800px; + margin: 0 auto; + border-radius: 10px; + overflow: hidden; + } + .profile-header { + background: #00B8DEFF; + color: white; + } + .form-control:read-only { + background-color: #f8f9fa; + border-color: #dee2e6; + } + .btn-primary { + background-color: #00B8DEFF; + border-color: #00B8DEFF; + } + .btn-outline-secondary { + border-color: #6c757d; + color: #6c757d; + } + .form-label { + font-weight: 500; + color: #495057; + } + .text-muted { + font-size: 0.85rem; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <div class="card profile-card shadow-lg"> + <div class="card-header profile-header py-3"> + <div class="d-flex justify-content-between align-items-center"> + <h2 class="h4 mb-0"> + <i class="bi bi-building-gear me-2"></i> Edit Company Profile + </h2> + </div> + </div> + + <div class="card-body p-4"> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${error}" class="alert alert-danger alert-dismissible fade show"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${error}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <form th:action="@{/companies/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${company.id}" /> + + <div class="row g-4"> + <!-- Left Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label">Email</label> + <input type="email" class="form-control" name="mail" + th:value="${company.mail}" readonly> + <small class="text-muted">Email cannot be changed</small> + </div> + + <div class="mb-3"> + <label class="form-label">Company Name</label> + <input type="text" class="form-control" name="denomination" + th:value="${company.denomination}" required> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label">Description</label> + <textarea class="form-control" name="description" + rows="3" required th:text="${company.description}"></textarea> + </div> + + <div class="mb-3"> + <label class="form-label">City</label> + <input type="text" class="form-control" name="city" + th:value="${company.city}" required> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <div> + <a th:href="@{/companies}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to list + </a> + </div> + <div> + <button type="submit" class="btn btn-primary px-4"> + <i class="bi bi-save me-2"></i> Save Changes + </button> + </div> + </div> + </form> + </div> + </div> </div> - - <form th:action="@{/companies/update}" method="post"> - <!-- ID (caché) --> - <input type="hidden" name="id" th:value="${company.id}" /> - - <!-- Email (readonly) --> - <fieldset class="mb-3"> - <label for="emailid" class="form-label">Email</label>: - <input type="email" id="emailid" class="form-control" name="mail" th:value="${company.mail}" readonly /> - </fieldset> - - <!-- Dénomination --> - <fieldset class="mb-3"> - <label for="nameid" class="form-label">Nom</label>: - <input type="text" id="nameid" class="form-control" name="denomination" th:value="${company.denomination}" required /> - </fieldset> - - <!-- Description --> - <fieldset class="mb-3"> - <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" th:value="${company.description}" required /> - </fieldset> - - <!-- Ville --> - <fieldset class="mb-3"> - <label for="cityid" class="form-label">Ville</label>: - <input type="text" id="cityid" class="form-control" name="city" th:value="${company.city}" required /> - </fieldset> - - <!-- Boutons --> - <button type="submit" class="btn btn-success">💾 Enregistrer</button> - <a th:href="@{/companies/view/{id}(id=${company.id})}" class="btn btn-danger">❌ Annuler</a> - </form> -</article> -</html> + + <script th:src="@{/js/bootstrap.bundle.min.js}"></script> + <script> + // Form validation + document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('.needs-validation'); + + form.addEventListener('submit', function(event) { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + + // Add input validation on blur + form.querySelectorAll('input[required], textarea[required]').forEach(input => { + input.addEventListener('blur', () => { + input.classList.toggle('is-invalid', !input.checkValidity()); + }); + }); + }); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/company/companyForm.html b/src/main/resources/templates/company/companyForm.html index e594bca8e4ca4cbc7fb21c5ac7b1935dbafa28d7..ca2b0a6110f9ef081dee16a4d8090cbc2a614f1c 100644 --- a/src/main/resources/templates/company/companyForm.html +++ b/src/main/resources/templates/company/companyForm.html @@ -1,6 +1,7 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> <header> <h2>Company entry form</h2> </header> @@ -10,23 +11,23 @@ <strong>Succès :</strong> <span th:text="${successMessage}"></span> </div> - <!-- ❌ Message d'erreur --> - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> </div> <form th:action="@{/companies/create}" method="post"> <!-- Email --> <fieldset class="mb-3"> <label for="emailid" class="form-label">Email Address</label>: - <input type="email" id="emailid" class="form-control" name="mail" + <input type="email" id="emailid" class="form-control" name="mail" th:value="${company.mail}" required /> </fieldset> <!-- Mot de passe --> <fieldset class="mb-3"> <label for="passwordid" class="form-label">Password</label>: - <input type="password" id="passwordid" class="form-control" name="password" + <input type="password" id="passwordid" class="form-control" name="password" minlength="4" required /> </fieldset> @@ -40,20 +41,20 @@ <!-- 📌 CHAMP DESCRIPTION (Ajouté ici) --> <fieldset class="mb-3"> <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" + <input type="text" id="descid" class="form-control" name="description" th:value="${company.description}" required /> </fieldset> <!-- Ville --> <fieldset class="mb-3"> <label for="cityid" class="form-label">City</label>: - <input type="text" id="cityid" class="form-control" name="city" + <input type="text" id="cityid" class="form-control" name="city" th:value="${company.city}" required /> </fieldset> <!-- Boutons --> - <button type="submit" class="btn btn-primary">Enregistrer</button> - <a th:href="@{/companies}" class="btn btn-danger">Annuler</a> + <button type="submit" class="btn favorite_back text-white">Save</button> + <a th:href="@{/login}" class="btn btn-light text-black " >Cancel</a> </form> -</article> +</section> </html> \ No newline at end of file diff --git a/src/main/resources/templates/company/companyList.html b/src/main/resources/templates/company/companyList.html index 76c1e011b3ceae67edb1a128fb6d7d89af2f6720..5e793d4b88b18adbf02ce664f1371f3c47a4bc9e 100644 --- a/src/main/resources/templates/company/companyList.html +++ b/src/main/resources/templates/company/companyList.html @@ -1,49 +1,108 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <title>Liste des Entreprises</title> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> -</head> -<body> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> -<div class="container mt-4"> - <h2 class="text-center mb-4">Liste des Entreprises</h2> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .company-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .company-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .description-cell { + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .content-footer { + margin-top: 5rem; + padding-top: 1rem; + border-top: 1px solid #dee2e6; + } + </style> + </head> + <body class="bg-light"> - <table class="table table-striped table-bordered table-hover"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Dénomination</th> - <th>Ville</th> - <th>Description</th> - <th>Nombre d'Offres</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="company : ${companies}"> - <td th:text="${company.id}"></td> - <td th:text="${company.denomination}"></td> - <td th:text="${company.city}"></td> - <td th:text="${company.description}"></td> - <td th:text="${company.getJobOfferCount()}"></td> - <td> - <a th:href="@{'/companies/view/' + ${company.id}}" class="btn btn-primary btn-sm">Voir</a> - </td> - </tr> - </tbody> - </table> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class=" color-title bi bi-buildings me-2"></i> Lists of Companies + </h2> + </div> - <a th:href="@{/}" class="btn btn-secondary mt-3">🏠 Retour à l'accueil</a> + <!-- Companies Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(companies)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">Company Name</th> + <th>Location</th> + <th>Description</th> + <th>Job Offers</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="company : ${companies}" class="company-card"> + <td class="ps-4 fw-semibold" th:text="${company.denomination}"></td> + <td> + <i class="bi bi-geo-alt text-secondary me-1"></i> + <span th:text="${company.city}"></span> + </td> + <td class="description-cell" th:text="${company.description}"></td> + <td> + <span class="badge favorite_back rounded-pill" + th:text="${company.getJobOfferCount()}"></span> + </td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/companies/view/' + ${company.id}}" + class="btn btn favorite_outline" > + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}"> + <a th:href="@{'/companies/edit/' + ${company.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/companies/delete/' + ${company.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this company offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(companies)}" class="text-center py-5"> + <i class="bi bi-buildings empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No companies registered</h4> + </div> + </div> + </div> + </div> + </div> - - <!-- Vérifie si l'utilisateur est authentifié ET est une entreprise --> - <th:block th:if="${#httpServletRequest != null and #httpServletRequest.getSession(false) != null and #httpServletRequest.getSession().getAttribute('usertype') == 'company'}"> - <a th:href="@{/companies/create}" class="btn btn-success">Sign up as a company</a> - </th:block> - -</div> - -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -</body> -</html> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/company/companyView.html b/src/main/resources/templates/company/companyView.html index 7d5f091eabe18a237ccb90de75bfc607697b8202..39baacae644d237370c1f783390ee05e9bc4ff8c 100644 --- a/src/main/resources/templates/company/companyView.html +++ b/src/main/resources/templates/company/companyView.html @@ -1,123 +1,133 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" - th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> - <header> - <h2>Company details</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Company Details</h2> </header> - - <div th:if="${successMessage}" class="alert alert-success"> - <strong>Succès :</strong> <span th:text="${successMessage}"></span> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> + + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div th:if="${company != null}"> - <form> - <fieldset class="mb-3"> - <label for="nameid" class="form-label">Name</label>: - <input type="text" id="nameid" class="form-control" name="denomination" - th:value="${company.denomination}" readonly /> - </fieldset> - <fieldset class="mb-3"> - <label for="emailid" class="form-label">Email</label>: - <input type="text" id="emailid" class="form-control" name="email" - th:value="${company.email}" readonly /> - </fieldset> - - <fieldset class="mb-3"> - <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" - th:value="${company.description}" readonly /> - </fieldset> - <fieldset class="mb-3"> - <label for="cityid" class="form-label">City</label>: - <input type="text" id="cityid" class="form-control" name="city" - th:value="${company.city}" readonly /> - </fieldset> - - - <fieldset class="mb-3"> - <label class="form-label">Nombre d'offres d'emploi publiées :</label> - <input type="text" class="form-control" th:value="${jobOfferCount}" readonly /> - </fieldset> - - <fieldset class="mb-3"> - <h3>📋 Offres d'emploi publiées</h3> - <table class="table table-striped table-bordered"> - <thead class="table-dark"> + <div th:if="${company != null}" class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-building"></i> Company Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Company Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.denomination}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.email}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.city}"></div> + </div> + </div> + + <!-- Right Column (Aligned with left column) --> + <div class="col-md-6"> + <div class="mb-3" style="min-height: 96px"> + <label class="form-label fw-semibold">Description</label> + <div class="form-control-plaintext bg-light p-2 rounded" style="min-height: 96px" th:text="${company.description}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Job Offers</label> + <div class="form-control-plaintext bg-light p-2 rounded fw-bold" + th:text="${jobOfferCount} + (${jobOfferCount} == 1 ? ' active job' : ' active jobs')"></div> + </div> + </div> + </div> + + <!-- Job Offers Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-briefcase"></i> Current Job Offers + </h4> + + <div th:if="${#lists.isEmpty(jobOffers)}" class="alert alert-info"> + <i class="bi bi-info-circle"></i> No active job openings currently + </div> + + <div th:unless="${#lists.isEmpty(jobOffers)}" class="table-responsive"> + <table class="table table-hover align-middle"> + <thead class="table-light"> <tr> <th>ID</th> - <th>Titre</th> + <th>Position</th> <th>Description</th> - <th>Action</th> + <th>Sectors</th> + <th>Qualification</th> + <th>Posted Date</th> + <th class="text-end">Actions</th> </tr> - </thead> - <tbody> + </thead> + <tbody> <tr th:each="offer : ${jobOffers}"> <td th:text="${offer.id}"></td> <td th:text="${offer.title}"></td> - <td th:text="${offer.taskDescription}"></td> - <td> - <a th:href="@{/jobOffers/view/{id}(id=${offer.id})}" class="btn btn-info">Voir l'offre</a> + <div class="text-truncate" style="max-width: 300px" th:text="${offer.taskDescription}"></div> + </td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${offer.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${offer.qualificationLevel.label}"></td> + <td th:text="${offer.publicationDate != null} ? ${#temporals.format(offer.publicationDate, 'yyyy-MM-dd')} : 'N/A'"></td> + <td class="text-end" > + <a th:href="@{/jobs/view/{id}(id=${offer.id})}" th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}" + class="btn btn-sm text-black"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}"> + <a th:href="@{'/jobs/edit/' + ${offer.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/jobs/delete/' + ${offer.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this job offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> </td> </tr> - <tr th:if="${#lists.isEmpty(jobOffers)}"> - <td colspan="4" class="text-center text-danger">⚠️ Aucune offre d'emploi trouvée.</td> - </tr> - </tbody> - </table> - </fieldset> - - -<!-- <fieldset class="mb-3"> - <h3>📋 Offres d'emploi publiées</h3> - <table class="table table-striped table-bordered"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Titre</th> - <th>Description</th> - </tr> - </thead> - <tbody> - <tr th:each="offer : ${jobOffers}"> - <td th:text="${offer.id}"></td> - <td th:text="${offer.title}"></td> - <td th:text="${offer.taskDescription}"></td> - </tr> - <tr th:if="${#lists.isEmpty(jobOffers)}"> - <td colspan="3" class="text-center text-danger">⚠️ Aucune offre d'emploi trouvée.</td> - </tr> - </tbody> - </table> -</fieldset> --> -</form> - + </tbody> + </table> + </div> + </div> + </div> </div> - <div th:if="${company == null}"> - <p class="text-danger">❌ Erreur : Aucune entreprise trouvée.</p> - </div> - <footer> - <a th:href="@{/companies/{id}/edit(id=${company.id})}" class="btn btn-primary" title="Modifier"> - ✏️ Modifier - </a> - <!-- <a th:href="@{/companies/{id}/jobOffers(id=${company.id})}" class="btn btn-info"> - 📄 Voir les offres publiées - </a> --> - - - <a th:href="@{/companies/delete/{id}(id=${company.id})}" class="btn btn-danger" - onclick="return confirm('⚠️ Êtes-vous sûr de vouloir supprimer cette entreprise ?');"> - 🗑 Supprimer - </a> - - </footer> -</article> -</html> + + + <div class="mt-3"> + + </div> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/error/accessDenied.html b/src/main/resources/templates/error/accessDenied.html index d4ed95d38c64e16f10ac5d1501d02efc99a6c4ae..6ddc6e763feb5c327b6d2603f81c0c441be276a1 100644 --- a/src/main/resources/templates/error/accessDenied.html +++ b/src/main/resources/templates/error/accessDenied.html @@ -9,7 +9,7 @@ <div class="container mt-5 text-center"> <h2 class="text-danger">⛔ Accès interdit</h2> <p>Vous n'avez pas les permissions nécessaires pour voir cette page.</p> - <a href="/" class="btn btn-primary">Retour à l'accueil</a> + <a href="/" class="btn favorite_back">Retour à l'accueil</a> </div> </body> diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d53f468fed2d575137455cbab6fcbe5c80ab9573..347b1641e03d27b315c2ddd042c49c459372b3de 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -59,6 +59,31 @@ </a> </article> --> + + <!-- Nouvelle section profil améliorée --> + <div th:if="${session.loggedInUser != null}" > + + <div class="d-flex justify-content-between align-items-center text-black"> + <div> + <h2> + <i th:classappend="${session.userType == 'Company'} ? 'bi-building' : 'bi-person'" + class="bi me-2"></i> + WELCOME, + <span th:if="${session.userType == 'Company'}"> + [[${session.loggedInUser.denomination}]] + </span> + <span th:if="${session.userType == 'Candidate'}"> + [[${session.loggedInUser.firstname}]] [[${session.loggedInUser.lastname}]] + </span> + </h2> + </div> + <div> + <a th:if="${session.hasMessages}" th:href="@{/messages}" class="btn btn-outline-light me-2"> + <i class="bi bi-envelope-at"></i> Messages + </a> + </div> + </div> + </div> <div class="row row-cols-1 row-cols-lg-3 align-items-stretch g-4 py-5"> <article class="col"> <a th:href="@{/companies}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" @@ -70,7 +95,7 @@ </article> <article class="col"> - <a th:href="@{/candidates}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" + <a th:href="@{/candidates/list}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" style="background-image: url('/img/candidates.jpg'); background-size: cover;"> <div class="d-flex flex-column h-100 p-5 pb-3 text-white text-shadow-1"> <h2 class="pt-5 mt-5 mb-4 display-6 lh-1 fw-bold">Candidates</h2> @@ -79,7 +104,7 @@ </article> <!-- ✅ Déplacement de "Postuler" dans la même ligne --> - <article class="col"> + <article class="col" th:if="${session!=null and session.userType=='Candidate'} "> <a th:href="@{/applications/apply}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" style="background-image: url('/img/postuler.jpeg'); background-size: cover;"> <div class="d-flex flex-column h-100 p-5 pb-3 text-white text-shadow-1"> diff --git a/src/main/resources/templates/jobOffer/companyJobOfferView.html b/src/main/resources/templates/jobOffer/companyJobOfferView.html index 37cb1c21e83e9402c449bcd4aecd9293f5605745..1ce555003f56312fc47328e26af14dba651d88a7 100644 --- a/src/main/resources/templates/jobOffer/companyJobOfferView.html +++ b/src/main/resources/templates/jobOffer/companyJobOfferView.html @@ -1,19 +1,22 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Create Job Offer</title> +<section> <head> <title>Détails de l'Offre d'Emploi</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> </head> <body> <div class="container"> - <h2>Offre d'Emploi : <span th:text="${jobOffer.title}"></span></h2> + <h2>Offre d'Emploi : <span th:text="${offer.title}"></span></h2> - <p><strong>ID:</strong> <span th:text="${jobOffer.id}"></span></p> - <p><strong>Titre:</strong> <span th:text="${jobOffer.title}"></span></p> - <p><strong>Description:</strong> <span th:text="${jobOffer.taskDescription}"></span></p> - <p><strong>Date de Publication:</strong> <span th:text="${jobOffer.publicationDate}"></span></p> + <p><strong>ID:</strong> <span th:text="${offer.id}"></span></p> + <p><strong>Titre:</strong> <span th:text="${offer.title}"></span></p> + <p><strong>Description:</strong> <span th:text="${offer.taskDescription}"></span></p> + <p><strong>Date de Publication:</strong> <span th:text="${offer.publicationDate}"></span></p> <a th:href="@{/companies}" class="btn btn-secondary">Retour aux entreprises</a> </div> </body> +</section> </html> diff --git a/src/main/resources/templates/jobOffer/jobOfferEdit.html b/src/main/resources/templates/jobOffer/jobOfferEdit.html new file mode 100644 index 0000000000000000000000000000000000000000..0806a8b7a967b97fce5c06cf2af70519cbf8c010 --- /dev/null +++ b/src/main/resources/templates/jobOffer/jobOfferEdit.html @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Edit Job Offer</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + transition: all 0.3s ease; + } + .sector-item.selected { + background: rgb(0, 184, 222); + color: white; + border-color: rgb(0, 184, 222); + } + #selectedSectorsDisplay { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + background-color: #f8f9fa; + } + .selected-sector-tag { + display: inline-block; + background: rgb(0, 184, 222); + color: white; + padding: 2px 8px; + border-radius: 4px; + margin-right: 5px; + margin-bottom: 5px; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-briefcase me-2"></i> + Edit Job Offer + </h2> + + <form th:action="@{/jobs/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${job.id}" /> + + <!-- Champs de base --> + <div class="mb-3"> + <label class="form-label">Job Title*</label> + <input type="text" class="form-control" th:value="${job.title}" name="title" required> + </div> + + <div class="mb-3"> + <label class="form-label">Description*</label> + <textarea class="form-control" rows="4" name="taskDescription" required + th:text="${job.taskDescription}"></textarea> + </div> + + <div class="row mb-3"> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${job.publicationDate != null} ? ${#temporals.format(job.publicationDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualificationLevels}" + th:value="${level.id}" + th:text="${level.label}" + th:selected="${job.qualificationLevel?.id == level.id}"> + </option> + </select> + </div> + </div> + + <!-- Industry Sectors - Version corrigée --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + + <!-- Liste complète des secteurs --> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:classappend="${#lists.contains(job.sectors.![id], sector.id)} ? 'selected' : ''" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + + <!-- Champ affichant les secteurs sélectionnés --> + <div id="selectedSectorsDisplay" class="mb-2"> + <span th:if="${job.sectors.empty}">No sectors selected</span> + <th:block th:each="sector : ${job.sectors}"> + <div th:data-id="${sector.id}"> + <span th:text="${sector.label}"></span> + </div> + </th:block> + </div> + + <input type="hidden" id="sectorIds" name="sectorIds" + th:value="${#strings.listJoin(job.sectors.![id], ',')}" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/jobs}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Save Changes + </button> + </div> + </form> + </div> + </div> + + <script> + // Initialize with already selected sectors from the job object + const selectedSectors = new Set( + document.getElementById('sectorIds').value.split(',').filter(Boolean) + ); + + // Mark selected sectors on page load + document.addEventListener('DOMContentLoaded', function() { + updateSelectedDisplay(); + + // Mark initially selected sectors in the list + selectedSectors.forEach(sectorId => { + const element = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (element) { + element.classList.add('selected'); + } + }); + }); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + const sectorLabel = element.textContent; + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectorsDisplay'); + const hiddenInput = document.getElementById('sectorIds'); + + // Clear the display + displayDiv.innerHTML = ''; + + if (selectedSectors.size === 0) { + displayDiv.innerHTML = '<span>No sectors selected</span>'; + } else { + // Add selected sectors as tags + selectedSectors.forEach(sectorId => { + const sectorElement = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (sectorElement) { + const tag = document.createElement('div'); + tag.setAttribute('data-id', sectorId); + tag.innerHTML = `<span>${sectorElement.textContent}</span>`; + displayDiv.appendChild(tag); + } + }); + } + + // Update hidden input with selected sector IDs + hiddenInput.value = Array.from(selectedSectors).join(','); + } + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/jobOffer/jobOfferForm.html b/src/main/resources/templates/jobOffer/jobOfferForm.html index 7d2709416d172ef9eb6d0a644b6b11a6a3271978..a6c538b3e7894a28034d5357d72d63005b5482ab 100644 --- a/src/main/resources/templates/jobOffer/jobOfferForm.html +++ b/src/main/resources/templates/jobOffer/jobOfferForm.html @@ -1,70 +1,164 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <title>Créer/Modifier une offre d'emploi</title> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> -</head> -<body> - -<div class="container mt-4"> - <h2 class="text-center mb-4">Créer une offre d'emploi</h2> - - <!-- ✅ Affichage des messages d'erreur --> - <div th:if="${errorMessage}" class="alert alert-danger text-center"> - <span th:text="${errorMessage}"></span> - </div> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Create Job Offer</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: rgb(0, 184, 222); + color: white; + border-color: rgb(0, 184, 222); + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .hidden-input { + display: none; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> - <form th:action="@{${jobOffer.id != null} ? '/jobs/update/' + jobOffer.id : '/jobs/save'}" - th:object="${jobOffer}" method="post"> + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-briefcase me-2"></i> + Create Job Offer + </h2> - <input type="hidden" th:field="*{id}" /> + <form th:action="@{/jobs/save}" method="post" class="needs-validation" novalidate> - <div class="mb-3"> - <label class="form-label">Titre :</label> - <input type="text" class="form-control" th:field="*{title}" required> - </div> + <!-- Champs de base --> + <div class="mb-3"> + <label class="form-label">Job Title*</label> + <input type="text" class="form-control" name="title" required> + </div> - <div class="mb-3"> - <label class="form-label">Description :</label> - <textarea class="form-control" rows="3" th:field="*{taskDescription}" required></textarea> - </div> + <div class="mb-3"> + <label class="form-label">Description*</label> + <textarea class="form-control" rows="4" name="taskDescription" required></textarea> + </div> - <div class="mb-3"> - <label class="form-label">Date de publication :</label> - <input type="date" class="form-control" th:field="*{publicationDate}" required> - </div> + <div class="row mb-3"> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${jobOffer.publicationDate != null} ? ${#temporals.format(jobOffer.publicationDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualificationLevels}" + th:value="${level.id}" + th:text="${level.label}"></option> + </select> + </div> + </div> - <div class="mb-3"> - <label class="form-label">Entreprise :</label> - <select class="form-select" th:field="*{company}" required> - <option value="">-- Sélectionner une entreprise --</option> - <option th:each="company : ${companies}" th:value="${company.id}" th:text="${company.denomination}"></option> - </select> - </div> + <!-- Sélection des secteurs --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + <div id="selectedSectors" class="mb-2">No sectors selected</div> + <input type="hidden" id="sectorIds" name="sectorIds" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> - <div class="mb-3"> - <label class="form-label">Niveau de qualification :</label> - <select class="form-select" th:field="*{qualificationLevel}"> - <option value="0">-- Sélectionner un niveau de qualification --</option> - <option th:each="level : ${qualificationLevels}" th:value="${level.id}" th:text="${level.label}"></option> - </select> + <div class="d-flex justify-content-between mt-4"> + <a href="/jobs" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Create Offer + </button> + </div> + </form> </div> + </div> - <div class="mb-3"> - <label class="form-label">Secteurs d'activité :</label> - <select class="form-select" multiple th:name="sectorIds"> - <option th:each="sector : ${sectors}" th:value="${sector.id}" th:text="${sector.label}"></option> - </select> - </div> + <script> + const selectedSectors = new Set(); - <div class="text-center"> - <button type="submit" class="btn btn-success">Enregistrer</button> - <a href="/jobs" class="btn btn-secondary">Retour</a> - </div> + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectors'); + const hiddenInput = document.getElementById('sectorIds'); + + if (selectedSectors.size === 0) { + displayDiv.textContent = 'No sectors selected'; + hiddenInput.value = ''; + } else { + displayDiv.textContent = Array.from(selectedSectors).map(id => { + return document.querySelector(`.sector-item[data-id="${id}"]`).textContent; + }).join(', '); + + hiddenInput.value = Array.from(selectedSectors).join(','); + } + } - </form> -</div> + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -</body> -</html> + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/jobOffer/jobOfferList.html b/src/main/resources/templates/jobOffer/jobOfferList.html index ea6688dc7bc6a73552a4206723355f8b1fb4c434..228e5a03a600ca3c3e82278ef8aa644723fba898 100644 --- a/src/main/resources/templates/jobOffer/jobOfferList.html +++ b/src/main/resources/templates/jobOffer/jobOfferList.html @@ -1,85 +1,123 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Jobs</title> +<section> <head> - <title>Liste des Offres d'Emploi</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .job-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .job-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .sector-badge { + margin-right: 4px; + margin-bottom: 4px; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> </head> -<body> +<body class="bg-light"> -<div class="container mt-4"> - <h2 class="text-center mb-4">Liste des Offres d'Emploi</h2> - - <div th:if="${successMessage}" class="alert alert-success"> - <p th:text="${successMessage}"></p> - </div> - <div th:if="${errorMessage}" class="alert alert-danger"> - <p th:text="${errorMessage}"></p> +<div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-briefcase me-2"></i> Job Offers + </h2> + <a th:href="@{/jobs/create}" class="btn btn-outline-secondary" th:if="${session != null and session.userType == 'Company'}"> + <i class="bi bi-plus-circle"></i> Create Job Offer + </a> </div> - <table class="table table-striped table-bordered table-hover"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Titre</th> - <th>Entreprise</th> - <th>Secteurs</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="job : ${jobOffers}"> - <td th:text="${job.id}"></td> - <td th:text="${job.title}"></td> - <td th:text="${job.company.denomination}"></td> - <td> - <ul class="list-unstyled"> - <li th:each="sector : ${job.sectors}" th:text="${sector.label}"></li> - </ul> - </td> - <td> - <a th:href="@{'/jobs/view/' + ${job.id}}" class="btn btn-primary btn-sm">Voir</a> - - <!-- Vérification si l'utilisateur connecté est une entreprise ET qu'il est le propriétaire de l'offre --> - <th:block th:if="${session.loggedInUser != null - and session.loggedInUser.usertype == 'company' - and session.loggedInUser.mail == job.company.mail}"> - <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> - <a th:href="@{'/jobs/delete/' + ${job.id}}" - class="btn btn-danger btn-sm" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - Supprimer - </a> - </th:block> - <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> - <a th:href="@{'/jobs/delete/' + ${job.id}}" - class="btn btn-danger btn-sm" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - Supprimer - </a> - </th:block> - </td> - </tr> - </tbody> - - - </table> - <div class="text-center mt-4"> - <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> - - <th:block th:if="${session.usertype != null and session.usertype == 'company'}"> - <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> -</th:block> + <!-- Job Offers Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(jobOffers)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">ID</th> + <th>Title</th> + <th>Company</th> + <th>Sectors</th> + <th>Qualification</th> + <th>Posted Date</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="job : ${jobOffers}" class="job-card"> + <td class="ps-4" th:text="${job.id}"></td> + <td class="fw-semibold" th:text="${job.title}"></td> + <td th:text="${job.company?.denomination} ?: 'N/A'"></td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${job.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${job.qualificationLevel.label}"></td> + <td th:text="${job.publicationDate != null} ? ${#temporals.format(job.publicationDate, 'yyyy-MM-dd')} : 'N/A'"></td> <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/jobs/view/' + ${job.id}}" + class="btn btn-sm favorite_outline"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == job.company.id}"> + <a th:href="@{'/jobs/edit/' + ${job.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this job offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State --> + <div th:if="${#lists.isEmpty(jobOffers)}" class="text-center py-5"> + <i class="bi bi-briefcase empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No job offers found</h4> + </div> + </div> + </div> + </div> - </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> +</section> </html> - @@ -137,8 +175,31 @@ <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> <a th:href="@{/}" class="btn btn-secondary mt-3">🏠 Retour à l'accueil</a> + </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> -</html> --> +</html> + <td> + <a th:href="@{'/jobs/view/' + ${job.id}}" class="btn btn-primary btn-sm">Voir</a> + +<th:block th:if="${session.loggedInUser != null + and session.loggedInUser.usertype == 'COMPANY' + and session.loggedInUser.mail == job.company.mail}"> + <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> + <a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-danger btn-sm" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> + Supprimer + </a> +</th:block> +<a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> +<a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-danger btn-sm" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> + Supprimer +</a> +</th:block> +</td> + --> diff --git a/src/main/resources/templates/jobOffer/jobOfferView.html b/src/main/resources/templates/jobOffer/jobOfferView.html index e5fbffb1925c85799ef98bf9f505d1808cc142d1..bd9bc0968b1be6c7dfc3a4e4f4f6c25826db02f4 100644 --- a/src/main/resources/templates/jobOffer/jobOfferView.html +++ b/src/main/resources/templates/jobOffer/jobOfferView.html @@ -1,55 +1,94 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <head> - <title>Détails de l'Offre d'Emploi</title> + <title>Job Details</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + :root { + --primary: #072262; + --accent: #ff6b35; + } + </style> </head> -<body class="bg-light"> +<body> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Job Details</h2> + </header> -<div class="container mt-5"> - <h2 class="text-center mb-4"> Détails de l'Offre d'Emploi</h2> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> - <div class="card shadow-lg"> - <div class="card-header bg-primary text-white text-center"> - <h4 th:text="${jobOffer.title}"></h4> - </div> + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${job != null}" class="card shadow-sm mb-4"> <div class="card-body"> - <p><strong>ID de l'offre :</strong> <span th:text="${jobOffer.id}"></span></p> - <p><strong>Description :</strong> <span th:text="${jobOffer.taskDescription}"></span></p> - <p><strong>Entreprise :</strong> <span th:text="${jobOffer.company.denomination}"></span></p> - <p><strong>Date de publication :</strong> <span th:text="${jobOffer.publicationDate}"></span></p> - - <!-- ✅ Ajout du Niveau de Qualification --> - <p><strong>Niveau de qualification requis :</strong> - <span th:text="${jobOffer.qualificationLevel != null ? jobOffer.qualificationLevel.label : 'Non spécifié'}"></span> - </p> - - <!-- ✅ Section Secteurs améliorée avec des badges --> - <h5 class="mt-3">Secteurs :</h5> - <div class="d-flex flex-wrap"> - <span th:each="sector : ${jobOffer.sectors}" - th:text="${sector.label}" - class="badge bg-success text-white m-1 p-2"> - </span> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-briefcase"></i> Job Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Job Title</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${job.title}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Company</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${job.company?.denomination} ?: 'N/A'"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Qualification Level</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${job.qualificationLevel.label}"></div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-calendar-event"></i> Additional Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Publication Date</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${#temporals.format(job.publicationDate, 'MMMM d, yyyy')}"></div> + </div> + + <div th:if="${not #lists.isEmpty(job.sectors)}" class="mb-3"> + <label class="form-label fw-semibold">Sectors</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:each="sector : ${job.sectors}" class="badge favorite_back me-1" + th:text="${sector.label}"></span> + </div> + </div> + </div> </div> - </div> - </div> - <div class="text-center mt-4"> - <a href="/jobs" class="btn btn-secondary btn-lg"> - ⬅ <i class="bi bi-arrow-left"></i> Retour - </a> - <a th:href="@{'/jobs/' + ${jobOffer.id} + '/edit'}" class="btn btn-warning btn-lg"> - ✏ <i class="bi bi-pencil-square"></i> Modifier - </a> - <a th:href="@{'/jobs/delete/' + ${jobOffer.id}}" - class="btn btn-danger btn-lg" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - 🗑 <i class="bi bi-trash"></i> Supprimer - </a> + <!-- Job Description Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-file-text"></i> Job Description + </h4> + <div class="form-control-plaintext bg-light p-3 rounded" th:utext="${job.taskDescription}"></div> + </div> + </div> </div> -</div> -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> +</section> </body> -</html> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index fbaa5e7d82095b9735edaf37a77c33436b1ff78e..658fa7407419ff96a934949a1464ed33f366527d 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -1,45 +1,161 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="article(subcontent)" - th:replace="~{/baseTemplate/base :: layout(~{},~{::section})}"> - -<section class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1"> - <div class="modal-dialog" role="document"> - <div class="modal-content rounded-4 shadow"> - <header class="modal-header p-5 pb-4 border-bottom-0"> - <h1 class="fw-bold mb-0 fs-2">Sign in</h1> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> - </header> - - <article class="modal-body p-5 pt-0"> - <small class="text-secondary">Log in by entering your email address and password.</small> - <form action="/login" method="post"> - <fieldset class="mb-3 form-floating"> - <input type="email" id="uid" class="form-control rounded-3" name="mail" autofocus="autofocus" required - placeholder="name@domain.com" /> - <label for="uid">Email address</label> - </fieldset> - <fieldset class="mb-3 form-floating"> - <input type="password" id="idpwd" class="form-control rounded-3" name="password" placeholder="your password" - required /> - <label for="idpwd">Password</label> - </fieldset> - <input type="submit" value="Sign in" class="w-100 mb-2 btn btn-lg rounded-3 btn-primary" /> - </form> - - <!-- Affichage des erreurs de connexion --> - <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> - <p th:text="${error}"></p> + th:replace="~{/baseTemplate/base :: layout(~{},~{::section})}"> + +<head> + <style> + .favorite_color { + color:rgb(0, 183, 211); + } + + .favorite_back { + background-color: rgb(0, 183, 211); + } + + .favorite_outline { + border-color: rgb(0, 183, 211); + color: rgb(0, 183, 211); + } + + .favorite_outline:hover { + background-color: rgb(0, 183, 211); + color: white; + } + + .login-card { + border: none; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(7, 34, 98, 0.1); + overflow: hidden; + } + + .card-header { + background-color: rgb(0, 183, 211); + color: white; + padding: 1.5rem; + } + + .form-control { + border-radius: 8px; + padding: 12px 15px; + border: 1px solid #e0e0e0; + transition: all 0.3s; + } + + .form-control:focus { + border-color:rgb(0, 183, 211); + box-shadow: 0 0 0 0.25rem rgba(7, 34, 98, 0.25); + } + + .floating-label { + color: #6c757d; + transition: all 0.3s; + } + + .form-floating>.form-control:focus~label, + .form-floating>.form-control:not(:placeholder-shown)~label { + color:rgb(0, 183, 211); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); + } + + .login-btn { + padding: 12px; + font-size: 1rem; + letter-spacing: 0.5px; + transition: all 0.3s; + } + + .login-btn:hover { + background-color: rgb(0, 183, 211); + transform: translateY(-2px); + } + + .divider { + display: flex; + align-items: center; + margin: 1.5rem 0; + } + + .divider::before, + .divider::after { + content: ""; + flex: 1; + border-bottom: 1px solid #e0e0e0; + } + + .divider-text { + padding: 0 1rem; + color: #6c757d; + font-size: 0.9rem; + } + + .register-option { + transition: all 0.3s; + padding: 10px; + border-radius: 8px; + } + + .register-option:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(7, 34, 98, 0.1); + } + + .register-icon { + margin-right: 8px; + } + </style> +</head> + +<section class="container my-5"> + <div class="row justify-content-center"> + <div class="col-md-6 col-lg-5"> + <div class="card login-card"> + <div class="card-header favorite_back text-white text-center"> + <h3 class="mb-0">Welcome Back</h3> + <p class="mb-0 opacity-75">Sign in to your account</p> + </div> + + <div class="card-body p-4"> + <form th:action="@{/login}" method="post"> + <fieldset class="mb-3 form-floating"> + <input type="email" id="uid" class="form-control" name="mail" autofocus="autofocus" required + placeholder="name@domain.com" /> + <label for="uid" class="floating-label">Email address</label> + </fieldset> + + <fieldset class="mb-4 form-floating"> + <input type="password" id="idpwd" class="form-control" name="password" placeholder="your password" + required /> + <label for="idpwd" class="floating-label">Password</label> + </fieldset> + + <input type="submit" value="Sign in" class="w-100 btn login-btn text-black favorite_outline" /> + </form> + + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> + </div> </div> - </article> - - <footer class="modal-body p-5 pt-0"> - <hr class="my-4"> - <a th:href="@{/companies/create}" class="btn w-100 mb-2 btn btn-lg rounded-3 btn-secondary">Sign up as company</a> - <a th:href="@{/candidates/signup}" class="btn w-100 mb-2 btn btn-lg rounded-3 btn-secondary">Sign up as candidate</a> - <small class="text-secondary">By clicking Sign up, you agree to the terms of use.</small> - </footer> + + <div class="px-4 pb-4"> + <div class="divider"> + <span class="divider-text">OR</span> + </div> + + <p class="mb-3 text-center">Don't have an account?</p> + + <div class="d-grid gap-3"> + <a th:href="@{/companies/create}" class="btn favorite_outline register-option"> + <i class="bi bi-building register-icon "></i> Register as Company + </a> + <a th:href="@{/candidates/signup}" class="btn favorite_outline register-option"> + <i class="bi bi-person register-icon"></i> Register as Candidate + </a> + </div> + </div> + </div> </div> </div> </section> - -</html> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/qualificationLevel/qualificationLevelList.html b/src/main/resources/templates/qualificationLevel/qualificationLevelList.html index c97eb594998f17f9d42c75fb4be29ca89b36c7f3..edadb95f7d837654824babecc7a1081cee46fae1 100644 --- a/src/main/resources/templates/qualificationLevel/qualificationLevelList.html +++ b/src/main/resources/templates/qualificationLevel/qualificationLevelList.html @@ -1,45 +1,303 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> -<title>Qualification Level</title> +<title>Qualification Levels</title> <section> - <header> - <h1>List of qualification levels</h1> - </header> - <article> - <!-- <a href="javascript:history.back()"><img th:src="@{/img/back.png}" alt="Back"/></a> --> - <p th:if="${#lists.size(qualificationlevellist)} == 0">No qualification level defined yet.</p> - <th:block th:if="${#lists.size(qualificationlevellist)} > 0"> - <table class="table table-striped"> - <caption>List of qualification levels</caption> - <thead> - <tr> - <th scope="col">#</th> - <th scope="col">Label</th> - </tr> - </thead> - <tbody> - <tr th:each="ql : ${qualificationlevellist}"> - <th scope="row" th:text="${ql.id}" /> - <td th:text="${ql.label}" /> - <!-- <td th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="nav-item active"> - <a th:href="@{/deletequalificationlevel/{id}(id=${ql.id})}"> - <img th:src="@{img/minus.png}" alt="Delete this sector" class="minilogo"/> - </a> - </td> --> - </tr> - </tbody> - </table> - </th:block> - <!-- <div th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="row h-10"> - <form action="/addqualificationlevel" method="get" class="col-xs-12 col-sm-6 col-md-4 col-lg-2"> - <label for="labelql">Label</label> - <input type="text" id="labelql" name="labelql" autofocus="autofocus" minlength="3" required/> <br /> - - <input type="submit" value="Add" /> - </form> - </form> - </div> --> - </article> -</section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .qualification-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .qualification-card:hover { + transform: translateY(-2px); + border-left-color: #3a7bd5; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .empty-state { + min-height: 300px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .search-container { + position: relative; + margin-bottom: 1.5rem; + } + .search-icon { + position: absolute; + left: 12px; + top: 10px; + color: #6c757d; + } + .search-input { + padding-left: 40px; + border-radius: 20px; + border: 1px solid #ced4da; + transition: all 0.3s; + } + .search-input:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + border-color: #86b7fe; + } + .hidden-row { + display: none; + } + .pagination .page-item.active .page-link { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + color: white; + } + .pagination .page-link { + color: rgb(0, 184, 222); + } + .page-size-selector { + width: auto; + display: inline-block; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-award me-2"></i> Qualification Levels + </h2> + </div> + + <!-- Search and Page Size Controls --> + <div class="row mb-3"> + <div class="col-md-6"> + <div class="search-container"> + <i class="bi bi-search search-icon"></i> + <input type="text" class="form-control search-input" id="liveSearchInput" + placeholder="Start typing to filter qualification levels..." autocomplete="off"> + </div> + </div> + <div class="col-md-6 text-md-end"> + <div class="d-inline-flex align-items-center"> + <span class="me-2">Show:</span> + <select class="form-select form-select-sm page-size-selector" id="pageSizeSelect"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + <span class="ms-2">entries</span> + </div> + </div> + </div> + + <!-- Qualification Levels Table --> + <div class="card shadow-sm table-container mb-3"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" id="qualificationsTable"> + <thead class="table-light"> + <tr> + <th>Label</th> + <th class="text-end pe-4" th:if="${session != null and session.logintype == 'adm'}">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="ql : ${qualificationlevellist}" class="qualification-card"> + <td class="fw-semibold qualification-label" th:text="${ql.label}"></td> + <td class="text-end pe-4" th:if="${session != null and session.logintype == 'adm'}"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{/deletequalificationlevel/{id}(id=${ql.id})}" + class="btn btn-sm btn-outline-danger"> + <i class="bi bi-trash"></i> Delete + </a> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State (initial) --> + <div th:if="${#lists.isEmpty(qualificationlevellist)}" class="empty-state text-center py-5"> + <i class="bi bi-award text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No qualification levels defined</h4> + </div> + + <!-- Empty State (after filtering) --> + <div id="noResultsState" class="empty-state text-center py-5" style="display: none;"> + <i class="bi bi-search text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No matching qualifications found</h4> + <p class="text-muted">Try adjusting your search</p> + </div> + </div> + </div> + </div> + + <!-- Pagination and Info --> + <div class="row"> + <div class="col-md-6"> + <div id="pageInfo" class="text-muted">Showing 1 to 10 of <span id="totalRecords">0</span> entries</div> + </div> + <div class="col-md-6"> + <nav aria-label="Page navigation" class="float-md-end"> + <ul class="pagination" id="pagination"> + <li class="page-item disabled" id="prevPage"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> + </a> + </li> + <!-- Pages will be inserted here by JavaScript --> + <li class="page-item" id="nextPage"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + </div> + </div> + </div> + + <script> + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('liveSearchInput'); + const tableRows = document.querySelectorAll('#qualificationsTable tbody tr'); + const noResultsState = document.getElementById('noResultsState'); + const initialEmptyState = document.querySelector('.empty-state'); + const pageSizeSelect = document.getElementById('pageSizeSelect'); + const pagination = document.getElementById('pagination'); + const prevPage = document.getElementById('prevPage'); + const nextPage = document.getElementById('nextPage'); + const pageInfo = document.getElementById('pageInfo'); + const totalRecords = document.getElementById('totalRecords'); + + let currentPage = 1; + let pageSize = parseInt(pageSizeSelect.value); + let filteredRows = Array.from(tableRows); + + // Initialize + updateTable(); + updatePagination(); + + // Search functionality + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + filteredRows = Array.from(tableRows).filter(row => { + const qualLabel = row.querySelector('.qualification-label').textContent.toLowerCase(); + return qualLabel.includes(searchTerm); + }); + + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Page size change + pageSizeSelect.addEventListener('change', function() { + pageSize = parseInt(this.value); + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Pagination click handlers + pagination.addEventListener('click', function(e) { + e.preventDefault(); + if (e.target.closest('.page-link')) { + const target = e.target.closest('.page-link'); + if (target.getAttribute('aria-label') === 'Previous') { + if (currentPage > 1) currentPage--; + } else if (target.getAttribute('aria-label') === 'Next') { + if (currentPage < Math.ceil(filteredRows.length / pageSize)) currentPage++; + } else { + currentPage = parseInt(target.textContent); + } + updateTable(); + updatePagination(); + } + }); + function updateTable() { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const visibleRows = filteredRows.slice(start, end); + + // Hide all rows first + tableRows.forEach(row => { + row.classList.add('hidden-row'); + }); + + // Show only visible rows + visibleRows.forEach(row => { + row.classList.remove('hidden-row'); + }); + + // Update empty states + if (filteredRows.length === 0) { + noResultsState.style.display = 'flex'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } else { + noResultsState.style.display = 'none'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } + } + + function updatePagination() { + const totalPages = Math.ceil(filteredRows.length / pageSize); + totalRecords.textContent = filteredRows.length; + + // Update page info + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredRows.length); + pageInfo.textContent = `Showing ${start} to ${end} of ${filteredRows.length} entries`; + + // Clear existing page numbers + const pageNumbers = pagination.querySelectorAll('.page-number'); + pageNumbers.forEach(el => el.remove()); + + // Add new page numbers + const maxVisiblePages = 5; + let startPage, endPage; + + if (totalPages <= maxVisiblePages) { + startPage = 1; + endPage = totalPages; + } else { + const maxVisibleBeforeCurrent = Math.floor(maxVisiblePages / 2); + const maxVisibleAfterCurrent = Math.ceil(maxVisiblePages / 2) - 1; + + if (currentPage <= maxVisibleBeforeCurrent) { + startPage = 1; + endPage = maxVisiblePages; + } else if (currentPage + maxVisibleAfterCurrent >= totalPages) { + startPage = totalPages - maxVisiblePages + 1; + endPage = totalPages; + } else { + startPage = currentPage - maxVisibleBeforeCurrent; + endPage = currentPage + maxVisibleAfterCurrent; + } + } + + // Add page number items + for (let i = startPage; i <= endPage; i++) { + const pageItem = document.createElement('li'); + pageItem.className = `page-item page-number ${i === currentPage ? 'active' : ''}`; + pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; + nextPage.parentNode.insertBefore(pageItem, nextPage); + } + + // Update prev/next buttons + prevPage.classList.toggle('disabled', currentPage === 1); + nextPage.classList.toggle('disabled', currentPage === totalPages || totalPages === 0); + } + }); + </script> + </body> +</section> </html> \ No newline at end of file diff --git a/src/main/resources/templates/sector/sectorList.html b/src/main/resources/templates/sector/sectorList.html index b30d47150d06d3924541dfb2ccdae1c77e5874f6..cb3e68a31b1340aa603bbbf562079d54661bb450 100644 --- a/src/main/resources/templates/sector/sectorList.html +++ b/src/main/resources/templates/sector/sectorList.html @@ -2,48 +2,307 @@ <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <title>Sectors</title> <section> - <header> - <h1>List of sectors</h1> - </header> - <article> - <p th:if="${#lists.size(sectorlist)} == 0">No sector defined yet.</p> - - <th:block th:if="${#lists.size(sectorlist)} > 0"> - - <table class="table table-striped"> - <caption>List of sectors</caption> - <thead> - <tr> - <th scope="col">#</th> - <th scope="col">Label</th> - </tr> - </thead> - <tbody> - <tr th:each="sec : ${sectorlist}"> - <th scope="row" th:text="${sec.id}" /> - <td th:text="${sec.label}" /> - <td th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" - class="nav-item active"> - <a th:href="@{/deletesector/{id}(id=${sec.id})}"> - <img th:src="@{img/minus.png}" alt="Delete this sector" class="minilogo" /> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .sector-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .sector-card:hover { + transform: translateY(-2px); + border-left-color: #00B8DEFF; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .empty-state { + min-height: 300px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .search-container { + position: relative; + margin-bottom: 1.5rem; + } + .search-icon { + position: absolute; + left: 12px; + top: 10px; + color: #6c757d; + } + .search-input { + padding-left: 40px; + border-radius: 20px; + border: 1px solid #ced4da; + transition: all 0.3s; + } + .search-input:focus { + box-shadow: 0 0 0 0.25rem rgb(0, 184, 222); + border-color: #00B8DEFF; + + } + .hidden-row { + display: none; + } + .pagination .page-item.active .page-link { + background-color: #00B8DEFF; + border-color: #00B8DEFF; + color: #ffffff; + } + .pagination .page-link { + color: #00B8DEFF; + } + .page-size-selector { + width: auto; + display: inline-block; + } + .controls-row { + margin-bottom: 1.5rem; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-tags me-2"></i> Sectors + </h2> + </div> + + <!-- Search and Page Size Controls --> + <div class="row controls-row"> + <div class="col-md-6"> + <div class="search-container"> + <i class="bi bi-search search-icon"></i> + <input type="text" class="form-control search-input" id="liveSearchInput" + placeholder="Start typing to filter sectors..." autocomplete="off"> + </div> + </div> + <div class="col-md-6 text-md-end"> + <div class="d-inline-flex align-items-center"> + <span class="me-2">Show:</span> + <select class="form-select form-select-sm page-size-selector" id="pageSizeSelect"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + <span class="ms-2">entries</span> + </div> + </div> + </div> + + <!-- Sectors Table --> + <div class="card shadow-sm table-container mb-3"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" id="sectorsTable"> + <thead class="table-light"> + <tr> + <th>Label</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="sec : ${sectorlist}" class="sector-card"> + <td class="fw-semibold sector-label" th:text="${sec.label}"></td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{/deletesector/{id}(id=${sec.id})}" + class="btn btn-sm btn-outline-danger" + th:if="${session != null and session.logintype == 'adm'}"> + <i class="bi bi-trash"></i> Delete + </a> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State (initial) --> + <div th:if="${#lists.isEmpty(sectorlist)}" class="empty-state text-center py-5"> + <i class="bi bi-tag text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No sectors defined</h4> + </div> + + <!-- Empty State (after filtering) --> + <div id="noResultsState" class="empty-state text-center py-5" style="display: none;"> + <i class="bi bi-search text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No matching sectors found</h4> + <p class="text-muted">Try adjusting your search</p> + </div> + </div> + </div> + </div> + + <!-- Pagination and Info --> + <div class="row"> + <div class="col-md-6"> + <div id="pageInfo" class="text-muted">Showing 1 to 10 of <span id="totalRecords">0</span> entries</div> + </div> + <div class="col-md-6"> + <nav aria-label="Page navigation" class="float-md-end"> + <ul class="pagination" id="pagination"> + <li class="page-item disabled" id="prevPage"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> </a> - </td> - </tr> - </tbody> - </table> - </th:block> - - <aside th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="row h-10"> - <form action="/addsector" method="get" class="col-xs-12 col-sm-6 col-md-4 col-lg-2"> - <fieldset> - <label for="labelsector">Label</label> - <input type="text" id="labelsector" name="labelsector" autofocus="autofocus" minlength="3" - required /> - </fieldset> - <input type="submit" value="Add" /> - </form> - </aside> - </article> -</section> + </li> + <!-- Pages will be inserted here by JavaScript --> + <li class="page-item" id="nextPage"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + </div> + </div> + </div> + + <script> + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('liveSearchInput'); + const tableRows = document.querySelectorAll('#sectorsTable tbody tr'); + const noResultsState = document.getElementById('noResultsState'); + const initialEmptyState = document.querySelector('.empty-state'); + const pageSizeSelect = document.getElementById('pageSizeSelect'); + const pagination = document.getElementById('pagination'); + const prevPage = document.getElementById('prevPage'); + const nextPage = document.getElementById('nextPage'); + const pageInfo = document.getElementById('pageInfo'); + const totalRecords = document.getElementById('totalRecords'); + + let currentPage = 1; + let pageSize = parseInt(pageSizeSelect.value); + let filteredRows = Array.from(tableRows); + + // Initialize + updateTable(); + updatePagination(); + + // Search functionality + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + filteredRows = Array.from(tableRows).filter(row => { + const sectorLabel = row.querySelector('.sector-label').textContent.toLowerCase(); + return sectorLabel.includes(searchTerm); + }); + + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Page size change + pageSizeSelect.addEventListener('change', function() { + pageSize = parseInt(this.value); + currentPage = 1; + updateTable(); + updatePagination(); + }); + // Pagination click handlers + pagination.addEventListener('click', function(e) { + e.preventDefault(); + if (e.target.closest('.page-link')) { + const target = e.target.closest('.page-link'); + if (target.getAttribute('aria-label') === 'Previous') { + if (currentPage > 1) currentPage--; + } else if (target.getAttribute('aria-label') === 'Next') { + if (currentPage < Math.ceil(filteredRows.length / pageSize)) currentPage++; + } else { + currentPage = parseInt(target.textContent); + } + updateTable(); + updatePagination(); + } + }); + + function updateTable() { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const visibleRows = filteredRows.slice(start, end); + + // Hide all rows first + tableRows.forEach(row => { + row.classList.add('hidden-row'); + }); + + // Show only visible rows + visibleRows.forEach(row => { + row.classList.remove('hidden-row'); + }); + + // Update empty states + if (filteredRows.length === 0) { + noResultsState.style.display = 'flex'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } else { + noResultsState.style.display = 'none'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } + } + + function updatePagination() { + const totalPages = Math.ceil(filteredRows.length / pageSize); + totalRecords.textContent = filteredRows.length; + + // Update page info + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredRows.length); + pageInfo.textContent = `Showing ${start} to ${end} of ${filteredRows.length} entries`; + + // Clear existing page numbers + const pageNumbers = pagination.querySelectorAll('.page-number'); + pageNumbers.forEach(el => el.remove()); + + // Add new page numbers + const maxVisiblePages = 5; + let startPage, endPage; + + if (totalPages <= maxVisiblePages) { + startPage = 1; + endPage = totalPages; + } else { + const maxVisibleBeforeCurrent = Math.floor(maxVisiblePages / 2); + const maxVisibleAfterCurrent = Math.ceil(maxVisiblePages / 2) - 1; + + if (currentPage <= maxVisibleBeforeCurrent) { + startPage = 1; + endPage = maxVisiblePages; + } else if (currentPage + maxVisibleAfterCurrent >= totalPages) { + startPage = totalPages - maxVisiblePages + 1; + endPage = totalPages; + } else { + startPage = currentPage - maxVisibleBeforeCurrent; + endPage = currentPage + maxVisibleAfterCurrent; + } + } + + // Add page number items + for (let i = startPage; i <= endPage; i++) { + const pageItem = document.createElement('li'); + pageItem.className = `page-item page-number ${i === currentPage ? 'active' : ''}`; + pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; + nextPage.parentNode.insertBefore(pageItem, nextPage); + } + + // Update prev/next buttons + prevPage.classList.toggle('disabled', currentPage === 1); + nextPage.classList.toggle('disabled', currentPage === totalPages || totalPages === 0); + } + }); + </script> + </body> +</section> </html> \ No newline at end of file diff --git a/target/classes/application.properties b/target/classes/application.properties index a5b3aeddcb74787f0c0ef818477f2760c026df6a..c970441d4741c52eabaf7b4f2dbe76f1cf66c380 100644 --- a/target/classes/application.properties +++ b/target/classes/application.properties @@ -18,7 +18,7 @@ spring.jpa.show-sql=true logging.level.root=info logging.level.org.springframework.web=debug spring.jackson.serialization.indent_output=true - +spring.thymeleaf.expose-authentication-attributes=true # Session timeout in seconds server.servlet.session.timeout=10m @@ -29,4 +29,11 @@ jobmngt.admin=007 spring.thymeleaf.cache=false -spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false \ No newline at end of file +spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false + +temp.upload.dir=./temp_uploads + +# Taille max des fichiers +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.class new file mode 100644 index 0000000000000000000000000000000000000000..ad6039299fc3de4be3ddced5a871d4a260cb45d2 Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/AppUserDetailsService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.class new file mode 100644 index 0000000000000000000000000000000000000000..cac60139015c2d5e855b6b68d64f0e83b948b1de Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/CheckAuth.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.class new file mode 100644 index 0000000000000000000000000000000000000000..e2f8cc63a47c4310690bf473a1b2c7cc67b4995f Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/DataInitializer.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..95ed94c03ea58bc843bb2dc181c01624cb8f19cb Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/config/SecurityConfig.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.class index 3fcaf5ff35a551475b6e68047acb99678d811296..4cc1994c9a506541ebfb1c18b899fa6077dc2467 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/ApplicationController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.class index c1d26ac598126b95b48b57bfb0a05a5f924f14ca..17db613fec568b92ab5c0ff7f772e05775016ea9 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CandidateController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.class index 2d63e8afadfea3eb2a4cc75e3f6f7f00c4a55ad9..87cbd216691b76f5806fe8469d6c459b9105acb6 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/CompanyController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.class index 811cc81375a5b878017bc133bf637de270a86cb0..504c1def4d4fe98ad2eff56c8f9520b32169633e 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/JobOfferController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.class index 90153c1ab690d1c2889ce5dbcc4d948667b050bc..0d97931f30745e3b35e374a3d83aecec418fa65b 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/LoginController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/SectorController.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/SectorController.class index 891e652c6835417fc6eb01c4fa8296edae1ee9ac..6fb5cf1720deddfa5246f5ce24d64e2e0eb61775 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/SectorController.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/controller/SectorController.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.class index 69b5b745c27680d537d723367dd1d26780b226b5..0602d9553e6903813a9589831d9aeb7caba404e3 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/converter/CompanyConverter.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.class index 72371914d226adbddc19b8e87dfb2415e2f364e5..85698403b3217618b238cfa5dc8b43f6e454ea23 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/AppUserDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.class index 8387ebab7a1575d0e80c1445458e1423f979e1da..156940b0ccbc02225287ba7f8611daebc3acfe9c 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/ApplicationDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.class index f18ce38b42f4a6f07d91ac4c0a5f675eb28d12d6..c9d4923bc1613f83c9280539017f6a48a2151098 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CandidateDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.class index f4523efaf9abc0da87905cd33c5719743f932207..b4336a90fda6f25e7fd7b52f0fa7dac8675427eb 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/CompanyDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.class index a925598f027de2690213cf12ef27ff30629aba44..d3684d1d03653fefd23d4e4029648fd78c1fa2e9 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/JobOfferDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.class index 2ea3970e656f6e73dd8ac2f98f7da640b427b7aa..6f9f00faaeefea0b361676b0a410334ec66c2c69 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/QualificationLevelDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.class index e8247d42b70c88de69e88771d4a9efe9868227a7..78bea8e94bce9a36d8c9bc4dbcece568a341866f 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/dao/SectorDao.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.class index e9fb9bca45ab2cf9833d8ea6508cd46b56c1e4e7..398c5f8475ee1a5bc548a2fc1e74f7d2eeea492f 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/AppUser.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Application.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Application.class index 1db376ad3094759e8307757ae5055fc14d64284f..129943da2efc4139b062c2506e202a6c51cc7318 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Application.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Application.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.class index 1e2bc7aa657a3cfe9be477d995cdb16276863040..c84d358a7c81540f2739346a2bd05ea586743aed 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Candidate.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Company.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Company.class index a1fd23885096354fe952948017a5a2239e96ceb5..73fbf5835ec6b52d6bce5bc9b046ab2a7a8707f6 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Company.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Company.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.class index 1821fd1a0edd009d51a7d0cd1e66947164735014..da220df5c537325548cf56d56761ddbf1b61d035 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/JobOffer.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Role.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Role.class deleted file mode 100644 index 9bf8dd8acd032b883c07ca8bb9c1d4057510b7d6..0000000000000000000000000000000000000000 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/Role.class and /dev/null differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.class new file mode 100644 index 0000000000000000000000000000000000000000..7ca0e068bca927361b5db70ea72b3344c1fb3d5e Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/entity/RoleType.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.class index 9d72472a36a6ab69616234ea7df7a8b0f74d3aa5..c8be8b3025cebc0a2a2dc1d824bfd6407c080de5 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.class index 3228527ea8ab8d6aa2fbdc25684e9fe6230ed4e0..295eaebbc0b1d2449215d5e0a4aa3e42c0407908 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/AppUserServiceImpl.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.class index 968568c5ee8c9d52857e324fe49acf398ffbd298..fda6c0c60ab7fb4da13c008d3695083cbb03be77 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.class index fe49a05345bec1cfe13f4d8ba89a2155edcc41cb..8b62aae27c2c7120da87901f1a90f4051ca15421 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CandidateServiceImpl.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.class index e2861966110f6a9d7e4017cb32fa8e8136582dd6..f2c0169789e097e04bce6f7d79188352b10e54c9 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.class index 8ad1a9e13a4bda5e25887a5aab25d99113405907..9dec0beec750150ab9b42b2b561765e0d2f5f8a5 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/CompanyServiceImpl.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.class index 26391a03bc3ab02727f3298cfefbca83ab08cd5f..6f3c8fcd9716487419fb0ead7d4b7323b6d037ae 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.class index f6bb8ccbf30bcc046b83cc026ed53ce875ef1e96..d5a5343207922c205e653ed28e7dd87cee2c2beb 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/JobOfferServiceImpl.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.class new file mode 100644 index 0000000000000000000000000000000000000000..93d201cfce40c6c46b25893c228e9250cb46f360 Binary files /dev/null and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/MatchingIndexService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorService.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorService.class index 16a09ecf2262483ee0bebe3ff3837ef520291c49..676704a05351c416b8605f7221479f20fceead8e 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorService.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorService.class differ diff --git a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.class b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.class index b57324062d1f8c4154ef0ea63660752754e0d559..fa1818274996582b09f5a97b95b8ae84385f01a9 100644 Binary files a/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.class and b/target/classes/fr/atlantique/imt/inf211/jobmngt/service/SectorServiceImpl.class differ diff --git a/target/classes/static/css/gyj_imt.css b/target/classes/static/css/gyj_imt.css index fae8405c753b39e5943feb2ea79137246e36ac93..4cc3cb88e6e4d3e34484948d256df7d7b40b7287 100644 --- a/target/classes/static/css/gyj_imt.css +++ b/target/classes/static/css/gyj_imt.css @@ -7,6 +7,9 @@ #header_div{ color: white; background-color: rgb(179, 199, 71); + padding: 10px; + + } #tiny_text{ @@ -20,7 +23,7 @@ #content_div{ background-color: white; - + } #central_div{ padding: 10px; @@ -32,6 +35,15 @@ color: white; background-color: rgb(0, 32, 65); } +.favorite_color { + color: rgb(0, 184, 222); +} +.favorite_back { + background-color: rgb(0, 184, 222); +} +.favorite_outline { + border-color: rgb(0, 184, 222); +} .logo{ width:30px; @@ -148,4 +160,82 @@ .card-black h2 { color: white !important; } +/* Ajoutez ces styles pour améliorer l'interface */ +.navbar { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + + +.alert-info { + background-color: #f8f9fa; + border-left: 4px solid #072262; + color: #072262; +} + +.nav-link { + font-weight: 500; +} + +.btn-outline-light:hover { + color: #072262 !important; +} +/* Dashboard */ +.card { + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: transform 0.3s; +} + +.card:hover { + transform: translateY(-3px); +} + +.card-header { + border-radius: 10px 10px 0 0 !important; +} + +/* Profil */ +dl.row dt { + font-weight: normal; + color: #6c757d; +} + +.list-group-item { + border-left: 3px solid transparent; + transition: all 0.3s; +} + +.list-group-item:hover { + border-left-color: #072262; + background-color: #f8f9fa; +} +/* Style cohérent pour l'interface anglaise */ +.navbar-brand { + font-weight: 600; +} + + +.btn-outline-danger { + border-color: #dc3545; + color: #dc3545; +} +/* Style pour la bannière de bienvenue */ +.welcome-banner { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.welcome-banner:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.welcome-banner i { + font-size: 1.5rem; +} +/* Style pour le nom utilisateur */ +.text-white { + font-size: 1rem; + vertical-align: middle; +} diff --git a/target/classes/templates/application/application-confirmation.html b/target/classes/templates/application/application-confirmation.html index 49c8ab4c0d24fd87c8419c8f3120fa7036c754d8..aa8fd92be082199e7a0ef77c5be8231d4a35f426 100644 --- a/target/classes/templates/application/application-confirmation.html +++ b/target/classes/templates/application/application-confirmation.html @@ -1,36 +1,130 @@ -<!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Confirmation de Candidature</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <div class="alert alert-success mt-5 text-center"> - <h2> Votre candidature a été soumise avec succès !</h2> - </div> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Application Confirmation</title> +<section> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <style> + .confirmation-card { + border-radius: 12px; + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08); + border: none; + overflow: hidden; + } + .confirmation-header { + background:#00B8DEFF; + color: white; + padding: 1.8rem; + text-align: center; + } + .confirmation-icon { + font-size: 2.5rem; + margin-bottom: 1rem; + } + .detail-card { + border-left: 4px solid #00B8DEFF; + border-radius: 8px; + margin-bottom: 1.5rem; + } + .detail-item { + padding: 1rem 1.5rem; + border-bottom: 1px solid #f0f0f0; + } + .detail-item:last-child { + border-bottom: none; + } + .sector-badge { + background-color: #e8f0fe; + color: #00B8DEFF; + padding: 6px 12px; + border-radius: 18px; + margin-right: 8px; + margin-bottom: 8px; + display: inline-block; + font-size: 0.85rem; + } + .cv-link i { + margin-right: 8px; + } + .timestamp { + color: #5f6368; + font-size: 0.9rem; + } + </style> + </head> + <body> + <div class="container py-5"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <!-- Confirmation Card --> + <div class="card confirmation-card"> + <div class="confirmation-header"> + <div class="confirmation-icon"> + <i class="fas fa-check-circle"></i> + </div> + <h2>Application Submitted Successfully</h2> + <p class="mb-0">Thank you for your application</p> + </div> - <h3 class="mt-4">📄 Détails de la candidature :</h3> + <div class="card-body p-4"> + <!-- Application Details --> + <div class="card detail-card mb-4"> + <div class="card-body p-0"> + <div class="detail-item"> + <h5 class="text-muted mb-3">Application Details</h5> + <dl class="row"> + <dt class="col-sm-4">Application Number</dt> + <dd class="col-sm-8 font-weight-bold" th:text="${appId}"></dd> - <div th:if="${application != null}"> - <table class="table table-bordered"> - <tr><th>ID</th><td th:text="${application.id != null ? application.id : 'Non spécifié'}"></td></tr> - <tr><th> CV</th><td th:text="${application.cv != null ? application.cv : 'Non spécifié'}"></td></tr> - <tr><th> Niveau de qualification</th> - <td th:text="${application.qualificationlevel != null ? application.qualificationlevel.label : 'Non spécifié'}"></td> - </tr> - <tr><th> Secteurs d'activité</th> - <td th:each="sector : ${application.sectors}" th:text="${sector.label}"></td> - </tr> - <tr><th> Date de dépôt</th> - <td th:text="${application.appdate != null ? application.appdate : 'Non spécifié'}"></td> - </tr> - - </table> - </div> + <dt class="col-sm-4">Submission Date</dt> + <dd class="col-sm-8"> + <span th:text="${#temporals.format(appDate, 'dd MMMM yyyy - HH:mm')}" class="timestamp"></span> + </dd> + </dl> + </div> + </div> + </div> + + <!-- Candidate Information --> + <div class="card detail-card"> + <div class="card-body p-0"> + <div class="detail-item"> + <h5 class="text-muted mb-3">Your Information</h5> + <dl class="row"> + <dt class="col-sm-4">CV Document</dt> + <dd class="col-sm-8"> + <span th:text="${appCv} ?: 'Not specified'" + class="badge favorite_back text-white"></span> + </dd> + + <dt class="col-sm-4">Qualification Level</dt> + <dd class="col-sm-8"> + <span th:text="${qualification?.label} ?: 'Not specified'" + class="badge favorite_back text-white"></span> + </dd> - <a href="/" class="btn btn-primary mt-3"> Retour à l'accueil</a> + <dt class="col-sm-4">Activity Sectors</dt> + <dd class="col-sm-8"> + <div th:each="sector : ${sectors}"> + <span class="sector-badge" th:text="${sector.label}"></span> + </div> + <div th:if="${#lists.isEmpty(sectors)}" class="text-muted"> + No sectors specified + </div> + </dd> + </dl> + </div> + </div> + </div> + + </div> + </div> + </div> + </div> </div> -</body> -</html> + + <!-- Font Awesome for icons --> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/application/application-details.html b/target/classes/templates/application/application-details.html index 4a53d5b0d38cedcd9c3949f8e676ea2f5cfa13eb..1f060334a0d5b279a5cef6e7f9dddf3f66760672 100644 --- a/target/classes/templates/application/application-details.html +++ b/target/classes/templates/application/application-details.html @@ -1,49 +1,113 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <head> - <meta charset="UTF-8"> - <title>Détails de la Candidature</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <title>Applications</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + :root { + --primary: #072262; + --accent: #1ABC9C; + --text-primary: #333333; + --text-secondary: #666666; + --bg-light: #F5F5F5; + --border-light: #E0E0E0; + } + + .favorite_color { + color: var(--primary); + } + + .favorite_back { + background-color: var(--accent); + color: white; + border: none; + } + + .favorite_outline { + border-color: var(--accent); + color: var(--accent); + } + + .favorite_outline:hover { + background-color: var(--accent); + color: white; + } + </style> </head> <body> - <div class="container mt-5"> - <h2 class="mb-4"> Détails de la Candidature</h2> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Application Details</h2> + </header> - <div th:if="${error}" class="alert alert-warning"> - <p th:text="${error}"></p> - </div> + <!-- Alert Messages --> + <div th:if="${error}" class="alert alert-warning alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${error}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-person-badge"></i> Candidate Information + </h4> - <div class="card"> - <div class="card-body"> - <h5 class="card-title"> - Nom: - <span th:if="${application.candidate != null}" th:text="${application.candidate.firstname + ' ' + application.candidate.lastname}"></span> - <span th:unless="${application.candidate != null}">Non disponible</span> - </h5> - - <p class="card-text"> - <span th:if="${application.cv != null}" th:text="'CV: ' + ${application.cv}"></span> - <span th:unless="${application.cv != null}">Non disponible</span> - </p> - - <p class="card-text"> - <span th:if="${application.qualificationlevel != null}" th:text="'Niveau de Qualification: ' + ${application.qualificationlevel.label}"></span> - <span th:unless="${application.qualificationlevel != null}">Non disponible</span> - </p> - - <p class="card-text"> - <span th:each="sector : ${application.sectors}" th:text="'Secteur: ' + ${sector.label}"></span> - <span th:if="${#lists.isEmpty(application.sectors)}">Secteurs non disponibles</span> - </p> - - <p class="card-text"> - <span th:if="${application.appdate != null}" th:text="'Date de dépôt: ' + ${application.appdate}"></span> - <span th:unless="${application.appdate != null}">Non disponible</span> - </p> + <div class="mb-3"> + <label class="form-label fw-semibold">Full Name</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${candidate.firstname + ' ' + candidate.lastname}"></span> + </div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">CV</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${cv}"></span> + </div> + </div> + <div class="mb-3"> + <label class="form-label fw-semibold">Application Date</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${#temporals.format(appdate, 'MMMM dd, yyyy')}"></span> + </div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-award"></i> Qualifications + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Qualification Level</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:text="${qualification.label}"></span> + </div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Sectors</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:each="sector : ${sectors}" class="badge favorite_back me-1 mb-1" + th:text="${sector.label}"></span> + </div> + </div> + </div> </div> </div> - - <a th:href="@{/applications/list}" class="btn btn-primary mt-3">Retour à la liste des candidatures</a> </div> + + <footer class="d-flex justify-content-between mt-4"> + <a th:href="@{/applications/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to Applications List + </a> + </footer> +</section> </body> </html> \ No newline at end of file diff --git a/target/classes/templates/application/application-edit.html b/target/classes/templates/application/application-edit.html index f68c9eca601803bc0fefcd04d0de283c53478c41..6a3bb686d472ca1cce48b3a2c36144cdab9926e8 100644 --- a/target/classes/templates/application/application-edit.html +++ b/target/classes/templates/application/application-edit.html @@ -1,17 +1,240 @@ -<form th:action="@{/applications/update}" method="post"> - <input type="hidden" name="id" th:value="${application.id}"> - <label>CV :</label> - <input type="text" name="cv" th:value="${application.cv}" required> - - <label>Niveau de qualification :</label> - <select name="qualificationLevel"> - <option th:each="q : ${qualifications}" th:value="${q.id}" th:text="${q.label}"></option> - </select> - - <label>Secteurs :</label> - <select name="sectors" multiple> - <option th:each="s : ${sectors}" th:value="${s.id}" th:text="${s.label}"></option> - </select> - - <button type="submit">Modifier</button> -</form> + <!DOCTYPE html> + <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> + <title>Edit Job Application</title> + <section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: #00B8DEFF; + color: white; + border-color: #00B8DEFF; + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .file-upload { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s; + } + .file-upload:hover { + border-color: #00B8DEFF; + background-color: #f8f9fa; + } + .file-upload i { + font-size: 2rem; + color: #6c757d; + } + .file-name { + margin-top: 10px; + font-weight: bold; + color: #00B8DEFF; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + Edit Job Application + </h2> + <form th:action="@{/applications/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${id}" /> + <!-- Improved CV Field --> + <div class="mb-3"> + <label class="form-label">Resume*</label> + <div class="file-upload" onclick="document.getElementById('cvFile').click()"> + <i class="bi bi-upload"></i> + <div id="cvText">Click to upload your resume</div> + <div id="fileName" class="file-name d-none"></div> + <input type="file" id="cvFile" class="d-none bold" accept=".pdf,.doc,.docx"> + <input type="hidden" id="cv" name="cv" th:text="${cv}" required> + </div> + <small class="text-muted">Accepted formats: PDF, DOC, DOCX</small> + <div class="invalid-feedback">Please upload your resume</div> + </div> + + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${appDate != null} ? ${#temporals.format(appDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + + <!-- Qualification Level --> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualifications}" + th:value="${level.id}" + th:text="${level.label}" + th:selected="${qualApp?.id == level.id}"> + </option> + </select> + </div> + + <!-- Industry Sectors - Version corrigée --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + + <!-- Liste complète des secteurs --> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:classappend="${#lists.contains(sectorApp.![id], sector.id)} ? 'selected' : ''" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + + <!-- Champ affichant les secteurs sélectionnés --> + <div id="selectedSectorsDisplay" class="mb-2"> + <span th:if="${sectorApp.empty}">No sectors selected</span> + <th:block th:each="sector : ${sectorApp}"> + <div th:data-id="${sector.id}"> + <span th:text="${sector.label}"></span> + </div> + </th:block> + </div> + + <input type="hidden" id="sectorIds" name="sectorIds" + th:value="${#strings.listJoin(sectorApp.![id], ',')}" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> + + <!-- Buttons --> + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/applications/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Save Changes + </button> + </div> + </form> + </div> + </div> + + <script> + // Initialize with already selected sectors from the application object + const selectedSectors = new Set( + document.getElementById('sectorIds').value.split(',').filter(Boolean) + ); + + // Mark selected sectors on page load + document.addEventListener('DOMContentLoaded', function() { + updateSelectedDisplay(); + + // Mark initially selected sectors in the list + selectedSectors.forEach(sectorId => { + const element = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (element) { + element.classList.add('selected'); + } + }); + }); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + const sectorLabel = element.textContent; + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectorsDisplay'); + const hiddenInput = document.getElementById('sectorIds'); + + // Clear the display + displayDiv.innerHTML = ''; + + if (selectedSectors.size === 0) { + displayDiv.innerHTML = '<span>No sectors selected</span>'; + } else { + // Add selected sectors as tags + selectedSectors.forEach(sectorId => { + const sectorElement = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (sectorElement) { + const tag = document.createElement('div'); + tag.setAttribute('data-id', sectorId); + tag.innerHTML = `<span>${sectorElement.textContent}</span>`; + displayDiv.appendChild(tag); + } + }); + } + + // Update hidden input with selected sector IDs + hiddenInput.value = Array.from(selectedSectors).join(','); + } + + + + // File upload handling + document.getElementById('cvFile').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + document.getElementById('cvText').classList.add('d-none'); + document.getElementById('fileName').textContent = file.name; + document.getElementById('fileName').classList.remove('d-none'); + document.getElementById('cv').value = file.name; + } + }); + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> + </section> + </html> \ No newline at end of file diff --git a/target/classes/templates/application/application-list.html b/target/classes/templates/application/application-list.html index 2200f3182c91b13442415c476cc118d9565b44c9..2bdac216bc0d8f9774dcfe73ddbbbcc0bf3a6e37 100644 --- a/target/classes/templates/application/application-list.html +++ b/target/classes/templates/application/application-list.html @@ -1,75 +1,133 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Liste des Candidatures</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container mt-5"> - <h2 class="mb-4">Liste des Candidatures</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Applications</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .application-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .application-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .sector-badge { + margin-right: 4px; + margin-bottom: 4px; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> + </head> + <body class="bg-light"> - <div th:if="${error}" class="alert alert-warning"> - <p th:text="${error}"></p> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-file-earmark-text me-2"></i> Job Applications + </h2> </div> - <table class="table table-bordered table-striped" th:if="${applicationsList}"> - <thead> - <tr> - <th>ID</th> - <th>Nom</th> - <th>Prénom</th> - <th>CV</th> - <th>Niveau de Qualification</th> - <th>Secteurs d'activité</th> - <th>Date de dépôt</th> - <th>Détails</th> - <th>Modifier</th> - <th>Supprimer</th> - </tr> - </thead> - <tbody> - <tr th:each="app : ${applicationsList}"> - <td th:text="${app.id}"></td> - <td th:text="${app.candidate.firstname}"></td> - <td th:text="${app.candidate.lastname}"></td> - <td><a th:href="@{${app.cv}}" th:text="${app.cv}"></a></td> - <td th:text="${app.qualificationlevel.label}"></td> - <td> - <ul> - <li th:each="sector : ${app.sectors}" th:text="${sector.label}"></li> - </ul> - </td> - <td th:text="${app.appdate}"></td> - <td> - <a th:href="@{/applications/details/{id}(id=${app.id})}" class="btn btn-info">Détails</a> - </td> - <!-- Vérification que l'utilisateur est connecté et est le propriétaire --> - <td th:if="${session.uid != null && session.uid == app.candidate.id}"> - <a th:href="@{/applications/edit/{id}(id=${app.id})}" class="btn btn-warning">Modifier</a> - </td> - <td th:if="${session.uid != null && session.uid == app.candidate.id}"> - <a th:href="@{/applications/delete/{id}(id=${app.id})}" class="btn btn-danger" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette candidature ?');"> - Supprimer - </a> - </td> - <!-- Affichage d'un message si l'utilisateur n'est pas connecté --> - <td th:if="${session.uid == null}" colspan="2"> - <a th:href="@{/login}" class="btn btn-secondary">Se connecter</a> - </td> - </tr> - </tbody> - </table> + <!-- Applications Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(applicationsList)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">First Name</th> + <th>Last Name</th> + <th>Qualification</th> + <th>Sectors</th> + <th>Applied Date</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="app : ${applicationsList}" class="application-card"> + <td class="ps-4 fw-semibold" th:text="${app.candidate.firstname}"></td> + <td th:text="${app.candidate.lastname}"></td> + <td th:text="${app.qualificationlevel?.label} ?: 'N/A'"></td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${app.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${#temporals.format(app.appdate, 'yyyy-MM-dd')}"></td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/applications/details/' + ${app.id}}" + class="btn btn-sm favorite_outline"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == app.candidate.id}"> + <a th:href="@{'/applications/edit/' + ${app.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/applications/delete/' + ${app.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this application?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> - <a th:href="@{/}" class="btn btn-primary mt-3">Retour à l'accueil</a> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(applicationsList)}" class="text-center py-5"> + <i class="bi bi-file-earmark-x empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No applications found</h4> + </div> + </div> + </div> + </div> </div> -</body> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> </html> +<!-- +</td> +<td> + <a th:href="@{/applications/details/{id}(id=${app.id})}" class="btn btn-info">Détails</a> +</td> + +<td th:if="${session.uid != null && session.uid == app.candidate.id}"> + <a th:href="@{/applications/edit/{id}(id=${app.id})}" class="btn btn-warning">Modifier</a> +</td> +<td th:if="${session.uid != null && session.uid == app.candidate.id}"> + <a th:href="@{/applications/delete/{id}(id=${app.id})}" class="btn btn-danger" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette candidature ?');"> + Supprimer + </a> +</td> +<td th:if="${session.uid == null}" colspan="2"> + <a th:href="@{/login}" class="btn btn-secondary">Se connecter</a> +</td> +!--> diff --git a/target/classes/templates/application/application-update-form.html b/target/classes/templates/application/application-update-form.html index 26266c1eb803c2a61e4fa4d794429159bb970af7..3eefb8c3042e099ffd27c19e822afd349f987ff4 100644 --- a/target/classes/templates/application/application-update-form.html +++ b/target/classes/templates/application/application-update-form.html @@ -1,6 +1,8 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Applications</title> +<section> + <head> <meta charset="UTF-8"> <title>Mettre à jour la Candidature</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> @@ -12,7 +14,7 @@ <form th:action="@{/applications/update/{id}(id=${application.id})}" method="post"> <div class="form-group"> <label for="cv">CV</label> - <input type="text" id="cv" name="cv" class="form-control" th:value="${application.cv}" required> + <input type="text" id="cv" name="cvPath" class="form-control" th:value="${application.cv}" required> </div> <div class="form-group"> @@ -37,10 +39,11 @@ </select> </div> - <button type="submit" class="btn btn-primary mt-3">Mettre à jour</button> + <button type="submit" class="btn favorite_back mt-3">Mettre à jour</button> </form> <a th:href="@{/applications/list}" class="btn btn-secondary mt-3">Retour à la liste des candidatures</a> </div> </body> +</section> </html> diff --git a/target/classes/templates/application/apply.html b/target/classes/templates/application/apply.html index 4615526e200b1aff678ccab99012d1ea5eb414a7..66f297c302e5475ecda82aa0793b4ead4a4db82e 100644 --- a/target/classes/templates/application/apply.html +++ b/target/classes/templates/application/apply.html @@ -1,74 +1,192 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Postuler à une offre</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> - <style> - body { - background-color: #f8f9fa; /* Couleur de fond */ - } - .container { - max-width: 600px; - margin-top: 50px; - background: #ffffff; - padding: 20px; - border-radius: 10px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); - } - h2 { - text-align: center; - color: #007bff; /* Bleu Bootstrap */ - } - label { - font-weight: bold; - color: #343a40; - } - .form-control { - margin-bottom: 10px; - } - .btn-submit { - background-color: #007bff; - color: white; - width: 100%; - } - .btn-submit:hover { - background-color: #0056b3; - } - </style> -</head> -<body> - <div class="container"> - <h2> Postuler à une offre</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Job Application</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: #0d6efd; + color: white; + border-color: #0d6efd; + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .file-upload { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s; + } + .file-upload:hover { + border-color: #0d6efd; + background-color: #f8f9fa; + } + .file-upload i { + font-size: 2rem; + color: #6c757d; + } + .file-name { + margin-top: 10px; + font-weight: bold; + color: #0d6efd; + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-person-plus me-2"></i> + Apply for Job + </h2> - <form th:action="@{/applications/apply}" method="post"> - <!-- Champ CV --> - <div class="form-group"> - <label for="cv">CV :</label> - <input type="text" id="cv" name="cv" class="form-control" placeholder="Lien vers votre CV" required> - </div> + <form th:action="@{/applications/apply}" method="post" class="needs-validation" novalidate> + <!-- Improved CV Field --> + <div class="mb-3"> + <label class="form-label">Resume*</label> + <div class="file-upload" onclick="document.getElementById('cvFile').click()"> + <i class="bi bi-upload"></i> + <div id="cvText">Click to upload your resume</div> + <div id="fileName" class="file-name d-none"></div> + <input type="file" id="cvFile" class="d-none" accept=".pdf,.doc,.docx"> + <input type="hidden" id="cv" name="cv" required> + </div> + <small class="text-muted">Accepted formats: PDF, DOC, DOCX</small> + <div class="invalid-feedback">Please upload your resume</div> + </div> - <!-- Niveau de qualification --> - <div class="form-group"> - <label for="qualificationLevel">Niveau de qualification :</label> - <select id="qualificationLevel" name="qualificationLevel" class="form-control" required> - <option value="">Choisir un niveau</option> - <option th:each="qualification : ${qualifications}" th:value="${qualification.id}" th:text="${qualification.label}"></option> - </select> - </div> + <!-- Qualification Level --> + <div class="mb-3"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="qualification : ${qualifications}" + th:value="${qualification.id}" + th:text="${qualification.label}"></option> + </select> + </div> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${application.appdate != null} ? ${#temporals.format(jobOffer.appdate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> - <!-- Secteurs d'activité --> - <div class="form-group"> - <label for="sectors">Secteurs d'activité :</label> - <select id="sectors" name="sectors" class="form-control" multiple required> - <option th:each="sector : ${sectors}" th:value="${sector.id}" th:text="${sector.label}"></option> - </select> - <small class="form-text text-muted">Maintenez la touche Ctrl (Cmd sur Mac) pour sélectionner plusieurs options.</small> - </div> + <!-- Improved Industry Sectors Selection --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + <div id="selectedSectors" class="mb-2">No sectors selected</div> + <input type="hidden" id="sectorIds" name="sectors" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> - <!-- Bouton de soumission --> - <button type="submit" class="btn btn-submit"> Soumettre</button> - </form> + <!-- Buttons --> + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/jobs}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn favorite_back"> + <i class="bi bi-send me-1"></i> Submit Application + </button> + </div> + </form> + </div> </div> -</body> -</html> + + <script> + // Sector selection management + const selectedSectors = new Set(); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectors'); + const hiddenInput = document.getElementById('sectorIds'); + + if (selectedSectors.size === 0) { + displayDiv.textContent = 'No sectors selected'; + hiddenInput.value = ''; + } else { + displayDiv.textContent = Array.from(selectedSectors).map(id => { + return document.querySelector(`.sector-item[data-id="${id}"]`).textContent; + }).join(', '); + + hiddenInput.value = Array.from(selectedSectors).join(','); + } + } + + // File upload handling + document.getElementById('cvFile').addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + document.getElementById('cvText').classList.add('d-none'); + document.getElementById('fileName').textContent = file.name; + document.getElementById('fileName').classList.remove('d-none'); + document.getElementById('cv').value = file.name; + } + }); + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/baseTemplate/base.html b/target/classes/templates/baseTemplate/base.html index f480ca2cf07d454514c49b5be5e8b86d5a3d49f8..264b500da1438921058df091ac1acc1c00b07dc7 100644 --- a/target/classes/templates/baseTemplate/base.html +++ b/target/classes/templates/baseTemplate/base.html @@ -7,44 +7,64 @@ <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <link th:href="@{/css/gyj_imt.css}" rel="stylesheet"> <link th:href="@{/css/bootstrap-icons.min.css}" rel="stylesheet"> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> - + <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script th:src="@{/js/bootstrap.bundle.min.js}"></script> <script th:src="@{/js/gyj_imt.js}"></script> <script src="http://localhost:35729/livereload.js"></script> - <link rel="icon" th:href="@{/img/favicon-32x32.png}" sizes="32x32" type="image/png"> <title th:replace="${title}">IMT Atlantique: Get Your Job</title> + <style> + html, body { + height: 100%; + } + + body { + display: flex; + flex-direction: column; + } + + main { + flex: 1 0 auto; + padding-bottom: 2rem; /* Espace avant le footer */ + } + footer { + flex-shrink: 0; + background-color: #f8f9fa; + padding: 1rem 0; + } + </style> </head> -<body> - <nav th:insert="~{/baseTemplate/nav :: fheader}" /> - <main class="container"> - <th:block th:replace="${content}"> - Empty Page - </th:block> - <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="exampleModalLabel">Confirmation of deletion</h5> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="modal-body"> - <p class="alert alert-danger">Are you sure you want to <span id="delRecord"> </span> ?</p> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> - <a href="" class="btn btn-danger" id="delRef">Delete</a> - </div> +<body class="d-flex flex-column min-vh-100"> +<nav th:insert="~{/baseTemplate/nav :: fheader}" /> + +<main class="container flex-grow-1"> + <th:block th:replace="${content}"> + Empty Page + </th:block> + <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">Confirmation of deletion</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <p class="alert alert-danger">Are you sure you want to <span id="delRecord"> </span> ?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <a href="" class="btn btn-danger" id="delRef">Delete</a> </div> </div> </div> - </main> - <footer class="mt-4" th:replace="~{/baseTemplate/footer:: footer}" /> + </div> +</main> + +<footer class="mt-auto py-3 bg-light" th:replace="~{/baseTemplate/footer:: footer}" /> </body> </html> \ No newline at end of file diff --git a/target/classes/templates/baseTemplate/footer.html b/target/classes/templates/baseTemplate/footer.html index bfe41e52f59372904d60a452cc5858c9c2b2a4c5..a6172ccda86d50607e66223d1862ec362fddb8d4 100644 --- a/target/classes/templates/baseTemplate/footer.html +++ b/target/classes/templates/baseTemplate/footer.html @@ -1,5 +1,5 @@ <footer th:fragment="ffooter"> - <div id="footer_div" class="row"> + <div id="footer_div" class="row"> <div class="col"> IMT Atlantique - FIP </div> diff --git a/target/classes/templates/baseTemplate/head.html b/target/classes/templates/baseTemplate/head.html index 28f9bec37142655cc47cfd20d30a8197ef211892..e60a2d4d353c7526f7d35531bbc583c83a9a9aa3 100644 --- a/target/classes/templates/baseTemplate/head.html +++ b/target/classes/templates/baseTemplate/head.html @@ -18,4 +18,6 @@ <title>IMT Atlantique: Get Your Job</title> </head> <body> + </body> + </html> </div> \ No newline at end of file diff --git a/target/classes/templates/baseTemplate/nav.html b/target/classes/templates/baseTemplate/nav.html index 36532658f8e5cad9ec42eda3dd4815dcf37db6ef..08e5b977f01fce4b3bc72bc6a5f93630b013565a 100644 --- a/target/classes/templates/baseTemplate/nav.html +++ b/target/classes/templates/baseTemplate/nav.html @@ -1,31 +1,20 @@ -//header.html <div th:fragment="fheader"> <div id="header_div" class="row h-10"> <div class="col-2 align-self-start"> <img th:src="@{/img/logo_imt.png}" /> </div> - <div class="col-6 align-self-center"></div> - <div class="col-2 align-self-end"> - <p th:if="${#ctx.session.uid} != null"> - <i class="bi bi-" style="font-size: 2rem; color: white;" th:title="${#ctx.session.usertype}" - th:attrappend="class=${#ctx.session.usertype=='company'?'buildings':'person'}"></i> - <span th:text="${#ctx.session.user.mail}" class="tiny_text" /> - </p> - </div> - - <div class="col-2 align-self-end"> - <th:block th:if="${#ctx.session.uid} != null"> - <a th:if="${#ctx.session.hasMessages == true}" th:href="@{/messages}" title="access to your message"><i - class="bi bi-envelope-at" style="font-size: 2rem; color: white;"></i></a> - <a href="/logout" title="logout from the webapp"><i class="bi bi-box-arrow-in-up" - style="font-size: 2rem; color: white;"></i></a> - </th:block> - <a th:if="${#ctx.session.uid} == null" title="Click to login" href="/login"> - <i class="bi bi-box-arrow-in-down" style="font-size: 2rem; color: white;" - title="login to access your data"></i> + <div class="col-8"></div> + <div class="col-2 text-end "> + <!-- Bouton de connexion/déconnexion seulement --> + <div th:if="${session.loggedInUser != null}"> + <a href="/logout" class="btn text-white fw-bold "> + <i class="bi bi-box-arrow-right"></i> Logout + </a> + </div> + <a th:unless="${session.loggedInUser != null}" href="/login" class="btn text-white fw-bold"> + <i class="bi bi-box-arrow-right"></i> Login </a> </div> - </div> <nav class="navbar navbar-expand-md navbar-light"> <div class="container-fluid"> @@ -41,24 +30,22 @@ <a class="nav-link" th:href="@{/companies}">Companies</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/jobs}">Jobs</a> + <a class="nav-link" th:href="@{/candidates/list}">Candidates</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/candidates/list}">Candidates</a> + <a class="nav-link" th:href="@{/jobs}">Job Offers</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/applications/list}">Applications</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/qualificationLevels}">Qualification levels</a> + <a class="nav-link" th:href="@{/sectors}">Sectors</a> </li> <li class="nav-item"> - <a class="nav-link" th:href="@{/sectors}">Sectors</a> + <a class="nav-link" th:href="@{/qualificationLevels}">Qualification Levels</a> </li> </ul> - </div> </div> </nav> - </div> \ No newline at end of file diff --git a/target/classes/templates/candidate/candidates-list.html b/target/classes/templates/candidate/candidates-list.html index 039447c90e40d3dca0a29cd98e7432b9eb005b83..283fddec8b427e79d231b5d2412e12ba63ea177e 100644 --- a/target/classes/templates/candidate/candidates-list.html +++ b/target/classes/templates/candidate/candidates-list.html @@ -1,44 +1,106 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Liste des Candidats</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <h2 class="mt-5">Liste des Candidats</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <head> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .candidate-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .candidate-card:hover { + transform: translateY(-2px); + border-left-color: #3a7bd5; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .actions-cell { + white-space: nowrap; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> + </head> + <body class="bg-light"> - <!-- Message d'erreur --> - <div th:if="${error}" class="alert alert-danger" th:text="${error}"></div> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-people me-2"></i> Candidates List + </h2> - <table class="table table-striped mt-3"> - <thead> - <tr> - <th>ID</th> - <th>Nom</th> - <th>Prénom</th> - <th>Ville</th> - <th>Email</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="candidate : ${candidates}"> - <td th:text="${candidate.id}"></td> - <td th:text="${candidate.lastname}"></td> - <td th:text="${candidate.firstname}"></td> - <td th:text="${candidate.city}"></td> - <td th:text="${candidate.mail}"></td> - <td> - <a th:href="@{/candidates/details/{id}(id=${candidate.id})}" class="btn btn-info btn-sm">Détails</a> - <span th:if="${#ctx.session.uid} != null and ${#ctx.session.uid} == ${candidate.id} ">PEUT SUPPRIMER</span> - </td> - </tr> - </tbody> - </table> + </div> - <a th:href="@{/}" class="btn btn-secondary mt-3">Retour</a> + + <!-- Candidates Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(candidates)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">Last Name</th> + <th>First Name</th> + <th>City</th> + <th>Applications Submitted</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="candidate : ${candidates}" class="candidate-card"> + <td class="ps-4 fw-semibold" th:text="${candidate.lastname}"></td> + <td th:text="${candidate.firstname}"></td> + <td> + <i class="bi bi-geo-alt text-secondary me-1"></i> + <span th:text="${candidate.city}"></span> + </td> + <td> + <span class="badge favorite_back rounded-pill" + th:text="${candidate.getCandidatAppCount()}"></span> + </td> + <td class="text-end pe-4 actions-cell"> + <div > + <a th:href="@{/candidates/details/{id}(id=${candidate.id})}" + class="btn btn-sm favorite_outline" + aria-label="View candidate details"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == candidate.id}"> + <a th:href="@{'/candidates/edit/' + ${candidate.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/candidates/delete/' + ${candidate.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this candidate?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(candidates)}" class="text-center py-5"> + <i class="bi bi bi-people empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No candidates registered</h4> + </div> + </div> + </div> + </div> </div> -</body> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> </html> diff --git a/target/classes/templates/candidate/confirmation.html b/target/classes/templates/candidate/confirmation.html index 8a22d725d2dd69e9120247d20355b72a469b08a1..bd630bfca8b700513c24eaf217c1be4aa1dda57c 100644 --- a/target/classes/templates/candidate/confirmation.html +++ b/target/classes/templates/candidate/confirmation.html @@ -1,34 +1,97 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Confirmation</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Confirmation</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .confirmation-card { + max-width: 800px; + margin: 2rem auto; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); + border: none; + } + .confirmation-header { + background: rgb(0, 184, 222); + color: white; + padding: 2rem; + text-align: center; + } + .confirmation-icon { + font-size: 3.5rem; + margin-bottom: 1rem; + } + .info-card { + border-left: 4px solid #00B8DEFF; + background-color: #f8f9fa; + } + .btn-custom { + background: rgb(0, 184, 222); + border: none; + padding: 10px 25px; + font-weight: 500; + transition: all 0.3s; + } + .btn-custom:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4); + } + </style> + </head> + <body class="bg-light"> <div class="container"> - <div class="alert alert-success mt-5 text-center"> - <h2>Operation Successful</h2> - <p th:text="${message}"></p> - </div> + <div class="card confirmation-card"> + <div class="confirmation-header"> + <i class="bi bi-check-circle-fill confirmation-icon"></i> + <h2 class="mb-3">Opération réussie</h2> + <p class="lead mb-0" th:text="${message}"></p> + </div> - <!-- Affichage des infos du candidat uniquement si c'est une modification --> - <div th:if="${candidate != null}"> - <h3>Informations mises à jour :</h3> - <table class="table table-bordered"> - <tr><th>ID</th><td th:text="${candidate.id}"></td></tr> - <tr><th>Nom</th><td th:text="${candidate.lastname}"></td></tr> - <tr><th>Prénom</th><td th:text="${candidate.firstname}"></td></tr> - <tr><th>Email</th><td th:text="${candidate.mail}"></td></tr> - <tr><th>Ville</th><td th:text="${candidate.city}"></td></tr> - </table> - </div> + <div class="card-body p-4"> + <!-- Affichage des infos du candidat --> + <div th:if="${candidate != null}" class="mb-4"> + <h4 class="mb-3 text-primary"> + <i class="bi bi-person-badge me-2"></i>Informations mises à jour + </h4> + + <div class="row"> + <div class="col-md-6 mb-3"> + <div class="p-3 info-card"> + <h6 class="text-muted">Identité</h6> + <p class="mb-1"><strong th:text="${candidate.firstname + ' ' + candidate.lastname}"></strong></p> + <small class="text-muted" th:text="'ID: ' + ${candidate.id}"></small> + </div> + </div> + + <div class="col-md-6 mb-3"> + <div class="p-3 info-card"> + <h6 class="text-muted">Coordonnées</h6> + <p class="mb-1" th:text="${candidate.mail}"></p> + <p class="mb-0" th:text="${candidate.city}"></p> + </div> + </div> + </div> + </div> - <!-- Bouton différent selon la situation --> - <div class="text-center"> - <a th:if="${candidate != null}" th:href="@{/candidates/list}" class="btn btn-primary">Retour à la liste des candidats</a> - <a th:if="${candidate == null}" th:href="@{/login}" class="btn btn-primary">Go to Login</a> + <!-- Bouton d'action --> + <div class="text-center mt-4"> + <a th:if="${candidate != null}" + th:href="@{/candidates/list}" + class="btn btn-custom text-white"> + <i class="bi bi-arrow-left me-2"></i>Retour à la liste + </a> + <a th:if="${candidate == null}" + th:href="@{/login}" + class="btn btn-custom text-white"> + <i class="bi bi-box-arrow-in-right me-2"></i>Se connecter + </a> + </div> + </div> </div> </div> -</body> -</html> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/candidate/confirmationSupp.html b/target/classes/templates/candidate/confirmationSupp.html index a8ebbf083b54a7d5ec3e83107bcd51533c418614..9563393fc887f28959c4e4fdc444423113b14733 100644 --- a/target/classes/templates/candidate/confirmationSupp.html +++ b/target/classes/templates/candidate/confirmationSupp.html @@ -12,7 +12,7 @@ <p th:text="${message}"></p> </div> - <a th:href="@{/candidates/list}" class="btn btn-primary">Retour à la liste des candidats</a> + <a th:href="@{/candidates/list}" class="btn favorite_back">Retour à la liste des candidats</a> </div> </body> </html> diff --git a/target/classes/templates/candidate/details.html b/target/classes/templates/candidate/details.html index 7e2ea97e99c78a87d1b2d7395335abc4398d9579..9e7c5948f0ea08debc6319fc54d49fac92d82420 100644 --- a/target/classes/templates/candidate/details.html +++ b/target/classes/templates/candidate/details.html @@ -1,25 +1,114 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <meta charset="UTF-8"> - <title>Détails du Candidat</title> - <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> -</head> -<body> - <div class="container"> - <h2 class="mt-5">Détails du Candidat</h2> - - <table class="table table-bordered mt-3"> - <tr><th>ID</th><td th:text="${candidate.id}"></td></tr> - <tr><th>Nom</th><td th:text="${candidate.lastname}"></td></tr> - <tr><th>Prénom</th><td th:text="${candidate.firstname}"></td></tr> - <tr><th>Email</th><td th:text="${candidate.mail}"></td></tr> - <tr><th>Ville</th><td th:text="${candidate.city}"></td></tr> - </table> - - <a th:href="@{/candidates/edit/{id}(id=${candidate.id})}" class="btn btn-warning">Modifier</a> - <a th:href="@{/candidates/delete/{id}(id=${candidate.id})}" class="btn btn-danger" onclick="return confirm('Voulez-vous vraiment supprimer ce candidat ?')">Supprimer</a> - <a th:href="@{/candidates/list}" class="btn btn-secondary">Retour</a> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Candidate Profile</h2> + </header> + + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> -</body> -</html> + + <div th:if="${candidate != null}" class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-person-badge"></i> Personal Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">First Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.firstname}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Last Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.lastname}"></div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.mail}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${candidate.city}"></div> + </div> + </div> + </div> + + <!-- Job Applications Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-file-earmark-text"></i> Job Applications + </h4> + + <div th:if="${#lists.isEmpty(candidateApplications)}" class="alert alert-info"> + <i class="bi bi-info-circle"></i> No job applications found + </div> + + <div th:unless="${#lists.isEmpty(candidateApplications)}" class="table-responsive"> + <table class="table table-hover align-middle"> + <thead class="table-light"> + <tr> + <th>Application ID</th> + <th>CV Reference</th> + <th>Qualification</th> + <th>Sectors</th> + <th>Applied Date</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="app : ${candidateApplications}"> <!-- Changed from application to app --> + <td th:text="${app.id}"></td> + <td th:text="${app.cv}"></td> + <td th:text="${app.qualificationlevel.label}"></td> + <td> + <div th:each="sector : ${app.sectors}" class="badge favorite_back me-1 mb-1" + th:text="${sector.label}"></div> + </td> + <td th:text="${#temporals.format(app.appdate, 'yyyy-MM-dd')}"></td> + <td class="text-end"> + <span th:if="${session != null and session.userType == 'Candidate' and session.loggedInUser.id == candidate.id}" > + <a th:href="@{'/applications/details/' + ${app.id}}" + class="btn btn-sm btn-outline-secondary me-1"> + <i class="bi bi-eye"></i> View + </a> + <a th:href="@{'/applications/edit/' + ${app.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/applications/delete/' + ${app.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this application?');"> + <i class="bi bi-trash"></i> Delete + </a> + </span> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/candidate/editCandidate.html b/target/classes/templates/candidate/editCandidate.html index 8bc1e65f63dff81577a04e2e1c90e51ec3c3d776..4bb63f8a15d9adffa99531d422bca86ecb9dc4ff 100644 --- a/target/classes/templates/candidate/editCandidate.html +++ b/target/classes/templates/candidate/editCandidate.html @@ -1,46 +1,176 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> <head> <meta charset="UTF-8"> - <title>Modifier un Candidat</title> + <title>Edit Candidate</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" th:href="@{/css/bootstrap-icons.css}"> + <style> + .profile-card { + max-width: 700px; + margin: 0 auto; + border-radius: 10px; + overflow: hidden; + } + .profile-header { + background: #00B8DEFF; + color: white; + } + .form-control:read-only { + background-color: #f8f9fa; + border-color: #dee2e6; + } + .password-container { + position: relative; + } + .password-toggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + z-index: 5; + background: none; + border: none; + color: #6c757d; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> </head> -<body> - <div class="container"> - <h2 class="mt-5">Modifier un Candidat</h2> - - <form th:action="@{/candidates/edit}" method="post"> - <input type="hidden" name="id" th:value="${candidate.id}" /> - - <div class="form-group"> - <label>Email :</label> - <!-- <input type="email" name="mail" class="form-control" th:value="${candidate.mail}" required> --> - <input type="email" class="form-control" name="mail" required th:value="${candidate.mail}" readonly /> +<body class="bg-light"> +<div class="container py-4"> + <div class="card profile-card shadow-lg"> + <div class="card-header profile-header py-3"> + <div class="d-flex justify-content-between align-items-center"> + <h2 class="h4 mb-0"> + <i class="bi bi-person-gear me-2"></i> Edit Candidate Profile + </h2> </div> + </div> - <div class="form-group"> - <label>Mot de passe :</label> - <input type="password" name="password" class="form-control" required> + <div class="card-body p-4"> + <!-- Success Message --> + <div th:if="${successMessage != null}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div class="form-group"> - <label>Nom :</label> - <input type="text" name="lastname" class="form-control" th:value="${candidate.lastname}" required> + <!-- Error Messages (simplified to avoid Thymeleaf errors) --> + <div th:if="${errorMessage != null}" class="alert alert-danger alert-dismissible fade show"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div class="form-group"> - <label>Prénom :</label> - <input type="text" name="firstname" class="form-control" th:value="${candidate.firstname}" required> - </div> + <form th:action="@{/candidates/edit}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${candidate.id}" /> - <div class="form-group"> - <label>Ville :</label> - <input type="text" name="city" class="form-control" th:value="${candidate.city}" required> - </div> + <div class="row g-4"> + <!-- Left Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <input type="email" class="form-control" name="mail" + th:value="${candidate.mail}" readonly> + <small class="text-muted">Email cannot be changed</small> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Password</label> + <div class="password-container"> + <input type="password" id="passwordField" name="password" + class="form-control" + placeholder="Leave blank to keep current password"> + <button type="button" class="password-toggle" onclick="togglePassword()"> + <i class="bi bi-eye"></i> + </button> + </div> + <small class="text-muted">Minimum 8 characters</small> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label fw-semibold">First Name</label> + <input type="text" name="firstname" class="form-control" + th:value="${candidate.firstname}" required> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Last Name</label> + <input type="text" name="lastname" class="form-control" + th:value="${candidate.lastname}" required> + </div> - <button type="submit" class="btn btn-success mt-3">Enregistrer</button> - <a th:href="@{/candidates/list}" class="btn btn-secondary mt-3">Annuler</a> - </form> + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <input type="text" name="city" class="form-control" + th:value="${candidate.city}" required> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <div> + <a th:href="@{/candidates/list}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to list + </a> + </div> + + <div > + <button type="submit" class="btn btn-primary px-4"> + <i class="bi bi-save me-2"></i> Save Changes + </button> + </div> + </div> + </form> + </div> </div> +</div> + +<script th:src="@{/js/bootstrap.bundle.min.js}"></script> +<script> + // Password toggle function + function togglePassword() { + const passwordField = document.getElementById('passwordField'); + const toggleIcon = document.querySelector('.password-toggle i'); + + if (passwordField.type === 'password') { + passwordField.type = 'text'; + toggleIcon.classList.replace('bi-eye', 'bi-eye-slash'); + } else { + passwordField.type = 'password'; + toggleIcon.classList.replace('bi-eye-slash', 'bi-eye'); + } + } + + // Form validation + document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('.needs-validation'); + + form.addEventListener('submit', function(event) { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + + // Add input validation on blur + form.querySelectorAll('input[required]').forEach(input => { + input.addEventListener('blur', () => { + input.classList.toggle('is-invalid', !input.checkValidity()); + }); + }); + }); +</script> </body> -</html> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/candidate/signupCandidate.html b/target/classes/templates/candidate/signupCandidate.html index f8d74e5e6e033c038888c4f69ec112a65775d012..dd6a1008514dc6e994abc142f91a370a1cbd1761 100644 --- a/target/classes/templates/candidate/signupCandidate.html +++ b/target/classes/templates/candidate/signupCandidate.html @@ -1,17 +1,26 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Candidates</title> +<section> + <head> <meta charset="UTF-8"> <title>Candidate Sign Up</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <style> + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> </head> <body> <div class="container"> <h2 class="mt-5">Sign Up as Candidate</h2> - <!-- Affichage des erreurs --> - <div th:if="${error}" class="alert alert-danger" th:text="${error}"></div> - + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> + </div> <!-- Formulaire d'inscription --> <form th:action="@{/candidates/signup}" method="post"> <div class="mb-3"> @@ -35,13 +44,15 @@ <input type="text" class="form-control" id="city" name="city" th:value="${city}" required> </div> - <button type="submit" class="btn btn-primary">Sign Up</button> - </form> + <button type="submit" class="btn btn-primary ">Sign Up</button> + <a th:href="@{/login}" class="btn btn-light text-black " >Annuler</a> + </form> <!-- Lien de retour à la page de connexion --> <p class="mt-3"> Already have an account? <a th:href="@{/login}">Sign in here</a> </p> </div> </body> +</section> </html> diff --git a/target/classes/templates/company/companyBase.html b/target/classes/templates/company/companyBase.html deleted file mode 100644 index eed3128d24fc488ded44f3d635d2629e4a8066f5..0000000000000000000000000000000000000000 --- a/target/classes/templates/company/companyBase.html +++ /dev/null @@ -1,13 +0,0 @@ -<!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:fragment="article(subcontent)" - th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> -<title>Companies</title> -<section> - <header> - <h1>Companies Management</h1> - </header> - <th:block th:insert="${subcontent}"> - </th:block> -</section> - -</html> \ No newline at end of file diff --git a/target/classes/templates/company/companyEdit.html b/target/classes/templates/company/companyEdit.html index 82ea247f8dfe4eba39fd8cfb5b8d39b3175ab559..dea5445fb50b3fb18534cf0945ead527f6c40e9d 100644 --- a/target/classes/templates/company/companyEdit.html +++ b/target/classes/templates/company/companyEdit.html @@ -1,50 +1,143 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> - <header> - <h2>Modifier l'entreprise</h2> - </header> - - <!-- Message d'erreur ou de succès --> - <div th:if="${successMessage}" class="alert alert-success"> - <strong>Succès :</strong> <span th:text="${successMessage}"></span> - </div> - - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> + <link rel="stylesheet" th:href="@{/css/bootstrap-icons.css}"> + <style> + .profile-card { + max-width: 800px; + margin: 0 auto; + border-radius: 10px; + overflow: hidden; + } + .profile-header { + background: #00B8DEFF; + color: white; + } + .form-control:read-only { + background-color: #f8f9fa; + border-color: #dee2e6; + } + .btn-primary { + background-color: #00B8DEFF; + border-color: #00B8DEFF; + } + .btn-outline-secondary { + border-color: #6c757d; + color: #6c757d; + } + .form-label { + font-weight: 500; + color: #495057; + } + .text-muted { + font-size: 0.85rem; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <div class="card profile-card shadow-lg"> + <div class="card-header profile-header py-3"> + <div class="d-flex justify-content-between align-items-center"> + <h2 class="h4 mb-0"> + <i class="bi bi-building-gear me-2"></i> Edit Company Profile + </h2> + </div> + </div> + + <div class="card-body p-4"> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${error}" class="alert alert-danger alert-dismissible fade show"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${error}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <form th:action="@{/companies/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${company.id}" /> + + <div class="row g-4"> + <!-- Left Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label">Email</label> + <input type="email" class="form-control" name="mail" + th:value="${company.mail}" readonly> + <small class="text-muted">Email cannot be changed</small> + </div> + + <div class="mb-3"> + <label class="form-label">Company Name</label> + <input type="text" class="form-control" name="denomination" + th:value="${company.denomination}" required> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label">Description</label> + <textarea class="form-control" name="description" + rows="3" required th:text="${company.description}"></textarea> + </div> + + <div class="mb-3"> + <label class="form-label">City</label> + <input type="text" class="form-control" name="city" + th:value="${company.city}" required> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <div> + <a th:href="@{/companies}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to list + </a> + </div> + <div> + <button type="submit" class="btn btn-primary px-4"> + <i class="bi bi-save me-2"></i> Save Changes + </button> + </div> + </div> + </form> + </div> + </div> </div> - - <form th:action="@{/companies/update}" method="post"> - <!-- ID (caché) --> - <input type="hidden" name="id" th:value="${company.id}" /> - - <!-- Email (readonly) --> - <fieldset class="mb-3"> - <label for="emailid" class="form-label">Email</label>: - <input type="email" id="emailid" class="form-control" name="mail" th:value="${company.mail}" readonly /> - </fieldset> - - <!-- Dénomination --> - <fieldset class="mb-3"> - <label for="nameid" class="form-label">Nom</label>: - <input type="text" id="nameid" class="form-control" name="denomination" th:value="${company.denomination}" required /> - </fieldset> - - <!-- Description --> - <fieldset class="mb-3"> - <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" th:value="${company.description}" required /> - </fieldset> - - <!-- Ville --> - <fieldset class="mb-3"> - <label for="cityid" class="form-label">Ville</label>: - <input type="text" id="cityid" class="form-control" name="city" th:value="${company.city}" required /> - </fieldset> - - <!-- Boutons --> - <button type="submit" class="btn btn-success">💾 Enregistrer</button> - <a th:href="@{/companies/view/{id}(id=${company.id})}" class="btn btn-danger">❌ Annuler</a> - </form> -</article> -</html> + + <script th:src="@{/js/bootstrap.bundle.min.js}"></script> + <script> + // Form validation + document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('.needs-validation'); + + form.addEventListener('submit', function(event) { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + + // Add input validation on blur + form.querySelectorAll('input[required], textarea[required]').forEach(input => { + input.addEventListener('blur', () => { + input.classList.toggle('is-invalid', !input.checkValidity()); + }); + }); + }); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/company/companyForm.html b/target/classes/templates/company/companyForm.html index e594bca8e4ca4cbc7fb21c5ac7b1935dbafa28d7..ca2b0a6110f9ef081dee16a4d8090cbc2a614f1c 100644 --- a/target/classes/templates/company/companyForm.html +++ b/target/classes/templates/company/companyForm.html @@ -1,6 +1,7 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> <header> <h2>Company entry form</h2> </header> @@ -10,23 +11,23 @@ <strong>Succès :</strong> <span th:text="${successMessage}"></span> </div> - <!-- ❌ Message d'erreur --> - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> </div> <form th:action="@{/companies/create}" method="post"> <!-- Email --> <fieldset class="mb-3"> <label for="emailid" class="form-label">Email Address</label>: - <input type="email" id="emailid" class="form-control" name="mail" + <input type="email" id="emailid" class="form-control" name="mail" th:value="${company.mail}" required /> </fieldset> <!-- Mot de passe --> <fieldset class="mb-3"> <label for="passwordid" class="form-label">Password</label>: - <input type="password" id="passwordid" class="form-control" name="password" + <input type="password" id="passwordid" class="form-control" name="password" minlength="4" required /> </fieldset> @@ -40,20 +41,20 @@ <!-- 📌 CHAMP DESCRIPTION (Ajouté ici) --> <fieldset class="mb-3"> <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" + <input type="text" id="descid" class="form-control" name="description" th:value="${company.description}" required /> </fieldset> <!-- Ville --> <fieldset class="mb-3"> <label for="cityid" class="form-label">City</label>: - <input type="text" id="cityid" class="form-control" name="city" + <input type="text" id="cityid" class="form-control" name="city" th:value="${company.city}" required /> </fieldset> <!-- Boutons --> - <button type="submit" class="btn btn-primary">Enregistrer</button> - <a th:href="@{/companies}" class="btn btn-danger">Annuler</a> + <button type="submit" class="btn favorite_back text-white">Save</button> + <a th:href="@{/login}" class="btn btn-light text-black " >Cancel</a> </form> -</article> +</section> </html> \ No newline at end of file diff --git a/target/classes/templates/company/companyList.html b/target/classes/templates/company/companyList.html index 76c1e011b3ceae67edb1a128fb6d7d89af2f6720..5e793d4b88b18adbf02ce664f1371f3c47a4bc9e 100644 --- a/target/classes/templates/company/companyList.html +++ b/target/classes/templates/company/companyList.html @@ -1,49 +1,108 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <title>Liste des Entreprises</title> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> -</head> -<body> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> -<div class="container mt-4"> - <h2 class="text-center mb-4">Liste des Entreprises</h2> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .company-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .company-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .description-cell { + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .content-footer { + margin-top: 5rem; + padding-top: 1rem; + border-top: 1px solid #dee2e6; + } + </style> + </head> + <body class="bg-light"> - <table class="table table-striped table-bordered table-hover"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Dénomination</th> - <th>Ville</th> - <th>Description</th> - <th>Nombre d'Offres</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="company : ${companies}"> - <td th:text="${company.id}"></td> - <td th:text="${company.denomination}"></td> - <td th:text="${company.city}"></td> - <td th:text="${company.description}"></td> - <td th:text="${company.getJobOfferCount()}"></td> - <td> - <a th:href="@{'/companies/view/' + ${company.id}}" class="btn btn-primary btn-sm">Voir</a> - </td> - </tr> - </tbody> - </table> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class=" color-title bi bi-buildings me-2"></i> Lists of Companies + </h2> + </div> - <a th:href="@{/}" class="btn btn-secondary mt-3">🏠 Retour à l'accueil</a> + <!-- Companies Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(companies)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">Company Name</th> + <th>Location</th> + <th>Description</th> + <th>Job Offers</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="company : ${companies}" class="company-card"> + <td class="ps-4 fw-semibold" th:text="${company.denomination}"></td> + <td> + <i class="bi bi-geo-alt text-secondary me-1"></i> + <span th:text="${company.city}"></span> + </td> + <td class="description-cell" th:text="${company.description}"></td> + <td> + <span class="badge favorite_back rounded-pill" + th:text="${company.getJobOfferCount()}"></span> + </td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/companies/view/' + ${company.id}}" + class="btn btn favorite_outline" > + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}"> + <a th:href="@{'/companies/edit/' + ${company.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/companies/delete/' + ${company.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this company offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + <!-- Empty State --> + <div th:if="${#lists.isEmpty(companies)}" class="text-center py-5"> + <i class="bi bi-buildings empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No companies registered</h4> + </div> + </div> + </div> + </div> + </div> - - <!-- Vérifie si l'utilisateur est authentifié ET est une entreprise --> - <th:block th:if="${#httpServletRequest != null and #httpServletRequest.getSession(false) != null and #httpServletRequest.getSession().getAttribute('usertype') == 'company'}"> - <a th:href="@{/companies/create}" class="btn btn-success">Sign up as a company</a> - </th:block> - -</div> - -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -</body> -</html> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/company/companyView.html b/target/classes/templates/company/companyView.html index 7d5f091eabe18a237ccb90de75bfc607697b8202..39baacae644d237370c1f783390ee05e9bc4ff8c 100644 --- a/target/classes/templates/company/companyView.html +++ b/target/classes/templates/company/companyView.html @@ -1,123 +1,133 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org" - th:replace="~{/company/companyBase :: article(~{::article})}"> -<article> - <header> - <h2>Company details</h2> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Companies</title> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Company Details</h2> </header> - - <div th:if="${successMessage}" class="alert alert-success"> - <strong>Succès :</strong> <span th:text="${successMessage}"></span> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - - <div th:if="${errorMessage}" class="alert alert-danger"> - <strong>Erreur :</strong> <span th:text="${errorMessage}"></span> + + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> - <div th:if="${company != null}"> - <form> - <fieldset class="mb-3"> - <label for="nameid" class="form-label">Name</label>: - <input type="text" id="nameid" class="form-control" name="denomination" - th:value="${company.denomination}" readonly /> - </fieldset> - <fieldset class="mb-3"> - <label for="emailid" class="form-label">Email</label>: - <input type="text" id="emailid" class="form-control" name="email" - th:value="${company.email}" readonly /> - </fieldset> - - <fieldset class="mb-3"> - <label for="descid" class="form-label">Description</label>: - <input type="text" id="descid" class="form-control" name="description" - th:value="${company.description}" readonly /> - </fieldset> - <fieldset class="mb-3"> - <label for="cityid" class="form-label">City</label>: - <input type="text" id="cityid" class="form-control" name="city" - th:value="${company.city}" readonly /> - </fieldset> - - - <fieldset class="mb-3"> - <label class="form-label">Nombre d'offres d'emploi publiées :</label> - <input type="text" class="form-control" th:value="${jobOfferCount}" readonly /> - </fieldset> - - <fieldset class="mb-3"> - <h3>📋 Offres d'emploi publiées</h3> - <table class="table table-striped table-bordered"> - <thead class="table-dark"> + <div th:if="${company != null}" class="card shadow-sm mb-4"> + <div class="card-body"> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-building"></i> Company Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Company Name</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.denomination}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Email</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.email}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">City</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${company.city}"></div> + </div> + </div> + + <!-- Right Column (Aligned with left column) --> + <div class="col-md-6"> + <div class="mb-3" style="min-height: 96px"> + <label class="form-label fw-semibold">Description</label> + <div class="form-control-plaintext bg-light p-2 rounded" style="min-height: 96px" th:text="${company.description}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Job Offers</label> + <div class="form-control-plaintext bg-light p-2 rounded fw-bold" + th:text="${jobOfferCount} + (${jobOfferCount} == 1 ? ' active job' : ' active jobs')"></div> + </div> + </div> + </div> + + <!-- Job Offers Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-briefcase"></i> Current Job Offers + </h4> + + <div th:if="${#lists.isEmpty(jobOffers)}" class="alert alert-info"> + <i class="bi bi-info-circle"></i> No active job openings currently + </div> + + <div th:unless="${#lists.isEmpty(jobOffers)}" class="table-responsive"> + <table class="table table-hover align-middle"> + <thead class="table-light"> <tr> <th>ID</th> - <th>Titre</th> + <th>Position</th> <th>Description</th> - <th>Action</th> + <th>Sectors</th> + <th>Qualification</th> + <th>Posted Date</th> + <th class="text-end">Actions</th> </tr> - </thead> - <tbody> + </thead> + <tbody> <tr th:each="offer : ${jobOffers}"> <td th:text="${offer.id}"></td> <td th:text="${offer.title}"></td> - <td th:text="${offer.taskDescription}"></td> - <td> - <a th:href="@{/jobOffers/view/{id}(id=${offer.id})}" class="btn btn-info">Voir l'offre</a> + <div class="text-truncate" style="max-width: 300px" th:text="${offer.taskDescription}"></div> + </td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${offer.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${offer.qualificationLevel.label}"></td> + <td th:text="${offer.publicationDate != null} ? ${#temporals.format(offer.publicationDate, 'yyyy-MM-dd')} : 'N/A'"></td> + <td class="text-end" > + <a th:href="@{/jobs/view/{id}(id=${offer.id})}" th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}" + class="btn btn-sm text-black"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == company.id}"> + <a th:href="@{'/jobs/edit/' + ${offer.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/jobs/delete/' + ${offer.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this job offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> </td> </tr> - <tr th:if="${#lists.isEmpty(jobOffers)}"> - <td colspan="4" class="text-center text-danger">⚠️ Aucune offre d'emploi trouvée.</td> - </tr> - </tbody> - </table> - </fieldset> - - -<!-- <fieldset class="mb-3"> - <h3>📋 Offres d'emploi publiées</h3> - <table class="table table-striped table-bordered"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Titre</th> - <th>Description</th> - </tr> - </thead> - <tbody> - <tr th:each="offer : ${jobOffers}"> - <td th:text="${offer.id}"></td> - <td th:text="${offer.title}"></td> - <td th:text="${offer.taskDescription}"></td> - </tr> - <tr th:if="${#lists.isEmpty(jobOffers)}"> - <td colspan="3" class="text-center text-danger">⚠️ Aucune offre d'emploi trouvée.</td> - </tr> - </tbody> - </table> -</fieldset> --> -</form> - + </tbody> + </table> + </div> + </div> + </div> </div> - <div th:if="${company == null}"> - <p class="text-danger">❌ Erreur : Aucune entreprise trouvée.</p> - </div> - <footer> - <a th:href="@{/companies/{id}/edit(id=${company.id})}" class="btn btn-primary" title="Modifier"> - ✏️ Modifier - </a> - <!-- <a th:href="@{/companies/{id}/jobOffers(id=${company.id})}" class="btn btn-info"> - 📄 Voir les offres publiées - </a> --> - - - <a th:href="@{/companies/delete/{id}(id=${company.id})}" class="btn btn-danger" - onclick="return confirm('⚠️ Êtes-vous sûr de vouloir supprimer cette entreprise ?');"> - 🗑 Supprimer - </a> - - </footer> -</article> -</html> + + + <div class="mt-3"> + + </div> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/error/accessDenied.html b/target/classes/templates/error/accessDenied.html index d4ed95d38c64e16f10ac5d1501d02efc99a6c4ae..6ddc6e763feb5c327b6d2603f81c0c441be276a1 100644 --- a/target/classes/templates/error/accessDenied.html +++ b/target/classes/templates/error/accessDenied.html @@ -9,7 +9,7 @@ <div class="container mt-5 text-center"> <h2 class="text-danger">⛔ Accès interdit</h2> <p>Vous n'avez pas les permissions nécessaires pour voir cette page.</p> - <a href="/" class="btn btn-primary">Retour à l'accueil</a> + <a href="/" class="btn favorite_back">Retour à l'accueil</a> </div> </body> diff --git a/target/classes/templates/index.html b/target/classes/templates/index.html index d53f468fed2d575137455cbab6fcbe5c80ab9573..347b1641e03d27b315c2ddd042c49c459372b3de 100644 --- a/target/classes/templates/index.html +++ b/target/classes/templates/index.html @@ -59,6 +59,31 @@ </a> </article> --> + + <!-- Nouvelle section profil améliorée --> + <div th:if="${session.loggedInUser != null}" > + + <div class="d-flex justify-content-between align-items-center text-black"> + <div> + <h2> + <i th:classappend="${session.userType == 'Company'} ? 'bi-building' : 'bi-person'" + class="bi me-2"></i> + WELCOME, + <span th:if="${session.userType == 'Company'}"> + [[${session.loggedInUser.denomination}]] + </span> + <span th:if="${session.userType == 'Candidate'}"> + [[${session.loggedInUser.firstname}]] [[${session.loggedInUser.lastname}]] + </span> + </h2> + </div> + <div> + <a th:if="${session.hasMessages}" th:href="@{/messages}" class="btn btn-outline-light me-2"> + <i class="bi bi-envelope-at"></i> Messages + </a> + </div> + </div> + </div> <div class="row row-cols-1 row-cols-lg-3 align-items-stretch g-4 py-5"> <article class="col"> <a th:href="@{/companies}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" @@ -70,7 +95,7 @@ </article> <article class="col"> - <a th:href="@{/candidates}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" + <a th:href="@{/candidates/list}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" style="background-image: url('/img/candidates.jpg'); background-size: cover;"> <div class="d-flex flex-column h-100 p-5 pb-3 text-white text-shadow-1"> <h2 class="pt-5 mt-5 mb-4 display-6 lh-1 fw-bold">Candidates</h2> @@ -79,7 +104,7 @@ </article> <!-- ✅ Déplacement de "Postuler" dans la même ligne --> - <article class="col"> + <article class="col" th:if="${session!=null and session.userType=='Candidate'} "> <a th:href="@{/applications/apply}" class="nav-link card card-cover h-100 overflow-hidden text-white bg-dark rounded-5 shadow-lg" style="background-image: url('/img/postuler.jpeg'); background-size: cover;"> <div class="d-flex flex-column h-100 p-5 pb-3 text-white text-shadow-1"> diff --git a/target/classes/templates/jobOffer/companyJobOfferView.html b/target/classes/templates/jobOffer/companyJobOfferView.html index 37cb1c21e83e9402c449bcd4aecd9293f5605745..1ce555003f56312fc47328e26af14dba651d88a7 100644 --- a/target/classes/templates/jobOffer/companyJobOfferView.html +++ b/target/classes/templates/jobOffer/companyJobOfferView.html @@ -1,19 +1,22 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Create Job Offer</title> +<section> <head> <title>Détails de l'Offre d'Emploi</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> </head> <body> <div class="container"> - <h2>Offre d'Emploi : <span th:text="${jobOffer.title}"></span></h2> + <h2>Offre d'Emploi : <span th:text="${offer.title}"></span></h2> - <p><strong>ID:</strong> <span th:text="${jobOffer.id}"></span></p> - <p><strong>Titre:</strong> <span th:text="${jobOffer.title}"></span></p> - <p><strong>Description:</strong> <span th:text="${jobOffer.taskDescription}"></span></p> - <p><strong>Date de Publication:</strong> <span th:text="${jobOffer.publicationDate}"></span></p> + <p><strong>ID:</strong> <span th:text="${offer.id}"></span></p> + <p><strong>Titre:</strong> <span th:text="${offer.title}"></span></p> + <p><strong>Description:</strong> <span th:text="${offer.taskDescription}"></span></p> + <p><strong>Date de Publication:</strong> <span th:text="${offer.publicationDate}"></span></p> <a th:href="@{/companies}" class="btn btn-secondary">Retour aux entreprises</a> </div> </body> +</section> </html> diff --git a/target/classes/templates/jobOffer/jobOfferEdit.html b/target/classes/templates/jobOffer/jobOfferEdit.html new file mode 100644 index 0000000000000000000000000000000000000000..0806a8b7a967b97fce5c06cf2af70519cbf8c010 --- /dev/null +++ b/target/classes/templates/jobOffer/jobOfferEdit.html @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Edit Job Offer</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + transition: all 0.3s ease; + } + .sector-item.selected { + background: rgb(0, 184, 222); + color: white; + border-color: rgb(0, 184, 222); + } + #selectedSectorsDisplay { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + background-color: #f8f9fa; + } + .selected-sector-tag { + display: inline-block; + background: rgb(0, 184, 222); + color: white; + padding: 2px 8px; + border-radius: 4px; + margin-right: 5px; + margin-bottom: 5px; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> + + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-briefcase me-2"></i> + Edit Job Offer + </h2> + + <form th:action="@{/jobs/update}" method="post" class="needs-validation" novalidate> + <input type="hidden" name="id" th:value="${job.id}" /> + + <!-- Champs de base --> + <div class="mb-3"> + <label class="form-label">Job Title*</label> + <input type="text" class="form-control" th:value="${job.title}" name="title" required> + </div> + + <div class="mb-3"> + <label class="form-label">Description*</label> + <textarea class="form-control" rows="4" name="taskDescription" required + th:text="${job.taskDescription}"></textarea> + </div> + + <div class="row mb-3"> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${job.publicationDate != null} ? ${#temporals.format(job.publicationDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualificationLevels}" + th:value="${level.id}" + th:text="${level.label}" + th:selected="${job.qualificationLevel?.id == level.id}"> + </option> + </select> + </div> + </div> + + <!-- Industry Sectors - Version corrigée --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + + <!-- Liste complète des secteurs --> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:classappend="${#lists.contains(job.sectors.![id], sector.id)} ? 'selected' : ''" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + + <!-- Champ affichant les secteurs sélectionnés --> + <div id="selectedSectorsDisplay" class="mb-2"> + <span th:if="${job.sectors.empty}">No sectors selected</span> + <th:block th:each="sector : ${job.sectors}"> + <div th:data-id="${sector.id}"> + <span th:text="${sector.label}"></span> + </div> + </th:block> + </div> + + <input type="hidden" id="sectorIds" name="sectorIds" + th:value="${#strings.listJoin(job.sectors.![id], ',')}" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <a th:href="@{/jobs}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Save Changes + </button> + </div> + </form> + </div> + </div> + + <script> + // Initialize with already selected sectors from the job object + const selectedSectors = new Set( + document.getElementById('sectorIds').value.split(',').filter(Boolean) + ); + + // Mark selected sectors on page load + document.addEventListener('DOMContentLoaded', function() { + updateSelectedDisplay(); + + // Mark initially selected sectors in the list + selectedSectors.forEach(sectorId => { + const element = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (element) { + element.classList.add('selected'); + } + }); + }); + + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + const sectorLabel = element.textContent; + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectorsDisplay'); + const hiddenInput = document.getElementById('sectorIds'); + + // Clear the display + displayDiv.innerHTML = ''; + + if (selectedSectors.size === 0) { + displayDiv.innerHTML = '<span>No sectors selected</span>'; + } else { + // Add selected sectors as tags + selectedSectors.forEach(sectorId => { + const sectorElement = document.querySelector(`.sector-item[data-id="${sectorId}"]`); + if (sectorElement) { + const tag = document.createElement('div'); + tag.setAttribute('data-id', sectorId); + tag.innerHTML = `<span>${sectorElement.textContent}</span>`; + displayDiv.appendChild(tag); + } + }); + } + + // Update hidden input with selected sector IDs + hiddenInput.value = Array.from(selectedSectors).join(','); + } + + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); + + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/jobOffer/jobOfferForm.html b/target/classes/templates/jobOffer/jobOfferForm.html index 7d2709416d172ef9eb6d0a644b6b11a6a3271978..a6c538b3e7894a28034d5357d72d63005b5482ab 100644 --- a/target/classes/templates/jobOffer/jobOfferForm.html +++ b/target/classes/templates/jobOffer/jobOfferForm.html @@ -1,70 +1,164 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> -<head> - <title>Créer/Modifier une offre d'emploi</title> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> -</head> -<body> - -<div class="container mt-4"> - <h2 class="text-center mb-4">Créer une offre d'emploi</h2> - - <!-- ✅ Affichage des messages d'erreur --> - <div th:if="${errorMessage}" class="alert alert-danger text-center"> - <span th:text="${errorMessage}"></span> - </div> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Create Job Offer</title> +<section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .form-container { + max-width: 800px; + margin: 0 auto; + background-color: #fff; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .sector-item { + display: inline-block; + margin: 5px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + background: #f8f9fa; + border: 1px solid #dee2e6; + } + .sector-item.selected { + background: rgb(0, 184, 222); + color: white; + border-color: rgb(0, 184, 222); + } + #selectedSectors { + min-height: 40px; + border: 1px solid #ced4da; + border-radius: 4px; + padding: 8px; + margin-top: 5px; + } + .hidden-input { + display: none; + } + .btn-primary { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + } + </style> + </head> + <body class="bg-light"> - <form th:action="@{${jobOffer.id != null} ? '/jobs/update/' + jobOffer.id : '/jobs/save'}" - th:object="${jobOffer}" method="post"> + <div class="container py-4"> + <div class="form-container"> + <h2 class="text-center mb-4"> + <i class="bi bi-briefcase me-2"></i> + Create Job Offer + </h2> - <input type="hidden" th:field="*{id}" /> + <form th:action="@{/jobs/save}" method="post" class="needs-validation" novalidate> - <div class="mb-3"> - <label class="form-label">Titre :</label> - <input type="text" class="form-control" th:field="*{title}" required> - </div> + <!-- Champs de base --> + <div class="mb-3"> + <label class="form-label">Job Title*</label> + <input type="text" class="form-control" name="title" required> + </div> - <div class="mb-3"> - <label class="form-label">Description :</label> - <textarea class="form-control" rows="3" th:field="*{taskDescription}" required></textarea> - </div> + <div class="mb-3"> + <label class="form-label">Description*</label> + <textarea class="form-control" rows="4" name="taskDescription" required></textarea> + </div> - <div class="mb-3"> - <label class="form-label">Date de publication :</label> - <input type="date" class="form-control" th:field="*{publicationDate}" required> - </div> + <div class="row mb-3"> + <div class="col-md-6"> + <label class="form-label">Publication Date*</label> + <input type="date" class="form-control" name="publicationDate" required + th:value="${jobOffer.publicationDate != null} ? ${#temporals.format(jobOffer.publicationDate, 'yyyy-MM-dd')} : ${#temporals.format(#temporals.createToday(), 'yyyy-MM-dd')}"/> + </div> + <div class="col-md-6"> + <label class="form-label">Qualification Level*</label> + <select class="form-select" name="qualificationLevel" required> + <option value="">-- Select level --</option> + <option th:each="level : ${qualificationLevels}" + th:value="${level.id}" + th:text="${level.label}"></option> + </select> + </div> + </div> - <div class="mb-3"> - <label class="form-label">Entreprise :</label> - <select class="form-select" th:field="*{company}" required> - <option value="">-- Sélectionner une entreprise --</option> - <option th:each="company : ${companies}" th:value="${company.id}" th:text="${company.denomination}"></option> - </select> - </div> + <!-- Sélection des secteurs --> + <div class="mb-3"> + <label class="form-label">Industry Sectors*</label> + <div class="mb-2"> + <div th:each="sector : ${sectors}" + class="sector-item" + th:data-id="${sector.id}" + th:text="${sector.label}" + onclick="toggleSector(this)"></div> + </div> + <div id="selectedSectors" class="mb-2">No sectors selected</div> + <input type="hidden" id="sectorIds" name="sectorIds" required> + <div class="invalid-feedback">Please select at least one sector</div> + </div> - <div class="mb-3"> - <label class="form-label">Niveau de qualification :</label> - <select class="form-select" th:field="*{qualificationLevel}"> - <option value="0">-- Sélectionner un niveau de qualification --</option> - <option th:each="level : ${qualificationLevels}" th:value="${level.id}" th:text="${level.label}"></option> - </select> + <div class="d-flex justify-content-between mt-4"> + <a href="/jobs" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left me-1"></i> Cancel + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-save me-1"></i> Create Offer + </button> + </div> + </form> </div> + </div> - <div class="mb-3"> - <label class="form-label">Secteurs d'activité :</label> - <select class="form-select" multiple th:name="sectorIds"> - <option th:each="sector : ${sectors}" th:value="${sector.id}" th:text="${sector.label}"></option> - </select> - </div> + <script> + const selectedSectors = new Set(); - <div class="text-center"> - <button type="submit" class="btn btn-success">Enregistrer</button> - <a href="/jobs" class="btn btn-secondary">Retour</a> - </div> + function toggleSector(element) { + const sectorId = element.getAttribute('data-id'); + + if (selectedSectors.has(sectorId)) { + selectedSectors.delete(sectorId); + element.classList.remove('selected'); + } else { + selectedSectors.add(sectorId); + element.classList.add('selected'); + } + + updateSelectedDisplay(); + } + + function updateSelectedDisplay() { + const displayDiv = document.getElementById('selectedSectors'); + const hiddenInput = document.getElementById('sectorIds'); + + if (selectedSectors.size === 0) { + displayDiv.textContent = 'No sectors selected'; + hiddenInput.value = ''; + } else { + displayDiv.textContent = Array.from(selectedSectors).map(id => { + return document.querySelector(`.sector-item[data-id="${id}"]`).textContent; + }).join(', '); + + hiddenInput.value = Array.from(selectedSectors).join(','); + } + } - </form> -</div> + // Form validation + (function() { + 'use strict'; + const forms = document.querySelectorAll('.needs-validation'); -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -</body> -</html> + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + })(); + </script> + </body> +</section> +</html> \ No newline at end of file diff --git a/target/classes/templates/jobOffer/jobOfferList.html b/target/classes/templates/jobOffer/jobOfferList.html index ea6688dc7bc6a73552a4206723355f8b1fb4c434..228e5a03a600ca3c3e82278ef8aa644723fba898 100644 --- a/target/classes/templates/jobOffer/jobOfferList.html +++ b/target/classes/templates/jobOffer/jobOfferList.html @@ -1,85 +1,123 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> +<title>Jobs</title> +<section> <head> - <title>Liste des Offres d'Emploi</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .job-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .job-card:hover { + transform: translateY(-2px); + border-left-color: #00b8de; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .sector-badge { + margin-right: 4px; + margin-bottom: 4px; + } + .empty-state-icon { + font-size: 3rem; + opacity: 0.5; + } + </style> </head> -<body> +<body class="bg-light"> -<div class="container mt-4"> - <h2 class="text-center mb-4">Liste des Offres d'Emploi</h2> - - <div th:if="${successMessage}" class="alert alert-success"> - <p th:text="${successMessage}"></p> - </div> - <div th:if="${errorMessage}" class="alert alert-danger"> - <p th:text="${errorMessage}"></p> +<div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-briefcase me-2"></i> Job Offers + </h2> + <a th:href="@{/jobs/create}" class="btn btn-outline-secondary" th:if="${session != null and session.userType == 'Company'}"> + <i class="bi bi-plus-circle"></i> Create Job Offer + </a> </div> - <table class="table table-striped table-bordered table-hover"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>Titre</th> - <th>Entreprise</th> - <th>Secteurs</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr th:each="job : ${jobOffers}"> - <td th:text="${job.id}"></td> - <td th:text="${job.title}"></td> - <td th:text="${job.company.denomination}"></td> - <td> - <ul class="list-unstyled"> - <li th:each="sector : ${job.sectors}" th:text="${sector.label}"></li> - </ul> - </td> - <td> - <a th:href="@{'/jobs/view/' + ${job.id}}" class="btn btn-primary btn-sm">Voir</a> - - <!-- Vérification si l'utilisateur connecté est une entreprise ET qu'il est le propriétaire de l'offre --> - <th:block th:if="${session.loggedInUser != null - and session.loggedInUser.usertype == 'company' - and session.loggedInUser.mail == job.company.mail}"> - <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> - <a th:href="@{'/jobs/delete/' + ${job.id}}" - class="btn btn-danger btn-sm" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - Supprimer - </a> - </th:block> - <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> - <a th:href="@{'/jobs/delete/' + ${job.id}}" - class="btn btn-danger btn-sm" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - Supprimer - </a> - </th:block> - </td> - </tr> - </tbody> - - - </table> - <div class="text-center mt-4"> - <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> - - <th:block th:if="${session.usertype != null and session.usertype == 'company'}"> - <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> -</th:block> + <!-- Job Offers Table --> + <div class="card shadow-sm table-container"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" th:if="${not #lists.isEmpty(jobOffers)}"> + <thead class="table-light"> + <tr> + <th class="ps-4">ID</th> + <th>Title</th> + <th>Company</th> + <th>Sectors</th> + <th>Qualification</th> + <th>Posted Date</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="job : ${jobOffers}" class="job-card"> + <td class="ps-4" th:text="${job.id}"></td> + <td class="fw-semibold" th:text="${job.title}"></td> + <td th:text="${job.company?.denomination} ?: 'N/A'"></td> + <td> + <div class="d-flex flex-wrap"> + <span th:each="sector : ${job.sectors}" + class="badge favorite_back sector-badge" + th:text="${sector.label}"></span> + </div> + </td> + <td th:text="${job.qualificationLevel.label}"></td> + <td th:text="${job.publicationDate != null} ? ${#temporals.format(job.publicationDate, 'yyyy-MM-dd')} : 'N/A'"></td> <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{'/jobs/view/' + ${job.id}}" + class="btn btn-sm favorite_outline"> + <i class="bi bi-eye"></i> View + </a> + <th:block th:if="${session != null and session.userType == 'Company' and session.loggedInUser.id == job.company.id}"> + <a th:href="@{'/jobs/edit/' + ${job.id}}" + class="btn btn-sm btn-outline-warning"> + <i class="bi bi-pencil"></i> Edit + </a> + <a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-sm btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this job offer?');"> + <i class="bi bi-trash"></i> Delete + </a> + </th:block> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State --> + <div th:if="${#lists.isEmpty(jobOffers)}" class="text-center py-5"> + <i class="bi bi-briefcase empty-state-icon text-muted"></i> + <h4 class="mt-3 text-muted">No job offers found</h4> + </div> + </div> + </div> + </div> - </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> +</section> </html> - @@ -137,8 +175,31 @@ <a href="/jobs/create" class="btn btn-success">Créer une nouvelle offre</a> <a th:href="@{/}" class="btn btn-secondary mt-3">🏠 Retour à l'accueil</a> + </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> -</html> --> +</html> + <td> + <a th:href="@{'/jobs/view/' + ${job.id}}" class="btn btn-primary btn-sm">Voir</a> + +<th:block th:if="${session.loggedInUser != null + and session.loggedInUser.usertype == 'COMPANY' + and session.loggedInUser.mail == job.company.mail}"> + <a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> + <a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-danger btn-sm" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> + Supprimer + </a> +</th:block> +<a th:href="@{'/jobs/' + ${job.id} + '/edit'}" class="btn btn-warning btn-sm">Modifier</a> +<a th:href="@{'/jobs/delete/' + ${job.id}}" + class="btn btn-danger btn-sm" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> + Supprimer +</a> +</th:block> +</td> + --> diff --git a/target/classes/templates/jobOffer/jobOfferView.html b/target/classes/templates/jobOffer/jobOfferView.html index e5fbffb1925c85799ef98bf9f505d1808cc142d1..bd9bc0968b1be6c7dfc3a4e4f4f6c25826db02f4 100644 --- a/target/classes/templates/jobOffer/jobOfferView.html +++ b/target/classes/templates/jobOffer/jobOfferView.html @@ -1,55 +1,94 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <head> - <title>Détails de l'Offre d'Emploi</title> + <title>Job Details</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + :root { + --primary: #072262; + --accent: #ff6b35; + } + </style> </head> -<body class="bg-light"> +<body> +<section> + <header class="mb-4"> + <h2 class="fw-bold border-bottom pb-2">Job Details</h2> + </header> -<div class="container mt-5"> - <h2 class="text-center mb-4"> Détails de l'Offre d'Emploi</h2> + <!-- Alert Messages --> + <div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert"> + <i class="bi bi-check-circle-fill me-2"></i> + <span th:text="${successMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> - <div class="card shadow-lg"> - <div class="card-header bg-primary text-white text-center"> - <h4 th:text="${jobOffer.title}"></h4> - </div> + <div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert"> + <i class="bi bi-exclamation-triangle-fill me-2"></i> + <span th:text="${errorMessage}"></span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + + <div th:if="${job != null}" class="card shadow-sm mb-4"> <div class="card-body"> - <p><strong>ID de l'offre :</strong> <span th:text="${jobOffer.id}"></span></p> - <p><strong>Description :</strong> <span th:text="${jobOffer.taskDescription}"></span></p> - <p><strong>Entreprise :</strong> <span th:text="${jobOffer.company.denomination}"></span></p> - <p><strong>Date de publication :</strong> <span th:text="${jobOffer.publicationDate}"></span></p> - - <!-- ✅ Ajout du Niveau de Qualification --> - <p><strong>Niveau de qualification requis :</strong> - <span th:text="${jobOffer.qualificationLevel != null ? jobOffer.qualificationLevel.label : 'Non spécifié'}"></span> - </p> - - <!-- ✅ Section Secteurs améliorée avec des badges --> - <h5 class="mt-3">Secteurs :</h5> - <div class="d-flex flex-wrap"> - <span th:each="sector : ${jobOffer.sectors}" - th:text="${sector.label}" - class="badge bg-success text-white m-1 p-2"> - </span> + <div class="row g-3"> + <!-- Left Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-briefcase"></i> Job Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Job Title</label> + <div class="form-control-plaintext bg-light p-2 rounded" th:text="${job.title}"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Company</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${job.company?.denomination} ?: 'N/A'"></div> + </div> + + <div class="mb-3"> + <label class="form-label fw-semibold">Qualification Level</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${job.qualificationLevel.label}"></div> + </div> + </div> + + <!-- Right Column --> + <div class="col-md-6"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-calendar-event"></i> Additional Information + </h4> + + <div class="mb-3"> + <label class="form-label fw-semibold">Publication Date</label> + <div class="form-control-plaintext bg-light p-2 rounded" + th:text="${#temporals.format(job.publicationDate, 'MMMM d, yyyy')}"></div> + </div> + + <div th:if="${not #lists.isEmpty(job.sectors)}" class="mb-3"> + <label class="form-label fw-semibold">Sectors</label> + <div class="form-control-plaintext bg-light p-2 rounded"> + <span th:each="sector : ${job.sectors}" class="badge favorite_back me-1" + th:text="${sector.label}"></span> + </div> + </div> + </div> </div> - </div> - </div> - <div class="text-center mt-4"> - <a href="/jobs" class="btn btn-secondary btn-lg"> - ⬅ <i class="bi bi-arrow-left"></i> Retour - </a> - <a th:href="@{'/jobs/' + ${jobOffer.id} + '/edit'}" class="btn btn-warning btn-lg"> - ✏ <i class="bi bi-pencil-square"></i> Modifier - </a> - <a th:href="@{'/jobs/delete/' + ${jobOffer.id}}" - class="btn btn-danger btn-lg" - onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette offre ?');"> - 🗑 <i class="bi bi-trash"></i> Supprimer - </a> + <!-- Job Description Section --> + <div class="mt-4"> + <h4 class="favorite_color fw-bold mb-3"> + <i class="bi bi-file-text"></i> Job Description + </h4> + <div class="form-control-plaintext bg-light p-3 rounded" th:utext="${job.taskDescription}"></div> + </div> + </div> </div> -</div> -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> +</section> </body> -</html> +</html> \ No newline at end of file diff --git a/target/classes/templates/login.html b/target/classes/templates/login.html index fbaa5e7d82095b9735edaf37a77c33436b1ff78e..658fa7407419ff96a934949a1464ed33f366527d 100644 --- a/target/classes/templates/login.html +++ b/target/classes/templates/login.html @@ -1,45 +1,161 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="article(subcontent)" - th:replace="~{/baseTemplate/base :: layout(~{},~{::section})}"> - -<section class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1"> - <div class="modal-dialog" role="document"> - <div class="modal-content rounded-4 shadow"> - <header class="modal-header p-5 pb-4 border-bottom-0"> - <h1 class="fw-bold mb-0 fs-2">Sign in</h1> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> - </header> - - <article class="modal-body p-5 pt-0"> - <small class="text-secondary">Log in by entering your email address and password.</small> - <form action="/login" method="post"> - <fieldset class="mb-3 form-floating"> - <input type="email" id="uid" class="form-control rounded-3" name="mail" autofocus="autofocus" required - placeholder="name@domain.com" /> - <label for="uid">Email address</label> - </fieldset> - <fieldset class="mb-3 form-floating"> - <input type="password" id="idpwd" class="form-control rounded-3" name="password" placeholder="your password" - required /> - <label for="idpwd">Password</label> - </fieldset> - <input type="submit" value="Sign in" class="w-100 mb-2 btn btn-lg rounded-3 btn-primary" /> - </form> - - <!-- Affichage des erreurs de connexion --> - <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> - <p th:text="${error}"></p> + th:replace="~{/baseTemplate/base :: layout(~{},~{::section})}"> + +<head> + <style> + .favorite_color { + color:rgb(0, 183, 211); + } + + .favorite_back { + background-color: rgb(0, 183, 211); + } + + .favorite_outline { + border-color: rgb(0, 183, 211); + color: rgb(0, 183, 211); + } + + .favorite_outline:hover { + background-color: rgb(0, 183, 211); + color: white; + } + + .login-card { + border: none; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(7, 34, 98, 0.1); + overflow: hidden; + } + + .card-header { + background-color: rgb(0, 183, 211); + color: white; + padding: 1.5rem; + } + + .form-control { + border-radius: 8px; + padding: 12px 15px; + border: 1px solid #e0e0e0; + transition: all 0.3s; + } + + .form-control:focus { + border-color:rgb(0, 183, 211); + box-shadow: 0 0 0 0.25rem rgba(7, 34, 98, 0.25); + } + + .floating-label { + color: #6c757d; + transition: all 0.3s; + } + + .form-floating>.form-control:focus~label, + .form-floating>.form-control:not(:placeholder-shown)~label { + color:rgb(0, 183, 211); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); + } + + .login-btn { + padding: 12px; + font-size: 1rem; + letter-spacing: 0.5px; + transition: all 0.3s; + } + + .login-btn:hover { + background-color: rgb(0, 183, 211); + transform: translateY(-2px); + } + + .divider { + display: flex; + align-items: center; + margin: 1.5rem 0; + } + + .divider::before, + .divider::after { + content: ""; + flex: 1; + border-bottom: 1px solid #e0e0e0; + } + + .divider-text { + padding: 0 1rem; + color: #6c757d; + font-size: 0.9rem; + } + + .register-option { + transition: all 0.3s; + padding: 10px; + border-radius: 8px; + } + + .register-option:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(7, 34, 98, 0.1); + } + + .register-icon { + margin-right: 8px; + } + </style> +</head> + +<section class="container my-5"> + <div class="row justify-content-center"> + <div class="col-md-6 col-lg-5"> + <div class="card login-card"> + <div class="card-header favorite_back text-white text-center"> + <h3 class="mb-0">Welcome Back</h3> + <p class="mb-0 opacity-75">Sign in to your account</p> + </div> + + <div class="card-body p-4"> + <form th:action="@{/login}" method="post"> + <fieldset class="mb-3 form-floating"> + <input type="email" id="uid" class="form-control" name="mail" autofocus="autofocus" required + placeholder="name@domain.com" /> + <label for="uid" class="floating-label">Email address</label> + </fieldset> + + <fieldset class="mb-4 form-floating"> + <input type="password" id="idpwd" class="form-control" name="password" placeholder="your password" + required /> + <label for="idpwd" class="floating-label">Password</label> + </fieldset> + + <input type="submit" value="Sign in" class="w-100 btn login-btn text-black favorite_outline" /> + </form> + + <!-- Affichage des erreurs de connexion --> + <div th:if="${error}" class="alert alert-danger mt-3" role="alert"> + <p class="mb-0" th:text="${error}"></p> + </div> </div> - </article> - - <footer class="modal-body p-5 pt-0"> - <hr class="my-4"> - <a th:href="@{/companies/create}" class="btn w-100 mb-2 btn btn-lg rounded-3 btn-secondary">Sign up as company</a> - <a th:href="@{/candidates/signup}" class="btn w-100 mb-2 btn btn-lg rounded-3 btn-secondary">Sign up as candidate</a> - <small class="text-secondary">By clicking Sign up, you agree to the terms of use.</small> - </footer> + + <div class="px-4 pb-4"> + <div class="divider"> + <span class="divider-text">OR</span> + </div> + + <p class="mb-3 text-center">Don't have an account?</p> + + <div class="d-grid gap-3"> + <a th:href="@{/companies/create}" class="btn favorite_outline register-option"> + <i class="bi bi-building register-icon "></i> Register as Company + </a> + <a th:href="@{/candidates/signup}" class="btn favorite_outline register-option"> + <i class="bi bi-person register-icon"></i> Register as Candidate + </a> + </div> + </div> + </div> </div> </div> </section> - -</html> +</html> \ No newline at end of file diff --git a/target/classes/templates/qualificationLevel/qualificationLevelList.html b/target/classes/templates/qualificationLevel/qualificationLevelList.html index c97eb594998f17f9d42c75fb4be29ca89b36c7f3..edadb95f7d837654824babecc7a1081cee46fae1 100644 --- a/target/classes/templates/qualificationLevel/qualificationLevelList.html +++ b/target/classes/templates/qualificationLevel/qualificationLevelList.html @@ -1,45 +1,303 @@ <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> -<title>Qualification Level</title> +<title>Qualification Levels</title> <section> - <header> - <h1>List of qualification levels</h1> - </header> - <article> - <!-- <a href="javascript:history.back()"><img th:src="@{/img/back.png}" alt="Back"/></a> --> - <p th:if="${#lists.size(qualificationlevellist)} == 0">No qualification level defined yet.</p> - <th:block th:if="${#lists.size(qualificationlevellist)} > 0"> - <table class="table table-striped"> - <caption>List of qualification levels</caption> - <thead> - <tr> - <th scope="col">#</th> - <th scope="col">Label</th> - </tr> - </thead> - <tbody> - <tr th:each="ql : ${qualificationlevellist}"> - <th scope="row" th:text="${ql.id}" /> - <td th:text="${ql.label}" /> - <!-- <td th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="nav-item active"> - <a th:href="@{/deletequalificationlevel/{id}(id=${ql.id})}"> - <img th:src="@{img/minus.png}" alt="Delete this sector" class="minilogo"/> - </a> - </td> --> - </tr> - </tbody> - </table> - </th:block> - <!-- <div th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="row h-10"> - <form action="/addqualificationlevel" method="get" class="col-xs-12 col-sm-6 col-md-4 col-lg-2"> - <label for="labelql">Label</label> - <input type="text" id="labelql" name="labelql" autofocus="autofocus" minlength="3" required/> <br /> - - <input type="submit" value="Add" /> - </form> - </form> - </div> --> - </article> -</section> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .qualification-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .qualification-card:hover { + transform: translateY(-2px); + border-left-color: #3a7bd5; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .empty-state { + min-height: 300px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .search-container { + position: relative; + margin-bottom: 1.5rem; + } + .search-icon { + position: absolute; + left: 12px; + top: 10px; + color: #6c757d; + } + .search-input { + padding-left: 40px; + border-radius: 20px; + border: 1px solid #ced4da; + transition: all 0.3s; + } + .search-input:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + border-color: #86b7fe; + } + .hidden-row { + display: none; + } + .pagination .page-item.active .page-link { + background-color: rgb(0, 184, 222); + border-color: rgb(0, 184, 222); + color: white; + } + .pagination .page-link { + color: rgb(0, 184, 222); + } + .page-size-selector { + width: auto; + display: inline-block; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-award me-2"></i> Qualification Levels + </h2> + </div> + + <!-- Search and Page Size Controls --> + <div class="row mb-3"> + <div class="col-md-6"> + <div class="search-container"> + <i class="bi bi-search search-icon"></i> + <input type="text" class="form-control search-input" id="liveSearchInput" + placeholder="Start typing to filter qualification levels..." autocomplete="off"> + </div> + </div> + <div class="col-md-6 text-md-end"> + <div class="d-inline-flex align-items-center"> + <span class="me-2">Show:</span> + <select class="form-select form-select-sm page-size-selector" id="pageSizeSelect"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + <span class="ms-2">entries</span> + </div> + </div> + </div> + + <!-- Qualification Levels Table --> + <div class="card shadow-sm table-container mb-3"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" id="qualificationsTable"> + <thead class="table-light"> + <tr> + <th>Label</th> + <th class="text-end pe-4" th:if="${session != null and session.logintype == 'adm'}">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="ql : ${qualificationlevellist}" class="qualification-card"> + <td class="fw-semibold qualification-label" th:text="${ql.label}"></td> + <td class="text-end pe-4" th:if="${session != null and session.logintype == 'adm'}"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{/deletequalificationlevel/{id}(id=${ql.id})}" + class="btn btn-sm btn-outline-danger"> + <i class="bi bi-trash"></i> Delete + </a> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State (initial) --> + <div th:if="${#lists.isEmpty(qualificationlevellist)}" class="empty-state text-center py-5"> + <i class="bi bi-award text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No qualification levels defined</h4> + </div> + + <!-- Empty State (after filtering) --> + <div id="noResultsState" class="empty-state text-center py-5" style="display: none;"> + <i class="bi bi-search text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No matching qualifications found</h4> + <p class="text-muted">Try adjusting your search</p> + </div> + </div> + </div> + </div> + + <!-- Pagination and Info --> + <div class="row"> + <div class="col-md-6"> + <div id="pageInfo" class="text-muted">Showing 1 to 10 of <span id="totalRecords">0</span> entries</div> + </div> + <div class="col-md-6"> + <nav aria-label="Page navigation" class="float-md-end"> + <ul class="pagination" id="pagination"> + <li class="page-item disabled" id="prevPage"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> + </a> + </li> + <!-- Pages will be inserted here by JavaScript --> + <li class="page-item" id="nextPage"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + </div> + </div> + </div> + + <script> + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('liveSearchInput'); + const tableRows = document.querySelectorAll('#qualificationsTable tbody tr'); + const noResultsState = document.getElementById('noResultsState'); + const initialEmptyState = document.querySelector('.empty-state'); + const pageSizeSelect = document.getElementById('pageSizeSelect'); + const pagination = document.getElementById('pagination'); + const prevPage = document.getElementById('prevPage'); + const nextPage = document.getElementById('nextPage'); + const pageInfo = document.getElementById('pageInfo'); + const totalRecords = document.getElementById('totalRecords'); + + let currentPage = 1; + let pageSize = parseInt(pageSizeSelect.value); + let filteredRows = Array.from(tableRows); + + // Initialize + updateTable(); + updatePagination(); + + // Search functionality + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + filteredRows = Array.from(tableRows).filter(row => { + const qualLabel = row.querySelector('.qualification-label').textContent.toLowerCase(); + return qualLabel.includes(searchTerm); + }); + + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Page size change + pageSizeSelect.addEventListener('change', function() { + pageSize = parseInt(this.value); + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Pagination click handlers + pagination.addEventListener('click', function(e) { + e.preventDefault(); + if (e.target.closest('.page-link')) { + const target = e.target.closest('.page-link'); + if (target.getAttribute('aria-label') === 'Previous') { + if (currentPage > 1) currentPage--; + } else if (target.getAttribute('aria-label') === 'Next') { + if (currentPage < Math.ceil(filteredRows.length / pageSize)) currentPage++; + } else { + currentPage = parseInt(target.textContent); + } + updateTable(); + updatePagination(); + } + }); + function updateTable() { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const visibleRows = filteredRows.slice(start, end); + + // Hide all rows first + tableRows.forEach(row => { + row.classList.add('hidden-row'); + }); + + // Show only visible rows + visibleRows.forEach(row => { + row.classList.remove('hidden-row'); + }); + + // Update empty states + if (filteredRows.length === 0) { + noResultsState.style.display = 'flex'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } else { + noResultsState.style.display = 'none'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } + } + + function updatePagination() { + const totalPages = Math.ceil(filteredRows.length / pageSize); + totalRecords.textContent = filteredRows.length; + + // Update page info + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredRows.length); + pageInfo.textContent = `Showing ${start} to ${end} of ${filteredRows.length} entries`; + + // Clear existing page numbers + const pageNumbers = pagination.querySelectorAll('.page-number'); + pageNumbers.forEach(el => el.remove()); + + // Add new page numbers + const maxVisiblePages = 5; + let startPage, endPage; + + if (totalPages <= maxVisiblePages) { + startPage = 1; + endPage = totalPages; + } else { + const maxVisibleBeforeCurrent = Math.floor(maxVisiblePages / 2); + const maxVisibleAfterCurrent = Math.ceil(maxVisiblePages / 2) - 1; + + if (currentPage <= maxVisibleBeforeCurrent) { + startPage = 1; + endPage = maxVisiblePages; + } else if (currentPage + maxVisibleAfterCurrent >= totalPages) { + startPage = totalPages - maxVisiblePages + 1; + endPage = totalPages; + } else { + startPage = currentPage - maxVisibleBeforeCurrent; + endPage = currentPage + maxVisibleAfterCurrent; + } + } + + // Add page number items + for (let i = startPage; i <= endPage; i++) { + const pageItem = document.createElement('li'); + pageItem.className = `page-item page-number ${i === currentPage ? 'active' : ''}`; + pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; + nextPage.parentNode.insertBefore(pageItem, nextPage); + } + + // Update prev/next buttons + prevPage.classList.toggle('disabled', currentPage === 1); + nextPage.classList.toggle('disabled', currentPage === totalPages || totalPages === 0); + } + }); + </script> + </body> +</section> </html> \ No newline at end of file diff --git a/target/classes/templates/sector/sectorList.html b/target/classes/templates/sector/sectorList.html index b30d47150d06d3924541dfb2ccdae1c77e5874f6..cb3e68a31b1340aa603bbbf562079d54661bb450 100644 --- a/target/classes/templates/sector/sectorList.html +++ b/target/classes/templates/sector/sectorList.html @@ -2,48 +2,307 @@ <html xmlns:th="http://www.thymeleaf.org" th:replace="~{/baseTemplate/base :: layout(~{::title},~{::section})}"> <title>Sectors</title> <section> - <header> - <h1>List of sectors</h1> - </header> - <article> - <p th:if="${#lists.size(sectorlist)} == 0">No sector defined yet.</p> - - <th:block th:if="${#lists.size(sectorlist)} > 0"> - - <table class="table table-striped"> - <caption>List of sectors</caption> - <thead> - <tr> - <th scope="col">#</th> - <th scope="col">Label</th> - </tr> - </thead> - <tbody> - <tr th:each="sec : ${sectorlist}"> - <th scope="row" th:text="${sec.id}" /> - <td th:text="${sec.label}" /> - <td th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" - class="nav-item active"> - <a th:href="@{/deletesector/{id}(id=${sec.id})}"> - <img th:src="@{img/minus.png}" alt="Delete this sector" class="minilogo" /> + <head> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> + <style> + .sector-card { + transition: all 0.3s ease; + border-left: 4px solid transparent; + } + .sector-card:hover { + transform: translateY(-2px); + border-left-color: #00B8DEFF; + background-color: #f8f9fa; + } + .table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .empty-state { + min-height: 300px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .search-container { + position: relative; + margin-bottom: 1.5rem; + } + .search-icon { + position: absolute; + left: 12px; + top: 10px; + color: #6c757d; + } + .search-input { + padding-left: 40px; + border-radius: 20px; + border: 1px solid #ced4da; + transition: all 0.3s; + } + .search-input:focus { + box-shadow: 0 0 0 0.25rem rgb(0, 184, 222); + border-color: #00B8DEFF; + + } + .hidden-row { + display: none; + } + .pagination .page-item.active .page-link { + background-color: #00B8DEFF; + border-color: #00B8DEFF; + color: #ffffff; + } + .pagination .page-link { + color: #00B8DEFF; + } + .page-size-selector { + width: auto; + display: inline-block; + } + .controls-row { + margin-bottom: 1.5rem; + } + </style> + </head> + <body class="bg-light"> + <div class="container py-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h2 class="fw-bold text-black mb-0"> + <i class="bi bi-tags me-2"></i> Sectors + </h2> + </div> + + <!-- Search and Page Size Controls --> + <div class="row controls-row"> + <div class="col-md-6"> + <div class="search-container"> + <i class="bi bi-search search-icon"></i> + <input type="text" class="form-control search-input" id="liveSearchInput" + placeholder="Start typing to filter sectors..." autocomplete="off"> + </div> + </div> + <div class="col-md-6 text-md-end"> + <div class="d-inline-flex align-items-center"> + <span class="me-2">Show:</span> + <select class="form-select form-select-sm page-size-selector" id="pageSizeSelect"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + <span class="ms-2">entries</span> + </div> + </div> + </div> + + <!-- Sectors Table --> + <div class="card shadow-sm table-container mb-3"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover align-middle mb-0" id="sectorsTable"> + <thead class="table-light"> + <tr> + <th>Label</th> + <th class="text-end pe-4">Actions</th> + </tr> + </thead> + <tbody> + <tr th:each="sec : ${sectorlist}" class="sector-card"> + <td class="fw-semibold sector-label" th:text="${sec.label}"></td> + <td class="text-end pe-4"> + <div class="d-flex gap-2 justify-content-end"> + <a th:href="@{/deletesector/{id}(id=${sec.id})}" + class="btn btn-sm btn-outline-danger" + th:if="${session != null and session.logintype == 'adm'}"> + <i class="bi bi-trash"></i> Delete + </a> + </div> + </td> + </tr> + </tbody> + </table> + + <!-- Empty State (initial) --> + <div th:if="${#lists.isEmpty(sectorlist)}" class="empty-state text-center py-5"> + <i class="bi bi-tag text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No sectors defined</h4> + </div> + + <!-- Empty State (after filtering) --> + <div id="noResultsState" class="empty-state text-center py-5" style="display: none;"> + <i class="bi bi-search text-muted" style="font-size: 3rem;"></i> + <h4 class="mt-3 text-muted">No matching sectors found</h4> + <p class="text-muted">Try adjusting your search</p> + </div> + </div> + </div> + </div> + + <!-- Pagination and Info --> + <div class="row"> + <div class="col-md-6"> + <div id="pageInfo" class="text-muted">Showing 1 to 10 of <span id="totalRecords">0</span> entries</div> + </div> + <div class="col-md-6"> + <nav aria-label="Page navigation" class="float-md-end"> + <ul class="pagination" id="pagination"> + <li class="page-item disabled" id="prevPage"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> </a> - </td> - </tr> - </tbody> - </table> - </th:block> - - <aside th:if="${#ctx.session.uid} != null AND ${#ctx.session.logintype} == 'adm'" class="row h-10"> - <form action="/addsector" method="get" class="col-xs-12 col-sm-6 col-md-4 col-lg-2"> - <fieldset> - <label for="labelsector">Label</label> - <input type="text" id="labelsector" name="labelsector" autofocus="autofocus" minlength="3" - required /> - </fieldset> - <input type="submit" value="Add" /> - </form> - </aside> - </article> -</section> + </li> + <!-- Pages will be inserted here by JavaScript --> + <li class="page-item" id="nextPage"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + </div> + </div> + </div> + + <script> + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('liveSearchInput'); + const tableRows = document.querySelectorAll('#sectorsTable tbody tr'); + const noResultsState = document.getElementById('noResultsState'); + const initialEmptyState = document.querySelector('.empty-state'); + const pageSizeSelect = document.getElementById('pageSizeSelect'); + const pagination = document.getElementById('pagination'); + const prevPage = document.getElementById('prevPage'); + const nextPage = document.getElementById('nextPage'); + const pageInfo = document.getElementById('pageInfo'); + const totalRecords = document.getElementById('totalRecords'); + + let currentPage = 1; + let pageSize = parseInt(pageSizeSelect.value); + let filteredRows = Array.from(tableRows); + + // Initialize + updateTable(); + updatePagination(); + + // Search functionality + searchInput.addEventListener('input', function() { + const searchTerm = this.value.toLowerCase(); + filteredRows = Array.from(tableRows).filter(row => { + const sectorLabel = row.querySelector('.sector-label').textContent.toLowerCase(); + return sectorLabel.includes(searchTerm); + }); + + currentPage = 1; + updateTable(); + updatePagination(); + }); + + // Page size change + pageSizeSelect.addEventListener('change', function() { + pageSize = parseInt(this.value); + currentPage = 1; + updateTable(); + updatePagination(); + }); + // Pagination click handlers + pagination.addEventListener('click', function(e) { + e.preventDefault(); + if (e.target.closest('.page-link')) { + const target = e.target.closest('.page-link'); + if (target.getAttribute('aria-label') === 'Previous') { + if (currentPage > 1) currentPage--; + } else if (target.getAttribute('aria-label') === 'Next') { + if (currentPage < Math.ceil(filteredRows.length / pageSize)) currentPage++; + } else { + currentPage = parseInt(target.textContent); + } + updateTable(); + updatePagination(); + } + }); + + function updateTable() { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const visibleRows = filteredRows.slice(start, end); + + // Hide all rows first + tableRows.forEach(row => { + row.classList.add('hidden-row'); + }); + + // Show only visible rows + visibleRows.forEach(row => { + row.classList.remove('hidden-row'); + }); + + // Update empty states + if (filteredRows.length === 0) { + noResultsState.style.display = 'flex'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } else { + noResultsState.style.display = 'none'; + if (initialEmptyState) initialEmptyState.style.display = 'none'; + } + } + + function updatePagination() { + const totalPages = Math.ceil(filteredRows.length / pageSize); + totalRecords.textContent = filteredRows.length; + + // Update page info + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredRows.length); + pageInfo.textContent = `Showing ${start} to ${end} of ${filteredRows.length} entries`; + + // Clear existing page numbers + const pageNumbers = pagination.querySelectorAll('.page-number'); + pageNumbers.forEach(el => el.remove()); + + // Add new page numbers + const maxVisiblePages = 5; + let startPage, endPage; + + if (totalPages <= maxVisiblePages) { + startPage = 1; + endPage = totalPages; + } else { + const maxVisibleBeforeCurrent = Math.floor(maxVisiblePages / 2); + const maxVisibleAfterCurrent = Math.ceil(maxVisiblePages / 2) - 1; + + if (currentPage <= maxVisibleBeforeCurrent) { + startPage = 1; + endPage = maxVisiblePages; + } else if (currentPage + maxVisibleAfterCurrent >= totalPages) { + startPage = totalPages - maxVisiblePages + 1; + endPage = totalPages; + } else { + startPage = currentPage - maxVisibleBeforeCurrent; + endPage = currentPage + maxVisibleAfterCurrent; + } + } + + // Add page number items + for (let i = startPage; i <= endPage; i++) { + const pageItem = document.createElement('li'); + pageItem.className = `page-item page-number ${i === currentPage ? 'active' : ''}`; + pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; + nextPage.parentNode.insertBefore(pageItem, nextPage); + } + + // Update prev/next buttons + prevPage.classList.toggle('disabled', currentPage === 1); + nextPage.classList.toggle('disabled', currentPage === totalPages || totalPages === 0); + } + }); + </script> + </body> +</section> </html> \ No newline at end of file diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 35313550cab125c17caea98db584f277ab2490c3..57d806f7718efa6532a2dcbe48c875ed84c8f7d3 100644 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -10,6 +10,7 @@ fr\atlantique\imt\inf211\jobmngt\dao\ApplicationMessageDao.class fr\atlantique\imt\inf211\jobmngt\entity\Candidate.class fr\atlantique\imt\inf211\jobmngt\controller\LoginController.class fr\atlantique\imt\inf211\jobmngt\config\WebConfig.class +fr\atlantique\imt\inf211\jobmngt\config\CompanyUserDetailsService.class fr\atlantique\imt\inf211\jobmngt\service\JobOfferService.class fr\atlantique\imt\inf211\jobmngt\controller\ApplicationController.class fr\atlantique\imt\inf211\jobmngt\service\CandidateServiceImpl.class @@ -19,6 +20,7 @@ fr\atlantique\imt\inf211\jobmngt\dao\ApplicationDao.class fr\atlantique\imt\inf211\jobmngt\service\AppUserService.class fr\atlantique\imt\inf211\jobmngt\dao\AppUserDao.class fr\atlantique\imt\inf211\jobmngt\dao\CompanyDao.class +fr\atlantique\imt\inf211\jobmngt\config\SecurityConfig.class fr\atlantique\imt\inf211\jobmngt\entity\QualificationLevel.class fr\atlantique\imt\inf211\jobmngt\service\JobOfferServiceImpl.class fr\atlantique\imt\inf211\jobmngt\controller\PagesController.class @@ -30,6 +32,7 @@ fr\atlantique\imt\inf211\jobmngt\entity\JobOffer.class fr\atlantique\imt\inf211\jobmngt\service\ApplicationServiceImpl.class fr\atlantique\imt\inf211\jobmngt\converter\QualificationLevelConverter.class fr\atlantique\imt\inf211\jobmngt\service\QualificationLevelService.class +fr\atlantique\imt\inf211\jobmngt\entity\RoleType.class fr\atlantique\imt\inf211\jobmngt\service\AppUserServiceImpl.class fr\atlantique\imt\inf211\jobmngt\entity\Sector.class fr\atlantique\imt\inf211\jobmngt\service\CompanyService.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 6646ede2f30f239d1fc9b576e5a070bda455cf0f..82fd1952fe706376f8782ca62ef427470dc931bb 100644 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,44 +1,47 @@ -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\OfferMessageDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\AppUser.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\SectorServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\AppUserServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\ApplicationService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\ApplicationMessage.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Candidate.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\PagesController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\JobOfferController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Company.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\CompanyController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\ApplicationDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CandidateServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Application.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\CandidateController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\QualificationLevelController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\AppUserService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\QualificationLevelServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\QualificationLevelDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\config\WebConfig.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\JobOffer.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\ApplicationServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CompanyServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\ApplicationController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\SectorService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\JobOfferService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\JobOfferServiceImpl.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\JobOfferDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\CandidateDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CompanyService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CandidateService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\QualificationLevelService.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\converter\QualificationLevelConverter.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Sector.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\converter\CompanyConverter.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\JobmngtApplication.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\ApplicationMessageDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\CompanyDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\SectorDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\AppUserDao.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\OfferMessage.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\QualificationLevel.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\LoginController.java -C:\Users\LENOVO\Desktop\JobManagement-1\JobmngmntVersionAFP\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\SectorController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\AppUserService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CompanyServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\ApplicationDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\AppUser.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\config\WebConfig.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\AppUserDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CompanyService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\SectorController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\QualificationLevelDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\JobOfferServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\CandidateDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\SectorService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\PagesController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\RoleType.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\ApplicationService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\CompanyController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\converter\QualificationLevelConverter.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\JobmngtApplication.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CandidateServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\QualificationLevelServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\JobOffer.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\OfferMessageDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\JobOfferDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\CandidateService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\QualificationLevelController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Candidate.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\SectorServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\ApplicationMessageDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\OfferMessage.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\CompanyDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\AppUserServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\config\CompanyUserDetailsService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\ApplicationController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\CandidateController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\dao\SectorDao.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\ApplicationMessage.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\JobOfferService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Sector.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\LoginController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\ApplicationServiceImpl.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\controller\JobOfferController.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\converter\CompanyConverter.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\service\QualificationLevelService.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Company.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\QualificationLevel.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\entity\Application.java +C:\Users\Marwa\Desktop\tutorial\rania\jobmgmnt_merges\src\main\java\fr\atlantique\imt\inf211\jobmngt\config\SecurityConfig.java