Professor reorder

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

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.util.List;
import java.util.UUID;
import static org.springframework.http.HttpStatus.OK;
@ -33,19 +34,19 @@ public class ProfessorResource {
@PostMapping("register")
public Professor register(@RequestBody Professor professor) {
return professorService.register(professor.getFirstName(), professor.getLastName(), professor.getEmail(), professor.getDepartment(), professor.getPosition());
return professorService.register(professor.getFirstName(), professor.getLastName(),
professor.getEmail(), professor.getDepartment(), professor.getPosition());
}
@PostMapping("add")
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
log.debug("Professor DTO: {}", professorDto);
Professor professor = professorService.addNewProfessor(professorDto);
return ResponseEntity.ok(professor);
}
@PutMapping("{professorId}")
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
log.debug("Professor DTO: {}", professorDto);
return professorService.updateProfessor(professorId, professorDto);
}
@ -77,12 +78,14 @@ public class ProfessorResource {
}
@PutMapping("{professorId}/profile-image")
public Professor updateProfileImage(@PathVariable UUID professorId, @RequestParam MultipartFile profileImage) {
public Professor updateProfileImage(@PathVariable UUID professorId,
@RequestParam MultipartFile profileImage) {
return professorService.updateProfileImage(professorId, profileImage);
}
@GetMapping(path = "{professorId}/profile-image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId, @PathVariable String filename) {
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId,
@PathVariable String filename) {
return professorService.getImageByProfessorId(professorId, filename);
}
@ -90,4 +93,24 @@ public class ProfessorResource {
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
return professorService.getDefaultProfileImage(professorId);
}
}
/**
* Bulk-update displayOrder.
*
* Accepts a JSON array of professor UUIDs in the desired display order.
* The array index becomes each professor's displayOrder value.
*
* Example request body:
* ["uuid-A", "uuid-B", "uuid-C"]
*
* Returns the full professor list sorted by the new displayOrder.
*
* Requires ADMIN or MANAGER role (configure in your SecurityConfig).
*/
@PutMapping("order")
public ResponseEntity<List<Professor>> updateDisplayOrder(@RequestBody List<UUID> orderedIds) {
log.info("Updating display order for {} professors", orderedIds.size());
List<Professor> updated = professorService.updateDisplayOrder(orderedIds);
return ResponseEntity.ok(updated);
}
}

View File

@ -2,8 +2,6 @@ package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
import javax.persistence.*;
@ -46,35 +44,40 @@ public class Professor implements Serializable {
@Enumerated(EnumType.STRING)
private ProfessorCategory category;
/**
* Controls display order within each category section on the public frontend.
* Lower values appear first. Defaults to 0 (new professors appear at the top).
* Admins can drag-and-drop rows in the management UI to reorder.
*/
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
@Builder.Default
private Integer displayOrder = 0;
// Additional fields for Next.js integration
private String phone;
private String specialty;
@Column(columnDefinition = "TEXT")
private String certification;
@Column(columnDefinition = "TEXT")
private String training;
private String experience;
@Column(columnDefinition = "TEXT")
private String description;
private String designation;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
@Column(name = "work_day")
private List<String> workDays;
// ✅ CRITICAL FIX: Added orphanRemoval = true
// This tells JPA to DELETE skills that are removed from the collection
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ProfessorSkill> skills;
// ✅ CRITICAL FIX: Added orphanRemoval = true
// This tells JPA to DELETE awards that are removed from the collection
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ProfessorAward> awards;
@ -82,7 +85,6 @@ public class Professor implements Serializable {
@JsonIgnore
private List<Post> posts;
// Convenience method to get full name
public String getName() {
return firstName + " " + lastName;
}

View File

@ -3,9 +3,11 @@ package net.shyshkin.study.fullstack.supportportal.backend.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -25,15 +27,35 @@ public interface ProfessorRepository extends JpaRepository<Professor, Long> {
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
Optional<Professor> findByProfessorId(@Param("professorId") UUID professorId);
@Query("SELECT p FROM Professor p WHERE p.status = :status")
// ── Status / category filters, ordered by displayOrder then lastName ────────
@Query("SELECT p FROM Professor p WHERE p.status = :status ORDER BY p.displayOrder ASC, p.lastName ASC")
Page<Professor> findByStatus(@Param("status") WorkingStatus status, Pageable pageable);
@Query("SELECT p FROM Professor p WHERE p.category = :category")
@Query("SELECT p FROM Professor p WHERE p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category")
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
@Param("category") ProfessorCategory category,
Pageable pageable);
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
@Param("category") ProfessorCategory category,
Pageable pageable);
// ── Bulk display-order update ────────────────────────────────────────────────
/**
* Sets displayOrder for a single professor identified by professorId.
* Used by the bulk-reorder service method.
*/
@Modifying
@Query("UPDATE Professor p SET p.displayOrder = :displayOrder WHERE p.professorId = :professorId")
void updateDisplayOrder(@Param("professorId") UUID professorId,
@Param("displayOrder") int displayOrder);
/**
* Returns all professors ordered by displayOrder ASC, then lastName ASC.
* Used by the admin reorder endpoint to return the updated list.
*/
@Query("SELECT p FROM Professor p ORDER BY p.displayOrder ASC, p.lastName ASC")
List<Professor> findAllOrderedByDisplayOrder();
}

View File

@ -7,6 +7,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.UUID;
public interface ProfessorService {
@ -30,15 +31,20 @@ public interface ProfessorService {
byte[] getImageByProfessorId(UUID professorId, String filename);
byte[] getDefaultProfileImage(UUID professorId);
// Existing method for active professors
Page<Professor> findActiveProfessors(Pageable pageable);
// New methods for category-based filtering
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
// Method to find professor with details
Professor findProfessorWithDetailsById(UUID professorId);
/**
* Bulk-update displayOrder for all professors.
*
* @param orderedIds Professor UUIDs in the desired display order (index 0 = first).
* @return All professors sorted by their new displayOrder.
*/
List<Professor> updateDisplayOrder(List<UUID> orderedIds);
}

View File

@ -35,6 +35,7 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
import static org.springframework.http.MediaType.*;
@ -63,10 +64,6 @@ public class ProfessorServiceImpl implements ProfessorService {
.build();
}
/**
* Parses ISO date string from frontend (e.g. "2026-04-22T23:17:58.831Z")
* into LocalDateTime. Falls back to now() if null/blank/invalid.
*/
private LocalDateTime parseJoinDate(String joinDate) {
if (joinDate == null || joinDate.isBlank()) {
return LocalDateTime.now();
@ -179,6 +176,12 @@ public class ProfessorServiceImpl implements ProfessorService {
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
// New professors get displayOrder = 0 by default (appear first).
// Admins can re-order via the drag-and-drop endpoint.
if (professor.getDisplayOrder() == null) {
professor.setDisplayOrder(0);
}
Professor savedProfessor = professorRepository.save(professor);
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
@ -238,17 +241,15 @@ public class ProfessorServiceImpl implements ProfessorService {
professor.setDescription(professorDto.getDescription());
professor.setDesignation(professorDto.getDesignation());
professor.setWorkDays(professorDto.getWorkDays());
// displayOrder is intentionally NOT updated here — only via the reorder endpoint.
// Parse joinDate string safely
if (professorDto.getJoinDate() != null && !professorDto.getJoinDate().isBlank()) {
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
}
final Professor professorRef = professor;
if (professor.getSkills() == null) {
professor.setSkills(new HashSet<>());
}
if (professor.getSkills() == null) professor.setSkills(new HashSet<>());
professor.getSkills().clear();
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
@ -263,9 +264,7 @@ public class ProfessorServiceImpl implements ProfessorService {
professor.getSkills().addAll(newSkills);
}
if (professor.getAwards() == null) {
professor.setAwards(new HashSet<>());
}
if (professor.getAwards() == null) professor.setAwards(new HashSet<>());
professor.getAwards().clear();
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
@ -329,4 +328,19 @@ public class ProfessorServiceImpl implements ProfessorService {
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
return responseEntity.getBody();
}
/**
* Persists a new displayOrder for every professor in the given list.
* The list position (index) becomes the professor's displayOrder value.
*
* @param orderedIds UUIDs in desired display order; index 0 → displayOrder=0, etc.
* @return All professors sorted by their updated displayOrder.
*/
@Override
@Transactional
public List<Professor> updateDisplayOrder(List<UUID> orderedIds) {
IntStream.range(0, orderedIds.size())
.forEach(i -> professorRepository.updateDisplayOrder(orderedIds.get(i), i));
return professorRepository.findAllOrderedByDisplayOrder();
}
}

View File

@ -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,

View File

@ -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 &mdash; 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 &amp; 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 &amp; 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 (0100)" 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 &amp; 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 &amp; 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 (0100)" 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 &amp; 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>

View File

@ -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 = [];
}

View File

@ -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[];

View File

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