Professor reorder

This commit is contained in:
2026-05-12 12:15:55 +05:30
parent 3856524152
commit a10803ee73
10 changed files with 881 additions and 596 deletions

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
}
}