Professor reorder
This commit is contained in:
@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.OK;
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
@ -33,19 +34,19 @@ public class ProfessorResource {
|
|||||||
|
|
||||||
@PostMapping("register")
|
@PostMapping("register")
|
||||||
public Professor register(@RequestBody Professor professor) {
|
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")
|
@PostMapping("add")
|
||||||
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||||
log.debug("Professor DTO: {}", professorDto);
|
log.debug("Professor DTO: {}", professorDto);
|
||||||
Professor professor = professorService.addNewProfessor(professorDto);
|
Professor professor = professorService.addNewProfessor(professorDto);
|
||||||
return ResponseEntity.ok(professor);
|
return ResponseEntity.ok(professor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PutMapping("{professorId}")
|
@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);
|
log.debug("Professor DTO: {}", professorDto);
|
||||||
return professorService.updateProfessor(professorId, professorDto);
|
return professorService.updateProfessor(professorId, professorDto);
|
||||||
}
|
}
|
||||||
@ -77,12 +78,14 @@ public class ProfessorResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("{professorId}/profile-image")
|
@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);
|
return professorService.updateProfileImage(professorId, profileImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "{professorId}/profile-image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
|
@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);
|
return professorService.getImageByProfessorId(professorId, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,4 +93,24 @@ public class ProfessorResource {
|
|||||||
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
|
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
|
||||||
return professorService.getDefaultProfileImage(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 com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.Fetch;
|
|
||||||
import org.hibernate.annotations.FetchMode;
|
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
@ -46,35 +44,40 @@ public class Professor implements Serializable {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private ProfessorCategory category;
|
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
|
// Additional fields for Next.js integration
|
||||||
private String phone;
|
private String phone;
|
||||||
private String specialty;
|
private String specialty;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String certification;
|
private String certification;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String training;
|
private String training;
|
||||||
|
|
||||||
private String experience;
|
private String experience;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
private String designation;
|
private String designation;
|
||||||
|
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
|
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
|
||||||
@Column(name = "work_day")
|
@Column(name = "work_day")
|
||||||
private List<String> workDays;
|
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)
|
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
private Set<ProfessorSkill> skills;
|
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)
|
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
private Set<ProfessorAward> awards;
|
private Set<ProfessorAward> awards;
|
||||||
|
|
||||||
@ -82,7 +85,6 @@ public class Professor implements Serializable {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private List<Post> posts;
|
private List<Post> posts;
|
||||||
|
|
||||||
// Convenience method to get full name
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return firstName + " " + lastName;
|
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.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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")
|
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
|
||||||
Optional<Professor> findByProfessorId(@Param("professorId") UUID 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);
|
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);
|
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category")
|
@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,
|
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||||
@Param("category") ProfessorCategory category,
|
@Param("category") ProfessorCategory category,
|
||||||
Pageable pageable);
|
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.data.domain.Pageable;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface ProfessorService {
|
public interface ProfessorService {
|
||||||
@ -30,15 +31,20 @@ public interface ProfessorService {
|
|||||||
byte[] getImageByProfessorId(UUID professorId, String filename);
|
byte[] getImageByProfessorId(UUID professorId, String filename);
|
||||||
|
|
||||||
byte[] getDefaultProfileImage(UUID professorId);
|
byte[] getDefaultProfileImage(UUID professorId);
|
||||||
|
|
||||||
// Existing method for active professors
|
|
||||||
Page<Professor> findActiveProfessors(Pageable pageable);
|
Page<Professor> findActiveProfessors(Pageable pageable);
|
||||||
|
|
||||||
// New methods for category-based filtering
|
|
||||||
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
|
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
|
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
// Method to find professor with details
|
|
||||||
Professor findProfessorWithDetailsById(UUID professorId);
|
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.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
|
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
|
||||||
import static org.springframework.http.MediaType.*;
|
import static org.springframework.http.MediaType.*;
|
||||||
@ -63,10 +64,6 @@ public class ProfessorServiceImpl implements ProfessorService {
|
|||||||
.build();
|
.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) {
|
private LocalDateTime parseJoinDate(String joinDate) {
|
||||||
if (joinDate == null || joinDate.isBlank()) {
|
if (joinDate == null || joinDate.isBlank()) {
|
||||||
return LocalDateTime.now();
|
return LocalDateTime.now();
|
||||||
@ -179,6 +176,12 @@ public class ProfessorServiceImpl implements ProfessorService {
|
|||||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||||
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
|
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);
|
Professor savedProfessor = professorRepository.save(professor);
|
||||||
|
|
||||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||||
@ -238,17 +241,15 @@ public class ProfessorServiceImpl implements ProfessorService {
|
|||||||
professor.setDescription(professorDto.getDescription());
|
professor.setDescription(professorDto.getDescription());
|
||||||
professor.setDesignation(professorDto.getDesignation());
|
professor.setDesignation(professorDto.getDesignation());
|
||||||
professor.setWorkDays(professorDto.getWorkDays());
|
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()) {
|
if (professorDto.getJoinDate() != null && !professorDto.getJoinDate().isBlank()) {
|
||||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Professor professorRef = professor;
|
final Professor professorRef = professor;
|
||||||
|
|
||||||
if (professor.getSkills() == null) {
|
if (professor.getSkills() == null) professor.setSkills(new HashSet<>());
|
||||||
professor.setSkills(new HashSet<>());
|
|
||||||
}
|
|
||||||
professor.getSkills().clear();
|
professor.getSkills().clear();
|
||||||
|
|
||||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||||
@ -263,9 +264,7 @@ public class ProfessorServiceImpl implements ProfessorService {
|
|||||||
professor.getSkills().addAll(newSkills);
|
professor.getSkills().addAll(newSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (professor.getAwards() == null) {
|
if (professor.getAwards() == null) professor.setAwards(new HashSet<>());
|
||||||
professor.setAwards(new HashSet<>());
|
|
||||||
}
|
|
||||||
professor.getAwards().clear();
|
professor.getAwards().clear();
|
||||||
|
|
||||||
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
|
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
|
||||||
@ -329,4 +328,19 @@ public class ProfessorServiceImpl implements ProfessorService {
|
|||||||
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
|
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
|
||||||
return responseEntity.getBody();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +71,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search Box */
|
/* Search Box */
|
||||||
@ -162,6 +163,109 @@
|
|||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Reorder toolbar buttons ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.btn-reorder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reorder:hover {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-order {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
background: #16a34a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-order:hover:not(:disabled) {
|
||||||
|
background: #15803d;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-order:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-reorder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: white;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-reorder:hover:not(:disabled) {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #dc2626;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel-reorder:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Instructional hint badge shown while in reorder mode */
|
||||||
|
.reorder-hint-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tiny spinner inside Save button */
|
||||||
|
.spinner-sm {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Table Container */
|
/* Table Container */
|
||||||
.table-container {
|
.table-container {
|
||||||
animation: fadeIn 0.3s ease;
|
animation: fadeIn 0.3s ease;
|
||||||
@ -205,9 +309,16 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact header columns for drag handle and order number */
|
||||||
|
.professors-table thead th.th-handle,
|
||||||
|
.professors-table thead th.th-order {
|
||||||
|
padding: 16px 8px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.professors-table tbody tr {
|
.professors-table tbody tr {
|
||||||
border-bottom: 1px solid #f3f4f6;
|
border-bottom: 1px solid #f3f4f6;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.15s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +337,93 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Table in reorder mode ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Subtle amber top-border to signal reorder mode */
|
||||||
|
.professors-table.reorder-active {
|
||||||
|
border-top: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All rows become draggable */
|
||||||
|
tr.reorder-row {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.15s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.reorder-row:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row being dragged — ghost placeholder */
|
||||||
|
tr.row-dragging {
|
||||||
|
opacity: 0.35;
|
||||||
|
background-color: #fef9c3 !important;
|
||||||
|
outline: 2px dashed #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row that is the current drop target */
|
||||||
|
tr.row-drag-over {
|
||||||
|
background-color: #dbeafe !important;
|
||||||
|
border-top: 3px solid #3b82f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag-handle cell */
|
||||||
|
.drag-handle-cell {
|
||||||
|
width: 28px;
|
||||||
|
padding: 0 6px !important;
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: grab;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-icon:hover {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position-number cell */
|
||||||
|
.order-number-cell {
|
||||||
|
width: 36px;
|
||||||
|
padding: 0 8px !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-badge {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 24px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dim action buttons while in reorder mode */
|
||||||
|
.actions-disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display order indicator in the view modal badges row */
|
||||||
|
.order-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* Professor Avatar */
|
/* Professor Avatar */
|
||||||
.professor-avatar {
|
.professor-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@ -286,40 +484,13 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-faculty {
|
.badge-faculty { background: #dbeafe; color: #1e40af; }
|
||||||
background: #dbeafe;
|
.badge-support { background: #e0e7ff; color: #3730a3; }
|
||||||
color: #1e40af;
|
.badge-trainee { background: #fef3c7; color: #92400e; }
|
||||||
}
|
.badge-resigned { background: #f3f4f6; color: #374151; }
|
||||||
|
.badge-guides { background: #dbeafe; color: #1e3a8a; }
|
||||||
.badge-support {
|
.badge-friends { background: #dcfce7; color: #14532d; }
|
||||||
background: #e0e7ff;
|
.badge-patrons { background: #fae8ff; color: #6b21a8; }
|
||||||
color: #3730a3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-trainee {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-resigned {
|
|
||||||
background: #f3f4f6;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-guides {
|
|
||||||
background: #dbeafe;
|
|
||||||
color: #1e3a8a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-friends {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #14532d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-patrons {
|
|
||||||
background: #fae8ff;
|
|
||||||
color: #6b21a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status Badge */
|
/* Status Badge */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@ -332,20 +503,10 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-active {
|
.status-active { background: #d1fae5; color: #065f46; }
|
||||||
background: #d1fae5;
|
.status-leave { background: #fef3c7; color: #92400e; }
|
||||||
color: #065f46;
|
.status-retired { background: #fee2e2; color: #991b1b; }
|
||||||
}
|
.status-inactive { background: #f3f4f6; color: #6b7280; }
|
||||||
|
|
||||||
.status-leave {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-retired {
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action Buttons */
|
/* Action Buttons */
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@ -422,6 +583,24 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.loading-state {
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-lg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e5e7eb;
|
||||||
|
border-top-color: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
/* Modal Styles */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -454,14 +633,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
from {
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
opacity: 0;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@ -564,6 +737,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-grid {
|
.info-grid {
|
||||||
@ -699,6 +873,25 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background: white;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* File Upload */
|
/* File Upload */
|
||||||
.file-upload-wrapper {
|
.file-upload-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -728,6 +921,11 @@
|
|||||||
background: #f0f9ff;
|
background: #f0f9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-label.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.file-label i {
|
.file-label i {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
@ -738,7 +936,7 @@
|
|||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox */
|
/* Form Section */
|
||||||
.form-section {
|
.form-section {
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@ -746,6 +944,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
.checkbox-wrapper {
|
.checkbox-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -820,100 +1019,6 @@
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.user-content {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.user-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.users-table {
|
|
||||||
min-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header,
|
|
||||||
.modal-body,
|
|
||||||
.modal-footer {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.user-content {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print Styles */
|
|
||||||
@media print {
|
|
||||||
.sidebar,
|
|
||||||
.header-actions,
|
|
||||||
.action-buttons {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-content {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Professor-Specific Styles */
|
|
||||||
|
|
||||||
/* Section Title */
|
/* Section Title */
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -1090,7 +1195,7 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Adjustments for Professor Component */
|
/* Responsive Design */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.professor-content {
|
.professor-content {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -1098,11 +1203,28 @@
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
.professor-header {
|
.professor-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.award-fields {
|
.award-fields {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@ -1117,10 +1239,32 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.professors-table {
|
.professors-table {
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-body,
|
||||||
|
.modal-footer {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.work-days-grid {
|
.work-days-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@ -1132,6 +1276,11 @@
|
|||||||
.detail-row {
|
.detail-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reorder-hint-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
@ -1139,6 +1288,15 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions > * {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.professor-detail-header {
|
.professor-detail-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -1149,7 +1307,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Print Styles for Professor */
|
/* Print Styles */
|
||||||
@media print {
|
@media print {
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.header-actions,
|
.header-actions,
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="professor-content">
|
<div class="professor-content">
|
||||||
|
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="professor-header">
|
<div class="professor-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
@ -11,17 +12,49 @@
|
|||||||
<p class="page-subtitle">Manage faculty, support team, and trainee/fellow profiles</p>
|
<p class="page-subtitle">Manage faculty, support team, and trainee/fellow profiles</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="search-box">
|
<!-- Search (hidden in reorder mode so it doesn't confuse filtered lists) -->
|
||||||
|
<div class="search-box" *ngIf="!reorderMode">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
<input name="searchTerm" #searchTerm="ngModel" class="search-input" type="search"
|
<input name="searchTerm" #searchTerm="ngModel" class="search-input" type="search"
|
||||||
placeholder="Search professors..." ngModel (ngModelChange)="searchProfessors(searchTerm.value)">
|
placeholder="Search professors..." ngModel (ngModelChange)="searchProfessors(searchTerm.value)">
|
||||||
</div>
|
</div>
|
||||||
<button *ngIf="isManager" class="btn-primary" data-bs-toggle="modal" data-bs-target="#addProfessorModal">
|
|
||||||
|
<!-- Reorder mode: instructional badge -->
|
||||||
|
<span *ngIf="reorderMode" class="reorder-hint-badge">
|
||||||
|
<i class="fas fa-grip-lines"></i>
|
||||||
|
Drag rows to reorder — unsaved until you click Save
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Add professor button (hidden in reorder mode) -->
|
||||||
|
<button *ngIf="isManager && !reorderMode" class="btn-primary" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addProfessorModal">
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fa fa-plus"></i>
|
||||||
<span>New Professor</span>
|
<span>New Professor</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-refresh" (click)="getProfessors(true)">
|
|
||||||
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing }"></i>
|
<!-- Reorder toggle button (visible when NOT in reorder mode) -->
|
||||||
|
<button *ngIf="isManager && !reorderMode" class="btn-reorder" (click)="toggleReorderMode()"
|
||||||
|
title="Switch to drag-and-drop reorder mode">
|
||||||
|
<i class="fas fa-sort"></i>
|
||||||
|
<span>Reorder</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Save order button (visible in reorder mode) -->
|
||||||
|
<button *ngIf="reorderMode" class="btn-save-order" [disabled]="savingOrder" (click)="saveDisplayOrder()">
|
||||||
|
<span *ngIf="savingOrder" class="spinner-sm"></span>
|
||||||
|
<i *ngIf="!savingOrder" class="fas fa-save"></i>
|
||||||
|
<span>{{ savingOrder ? 'Saving…' : 'Save Order' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Cancel reorder button -->
|
||||||
|
<button *ngIf="reorderMode" class="btn-cancel-reorder" [disabled]="savingOrder" (click)="cancelReorder()">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
<span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Refresh (hidden in reorder mode) -->
|
||||||
|
<button *ngIf="!reorderMode" class="btn-refresh" (click)="getProfessors(true)">
|
||||||
|
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing}"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -29,43 +62,80 @@
|
|||||||
<!-- Professors Table -->
|
<!-- Professors Table -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="professors-table">
|
<table class="professors-table" [class.reorder-active]="reorderMode">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<!-- Drag handle + position columns appear only in reorder mode -->
|
||||||
|
<th *ngIf="reorderMode" class="th-handle"></th>
|
||||||
|
<th *ngIf="reorderMode" class="th-order">#</th>
|
||||||
|
|
||||||
<th>Photo</th>
|
<th>Photo</th>
|
||||||
<th>Professor ID</th>
|
<th *ngIf="!reorderMode">Professor ID</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th *ngIf="!reorderMode">Email</th>
|
||||||
<th>Department</th>
|
<th>Department</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let professor of professors">
|
<tr *ngFor="let professor of professors; let i = index"
|
||||||
<td (click)="onSelectProfessor(professor)">
|
[attr.draggable]="reorderMode ? 'true' : null"
|
||||||
|
[class.reorder-row]="reorderMode"
|
||||||
|
[class.row-dragging]="dragIndex === i"
|
||||||
|
[class.row-drag-over]="dragOverIndex === i && dragIndex !== i"
|
||||||
|
(dragstart)="reorderMode && onDragStart(i)"
|
||||||
|
(dragover)="reorderMode && onDragOver($event, i)"
|
||||||
|
(dragleave)="reorderMode && onDragLeave()"
|
||||||
|
(drop)="reorderMode && onDrop($event, i)"
|
||||||
|
(dragend)="reorderMode && onDragEnd()">
|
||||||
|
|
||||||
|
<!-- Drag handle cell -->
|
||||||
|
<td *ngIf="reorderMode" class="drag-handle-cell">
|
||||||
|
<i class="fas fa-grip-vertical drag-handle-icon"></i>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Position badge -->
|
||||||
|
<td *ngIf="reorderMode" class="order-number-cell">
|
||||||
|
<span class="order-badge">{{ i + 1 }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Photo -->
|
||||||
|
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||||
<div class="professor-avatar">
|
<div class="professor-avatar">
|
||||||
<img [src]="professor?.profileImageUrl" [alt]="professor?.firstName">
|
<img [src]="professor?.profileImageUrl" [alt]="professor?.firstName">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
|
<!-- Professor ID (hidden in reorder mode to save space) -->
|
||||||
|
<td *ngIf="!reorderMode" (click)="onSelectProfessor(professor)">
|
||||||
<span class="professor-id">{{ professor?.professorId }}</span>
|
<span class="professor-id">{{ professor?.professorId }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
|
<!-- Name -->
|
||||||
|
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||||
<div class="professor-name-cell">
|
<div class="professor-name-cell">
|
||||||
<span class="full-name">{{ professor?.firstName }} {{ professor?.lastName }}</span>
|
<span class="full-name">{{ professor?.firstName }} {{ professor?.lastName }}</span>
|
||||||
<span class="specialty-text" *ngIf="professor?.specialty">{{ professor?.specialty }}</span>
|
<span class="specialty-text" *ngIf="professor?.specialty">{{ professor?.specialty }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
|
<!-- Email (hidden in reorder mode to save space) -->
|
||||||
|
<td *ngIf="!reorderMode" (click)="onSelectProfessor(professor)">
|
||||||
<span class="email-text">{{ professor?.email }}</span>
|
<span class="email-text">{{ professor?.email }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
|
<!-- Department -->
|
||||||
|
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||||
<span class="department-text">{{ professor?.department || 'N/A' }}</span>
|
<span class="department-text">{{ professor?.department || 'N/A' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
<span class="category-badge" [class.badge-faculty]="professor?.category === 'FACULTY'"
|
<!-- Category -->
|
||||||
|
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||||
|
<span class="category-badge"
|
||||||
|
[class.badge-faculty]="professor?.category === 'FACULTY'"
|
||||||
[class.badge-support]="professor?.category === 'SUPPORT_TEAM'"
|
[class.badge-support]="professor?.category === 'SUPPORT_TEAM'"
|
||||||
[class.badge-trainee]="professor?.category === 'TRAINEE_FELLOW'"
|
[class.badge-trainee]="professor?.category === 'TRAINEE_FELLOW'"
|
||||||
[class.badge-resigned]="professor?.category === 'RESIGNED'"
|
[class.badge-resigned]="professor?.category === 'RESIGNED'"
|
||||||
@ -75,25 +145,33 @@
|
|||||||
{{ getCategoryDisplayName(professor?.category) }}
|
{{ getCategoryDisplayName(professor?.category) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="onSelectProfessor(professor)">
|
|
||||||
<span class="status-badge" [class.status-active]="professor?.status === WorkingStatus.ACTIVE"
|
<!-- Status -->
|
||||||
|
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||||
|
<span class="status-badge"
|
||||||
|
[class.status-active]="professor?.status === WorkingStatus.ACTIVE"
|
||||||
[class.status-leave]="professor?.status === WorkingStatus.ON_LEAVE"
|
[class.status-leave]="professor?.status === WorkingStatus.ON_LEAVE"
|
||||||
[class.status-retired]="professor?.status === WorkingStatus.RETIRED"
|
[class.status-retired]="professor?.status === WorkingStatus.RETIRED"
|
||||||
[class.status-inactive]="professor?.status === WorkingStatus.INACTIVE">
|
[class.status-inactive]="professor?.status === WorkingStatus.INACTIVE">
|
||||||
<i class="fa" [class.fa-check-circle]="professor?.status === WorkingStatus.ACTIVE"
|
<i class="fa"
|
||||||
|
[class.fa-check-circle]="professor?.status === WorkingStatus.ACTIVE"
|
||||||
[class.fa-pause-circle]="professor?.status === WorkingStatus.ON_LEAVE"
|
[class.fa-pause-circle]="professor?.status === WorkingStatus.ON_LEAVE"
|
||||||
[class.fa-times-circle]="professor?.status === WorkingStatus.RETIRED"
|
[class.fa-times-circle]="professor?.status === WorkingStatus.RETIRED"
|
||||||
[class.fa-ban]="professor?.status === WorkingStatus.INACTIVE"></i>
|
[class.fa-ban]="professor?.status === WorkingStatus.INACTIVE"></i>
|
||||||
{{ professor?.status }}
|
{{ professor?.status }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions (dimmed / disabled during reorder mode) -->
|
||||||
<td>
|
<td>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons" [class.actions-disabled]="reorderMode">
|
||||||
<button class="btn-action btn-edit" (click)="onEditProfessor(professor)" title="Edit">
|
<button class="btn-action btn-edit" (click)="!reorderMode && onEditProfessor(professor)"
|
||||||
|
[disabled]="reorderMode" title="Edit">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="isAdmin" class="btn-action btn-delete" (click)="onDeleteProfessor(professor)"
|
<button *ngIf="isAdmin" class="btn-action btn-delete"
|
||||||
title="Delete">
|
(click)="!reorderMode && onDeleteProfessor(professor)"
|
||||||
|
[disabled]="reorderMode" title="Delete">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -111,6 +189,12 @@
|
|||||||
<h3>No professors found</h3>
|
<h3>No professors found</h3>
|
||||||
<p>Try adjusting your search or add a new professor</p>
|
<p>Try adjusting your search or add a new professor</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div *ngIf="refreshing" class="loading-state">
|
||||||
|
<div class="spinner-border-lg"></div>
|
||||||
|
<p>Loading professors…</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden Modal Triggers -->
|
<!-- Hidden Modal Triggers -->
|
||||||
@ -121,7 +205,10 @@
|
|||||||
<button [hidden]="true" type="button" id="new-professor-close" data-bs-dismiss="modal"></button>
|
<button [hidden]="true" type="button" id="new-professor-close" data-bs-dismiss="modal"></button>
|
||||||
<button [hidden]="true" type="button" id="closeEditProfessorButton" data-bs-dismiss="modal"></button>
|
<button [hidden]="true" type="button" id="closeEditProfessorButton" data-bs-dismiss="modal"></button>
|
||||||
|
|
||||||
<!-- View Professor Modal -->
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
VIEW PROFESSOR MODAL
|
||||||
|
═══════════════════════════════════════════════════════════════════════ -->
|
||||||
<div *ngIf="selectedProfessor" class="modal fade" id="viewProfessorModal" tabindex="-1">
|
<div *ngIf="selectedProfessor" class="modal fade" id="viewProfessorModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -141,7 +228,8 @@
|
|||||||
<h4>{{ selectedProfessor.firstName }} {{ selectedProfessor.lastName }}</h4>
|
<h4>{{ selectedProfessor.firstName }} {{ selectedProfessor.lastName }}</h4>
|
||||||
<p class="email">{{ selectedProfessor.email }}</p>
|
<p class="email">{{ selectedProfessor.email }}</p>
|
||||||
<div class="badges-row">
|
<div class="badges-row">
|
||||||
<span class="category-badge" [class.badge-faculty]="selectedProfessor.category === 'FACULTY'"
|
<span class="category-badge"
|
||||||
|
[class.badge-faculty]="selectedProfessor.category === 'FACULTY'"
|
||||||
[class.badge-support]="selectedProfessor.category === 'SUPPORT_TEAM'"
|
[class.badge-support]="selectedProfessor.category === 'SUPPORT_TEAM'"
|
||||||
[class.badge-trainee]="selectedProfessor.category === 'TRAINEE_FELLOW'"
|
[class.badge-trainee]="selectedProfessor.category === 'TRAINEE_FELLOW'"
|
||||||
[class.badge-resigned]="selectedProfessor.category === 'RESIGNED'"
|
[class.badge-resigned]="selectedProfessor.category === 'RESIGNED'"
|
||||||
@ -150,12 +238,18 @@
|
|||||||
[class.badge-patrons]="selectedProfessor.category === 'PATRONS'">
|
[class.badge-patrons]="selectedProfessor.category === 'PATRONS'">
|
||||||
{{ getCategoryDisplayName(selectedProfessor.category) }}
|
{{ getCategoryDisplayName(selectedProfessor.category) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="status-badge" [class.status-active]="selectedProfessor.status === 'ACTIVE'"
|
<span class="status-badge"
|
||||||
|
[class.status-active]="selectedProfessor.status === 'ACTIVE'"
|
||||||
[class.status-leave]="selectedProfessor.status === 'ON_LEAVE'"
|
[class.status-leave]="selectedProfessor.status === 'ON_LEAVE'"
|
||||||
[class.status-retired]="selectedProfessor.status === 'RETIRED'"
|
[class.status-retired]="selectedProfessor.status === 'RETIRED'"
|
||||||
[class.status-inactive]="selectedProfessor.status === 'INACTIVE'">
|
[class.status-inactive]="selectedProfessor.status === 'INACTIVE'">
|
||||||
{{ selectedProfessor.status }}
|
{{ selectedProfessor.status }}
|
||||||
</span>
|
</span>
|
||||||
|
<!-- Display order indicator -->
|
||||||
|
<span class="order-indicator" *ngIf="selectedProfessor.displayOrder !== undefined">
|
||||||
|
<i class="fas fa-sort-numeric-down"></i>
|
||||||
|
Position {{ (selectedProfessor.displayOrder ?? 0) + 1 }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item" *ngIf="selectedProfessor.phone">
|
<div class="info-item" *ngIf="selectedProfessor.phone">
|
||||||
@ -220,15 +314,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-section" *ngIf="selectedProfessor.workDays && selectedProfessor.workDays.length > 0">
|
<div class="detail-section"
|
||||||
|
*ngIf="selectedProfessor.workDays && selectedProfessor.workDays.length > 0">
|
||||||
<h5>Work Days</h5>
|
<h5>Work Days</h5>
|
||||||
<div class="work-days-list">
|
<div class="work-days-list">
|
||||||
<span class="day-badge" *ngFor="let day of selectedProfessor.workDays">{{ day }}</span>
|
<span class="day-badge" *ngFor="let day of selectedProfessor.workDays">{{ day }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-section" *ngIf="selectedProfessor.awards && selectedProfessor.awards.length > 0">
|
<div class="detail-section"
|
||||||
<h5>Awards & Recognition</h5>
|
*ngIf="selectedProfessor.awards && selectedProfessor.awards.length > 0">
|
||||||
|
<h5>Awards & Recognition</h5>
|
||||||
<div class="awards-grid">
|
<div class="awards-grid">
|
||||||
<div class="award-card" *ngFor="let award of selectedProfessor.awards">
|
<div class="award-card" *ngFor="let award of selectedProfessor.awards">
|
||||||
<div class="award-header">
|
<div class="award-header">
|
||||||
@ -249,7 +345,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Professor Modal -->
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
ADD PROFESSOR MODAL
|
||||||
|
═══════════════════════════════════════════════════════════════════════ -->
|
||||||
<div *ngIf="isManager" class="modal fade" id="addProfessorModal" tabindex="-1">
|
<div *ngIf="isManager" class="modal fade" id="addProfessorModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -262,25 +361,18 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form #newProfessorForm="ngForm" (ngSubmit)="onAddNewProfessor(newProfessorForm)">
|
<form #newProfessorForm="ngForm" (ngSubmit)="onAddNewProfessor(newProfessorForm)">
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
<!-- Basic Information -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Basic Information</h4>
|
<h4 class="section-title">Basic Information</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="firstName">
|
<label for="firstName"><i class="fa fa-user"></i> First Name *</label>
|
||||||
<i class="fa fa-user"></i>
|
|
||||||
First Name *
|
|
||||||
</label>
|
|
||||||
<input type="text" id="firstName" name="firstName" class="form-input" ngModel required
|
<input type="text" id="firstName" name="firstName" class="form-input" ngModel required
|
||||||
placeholder="Enter first name">
|
placeholder="Enter first name">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="lastName">
|
<label for="lastName"><i class="fa fa-user"></i> Last Name *</label>
|
||||||
<i class="fa fa-user"></i>
|
|
||||||
Last Name *
|
|
||||||
</label>
|
|
||||||
<input type="text" id="lastName" name="lastName" class="form-input" ngModel required
|
<input type="text" id="lastName" name="lastName" class="form-input" ngModel required
|
||||||
placeholder="Enter last name">
|
placeholder="Enter last name">
|
||||||
</div>
|
</div>
|
||||||
@ -288,29 +380,20 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">
|
<label for="email"><i class="fa fa-envelope"></i> Email *</label>
|
||||||
<i class="fa fa-envelope"></i>
|
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input type="email" id="email" name="email" class="form-input" ngModel required
|
<input type="email" id="email" name="email" class="form-input" ngModel required
|
||||||
placeholder="professor@example.com">
|
placeholder="professor@example.com">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="phone">
|
<label for="phone"><i class="fa fa-phone"></i> Phone</label>
|
||||||
<i class="fa fa-phone"></i>
|
<input type="text" id="phone" name="phone" class="form-input" ngModel
|
||||||
Phone
|
placeholder="Contact number">
|
||||||
</label>
|
|
||||||
<input type="text" id="phone" name="phone" class="form-input" ngModel placeholder="Contact number">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="category">
|
<label for="category"><i class="fa fa-tag"></i> Category *</label>
|
||||||
<i class="fa fa-tag"></i>
|
|
||||||
Category *
|
|
||||||
</label>
|
|
||||||
<select id="category" name="category" class="form-select" ngModel required>
|
<select id="category" name="category" class="form-select" ngModel required>
|
||||||
<option value="">Select Category</option>
|
<option value="">Select Category</option>
|
||||||
<option value="FACULTY">Faculty</option>
|
<option value="FACULTY">Faculty</option>
|
||||||
@ -322,12 +405,8 @@
|
|||||||
<option value="PATRONS">Patrons</option>
|
<option value="PATRONS">Patrons</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status">
|
<label for="status"><i class="fa fa-info-circle"></i> Status *</label>
|
||||||
<i class="fa fa-info-circle"></i>
|
|
||||||
Status *
|
|
||||||
</label>
|
|
||||||
<select id="status" name="status" class="form-select" ngModel required>
|
<select id="status" name="status" class="form-select" ngModel required>
|
||||||
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
||||||
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
||||||
@ -335,37 +414,26 @@
|
|||||||
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="experience">
|
<label for="experience"><i class="fa fa-briefcase"></i> Experience</label>
|
||||||
<i class="fa fa-briefcase"></i>
|
|
||||||
Experience
|
|
||||||
</label>
|
|
||||||
<input type="text" id="experience" name="experience" class="form-input" ngModel
|
<input type="text" id="experience" name="experience" class="form-input" ngModel
|
||||||
placeholder="e.g., 10+ years">
|
placeholder="e.g., 10+ years">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Professional Details Section -->
|
<!-- Professional Details -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Professional Details</h4>
|
<h4 class="section-title">Professional Details</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="department">
|
<label for="department"><i class="fa fa-building"></i> Department</label>
|
||||||
<i class="fa fa-building"></i>
|
|
||||||
Department
|
|
||||||
</label>
|
|
||||||
<input type="text" id="department" name="department" class="form-input" ngModel
|
<input type="text" id="department" name="department" class="form-input" ngModel
|
||||||
placeholder="Enter department">
|
placeholder="Enter department">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="position">
|
<label for="position"><i class="fa fa-id-badge"></i> Position</label>
|
||||||
<i class="fa fa-id-badge"></i>
|
|
||||||
Position
|
|
||||||
</label>
|
|
||||||
<input type="text" id="position" name="position" class="form-input" ngModel
|
<input type="text" id="position" name="position" class="form-input" ngModel
|
||||||
placeholder="Enter position">
|
placeholder="Enter position">
|
||||||
</div>
|
</div>
|
||||||
@ -373,81 +441,59 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="designation">
|
<label for="designation"><i class="fa fa-award"></i> Designation</label>
|
||||||
<i class="fa fa-award"></i>
|
|
||||||
Designation
|
|
||||||
</label>
|
|
||||||
<input type="text" id="designation" name="designation" class="form-input" ngModel
|
<input type="text" id="designation" name="designation" class="form-input" ngModel
|
||||||
placeholder="Professional title">
|
placeholder="Professional title">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="officeLocation">
|
<label for="officeLocation"><i class="fa fa-map-marker-alt"></i> Office Location</label>
|
||||||
<i class="fa fa-map-marker-alt"></i>
|
|
||||||
Office Location
|
|
||||||
</label>
|
|
||||||
<input type="text" id="officeLocation" name="officeLocation" class="form-input" ngModel
|
<input type="text" id="officeLocation" name="officeLocation" class="form-input" ngModel
|
||||||
placeholder="Office location">
|
placeholder="Office location">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="specialty">
|
<label for="specialty"><i class="fa fa-star"></i> Specialty</label>
|
||||||
<i class="fa fa-star"></i>
|
|
||||||
Specialty
|
|
||||||
</label>
|
|
||||||
<input type="text" id="specialty" name="specialty" class="form-input" ngModel
|
<input type="text" id="specialty" name="specialty" class="form-input" ngModel
|
||||||
placeholder="Medical specialty or area of expertise">
|
placeholder="Medical specialty or area of expertise">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">
|
<label for="description"><i class="fa fa-align-left"></i> Description</label>
|
||||||
<i class="fa fa-align-left"></i>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea id="description" name="description" class="form-textarea" rows="3" ngModel
|
<textarea id="description" name="description" class="form-textarea" rows="3" ngModel
|
||||||
placeholder="Brief professional description"></textarea>
|
placeholder="Brief professional description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Qualifications Section -->
|
<!-- Qualifications -->
|
||||||
<!-- Qualifications Section -->
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Qualifications</h4>
|
<h4 class="section-title">Qualifications</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="certification">
|
<label for="certification"><i class="fa fa-certificate"></i> Certification</label>
|
||||||
<i class="fa fa-certificate"></i>
|
|
||||||
Certification
|
|
||||||
</label>
|
|
||||||
<textarea id="certification" name="certification" class="form-textarea" rows="2" ngModel
|
<textarea id="certification" name="certification" class="form-textarea" rows="2" ngModel
|
||||||
placeholder="Educational qualifications and certifications"></textarea>
|
placeholder="Educational qualifications and certifications"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="training">
|
<label for="training"><i class="fa fa-graduation-cap"></i> Training & Professional
|
||||||
<i class="fa fa-graduation-cap"></i>
|
Development</label>
|
||||||
Training & Professional Development
|
|
||||||
</label>
|
|
||||||
<textarea id="training" name="training" class="form-textarea" rows="2" ngModel
|
<textarea id="training" name="training" class="form-textarea" rows="2" ngModel
|
||||||
placeholder="Professional training and development"
|
placeholder="Professional training and development"
|
||||||
[disabled]="newProfessorForm.value.status === WorkingStatus.RETIRED"></textarea>
|
[disabled]="newProfessorForm.value.status === WorkingStatus.RETIRED"></textarea>
|
||||||
<small class="form-text text-muted" *ngIf="newProfessorForm.value.status === WorkingStatus.RETIRED">
|
<small class="form-text text-muted"
|
||||||
|
*ngIf="newProfessorForm.value.status === WorkingStatus.RETIRED">
|
||||||
Training field is disabled for retired faculty
|
Training field is disabled for retired faculty
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Work Days Section -->
|
<!-- Work Schedule -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Work Schedule</h4>
|
<h4 class="section-title">Work Schedule</h4>
|
||||||
|
|
||||||
<label class="work-days-label">
|
<label class="work-days-label"><i class="fa fa-calendar"></i> Work Days</label>
|
||||||
<i class="fa fa-calendar"></i>
|
|
||||||
Work Days
|
|
||||||
</label>
|
|
||||||
<div class="work-days-grid">
|
<div class="work-days-grid">
|
||||||
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
||||||
<input type="checkbox" [id]="'day-' + day" [value]="day" [(ngModel)]="selectedWorkDays[day]"
|
<input type="checkbox" [id]="'day-' + day" [value]="day" [(ngModel)]="selectedWorkDays[day]"
|
||||||
@ -460,10 +506,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Awards Section -->
|
<!-- Skills Section -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Awards & Recognition</h4>
|
<h4 class="section-title">Skills</h4>
|
||||||
|
<div class="awards-manager">
|
||||||
|
<div class="award-item" *ngFor="let skill of newProfessorSkills; let i = index">
|
||||||
|
<div class="award-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-input" [(ngModel)]="skill.name" [name]="'skillName' + i"
|
||||||
|
placeholder="Skill name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="number" class="form-input" [(ngModel)]="skill.level" [name]="'skillLevel' + i"
|
||||||
|
placeholder="Level (0–100)" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-remove-award" (click)="removeSkill(i)" title="Remove">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-add-award" (click)="addNewSkill()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span>Add Skill</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Awards & Recognition -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h4 class="section-title">Awards & Recognition</h4>
|
||||||
<div class="awards-manager">
|
<div class="awards-manager">
|
||||||
<div class="award-item" *ngFor="let award of newProfessorAwards; let i = index">
|
<div class="award-item" *ngFor="let award of newProfessorAwards; let i = index">
|
||||||
<div class="award-fields">
|
<div class="award-fields">
|
||||||
@ -491,16 +562,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Profile Picture Section -->
|
<!-- Profile Picture -->
|
||||||
<!-- Profile Picture Section -->
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Profile Picture</h4>
|
<h4 class="section-title">Profile Picture</h4>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label><i class="fa fa-image"></i> Upload Profile Picture</label>
|
||||||
<i class="fa fa-image"></i>
|
|
||||||
Upload Profile Picture
|
|
||||||
</label>
|
|
||||||
<div class="file-upload-wrapper">
|
<div class="file-upload-wrapper">
|
||||||
<input type="file" id="newProfessorProfileImage" accept="image/*" name="profileImage"
|
<input type="file" id="newProfessorProfileImage" accept="image/*" name="profileImage"
|
||||||
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
||||||
@ -508,20 +574,20 @@
|
|||||||
<label for="newProfessorProfileImage" class="file-label"
|
<label for="newProfessorProfileImage" class="file-label"
|
||||||
[class.disabled]="isImageUploadDisabled(newProfessorForm.value.category)">
|
[class.disabled]="isImageUploadDisabled(newProfessorForm.value.category)">
|
||||||
<i class="fa fa-cloud-upload-alt"></i>
|
<i class="fa fa-cloud-upload-alt"></i>
|
||||||
<span>{{ isImageUploadDisabled(newProfessorForm.value.category) ? 'Upload disabled for ' +
|
<span>{{ isImageUploadDisabled(newProfessorForm.value.category)
|
||||||
getCategoryDisplayName(newProfessorForm.value.category) : (profileImageFileName || 'Choose
|
? 'Upload disabled for ' + getCategoryDisplayName(newProfessorForm.value.category)
|
||||||
profile picture') }}</span>
|
: (profileImageFileName || 'Choose profile picture') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted"
|
<small class="form-text text-muted"
|
||||||
*ngIf="isImageUploadDisabled(newProfessorForm.value.category)">
|
*ngIf="isImageUploadDisabled(newProfessorForm.value.category)">
|
||||||
Profile image upload is not available for {{ getCategoryDisplayName(newProfessorForm.value.category)
|
Profile image upload is not available for
|
||||||
}} category
|
{{ getCategoryDisplayName(newProfessorForm.value.category) }} category
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" style="display: none;" id="new-professor-save"></button>
|
<button type="submit" style="display:none;" id="new-professor-save"></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@ -535,7 +601,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit Professor Modal -->
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
EDIT PROFESSOR MODAL
|
||||||
|
═══════════════════════════════════════════════════════════════════════ -->
|
||||||
<div class="modal fade" id="editProfessorModal" tabindex="-1">
|
<div class="modal fade" id="editProfessorModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -548,25 +617,18 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form #editProfessorForm="ngForm" (ngSubmit)="onUpdateProfessor(editProfessorForm)">
|
<form #editProfessorForm="ngForm" (ngSubmit)="onUpdateProfessor(editProfessorForm)">
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
<!-- Basic Information -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Basic Information</h4>
|
<h4 class="section-title">Basic Information</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editFirstName">
|
<label for="editFirstName"><i class="fa fa-user"></i> First Name *</label>
|
||||||
<i class="fa fa-user"></i>
|
|
||||||
First Name *
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editFirstName" name="firstName" class="form-input"
|
<input type="text" id="editFirstName" name="firstName" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.firstName" required>
|
[(ngModel)]="selectedProfessor.firstName" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editLastName">
|
<label for="editLastName"><i class="fa fa-user"></i> Last Name *</label>
|
||||||
<i class="fa fa-user"></i>
|
|
||||||
Last Name *
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editLastName" name="lastName" class="form-input"
|
<input type="text" id="editLastName" name="lastName" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.lastName" required>
|
[(ngModel)]="selectedProfessor.lastName" required>
|
||||||
</div>
|
</div>
|
||||||
@ -574,19 +636,12 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editEmail">
|
<label for="editEmail"><i class="fa fa-envelope"></i> Email *</label>
|
||||||
<i class="fa fa-envelope"></i>
|
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input type="email" id="editEmail" name="email" class="form-input"
|
<input type="email" id="editEmail" name="email" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.email" required>
|
[(ngModel)]="selectedProfessor.email" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editPhone">
|
<label for="editPhone"><i class="fa fa-phone"></i> Phone</label>
|
||||||
<i class="fa fa-phone"></i>
|
|
||||||
Phone
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editPhone" name="phone" class="form-input"
|
<input type="text" id="editPhone" name="phone" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.phone">
|
[(ngModel)]="selectedProfessor.phone">
|
||||||
</div>
|
</div>
|
||||||
@ -594,10 +649,7 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editCategory">
|
<label for="editCategory"><i class="fa fa-tag"></i> Category *</label>
|
||||||
<i class="fa fa-tag"></i>
|
|
||||||
Category *
|
|
||||||
</label>
|
|
||||||
<select id="editCategory" name="category" class="form-select"
|
<select id="editCategory" name="category" class="form-select"
|
||||||
[(ngModel)]="selectedProfessor.category" required>
|
[(ngModel)]="selectedProfessor.category" required>
|
||||||
<option value="FACULTY">Faculty</option>
|
<option value="FACULTY">Faculty</option>
|
||||||
@ -609,51 +661,36 @@
|
|||||||
<option value="PATRONS">Patrons</option>
|
<option value="PATRONS">Patrons</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editStatus">
|
<label for="editStatus"><i class="fa fa-info-circle"></i> Status *</label>
|
||||||
<i class="fa fa-info-circle"></i>
|
<select id="editStatus" name="status" class="form-select"
|
||||||
Status *
|
[(ngModel)]="selectedProfessor.status" required>
|
||||||
</label>
|
|
||||||
<select id="editStatus" name="status" class="form-select" [(ngModel)]="selectedProfessor.status"
|
|
||||||
required>
|
|
||||||
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
||||||
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
||||||
<option [value]="WorkingStatus.RETIRED">Retired</option>
|
<option [value]="WorkingStatus.RETIRED">Retired</option>
|
||||||
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editExperience">
|
<label for="editExperience"><i class="fa fa-briefcase"></i> Experience</label>
|
||||||
<i class="fa fa-briefcase"></i>
|
|
||||||
Experience
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editExperience" name="experience" class="form-input"
|
<input type="text" id="editExperience" name="experience" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.experience">
|
[(ngModel)]="selectedProfessor.experience">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Professional Details Section -->
|
<!-- Professional Details -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Professional Details</h4>
|
<h4 class="section-title">Professional Details</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editDepartment">
|
<label for="editDepartment"><i class="fa fa-building"></i> Department</label>
|
||||||
<i class="fa fa-building"></i>
|
|
||||||
Department
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editDepartment" name="department" class="form-input"
|
<input type="text" id="editDepartment" name="department" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.department">
|
[(ngModel)]="selectedProfessor.department">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editPosition">
|
<label for="editPosition"><i class="fa fa-id-badge"></i> Position</label>
|
||||||
<i class="fa fa-id-badge"></i>
|
|
||||||
Position
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editPosition" name="position" class="form-input"
|
<input type="text" id="editPosition" name="position" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.position">
|
[(ngModel)]="selectedProfessor.position">
|
||||||
</div>
|
</div>
|
||||||
@ -661,84 +698,63 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editDesignation">
|
<label for="editDesignation"><i class="fa fa-award"></i> Designation</label>
|
||||||
<i class="fa fa-award"></i>
|
|
||||||
Designation
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editDesignation" name="designation" class="form-input"
|
<input type="text" id="editDesignation" name="designation" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.designation">
|
[(ngModel)]="selectedProfessor.designation">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editOfficeLocation">
|
<label for="editOfficeLocation"><i class="fa fa-map-marker-alt"></i> Office Location</label>
|
||||||
<i class="fa fa-map-marker-alt"></i>
|
|
||||||
Office Location
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editOfficeLocation" name="officeLocation" class="form-input"
|
<input type="text" id="editOfficeLocation" name="officeLocation" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.officeLocation">
|
[(ngModel)]="selectedProfessor.officeLocation">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editSpecialty">
|
<label for="editSpecialty"><i class="fa fa-star"></i> Specialty</label>
|
||||||
<i class="fa fa-star"></i>
|
|
||||||
Specialty
|
|
||||||
</label>
|
|
||||||
<input type="text" id="editSpecialty" name="specialty" class="form-input"
|
<input type="text" id="editSpecialty" name="specialty" class="form-input"
|
||||||
[(ngModel)]="selectedProfessor.specialty">
|
[(ngModel)]="selectedProfessor.specialty">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editDescription">
|
<label for="editDescription"><i class="fa fa-align-left"></i> Description</label>
|
||||||
<i class="fa fa-align-left"></i>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea id="editDescription" name="description" class="form-textarea" rows="3"
|
<textarea id="editDescription" name="description" class="form-textarea" rows="3"
|
||||||
[(ngModel)]="selectedProfessor.description"></textarea>
|
[(ngModel)]="selectedProfessor.description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Qualifications Section -->
|
<!-- Qualifications -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Qualifications</h4>
|
<h4 class="section-title">Qualifications</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editCertification">
|
<label for="editCertification"><i class="fa fa-certificate"></i> Certification</label>
|
||||||
<i class="fa fa-certificate"></i>
|
|
||||||
Certification
|
|
||||||
</label>
|
|
||||||
<textarea id="editCertification" name="certification" class="form-textarea" rows="2"
|
<textarea id="editCertification" name="certification" class="form-textarea" rows="2"
|
||||||
[(ngModel)]="selectedProfessor.certification"></textarea>
|
[(ngModel)]="selectedProfessor.certification"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editTraining">
|
<label for="editTraining"><i class="fa fa-graduation-cap"></i> Training & Professional
|
||||||
<i class="fa fa-graduation-cap"></i>
|
Development</label>
|
||||||
Training & Professional Development
|
|
||||||
</label>
|
|
||||||
<textarea id="editTraining" name="training" class="form-textarea" rows="2"
|
<textarea id="editTraining" name="training" class="form-textarea" rows="2"
|
||||||
[(ngModel)]="selectedProfessor.training"
|
[(ngModel)]="selectedProfessor.training"
|
||||||
[disabled]="selectedProfessor.status === WorkingStatus.RETIRED || selectedProfessor.status === WorkingStatus.INACTIVE"></textarea>
|
[disabled]="selectedProfessor.status === WorkingStatus.RETIRED || selectedProfessor.status === WorkingStatus.INACTIVE"></textarea>
|
||||||
<small class="form-text text-muted" *ngIf="selectedProfessor.status === WorkingStatus.RETIRED || selectedProfessor.status === WorkingStatus.INACTIVE">
|
<small class="form-text text-muted"
|
||||||
|
*ngIf="selectedProfessor.status === WorkingStatus.RETIRED || selectedProfessor.status === WorkingStatus.INACTIVE">
|
||||||
Training field is disabled for retired/inactive faculty
|
Training field is disabled for retired/inactive faculty
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Work Days Section -->
|
<!-- Work Schedule -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Work Schedule</h4>
|
<h4 class="section-title">Work Schedule</h4>
|
||||||
|
|
||||||
<label class="work-days-label">
|
<label class="work-days-label"><i class="fa fa-calendar"></i> Work Days</label>
|
||||||
<i class="fa fa-calendar"></i>
|
|
||||||
Work Days
|
|
||||||
</label>
|
|
||||||
<div class="work-days-grid">
|
<div class="work-days-grid">
|
||||||
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
||||||
<input type="checkbox" [id]="'edit-day-' + day" [value]="day" [(ngModel)]="selectedWorkDays[day]"
|
<input type="checkbox" [id]="'edit-day-' + day" [value]="day"
|
||||||
name="editWorkDays" class="checkbox-input">
|
[(ngModel)]="selectedWorkDays[day]" name="editWorkDays" class="checkbox-input">
|
||||||
<label [for]="'edit-day-' + day" class="checkbox-label">
|
<label [for]="'edit-day-' + day" class="checkbox-label">
|
||||||
<span class="checkbox-custom"></span>
|
<span class="checkbox-custom"></span>
|
||||||
<span class="checkbox-text">{{ day }}</span>
|
<span class="checkbox-text">{{ day }}</span>
|
||||||
@ -747,26 +763,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Awards Section -->
|
<!-- Skills Section -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Awards & Recognition</h4>
|
<h4 class="section-title">Skills</h4>
|
||||||
|
<div class="awards-manager">
|
||||||
|
<div class="award-item" *ngFor="let skill of selectedProfessorSkills; let i = index">
|
||||||
|
<div class="award-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-input" [(ngModel)]="skill.name"
|
||||||
|
[name]="'editSkillName' + i" placeholder="Skill name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="number" class="form-input" [(ngModel)]="skill.level"
|
||||||
|
[name]="'editSkillLevel' + i" placeholder="Level (0–100)" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-remove-award" (click)="removeEditSkill(i)" title="Remove">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-add-award" (click)="addEditSkill()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span>Add Skill</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Awards & Recognition -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h4 class="section-title">Awards & Recognition</h4>
|
||||||
<div class="awards-manager">
|
<div class="awards-manager">
|
||||||
<div class="award-item" *ngFor="let award of selectedProfessorAwards; let i = index">
|
<div class="award-item" *ngFor="let award of selectedProfessorAwards; let i = index">
|
||||||
<div class="award-fields">
|
<div class="award-fields">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-input" [(ngModel)]="award.title" [name]="'editAwardTitle' + i"
|
<input type="text" class="form-input" [(ngModel)]="award.title"
|
||||||
placeholder="Award Title">
|
[name]="'editAwardTitle' + i" placeholder="Award Title">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-input" [(ngModel)]="award.year" [name]="'editAwardYear' + i"
|
<input type="text" class="form-input" [(ngModel)]="award.year"
|
||||||
placeholder="Year">
|
[name]="'editAwardYear' + i" placeholder="Year">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-group-wide">
|
<div class="form-group form-group-wide">
|
||||||
<textarea class="form-textarea" rows="2" [(ngModel)]="award.description"
|
<textarea class="form-textarea" rows="2" [(ngModel)]="award.description"
|
||||||
[name]="'editAwardDesc' + i" placeholder="Description"></textarea>
|
[name]="'editAwardDesc' + i" placeholder="Description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-remove-award" (click)="removeEditAward(i)" title="Remove">
|
<button type="button" class="btn-remove-award" (click)="removeEditAward(i)"
|
||||||
|
title="Remove">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -778,16 +820,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Profile Picture Section -->
|
<!-- Profile Picture -->
|
||||||
<!-- Profile Picture Section -->
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">Profile Picture</h4>
|
<h4 class="section-title">Profile Picture</h4>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label><i class="fa fa-image"></i> Upload Profile Picture</label>
|
||||||
<i class="fa fa-image"></i>
|
|
||||||
Upload Profile Picture
|
|
||||||
</label>
|
|
||||||
<div class="file-upload-wrapper">
|
<div class="file-upload-wrapper">
|
||||||
<input type="file" id="editProfessorProfileImage" accept="image/*" name="profileImage"
|
<input type="file" id="editProfessorProfileImage" accept="image/*" name="profileImage"
|
||||||
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
||||||
@ -795,14 +832,15 @@
|
|||||||
<label for="editProfessorProfileImage" class="file-label"
|
<label for="editProfessorProfileImage" class="file-label"
|
||||||
[class.disabled]="!isManager || isImageUploadDisabled(selectedProfessor.category)">
|
[class.disabled]="!isManager || isImageUploadDisabled(selectedProfessor.category)">
|
||||||
<i class="fa fa-cloud-upload-alt"></i>
|
<i class="fa fa-cloud-upload-alt"></i>
|
||||||
<span>{{ isImageUploadDisabled(selectedProfessor.category) ? 'Upload disabled for ' + getCategoryDisplayName(selectedProfessor.category) :
|
<span>{{ isImageUploadDisabled(selectedProfessor.category)
|
||||||
(profileImageFileName || 'Choose profile picture') }}</span>
|
? 'Upload disabled for ' + getCategoryDisplayName(selectedProfessor.category)
|
||||||
|
: (profileImageFileName || 'Choose profile picture') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted"
|
<small class="form-text text-muted"
|
||||||
*ngIf="isImageUploadDisabled(selectedProfessor.category)">
|
*ngIf="isImageUploadDisabled(selectedProfessor.category)">
|
||||||
Profile image upload is not available for {{ getCategoryDisplayName(selectedProfessor.category) }}
|
Profile image upload is not available for
|
||||||
category
|
{{ getCategoryDisplayName(selectedProfessor.category) }} category
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
public refreshing: boolean;
|
public refreshing: boolean;
|
||||||
private subs = new SubSink();
|
private subs = new SubSink();
|
||||||
|
|
||||||
|
// ── Drag-and-drop reordering state ────────────────────────────────────────
|
||||||
|
/** Whether the user has switched to "reorder mode" */
|
||||||
|
public reorderMode: boolean = false;
|
||||||
|
/** Index of the row currently being dragged */
|
||||||
|
public dragIndex: number | null = null;
|
||||||
|
/** Visual index of the row the drag is over */
|
||||||
|
public dragOverIndex: number | null = null;
|
||||||
|
/** True while saving the new order to the backend */
|
||||||
|
public savingOrder: boolean = false;
|
||||||
|
|
||||||
selectedProfessor: Professor = {
|
selectedProfessor: Professor = {
|
||||||
professorId: '',
|
professorId: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@ -77,26 +87,20 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
public profileImage: File | null;
|
public profileImage: File | null;
|
||||||
public fileUploadStatus: FileUploadStatus = new FileUploadStatus();
|
public fileUploadStatus: FileUploadStatus = new FileUploadStatus();
|
||||||
|
|
||||||
// Additional properties for extended functionality
|
|
||||||
public availableDays: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
public availableDays: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
public selectedWorkDays: { [key: string]: boolean } = {};
|
public selectedWorkDays: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
// ✅ Awards management
|
|
||||||
public newProfessorAwards: Award[] = [];
|
public newProfessorAwards: Award[] = [];
|
||||||
public selectedProfessorAwards: Award[] = [];
|
public selectedProfessorAwards: Award[] = [];
|
||||||
|
|
||||||
// ✅ Skills management
|
|
||||||
public newProfessorSkills: Skill[] = [];
|
public newProfessorSkills: Skill[] = [];
|
||||||
public selectedProfessorSkills: Skill[] = [];
|
public selectedProfessorSkills: Skill[] = [];
|
||||||
|
|
||||||
private closeModal(modalId: string): void {
|
private closeModal(modalId: string): void {
|
||||||
const modalElement = document.getElementById(modalId);
|
const modalElement = document.getElementById(modalId);
|
||||||
if (!modalElement) return;
|
if (!modalElement) return;
|
||||||
|
|
||||||
const modalInstance = Modal.getInstance(modalElement) || new Modal(modalElement);
|
const modalInstance = Modal.getInstance(modalElement) || new Modal(modalElement);
|
||||||
modalInstance.hide();
|
modalInstance.hide();
|
||||||
|
|
||||||
// Force-remove leftover background overlays if any remain
|
|
||||||
document.body.classList.remove('modal-open');
|
document.body.classList.remove('modal-open');
|
||||||
document.querySelectorAll('.modal-backdrop').forEach(b => b.remove());
|
document.querySelectorAll('.modal-backdrop').forEach(b => b.remove());
|
||||||
}
|
}
|
||||||
@ -119,7 +123,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.unsubscribe();
|
this.subs.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX: Renamed and now initializes both awards and skills
|
// ── Collection helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
private initializeCollections(): void {
|
private initializeCollections(): void {
|
||||||
this.newProfessorAwards = [];
|
this.newProfessorAwards = [];
|
||||||
this.selectedProfessorAwards = [];
|
this.selectedProfessorAwards = [];
|
||||||
@ -130,16 +135,11 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
private setupModalEventListeners(): void {
|
private setupModalEventListeners(): void {
|
||||||
const editModal = document.getElementById('editProfessorModal');
|
const editModal = document.getElementById('editProfessorModal');
|
||||||
if (editModal) {
|
if (editModal) {
|
||||||
editModal.addEventListener('hidden.bs.modal', () => {
|
editModal.addEventListener('hidden.bs.modal', () => this.clearEditProfessorData());
|
||||||
this.clearEditProfessorData();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addModal = document.getElementById('addProfessorModal');
|
const addModal = document.getElementById('addProfessorModal');
|
||||||
if (addModal) {
|
if (addModal) {
|
||||||
addModal.addEventListener('hidden.bs.modal', () => {
|
addModal.addEventListener('hidden.bs.modal', () => this.clearNewProfessorData());
|
||||||
this.clearNewProfessorData();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +148,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.profileImageFileName = null;
|
this.profileImageFileName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX: Now clears skills too
|
|
||||||
private clearNewProfessorData(): void {
|
private clearNewProfessorData(): void {
|
||||||
this.profileImage = null;
|
this.profileImage = null;
|
||||||
this.profileImageFileName = null;
|
this.profileImageFileName = null;
|
||||||
@ -157,7 +156,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.newProfessorSkills = [];
|
this.newProfessorSkills = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX: Now clears skills too
|
|
||||||
private clearEditProfessorData(): void {
|
private clearEditProfessorData(): void {
|
||||||
this.profileImage = null;
|
this.profileImage = null;
|
||||||
this.profileImageFileName = null;
|
this.profileImageFileName = null;
|
||||||
@ -166,76 +164,124 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.selectedProfessorSkills = [];
|
this.selectedProfessorSkills = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Award management methods ────────────────────────────────────────────────
|
// ── Award helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public addNewAward(): void {
|
public addNewAward(): void { this.newProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' }); }
|
||||||
this.newProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' });
|
public removeAward(index: number): void { this.newProfessorAwards.splice(index, 1); }
|
||||||
}
|
public addEditAward(): void { this.selectedProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' }); }
|
||||||
|
public removeEditAward(index: number): void { this.selectedProfessorAwards.splice(index, 1); }
|
||||||
|
|
||||||
public removeAward(index: number): void {
|
// ── Skill helpers ──────────────────────────────────────────────────────────
|
||||||
this.newProfessorAwards.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addEditAward(): void {
|
public addNewSkill(): void { this.newProfessorSkills.push({ name: '', level: 0 }); }
|
||||||
this.selectedProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' });
|
public removeSkill(index: number): void { this.newProfessorSkills.splice(index, 1); }
|
||||||
}
|
public addEditSkill(): void { this.selectedProfessorSkills.push({ name: '', level: 0 }); }
|
||||||
|
public removeEditSkill(index: number): void { this.selectedProfessorSkills.splice(index, 1); }
|
||||||
|
|
||||||
public removeEditAward(index: number): void {
|
// ── Category helper ────────────────────────────────────────────────────────
|
||||||
this.selectedProfessorAwards.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Skill management methods ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// ✅ NEW: Add skill for new professor form
|
|
||||||
public addNewSkill(): void {
|
|
||||||
this.newProfessorSkills.push({ name: '', level: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ NEW: Remove skill from new professor form
|
|
||||||
public removeSkill(index: number): void {
|
|
||||||
this.newProfessorSkills.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ NEW: Add skill for edit professor form
|
|
||||||
public addEditSkill(): void {
|
|
||||||
this.selectedProfessorSkills.push({ name: '', level: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ NEW: Remove skill from edit professor form
|
|
||||||
public removeEditSkill(index: number): void {
|
|
||||||
this.selectedProfessorSkills.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Category display helper ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public getCategoryDisplayName(category: string): string {
|
public getCategoryDisplayName(category: string): string {
|
||||||
switch (category) {
|
const map: Record<string, string> = {
|
||||||
case 'FACULTY': return 'Faculty';
|
FACULTY: 'Faculty',
|
||||||
case 'SUPPORT_TEAM': return 'Support Team';
|
SUPPORT_TEAM: 'Support Team',
|
||||||
case 'TRAINEE_FELLOW': return 'Trainee/Fellow';
|
TRAINEE_FELLOW: 'Trainee/Fellow',
|
||||||
case 'RESIGNED': return 'Resigned';
|
RESIGNED: 'Resigned',
|
||||||
case 'GUIDES': return 'Guides';
|
GUIDES: 'Guides',
|
||||||
case 'FRIENDS': return 'Friends';
|
FRIENDS: 'Friends',
|
||||||
case 'PATRONS': return 'Patrons';
|
PATRONS: 'Patrons',
|
||||||
default: return category || 'Unknown';
|
};
|
||||||
}
|
return map[category] ?? category ?? 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Categories that should NOT have a profile image upload */
|
|
||||||
private readonly NO_IMAGE_CATEGORIES = ['TRAINEE_FELLOW', 'SUPPORT_TEAM', 'GUIDES', 'FRIENDS', 'PATRONS'];
|
private readonly NO_IMAGE_CATEGORIES = ['TRAINEE_FELLOW', 'SUPPORT_TEAM', 'GUIDES', 'FRIENDS', 'PATRONS'];
|
||||||
|
|
||||||
public isImageUploadDisabled(category: string): boolean {
|
public isImageUploadDisabled(category: string): boolean {
|
||||||
return this.NO_IMAGE_CATEGORIES.includes(category);
|
return this.NO_IMAGE_CATEGORIES.includes(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTitleChange(title: string): void {
|
// ── Drag-and-drop reordering ───────────────────────────────────────────────
|
||||||
this.titleSubject.next(title);
|
|
||||||
|
/** Toggle between normal view and drag-and-drop reorder mode. */
|
||||||
|
public toggleReorderMode(): void {
|
||||||
|
this.reorderMode = !this.reorderMode;
|
||||||
|
if (!this.reorderMode) {
|
||||||
|
this.dragIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeTitle(title: string): void {
|
public onDragStart(index: number): void {
|
||||||
this.titleSubject.next(title);
|
this.dragIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDragOver(event: DragEvent, index: number): void {
|
||||||
|
event.preventDefault(); // Required to allow drop
|
||||||
|
this.dragOverIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDragLeave(): void {
|
||||||
|
// Keep dragOverIndex so the visual indicator stays while hovering over child elements
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDrop(event: DragEvent, dropIndex: number): void {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.dragIndex === null || this.dragIndex === dropIndex) {
|
||||||
|
this.dragIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder the local array
|
||||||
|
const reordered = [...this.professors];
|
||||||
|
const [moved] = reordered.splice(this.dragIndex, 1);
|
||||||
|
reordered.splice(dropIndex, 0, moved);
|
||||||
|
this.professors = reordered;
|
||||||
|
|
||||||
|
this.dragIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDragEnd(): void {
|
||||||
|
this.dragIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current visual order to the backend.
|
||||||
|
* Sends professor UUIDs in the current array order.
|
||||||
|
*/
|
||||||
|
public saveDisplayOrder(): void {
|
||||||
|
this.savingOrder = true;
|
||||||
|
const orderedIds = this.professors.map(p => p.professorId);
|
||||||
|
|
||||||
|
this.subs.sink = this.professorService.updateDisplayOrder(orderedIds).subscribe(
|
||||||
|
(updatedProfessors: Professor[]) => {
|
||||||
|
this.professors = updatedProfessors;
|
||||||
|
this.professorService.addProfessorsToLocalStorage(this.professors);
|
||||||
|
this.reorderMode = false;
|
||||||
|
this.savingOrder = false;
|
||||||
|
this.notificationService.notify(NotificationType.SUCCESS, 'Display order saved successfully');
|
||||||
|
},
|
||||||
|
(errorResponse: HttpErrorResponse) => {
|
||||||
|
this.sendErrorNotification(errorResponse.error?.message ?? 'Failed to save order');
|
||||||
|
this.savingOrder = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancel reorder: restore the last saved order from localStorage. */
|
||||||
|
public cancelReorder(): void {
|
||||||
|
this.professors = this.professorService.getProfessorsFromLocalStorage();
|
||||||
|
this.reorderMode = false;
|
||||||
|
this.dragIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Core CRUD ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
handleTitleChange(title: string): void { this.titleSubject.next(title); }
|
||||||
|
public changeTitle(title: string): void { this.titleSubject.next(title); }
|
||||||
|
|
||||||
public getProfessors(showNotification: boolean): void {
|
public getProfessors(showNotification: boolean): void {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
this.subs.sink = this.professorService.getAllProfessors().subscribe(
|
this.subs.sink = this.professorService.getAllProfessors().subscribe(
|
||||||
@ -243,7 +289,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.professors = professorsPage.content;
|
this.professors = professorsPage.content;
|
||||||
this.professorService.addProfessorsToLocalStorage(this.professors);
|
this.professorService.addProfessorsToLocalStorage(this.professors);
|
||||||
if (showNotification) {
|
if (showNotification) {
|
||||||
this.notificationService.notify(NotificationType.SUCCESS, `${this.professors.length} professors loaded successfully`);
|
this.notificationService.notify(NotificationType.SUCCESS,
|
||||||
|
`${this.professors.length} professors loaded successfully`);
|
||||||
}
|
}
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
},
|
},
|
||||||
@ -257,23 +304,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
public onSelectProfessor(selectedProfessor: Professor): void {
|
public onSelectProfessor(selectedProfessor: Professor): void {
|
||||||
this.selectedProfessor = JSON.parse(JSON.stringify(selectedProfessor));
|
this.selectedProfessor = JSON.parse(JSON.stringify(selectedProfessor));
|
||||||
|
|
||||||
// ✅ FIX: Load awards
|
|
||||||
this.selectedProfessorAwards = selectedProfessor.awards
|
this.selectedProfessorAwards = selectedProfessor.awards
|
||||||
? JSON.parse(JSON.stringify(selectedProfessor.awards))
|
? JSON.parse(JSON.stringify(selectedProfessor.awards)) : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
// ✅ FIX: Load skills
|
|
||||||
this.selectedProfessorSkills = selectedProfessor.skills
|
this.selectedProfessorSkills = selectedProfessor.skills
|
||||||
? JSON.parse(JSON.stringify(selectedProfessor.skills))
|
? JSON.parse(JSON.stringify(selectedProfessor.skills)) : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
// Set up work days for viewing
|
|
||||||
this.selectedWorkDays = {};
|
this.selectedWorkDays = {};
|
||||||
if (selectedProfessor.workDays && Array.isArray(selectedProfessor.workDays)) {
|
if (selectedProfessor.workDays && Array.isArray(selectedProfessor.workDays)) {
|
||||||
selectedProfessor.workDays.forEach(day => {
|
selectedProfessor.workDays.forEach(day => {
|
||||||
if (this.availableDays.includes(day)) {
|
if (this.availableDays.includes(day)) this.selectedWorkDays[day] = true;
|
||||||
this.selectedWorkDays[day] = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,7 +353,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.closeModal('addProfessorModal');
|
this.closeModal('addProfessorModal');
|
||||||
this.getProfessors(false);
|
this.getProfessors(false);
|
||||||
professorForm.reset();
|
professorForm.reset();
|
||||||
this.notificationService.notify(NotificationType.SUCCESS, `Professor ${professor.firstName} added successfully`);
|
this.notificationService.notify(NotificationType.SUCCESS,
|
||||||
|
`Professor ${professor.firstName} added successfully`);
|
||||||
},
|
},
|
||||||
(errorResponse: HttpErrorResponse) => {
|
(errorResponse: HttpErrorResponse) => {
|
||||||
this.sendErrorNotification(errorResponse.error.message);
|
this.sendErrorNotification(errorResponse.error.message);
|
||||||
@ -321,32 +362,23 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveNewProfessor(): void {
|
public saveNewProfessor(): void { this.clickButton('new-professor-save'); }
|
||||||
this.clickButton('new-professor-save');
|
|
||||||
}
|
|
||||||
|
|
||||||
public searchProfessors(searchTerm: string): void {
|
public searchProfessors(searchTerm: string): void {
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
this.professors = this.professorService.getProfessorsFromLocalStorage();
|
this.professors = this.professorService.getProfessorsFromLocalStorage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchProfessors: Professor[] = [];
|
|
||||||
searchTerm = searchTerm.toLowerCase();
|
searchTerm = searchTerm.toLowerCase();
|
||||||
for (const professor of this.professorService.getProfessorsFromLocalStorage()) {
|
this.professors = this.professorService.getProfessorsFromLocalStorage().filter(p =>
|
||||||
if (
|
p.firstName.toLowerCase().includes(searchTerm) ||
|
||||||
professor.firstName.toLowerCase().includes(searchTerm) ||
|
p.lastName.toLowerCase().includes(searchTerm) ||
|
||||||
professor.lastName.toLowerCase().includes(searchTerm) ||
|
p.email.toLowerCase().includes(searchTerm) ||
|
||||||
professor.email.toLowerCase().includes(searchTerm) ||
|
(p.department && p.department.toLowerCase().includes(searchTerm)) ||
|
||||||
(professor.department && professor.department.toLowerCase().includes(searchTerm)) ||
|
(p.phone && p.phone.toLowerCase().includes(searchTerm)) ||
|
||||||
(professor.phone && professor.phone.toLowerCase().includes(searchTerm)) ||
|
(p.specialty && p.specialty.toLowerCase().includes(searchTerm)) ||
|
||||||
(professor.specialty && professor.specialty.toLowerCase().includes(searchTerm)) ||
|
(p.category && this.getCategoryDisplayName(p.category).toLowerCase().includes(searchTerm))
|
||||||
(professor.category && this.getCategoryDisplayName(professor.category).toLowerCase().includes(searchTerm))
|
);
|
||||||
) {
|
|
||||||
matchProfessors.push(professor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.professors = matchProfessors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clickButton(buttonId: string): void {
|
private clickButton(buttonId: string): void {
|
||||||
@ -356,25 +388,18 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
public onEditProfessor(professor: Professor): void {
|
public onEditProfessor(professor: Professor): void {
|
||||||
this.selectedProfessor = JSON.parse(JSON.stringify(professor));
|
this.selectedProfessor = JSON.parse(JSON.stringify(professor));
|
||||||
|
|
||||||
// Set up work days for editing
|
|
||||||
this.selectedWorkDays = {};
|
this.selectedWorkDays = {};
|
||||||
if (professor.workDays && Array.isArray(professor.workDays)) {
|
if (professor.workDays && Array.isArray(professor.workDays)) {
|
||||||
professor.workDays.forEach(day => {
|
professor.workDays.forEach(day => {
|
||||||
if (this.availableDays.includes(day)) {
|
if (this.availableDays.includes(day)) this.selectedWorkDays[day] = true;
|
||||||
this.selectedWorkDays[day] = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX: Load awards for editing
|
|
||||||
this.selectedProfessorAwards = professor.awards
|
this.selectedProfessorAwards = professor.awards
|
||||||
? JSON.parse(JSON.stringify(professor.awards))
|
? JSON.parse(JSON.stringify(professor.awards)) : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
// ✅ FIX: Load skills for editing
|
|
||||||
this.selectedProfessorSkills = professor.skills
|
this.selectedProfessorSkills = professor.skills
|
||||||
? JSON.parse(JSON.stringify(professor.skills))
|
? JSON.parse(JSON.stringify(professor.skills)) : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
this.clickButton('openProfessorEdit');
|
this.clickButton('openProfessorEdit');
|
||||||
}
|
}
|
||||||
@ -384,15 +409,14 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.');
|
this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = this.createExtendedProfessorFormData(this.selectedProfessor, this.profileImage);
|
const formData = this.createExtendedProfessorFormData(this.selectedProfessor, this.profileImage);
|
||||||
|
|
||||||
this.subs.add(this.professorService.updateProfessor(this.selectedProfessor.professorId, formData).subscribe(
|
this.subs.add(this.professorService.updateProfessor(this.selectedProfessor.professorId, formData).subscribe(
|
||||||
(professor: Professor) => {
|
(professor: Professor) => {
|
||||||
this.closeModal('editProfessorModal');
|
this.closeModal('editProfessorModal');
|
||||||
this.getProfessors(false);
|
this.getProfessors(false);
|
||||||
this.invalidateVariables();
|
this.invalidateVariables();
|
||||||
this.notificationService.notify(NotificationType.SUCCESS, `Professor ${professor.firstName} updated successfully`);
|
this.notificationService.notify(NotificationType.SUCCESS,
|
||||||
|
`Professor ${professor.firstName} updated successfully`);
|
||||||
},
|
},
|
||||||
(errorResponse: HttpErrorResponse) => {
|
(errorResponse: HttpErrorResponse) => {
|
||||||
this.sendErrorNotification(errorResponse.error.message);
|
this.sendErrorNotification(errorResponse.error.message);
|
||||||
@ -403,7 +427,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
private createExtendedProfessorFormData(professor: any, profileImage: File | null): FormData {
|
private createExtendedProfessorFormData(professor: any, profileImage: File | null): FormData {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// ─── Basic fields ────────────────────────────────────────────────────────
|
|
||||||
formData.append('firstName', professor.firstName || '');
|
formData.append('firstName', professor.firstName || '');
|
||||||
formData.append('lastName', professor.lastName || '');
|
formData.append('lastName', professor.lastName || '');
|
||||||
formData.append('email', professor.email || '');
|
formData.append('email', professor.email || '');
|
||||||
@ -413,62 +436,50 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
formData.append('status', professor.status || 'ACTIVE');
|
formData.append('status', professor.status || 'ACTIVE');
|
||||||
formData.append('category', professor.category || 'FACULTY');
|
formData.append('category', professor.category || 'FACULTY');
|
||||||
|
|
||||||
// ✅ FIX: Send joinDate
|
|
||||||
if (professor.joinDate) {
|
if (professor.joinDate) {
|
||||||
formData.append('joinDate', new Date(professor.joinDate).toISOString());
|
formData.append('joinDate', new Date(professor.joinDate).toISOString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Extended fields ─────────────────────────────────────────────────────
|
formData.append('phone', professor.phone || '');
|
||||||
formData.append('phone', professor.phone || '');
|
formData.append('specialty', professor.specialty || '');
|
||||||
formData.append('specialty', professor.specialty || '');
|
formData.append('experience', professor.experience || '');
|
||||||
formData.append('experience', professor.experience || '');
|
formData.append('designation', professor.designation || professor.position || '');
|
||||||
formData.append('designation', professor.designation || professor.position || '');
|
formData.append('description', professor.description || '');
|
||||||
formData.append('description', professor.description || '');
|
formData.append('certification', professor.certification || '');
|
||||||
formData.append('certification', professor.certification || '');
|
|
||||||
|
|
||||||
// Only include training if NOT retired or inactive
|
|
||||||
formData.append('training',
|
formData.append('training',
|
||||||
(professor.status !== WorkingStatus.RETIRED && professor.status !== WorkingStatus.INACTIVE)
|
(professor.status !== WorkingStatus.RETIRED && professor.status !== WorkingStatus.INACTIVE)
|
||||||
? (professor.training || '') : ''
|
? (professor.training || '') : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Work days ───────────────────────────────────────────────────────────
|
|
||||||
const workDays = Object.keys(this.selectedWorkDays).filter(day => this.selectedWorkDays[day]);
|
const workDays = Object.keys(this.selectedWorkDays).filter(day => this.selectedWorkDays[day]);
|
||||||
workDays.forEach(day => formData.append('workDays', day));
|
workDays.forEach(day => formData.append('workDays', day));
|
||||||
|
|
||||||
// ─── Awards ──────────────────────────────────────────────────────────────
|
|
||||||
const awardsToSubmit = professor.professorId ? this.selectedProfessorAwards : this.newProfessorAwards;
|
const awardsToSubmit = professor.professorId ? this.selectedProfessorAwards : this.newProfessorAwards;
|
||||||
if (awardsToSubmit && awardsToSubmit.length > 0) {
|
if (awardsToSubmit?.length) {
|
||||||
const validAwards = awardsToSubmit.filter(award =>
|
awardsToSubmit
|
||||||
award.title && award.title.trim() && award.year && award.year.trim()
|
.filter(a => a.title?.trim() && a.year?.trim())
|
||||||
);
|
.forEach((award, i) => {
|
||||||
validAwards.forEach((award, index) => {
|
formData.append(`awards[${i}].title`, award.title.trim());
|
||||||
formData.append(`awards[${index}].title`, award.title.trim());
|
formData.append(`awards[${i}].year`, award.year.trim());
|
||||||
formData.append(`awards[${index}].year`, award.year.trim());
|
formData.append(`awards[${i}].description`, award.description || '');
|
||||||
formData.append(`awards[${index}].description`, award.description || '');
|
formData.append(`awards[${i}].imageUrl`, award.imageUrl || '');
|
||||||
formData.append(`awards[${index}].imageUrl`, award.imageUrl || '');
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX: Skills are now sent to backend
|
|
||||||
const skillsToSubmit = professor.professorId ? this.selectedProfessorSkills : this.newProfessorSkills;
|
const skillsToSubmit = professor.professorId ? this.selectedProfessorSkills : this.newProfessorSkills;
|
||||||
if (skillsToSubmit && skillsToSubmit.length > 0) {
|
if (skillsToSubmit?.length) {
|
||||||
const validSkills = skillsToSubmit.filter(skill => skill.name && skill.name.trim());
|
skillsToSubmit
|
||||||
validSkills.forEach((skill, index) => {
|
.filter(s => s.name?.trim())
|
||||||
formData.append(`skills[${index}].name`, skill.name.trim());
|
.forEach((skill, i) => {
|
||||||
formData.append(`skills[${index}].level`, String(skill.level ?? 0));
|
formData.append(`skills[${i}].name`, skill.name.trim());
|
||||||
});
|
formData.append(`skills[${i}].level`, String(skill.level ?? 0));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Profile image ───────────────────────────────────────────────────────
|
|
||||||
if (profileImage && !this.isImageUploadDisabled(professor.category)) {
|
if (profileImage && !this.isImageUploadDisabled(professor.category)) {
|
||||||
formData.append('profileImage', profileImage);
|
formData.append('profileImage', profileImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug log
|
|
||||||
console.log('FormData contents:');
|
|
||||||
formData.forEach((value, key) => console.log(`${key}:`, value));
|
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,20 +498,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateProfileImage(): void {
|
public updateProfileImage(): void { this.clickButton('profile-image-input'); }
|
||||||
this.clickButton('profile-image-input');
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUpdateProfileImage(): void {
|
public onUpdateProfileImage(): void {
|
||||||
if (!this.profileImage) return;
|
if (!this.profileImage) return;
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('profileImage', this.profileImage);
|
formData.append('profileImage', this.profileImage);
|
||||||
let professor = this.professorService.getSelectedProfessor();
|
const professor = this.professorService.getSelectedProfessor();
|
||||||
this.subs.sink = this.professorService.updateProfileImage(professor.professorId, formData).subscribe(
|
this.subs.sink = this.professorService.updateProfileImage(professor.professorId, formData).subscribe(
|
||||||
(event: HttpEvent<any>) => {
|
(event: HttpEvent<any>) => { this.reportUploadProgress(event); },
|
||||||
this.reportUploadProgress(event);
|
|
||||||
},
|
|
||||||
(errorResponse: HttpErrorResponse) => {
|
(errorResponse: HttpErrorResponse) => {
|
||||||
this.sendErrorNotification(errorResponse.error.message);
|
this.sendErrorNotification(errorResponse.error.message);
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
@ -522,9 +529,11 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
case HttpEventType.Response:
|
case HttpEventType.Response:
|
||||||
if (event.status === 200) {
|
if (event.status === 200) {
|
||||||
if (this.loggedInProfessor) {
|
if (this.loggedInProfessor) {
|
||||||
this.loggedInProfessor.profileImageUrl = `${event.body.profileImageUrl}?time=${new Date().getTime()}`;
|
this.loggedInProfessor.profileImageUrl =
|
||||||
|
`${event.body.profileImageUrl}?time=${new Date().getTime()}`;
|
||||||
}
|
}
|
||||||
this.notificationService.notify(NotificationType.SUCCESS, `${event.body.firstName}'s image updated successfully`);
|
this.notificationService.notify(NotificationType.SUCCESS,
|
||||||
|
`${event.body.firstName}'s image updated successfully`);
|
||||||
this.fileUploadStatus.status = 'done';
|
this.fileUploadStatus.status = 'done';
|
||||||
} else {
|
} else {
|
||||||
this.sendErrorNotification('Unable to upload image. Please try again');
|
this.sendErrorNotification('Unable to upload image. Please try again');
|
||||||
@ -536,7 +545,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get isAdmin(): boolean {
|
public get isAdmin(): boolean {
|
||||||
return this.loggedInUser && (this.loggedInUser.role === Role.ADMIN || this.loggedInUser.role === Role.SUPER_ADMIN);
|
return this.loggedInUser &&
|
||||||
|
(this.loggedInUser.role === Role.ADMIN || this.loggedInUser.role === Role.SUPER_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isManager(): boolean {
|
public get isManager(): boolean {
|
||||||
@ -569,7 +579,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
this.clearNewProfessorData();
|
this.clearNewProfessorData();
|
||||||
this.clearEditProfessorData();
|
this.clearEditProfessorData();
|
||||||
// ✅ FIX: Also reset skill arrays explicitly
|
|
||||||
this.newProfessorSkills = [];
|
this.newProfessorSkills = [];
|
||||||
this.selectedProfessorSkills = [];
|
this.selectedProfessorSkills = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,13 @@ export interface Professor {
|
|||||||
profileImageUrl: string;
|
profileImageUrl: string;
|
||||||
status: WorkingStatus;
|
status: WorkingStatus;
|
||||||
category: ProfessorCategory;
|
category: ProfessorCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls display order on the public frontend.
|
||||||
|
* Lower = appears first. Set via the admin drag-and-drop reorder UI.
|
||||||
|
*/
|
||||||
|
displayOrder?: number;
|
||||||
|
|
||||||
// Additional fields for Next.js integration
|
// Additional fields for Next.js integration
|
||||||
phone?: string;
|
phone?: string;
|
||||||
specialty?: string;
|
specialty?: string;
|
||||||
@ -25,7 +31,7 @@ export interface Professor {
|
|||||||
description?: string;
|
description?: string;
|
||||||
designation?: string;
|
designation?: string;
|
||||||
workDays?: string[];
|
workDays?: string[];
|
||||||
|
|
||||||
// Awards and skills
|
// Awards and skills
|
||||||
awards?: Award[];
|
awards?: Award[];
|
||||||
skills?: Skill[];
|
skills?: Skill[];
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { environment } from "../../environments/environment";
|
import { environment } from "../../environments/environment";
|
||||||
import { HttpClient, HttpEvent } from "@angular/common/http";
|
import { HttpClient, HttpEvent } from "@angular/common/http";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { Professor } from "../model/Professor"; // Ensure this model is correctly defined
|
import { Professor } from "../model/Professor";
|
||||||
import { CustomHttpResponse } from "../dto/custom-http-response";
|
import { CustomHttpResponse } from "../dto/custom-http-response";
|
||||||
import { map } from "rxjs/operators";
|
import { map } from "rxjs/operators";
|
||||||
|
|
||||||
@ -16,8 +16,7 @@ export class ProfessorService {
|
|||||||
|
|
||||||
private selectedProfessor: Professor;
|
private selectedProfessor: Professor;
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient) {
|
constructor(private httpClient: HttpClient) { }
|
||||||
}
|
|
||||||
|
|
||||||
public getAllProfessors(): Observable<ProfessorPage> {
|
public getAllProfessors(): Observable<ProfessorPage> {
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
@ -41,11 +40,20 @@ export class ProfessorService {
|
|||||||
|
|
||||||
public updateProfileImage(professorId: string, formData: FormData): Observable<HttpEvent<Professor>> {
|
public updateProfileImage(professorId: string, formData: FormData): Observable<HttpEvent<Professor>> {
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.put<Professor>(`${this.host}/user/${professorId}/profile-image`, formData,
|
.put<Professor>(`${this.host}/user/${professorId}/profile-image`, formData, {
|
||||||
{
|
reportProgress: true,
|
||||||
reportProgress: true,
|
observe: 'events'
|
||||||
observe: 'events'
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the new display order to the backend.
|
||||||
|
* @param orderedIds Professor UUIDs in the desired display order (index 0 = first).
|
||||||
|
* @returns The full professor list sorted by the new displayOrder.
|
||||||
|
*/
|
||||||
|
public updateDisplayOrder(orderedIds: string[]): Observable<Professor[]> {
|
||||||
|
return this.httpClient
|
||||||
|
.put<Professor[]>(`${this.host}/professor/order`, orderedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addProfessorsToLocalStorage(professors: Professor[]) {
|
public addProfessorsToLocalStorage(professors: Professor[]) {
|
||||||
@ -53,7 +61,7 @@ export class ProfessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getProfessorsFromLocalStorage(): Professor[] {
|
public getProfessorsFromLocalStorage(): Professor[] {
|
||||||
let professors = this.storage.getItem('professors');
|
const professors = this.storage.getItem('professors');
|
||||||
if (professors) {
|
if (professors) {
|
||||||
return JSON.parse(professors);
|
return JSON.parse(professors);
|
||||||
}
|
}
|
||||||
@ -70,7 +78,6 @@ export class ProfessorService {
|
|||||||
formData.append('position', professor.position);
|
formData.append('position', professor.position);
|
||||||
formData.append('officeLocation', professor.officeLocation);
|
formData.append('officeLocation', professor.officeLocation);
|
||||||
formData.append('status', professor.status);
|
formData.append('status', professor.status);
|
||||||
// formData.append('joinDate', professor.joinDate.toString()); // Convert LocalDateTime to string
|
|
||||||
if (profileImage)
|
if (profileImage)
|
||||||
formData.append('profileImage', profileImage);
|
formData.append('profileImage', profileImage);
|
||||||
|
|
||||||
@ -86,7 +93,7 @@ export class ProfessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public findProfessorById(id: string): Professor | Observable<Professor> {
|
public findProfessorById(id: string): Professor | Observable<Professor> {
|
||||||
let cachedProfessors = this.getProfessorsFromLocalStorage();
|
const cachedProfessors = this.getProfessorsFromLocalStorage();
|
||||||
const foundProfessor = cachedProfessors.find((p) => p.professorId === id);
|
const foundProfessor = cachedProfessors.find((p) => p.professorId === id);
|
||||||
|
|
||||||
if (foundProfessor) return foundProfessor;
|
if (foundProfessor) return foundProfessor;
|
||||||
@ -108,4 +115,4 @@ export interface ProfessorPage {
|
|||||||
numberOfElements: number;
|
numberOfElements: number;
|
||||||
number: number;
|
number: number;
|
||||||
empty: boolean;
|
empty: boolean;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user