Professor reorder
This commit is contained in:
@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
@ -33,19 +34,19 @@ public class ProfessorResource {
|
||||
|
||||
@PostMapping("register")
|
||||
public Professor register(@RequestBody Professor professor) {
|
||||
return professorService.register(professor.getFirstName(), professor.getLastName(), professor.getEmail(), professor.getDepartment(), professor.getPosition());
|
||||
return professorService.register(professor.getFirstName(), professor.getLastName(),
|
||||
professor.getEmail(), professor.getDepartment(), professor.getPosition());
|
||||
}
|
||||
|
||||
@PostMapping("add")
|
||||
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||
log.debug("Professor DTO: {}", professorDto);
|
||||
Professor professor = professorService.addNewProfessor(professorDto);
|
||||
return ResponseEntity.ok(professor);
|
||||
}
|
||||
|
||||
|
||||
@PutMapping("{professorId}")
|
||||
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
|
||||
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
|
||||
log.debug("Professor DTO: {}", professorDto);
|
||||
return professorService.updateProfessor(professorId, professorDto);
|
||||
}
|
||||
@ -77,12 +78,14 @@ public class ProfessorResource {
|
||||
}
|
||||
|
||||
@PutMapping("{professorId}/profile-image")
|
||||
public Professor updateProfileImage(@PathVariable UUID professorId, @RequestParam MultipartFile profileImage) {
|
||||
public Professor updateProfileImage(@PathVariable UUID professorId,
|
||||
@RequestParam MultipartFile profileImage) {
|
||||
return professorService.updateProfileImage(professorId, profileImage);
|
||||
}
|
||||
|
||||
@GetMapping(path = "{professorId}/profile-image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId, @PathVariable String filename) {
|
||||
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId,
|
||||
@PathVariable String filename) {
|
||||
return professorService.getImageByProfessorId(professorId, filename);
|
||||
}
|
||||
|
||||
@ -90,4 +93,24 @@ public class ProfessorResource {
|
||||
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
|
||||
return professorService.getDefaultProfileImage(professorId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-update displayOrder.
|
||||
*
|
||||
* Accepts a JSON array of professor UUIDs in the desired display order.
|
||||
* The array index becomes each professor's displayOrder value.
|
||||
*
|
||||
* Example request body:
|
||||
* ["uuid-A", "uuid-B", "uuid-C"]
|
||||
*
|
||||
* Returns the full professor list sorted by the new displayOrder.
|
||||
*
|
||||
* Requires ADMIN or MANAGER role (configure in your SecurityConfig).
|
||||
*/
|
||||
@PutMapping("order")
|
||||
public ResponseEntity<List<Professor>> updateDisplayOrder(@RequestBody List<UUID> orderedIds) {
|
||||
log.info("Updating display order for {} professors", orderedIds.size());
|
||||
List<Professor> updated = professorService.updateDisplayOrder(orderedIds);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,6 @@ package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.Fetch;
|
||||
import org.hibernate.annotations.FetchMode;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import javax.persistence.*;
|
||||
@ -46,35 +44,40 @@ public class Professor implements Serializable {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private ProfessorCategory category;
|
||||
|
||||
/**
|
||||
* Controls display order within each category section on the public frontend.
|
||||
* Lower values appear first. Defaults to 0 (new professors appear at the top).
|
||||
* Admins can drag-and-drop rows in the management UI to reorder.
|
||||
*/
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
@Builder.Default
|
||||
private Integer displayOrder = 0;
|
||||
|
||||
// Additional fields for Next.js integration
|
||||
private String phone;
|
||||
private String specialty;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String certification;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String training;
|
||||
|
||||
|
||||
private String experience;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
|
||||
private String designation;
|
||||
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
|
||||
@Column(name = "work_day")
|
||||
private List<String> workDays;
|
||||
|
||||
// ✅ CRITICAL FIX: Added orphanRemoval = true
|
||||
// This tells JPA to DELETE skills that are removed from the collection
|
||||
|
||||
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
private Set<ProfessorSkill> skills;
|
||||
|
||||
// ✅ CRITICAL FIX: Added orphanRemoval = true
|
||||
// This tells JPA to DELETE awards that are removed from the collection
|
||||
|
||||
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
private Set<ProfessorAward> awards;
|
||||
|
||||
@ -82,7 +85,6 @@ public class Professor implements Serializable {
|
||||
@JsonIgnore
|
||||
private List<Post> posts;
|
||||
|
||||
// Convenience method to get full name
|
||||
public String getName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@ package net.shyshkin.study.fullstack.supportportal.backend.repository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -25,15 +27,35 @@ public interface ProfessorRepository extends JpaRepository<Professor, Long> {
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
|
||||
Optional<Professor> findByProfessorId(@Param("professorId") UUID professorId);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status")
|
||||
|
||||
// ── Status / category filters, ordered by displayOrder then lastName ────────
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByStatus(@Param("status") WorkingStatus status, Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.category = :category")
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category")
|
||||
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||
@Param("category") ProfessorCategory category,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||
@Param("category") ProfessorCategory category,
|
||||
Pageable pageable);
|
||||
|
||||
// ── Bulk display-order update ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sets displayOrder for a single professor identified by professorId.
|
||||
* Used by the bulk-reorder service method.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Professor p SET p.displayOrder = :displayOrder WHERE p.professorId = :professorId")
|
||||
void updateDisplayOrder(@Param("professorId") UUID professorId,
|
||||
@Param("displayOrder") int displayOrder);
|
||||
|
||||
/**
|
||||
* Returns all professors ordered by displayOrder ASC, then lastName ASC.
|
||||
* Used by the admin reorder endpoint to return the updated list.
|
||||
*/
|
||||
@Query("SELECT p FROM Professor p ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
List<Professor> findAllOrderedByDisplayOrder();
|
||||
}
|
||||
@ -7,6 +7,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProfessorService {
|
||||
@ -30,15 +31,20 @@ public interface ProfessorService {
|
||||
byte[] getImageByProfessorId(UUID professorId, String filename);
|
||||
|
||||
byte[] getDefaultProfileImage(UUID professorId);
|
||||
|
||||
// Existing method for active professors
|
||||
|
||||
Page<Professor> findActiveProfessors(Pageable pageable);
|
||||
|
||||
// New methods for category-based filtering
|
||||
|
||||
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
|
||||
|
||||
|
||||
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
|
||||
|
||||
// Method to find professor with details
|
||||
|
||||
Professor findProfessorWithDetailsById(UUID professorId);
|
||||
|
||||
/**
|
||||
* Bulk-update displayOrder for all professors.
|
||||
*
|
||||
* @param orderedIds Professor UUIDs in the desired display order (index 0 = first).
|
||||
* @return All professors sorted by their new displayOrder.
|
||||
*/
|
||||
List<Professor> updateDisplayOrder(List<UUID> orderedIds);
|
||||
}
|
||||
@ -35,6 +35,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
|
||||
import static org.springframework.http.MediaType.*;
|
||||
@ -63,10 +64,6 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses ISO date string from frontend (e.g. "2026-04-22T23:17:58.831Z")
|
||||
* into LocalDateTime. Falls back to now() if null/blank/invalid.
|
||||
*/
|
||||
private LocalDateTime parseJoinDate(String joinDate) {
|
||||
if (joinDate == null || joinDate.isBlank()) {
|
||||
return LocalDateTime.now();
|
||||
@ -179,6 +176,12 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
|
||||
|
||||
// New professors get displayOrder = 0 by default (appear first).
|
||||
// Admins can re-order via the drag-and-drop endpoint.
|
||||
if (professor.getDisplayOrder() == null) {
|
||||
professor.setDisplayOrder(0);
|
||||
}
|
||||
|
||||
Professor savedProfessor = professorRepository.save(professor);
|
||||
|
||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||
@ -238,17 +241,15 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.setDescription(professorDto.getDescription());
|
||||
professor.setDesignation(professorDto.getDesignation());
|
||||
professor.setWorkDays(professorDto.getWorkDays());
|
||||
// displayOrder is intentionally NOT updated here — only via the reorder endpoint.
|
||||
|
||||
// Parse joinDate string safely
|
||||
if (professorDto.getJoinDate() != null && !professorDto.getJoinDate().isBlank()) {
|
||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||
}
|
||||
|
||||
final Professor professorRef = professor;
|
||||
|
||||
if (professor.getSkills() == null) {
|
||||
professor.setSkills(new HashSet<>());
|
||||
}
|
||||
if (professor.getSkills() == null) professor.setSkills(new HashSet<>());
|
||||
professor.getSkills().clear();
|
||||
|
||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||
@ -263,9 +264,7 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.getSkills().addAll(newSkills);
|
||||
}
|
||||
|
||||
if (professor.getAwards() == null) {
|
||||
professor.setAwards(new HashSet<>());
|
||||
}
|
||||
if (professor.getAwards() == null) professor.setAwards(new HashSet<>());
|
||||
professor.getAwards().clear();
|
||||
|
||||
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
|
||||
@ -329,4 +328,19 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
|
||||
return responseEntity.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a new displayOrder for every professor in the given list.
|
||||
* The list position (index) becomes the professor's displayOrder value.
|
||||
*
|
||||
* @param orderedIds UUIDs in desired display order; index 0 → displayOrder=0, etc.
|
||||
* @return All professors sorted by their updated displayOrder.
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public List<Professor> updateDisplayOrder(List<UUID> orderedIds) {
|
||||
IntStream.range(0, orderedIds.size())
|
||||
.forEach(i -> professorRepository.updateDisplayOrder(orderedIds.get(i), i));
|
||||
return professorRepository.findAllOrderedByDisplayOrder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user