Professor reorder
This commit is contained in:
@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
@ -33,19 +34,19 @@ public class ProfessorResource {
|
||||
|
||||
@PostMapping("register")
|
||||
public Professor register(@RequestBody Professor professor) {
|
||||
return professorService.register(professor.getFirstName(), professor.getLastName(), professor.getEmail(), professor.getDepartment(), professor.getPosition());
|
||||
return professorService.register(professor.getFirstName(), professor.getLastName(),
|
||||
professor.getEmail(), professor.getDepartment(), professor.getPosition());
|
||||
}
|
||||
|
||||
@PostMapping("add")
|
||||
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||
log.debug("Professor DTO: {}", professorDto);
|
||||
Professor professor = professorService.addNewProfessor(professorDto);
|
||||
return ResponseEntity.ok(professor);
|
||||
}
|
||||
|
||||
|
||||
@PutMapping("{professorId}")
|
||||
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
|
||||
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
|
||||
log.debug("Professor DTO: {}", professorDto);
|
||||
return professorService.updateProfessor(professorId, professorDto);
|
||||
}
|
||||
@ -77,12 +78,14 @@ public class ProfessorResource {
|
||||
}
|
||||
|
||||
@PutMapping("{professorId}/profile-image")
|
||||
public Professor updateProfileImage(@PathVariable UUID professorId, @RequestParam MultipartFile profileImage) {
|
||||
public Professor updateProfileImage(@PathVariable UUID professorId,
|
||||
@RequestParam MultipartFile profileImage) {
|
||||
return professorService.updateProfileImage(professorId, profileImage);
|
||||
}
|
||||
|
||||
@GetMapping(path = "{professorId}/profile-image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId, @PathVariable String filename) {
|
||||
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId,
|
||||
@PathVariable String filename) {
|
||||
return professorService.getImageByProfessorId(professorId, filename);
|
||||
}
|
||||
|
||||
@ -90,4 +93,24 @@ public class ProfessorResource {
|
||||
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
|
||||
return professorService.getDefaultProfileImage(professorId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-update displayOrder.
|
||||
*
|
||||
* Accepts a JSON array of professor UUIDs in the desired display order.
|
||||
* The array index becomes each professor's displayOrder value.
|
||||
*
|
||||
* Example request body:
|
||||
* ["uuid-A", "uuid-B", "uuid-C"]
|
||||
*
|
||||
* Returns the full professor list sorted by the new displayOrder.
|
||||
*
|
||||
* Requires ADMIN or MANAGER role (configure in your SecurityConfig).
|
||||
*/
|
||||
@PutMapping("order")
|
||||
public ResponseEntity<List<Professor>> updateDisplayOrder(@RequestBody List<UUID> orderedIds) {
|
||||
log.info("Updating display order for {} professors", orderedIds.size());
|
||||
List<Professor> updated = professorService.updateDisplayOrder(orderedIds);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,6 @@ package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.Fetch;
|
||||
import org.hibernate.annotations.FetchMode;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import javax.persistence.*;
|
||||
@ -46,35 +44,40 @@ public class Professor implements Serializable {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private ProfessorCategory category;
|
||||
|
||||
/**
|
||||
* Controls display order within each category section on the public frontend.
|
||||
* Lower values appear first. Defaults to 0 (new professors appear at the top).
|
||||
* Admins can drag-and-drop rows in the management UI to reorder.
|
||||
*/
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
@Builder.Default
|
||||
private Integer displayOrder = 0;
|
||||
|
||||
// Additional fields for Next.js integration
|
||||
private String phone;
|
||||
private String specialty;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String certification;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String training;
|
||||
|
||||
|
||||
private String experience;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
|
||||
private String designation;
|
||||
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
|
||||
@Column(name = "work_day")
|
||||
private List<String> workDays;
|
||||
|
||||
// ✅ CRITICAL FIX: Added orphanRemoval = true
|
||||
// This tells JPA to DELETE skills that are removed from the collection
|
||||
|
||||
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
private Set<ProfessorSkill> skills;
|
||||
|
||||
// ✅ CRITICAL FIX: Added orphanRemoval = true
|
||||
// This tells JPA to DELETE awards that are removed from the collection
|
||||
|
||||
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
private Set<ProfessorAward> awards;
|
||||
|
||||
@ -82,7 +85,6 @@ public class Professor implements Serializable {
|
||||
@JsonIgnore
|
||||
private List<Post> posts;
|
||||
|
||||
// Convenience method to get full name
|
||||
public String getName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@ package net.shyshkin.study.fullstack.supportportal.backend.repository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -25,15 +27,35 @@ public interface ProfessorRepository extends JpaRepository<Professor, Long> {
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
|
||||
Optional<Professor> findByProfessorId(@Param("professorId") UUID professorId);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status")
|
||||
|
||||
// ── Status / category filters, ordered by displayOrder then lastName ────────
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByStatus(@Param("status") WorkingStatus status, Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.category = :category")
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category")
|
||||
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||
@Param("category") ProfessorCategory category,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||
@Param("category") ProfessorCategory category,
|
||||
Pageable pageable);
|
||||
|
||||
// ── Bulk display-order update ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sets displayOrder for a single professor identified by professorId.
|
||||
* Used by the bulk-reorder service method.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Professor p SET p.displayOrder = :displayOrder WHERE p.professorId = :professorId")
|
||||
void updateDisplayOrder(@Param("professorId") UUID professorId,
|
||||
@Param("displayOrder") int displayOrder);
|
||||
|
||||
/**
|
||||
* Returns all professors ordered by displayOrder ASC, then lastName ASC.
|
||||
* Used by the admin reorder endpoint to return the updated list.
|
||||
*/
|
||||
@Query("SELECT p FROM Professor p ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||
List<Professor> findAllOrderedByDisplayOrder();
|
||||
}
|
||||
@ -7,6 +7,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProfessorService {
|
||||
@ -30,15 +31,20 @@ public interface ProfessorService {
|
||||
byte[] getImageByProfessorId(UUID professorId, String filename);
|
||||
|
||||
byte[] getDefaultProfileImage(UUID professorId);
|
||||
|
||||
// Existing method for active professors
|
||||
|
||||
Page<Professor> findActiveProfessors(Pageable pageable);
|
||||
|
||||
// New methods for category-based filtering
|
||||
|
||||
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
|
||||
|
||||
|
||||
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
|
||||
|
||||
// Method to find professor with details
|
||||
|
||||
Professor findProfessorWithDetailsById(UUID professorId);
|
||||
|
||||
/**
|
||||
* Bulk-update displayOrder for all professors.
|
||||
*
|
||||
* @param orderedIds Professor UUIDs in the desired display order (index 0 = first).
|
||||
* @return All professors sorted by their new displayOrder.
|
||||
*/
|
||||
List<Professor> updateDisplayOrder(List<UUID> orderedIds);
|
||||
}
|
||||
@ -35,6 +35,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
|
||||
import static org.springframework.http.MediaType.*;
|
||||
@ -63,10 +64,6 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses ISO date string from frontend (e.g. "2026-04-22T23:17:58.831Z")
|
||||
* into LocalDateTime. Falls back to now() if null/blank/invalid.
|
||||
*/
|
||||
private LocalDateTime parseJoinDate(String joinDate) {
|
||||
if (joinDate == null || joinDate.isBlank()) {
|
||||
return LocalDateTime.now();
|
||||
@ -179,6 +176,12 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
|
||||
|
||||
// New professors get displayOrder = 0 by default (appear first).
|
||||
// Admins can re-order via the drag-and-drop endpoint.
|
||||
if (professor.getDisplayOrder() == null) {
|
||||
professor.setDisplayOrder(0);
|
||||
}
|
||||
|
||||
Professor savedProfessor = professorRepository.save(professor);
|
||||
|
||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||
@ -238,17 +241,15 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.setDescription(professorDto.getDescription());
|
||||
professor.setDesignation(professorDto.getDesignation());
|
||||
professor.setWorkDays(professorDto.getWorkDays());
|
||||
// displayOrder is intentionally NOT updated here — only via the reorder endpoint.
|
||||
|
||||
// Parse joinDate string safely
|
||||
if (professorDto.getJoinDate() != null && !professorDto.getJoinDate().isBlank()) {
|
||||
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||
}
|
||||
|
||||
final Professor professorRef = professor;
|
||||
|
||||
if (professor.getSkills() == null) {
|
||||
professor.setSkills(new HashSet<>());
|
||||
}
|
||||
if (professor.getSkills() == null) professor.setSkills(new HashSet<>());
|
||||
professor.getSkills().clear();
|
||||
|
||||
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
|
||||
@ -263,9 +264,7 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
professor.getSkills().addAll(newSkills);
|
||||
}
|
||||
|
||||
if (professor.getAwards() == null) {
|
||||
professor.setAwards(new HashSet<>());
|
||||
}
|
||||
if (professor.getAwards() == null) professor.setAwards(new HashSet<>());
|
||||
professor.getAwards().clear();
|
||||
|
||||
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
|
||||
@ -329,4 +328,19 @@ public class ProfessorServiceImpl implements ProfessorService {
|
||||
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
|
||||
return responseEntity.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a new displayOrder for every professor in the given list.
|
||||
* The list position (index) becomes the professor's displayOrder value.
|
||||
*
|
||||
* @param orderedIds UUIDs in desired display order; index 0 → displayOrder=0, etc.
|
||||
* @return All professors sorted by their updated displayOrder.
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public List<Professor> updateDisplayOrder(List<UUID> orderedIds) {
|
||||
IntStream.range(0, orderedIds.size())
|
||||
.forEach(i -> professorRepository.updateDisplayOrder(orderedIds.get(i), i));
|
||||
return professorRepository.findAllOrderedByDisplayOrder();
|
||||
}
|
||||
}
|
||||
@ -71,6 +71,7 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Search Box */
|
||||
@ -162,6 +163,109 @@
|
||||
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 {
|
||||
animation: fadeIn 0.3s ease;
|
||||
@ -205,9 +309,16 @@
|
||||
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 {
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -226,6 +337,93 @@
|
||||
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 {
|
||||
width: 40px;
|
||||
@ -286,40 +484,13 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-faculty {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.badge-support {
|
||||
background: #e0e7ff;
|
||||
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;
|
||||
}
|
||||
.badge-faculty { background: #dbeafe; color: #1e40af; }
|
||||
.badge-support { background: #e0e7ff; 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 {
|
||||
@ -332,20 +503,10 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-leave {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-retired {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.status-active { background: #d1fae5; color: #065f46; }
|
||||
.status-leave { background: #fef3c7; color: #92400e; }
|
||||
.status-retired { background: #fee2e2; color: #991b1b; }
|
||||
.status-inactive { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
/* Action Buttons */
|
||||
.action-buttons {
|
||||
@ -422,6 +583,24 @@
|
||||
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-overlay {
|
||||
position: fixed;
|
||||
@ -454,14 +633,8 @@
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@ -564,6 +737,7 @@
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
@ -699,6 +873,25 @@
|
||||
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-wrapper {
|
||||
position: relative;
|
||||
@ -728,6 +921,11 @@
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.file-label.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-label i {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
@ -738,7 +936,7 @@
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
/* Form Section */
|
||||
.form-section {
|
||||
margin: 24px 0;
|
||||
padding: 20px;
|
||||
@ -746,6 +944,7 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -820,100 +1019,6 @@
|
||||
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 {
|
||||
font-size: 16px;
|
||||
@ -1090,7 +1195,7 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments for Professor Component */
|
||||
/* Responsive Design */
|
||||
@media (max-width: 992px) {
|
||||
.professor-content {
|
||||
margin-left: 0;
|
||||
@ -1098,11 +1203,28 @@
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.professor-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.award-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -1117,10 +1239,32 @@
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.professors-table {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-days-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -1132,6 +1276,11 @@
|
||||
.detail-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.reorder-hint-badge {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@ -1139,6 +1288,15 @@
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions > * {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.professor-detail-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
@ -1149,7 +1307,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles for Professor */
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.sidebar,
|
||||
.header-actions,
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="professor-content">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="professor-header">
|
||||
<div class="header-left">
|
||||
@ -11,17 +12,49 @@
|
||||
<p class="page-subtitle">Manage faculty, support team, and trainee/fellow profiles</p>
|
||||
</div>
|
||||
<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>
|
||||
<input name="searchTerm" #searchTerm="ngModel" class="search-input" type="search"
|
||||
placeholder="Search professors..." ngModel (ngModelChange)="searchProfessors(searchTerm.value)">
|
||||
</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>
|
||||
<span>New Professor</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,43 +62,80 @@
|
||||
<!-- Professors Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-wrapper">
|
||||
<table class="professors-table">
|
||||
<table class="professors-table" [class.reorder-active]="reorderMode">
|
||||
<thead>
|
||||
<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>Professor ID</th>
|
||||
<th *ngIf="!reorderMode">Professor ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th *ngIf="!reorderMode">Email</th>
|
||||
<th>Department</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr *ngFor="let professor of professors">
|
||||
<td (click)="onSelectProfessor(professor)">
|
||||
<tr *ngFor="let professor of professors; let i = index"
|
||||
[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">
|
||||
<img [src]="professor?.profileImageUrl" [alt]="professor?.firstName">
|
||||
</div>
|
||||
</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>
|
||||
</td>
|
||||
<td (click)="onSelectProfessor(professor)">
|
||||
|
||||
<!-- Name -->
|
||||
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||
<div class="professor-name-cell">
|
||||
<span class="full-name">{{ professor?.firstName }} {{ professor?.lastName }}</span>
|
||||
<span class="specialty-text" *ngIf="professor?.specialty">{{ professor?.specialty }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</td>
|
||||
<td (click)="onSelectProfessor(professor)">
|
||||
|
||||
<!-- Department -->
|
||||
<td (click)="!reorderMode && onSelectProfessor(professor)">
|
||||
<span class="department-text">{{ professor?.department || 'N/A' }}</span>
|
||||
</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-trainee]="professor?.category === 'TRAINEE_FELLOW'"
|
||||
[class.badge-resigned]="professor?.category === 'RESIGNED'"
|
||||
@ -75,25 +145,33 @@
|
||||
{{ getCategoryDisplayName(professor?.category) }}
|
||||
</span>
|
||||
</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-retired]="professor?.status === WorkingStatus.RETIRED"
|
||||
[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-times-circle]="professor?.status === WorkingStatus.RETIRED"
|
||||
[class.fa-ban]="professor?.status === WorkingStatus.INACTIVE"></i>
|
||||
{{ professor?.status }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions (dimmed / disabled during reorder mode) -->
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-action btn-edit" (click)="onEditProfessor(professor)" title="Edit">
|
||||
<div class="action-buttons" [class.actions-disabled]="reorderMode">
|
||||
<button class="btn-action btn-edit" (click)="!reorderMode && onEditProfessor(professor)"
|
||||
[disabled]="reorderMode" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button *ngIf="isAdmin" class="btn-action btn-delete" (click)="onDeleteProfessor(professor)"
|
||||
title="Delete">
|
||||
<button *ngIf="isAdmin" class="btn-action btn-delete"
|
||||
(click)="!reorderMode && onDeleteProfessor(professor)"
|
||||
[disabled]="reorderMode" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -111,6 +189,12 @@
|
||||
<h3>No professors found</h3>
|
||||
<p>Try adjusting your search or add a new professor</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="refreshing" class="loading-state">
|
||||
<div class="spinner-border-lg"></div>
|
||||
<p>Loading professors…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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="closeEditProfessorButton" data-bs-dismiss="modal"></button>
|
||||
|
||||
<!-- View Professor Modal -->
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
VIEW PROFESSOR MODAL
|
||||
═══════════════════════════════════════════════════════════════════════ -->
|
||||
<div *ngIf="selectedProfessor" class="modal fade" id="viewProfessorModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
@ -141,7 +228,8 @@
|
||||
<h4>{{ selectedProfessor.firstName }} {{ selectedProfessor.lastName }}</h4>
|
||||
<p class="email">{{ selectedProfessor.email }}</p>
|
||||
<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-trainee]="selectedProfessor.category === 'TRAINEE_FELLOW'"
|
||||
[class.badge-resigned]="selectedProfessor.category === 'RESIGNED'"
|
||||
@ -150,12 +238,18 @@
|
||||
[class.badge-patrons]="selectedProfessor.category === 'PATRONS'">
|
||||
{{ getCategoryDisplayName(selectedProfessor.category) }}
|
||||
</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-retired]="selectedProfessor.status === 'RETIRED'"
|
||||
[class.status-inactive]="selectedProfessor.status === 'INACTIVE'">
|
||||
{{ selectedProfessor.status }}
|
||||
</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 class="info-grid">
|
||||
<div class="info-item" *ngIf="selectedProfessor.phone">
|
||||
@ -220,15 +314,17 @@
|
||||
</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>
|
||||
<div class="work-days-list">
|
||||
<span class="day-badge" *ngFor="let day of selectedProfessor.workDays">{{ day }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" *ngIf="selectedProfessor.awards && selectedProfessor.awards.length > 0">
|
||||
<h5>Awards & Recognition</h5>
|
||||
<div class="detail-section"
|
||||
*ngIf="selectedProfessor.awards && selectedProfessor.awards.length > 0">
|
||||
<h5>Awards & Recognition</h5>
|
||||
<div class="awards-grid">
|
||||
<div class="award-card" *ngFor="let award of selectedProfessor.awards">
|
||||
<div class="award-header">
|
||||
@ -249,7 +345,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Professor Modal -->
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
ADD PROFESSOR MODAL
|
||||
═══════════════════════════════════════════════════════════════════════ -->
|
||||
<div *ngIf="isManager" class="modal fade" id="addProfessorModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
@ -262,25 +361,18 @@
|
||||
<div class="modal-body">
|
||||
<form #newProfessorForm="ngForm" (ngSubmit)="onAddNewProfessor(newProfessorForm)">
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Basic Information</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="firstName">
|
||||
<i class="fa fa-user"></i>
|
||||
First Name *
|
||||
</label>
|
||||
<label for="firstName"><i class="fa fa-user"></i> First Name *</label>
|
||||
<input type="text" id="firstName" name="firstName" class="form-input" ngModel required
|
||||
placeholder="Enter first name">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="lastName">
|
||||
<i class="fa fa-user"></i>
|
||||
Last Name *
|
||||
</label>
|
||||
<label for="lastName"><i class="fa fa-user"></i> Last Name *</label>
|
||||
<input type="text" id="lastName" name="lastName" class="form-input" ngModel required
|
||||
placeholder="Enter last name">
|
||||
</div>
|
||||
@ -288,29 +380,20 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
<i class="fa fa-envelope"></i>
|
||||
Email *
|
||||
</label>
|
||||
<label for="email"><i class="fa fa-envelope"></i> Email *</label>
|
||||
<input type="email" id="email" name="email" class="form-input" ngModel required
|
||||
placeholder="professor@example.com">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">
|
||||
<i class="fa fa-phone"></i>
|
||||
Phone
|
||||
</label>
|
||||
<input type="text" id="phone" name="phone" class="form-input" ngModel placeholder="Contact number">
|
||||
<label for="phone"><i class="fa fa-phone"></i> Phone</label>
|
||||
<input type="text" id="phone" name="phone" class="form-input" ngModel
|
||||
placeholder="Contact number">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="category">
|
||||
<i class="fa fa-tag"></i>
|
||||
Category *
|
||||
</label>
|
||||
<label for="category"><i class="fa fa-tag"></i> Category *</label>
|
||||
<select id="category" name="category" class="form-select" ngModel required>
|
||||
<option value="">Select Category</option>
|
||||
<option value="FACULTY">Faculty</option>
|
||||
@ -322,12 +405,8 @@
|
||||
<option value="PATRONS">Patrons</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
Status *
|
||||
</label>
|
||||
<label for="status"><i class="fa fa-info-circle"></i> Status *</label>
|
||||
<select id="status" name="status" class="form-select" ngModel required>
|
||||
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
||||
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
||||
@ -335,37 +414,26 @@
|
||||
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="experience">
|
||||
<i class="fa fa-briefcase"></i>
|
||||
Experience
|
||||
</label>
|
||||
<label for="experience"><i class="fa fa-briefcase"></i> Experience</label>
|
||||
<input type="text" id="experience" name="experience" class="form-input" ngModel
|
||||
placeholder="e.g., 10+ years">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional Details Section -->
|
||||
<!-- Professional Details -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Professional Details</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="department">
|
||||
<i class="fa fa-building"></i>
|
||||
Department
|
||||
</label>
|
||||
<label for="department"><i class="fa fa-building"></i> Department</label>
|
||||
<input type="text" id="department" name="department" class="form-input" ngModel
|
||||
placeholder="Enter department">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="position">
|
||||
<i class="fa fa-id-badge"></i>
|
||||
Position
|
||||
</label>
|
||||
<label for="position"><i class="fa fa-id-badge"></i> Position</label>
|
||||
<input type="text" id="position" name="position" class="form-input" ngModel
|
||||
placeholder="Enter position">
|
||||
</div>
|
||||
@ -373,81 +441,59 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="designation">
|
||||
<i class="fa fa-award"></i>
|
||||
Designation
|
||||
</label>
|
||||
<label for="designation"><i class="fa fa-award"></i> Designation</label>
|
||||
<input type="text" id="designation" name="designation" class="form-input" ngModel
|
||||
placeholder="Professional title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="officeLocation">
|
||||
<i class="fa fa-map-marker-alt"></i>
|
||||
Office Location
|
||||
</label>
|
||||
<label for="officeLocation"><i class="fa fa-map-marker-alt"></i> Office Location</label>
|
||||
<input type="text" id="officeLocation" name="officeLocation" class="form-input" ngModel
|
||||
placeholder="Office location">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="specialty">
|
||||
<i class="fa fa-star"></i>
|
||||
Specialty
|
||||
</label>
|
||||
<label for="specialty"><i class="fa fa-star"></i> Specialty</label>
|
||||
<input type="text" id="specialty" name="specialty" class="form-input" ngModel
|
||||
placeholder="Medical specialty or area of expertise">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Description
|
||||
</label>
|
||||
<label for="description"><i class="fa fa-align-left"></i> Description</label>
|
||||
<textarea id="description" name="description" class="form-textarea" rows="3" ngModel
|
||||
placeholder="Brief professional description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qualifications Section -->
|
||||
<!-- Qualifications Section -->
|
||||
<!-- Qualifications -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Qualifications</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="certification">
|
||||
<i class="fa fa-certificate"></i>
|
||||
Certification
|
||||
</label>
|
||||
<label for="certification"><i class="fa fa-certificate"></i> Certification</label>
|
||||
<textarea id="certification" name="certification" class="form-textarea" rows="2" ngModel
|
||||
placeholder="Educational qualifications and certifications"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="training">
|
||||
<i class="fa fa-graduation-cap"></i>
|
||||
Training & Professional Development
|
||||
</label>
|
||||
<label for="training"><i class="fa fa-graduation-cap"></i> Training & Professional
|
||||
Development</label>
|
||||
<textarea id="training" name="training" class="form-textarea" rows="2" ngModel
|
||||
placeholder="Professional training and development"
|
||||
[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
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Work Days Section -->
|
||||
<!-- Work Schedule -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Work Schedule</h4>
|
||||
|
||||
<label class="work-days-label">
|
||||
<i class="fa fa-calendar"></i>
|
||||
Work Days
|
||||
</label>
|
||||
<label class="work-days-label"><i class="fa fa-calendar"></i> Work Days</label>
|
||||
<div class="work-days-grid">
|
||||
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
||||
<input type="checkbox" [id]="'day-' + day" [value]="day" [(ngModel)]="selectedWorkDays[day]"
|
||||
@ -460,10 +506,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Awards Section -->
|
||||
<!-- Skills 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="award-item" *ngFor="let award of newProfessorAwards; let i = index">
|
||||
<div class="award-fields">
|
||||
@ -491,16 +562,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Picture Section -->
|
||||
<!-- Profile Picture Section -->
|
||||
<!-- Profile Picture -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Profile Picture</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fa fa-image"></i>
|
||||
Upload Profile Picture
|
||||
</label>
|
||||
<label><i class="fa fa-image"></i> Upload Profile Picture</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input type="file" id="newProfessorProfileImage" accept="image/*" name="profileImage"
|
||||
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
||||
@ -508,20 +574,20 @@
|
||||
<label for="newProfessorProfileImage" class="file-label"
|
||||
[class.disabled]="isImageUploadDisabled(newProfessorForm.value.category)">
|
||||
<i class="fa fa-cloud-upload-alt"></i>
|
||||
<span>{{ isImageUploadDisabled(newProfessorForm.value.category) ? 'Upload disabled for ' +
|
||||
getCategoryDisplayName(newProfessorForm.value.category) : (profileImageFileName || 'Choose
|
||||
profile picture') }}</span>
|
||||
<span>{{ isImageUploadDisabled(newProfessorForm.value.category)
|
||||
? 'Upload disabled for ' + getCategoryDisplayName(newProfessorForm.value.category)
|
||||
: (profileImageFileName || 'Choose profile picture') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted"
|
||||
*ngIf="isImageUploadDisabled(newProfessorForm.value.category)">
|
||||
Profile image upload is not available for {{ getCategoryDisplayName(newProfessorForm.value.category)
|
||||
}} category
|
||||
Profile image upload is not available for
|
||||
{{ getCategoryDisplayName(newProfessorForm.value.category) }} category
|
||||
</small>
|
||||
</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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -535,7 +601,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Professor Modal -->
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
EDIT PROFESSOR MODAL
|
||||
═══════════════════════════════════════════════════════════════════════ -->
|
||||
<div class="modal fade" id="editProfessorModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
@ -548,25 +617,18 @@
|
||||
<div class="modal-body">
|
||||
<form #editProfessorForm="ngForm" (ngSubmit)="onUpdateProfessor(editProfessorForm)">
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Basic Information</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editFirstName">
|
||||
<i class="fa fa-user"></i>
|
||||
First Name *
|
||||
</label>
|
||||
<label for="editFirstName"><i class="fa fa-user"></i> First Name *</label>
|
||||
<input type="text" id="editFirstName" name="firstName" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.firstName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editLastName">
|
||||
<i class="fa fa-user"></i>
|
||||
Last Name *
|
||||
</label>
|
||||
<label for="editLastName"><i class="fa fa-user"></i> Last Name *</label>
|
||||
<input type="text" id="editLastName" name="lastName" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.lastName" required>
|
||||
</div>
|
||||
@ -574,19 +636,12 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editEmail">
|
||||
<i class="fa fa-envelope"></i>
|
||||
Email *
|
||||
</label>
|
||||
<label for="editEmail"><i class="fa fa-envelope"></i> Email *</label>
|
||||
<input type="email" id="editEmail" name="email" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editPhone">
|
||||
<i class="fa fa-phone"></i>
|
||||
Phone
|
||||
</label>
|
||||
<label for="editPhone"><i class="fa fa-phone"></i> Phone</label>
|
||||
<input type="text" id="editPhone" name="phone" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.phone">
|
||||
</div>
|
||||
@ -594,10 +649,7 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editCategory">
|
||||
<i class="fa fa-tag"></i>
|
||||
Category *
|
||||
</label>
|
||||
<label for="editCategory"><i class="fa fa-tag"></i> Category *</label>
|
||||
<select id="editCategory" name="category" class="form-select"
|
||||
[(ngModel)]="selectedProfessor.category" required>
|
||||
<option value="FACULTY">Faculty</option>
|
||||
@ -609,51 +661,36 @@
|
||||
<option value="PATRONS">Patrons</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editStatus">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
Status *
|
||||
</label>
|
||||
<select id="editStatus" name="status" class="form-select" [(ngModel)]="selectedProfessor.status"
|
||||
required>
|
||||
<label for="editStatus"><i class="fa fa-info-circle"></i> Status *</label>
|
||||
<select id="editStatus" name="status" class="form-select"
|
||||
[(ngModel)]="selectedProfessor.status" required>
|
||||
<option [value]="WorkingStatus.ACTIVE">Active</option>
|
||||
<option [value]="WorkingStatus.ON_LEAVE">On Leave</option>
|
||||
<option [value]="WorkingStatus.RETIRED">Retired</option>
|
||||
<option [value]="WorkingStatus.INACTIVE">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editExperience">
|
||||
<i class="fa fa-briefcase"></i>
|
||||
Experience
|
||||
</label>
|
||||
<label for="editExperience"><i class="fa fa-briefcase"></i> Experience</label>
|
||||
<input type="text" id="editExperience" name="experience" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.experience">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional Details Section -->
|
||||
<!-- Professional Details -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Professional Details</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editDepartment">
|
||||
<i class="fa fa-building"></i>
|
||||
Department
|
||||
</label>
|
||||
<label for="editDepartment"><i class="fa fa-building"></i> Department</label>
|
||||
<input type="text" id="editDepartment" name="department" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.department">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editPosition">
|
||||
<i class="fa fa-id-badge"></i>
|
||||
Position
|
||||
</label>
|
||||
<label for="editPosition"><i class="fa fa-id-badge"></i> Position</label>
|
||||
<input type="text" id="editPosition" name="position" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.position">
|
||||
</div>
|
||||
@ -661,84 +698,63 @@
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editDesignation">
|
||||
<i class="fa fa-award"></i>
|
||||
Designation
|
||||
</label>
|
||||
<label for="editDesignation"><i class="fa fa-award"></i> Designation</label>
|
||||
<input type="text" id="editDesignation" name="designation" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.designation">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editOfficeLocation">
|
||||
<i class="fa fa-map-marker-alt"></i>
|
||||
Office Location
|
||||
</label>
|
||||
<label for="editOfficeLocation"><i class="fa fa-map-marker-alt"></i> Office Location</label>
|
||||
<input type="text" id="editOfficeLocation" name="officeLocation" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.officeLocation">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editSpecialty">
|
||||
<i class="fa fa-star"></i>
|
||||
Specialty
|
||||
</label>
|
||||
<label for="editSpecialty"><i class="fa fa-star"></i> Specialty</label>
|
||||
<input type="text" id="editSpecialty" name="specialty" class="form-input"
|
||||
[(ngModel)]="selectedProfessor.specialty">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editDescription">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Description
|
||||
</label>
|
||||
<label for="editDescription"><i class="fa fa-align-left"></i> Description</label>
|
||||
<textarea id="editDescription" name="description" class="form-textarea" rows="3"
|
||||
[(ngModel)]="selectedProfessor.description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qualifications Section -->
|
||||
<!-- Qualifications -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Qualifications</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editCertification">
|
||||
<i class="fa fa-certificate"></i>
|
||||
Certification
|
||||
</label>
|
||||
<label for="editCertification"><i class="fa fa-certificate"></i> Certification</label>
|
||||
<textarea id="editCertification" name="certification" class="form-textarea" rows="2"
|
||||
[(ngModel)]="selectedProfessor.certification"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editTraining">
|
||||
<i class="fa fa-graduation-cap"></i>
|
||||
Training & Professional Development
|
||||
</label>
|
||||
<label for="editTraining"><i class="fa fa-graduation-cap"></i> Training & Professional
|
||||
Development</label>
|
||||
<textarea id="editTraining" name="training" class="form-textarea" rows="2"
|
||||
[(ngModel)]="selectedProfessor.training"
|
||||
[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
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Work Days Section -->
|
||||
<!-- Work Schedule -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Work Schedule</h4>
|
||||
|
||||
<label class="work-days-label">
|
||||
<i class="fa fa-calendar"></i>
|
||||
Work Days
|
||||
</label>
|
||||
<label class="work-days-label"><i class="fa fa-calendar"></i> Work Days</label>
|
||||
<div class="work-days-grid">
|
||||
<div class="checkbox-wrapper" *ngFor="let day of availableDays">
|
||||
<input type="checkbox" [id]="'edit-day-' + day" [value]="day" [(ngModel)]="selectedWorkDays[day]"
|
||||
name="editWorkDays" class="checkbox-input">
|
||||
<input type="checkbox" [id]="'edit-day-' + day" [value]="day"
|
||||
[(ngModel)]="selectedWorkDays[day]" name="editWorkDays" class="checkbox-input">
|
||||
<label [for]="'edit-day-' + day" class="checkbox-label">
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text">{{ day }}</span>
|
||||
@ -747,26 +763,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Awards Section -->
|
||||
<!-- Skills 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="award-item" *ngFor="let award of selectedProfessorAwards; let i = index">
|
||||
<div class="award-fields">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-input" [(ngModel)]="award.title" [name]="'editAwardTitle' + i"
|
||||
placeholder="Award Title">
|
||||
<input type="text" class="form-input" [(ngModel)]="award.title"
|
||||
[name]="'editAwardTitle' + i" placeholder="Award Title">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-input" [(ngModel)]="award.year" [name]="'editAwardYear' + i"
|
||||
placeholder="Year">
|
||||
<input type="text" class="form-input" [(ngModel)]="award.year"
|
||||
[name]="'editAwardYear' + i" placeholder="Year">
|
||||
</div>
|
||||
<div class="form-group form-group-wide">
|
||||
<textarea class="form-textarea" rows="2" [(ngModel)]="award.description"
|
||||
[name]="'editAwardDesc' + i" placeholder="Description"></textarea>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@ -778,16 +820,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Picture Section -->
|
||||
<!-- Profile Picture Section -->
|
||||
<!-- Profile Picture -->
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Profile Picture</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fa fa-image"></i>
|
||||
Upload Profile Picture
|
||||
</label>
|
||||
<label><i class="fa fa-image"></i> Upload Profile Picture</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input type="file" id="editProfessorProfileImage" accept="image/*" name="profileImage"
|
||||
(change)="onProfileImageChange($any($event).target.files)" class="file-input"
|
||||
@ -795,14 +832,15 @@
|
||||
<label for="editProfessorProfileImage" class="file-label"
|
||||
[class.disabled]="!isManager || isImageUploadDisabled(selectedProfessor.category)">
|
||||
<i class="fa fa-cloud-upload-alt"></i>
|
||||
<span>{{ isImageUploadDisabled(selectedProfessor.category) ? 'Upload disabled for ' + getCategoryDisplayName(selectedProfessor.category) :
|
||||
(profileImageFileName || 'Choose profile picture') }}</span>
|
||||
<span>{{ isImageUploadDisabled(selectedProfessor.category)
|
||||
? 'Upload disabled for ' + getCategoryDisplayName(selectedProfessor.category)
|
||||
: (profileImageFileName || 'Choose profile picture') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted"
|
||||
*ngIf="isImageUploadDisabled(selectedProfessor.category)">
|
||||
Profile image upload is not available for {{ getCategoryDisplayName(selectedProfessor.category) }}
|
||||
category
|
||||
Profile image upload is not available for
|
||||
{{ getCategoryDisplayName(selectedProfessor.category) }} category
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -49,6 +49,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
public refreshing: boolean;
|
||||
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 = {
|
||||
professorId: '',
|
||||
firstName: '',
|
||||
@ -77,26 +87,20 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
public profileImage: File | null;
|
||||
public fileUploadStatus: FileUploadStatus = new FileUploadStatus();
|
||||
|
||||
// Additional properties for extended functionality
|
||||
public availableDays: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
public selectedWorkDays: { [key: string]: boolean } = {};
|
||||
|
||||
// ✅ Awards management
|
||||
public newProfessorAwards: Award[] = [];
|
||||
public selectedProfessorAwards: Award[] = [];
|
||||
|
||||
// ✅ Skills management
|
||||
public newProfessorSkills: Skill[] = [];
|
||||
public selectedProfessorSkills: Skill[] = [];
|
||||
|
||||
private closeModal(modalId: string): void {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
if (!modalElement) return;
|
||||
|
||||
const modalInstance = Modal.getInstance(modalElement) || new Modal(modalElement);
|
||||
modalInstance.hide();
|
||||
|
||||
// Force-remove leftover background overlays if any remain
|
||||
document.body.classList.remove('modal-open');
|
||||
document.querySelectorAll('.modal-backdrop').forEach(b => b.remove());
|
||||
}
|
||||
@ -119,7 +123,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
// ✅ FIX: Renamed and now initializes both awards and skills
|
||||
// ── Collection helpers ─────────────────────────────────────────────────────
|
||||
|
||||
private initializeCollections(): void {
|
||||
this.newProfessorAwards = [];
|
||||
this.selectedProfessorAwards = [];
|
||||
@ -130,16 +135,11 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
private setupModalEventListeners(): void {
|
||||
const editModal = document.getElementById('editProfessorModal');
|
||||
if (editModal) {
|
||||
editModal.addEventListener('hidden.bs.modal', () => {
|
||||
this.clearEditProfessorData();
|
||||
});
|
||||
editModal.addEventListener('hidden.bs.modal', () => this.clearEditProfessorData());
|
||||
}
|
||||
|
||||
const addModal = document.getElementById('addProfessorModal');
|
||||
if (addModal) {
|
||||
addModal.addEventListener('hidden.bs.modal', () => {
|
||||
this.clearNewProfessorData();
|
||||
});
|
||||
addModal.addEventListener('hidden.bs.modal', () => this.clearNewProfessorData());
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +148,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.profileImageFileName = null;
|
||||
}
|
||||
|
||||
// ✅ FIX: Now clears skills too
|
||||
private clearNewProfessorData(): void {
|
||||
this.profileImage = null;
|
||||
this.profileImageFileName = null;
|
||||
@ -157,7 +156,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.newProfessorSkills = [];
|
||||
}
|
||||
|
||||
// ✅ FIX: Now clears skills too
|
||||
private clearEditProfessorData(): void {
|
||||
this.profileImage = null;
|
||||
this.profileImageFileName = null;
|
||||
@ -166,76 +164,124 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.selectedProfessorSkills = [];
|
||||
}
|
||||
|
||||
// ─── Award management methods ────────────────────────────────────────────────
|
||||
// ── Award helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
public addNewAward(): void {
|
||||
this.newProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' });
|
||||
}
|
||||
public addNewAward(): void { 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 {
|
||||
this.newProfessorAwards.splice(index, 1);
|
||||
}
|
||||
// ── Skill helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
public addEditAward(): void {
|
||||
this.selectedProfessorAwards.push({ title: '', year: '', description: '', imageUrl: '' });
|
||||
}
|
||||
public addNewSkill(): void { this.newProfessorSkills.push({ name: '', level: 0 }); }
|
||||
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 {
|
||||
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 ─────────────────────────────────────────────────
|
||||
// ── Category helper ────────────────────────────────────────────────────────
|
||||
|
||||
public getCategoryDisplayName(category: string): string {
|
||||
switch (category) {
|
||||
case 'FACULTY': return 'Faculty';
|
||||
case 'SUPPORT_TEAM': return 'Support Team';
|
||||
case 'TRAINEE_FELLOW': return 'Trainee/Fellow';
|
||||
case 'RESIGNED': return 'Resigned';
|
||||
case 'GUIDES': return 'Guides';
|
||||
case 'FRIENDS': return 'Friends';
|
||||
case 'PATRONS': return 'Patrons';
|
||||
default: return category || 'Unknown';
|
||||
}
|
||||
const map: Record<string, string> = {
|
||||
FACULTY: 'Faculty',
|
||||
SUPPORT_TEAM: 'Support Team',
|
||||
TRAINEE_FELLOW: 'Trainee/Fellow',
|
||||
RESIGNED: 'Resigned',
|
||||
GUIDES: 'Guides',
|
||||
FRIENDS: 'Friends',
|
||||
PATRONS: 'Patrons',
|
||||
};
|
||||
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'];
|
||||
|
||||
public isImageUploadDisabled(category: string): boolean {
|
||||
return this.NO_IMAGE_CATEGORIES.includes(category);
|
||||
}
|
||||
|
||||
handleTitleChange(title: string): void {
|
||||
this.titleSubject.next(title);
|
||||
// ── Drag-and-drop reordering ───────────────────────────────────────────────
|
||||
|
||||
/** 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 {
|
||||
this.titleSubject.next(title);
|
||||
public onDragStart(index: number): void {
|
||||
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 {
|
||||
this.refreshing = true;
|
||||
this.subs.sink = this.professorService.getAllProfessors().subscribe(
|
||||
@ -243,7 +289,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.professors = professorsPage.content;
|
||||
this.professorService.addProfessorsToLocalStorage(this.professors);
|
||||
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;
|
||||
},
|
||||
@ -257,23 +304,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
public onSelectProfessor(selectedProfessor: Professor): void {
|
||||
this.selectedProfessor = JSON.parse(JSON.stringify(selectedProfessor));
|
||||
|
||||
// ✅ FIX: Load awards
|
||||
this.selectedProfessorAwards = selectedProfessor.awards
|
||||
? JSON.parse(JSON.stringify(selectedProfessor.awards))
|
||||
: [];
|
||||
? JSON.parse(JSON.stringify(selectedProfessor.awards)) : [];
|
||||
|
||||
// ✅ FIX: Load 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 = {};
|
||||
if (selectedProfessor.workDays && Array.isArray(selectedProfessor.workDays)) {
|
||||
selectedProfessor.workDays.forEach(day => {
|
||||
if (this.availableDays.includes(day)) {
|
||||
this.selectedWorkDays[day] = true;
|
||||
}
|
||||
if (this.availableDays.includes(day)) this.selectedWorkDays[day] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@ -313,7 +353,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.closeModal('addProfessorModal');
|
||||
this.getProfessors(false);
|
||||
professorForm.reset();
|
||||
this.notificationService.notify(NotificationType.SUCCESS, `Professor ${professor.firstName} added successfully`);
|
||||
this.notificationService.notify(NotificationType.SUCCESS,
|
||||
`Professor ${professor.firstName} added successfully`);
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
@ -321,32 +362,23 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
public saveNewProfessor(): void {
|
||||
this.clickButton('new-professor-save');
|
||||
}
|
||||
public saveNewProfessor(): void { this.clickButton('new-professor-save'); }
|
||||
|
||||
public searchProfessors(searchTerm: string): void {
|
||||
if (!searchTerm) {
|
||||
this.professors = this.professorService.getProfessorsFromLocalStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
const matchProfessors: Professor[] = [];
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
for (const professor of this.professorService.getProfessorsFromLocalStorage()) {
|
||||
if (
|
||||
professor.firstName.toLowerCase().includes(searchTerm) ||
|
||||
professor.lastName.toLowerCase().includes(searchTerm) ||
|
||||
professor.email.toLowerCase().includes(searchTerm) ||
|
||||
(professor.department && professor.department.toLowerCase().includes(searchTerm)) ||
|
||||
(professor.phone && professor.phone.toLowerCase().includes(searchTerm)) ||
|
||||
(professor.specialty && professor.specialty.toLowerCase().includes(searchTerm)) ||
|
||||
(professor.category && this.getCategoryDisplayName(professor.category).toLowerCase().includes(searchTerm))
|
||||
) {
|
||||
matchProfessors.push(professor);
|
||||
}
|
||||
}
|
||||
this.professors = matchProfessors;
|
||||
this.professors = this.professorService.getProfessorsFromLocalStorage().filter(p =>
|
||||
p.firstName.toLowerCase().includes(searchTerm) ||
|
||||
p.lastName.toLowerCase().includes(searchTerm) ||
|
||||
p.email.toLowerCase().includes(searchTerm) ||
|
||||
(p.department && p.department.toLowerCase().includes(searchTerm)) ||
|
||||
(p.phone && p.phone.toLowerCase().includes(searchTerm)) ||
|
||||
(p.specialty && p.specialty.toLowerCase().includes(searchTerm)) ||
|
||||
(p.category && this.getCategoryDisplayName(p.category).toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
|
||||
private clickButton(buttonId: string): void {
|
||||
@ -356,25 +388,18 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
public onEditProfessor(professor: Professor): void {
|
||||
this.selectedProfessor = JSON.parse(JSON.stringify(professor));
|
||||
|
||||
// Set up work days for editing
|
||||
this.selectedWorkDays = {};
|
||||
if (professor.workDays && Array.isArray(professor.workDays)) {
|
||||
professor.workDays.forEach(day => {
|
||||
if (this.availableDays.includes(day)) {
|
||||
this.selectedWorkDays[day] = true;
|
||||
}
|
||||
if (this.availableDays.includes(day)) this.selectedWorkDays[day] = true;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ FIX: Load awards for editing
|
||||
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
|
||||
? JSON.parse(JSON.stringify(professor.skills))
|
||||
: [];
|
||||
? JSON.parse(JSON.stringify(professor.skills)) : [];
|
||||
|
||||
this.clickButton('openProfessorEdit');
|
||||
}
|
||||
@ -384,15 +409,14 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.createExtendedProfessorFormData(this.selectedProfessor, this.profileImage);
|
||||
|
||||
this.subs.add(this.professorService.updateProfessor(this.selectedProfessor.professorId, formData).subscribe(
|
||||
(professor: Professor) => {
|
||||
this.closeModal('editProfessorModal');
|
||||
this.getProfessors(false);
|
||||
this.invalidateVariables();
|
||||
this.notificationService.notify(NotificationType.SUCCESS, `Professor ${professor.firstName} updated successfully`);
|
||||
this.notificationService.notify(NotificationType.SUCCESS,
|
||||
`Professor ${professor.firstName} updated successfully`);
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
@ -403,7 +427,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
private createExtendedProfessorFormData(professor: any, profileImage: File | null): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
// ─── Basic fields ────────────────────────────────────────────────────────
|
||||
formData.append('firstName', professor.firstName || '');
|
||||
formData.append('lastName', professor.lastName || '');
|
||||
formData.append('email', professor.email || '');
|
||||
@ -413,62 +436,50 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
formData.append('status', professor.status || 'ACTIVE');
|
||||
formData.append('category', professor.category || 'FACULTY');
|
||||
|
||||
// ✅ FIX: Send joinDate
|
||||
if (professor.joinDate) {
|
||||
formData.append('joinDate', new Date(professor.joinDate).toISOString());
|
||||
}
|
||||
|
||||
// ─── Extended fields ─────────────────────────────────────────────────────
|
||||
formData.append('phone', professor.phone || '');
|
||||
formData.append('specialty', professor.specialty || '');
|
||||
formData.append('experience', professor.experience || '');
|
||||
formData.append('designation', professor.designation || professor.position || '');
|
||||
formData.append('description', professor.description || '');
|
||||
formData.append('certification', professor.certification || '');
|
||||
|
||||
// Only include training if NOT retired or inactive
|
||||
formData.append('phone', professor.phone || '');
|
||||
formData.append('specialty', professor.specialty || '');
|
||||
formData.append('experience', professor.experience || '');
|
||||
formData.append('designation', professor.designation || professor.position || '');
|
||||
formData.append('description', professor.description || '');
|
||||
formData.append('certification', professor.certification || '');
|
||||
formData.append('training',
|
||||
(professor.status !== WorkingStatus.RETIRED && professor.status !== WorkingStatus.INACTIVE)
|
||||
? (professor.training || '') : ''
|
||||
);
|
||||
|
||||
// ─── Work days ───────────────────────────────────────────────────────────
|
||||
const workDays = Object.keys(this.selectedWorkDays).filter(day => this.selectedWorkDays[day]);
|
||||
workDays.forEach(day => formData.append('workDays', day));
|
||||
|
||||
// ─── Awards ──────────────────────────────────────────────────────────────
|
||||
const awardsToSubmit = professor.professorId ? this.selectedProfessorAwards : this.newProfessorAwards;
|
||||
if (awardsToSubmit && awardsToSubmit.length > 0) {
|
||||
const validAwards = awardsToSubmit.filter(award =>
|
||||
award.title && award.title.trim() && award.year && award.year.trim()
|
||||
);
|
||||
validAwards.forEach((award, index) => {
|
||||
formData.append(`awards[${index}].title`, award.title.trim());
|
||||
formData.append(`awards[${index}].year`, award.year.trim());
|
||||
formData.append(`awards[${index}].description`, award.description || '');
|
||||
formData.append(`awards[${index}].imageUrl`, award.imageUrl || '');
|
||||
});
|
||||
if (awardsToSubmit?.length) {
|
||||
awardsToSubmit
|
||||
.filter(a => a.title?.trim() && a.year?.trim())
|
||||
.forEach((award, i) => {
|
||||
formData.append(`awards[${i}].title`, award.title.trim());
|
||||
formData.append(`awards[${i}].year`, award.year.trim());
|
||||
formData.append(`awards[${i}].description`, award.description || '');
|
||||
formData.append(`awards[${i}].imageUrl`, award.imageUrl || '');
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ FIX: Skills are now sent to backend
|
||||
const skillsToSubmit = professor.professorId ? this.selectedProfessorSkills : this.newProfessorSkills;
|
||||
if (skillsToSubmit && skillsToSubmit.length > 0) {
|
||||
const validSkills = skillsToSubmit.filter(skill => skill.name && skill.name.trim());
|
||||
validSkills.forEach((skill, index) => {
|
||||
formData.append(`skills[${index}].name`, skill.name.trim());
|
||||
formData.append(`skills[${index}].level`, String(skill.level ?? 0));
|
||||
});
|
||||
if (skillsToSubmit?.length) {
|
||||
skillsToSubmit
|
||||
.filter(s => s.name?.trim())
|
||||
.forEach((skill, i) => {
|
||||
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)) {
|
||||
formData.append('profileImage', profileImage);
|
||||
}
|
||||
|
||||
// Debug log
|
||||
console.log('FormData contents:');
|
||||
formData.forEach((value, key) => console.log(`${key}:`, value));
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
@ -487,20 +498,16 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public updateProfileImage(): void {
|
||||
this.clickButton('profile-image-input');
|
||||
}
|
||||
public updateProfileImage(): void { this.clickButton('profile-image-input'); }
|
||||
|
||||
public onUpdateProfileImage(): void {
|
||||
if (!this.profileImage) return;
|
||||
this.refreshing = true;
|
||||
const formData = new FormData();
|
||||
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(
|
||||
(event: HttpEvent<any>) => {
|
||||
this.reportUploadProgress(event);
|
||||
},
|
||||
(event: HttpEvent<any>) => { this.reportUploadProgress(event); },
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
this.refreshing = false;
|
||||
@ -522,9 +529,11 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
case HttpEventType.Response:
|
||||
if (event.status === 200) {
|
||||
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';
|
||||
} else {
|
||||
this.sendErrorNotification('Unable to upload image. Please try again');
|
||||
@ -536,7 +545,8 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -569,7 +579,6 @@ export class ProfessorComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
this.clearNewProfessorData();
|
||||
this.clearEditProfessorData();
|
||||
// ✅ FIX: Also reset skill arrays explicitly
|
||||
this.newProfessorSkills = [];
|
||||
this.selectedProfessorSkills = [];
|
||||
}
|
||||
|
||||
@ -15,7 +15,13 @@ export interface Professor {
|
||||
profileImageUrl: string;
|
||||
status: WorkingStatus;
|
||||
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
|
||||
phone?: string;
|
||||
specialty?: string;
|
||||
@ -25,7 +31,7 @@ export interface Professor {
|
||||
description?: string;
|
||||
designation?: string;
|
||||
workDays?: string[];
|
||||
|
||||
|
||||
// Awards and skills
|
||||
awards?: Award[];
|
||||
skills?: Skill[];
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { environment } from "../../environments/environment";
|
||||
import { HttpClient, HttpEvent } from "@angular/common/http";
|
||||
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 { map } from "rxjs/operators";
|
||||
|
||||
@ -16,8 +16,7 @@ export class ProfessorService {
|
||||
|
||||
private selectedProfessor: Professor;
|
||||
|
||||
constructor(private httpClient: HttpClient) {
|
||||
}
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
public getAllProfessors(): Observable<ProfessorPage> {
|
||||
return this.httpClient
|
||||
@ -41,11 +40,20 @@ export class ProfessorService {
|
||||
|
||||
public updateProfileImage(professorId: string, formData: FormData): Observable<HttpEvent<Professor>> {
|
||||
return this.httpClient
|
||||
.put<Professor>(`${this.host}/user/${professorId}/profile-image`, formData,
|
||||
{
|
||||
reportProgress: true,
|
||||
observe: 'events'
|
||||
});
|
||||
.put<Professor>(`${this.host}/user/${professorId}/profile-image`, formData, {
|
||||
reportProgress: true,
|
||||
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[]) {
|
||||
@ -53,7 +61,7 @@ export class ProfessorService {
|
||||
}
|
||||
|
||||
public getProfessorsFromLocalStorage(): Professor[] {
|
||||
let professors = this.storage.getItem('professors');
|
||||
const professors = this.storage.getItem('professors');
|
||||
if (professors) {
|
||||
return JSON.parse(professors);
|
||||
}
|
||||
@ -70,7 +78,6 @@ export class ProfessorService {
|
||||
formData.append('position', professor.position);
|
||||
formData.append('officeLocation', professor.officeLocation);
|
||||
formData.append('status', professor.status);
|
||||
// formData.append('joinDate', professor.joinDate.toString()); // Convert LocalDateTime to string
|
||||
if (profileImage)
|
||||
formData.append('profileImage', profileImage);
|
||||
|
||||
@ -86,7 +93,7 @@ export class ProfessorService {
|
||||
}
|
||||
|
||||
public findProfessorById(id: string): Professor | Observable<Professor> {
|
||||
let cachedProfessors = this.getProfessorsFromLocalStorage();
|
||||
const cachedProfessors = this.getProfessorsFromLocalStorage();
|
||||
const foundProfessor = cachedProfessors.find((p) => p.professorId === id);
|
||||
|
||||
if (foundProfessor) return foundProfessor;
|
||||
@ -108,4 +115,4 @@ export interface ProfessorPage {
|
||||
numberOfElements: number;
|
||||
number: number;
|
||||
empty: boolean;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user