diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseApplicationController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseApplicationController.java new file mode 100644 index 0000000..b36d012 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseApplicationController.java @@ -0,0 +1,97 @@ +// CourseApplicationController.java - REST Controller for Course Applications +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Course; +import net.shyshkin.study.fullstack.supportportal.backend.domain.CourseApplication; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.CourseApplicationDto; +import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseRepository; +import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseApplicationRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; + +import java.util.List; + +@RestController +@RequestMapping("/api/course-applications") +@CrossOrigin(origins = "*") +public class CourseApplicationController { + + @Autowired + private CourseApplicationRepository courseApplicationRepository; + + @Autowired + private CourseRepository courseRepository; + + // Get all applications (for admin) + @GetMapping + public List getAllApplications() { + return courseApplicationRepository.findAll(); + } + + // Get applications by course ID + @GetMapping("/course/{courseId}") + public ResponseEntity> getApplicationsByCourseId(@PathVariable Long courseId) { + try { + List applications = courseApplicationRepository.findAllByCourseId(courseId); + return ResponseEntity.ok(applications); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Get a single application by ID + @GetMapping("/{id}") + public ResponseEntity getApplicationById(@PathVariable Long id) { + return courseApplicationRepository.findById(id) + .map(application -> ResponseEntity.ok(application)) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createApplication(@RequestBody CourseApplicationDto applicationDto) { + Course course = courseRepository.findById(applicationDto.getCourseId()) + .orElseThrow(() -> new ResourceNotFoundException("Course not found")); + + CourseApplication application = new CourseApplication(); + application.setCourse(course); + application.setFullName(applicationDto.getFullName()); + application.setEmail(applicationDto.getEmail()); + application.setPhone(applicationDto.getPhone()); + application.setQualification(applicationDto.getQualification()); + application.setExperience(applicationDto.getExperience()); + application.setCoverLetter(applicationDto.getCoverLetter()); + application.setResumeUrl(applicationDto.getResumeUrl()); + + courseApplicationRepository.save(application); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{id}/status") + public ResponseEntity updateApplicationStatus(@PathVariable Long id, @RequestParam String status) { + CourseApplication application = courseApplicationRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Application not found")); + + try { + application.setStatus(CourseApplication.ApplicationStatus.valueOf(status.toUpperCase())); + courseApplicationRepository.save(application); + return ResponseEntity.ok().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body("Invalid status: " + status); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteApplication(@PathVariable Long id) { + return courseApplicationRepository.findById(id) + .map(application -> { + courseApplicationRepository.delete(application); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseController.java new file mode 100644 index 0000000..e71011b --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/CourseController.java @@ -0,0 +1,102 @@ +// CourseController.java - REST Controller for Courses +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Course; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.CourseDto; +import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; + +import java.util.List; + +@RestController +@RequestMapping("/api/courses") +@CrossOrigin(origins = "*") +public class CourseController { + + @Autowired + private CourseRepository courseRepository; + + // Get all active courses (for public display) + @GetMapping("/active") + public ResponseEntity> getActiveCourses() { + try { + List courses = courseRepository.findAllByIsActiveTrue(); + return ResponseEntity.ok(courses); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Get all courses (for admin) + @GetMapping + public List getAllCourses() { + return courseRepository.findAll(); + } + + // Get a single course by ID + @GetMapping("/{id}") + public ResponseEntity getCourseById(@PathVariable Long id) { + return courseRepository.findById(id) + .map(course -> ResponseEntity.ok(course)) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createCourse(@RequestBody CourseDto courseDto) { + Course course = new Course(); + course.setTitle(courseDto.getTitle()); + course.setDescription(courseDto.getDescription()); + course.setDuration(courseDto.getDuration()); + course.setSeats(courseDto.getSeats()); + course.setCategory(courseDto.getCategory()); + course.setLevel(courseDto.getLevel()); + course.setInstructor(courseDto.getInstructor()); + course.setPrice(courseDto.getPrice()); + course.setStartDate(courseDto.getStartDate()); + course.setImageUrl(courseDto.getImageUrl()); + course.setEligibility(courseDto.getEligibility()); + course.setObjectives(courseDto.getObjectives()); + course.setActive(courseDto.isActive()); + + courseRepository.save(course); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{id}") + public ResponseEntity updateCourse(@PathVariable Long id, @RequestBody CourseDto courseDto) { + Course course = courseRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Course not found")); + + course.setTitle(courseDto.getTitle()); + course.setDescription(courseDto.getDescription()); + course.setDuration(courseDto.getDuration()); + course.setSeats(courseDto.getSeats()); + course.setCategory(courseDto.getCategory()); + course.setLevel(courseDto.getLevel()); + course.setInstructor(courseDto.getInstructor()); + course.setPrice(courseDto.getPrice()); + course.setStartDate(courseDto.getStartDate()); + course.setImageUrl(courseDto.getImageUrl()); + course.setEligibility(courseDto.getEligibility()); + course.setObjectives(courseDto.getObjectives()); + course.setActive(courseDto.isActive()); + + courseRepository.save(course); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCourse(@PathVariable Long id) { + return courseRepository.findById(id) + .map(course -> { + courseRepository.delete(course); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/EventController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/EventController.java index f9c048f..5544f1e 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/EventController.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/EventController.java @@ -13,6 +13,7 @@ import java.util.Optional; @RestController @RequestMapping("/api/events") +@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS}) public class EventController { @Autowired @@ -33,8 +34,12 @@ public class EventController { @PostMapping public ResponseEntity createEvent(@RequestBody Event event) { - Event savedEvent = eventRepository.save(event); - return new ResponseEntity<>(savedEvent, HttpStatus.CREATED); + try { + Event savedEvent = eventRepository.save(event); + return new ResponseEntity<>(savedEvent, HttpStatus.CREATED); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } @PutMapping("/{id}") @@ -42,9 +47,13 @@ public class EventController { if (!eventRepository.existsById(id)) { return ResponseEntity.notFound().build(); } - event.setId(id); - Event updatedEvent = eventRepository.save(event); - return new ResponseEntity<>(updatedEvent, HttpStatus.OK); + try { + event.setId(id); + Event updatedEvent = eventRepository.save(event); + return new ResponseEntity<>(updatedEvent, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } @DeleteMapping("/{id}") @@ -52,7 +61,25 @@ public class EventController { if (!eventRepository.existsById(id)) { return ResponseEntity.notFound().build(); } - eventRepository.deleteById(id); - return ResponseEntity.noContent().build(); + try { + eventRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } -} + + // Additional endpoint to get upcoming events + @GetMapping("/upcoming") + public ResponseEntity> getUpcomingEvents() { + List events = eventRepository.findByIsActiveTrueOrderByDateAsc(); + return new ResponseEntity<>(events, HttpStatus.OK); + } + + // Additional endpoint to get past events + @GetMapping("/past") + public ResponseEntity> getPastEvents() { + List events = eventRepository.findByIsActiveTrueOrderByDateDesc(); + return new ResponseEntity<>(events, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/FileController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/FileController.java new file mode 100644 index 0000000..b3f3802 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/FileController.java @@ -0,0 +1,120 @@ +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/files") +@CrossOrigin(origins = "*") +public class FileController { // Changed from FileUploadController to FileController + + @Value("${file.upload.directory:uploads}") + private String uploadDirectory; + + @Value("${app.base-url:http://localhost:8080}") + private String baseUrl; + + @PostMapping("/upload") + public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) { + System.out.println("=== FILE UPLOAD DEBUG ==="); + System.out.println("File name: " + file.getOriginalFilename()); + System.out.println("Content type: " + file.getContentType()); + + try { + if (file.isEmpty()) { + return ResponseEntity.badRequest().body("File is empty"); + } + + // Updated validation to accept both images and documents + String contentType = file.getContentType(); + if (!isValidFileType(contentType)) { + return ResponseEntity.badRequest().body("Invalid file type. Only images and documents (PDF, DOC, DOCX) are allowed."); + } + + // Create upload directory if it doesn't exist + Path uploadPath = Paths.get(uploadDirectory); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // Generate unique filename + String originalFilename = file.getOriginalFilename(); + String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String uniqueFilename = UUID.randomUUID().toString() + fileExtension; + + // Save file + Path filePath = uploadPath.resolve(uniqueFilename); + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + // Return file URL - use generic /files/ path for documents + String fileUrl = "/uploads/" + uniqueFilename; + Map response = new HashMap<>(); + response.put("url", fileUrl); + response.put("filename", uniqueFilename); + + return ResponseEntity.ok(response); + + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to upload file: " + e.getMessage()); + } + } + + // Updated validation method + private boolean isValidFileType(String contentType) { + return contentType != null && ( + // Image types + contentType.equals("image/jpeg") || + contentType.equals("image/jpg") || + contentType.equals("image/png") || + contentType.equals("image/gif") || + contentType.equals("image/webp") || + // Document types for resumes + contentType.equals("application/pdf") || + contentType.equals("application/msword") || + contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") + ); + } + + @GetMapping("/images/{filename}") + public ResponseEntity getImage(@PathVariable String filename) { + try { + Path filePath = Paths.get(uploadDirectory).resolve(filename); + if (!Files.exists(filePath)) { + return ResponseEntity.notFound().build(); + } + + byte[] imageBytes = Files.readAllBytes(filePath); + String contentType = Files.probeContentType(filePath); + + return ResponseEntity.ok() + .header("Content-Type", contentType != null ? contentType : "application/octet-stream") + .body(imageBytes); + + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private boolean isValidImageType(String contentType) { + return contentType != null && ( + contentType.equals("image/jpeg") || + contentType.equals("image/jpg") || + contentType.equals("image/png") || + contentType.equals("image/gif") || + contentType.equals("image/webp") + ); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/ImageController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/ImageController.java new file mode 100644 index 0000000..f0cb7e8 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/ImageController.java @@ -0,0 +1,127 @@ +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.shyshkin.study.fullstack.supportportal.backend.service.ProfileImageService; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; +import java.util.UUID; + +@Slf4j +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class ImageController { + + private final ProfileImageService profileImageService; + + @GetMapping("/{professorId}/profile-image/{filename}") + public ResponseEntity getProfileImage( + @PathVariable UUID professorId, + @PathVariable String filename) { + try { + log.debug("Fetching profile image for professor: {} with filename: {}", professorId, filename); + + byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, filename); + + if (imageBytes == null || imageBytes.length == 0) { + log.warn("No image data found for professor: {} with filename: {}", professorId, filename); + return ResponseEntity.notFound().build(); + } + + HttpHeaders headers = new HttpHeaders(); + + // Determine content type based on filename + String contentType = getContentTypeFromFilename(filename); + headers.setContentType(MediaType.parseMediaType(contentType)); + + // Set cache control + headers.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic()); + + log.debug("Successfully retrieved image for professor: {}, size: {} bytes", professorId, imageBytes.length); + + return ResponseEntity.ok() + .headers(headers) + .body(imageBytes); + + } catch (Exception e) { + log.error("Error retrieving profile image for professor: {} with filename: {}", professorId, filename, e); + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/{professorId}/profile-image") + public ResponseEntity getDefaultProfileImage(@PathVariable UUID professorId) { + try { + log.debug("Fetching default profile image for professor: {}", professorId); + + // Try to get the default image (avatar.jpg or similar) + byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, "avatar.jpg"); + + if (imageBytes == null || imageBytes.length == 0) { + log.warn("No default image found for professor: {}", professorId); + return ResponseEntity.notFound().build(); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_JPEG); + headers.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic()); + + log.debug("Successfully retrieved default image for professor: {}, size: {} bytes", professorId, imageBytes.length); + + return ResponseEntity.ok() + .headers(headers) + .body(imageBytes); + + } catch (Exception e) { + log.error("Error retrieving default profile image for professor: {}", professorId, e); + return ResponseEntity.notFound().build(); + } + } + + /** + * Endpoint to check if a profile image exists + */ + @GetMapping("/{professorId}/profile-image/exists") + public ResponseEntity checkProfileImageExists(@PathVariable UUID professorId) { + try { + byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, "avatar.jpg"); + boolean exists = imageBytes != null && imageBytes.length > 0; + + return ResponseEntity.ok(exists); + } catch (Exception e) { + log.error("Error checking if profile image exists for professor: {}", professorId, e); + return ResponseEntity.ok(false); + } + } + + /** + * Determine content type based on file extension + */ + private String getContentTypeFromFilename(String filename) { + if (filename == null) { + return MediaType.IMAGE_JPEG_VALUE; + } + + String lowerCaseFilename = filename.toLowerCase(); + + if (lowerCaseFilename.endsWith(".png")) { + return MediaType.IMAGE_PNG_VALUE; + } else if (lowerCaseFilename.endsWith(".gif")) { + return MediaType.IMAGE_GIF_VALUE; + } else if (lowerCaseFilename.endsWith(".webp")) { + return "image/webp"; + } else if (lowerCaseFilename.endsWith(".bmp")) { + return "image/bmp"; + } else { + // Default to JPEG + return MediaType.IMAGE_JPEG_VALUE; + } + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobApplicationController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobApplicationController.java new file mode 100644 index 0000000..afdf28c --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobApplicationController.java @@ -0,0 +1,95 @@ +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Job; +import net.shyshkin.study.fullstack.supportportal.backend.domain.JobApplication; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.JobApplicationDto; +import net.shyshkin.study.fullstack.supportportal.backend.repository.JobRepository; +import net.shyshkin.study.fullstack.supportportal.backend.repository.JobApplicationRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; + +import java.util.List; + +@RestController +@RequestMapping("/api/job-applications") +@CrossOrigin(origins = "*") +public class JobApplicationController { + + @Autowired + private JobApplicationRepository jobApplicationRepository; + + @Autowired + private JobRepository jobRepository; + + // Get all applications (for admin) + @GetMapping + public List getAllApplications() { + return jobApplicationRepository.findAll(); + } + + // Get applications by job ID + @GetMapping("/job/{jobId}") + public ResponseEntity> getApplicationsByJobId(@PathVariable Long jobId) { + try { + List applications = jobApplicationRepository.findAllByJobId(jobId); + return ResponseEntity.ok(applications); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Get a single application by ID + @GetMapping("/{id}") + public ResponseEntity getApplicationById(@PathVariable Long id) { + return jobApplicationRepository.findById(id) + .map(application -> ResponseEntity.ok(application)) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createApplication(@RequestBody JobApplicationDto applicationDto) { + Job job = jobRepository.findById(applicationDto.getJobId()) + .orElseThrow(() -> new ResourceNotFoundException("Job not found")); + + JobApplication application = new JobApplication(); + application.setJob(job); + application.setFullName(applicationDto.getFullName()); + application.setEmail(applicationDto.getEmail()); + application.setPhone(applicationDto.getPhone()); + application.setExperience(applicationDto.getExperience()); + application.setCoverLetter(applicationDto.getCoverLetter()); + application.setResumeUrl(applicationDto.getResumeUrl()); + + jobApplicationRepository.save(application); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{id}/status") + public ResponseEntity updateApplicationStatus(@PathVariable Long id, @RequestParam String status) { + JobApplication application = jobApplicationRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Application not found")); + + try { + application.setStatus(JobApplication.ApplicationStatus.valueOf(status.toUpperCase())); + jobApplicationRepository.save(application); + return ResponseEntity.ok().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body("Invalid status: " + status); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteApplication(@PathVariable Long id) { + return jobApplicationRepository.findById(id) + .map(application -> { + jobApplicationRepository.delete(application); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobController.java new file mode 100644 index 0000000..225502f --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/JobController.java @@ -0,0 +1,105 @@ +// JobController.java - REST Controller for Jobs +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Job; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.JobDto; +import net.shyshkin.study.fullstack.supportportal.backend.repository.JobRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; + +import java.util.List; + +@RestController +@RequestMapping("/api/jobs") +@CrossOrigin(origins = "*") +public class JobController { + + @Autowired + private JobRepository jobRepository; + + // Get all active jobs (for public display) + @GetMapping("/active") + public ResponseEntity> getActiveJobs() { + try { + List jobs = jobRepository.findAllByIsActiveTrue(); + return ResponseEntity.ok(jobs); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Get all jobs (for admin) + @GetMapping + public List getAllJobs() { + return jobRepository.findAll(); + } + + // Get a single job by ID + @GetMapping("/{id}") + public ResponseEntity getJobById(@PathVariable Long id) { + return jobRepository.findById(id) + .map(job -> ResponseEntity.ok(job)) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createJob(@RequestBody JobDto jobDto) { + System.out.println("=== BACKEND DEBUG ==="); + System.out.println("Received JobDto: " + jobDto); + System.out.println("isActive value: " + jobDto.isActive()); + // Remove the .getClass() line since boolean is a primitive + + Job job = new Job(); + job.setTitle(jobDto.getTitle()); + job.setDepartment(jobDto.getDepartment()); + job.setLocation(jobDto.getLocation()); + job.setType(jobDto.getType()); + job.setExperience(jobDto.getExperience()); + job.setSalary(jobDto.getSalary()); + job.setDescription(jobDto.getDescription()); + job.setRequirements(jobDto.getRequirements()); + job.setResponsibilities(jobDto.getResponsibilities()); + job.setActive(jobDto.isActive()); + + System.out.println("Job before save - isActive: " + job.isActive()); + + Job savedJob = jobRepository.save(job); + System.out.println("Job after save - isActive: " + savedJob.isActive()); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{id}") + public ResponseEntity updateJob(@PathVariable Long id, @RequestBody JobDto jobDto) { + Job job = jobRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Job not found")); + + job.setTitle(jobDto.getTitle()); + job.setDepartment(jobDto.getDepartment()); + job.setLocation(jobDto.getLocation()); + job.setType(jobDto.getType()); + job.setExperience(jobDto.getExperience()); + job.setSalary(jobDto.getSalary()); + job.setDescription(jobDto.getDescription()); + job.setRequirements(jobDto.getRequirements()); + job.setResponsibilities(jobDto.getResponsibilities()); + job.setActive(jobDto.isActive()); + + jobRepository.save(job); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteJob(@PathVariable Long id) { + return jobRepository.findById(id) + .map(job -> { + jobRepository.delete(job); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostController.java index a38cb9a..f4f3480 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostController.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostController.java @@ -1,6 +1,5 @@ package net.shyshkin.study.fullstack.supportportal.backend.controller; - import net.shyshkin.study.fullstack.supportportal.backend.domain.Post; import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor; import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.PostDto; @@ -12,6 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import lombok.extern.slf4j.Slf4j; import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; @@ -21,21 +21,28 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +@Slf4j @RestController @RequestMapping("/api/posts") +@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS}) public class PostController { @Autowired private PostRepository postRepository; + @Autowired + private ProfessorRepository professorRepository; - // Get all posts where isPosted is true + // Get all posts where isPosted is true @GetMapping("/posted") public ResponseEntity> getAllPostedPosts() { try { + log.info("Fetching all posted posts"); List posts = postRepository.findAllByIsPostedTrue(); + log.info("Retrieved {} posted posts", posts.size()); return ResponseEntity.ok(posts); } catch (Exception e) { + log.error("Error fetching posted posts: ", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @@ -44,13 +51,16 @@ public class PostController { @GetMapping("/tags/count") public ResponseEntity> getTagsWithCount() { try { + log.info("Fetching tag counts"); List tagCounts = postRepository.findTagsWithCount(); Map tagCountMap = new HashMap<>(); for (Object[] tagCount : tagCounts) { tagCountMap.put((String) tagCount[0], (Long) tagCount[1]); } + log.info("Retrieved {} unique tags", tagCountMap.size()); return ResponseEntity.ok(tagCountMap); } catch (Exception e) { + log.error("Error fetching tag counts: ", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @@ -59,84 +69,159 @@ public class PostController { @GetMapping("/tag/{tag}") public ResponseEntity> getPostsByTag(@PathVariable String tag) { try { + log.info("Fetching posts by tag: {}", tag); List posts = postRepository.findAllByTagAndIsPostedTrue(tag); + log.info("Retrieved {} posts for tag: {}", posts.size(), tag); return ResponseEntity.ok(posts); } catch (Exception e) { + log.error("Error fetching posts by tag {}: ", tag, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } // Get all posts @GetMapping - public List getAllPosts() { - return postRepository.findAll(); + public ResponseEntity> getAllPosts() { + try { + log.info("Fetching all posts"); + List posts = postRepository.findAll(); + log.info("Retrieved {} posts", posts.size()); + return ResponseEntity.ok(posts); + } catch (Exception e) { + log.error("Error fetching all posts: ", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } // Get a single post by ID @GetMapping("/{id}") public ResponseEntity getPostById(@PathVariable Long id) { - return postRepository.findById(id) - .map(post -> ResponseEntity.ok(post)) - .orElse(ResponseEntity.notFound().build()); + try { + log.info("Fetching post with id: {}", id); + return postRepository.findById(id) + .map(post -> { + log.info("Found post with id: {}", id); + return ResponseEntity.ok(post); + }) + .orElseGet(() -> { + log.warn("Post not found with id: {}", id); + return ResponseEntity.notFound().build(); + }); + } catch (Exception e) { + log.error("Error fetching post with id {}: ", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } - - @Autowired - private ProfessorRepository professorRepository; - - @PostMapping + @PostMapping public ResponseEntity createPost(@RequestBody PostDto postDto) { - Post post = new Post(); - post.setTitle(postDto.getTitle()); - post.setContent(postDto.getContent()); - post.setPosted(postDto.isPosted()); + try { + // Debug logging to see what data is being received + log.info("Creating new post with data:"); + log.info("Title: {}", postDto.getTitle()); + log.info("Content: {}", postDto.getContent()); + log.info("Posted: {}", postDto.isPosted()); + log.info("ImageUrl: {}", postDto.getImageUrl()); + log.info("Professors: {}", postDto.getProfessors()); + log.info("Tags: {}", postDto.getTags()); - // Fetch professors from IDs, filter out null IDs - List validProfessorIds = postDto.getProfessors().stream() - .filter(Objects::nonNull) - .collect(Collectors.toList()); - List professors = professorRepository.findAllById(validProfessorIds); - post.setProfessors(professors); + Post post = new Post(); + post.setTitle(postDto.getTitle()); + post.setContent(postDto.getContent()); + post.setPosted(postDto.isPosted()); + post.setImageUrl(postDto.getImageUrl()); - // Set tags - post.setTags(postDto.getTags()); + // Fetch professors from IDs, filter out null IDs + List validProfessorIds = postDto.getProfessors().stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List professors = professorRepository.findAllById(validProfessorIds); + post.setProfessors(professors); - // Save the post - postRepository.save(post); - return ResponseEntity.status(HttpStatus.CREATED).build(); + // Set tags + post.setTags(postDto.getTags()); + + // Save the post + Post savedPost = postRepository.save(post); + log.info("Successfully created post with id: {}", savedPost.getId()); + return ResponseEntity.status(HttpStatus.CREATED).body(savedPost); + + } catch (Exception e) { + log.error("Error creating post: ", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to create post: " + e.getMessage()); + } } @PutMapping("/{id}") public ResponseEntity updatePost(@PathVariable Long id, @RequestBody PostDto postDto) { - Post post = postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Post not found")); + try { + log.info("Updating post with id: {}", id); + log.info("New Title: {}", postDto.getTitle()); + log.info("New Content length: {}", postDto.getContent() != null ? postDto.getContent().length() : 0); + log.info("New Posted status: {}", postDto.isPosted()); + log.info("New ImageUrl: {}", postDto.getImageUrl()); - post.setTitle(postDto.getTitle()); - post.setContent(postDto.getContent()); - post.setPosted(postDto.isPosted()); + Post post = postRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Post not found with id: " + id)); - // Fetch professors from IDs, filter out null IDs - List validProfessorIds = postDto.getProfessors().stream() - .filter(Objects::nonNull) - .collect(Collectors.toList()); - List professors = professorRepository.findAllById(validProfessorIds); - post.setProfessors(professors); + post.setTitle(postDto.getTitle()); + post.setContent(postDto.getContent()); + post.setPosted(postDto.isPosted()); + post.setImageUrl(postDto.getImageUrl()); - // Set tags - post.setTags(postDto.getTags()); + // Fetch professors from IDs, filter out null IDs + List validProfessorIds = postDto.getProfessors().stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List professors = professorRepository.findAllById(validProfessorIds); + post.setProfessors(professors); - // Save the updated post - postRepository.save(post); - return ResponseEntity.ok().build(); + // Set tags + post.setTags(postDto.getTags()); + + // Save the updated post + Post updatedPost = postRepository.save(post); + log.info("Successfully updated post with id: {}", id); + return ResponseEntity.ok(updatedPost); + + } catch (ResourceNotFoundException e) { + log.warn("Post not found for update: {}", e.getMessage()); + return ResponseEntity.notFound().build(); + } catch (Exception e) { + log.error("Error updating post with id {}: ", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to update post: " + e.getMessage()); + } } @DeleteMapping("/{id}") public ResponseEntity deletePost(@PathVariable Long id) { - return postRepository.findById(id) - .map(post -> { - postRepository.delete(post); - return ResponseEntity.noContent().build(); // Explicitly specify the type parameter - }) - .orElse(ResponseEntity.notFound().build()); + try { + log.info("Deleting post with id: {}", id); + return postRepository.findById(id) + .map(post -> { + postRepository.delete(post); + log.info("Successfully deleted post with id: {}", id); + return ResponseEntity.noContent().build(); + }) + .orElseGet(() -> { + log.warn("Post not found for deletion with id: {}", id); + return ResponseEntity.notFound().build(); + }); + } catch (Exception e) { + log.error("Error deleting post with id {}: ", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } - -} + + // Handle preflight OPTIONS requests + @RequestMapping(method = RequestMethod.OPTIONS) + public ResponseEntity handleOptions() { + return ResponseEntity.ok() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + .header("Access-Control-Allow-Headers", "Content-Type, Authorization") + .build(); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostDTO.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostDTO.java deleted file mode 100644 index 6b73fc4..0000000 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PostDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.shyshkin.study.fullstack.supportportal.backend.controller; - - - -import java.util.List; - - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class PostDTO - { - - @NotNull - private String title; - - @NotNull - private String content; - - @NotEmpty(message = "At least one professor must be selected.") - private List professors; - - private List tags; - private boolean posted; - - // Getters and setters - // ... -} diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PublicProfessorController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PublicProfessorController.java new file mode 100644 index 0000000..61bebb2 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/PublicProfessorController.java @@ -0,0 +1,71 @@ +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; +import net.shyshkin.study.fullstack.supportportal.backend.service.ProfessorService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@Slf4j +@RestController +@RequestMapping("public/professor") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") // Configure this properly for production +public class PublicProfessorController { + + private final ProfessorService professorService; + + @GetMapping + public ResponseEntity> getAllProfessors(Pageable pageable) { + Page professors = professorService.findAll(pageable); + return ResponseEntity.ok(professors); + } + + @GetMapping("{professorId}") + public ResponseEntity getProfessorById(@PathVariable UUID professorId) { + Professor professor = professorService.findByProfessorId(professorId); + return ResponseEntity.ok(professor); + } + + @GetMapping("active") + public ResponseEntity> getActiveProfessors(Pageable pageable) { + Page activeProfessors = professorService.findActiveProfessors(pageable); + return ResponseEntity.ok(activeProfessors); + } + + // Add the missing endpoint that your Next.js frontend is calling + @GetMapping("active/category/{category}") + public ResponseEntity> getActiveProfessorsByCategory( + @PathVariable String category, + Pageable pageable) { + try { + ProfessorCategory professorCategory = ProfessorCategory.valueOf(category.toUpperCase()); + Page professors = professorService.findActiveProfessorsByCategory(professorCategory, pageable); + return ResponseEntity.ok(professors); + } catch (IllegalArgumentException e) { + log.warn("Invalid category provided: {}", category); + return ResponseEntity.badRequest().build(); + } + } + + // Additional endpoint for all professors by category (active and inactive) + @GetMapping("category/{category}") + public ResponseEntity> getProfessorsByCategory( + @PathVariable String category, + Pageable pageable) { + try { + ProfessorCategory professorCategory = ProfessorCategory.valueOf(category.toUpperCase()); + Page professors = professorService.findByCategory(professorCategory, pageable); + return ResponseEntity.ok(professors); + } catch (IllegalArgumentException e) { + log.warn("Invalid category provided: {}", category); + return ResponseEntity.badRequest().build(); + } + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UpcomingEventController.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UpcomingEventController.java new file mode 100644 index 0000000..375db9d --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UpcomingEventController.java @@ -0,0 +1,87 @@ +// UpcomingEventController.java - REST Controller for Upcoming Events +package net.shyshkin.study.fullstack.supportportal.backend.controller; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.UpcomingEvent; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UpcomingEventDto; +import net.shyshkin.study.fullstack.supportportal.backend.repository.UpcomingEventRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; + +import java.util.List; + +@RestController +@RequestMapping("/api/upcoming-events") +@CrossOrigin(origins = "*") +public class UpcomingEventController { + + @Autowired + private UpcomingEventRepository upcomingEventRepository; + + // Get all active upcoming events (for public display) + @GetMapping("/active") + public ResponseEntity> getActiveUpcomingEvents() { + try { + List events = upcomingEventRepository.findAllByIsActiveTrue(); + return ResponseEntity.ok(events); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Get all upcoming events (for admin) + @GetMapping + public List getAllUpcomingEvents() { + return upcomingEventRepository.findAll(); + } + + // Get a single upcoming event by ID + @GetMapping("/{id}") + public ResponseEntity getUpcomingEventById(@PathVariable Long id) { + return upcomingEventRepository.findById(id) + .map(event -> ResponseEntity.ok(event)) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity createUpcomingEvent(@RequestBody UpcomingEventDto eventDto) { + UpcomingEvent event = new UpcomingEvent(); + event.setTitle(eventDto.getTitle()); + event.setDescription(eventDto.getDescription()); + event.setSchedule(eventDto.getSchedule()); + event.setEventDate(eventDto.getEventDate()); + event.setActive(eventDto.isActive()); + + upcomingEventRepository.save(event); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PutMapping("/{id}") + public ResponseEntity updateUpcomingEvent(@PathVariable Long id, @RequestBody UpcomingEventDto eventDto) { + UpcomingEvent event = upcomingEventRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Upcoming event not found")); + + event.setTitle(eventDto.getTitle()); + event.setDescription(eventDto.getDescription()); + event.setSchedule(eventDto.getSchedule()); + event.setEventDate(eventDto.getEventDate()); + event.setActive(eventDto.isActive()); + + upcomingEventRepository.save(event); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUpcomingEvent(@PathVariable Long id) { + return upcomingEventRepository.findById(id) + .map(event -> { + upcomingEventRepository.delete(event); + return ResponseEntity.noContent().build(); + }) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Course.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Course.java new file mode 100644 index 0000000..088e26e --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Course.java @@ -0,0 +1,61 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import javax.persistence.*; +import java.time.LocalDate; +import java.util.List; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class Course extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(columnDefinition = "TEXT", nullable = false) + private String description; + + @Column(nullable = false) + private String duration; + + @Column(nullable = false) + private Integer seats; + + @Column(nullable = false) + private String category; + + @Column(nullable = false) + private String level; + + @Column(nullable = false) + private String instructor; + + private String price; + + @Column(name = "start_date") + private LocalDate startDate; + + private String imageUrl; + + @ElementCollection + @CollectionTable(name = "course_eligibility", joinColumns = @JoinColumn(name = "course_id")) + @Column(name = "eligibility") + private List eligibility; + + @ElementCollection + @CollectionTable(name = "course_objectives", joinColumns = @JoinColumn(name = "course_id")) + @Column(name = "objective", columnDefinition = "TEXT") + private List objectives; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/CourseApplication.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/CourseApplication.java new file mode 100644 index 0000000..beaf232 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/CourseApplication.java @@ -0,0 +1,49 @@ +// CourseApplication.java - Entity for course applications +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import javax.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class CourseApplication extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "course_id", nullable = false) + private Course course; + + @Column(nullable = false) + private String fullName; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String phone; + + @Column(nullable = false) + private String qualification; + + private String experience; + + @Column(columnDefinition = "TEXT") + private String coverLetter; + + private String resumeUrl; + + @Enumerated(EnumType.STRING) + private ApplicationStatus status = ApplicationStatus.PENDING; + + public enum ApplicationStatus { + PENDING, REVIEWED, SHORTLISTED, ACCEPTED, REJECTED, ENROLLED + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Event.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Event.java index 482e928..74652a8 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Event.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Event.java @@ -33,6 +33,18 @@ public class Event { private String subTitle; + // New fields to match Next.js component + private String description; + + private String detail; + + private String mainImage; + + @ElementCollection + @CollectionTable(name = "event_gallery_images", joinColumns = @JoinColumn(name = "event_id")) + @Column(name = "image_url") + private List galleryImages; + @Column(nullable = false) private String date; @@ -48,9 +60,10 @@ public class Event { @CollectionTable(name = "organisers", joinColumns = @JoinColumn(name = "event_id")) private List organisers; - // @ElementCollection - // @CollectionTable(name = "fees", joinColumns = @JoinColumn(name = "event_id")) - // private List fee; + // Fixed Fee mapping with proper column name + @ElementCollection + @CollectionTable(name = "event_fees", joinColumns = @JoinColumn(name = "event_id")) + private List fee; @Column(nullable = false) private String phone; @@ -61,6 +74,9 @@ public class Event { @Column(nullable = false) private Boolean isActive; + @Column(name = "is_deleted", nullable = false) + private Boolean isDeleted = false; + @ManyToMany @JoinTable( name = "event_professors", @@ -69,18 +85,59 @@ public class Event { ) private List professors; - // Assuming you have these classes defined as well + // Embedded classes @Embeddable public static class Venue { private String title; private String date; private String address; private String info; + + // Constructors, getters, setters + public Venue() {} + + public Venue(String title, String date, String address, String info) { + this.title = title; + this.date = date; + this.address = address; + this.info = info; + } + + // Getters and setters + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDate() { return date; } + public void setDate(String date) { this.date = date; } + + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + + public String getInfo() { return info; } + public void setInfo(String info) { this.info = info; } } @Embeddable public static class Fee { - private String desc; + @Column(name = "fee_description") // Explicit column mapping to avoid reserved word issues + private String description; + + @Column(name = "fee_cost") private Integer cost; + + // Constructors, getters, setters + public Fee() {} + + public Fee(String description, Integer cost) { + this.description = description; + this.cost = cost; + } + + // Getters and setters + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Integer getCost() { return cost; } + public void setCost(Integer cost) { this.cost = cost; } } -} +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Job.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Job.java new file mode 100644 index 0000000..0096f6f --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Job.java @@ -0,0 +1,54 @@ +// Job.java - Entity for job positions +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import javax.persistence.*; +import java.util.List; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class Job extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String department; + + @Column(nullable = false) + private String location; + + @Column(nullable = false) + private String type; // Full-time, Contract, Observership, etc. + + @Column(nullable = false) + private String experience; + + @Column(nullable = false) + private String salary; + + @Column(columnDefinition = "TEXT", nullable = false) + private String description; + + @ElementCollection + @CollectionTable(name = "job_requirements", joinColumns = @JoinColumn(name = "job_id")) + @Column(name = "requirement") + private List requirements; + + @ElementCollection + @CollectionTable(name = "job_responsibilities", joinColumns = @JoinColumn(name = "job_id")) + @Column(name = "responsibility") + private List responsibilities; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/JobApplication.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/JobApplication.java new file mode 100644 index 0000000..b4dba0e --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/JobApplication.java @@ -0,0 +1,47 @@ +// JobApplication.java - Entity for job applications +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import javax.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class JobApplication extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "job_id", nullable = false) + private Job job; + + @Column(nullable = false) + private String fullName; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String phone; + + @Column(nullable = false) + private String experience; + + @Column(columnDefinition = "TEXT") + private String coverLetter; + + private String resumeUrl; // Path to uploaded resume file + + @Enumerated(EnumType.STRING) + private ApplicationStatus status = ApplicationStatus.PENDING; + + public enum ApplicationStatus { + PENDING, REVIEWED, SHORTLISTED, INTERVIEWED, REJECTED, HIRED + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Post.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Post.java index 520646d..1ebf210 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Post.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Post.java @@ -10,7 +10,7 @@ import lombok.*; @NoArgsConstructor @AllArgsConstructor @ToString -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = false) // Changed from true since you're extending BaseEntity public class Post extends BaseEntity { @Id @@ -38,4 +38,7 @@ public class Post extends BaseEntity { @CollectionTable(name = "post_tags", joinColumns = @JoinColumn(name = "post_id")) @Column(name = "tag") private List tags; -} + + @Column + private String imageUrl; // Add this field +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Professor.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Professor.java index 1a02962..c574606 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Professor.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/Professor.java @@ -1,18 +1,18 @@ package net.shyshkin.study.fullstack.supportportal.backend.domain; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.Type; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import java.util.UUID; - - @Entity @Data @NoArgsConstructor @@ -25,8 +25,6 @@ public class Professor implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - // @EqualsAndHashCode.Include - // @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private Long id; @Type(type = "org.hibernate.type.UUIDCharType") @@ -43,11 +41,48 @@ public class Professor implements Serializable { private String profileImageUrl; @Enumerated(EnumType.STRING) - private WorkingStatus status; // Use enum to track detailed working status + private WorkingStatus status; + @Enumerated(EnumType.STRING) + private ProfessorCategory category; + + // 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 workDays; + + // Use Set instead of List to avoid MultipleBagFetchException + // Sets can be eagerly loaded together without issues + @OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private Set skills; + + // Use Set instead of List to avoid MultipleBagFetchException + @OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private Set awards; @ManyToMany(mappedBy = "professors") - @JsonIgnore + @JsonIgnore // Keep this as @JsonIgnore to avoid circular references private List posts; -} + // Convenience method to get full name + public String getName() { + return firstName + " " + lastName; + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorAward.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorAward.java new file mode 100644 index 0000000..f188513 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorAward.java @@ -0,0 +1,33 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProfessorAward { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String year; + + @Column(columnDefinition = "TEXT") + private String description; + + private String imageUrl; + + @ManyToOne + @JoinColumn(name = "professor_id") + @JsonIgnore + @ToString.Exclude + @EqualsAndHashCode.Exclude + private Professor professor; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorCategory.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorCategory.java new file mode 100644 index 0000000..582ae8c --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorCategory.java @@ -0,0 +1,7 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +public enum ProfessorCategory { + FACULTY, + SUPPORT_TEAM, + TRAINEE_FELLOW +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorSkill.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorSkill.java new file mode 100644 index 0000000..d4f526c --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/ProfessorSkill.java @@ -0,0 +1,28 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProfessorSkill { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private Integer level; + + @ManyToOne + @JoinColumn(name = "professor_id") + @JsonIgnore + @ToString.Exclude + @EqualsAndHashCode.Exclude + private Professor professor; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/UpcomingEvent.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/UpcomingEvent.java new file mode 100644 index 0000000..50395cc --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/UpcomingEvent.java @@ -0,0 +1,35 @@ +// UpcomingEvent.java - Entity for upcoming events +package net.shyshkin.study.fullstack.supportportal.backend.domain; + +import javax.persistence.*; +import java.time.LocalDate; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class UpcomingEvent extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(columnDefinition = "TEXT", nullable = false) + private String description; + + @Column(nullable = false) + private String schedule; // e.g., "Q3 2025", "Monthly Sessions", "Ongoing" + + @Column(name = "event_date") + private LocalDate eventDate; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/AwardDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/AwardDto.java new file mode 100644 index 0000000..be9a14c --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/AwardDto.java @@ -0,0 +1,17 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AwardDto { + private String title; + private String year; + private String description; + private String imageUrl; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseApplicationDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseApplicationDto.java new file mode 100644 index 0000000..6767e2c --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseApplicationDto.java @@ -0,0 +1,35 @@ +// CourseApplicationDto.java - DTO for Course Application +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Email; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CourseApplicationDto { + @NotNull + private Long courseId; + + @NotNull + private String fullName; + + @NotNull + @Email + private String email; + + @NotNull + private String phone; + + @NotNull + private String qualification; + + private String experience; + private String coverLetter; + private String resumeUrl; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseDto.java new file mode 100644 index 0000000..7d80866 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/CourseDto.java @@ -0,0 +1,56 @@ +// CourseDto.java - DTO for Course +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CourseDto { + @NotNull + private String title; + + @NotNull + private String description; + + @NotNull + private String duration; + + @NotNull + private Integer seats; + + @NotNull + private String category; + + @NotNull + private String level; + + @NotNull + private String instructor; + + private String price; + private LocalDate startDate; + private String imageUrl; + private List eligibility; + private List objectives; + private boolean active; + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public void setIsActive(boolean active) { + this.active = active; + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobApplicationDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobApplicationDto.java new file mode 100644 index 0000000..e33dde6 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobApplicationDto.java @@ -0,0 +1,34 @@ +// JobApplicationDto.java - DTO for Job Application +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Email; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class JobApplicationDto { + @NotNull + private Long jobId; + + @NotNull + private String fullName; + + @NotNull + @Email + private String email; + + @NotNull + private String phone; + + @NotNull + private String experience; + + private String coverLetter; + private String resumeUrl; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobDto.java new file mode 100644 index 0000000..59798b9 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/JobDto.java @@ -0,0 +1,56 @@ +// JobDto.java - DTO for Job +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class JobDto { + @NotNull + private String title; + + @NotNull + private String department; + + @NotNull + private String location; + + @NotNull + private String type; + + @NotNull + private String experience; + + @NotNull + private String salary; + + @NotNull + private String description; + + private List requirements; + private List responsibilities; + + // Explicit boolean field handling (remove @Data for this field) + private boolean active; // Change from isActive to active + + // Explicit getters and setters for the boolean field + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + // For Jackson JSON deserialization + public void setIsActive(boolean active) { + this.active = active; + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/PostDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/PostDto.java index c2d330b..11aa2f7 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/PostDto.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/PostDto.java @@ -1,23 +1,17 @@ package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; - - import java.util.List; - - +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; @Data @NoArgsConstructor @AllArgsConstructor @Builder - - public class PostDto { @NotNull @@ -31,7 +25,5 @@ public class PostDto { private List tags; private boolean posted; - - // Getters and setters - // ... -} + private String imageUrl; // Add this field +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorDto.java index 86c91b9..38b4482 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorDto.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorDto.java @@ -4,12 +4,16 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import net.shyshkin.study.fullstack.supportportal.backend.domain.Role; import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; +// Add these imports at the top +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.SkillDto; +import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.AwardDto; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; +import java.util.List; import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty; @@ -29,9 +33,19 @@ public class ProfessorDto { private String position; private String officeLocation; private WorkingStatus status; + private ProfessorCategory category; private LocalDateTime joinDate; - private MultipartFile profileImage; // Optional field for profile image URL - -} - - + private MultipartFile profileImage; + + // Additional fields for Next.js integration + private String phone; + private String specialty; + private String certification; + private String training; + private String experience; + private String description; + private String designation; + private List workDays; + private List skills; + private List awards; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorResponseDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorResponseDto.java new file mode 100644 index 0000000..486e034 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/ProfessorResponseDto.java @@ -0,0 +1,50 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProfessorResponseDto { + private Long id; + private UUID professorId; + private String firstName; + private String lastName; + private String email; + private String department; + private String position; + private String officeLocation; + private LocalDateTime joinDate; + private String profileImageUrl; + private WorkingStatus status; + private ProfessorCategory category; + + // Additional fields for Next.js integration + private String phone; + private String specialty; + private String certification; + private String training; + private String experience; + private String description; + private String designation; + private List workDays; + + // Nested DTOs for skills and awards + private List skills; + private List awards; + + // Convenience method to get full name + public String getName() { + return firstName + " " + lastName; + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/SkillDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/SkillDto.java new file mode 100644 index 0000000..084f4fa --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/SkillDto.java @@ -0,0 +1,15 @@ +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SkillDto { + private String name; + private Integer level; +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UpcomingEventDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UpcomingEventDto.java new file mode 100644 index 0000000..95da1c6 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UpcomingEventDto.java @@ -0,0 +1,39 @@ +// UpcomingEventDto.java - DTO for Upcoming Event +package net.shyshkin.study.fullstack.supportportal.backend.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpcomingEventDto { + @NotNull + private String title; + + @NotNull + private String description; + + @NotNull + private String schedule; + + private LocalDate eventDate; + private boolean active; + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public void setIsActive(boolean active) { + this.active = active; + } +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/mapper/UserMapper.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/mapper/UserMapper.java index 4e7369a..3dca147 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/mapper/UserMapper.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/mapper/UserMapper.java @@ -8,15 +8,12 @@ import org.mapstruct.Mapping; import java.time.LocalDateTime; -@Mapper(componentModel = "spring") // This is crucial for Spring integration -// @Mapper(componentModel = "spring",imports = {LocalDateTime.class}) +@Mapper(componentModel = "spring", imports = {LocalDateTime.class}) public interface UserMapper { - @Mapping(target = "isNotLocked", source = "notLocked") @Mapping(target = "isActive", source = "active") - @Mapping(target = "joinDate", expression = "java( LocalDateTime.now() )") + @Mapping(target = "joinDate", expression = "java(LocalDateTime.now())") @Mapping(target = "role", source = "role", resultType = String.class) @Mapping(target = "authorities", source = "role.authorities") User toEntity(UserDto userDto); - -} \ No newline at end of file +} diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseApplicationRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseApplicationRepository.java new file mode 100644 index 0000000..a0d50c7 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseApplicationRepository.java @@ -0,0 +1,15 @@ +// CourseApplicationRepository.java +package net.shyshkin.study.fullstack.supportportal.backend.repository; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.CourseApplication; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CourseApplicationRepository extends JpaRepository { + List findAllByCourseId(Long courseId); + List findAllByStatus(CourseApplication.ApplicationStatus status); + List findAllByEmail(String email); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseRepository.java new file mode 100644 index 0000000..cd4d3b5 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/CourseRepository.java @@ -0,0 +1,15 @@ +// CourseRepository.java +package net.shyshkin.study.fullstack.supportportal.backend.repository; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CourseRepository extends JpaRepository { + List findAllByIsActiveTrue(); + List findAllByCategory(String category); + List findAllByLevel(String level); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/EventRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/EventRepository.java index e3d1224..0f8c43a 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/EventRepository.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/EventRepository.java @@ -1,11 +1,39 @@ package net.shyshkin.study.fullstack.supportportal.backend.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import net.shyshkin.study.fullstack.supportportal.backend.domain.Event; +import java.util.List; + @Repository public interface EventRepository extends JpaRepository { - // Custom query methods can be added here if needed + + // Find active events ordered by date ascending (for upcoming events) + List findByIsActiveTrueOrderByDateAsc(); + + // Find active events ordered by date descending (for past events) + List findByIsActiveTrueOrderByDateDesc(); + + // Find events by year + List findByYearAndIsActiveTrue(String year); + + // Find events by subject + List findBySubjectContainingIgnoreCaseAndIsActiveTrue(String subject); + + // Find events by title containing keyword + List findByTitleContainingIgnoreCaseAndIsActiveTrue(String title); + + // Custom query to search events by multiple fields + @Query("SELECT e FROM Event e WHERE e.isActive = true AND " + + "(LOWER(e.title) LIKE LOWER(CONCAT('%', ?1, '%')) OR " + + "LOWER(e.description) LIKE LOWER(CONCAT('%', ?1, '%')) OR " + + "LOWER(e.subject) LIKE LOWER(CONCAT('%', ?1, '%')))") + List searchActiveEvents(String searchTerm); + + // Find events by date range (you might need to adjust based on your date format) + @Query("SELECT e FROM Event e WHERE e.isActive = true AND e.date BETWEEN ?1 AND ?2 ORDER BY e.date ASC") + List findEventsByDateRange(String startDate, String endDate); } \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobApplicationRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobApplicationRepository.java new file mode 100644 index 0000000..1269250 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobApplicationRepository.java @@ -0,0 +1,15 @@ +// JobApplicationRepository.java +package net.shyshkin.study.fullstack.supportportal.backend.repository; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.JobApplication; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface JobApplicationRepository extends JpaRepository { + List findAllByJobId(Long jobId); + List findAllByStatus(JobApplication.ApplicationStatus status); + List findAllByEmail(String email); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobRepository.java new file mode 100644 index 0000000..173d82a --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/JobRepository.java @@ -0,0 +1,15 @@ +// JobRepository.java +package net.shyshkin.study.fullstack.supportportal.backend.repository; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.Job; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface JobRepository extends JpaRepository { + List findAllByIsActiveTrue(); + List findAllByDepartment(String department); + List findAllByType(String type); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/ProfessorRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/ProfessorRepository.java index a32d905..cd055be 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/ProfessorRepository.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/ProfessorRepository.java @@ -1,17 +1,18 @@ 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.Query; import org.springframework.data.repository.query.Param; -// import org.springframework.data.rest.core.annotation.RepositoryRestResource; import java.util.Optional; import java.util.UUID; - import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor; +import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; -// @RepositoryRestResource(collectionResourceRel = "professors", path = "professors") public interface ProfessorRepository extends JpaRepository { @Query("SELECT p FROM Professor p WHERE p.email = :email") @@ -24,4 +25,15 @@ public interface ProfessorRepository extends JpaRepository { @Query("SELECT p FROM Professor p WHERE p.professorId = :professorId") Optional findByProfessorId(@Param("professorId") UUID professorId); -} + + @Query("SELECT p FROM Professor p WHERE p.status = :status") + Page findByStatus(@Param("status") WorkingStatus status, Pageable pageable); + + @Query("SELECT p FROM Professor p WHERE p.category = :category") + Page findByCategory(@Param("category") ProfessorCategory category, Pageable pageable); + + @Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category") + Page findByStatusAndCategory(@Param("status") WorkingStatus status, + @Param("category") ProfessorCategory category, + Pageable pageable); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/UpcomingEventRepository.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/UpcomingEventRepository.java new file mode 100644 index 0000000..905ad4d --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/repository/UpcomingEventRepository.java @@ -0,0 +1,13 @@ +// UpcomingEventRepository.java +package net.shyshkin.study.fullstack.supportportal.backend.repository; + +import net.shyshkin.study.fullstack.supportportal.backend.domain.UpcomingEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UpcomingEventRepository extends JpaRepository { + List findAllByIsActiveTrue(); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorService.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorService.java index 803fd33..37d5d36 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorService.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorService.java @@ -1,6 +1,7 @@ package net.shyshkin.study.fullstack.supportportal.backend.service; import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -29,4 +30,15 @@ public interface ProfessorService { byte[] getImageByProfessorId(UUID professorId, String filename); byte[] getDefaultProfileImage(UUID professorId); -} + + // Existing method for active professors + Page findActiveProfessors(Pageable pageable); + + // New methods for category-based filtering + Page findByCategory(ProfessorCategory category, Pageable pageable); + + Page findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable); + + // Method to find professor with details + Professor findProfessorWithDetailsById(UUID professorId); +} \ No newline at end of file diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorServiceImpl.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorServiceImpl.java index c37733b..6aa4924 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorServiceImpl.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/service/ProfessorServiceImpl.java @@ -3,7 +3,11 @@ package net.shyshkin.study.fullstack.supportportal.backend.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorAward; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory; +import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorSkill; import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto; +import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus; import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.EmailExistsException; import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.NotAnImageFileException; import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ProfessorNotFoundException; @@ -25,14 +29,14 @@ import javax.annotation.PostConstruct; import javax.transaction.Transactional; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; - +import java.util.stream.Collectors; import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*; - - import static org.springframework.http.MediaType.*; @Slf4j @@ -51,10 +55,8 @@ public class ProfessorServiceImpl implements ProfessorService { private final ProfileImageService profileImageService; private final RestTemplateBuilder restTemplateBuilder; - private RestTemplate restTemplate; - @PostConstruct void init() { restTemplate = restTemplateBuilder @@ -108,7 +110,26 @@ public class ProfessorServiceImpl implements ProfessorService { .orElseThrow(() -> new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG)); } - + @Override + public Page findActiveProfessors(Pageable pageable) { + return professorRepository.findByStatus(WorkingStatus.ACTIVE, pageable); + } + + @Override + public Page findByCategory(ProfessorCategory category, Pageable pageable) { + return professorRepository.findByCategory(category, pageable); + } + + @Override + public Page findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable) { + return professorRepository.findByStatusAndCategory(WorkingStatus.ACTIVE, category, pageable); + } + + @Override + public Professor findProfessorWithDetailsById(UUID professorId) { + return professorRepository.findByProfessorId(professorId) + .orElseThrow(() -> new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG)); + } private void saveProfileImage(Professor professor, MultipartFile profileImage) { if (profileImage == null) return; @@ -135,33 +156,68 @@ public class ProfessorServiceImpl implements ProfessorService { } @Override + @Transactional public Professor addNewProfessor(ProfessorDto professorDto) { - validateNewEmail(professorDto.getEmail()); Professor professor = professorMapper.toEntity(professorDto); // Set a unique identifier for the professor professor.setProfessorId(generateUuid()); - // professor.setProfessorId(UUID.randomUUID()); // Set a unique identifier for the professor - professor.setJoinDate(LocalDateTime.now()); // Set join date if not provided + professor.setJoinDate(LocalDateTime.now()); professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId())); - professorRepository.save(professor); + // Save the professor first to get the ID + Professor savedProfessor = professorRepository.save(professor); - // saveProfileImage(professor, professorDto.getProfileImage()); + // Handle skills if provided + if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) { + Set skills = professorDto.getSkills().stream() + .filter(skillDto -> skillDto.getName() != null && !skillDto.getName().trim().isEmpty()) + .map(skillDto -> ProfessorSkill.builder() + .name(skillDto.getName().trim()) + .level(skillDto.getLevel()) + .professor(savedProfessor) + .build()) + .collect(Collectors.toSet()); + savedProfessor.setSkills(skills); + } - return professor; + // Handle awards if provided + if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) { + Set awards = professorDto.getAwards().stream() + .filter(awardDto -> awardDto.getTitle() != null && !awardDto.getTitle().trim().isEmpty()) + .map(awardDto -> ProfessorAward.builder() + .title(awardDto.getTitle().trim()) + .year(awardDto.getYear()) + .description(awardDto.getDescription()) + .imageUrl(awardDto.getImageUrl()) + .professor(savedProfessor) + .build()) + .collect(Collectors.toSet()); + savedProfessor.setAwards(awards); + } + + // Save again to persist the relationships + Professor finalProfessor = professorRepository.save(savedProfessor); + + // Handle profile image if provided + if (professorDto.getProfileImage() != null) { + saveProfileImage(finalProfessor, professorDto.getProfileImage()); + } + + return finalProfessor; } @Override + @Transactional public Professor updateProfessor(UUID professorId, ProfessorDto professorDto) { - Professor professor = professorRepository.findByProfessorId(professorId) .orElseThrow(() -> new RuntimeException("Professor not found with id: " + professorId)); - validateUpdateEmail(professorId,professorDto.getEmail()); + validateUpdateEmail(professorId, professorDto.getEmail()); + // Update basic fields professor.setFirstName(professorDto.getFirstName()); professor.setLastName(professorDto.getLastName()); professor.setEmail(professorDto.getEmail()); @@ -169,13 +225,73 @@ public class ProfessorServiceImpl implements ProfessorService { professor.setPosition(professorDto.getPosition()); professor.setOfficeLocation(professorDto.getOfficeLocation()); professor.setStatus(professorDto.getStatus()); - professor.setJoinDate(professorDto.getJoinDate()); // Update join date if provided + professor.setCategory(professorDto.getCategory()); + + // Update extended fields + professor.setPhone(professorDto.getPhone()); + professor.setSpecialty(professorDto.getSpecialty()); + professor.setCertification(professorDto.getCertification()); + professor.setTraining(professorDto.getTraining()); + professor.setExperience(professorDto.getExperience()); + professor.setDescription(professorDto.getDescription()); + professor.setDesignation(professorDto.getDesignation()); + professor.setWorkDays(professorDto.getWorkDays()); - professorRepository.save(professor); + if (professorDto.getJoinDate() != null) { + professor.setJoinDate(professorDto.getJoinDate()); + } - saveProfileImage(professor, professorDto.getProfileImage()); + // Create a final reference for lambda expressions + final Professor professorRef = professor; - return professor; + // Update skills - clear existing and add new ones + if (professor.getSkills() != null) { + professor.getSkills().clear(); + } + if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) { + Set skills = professorDto.getSkills().stream() + .filter(skillDto -> skillDto.getName() != null && !skillDto.getName().trim().isEmpty()) + .map(skillDto -> ProfessorSkill.builder() + .name(skillDto.getName().trim()) + .level(skillDto.getLevel()) + .professor(professorRef) + .build()) + .collect(Collectors.toSet()); + if (professor.getSkills() == null) { + professor.setSkills(new HashSet<>()); + } + professor.getSkills().addAll(skills); + } + + // Update awards - clear existing and add new ones + if (professor.getAwards() != null) { + professor.getAwards().clear(); + } + if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) { + Set awards = professorDto.getAwards().stream() + .filter(awardDto -> awardDto.getTitle() != null && !awardDto.getTitle().trim().isEmpty()) + .map(awardDto -> ProfessorAward.builder() + .title(awardDto.getTitle().trim()) + .year(awardDto.getYear()) + .description(awardDto.getDescription()) + .imageUrl(awardDto.getImageUrl()) + .professor(professorRef) + .build()) + .collect(Collectors.toSet()); + if (professor.getAwards() == null) { + professor.setAwards(new HashSet<>()); + } + professor.getAwards().addAll(awards); + } + + Professor savedProfessor = professorRepository.save(professor); + + // Handle profile image if provided + if (professorDto.getProfileImage() != null) { + saveProfileImage(savedProfessor, professorDto.getProfileImage()); + } + + return savedProfessor; } @Override @@ -234,4 +350,4 @@ public class ProfessorServiceImpl implements ProfessorService { return currentProfessor; } -} +} \ No newline at end of file diff --git a/support-portal-backend/src/main/resources/application.yml b/support-portal-backend/src/main/resources/application.yml index 10363c8..8d4c622 100644 --- a/support-portal-backend/src/main/resources/application.yml +++ b/support-portal-backend/src/main/resources/application.yml @@ -1,10 +1,6 @@ server: error: path: /error -# whitelabel: -# enabled: false - - spring: mail: @@ -22,22 +18,20 @@ spring: enable: true ssl: enable: false + datasource: - # url: jdbc:mysql://mysql:3306/demo - url: jdbc:mysql://210.18.189.94:8098/demo - # url: jdbc:mysql://${MYSQL_HOST:db}:8098/demo - username: youruser - password: youruserpassword - # url: ${SPRING_DATASOURCE_URL} - # username: ${SPRING_DATASOURCE_USERNAME} - # password: ${SPRING_DATASOURCE_PASSWORD} + url: jdbc:mysql://localhost:3306/support_portal?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + username: support_portal_user + password: Supp0rt_Porta!_P@ssword driver-class-name: com.mysql.cj.jdbc.Driver + jpa: hibernate: ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect + servlet: multipart: max-file-size: 10MB @@ -49,137 +43,40 @@ spring: resources: add-mappings: false - - - +# File upload configuration +file: + upload: + directory: uploads app: - public-urls: /user/login,/user/register,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/professor,/professor/*,/api/events,/api/events/* + base-url: ${APP_BASE_URL:http://localhost:8080} + # Updated public URLs to include image endpoints + public-urls: /user/login,/user/register,/user/*/profile-image,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/api/posts/posted,/api/posts/tag/*,/api/posts/tags/count,/api/files/**,/professor,/professor/*,/api/events,/api/events/*,/api/public/**,/api/jobs/active,/api/job-applications,/api/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/active cors: - allowed-origins: http://localhost:4200,https://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://portal.shyshkin.net,* + allowed-origins: http://localhost:4200,https://localhost:4200,http://localhost:3000,https://localhost:3000,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://portal.shyshkin.net,* jwt: secret: custom_text - -# secret: ${random.value} #Does not work - every time generates new value -# jasypt: -# encryptor: -# # password: ${JASYPT_PASSWORD} -# password: custom_text -# algorithm: PBEWITHHMACSHA512ANDAES_256 -# iv-generator-classname: org.jasypt.iv.RandomIvGenerator +--- +# Production file upload configuration +spring: + config: + activate: + on-profile: production +file: + upload: + directory: /var/uploads/blog-images +app: + base-url: https://yourproductiondomain.com + cors: + allowed-origins: https://yourfrontenddomain.com,https://youradmindomain.com --- -# spring: -# config: -# activate: -# on-profile: local -# datasource: -# url: jdbc:mysql://210.18.189.94:8098/demo -# username: youruser -# password: youruserpassword - -# jpa: -# show-sql: true -# logging: -# level: -# net.shyshkin: debug - ---- -# spring: -# config: -# activate: -# on-profile: aws-local -# datasource: -# url: jdbc:mysql://210.18.189.94:8098/demo -# username: youruser -# password: youruserpassword - -# mail: -# host: email-smtp.eu-north-1.amazonaws.com -# port: 587 -# username: AKIAVW7XGDOWFHHCELIH -# password: BJyWOWS1xWYR35MRCFn3BuuQ6vY+k7DRsdAvOfqDs/Fk - -# we want to test (1) from localhost, (2) from S3 bucket Static Web Site, (3) from our EC2 instance -# app: -# email: -# from: d.art.shishkin@gmail.com -# carbon-copy: d.art.shishkin@gmail.com -# cors: -# allowed-origins: http://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://support-portal.shyshkin.net,http://portal.shyshkin.net -# server: -# port: 5000 -# logging: -# level: -# net.shyshkin: debug - ---- -# spring: -# config: -# activate: -# on-profile: aws-rds -# datasource: -# url: jdbc:mysql://210.18.189.94:8098/demo -# username: youruser -# password: youruserpassword - -# mail: -# host: email-smtp.eu-north-1.amazonaws.com -# port: 587 -# username: custom_text -# password: custom_text - -# we want to test (1) from localhost, (2) from S3 bucket Static Web Site, (3) from our EC2 instance -# app: -# email: -# from: d.art.shishkin@gmail.com -# carbon-copy: d.art.shishkin@gmail.com -# cors: -# allowed-origins: http://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://support-portal.shyshkin.net,http://portal.shyshkin.net -# server: -# port: 5000 -# logging: -# level: -# net.shyshkin: debug - -##### -# -# HTTPS configuration -# -##### - -# server.ssl: -# enabled: true # Enable HTTPS support (only accept HTTPS requests) -# key-alias: securedPortal # Alias that identifies the key in the key store -# key-store: classpath:securedPortal-keystore.p12 # Keystore location -# key-store-password: custom_text -# key-store-type: PKCS12 # Keystore format - -# --- -# spring: -# config: -# activate: -# on-profile: image-s3 -# app: -# amazon-s3: -# bucket-name: portal-user-profile-images - -# --- -# spring: -# config: -# activate: -# on-profile: image-s3-localstack -# app: -# amazon-s3: -# bucket-name: portal-user-profile-images -# config: -# aws: -# region: eu-north-1 -# s3: -# url: http://127.0.0.1:4566 -# bucket-name: portal-user-profile-images -# access-key: localstack -# secret-key: localstack - - +# Development file upload configuration with custom directory +spring: + config: + activate: + on-profile: dev-custom-upload +file: + upload: + directory: ${user.home}/blog-uploads \ No newline at end of file diff --git a/support-portal-backend/uploads/01ecabd7-d45e-49d1-9354-252ad8e6c038.jpg b/support-portal-backend/uploads/01ecabd7-d45e-49d1-9354-252ad8e6c038.jpg new file mode 100644 index 0000000..110e59d Binary files /dev/null and b/support-portal-backend/uploads/01ecabd7-d45e-49d1-9354-252ad8e6c038.jpg differ diff --git a/support-portal-backend/uploads/021d2554-f612-4af6-a036-264ba751b815.png b/support-portal-backend/uploads/021d2554-f612-4af6-a036-264ba751b815.png new file mode 100644 index 0000000..ef6d72e Binary files /dev/null and b/support-portal-backend/uploads/021d2554-f612-4af6-a036-264ba751b815.png differ diff --git a/support-portal-backend/uploads/07e3d3e0-466e-436e-9411-6761dfbc3205.jpg b/support-portal-backend/uploads/07e3d3e0-466e-436e-9411-6761dfbc3205.jpg new file mode 100644 index 0000000..110e59d Binary files /dev/null and b/support-portal-backend/uploads/07e3d3e0-466e-436e-9411-6761dfbc3205.jpg differ diff --git a/support-portal-backend/uploads/08fd14f9-1503-4cb2-aa95-fc1f55d5cf2c.png b/support-portal-backend/uploads/08fd14f9-1503-4cb2-aa95-fc1f55d5cf2c.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/08fd14f9-1503-4cb2-aa95-fc1f55d5cf2c.png differ diff --git a/support-portal-backend/uploads/1302778f-d927-496e-be9d-d3649cd87b2b.jpg b/support-portal-backend/uploads/1302778f-d927-496e-be9d-d3649cd87b2b.jpg new file mode 100644 index 0000000..a18ff1b Binary files /dev/null and b/support-portal-backend/uploads/1302778f-d927-496e-be9d-d3649cd87b2b.jpg differ diff --git a/support-portal-backend/uploads/1de2086b-a278-440a-a035-54ce20f84a3b.png b/support-portal-backend/uploads/1de2086b-a278-440a-a035-54ce20f84a3b.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/1de2086b-a278-440a-a035-54ce20f84a3b.png differ diff --git a/support-portal-backend/uploads/236e41b9-8b30-4cda-9555-8ebad4af01b5.png b/support-portal-backend/uploads/236e41b9-8b30-4cda-9555-8ebad4af01b5.png new file mode 100644 index 0000000..b153a93 Binary files /dev/null and b/support-portal-backend/uploads/236e41b9-8b30-4cda-9555-8ebad4af01b5.png differ diff --git a/support-portal-backend/uploads/25cfc1d6-fbf9-46b2-bc49-8bf6c1910753.webp b/support-portal-backend/uploads/25cfc1d6-fbf9-46b2-bc49-8bf6c1910753.webp new file mode 100644 index 0000000..c87f36b Binary files /dev/null and b/support-portal-backend/uploads/25cfc1d6-fbf9-46b2-bc49-8bf6c1910753.webp differ diff --git a/support-portal-backend/uploads/315d29e8-9339-41f4-9803-804e40511e66.png b/support-portal-backend/uploads/315d29e8-9339-41f4-9803-804e40511e66.png new file mode 100644 index 0000000..ef6d72e Binary files /dev/null and b/support-portal-backend/uploads/315d29e8-9339-41f4-9803-804e40511e66.png differ diff --git a/support-portal-backend/uploads/31c5b957-9418-44bf-8fc9-2992afeb563e.jpg b/support-portal-backend/uploads/31c5b957-9418-44bf-8fc9-2992afeb563e.jpg new file mode 100644 index 0000000..110e59d Binary files /dev/null and b/support-portal-backend/uploads/31c5b957-9418-44bf-8fc9-2992afeb563e.jpg differ diff --git a/support-portal-backend/uploads/3fe1d1a2-b9fb-4216-aa8d-69e294a705b0.jpg b/support-portal-backend/uploads/3fe1d1a2-b9fb-4216-aa8d-69e294a705b0.jpg new file mode 100644 index 0000000..94d6b9a Binary files /dev/null and b/support-portal-backend/uploads/3fe1d1a2-b9fb-4216-aa8d-69e294a705b0.jpg differ diff --git a/support-portal-backend/uploads/49e557e1-906a-4778-b8ef-57f3d0f4c63b.png b/support-portal-backend/uploads/49e557e1-906a-4778-b8ef-57f3d0f4c63b.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/49e557e1-906a-4778-b8ef-57f3d0f4c63b.png differ diff --git a/support-portal-backend/uploads/4c0e03bc-2078-4318-a714-a39bcd9acbd8.webp b/support-portal-backend/uploads/4c0e03bc-2078-4318-a714-a39bcd9acbd8.webp new file mode 100644 index 0000000..c87f36b Binary files /dev/null and b/support-portal-backend/uploads/4c0e03bc-2078-4318-a714-a39bcd9acbd8.webp differ diff --git a/support-portal-backend/uploads/4f16a0cd-12a4-43e1-b347-a6356c7530f2.jpg b/support-portal-backend/uploads/4f16a0cd-12a4-43e1-b347-a6356c7530f2.jpg new file mode 100644 index 0000000..efdc286 Binary files /dev/null and b/support-portal-backend/uploads/4f16a0cd-12a4-43e1-b347-a6356c7530f2.jpg differ diff --git a/support-portal-backend/uploads/5c4cdfef-f3ea-4a81-9d2a-9dd2158e0fa3.png b/support-portal-backend/uploads/5c4cdfef-f3ea-4a81-9d2a-9dd2158e0fa3.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/5c4cdfef-f3ea-4a81-9d2a-9dd2158e0fa3.png differ diff --git a/support-portal-backend/uploads/5ecc85c5-44af-4f4c-bf23-de994c4a5529.png b/support-portal-backend/uploads/5ecc85c5-44af-4f4c-bf23-de994c4a5529.png new file mode 100644 index 0000000..b153a93 Binary files /dev/null and b/support-portal-backend/uploads/5ecc85c5-44af-4f4c-bf23-de994c4a5529.png differ diff --git a/support-portal-backend/uploads/65e7d6b4-6f31-455e-a58d-d407dc708a26.png b/support-portal-backend/uploads/65e7d6b4-6f31-455e-a58d-d407dc708a26.png new file mode 100644 index 0000000..b153a93 Binary files /dev/null and b/support-portal-backend/uploads/65e7d6b4-6f31-455e-a58d-d407dc708a26.png differ diff --git a/support-portal-backend/uploads/691c5c6e-8457-4e7a-a93e-4d178b391ec3.jpg b/support-portal-backend/uploads/691c5c6e-8457-4e7a-a93e-4d178b391ec3.jpg new file mode 100644 index 0000000..a18ff1b Binary files /dev/null and b/support-portal-backend/uploads/691c5c6e-8457-4e7a-a93e-4d178b391ec3.jpg differ diff --git a/support-portal-backend/uploads/6b58a0ab-495b-476a-abd2-da9e21dfa59e.jpg b/support-portal-backend/uploads/6b58a0ab-495b-476a-abd2-da9e21dfa59e.jpg new file mode 100644 index 0000000..b73cff8 Binary files /dev/null and b/support-portal-backend/uploads/6b58a0ab-495b-476a-abd2-da9e21dfa59e.jpg differ diff --git a/support-portal-backend/uploads/7d96e07e-038f-41b8-87d6-c20a5fb28727.jpg b/support-portal-backend/uploads/7d96e07e-038f-41b8-87d6-c20a5fb28727.jpg new file mode 100644 index 0000000..01d98d9 Binary files /dev/null and b/support-portal-backend/uploads/7d96e07e-038f-41b8-87d6-c20a5fb28727.jpg differ diff --git a/support-portal-backend/uploads/82672f77-820a-4775-b7ef-1defd679ba8a.png b/support-portal-backend/uploads/82672f77-820a-4775-b7ef-1defd679ba8a.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/82672f77-820a-4775-b7ef-1defd679ba8a.png differ diff --git a/support-portal-backend/uploads/85a8365d-eb6f-4b23-91ca-12aff565d5d6.jpg b/support-portal-backend/uploads/85a8365d-eb6f-4b23-91ca-12aff565d5d6.jpg new file mode 100644 index 0000000..efdc286 Binary files /dev/null and b/support-portal-backend/uploads/85a8365d-eb6f-4b23-91ca-12aff565d5d6.jpg differ diff --git a/support-portal-backend/uploads/89589782-53a8-488d-8cfb-d9acaa460966.jpg b/support-portal-backend/uploads/89589782-53a8-488d-8cfb-d9acaa460966.jpg new file mode 100644 index 0000000..efdc286 Binary files /dev/null and b/support-portal-backend/uploads/89589782-53a8-488d-8cfb-d9acaa460966.jpg differ diff --git a/support-portal-backend/uploads/916da266-632e-450e-9ff3-9d89ac841271.jpg b/support-portal-backend/uploads/916da266-632e-450e-9ff3-9d89ac841271.jpg new file mode 100644 index 0000000..110e59d Binary files /dev/null and b/support-portal-backend/uploads/916da266-632e-450e-9ff3-9d89ac841271.jpg differ diff --git a/support-portal-backend/uploads/9348f24f-ab84-4e87-a41a-3e9d07061467.jpg b/support-portal-backend/uploads/9348f24f-ab84-4e87-a41a-3e9d07061467.jpg new file mode 100644 index 0000000..b73cff8 Binary files /dev/null and b/support-portal-backend/uploads/9348f24f-ab84-4e87-a41a-3e9d07061467.jpg differ diff --git a/support-portal-backend/uploads/a45263d4-abcc-484d-9eb6-5bd25f25b3ba.png b/support-portal-backend/uploads/a45263d4-abcc-484d-9eb6-5bd25f25b3ba.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/a45263d4-abcc-484d-9eb6-5bd25f25b3ba.png differ diff --git a/support-portal-backend/uploads/a758790f-dac4-41d6-8bed-58fc6c90a588.jpg b/support-portal-backend/uploads/a758790f-dac4-41d6-8bed-58fc6c90a588.jpg new file mode 100644 index 0000000..01d98d9 Binary files /dev/null and b/support-portal-backend/uploads/a758790f-dac4-41d6-8bed-58fc6c90a588.jpg differ diff --git a/support-portal-backend/uploads/a806b651-75e1-495c-9091-48af4581b6ff.png b/support-portal-backend/uploads/a806b651-75e1-495c-9091-48af4581b6ff.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/a806b651-75e1-495c-9091-48af4581b6ff.png differ diff --git a/support-portal-backend/uploads/ab635afc-ead9-407c-9818-3703bcd89984.png b/support-portal-backend/uploads/ab635afc-ead9-407c-9818-3703bcd89984.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/ab635afc-ead9-407c-9818-3703bcd89984.png differ diff --git a/support-portal-backend/uploads/b211c6f3-2805-4456-99eb-716aa46e3b30.jpg b/support-portal-backend/uploads/b211c6f3-2805-4456-99eb-716aa46e3b30.jpg new file mode 100644 index 0000000..b73cff8 Binary files /dev/null and b/support-portal-backend/uploads/b211c6f3-2805-4456-99eb-716aa46e3b30.jpg differ diff --git a/support-portal-backend/uploads/bbd5c8c7-c93f-454f-b0c1-92004cb9df34.webp b/support-portal-backend/uploads/bbd5c8c7-c93f-454f-b0c1-92004cb9df34.webp new file mode 100644 index 0000000..c87f36b Binary files /dev/null and b/support-portal-backend/uploads/bbd5c8c7-c93f-454f-b0c1-92004cb9df34.webp differ diff --git a/support-portal-backend/uploads/c04f39b9-8467-416f-91d1-728fd341384e.jpg b/support-portal-backend/uploads/c04f39b9-8467-416f-91d1-728fd341384e.jpg new file mode 100644 index 0000000..a18ff1b Binary files /dev/null and b/support-portal-backend/uploads/c04f39b9-8467-416f-91d1-728fd341384e.jpg differ diff --git a/support-portal-backend/uploads/c07d46bb-b86d-4636-9b69-76a26d4b5ece.jpg b/support-portal-backend/uploads/c07d46bb-b86d-4636-9b69-76a26d4b5ece.jpg new file mode 100644 index 0000000..94d6b9a Binary files /dev/null and b/support-portal-backend/uploads/c07d46bb-b86d-4636-9b69-76a26d4b5ece.jpg differ diff --git a/support-portal-backend/uploads/c4582369-483d-457a-b3c7-2e9d1c4bd988.jpg b/support-portal-backend/uploads/c4582369-483d-457a-b3c7-2e9d1c4bd988.jpg new file mode 100644 index 0000000..a18ff1b Binary files /dev/null and b/support-portal-backend/uploads/c4582369-483d-457a-b3c7-2e9d1c4bd988.jpg differ diff --git a/support-portal-backend/uploads/ccc6acd4-2afa-4d4c-ae26-f6e9ef1c2f3e.jpg b/support-portal-backend/uploads/ccc6acd4-2afa-4d4c-ae26-f6e9ef1c2f3e.jpg new file mode 100644 index 0000000..b73cff8 Binary files /dev/null and b/support-portal-backend/uploads/ccc6acd4-2afa-4d4c-ae26-f6e9ef1c2f3e.jpg differ diff --git a/support-portal-backend/uploads/d8a817b5-7cbd-406b-b8b7-954f3d2e93ff.png b/support-portal-backend/uploads/d8a817b5-7cbd-406b-b8b7-954f3d2e93ff.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/d8a817b5-7cbd-406b-b8b7-954f3d2e93ff.png differ diff --git a/support-portal-backend/uploads/d92485b0-a9e8-4940-92f6-7f6ec291c519.webp b/support-portal-backend/uploads/d92485b0-a9e8-4940-92f6-7f6ec291c519.webp new file mode 100644 index 0000000..c87f36b Binary files /dev/null and b/support-portal-backend/uploads/d92485b0-a9e8-4940-92f6-7f6ec291c519.webp differ diff --git a/support-portal-backend/uploads/dc66cc05-ce67-431c-821d-2e4a0a4b6089.jpg b/support-portal-backend/uploads/dc66cc05-ce67-431c-821d-2e4a0a4b6089.jpg new file mode 100644 index 0000000..94d6b9a Binary files /dev/null and b/support-portal-backend/uploads/dc66cc05-ce67-431c-821d-2e4a0a4b6089.jpg differ diff --git a/support-portal-backend/uploads/dda0a875-9945-4c45-a33e-502d5b99446d.pdf b/support-portal-backend/uploads/dda0a875-9945-4c45-a33e-502d5b99446d.pdf new file mode 100644 index 0000000..94d9477 Binary files /dev/null and b/support-portal-backend/uploads/dda0a875-9945-4c45-a33e-502d5b99446d.pdf differ diff --git a/support-portal-backend/uploads/e1a83161-2cd2-423d-8996-e17f012fe2c3.png b/support-portal-backend/uploads/e1a83161-2cd2-423d-8996-e17f012fe2c3.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/e1a83161-2cd2-423d-8996-e17f012fe2c3.png differ diff --git a/support-portal-backend/uploads/e1e614df-df10-4c19-b64e-12b73a99a79f.jpg b/support-portal-backend/uploads/e1e614df-df10-4c19-b64e-12b73a99a79f.jpg new file mode 100644 index 0000000..94d6b9a Binary files /dev/null and b/support-portal-backend/uploads/e1e614df-df10-4c19-b64e-12b73a99a79f.jpg differ diff --git a/support-portal-backend/uploads/e8a1812b-6dd5-43f8-9cd5-17ac13de1385.png b/support-portal-backend/uploads/e8a1812b-6dd5-43f8-9cd5-17ac13de1385.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/e8a1812b-6dd5-43f8-9cd5-17ac13de1385.png differ diff --git a/support-portal-backend/uploads/ee28865e-0b29-4ed1-b054-a0c6752b3144.png b/support-portal-backend/uploads/ee28865e-0b29-4ed1-b054-a0c6752b3144.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/ee28865e-0b29-4ed1-b054-a0c6752b3144.png differ diff --git a/support-portal-backend/uploads/eee2cfda-27b2-4220-bbe5-f6c10f5cef7b.jpg b/support-portal-backend/uploads/eee2cfda-27b2-4220-bbe5-f6c10f5cef7b.jpg new file mode 100644 index 0000000..efdc286 Binary files /dev/null and b/support-portal-backend/uploads/eee2cfda-27b2-4220-bbe5-f6c10f5cef7b.jpg differ diff --git a/support-portal-backend/uploads/f3813f92-ac41-48e2-b823-5944344fe35a.webp b/support-portal-backend/uploads/f3813f92-ac41-48e2-b823-5944344fe35a.webp new file mode 100644 index 0000000..c87f36b Binary files /dev/null and b/support-portal-backend/uploads/f3813f92-ac41-48e2-b823-5944344fe35a.webp differ diff --git a/support-portal-backend/uploads/f50acd2d-134d-4230-8168-55edc5924db1.jpg b/support-portal-backend/uploads/f50acd2d-134d-4230-8168-55edc5924db1.jpg new file mode 100644 index 0000000..efdc286 Binary files /dev/null and b/support-portal-backend/uploads/f50acd2d-134d-4230-8168-55edc5924db1.jpg differ diff --git a/support-portal-backend/uploads/f55786d8-c0f0-49cc-818a-f305838aa641.jpg b/support-portal-backend/uploads/f55786d8-c0f0-49cc-818a-f305838aa641.jpg new file mode 100644 index 0000000..b73cff8 Binary files /dev/null and b/support-portal-backend/uploads/f55786d8-c0f0-49cc-818a-f305838aa641.jpg differ diff --git a/support-portal-backend/uploads/faf027cc-191a-40fe-bf53-cf6add21bf96.jpg b/support-portal-backend/uploads/faf027cc-191a-40fe-bf53-cf6add21bf96.jpg new file mode 100644 index 0000000..110e59d Binary files /dev/null and b/support-portal-backend/uploads/faf027cc-191a-40fe-bf53-cf6add21bf96.jpg differ diff --git a/support-portal-backend/uploads/fbb80a79-c934-4071-9cd9-4a643d26743f.png b/support-portal-backend/uploads/fbb80a79-c934-4071-9cd9-4a643d26743f.png new file mode 100644 index 0000000..ef6d72e Binary files /dev/null and b/support-portal-backend/uploads/fbb80a79-c934-4071-9cd9-4a643d26743f.png differ diff --git a/support-portal-backend/uploads/fca35b1d-bf20-4815-b6c1-9bc4a7c8d8ce.jpg b/support-portal-backend/uploads/fca35b1d-bf20-4815-b6c1-9bc4a7c8d8ce.jpg new file mode 100644 index 0000000..a18ff1b Binary files /dev/null and b/support-portal-backend/uploads/fca35b1d-bf20-4815-b6c1-9bc4a7c8d8ce.jpg differ diff --git a/support-portal-backend/uploads/fd34d61a-fa10-4413-8e3c-06175f547dba.png b/support-portal-backend/uploads/fd34d61a-fa10-4413-8e3c-06175f547dba.png new file mode 100644 index 0000000..0220a74 Binary files /dev/null and b/support-portal-backend/uploads/fd34d61a-fa10-4413-8e3c-06175f547dba.png differ diff --git a/support-portal-backend/uploads/fddf99c0-4865-4fc6-b0b8-a7edca16c03f.png b/support-portal-backend/uploads/fddf99c0-4865-4fc6-b0b8-a7edca16c03f.png new file mode 100644 index 0000000..ef6d72e Binary files /dev/null and b/support-portal-backend/uploads/fddf99c0-4865-4fc6-b0b8-a7edca16c03f.png differ diff --git a/support-portal-backend/uploads/fe3cb85e-5975-451d-b481-852cbcd43758.png b/support-portal-backend/uploads/fe3cb85e-5975-451d-b481-852cbcd43758.png new file mode 100644 index 0000000..ef6d72e Binary files /dev/null and b/support-portal-backend/uploads/fe3cb85e-5975-451d-b481-852cbcd43758.png differ diff --git a/support-portal-backend/uploads/ff1c21e3-7e8b-4340-94c6-b34b1959bb48.jpg b/support-portal-backend/uploads/ff1c21e3-7e8b-4340-94c6-b34b1959bb48.jpg new file mode 100644 index 0000000..94d6b9a Binary files /dev/null and b/support-portal-backend/uploads/ff1c21e3-7e8b-4340-94c6-b34b1959bb48.jpg differ diff --git a/support-portal-backend/uploads/ffc92332-747f-493c-bef5-2ab2cd130f94.png b/support-portal-backend/uploads/ffc92332-747f-493c-bef5-2ab2cd130f94.png new file mode 100644 index 0000000..b684972 Binary files /dev/null and b/support-portal-backend/uploads/ffc92332-747f-493c-bef5-2ab2cd130f94.png differ diff --git a/support-portal-frontend/package-lock.json b/support-portal-frontend/package-lock.json index fb963d1..1a25a6a 100644 --- a/support-portal-frontend/package-lock.json +++ b/support-portal-frontend/package-lock.json @@ -18,10 +18,8 @@ "@angular/router": "~12.2.0", "@auth0/angular-jwt": "^3.0.1", "@josipv/angular-editor-k2": "^2.20.0", - "@nicky-lenaers/ngx-scroll-to": "^9.0.0", - "angular-notifier": "^9.1.0", + "angular-notifier": "^10.0.0", "ng-particles": "^2.1.11", - "ngx-owl-carousel-o": "^5.0.0", "ngx-typed-js": "^2.0.2", "rxjs": "~6.6.0", "subsink": "^1.0.2", @@ -330,6 +328,30 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/schematics": { "version": "12.2.18", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.18.tgz", @@ -2643,20 +2665,6 @@ "webpack": "^5.30.0" } }, - "node_modules/@nicky-lenaers/ngx-scroll-to": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@nicky-lenaers/ngx-scroll-to/-/ngx-scroll-to-9.0.0.tgz", - "integrity": "sha512-eS0vyx8qX4UTMluRYc+sQF/vJHCnAKiufWrwQRme0VURwp+RdOoZDZpYrOPTxPfx6CVj72arTeV9auDYa0WKtA==", - "engines": { - "node": ">=8.0.0", - "npm": ">=6.0.0" - }, - "peerDependencies": { - "@angular/common": "^8.0.0 || ^9.0.0", - "@angular/core": "^8.0.0 || ^9.0.0", - "tslib": "^1.10.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3215,14 +3223,15 @@ } }, "node_modules/ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { @@ -3256,6 +3265,30 @@ } } }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -3266,22 +3299,23 @@ } }, "node_modules/angular-notifier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/angular-notifier/-/angular-notifier-9.1.0.tgz", - "integrity": "sha512-K8D8UljdC4P4TfjYB0v39Zs3WjgvPR7Vvp3yzGhcW4I8gXIqkz4xQSbqJbIZABCGEWdKqPyAN48wl7yg8Q3Urg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/angular-notifier/-/angular-notifier-10.0.0.tgz", + "integrity": "sha512-hVzFd41ZCT0O6EBlwN1cygl3qjXU4C41DVcaDrHK3CK5Y0JFbpFrJVuIAczKPjaul13rzJ/L7qzwobQHaTUwLw==", + "license": "MIT", "dependencies": { - "tslib": "2.0.x" + "tslib": "2.3.x" }, "peerDependencies": { - "@angular/common": ">= 11.0.0 < 12.0.0", - "@angular/core": ">= 11.0.0 < 12.0.0", - "rxjs": ">= 6.0.0 < 7.0.0" + "@angular/common": ">= 12.0.0 < 13.0.0", + "@angular/core": ">= 12.0.0 < 13.0.0" } }, "node_modules/angular-notifier/node_modules/tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "license": "0BSD" }, "node_modules/ansi-colors": { "version": "4.1.1", @@ -4637,28 +4671,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/copy-webpack-plugin/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5104,28 +5116,6 @@ } } }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/css-minimizer-webpack-plugin/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6653,6 +6643,23 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -8472,10 +8479,11 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -9402,28 +9410,6 @@ "webpack": "^5.0.0" } }, - "node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -9763,19 +9749,6 @@ "tsparticles": "^1.43.1" } }, - "node_modules/ngx-owl-carousel-o": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/ngx-owl-carousel-o/-/ngx-owl-carousel-o-5.1.1.tgz", - "integrity": "sha512-AmaU02UzONGrBSj4K28ZWEZxLYySyiC7Vefq4VacjAhYxUR+HA9jdQghG4zM+QlVPkmgFdZeepeqZ3/h9fU3ug==", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@angular/common": " ^11.0.0-rc.0 || ^11.0.0", - "@angular/core": "^11.0.0-rc.0 || ^11.0.0", - "rxjs": "^6.0.1" - } - }, "node_modules/ngx-typed-js": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ngx-typed-js/-/ngx-typed-js-2.1.1.tgz", @@ -13120,6 +13093,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13470,28 +13444,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14625,28 +14577,6 @@ "webpack": "^5.1.0" } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/terser-webpack-plugin/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -15303,28 +15233,6 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -15398,22 +15306,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/webpack-dev-server/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -15613,12 +15505,6 @@ "node": ">=0.10.0" } }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/webpack-dev-server/node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -15875,28 +15761,6 @@ } } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", diff --git a/support-portal-frontend/package.json b/support-portal-frontend/package.json index b40eb58..da873c0 100644 --- a/support-portal-frontend/package.json +++ b/support-portal-frontend/package.json @@ -20,10 +20,8 @@ "@angular/router": "~12.2.0", "@auth0/angular-jwt": "^3.0.1", "@josipv/angular-editor-k2": "^2.20.0", - "@nicky-lenaers/ngx-scroll-to": "^9.0.0", - "angular-notifier": "^9.1.0", + "angular-notifier": "^10.0.0", "ng-particles": "^2.1.11", - "ngx-owl-carousel-o": "^5.0.0", "ngx-typed-js": "^2.0.2", "rxjs": "~6.6.0", "subsink": "^1.0.2", diff --git a/support-portal-frontend/src/app/admin/admin-routing.module.ts b/support-portal-frontend/src/app/admin/admin-routing.module.ts index a1e5dbf..c72e30e 100644 --- a/support-portal-frontend/src/app/admin/admin-routing.module.ts +++ b/support-portal-frontend/src/app/admin/admin-routing.module.ts @@ -17,6 +17,8 @@ import { HomeComponent } from '../component/home/home.component'; import { EventComponent } from '../component/event/event.component'; import { BlogComponent } from '../component/blog/blog.component'; import { EventFormComponent } from '../component/event-form/event-form.component'; +import { CareerComponent } from '../component/career/career.component'; +import { EducationComponent } from '../component/education/education.component'; const routes: Routes = [ { path: '', component: HomeComponent }, @@ -30,6 +32,8 @@ const routes: Routes = [ { path: 'eventForm', component: EventFormComponent, canActivate: [AuthenticationGuard] }, { path: 'eventForm/:id', component: EventFormComponent, canActivate: [AuthenticationGuard] }, { path: 'blogs', component: BlogComponent, canActivate: [AuthenticationGuard] }, + { path: 'career', component: CareerComponent, canActivate: [AuthenticationGuard] }, + { path: 'education', component: EducationComponent, canActivate: [AuthenticationGuard] }, { path: 'userManagement', component: UserComponent, canActivate: [AuthenticationGuard] }, { path: 'professorManagement', component: ProfessorComponent, canActivate: [AuthenticationGuard] }, { diff --git a/support-portal-frontend/src/app/admin/admin.module.ts b/support-portal-frontend/src/app/admin/admin.module.ts index 9af6280..2a78db2 100644 --- a/support-portal-frontend/src/app/admin/admin.module.ts +++ b/support-portal-frontend/src/app/admin/admin.module.ts @@ -31,6 +31,9 @@ import { NotificationModule } from '../notification/notification.module'; import { EventFormComponent } from '../component/event-form/event-form.component'; import { CommonModule } from '@angular/common'; import { AdminRoutingModule } from './admin-routing.module'; +import { CareerComponent } from '../component/career/career.component'; +import { EducationComponent } from '../component/education/education.component'; +import { CareerService } from '../service/career.service'; // import { PagesModule } from '../pages/pages.module'; @@ -55,8 +58,9 @@ import { AdminRoutingModule } from './admin-routing.module'; HomeComponent, BlogComponent, EventComponent, - EventFormComponent - + EventFormComponent, + CareerComponent, + EducationComponent ], imports: [ CommonModule, diff --git a/support-portal-frontend/src/app/app-routing.module.ts b/support-portal-frontend/src/app/app-routing.module.ts index 4a3e5f9..81208e6 100644 --- a/support-portal-frontend/src/app/app-routing.module.ts +++ b/support-portal-frontend/src/app/app-routing.module.ts @@ -17,84 +17,100 @@ import { EventComponent } from './component/event/event.component'; import { BlogComponent } from './component/blog/blog.component'; import { EventFormComponent } from './component/event-form/event-form.component'; -export const routes: Routes = [ - // { - // path: '', - // loadChildren: () => - // import('./pages/pages.module').then((m) => m.PagesModule), - // }, +export const routes: Routes = [ + // Dashboard/Admin routes with lazy loading { path: 'dashboard', loadChildren: () => import('./admin/admin.module').then((m) => m.AdminModule), }, + // Main application routes + { path: 'home', component: HomeComponent }, + { path: 'login', component: LoginComponent }, + { path: 'register', component: RegisterComponent }, + + // Event routes - UNCOMMENTED AND FIXED + { + path: 'events', + component: EventComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'eventForm', + component: EventFormComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'eventForm/:id', + component: EventFormComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'dashboard/eventForm', + component: EventFormComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'dashboard/eventForm/:id', + component: EventFormComponent, + canActivate: [AuthenticationGuard], + }, + + // Other routes + { + path: 'settings', + component: SettingsComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'profile', + component: ProfileComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'blogs', + component: BlogComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'user/management', + component: UserComponent, + canActivate: [AuthenticationGuard], + }, + { + path: 'professor/management', + component: ProfessorComponent, + canActivate: [AuthenticationGuard], + }, + + // Management routes with children + { + path: 'management', + component: ManagementComponent, + canActivate: [AuthenticationGuard], + children: [ + {path: 'settings', component: SettingsComponent}, + {path: 'profile', component: ProfileComponent}, + { + path: 'users', + component: UsersComponent, + children: [ + {path: ':id/view', component: UserViewComponent, resolve: {user: UserResolver}}, + {path: ':id/edit', component: UserEditComponent} + ] + } + ] + }, + + // Default redirects { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, - - // { path: 'home', component: HomeComponent }, - // { path: 'login', component: LoginComponent }, - // { path: 'register', component: RegisterComponent }, - // { - // path: 'settings', - // component: SettingsComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'profile', - // component: ProfileComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'events', - // component: EventComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'eventForm', - // component: EventFormComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'eventForm/:id', - // component: EventFormComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'blogs', - // component: BlogComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'user/management', - // component: UserComponent, - // canActivate: [AuthenticationGuard], - // }, - // { - // path: 'professor/management', - // component: ProfessorComponent, - // canActivate: [AuthenticationGuard], - // }, - - // { - // path: 'management', component: ManagementComponent, canActivate: [AuthenticationGuard], - // children: [ - // {path: 'settings', component: SettingsComponent}, - // {path: 'profile', component: ProfileComponent}, - // { - // path: 'users', component: UsersComponent, - // children: [ - // {path: ':id/view', component: UserViewComponent, resolve: {user: UserResolver}}, - // {path: ':id/edit', component: UserEditComponent} - // ] - // } - // ] - // }, - // { path: '', redirectTo: '/login', pathMatch: 'full' }, + { path: '**', redirectTo: '/dashboard' }, // Wildcard route for 404 - redirects to dashboard ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) -export class AppRoutingModule {} +export class AppRoutingModule {} \ No newline at end of file diff --git a/support-portal-frontend/src/app/app.module.ts b/support-portal-frontend/src/app/app.module.ts index b0fdc76..d7e8ac8 100644 --- a/support-portal-frontend/src/app/app.module.ts +++ b/support-portal-frontend/src/app/app.module.ts @@ -1,5 +1,6 @@ import {Compiler, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; + import { HashLocationStrategy, LocationStrategy } from '@angular/common'; @@ -30,6 +31,8 @@ import { BlogService } from './service/blog.service'; import { AngularEditorModule } from '@josipv/angular-editor-k2'; import { NotificationModule } from './notification/notification.module'; import { EventFormComponent } from './component/event-form/event-form.component'; +import { CareerComponent } from './component/career/career.component'; +import { EducationComponent } from './component/education/education.component'; // import { PagesModule } from './pages/pages.module'; @@ -39,6 +42,8 @@ import { EventFormComponent } from './component/event-form/event-form.component' @NgModule({ declarations: [ AppComponent, + // EducationComponent, + // CareerComponent, // LoginComponent, // RegisterComponent, // UserComponent, diff --git a/support-portal-frontend/src/app/component/blog/blog.component.html b/support-portal-frontend/src/app/component/blog/blog.component.html index ada913e..a55a7e5 100644 --- a/support-portal-frontend/src/app/component/blog/blog.component.html +++ b/support-portal-frontend/src/app/component/blog/blog.component.html @@ -11,13 +11,15 @@
- +
- +
Title is required.
@@ -26,34 +28,62 @@
-
- At least one professor must be selected. +
+ At least one professor must be selected.
-
- -
+
+ +
- +
- Tags are required. + Tags are required.
-
- -
- - -
- +
+ +
+ + + + +
+ Image preview + +
+ + +
+
+
+ Uploading... +
+
+
+
+ +
+ + +
+
- -
+ +
Content is required.
@@ -69,50 +99,53 @@
- -
-
- - - - - - - - - - - - - - - - - - - - - -
TitleAuthorsTagsPosted Actions
{{ blog.title }} - {{ professor.firstName }}
-
- {{ tag }}
-
- - - - - -
- -
-
- No blogs available. Please create a new blog. -
-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
TitleAuthorsTagsPosted Actions
{{ blog.title }} + {{ professor.firstName }}
+
+ {{ tag }}
+
+ + + + + + + +
-
+
+
+ No blogs available. Please create a new blog. +
+
+ +
\ No newline at end of file diff --git a/support-portal-frontend/src/app/component/blog/blog.component.ts b/support-portal-frontend/src/app/component/blog/blog.component.ts index 31c5bbe..5bc46a9 100644 --- a/support-portal-frontend/src/app/component/blog/blog.component.ts +++ b/support-portal-frontend/src/app/component/blog/blog.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AngularEditorConfig } from '@josipv/angular-editor-k2'; import { AuthenticationService } from 'src/app/service/authentication.service'; -; import { BlogService } from 'src/app/service/blog.service'; import { ProfessorService } from 'src/app/service/professor.service'; @@ -18,9 +17,14 @@ export class BlogComponent implements OnInit { blogForm: FormGroup; editing: boolean = false; loggedInUser: any; - currentBlog: any = null; // Holds the blog being edited - isShowForm = false; // Controls visibility of form vs. table + currentBlog: any = null; + isShowForm = false; content = ''; + + // Image upload properties + selectedImage: File | null = null; + imagePreviewUrl: string | null = null; + uploadingImage: boolean = false; constructor( private blogService: BlogService, @@ -32,9 +36,9 @@ export class BlogComponent implements OnInit { this.blogForm = this.fb.group({ title: ['', Validators.required], content: ['', Validators.required], - professors: [[], Validators.required], // To hold selected professor IDs - tags: ['', Validators.required], // To hold tags as a comma-separated string - posted: [true], // Initialize checkbox with default value false + professors: [[], Validators.required], + tags: ['', Validators.required], + posted: [true], }); } @@ -53,9 +57,6 @@ export class BlogComponent implements OnInit { defaultParagraphSeparator: '', defaultFontName: '', defaultFontSize: '', - // headers: [{ - - // }], fonts: [ { class: 'arial', name: 'Arial' }, { class: 'times-new-roman', name: 'Times New Roman' }, @@ -77,111 +78,236 @@ export class BlogComponent implements OnInit { tag: 'h1', }, ], - // uploadUrl: 'v1/image', - // upload: (file: File) => { ... } - // uploadWithCredentials: false, sanitize: true, toolbarPosition: 'top', toolbarHiddenButtons: [['bold', 'italic'], ['fontSize']], }; - + ngOnInit(): void { + this.loggedInUser = this.authService.getUserFromLocalStorage(); + this.professorService + .getAllProfessors() + .subscribe((res) => (this.allProfessors = res.content)); - ngOnInit(): void { - this.loggedInUser = this.authService.getUserFromLocalStorage(); - this.professorService - .getAllProfessors() - .subscribe((res) => (this.allProfessors = res.content)); + this.loadBlogs(); + + // Subscribe to form value changes to update content for preview + this.blogForm.get('content')?.valueChanges.subscribe((value) => { + this.content = value; + }); + } - this.loadBlogs(); - // Subscribe to form value changes to update content for preview - this.blogForm.get('content')?.valueChanges.subscribe((value) => { - this.content = value; - }); - } + showForm() { + this.isShowForm = true; + this.resetForm(); + } - showForm() { - this.isShowForm = true; - this.resetForm(); // Ensure form is reset when showing + showTable() { + this.isShowForm = false; + this.resetForm(); + } + + loadBlogs() { + this.blogService.getBlogs().subscribe(data => { + this.blogs = data; + }); + } + + createBlog() { + this.showForm(); + this.blogForm.reset(); + this.selectedBlog = null; + this.editing = false; + } + + editBlog(blog: any) { + this.selectedBlog = blog; + + this.blogForm.patchValue({ + title: blog.title, + content: blog.content, + posted: blog.posted, + professors: blog.professors.map((prof: any) => prof.id), + tags: blog.tags.join(', ') + }); + + // Set image preview if exists + if (blog.imageUrl) { + this.imagePreviewUrl = blog.imageUrl; } - - showTable() { - this.isShowForm = false; - this.resetForm(); // Ensure form is reset when switching back - } - - loadBlogs() { - this.blogService.getBlogs().subscribe(data => { - this.blogs = data; - }); - } - - createBlog() { - this.showForm(); // Show form to create a new blog - this.blogForm.reset(); - this.selectedBlog = null; - this.editing = false; - } - - editBlog(blog: any) { - - this.selectedBlog = blog; - - this.blogForm.patchValue({ - title: blog.title, - content: blog.content, - posted: blog.posted, - professors: blog.professors.map((prof: any) => prof.id), // Assuming professors are an array of objects - tags: blog.tags.join(', ') // Convert tags array back to comma-separated string - }); - this.editing = true; - this.isShowForm = true; + + this.editing = true; + this.isShowForm = true; + } + + // Image upload methods + onImageSelected(event: any) { + const file = event.target.files[0]; + if (file) { + // Validate file type + if (!file.type.startsWith('image/')) { + alert('Please select a valid image file.'); + return; + } + + // Validate file size (10MB max) + const maxSize = 10 * 1024 * 1024; // 10MB in bytes + if (file.size > maxSize) { + alert('File size must be less than 10MB.'); + return; + } + + this.selectedImage = file; + // Create preview URL + const reader = new FileReader(); + reader.onload = (e: any) => { + this.imagePreviewUrl = e.target.result; + }; + reader.readAsDataURL(file); } - - saveBlog() { - if (this.blogForm.valid) { - const blogData = this.blogForm.value; - - // Convert tags to array and professors to array of IDs - blogData.tags = blogData.tags.split(',').map((tag: string) => tag.trim()); - blogData.professors = blogData.professors || []; - - blogData.author = this.loggedInUser._id; // Associate logged-in user with the blog - - if (this.editing && this.selectedBlog) { - this.blogService.updateBlog(this.selectedBlog.id, blogData).subscribe(() => { - this.loadBlogs(); - this.resetForm(); - this.isShowForm = false; // Hide form after update - }); - } else { - this.blogService.createBlog(blogData).subscribe(() => { - this.loadBlogs(); - this.resetForm(); - this.isShowForm = false; // Hide form after creation - }); + } + + removeImage() { + this.selectedImage = null; + this.imagePreviewUrl = null; + + // Clear the file input + const fileInput = document.getElementById('image') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + async uploadImage(): Promise { + if (!this.selectedImage) return null; + + this.uploadingImage = true; + try { + const response = await this.blogService.uploadImage(this.selectedImage).toPromise(); + return response.url; + } catch (error) { + console.error('Error uploading image:', error); + alert('Failed to upload image. Please try again.'); + return null; + } finally { + this.uploadingImage = false; + } + } + + async saveBlog() { + if (this.blogForm.valid) { + let imageUrl = null; + + // Upload image if selected + if (this.selectedImage) { + imageUrl = await this.uploadImage(); + if (!imageUrl) { + return; // Stop if image upload failed } } - } - - deleteBlog(blog: any) { - if (confirm('Are you sure you want to delete this blog?')) { - this.blogService.deleteBlog(blog.id).subscribe(() => { - this.loadBlogs(); - }); + + const blogData = this.blogForm.value; + + // Convert tags to array and professors to array of IDs + blogData.tags = blogData.tags.split(',').map((tag: string) => tag.trim()); + blogData.professors = blogData.professors || []; + + // Add image URL if uploaded, or keep existing image for updates + if (imageUrl) { + blogData.imageUrl = imageUrl; + } else if (this.editing && this.selectedBlog && this.selectedBlog.imageUrl && this.imagePreviewUrl) { + // Keep existing image URL if editing and no new image selected + blogData.imageUrl = this.selectedBlog.imageUrl; } + blogData.author = this.loggedInUser._id; + + if (this.editing && this.selectedBlog) { + this.blogService.updateBlog(this.selectedBlog.id, blogData).subscribe({ + next: () => { + this.loadBlogs(); + this.resetForm(); + this.isShowForm = false; + alert('Blog updated successfully!'); + }, + error: (error) => { + console.error('Error updating blog:', error); + alert('Failed to update blog. Please try again.'); + } + }); + } else { + this.blogService.createBlog(blogData).subscribe({ + next: () => { + this.loadBlogs(); + this.resetForm(); + this.isShowForm = false; + alert('Blog created successfully!'); + }, + error: (error) => { + console.error('Error creating blog:', error); + alert('Failed to create blog. Please try again.'); + } + }); + } + } else { + // Mark all fields as touched to show validation errors + Object.keys(this.blogForm.controls).forEach(key => { + this.blogForm.get(key)?.markAsTouched(); + }); + alert('Please fill in all required fields.'); } - - resetForm() { - this.blogForm.reset(); - this.selectedBlog = null; - this.editing = false; - this.blogForm.patchValue({ - professors: [], - tags: '' + } + + deleteBlog(blog: any) { + if (confirm('Are you sure you want to delete this blog?')) { + this.blogService.deleteBlog(blog.id).subscribe({ + next: () => { + this.loadBlogs(); + alert('Blog deleted successfully!'); + }, + error: (error) => { + console.error('Error deleting blog:', error); + alert('Failed to delete blog. Please try again.'); + } }); } } - \ No newline at end of file + + resetForm() { + this.blogForm.reset(); + this.selectedBlog = null; + this.editing = false; + this.selectedImage = null; + this.imagePreviewUrl = null; + + this.blogForm.patchValue({ + professors: [], + tags: '', + posted: true + }); + + // Clear the file input + const fileInput = document.getElementById('image') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + // Utility method to check if form field has error + hasError(fieldName: string): boolean { + const field = this.blogForm.get(fieldName); + return !!(field && field.invalid && field.touched); + } + + // Utility method to get error message for a field + getErrorMessage(fieldName: string): string { + const field = this.blogForm.get(fieldName); + if (field && field.errors && field.touched) { + if (field.errors['required']) { + return `${fieldName} is required.`; + } + } + return ''; + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/career/career.component.css b/support-portal-frontend/src/app/component/career/career.component.css new file mode 100644 index 0000000..e69de29 diff --git a/support-portal-frontend/src/app/component/career/career.component.html b/support-portal-frontend/src/app/component/career/career.component.html new file mode 100644 index 0000000..aa62725 --- /dev/null +++ b/support-portal-frontend/src/app/component/career/career.component.html @@ -0,0 +1,248 @@ + +
+ +
+
+ + + +
+
+ Total Jobs: {{ jobs.length }} + Total Applications: {{ applications.length }} +
+
+ + +
+
+
{{ editing ? 'Edit Job' : 'Create New Job' }}
+
+
+
+
+
+
+ + +
+ Job title is required. +
+
+
+
+
+ + +
+ Department is required. +
+
+
+
+ +
+
+
+ + +
+ Location is required. +
+
+
+
+
+ + +
+ Job type is required. +
+
+
+
+ +
+
+
+ + +
+ Experience requirement is required. +
+
+
+
+
+ + +
+ Salary information is required. +
+
+
+
+ +
+ + +
+ Job description is required. +
+
+ +
+ + + Separate multiple requirements with commas +
+ +
+ + + Separate multiple responsibilities with commas +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
Applications for: {{ selectedJobForApplications.title }}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ApplicantEmailPhoneExperienceStatusApplied DateActions
{{ application.fullName }}{{ application.email }}{{ application.phone }}{{ application.experience }} + + {{ application.status || 'PENDING' }} + + {{ application.createdDate | date:'short' }} +
+ + +
+ +
+
+
+ No applications found for this job. +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
TitleDepartmentLocationTypeStatusApplicationsActions
+ {{ job.title }} +
+ {{ job.experience }} +
{{ job.department }}{{ job.location }} + {{ job.type }} + + Active + Inactive + + + + + +
+
+
+ No jobs available. Please create a new job. +
+
+
\ No newline at end of file diff --git a/support-portal-frontend/src/app/component/career/career.component.spec.ts b/support-portal-frontend/src/app/component/career/career.component.spec.ts new file mode 100644 index 0000000..16e3f6c --- /dev/null +++ b/support-portal-frontend/src/app/component/career/career.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CareerComponent } from './career.component'; + +describe('CareerComponent', () => { + let component: CareerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CareerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CareerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/support-portal-frontend/src/app/component/career/career.component.ts b/support-portal-frontend/src/app/component/career/career.component.ts new file mode 100644 index 0000000..c2f83be --- /dev/null +++ b/support-portal-frontend/src/app/component/career/career.component.ts @@ -0,0 +1,205 @@ +// career.component.ts +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { CareerService, Job, JobApplication } from 'src/app/service/career.service'; + +@Component({ + selector: 'app-career', + templateUrl: './career.component.html', + styleUrls: ['./career.component.css'], +}) +export class CareerComponent implements OnInit { + jobs: Job[] = []; + applications: JobApplication[] = []; + selectedJob: Job | null = null; + jobForm: FormGroup; + showJobForm = false; + editing = false; + showApplications = false; + + // Filter for applications + selectedJobForApplications: Job | null = null; + + constructor( + private careerService: CareerService, + private fb: FormBuilder + ) { + this.jobForm = this.fb.group({ + title: ['', Validators.required], + department: ['', Validators.required], + location: ['', Validators.required], + type: ['', Validators.required], + experience: ['', Validators.required], + salary: ['', Validators.required], + description: ['', Validators.required], + requirements: [''], + responsibilities: [''], + isActive: [true] // Default to true + }); + } + + ngOnInit(): void { + this.loadJobs(); + this.loadApplications(); + } + + loadJobs() { + this.careerService.getAllJobs().subscribe(data => { + this.jobs = data; + }); + } + + loadApplications() { + this.careerService.getAllApplications().subscribe(data => { + this.applications = data; + }); + } + + showJobFormModal() { + this.showJobForm = true; + this.resetJobForm(); + + // Ensure isActive is true for new jobs + setTimeout(() => { + this.jobForm.patchValue({ + isActive: true + }); + }, 0); + } + + hideJobForm() { + this.showJobForm = false; + this.resetJobForm(); + } + + editJob(job: Job) { + this.selectedJob = job; + this.jobForm.patchValue({ + title: job.title, + department: job.department, + location: job.location, + type: job.type, + experience: job.experience, + salary: job.salary, + description: job.description, + requirements: job.requirements ? job.requirements.join(', ') : '', + responsibilities: job.responsibilities ? job.responsibilities.join(', ') : '', + isActive: job.isActive // This will use the actual job's active status + }); + this.editing = true; + this.showJobForm = true; + } + + saveJob() { + if (this.jobForm.valid) { + const jobData = this.jobForm.value; + + console.log('=== ANGULAR DEBUG ==='); + console.log('Form value:', this.jobForm.value); + console.log('isActive form control value:', this.jobForm.get('isActive')?.value); + console.log('isActive in jobData:', jobData.isActive); + console.log('Type of isActive:', typeof jobData.isActive); + + // Ensure isActive is properly set as boolean + jobData.isActive = this.jobForm.get('isActive')?.value === true; + + console.log('isActive after boolean conversion:', jobData.isActive); + + // Convert comma-separated strings to arrays + jobData.requirements = jobData.requirements + ? jobData.requirements.split(',').map((req: string) => req.trim()).filter((req: string) => req.length > 0) + : []; + jobData.responsibilities = jobData.responsibilities + ? jobData.responsibilities.split(',').map((resp: string) => resp.trim()).filter((resp: string) => resp.length > 0) + : []; + + console.log('Final jobData being sent:', jobData); + + if (this.editing && this.selectedJob) { + this.careerService.updateJob(this.selectedJob.id!, jobData).subscribe(() => { + this.loadJobs(); + this.hideJobForm(); + }); + } else { + this.careerService.createJob(jobData).subscribe(() => { + this.loadJobs(); + this.hideJobForm(); + }); + } + } + } + + deleteJob(job: Job) { + if (confirm('Are you sure you want to delete this job?')) { + this.careerService.deleteJob(job.id!).subscribe(() => { + this.loadJobs(); + }); + } + } + + resetJobForm() { + this.jobForm.reset(); + this.selectedJob = null; + this.editing = false; + + // Explicitly set default values after reset + this.jobForm.patchValue({ + isActive: true + }); + } + + // Application management + viewApplications(job: Job) { + this.selectedJobForApplications = job; + this.showApplications = true; + this.careerService.getApplicationsByJobId(job.id!).subscribe(data => { + this.applications = data; + }); + } + + hideApplications() { + this.showApplications = false; + this.selectedJobForApplications = null; + this.loadApplications(); // Load all applications + } + + updateApplicationStatus(application: JobApplication, status: string) { + this.careerService.updateApplicationStatus(application.id!, status).subscribe(() => { + // Reload applications for the selected job + if (this.selectedJobForApplications) { + this.viewApplications(this.selectedJobForApplications); + } else { + this.loadApplications(); + } + }); + } + + deleteApplication(application: JobApplication) { + if (confirm('Are you sure you want to delete this application?')) { + this.careerService.deleteApplication(application.id!).subscribe(() => { + if (this.selectedJobForApplications) { + this.viewApplications(this.selectedJobForApplications); + } else { + this.loadApplications(); + } + }); + } + } + + getStatusBadgeClass(status: string): string { + switch (status?.toLowerCase()) { + case 'pending': return 'badge-warning'; + case 'reviewed': return 'badge-info'; + case 'shortlisted': return 'badge-primary'; + case 'interviewed': return 'badge-secondary'; + case 'hired': return 'badge-success'; + case 'rejected': return 'badge-danger'; + default: return 'badge-light'; + } + } + + getApplicationCount(jobId?: number): number { + if (!jobId) return 0; + return this.applications.filter(app => app.job?.id === jobId).length; + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/education/education.component.css b/support-portal-frontend/src/app/component/education/education.component.css new file mode 100644 index 0000000..e69de29 diff --git a/support-portal-frontend/src/app/component/education/education.component.html b/support-portal-frontend/src/app/component/education/education.component.html new file mode 100644 index 0000000..1eed965 --- /dev/null +++ b/support-portal-frontend/src/app/component/education/education.component.html @@ -0,0 +1,481 @@ +
+ +
+
+

Education & Training Management

+

Manage courses, programs, upcoming events and applications

+
+
+ + +
+
+ + + + + + + + +
+
+ Total Courses: {{ courses.length }} + Total Applications: {{ applications.length }} + Upcoming Events: {{ upcomingEvents.length }} +
+
+ + +
+
+
{{ editingUpcomingEvent ? 'Edit Upcoming Event' : 'Create New Upcoming Event' }}
+
+
+
+
+ + +
+ Event title is required. +
+
+ +
+ + +
+ Event description is required. +
+
+ +
+
+
+ + +
+ Schedule is required. +
+
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
+
Upcoming Events Management
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
TitleDescriptionScheduleEvent DateStatusActions
+ {{ event.title }} + + + {{ event.description.length > 100 ? (event.description | slice:0:100) + '...' : + event.description }} + + {{ event.schedule }}{{ event.eventDate ? (event.eventDate | date:'short') : 'N/A' }} + Active + Inactive + + + +
+
+
+ No upcoming events available. Please create a new event. +
+
+
+
+ + +
+
+
{{ editing ? 'Edit Course' : 'Create New Course' }}
+
+
+
+
+
+
+ + +
+ Course title is required. +
+
+
+
+
+ + +
+ Instructor is required. +
+
+
+
+ +
+ + +
+ Course description is required. +
+
+ +
+
+
+ + +
+ Duration is required. +
+
+
+
+
+ + +
+ Number of seats is required and must be at least 1. +
+
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+ Category is required. +
+
+
+
+
+ + +
+ Level is required. +
+
+
+
+
+ + +
+
+
+ + +
+ + + +
+
+ Preview + +
+
+ + +
+ + Supported formats: JPG, PNG, GIF. Max size: 5MB +
+ + +
+ {{ uploadError }} +
+ + +
+
+
+ Uploading image... +
+
+
+ +
+ + + Separate multiple criteria with commas +
+ +
+ + + Separate multiple objectives with commas +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
Applications for: {{ selectedCourseForApplications.title }}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ApplicantEmailPhoneQualificationExperienceStatusApplied DateActions
{{ application.fullName }}{{ application.email }}{{ application.phone }}{{ application.qualification }}{{ application.experience || 'N/A' }} + + {{ application.status || 'PENDING' }} + + {{ application.createdDate | date:'short' }} + + +
+
+
+ No applications found for this course. +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ImageCourseCategoryDurationSeatsStatusApplicationsActions
+ {{ course.title }} + No image + + {{ course.title }} +
+ {{ course.instructor }} +
+ {{ course.level }} +
+ {{ course.category }} + {{ course.duration }}{{ course.seats }} + Active + Inactive + + + + + +
+
+
+ No courses available. Please create a new course. +
+
+
\ No newline at end of file diff --git a/support-portal-frontend/src/app/component/education/education.component.spec.ts b/support-portal-frontend/src/app/component/education/education.component.spec.ts new file mode 100644 index 0000000..c9a8692 --- /dev/null +++ b/support-portal-frontend/src/app/component/education/education.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EducationComponent } from './education.component'; + +describe('EducationComponent', () => { + let component: EducationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EducationComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EducationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/support-portal-frontend/src/app/component/education/education.component.ts b/support-portal-frontend/src/app/component/education/education.component.ts new file mode 100644 index 0000000..9351ac1 --- /dev/null +++ b/support-portal-frontend/src/app/component/education/education.component.ts @@ -0,0 +1,385 @@ +// education.component.ts +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EducationService, Course, CourseApplication } from '../../service/education.service'; +import { UpcomingEventsService, UpcomingEvent } from '../../service/upcoming-events.service'; +import { FileUploadService } from '../../service/file-upload.service'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'app-education', + templateUrl: './education.component.html', + styleUrls: ['./education.component.css'], +}) +export class EducationComponent implements OnInit { + courses: Course[] = []; + applications: CourseApplication[] = []; + upcomingEvents: UpcomingEvent[] = []; + selectedCourse: Course | null = null; + selectedUpcomingEvent: UpcomingEvent | null = null; + courseForm: FormGroup; + upcomingEventForm: FormGroup; + showCourseForm = false; + showUpcomingEventForm = false; + editing = false; + editingUpcomingEvent = false; + showApplications = false; + showUpcomingEvents = false; + + // Filter for applications + selectedCourseForApplications: Course | null = null; + + // Image upload properties + selectedImage: File | null = null; + imagePreview: string | null = null; + isImageUploading = false; + uploadError: string | null = null; + + constructor( + private educationService: EducationService, + private upcomingEventsService: UpcomingEventsService, + private fileUploadService: FileUploadService, + private fb: FormBuilder + ) { + this.courseForm = this.fb.group({ + title: ['', Validators.required], + description: ['', Validators.required], + duration: ['', Validators.required], + seats: ['', [Validators.required, Validators.min(1)]], + category: ['', Validators.required], + level: ['', Validators.required], + instructor: ['', Validators.required], + price: [''], + startDate: [''], + eligibility: [''], + objectives: [''], + imageUrl: [''], + isActive: [true] + }); + this.upcomingEventForm = this.fb.group({ + title: ['', Validators.required], + description: ['', Validators.required], + schedule: ['', Validators.required], + eventDate: [''], + isActive: [true] + }); + } + + ngOnInit(): void { + this.loadCourses(); + this.loadApplications(); + this.loadUpcomingEvents(); + } + + loadCourses() { + this.educationService.getAllCourses().subscribe(data => { + this.courses = data; + }); + } + + loadApplications() { + this.educationService.getAllApplications().subscribe(data => { + this.applications = data; + }); + } + + getFullImageUrl(imageUrl: string): string { + if (!imageUrl) return ''; + + // If it's already a full URL, return as-is + if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { + return imageUrl; + } + + // Otherwise, prepend your API base URL + return environment.apiUrl + imageUrl; +} + loadUpcomingEvents() { + this.upcomingEventsService.getAllUpcomingEvents().subscribe(data => { + this.upcomingEvents = data; + }); + } + + // Upcoming Events Management + showUpcomingEventsModal() { + this.showUpcomingEvents = true; + } + + hideUpcomingEventsModal() { + this.showUpcomingEvents = false; + this.hideUpcomingEventForm(); + } + + showUpcomingEventFormModal() { + this.showUpcomingEventForm = true; + this.resetUpcomingEventForm(); + + setTimeout(() => { + this.upcomingEventForm.patchValue({ + isActive: true + }); + }, 0); + } + + hideUpcomingEventForm() { + this.showUpcomingEventForm = false; + this.resetUpcomingEventForm(); + } + + editUpcomingEvent(event: UpcomingEvent) { + this.selectedUpcomingEvent = event; + this.upcomingEventForm.patchValue({ + title: event.title, + description: event.description, + schedule: event.schedule, + eventDate: event.eventDate || '', + isActive: event.isActive + }); + this.editingUpcomingEvent = true; + this.showUpcomingEventForm = true; + } + + saveUpcomingEvent() { + if (this.upcomingEventForm.valid) { + const eventData = this.upcomingEventForm.value; + eventData.isActive = this.upcomingEventForm.get('isActive')?.value === true; + + if (this.editingUpcomingEvent && this.selectedUpcomingEvent) { + this.upcomingEventsService.updateUpcomingEvent(this.selectedUpcomingEvent.id!, eventData).subscribe(() => { + this.loadUpcomingEvents(); + this.hideUpcomingEventForm(); + }); + } else { + this.upcomingEventsService.createUpcomingEvent(eventData).subscribe(() => { + this.loadUpcomingEvents(); + this.hideUpcomingEventForm(); + }); + } + } + } + + deleteUpcomingEvent(event: UpcomingEvent) { + if (confirm('Are you sure you want to delete this upcoming event?')) { + this.upcomingEventsService.deleteUpcomingEvent(event.id!).subscribe(() => { + this.loadUpcomingEvents(); + }); + } + } + + resetUpcomingEventForm() { + this.upcomingEventForm.reset(); + this.selectedUpcomingEvent = null; + this.editingUpcomingEvent = false; + this.upcomingEventForm.patchValue({ + isActive: true + }); + } + + showCourseFormModal() { + this.showCourseForm = true; + this.resetCourseForm(); + + setTimeout(() => { + this.courseForm.patchValue({ + isActive: true + }); + }, 0); + } + + hideCourseForm() { + this.showCourseForm = false; + this.resetCourseForm(); + } + + editCourse(course: Course) { + this.selectedCourse = course; + this.courseForm.patchValue({ + title: course.title, + description: course.description, + duration: course.duration, + seats: course.seats, + category: course.category, + level: course.level, + instructor: course.instructor, + price: course.price || '', + startDate: course.startDate || '', + eligibility: course.eligibility ? course.eligibility.join(', ') : '', + objectives: course.objectives ? course.objectives.join(', ') : '', + imageUrl: course.imageUrl || '', + isActive: course.isActive + }); + + if (course.imageUrl) { + this.imagePreview = course.imageUrl; + } + + this.editing = true; + this.showCourseForm = true; + } + + // Image handling methods + onImageSelected(event: any) { + const file = event.target.files[0]; + if (file) { + this.selectedImage = file; + this.uploadError = null; + + const reader = new FileReader(); + reader.onload = (e: any) => { + this.imagePreview = e.target.result; + }; + reader.readAsDataURL(file); + } + } + + uploadImage(): Promise { + return new Promise((resolve, reject) => { + if (!this.selectedImage) { + resolve(''); + return; + } + + this.isImageUploading = true; + this.uploadError = null; + + this.fileUploadService.uploadFile(this.selectedImage).subscribe({ + next: (response) => { + this.isImageUploading = false; + this.courseForm.patchValue({ imageUrl: response.url }); + resolve(response.url); + }, + error: (error) => { + this.isImageUploading = false; + this.uploadError = error.error?.error || 'Failed to upload image'; + reject(error); + } + }); + }); + } + + removeImage() { + this.selectedImage = null; + this.imagePreview = null; + this.courseForm.patchValue({ imageUrl: '' }); + this.uploadError = null; + + const fileInput = document.getElementById('imageInput') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + async saveCourse() { + if (this.courseForm.valid) { + try { + if (this.selectedImage) { + await this.uploadImage(); + } + + const courseData = this.courseForm.value; + + courseData.isActive = this.courseForm.get('isActive')?.value === true; + + courseData.eligibility = courseData.eligibility + ? courseData.eligibility.split(',').map((item: string) => item.trim()).filter((item: string) => item.length > 0) + : []; + courseData.objectives = courseData.objectives + ? courseData.objectives.split(',').map((item: string) => item.trim()).filter((item: string) => item.length > 0) + : []; + + if (this.editing && this.selectedCourse) { + this.educationService.updateCourse(this.selectedCourse.id!, courseData).subscribe(() => { + this.loadCourses(); + this.hideCourseForm(); + }); + } else { + this.educationService.createCourse(courseData).subscribe(() => { + this.loadCourses(); + this.hideCourseForm(); + }); + } + } catch (error) { + console.error('Error saving course:', error); + } + } + } + + deleteCourse(course: Course) { + if (confirm('Are you sure you want to delete this course?')) { + this.educationService.deleteCourse(course.id!).subscribe(() => { + this.loadCourses(); + }); + } + } + + resetCourseForm() { + this.courseForm.reset(); + this.selectedCourse = null; + this.editing = false; + this.selectedImage = null; + this.imagePreview = null; + this.uploadError = null; + this.courseForm.patchValue({ + isActive: true + }); + + const fileInput = document.getElementById('imageInput') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + // Application management + viewApplications(course: Course) { + this.selectedCourseForApplications = course; + this.showApplications = true; + this.educationService.getApplicationsByCourseId(course.id!).subscribe(data => { + this.applications = data; + }); + } + + hideApplications() { + this.showApplications = false; + this.selectedCourseForApplications = null; + this.loadApplications(); + } + + updateApplicationStatus(application: CourseApplication, status: string) { + this.educationService.updateApplicationStatus(application.id!, status).subscribe(() => { + if (this.selectedCourseForApplications) { + this.viewApplications(this.selectedCourseForApplications); + } else { + this.loadApplications(); + } + }); + } + + deleteApplication(application: CourseApplication) { + if (confirm('Are you sure you want to delete this application?')) { + this.educationService.deleteApplication(application.id!).subscribe(() => { + if (this.selectedCourseForApplications) { + this.viewApplications(this.selectedCourseForApplications); + } else { + this.loadApplications(); + } + }); + } + } + + getStatusBadgeClass(status: string): string { + switch (status?.toLowerCase()) { + case 'pending': return 'badge-warning'; + case 'reviewed': return 'badge-info'; + case 'shortlisted': return 'badge-primary'; + case 'accepted': return 'badge-success'; + case 'enrolled': return 'badge-success'; + case 'rejected': return 'badge-danger'; + default: return 'badge-light'; + } + } + + getApplicationCount(courseId?: number): number { + if (!courseId) return 0; + return this.applications.filter(app => app.course?.id === courseId).length; + }} + \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/event-form/event-form.component.html b/support-portal-frontend/src/app/component/event-form/event-form.component.html index f4b3979..f072a2b 100644 --- a/support-portal-frontend/src/app/component/event-form/event-form.component.html +++ b/support-portal-frontend/src/app/component/event-form/event-form.component.html @@ -24,6 +24,16 @@ +
+ + +
+
+ + +
@@ -43,6 +53,119 @@
+ +
+ + + +
+
+
Upload Image File
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
Or Enter Image URL
+ +
+
+ + +
+ Main Image Preview +
+ + This will be the primary image displayed for the event +
+ + +
+ + + +
+
+
Upload Gallery Images
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
Or Add Gallery URLs
+
+
+
+ +
+
+ +
+ +
+ Gallery Preview +
+
+ +
+
+
+ + Add up to 4 gallery images for the event grid +
+
@@ -69,7 +192,7 @@
@@ -86,7 +209,7 @@
@@ -103,7 +226,7 @@
@@ -121,7 +244,7 @@
- +
@@ -129,7 +252,7 @@
@@ -142,4 +265,4 @@ - + \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/event-form/event-form.component.ts b/support-portal-frontend/src/app/component/event-form/event-form.component.ts index 6422df2..69f5549 100644 --- a/support-portal-frontend/src/app/component/event-form/event-form.component.ts +++ b/support-portal-frontend/src/app/component/event-form/event-form.component.ts @@ -1,4 +1,3 @@ - import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -11,125 +10,334 @@ import { EventService } from 'src/app/service/event.service'; }) export class EventFormComponent implements OnInit { + eventForm: FormGroup; + eventId: number | null; + // File upload properties + selectedMainImageFile: File | null = null; + selectedGalleryImageFiles: File[] = []; + mainImageUploading = false; + galleryImagesUploading = false; - - eventForm: FormGroup; - - constructor( - private fb: FormBuilder, - private eventService: EventService, - private route: ActivatedRoute, - private router: Router - - ) {} - eventId: number | null; - - ngOnInit() { - this.eventForm = this.fb.group({ - code: ['', Validators.required], - year: ['', Validators.required], - subject: ['', Validators.required], - title: ['', Validators.required], - subTitle: [''], - date: ['', Validators.required], - venues: this.fb.array([]), - highlights: this.fb.array([]), - organisers: this.fb.array([]), - fees: this.fb.array([]), - phone: ['', Validators.required], - email: ['', [Validators.required, Validators.email]], - isActive: [true] - }); - - this.route.paramMap.subscribe(params => { - const idParam = params.get('id'); - this.eventId = idParam ? +idParam : null; - - if (this.eventId !== null) { - this.loadEvent(this.eventId); - } - }); - } - - loadEvent(id: number): void { - this.eventService.getEvent(id).subscribe(event => { - this.eventForm.patchValue(event); - this.setArrayValues('venues', event.venues); - this.setArrayValues('highlights', event.highlights); - this.setArrayValues('organisers', event.organisers); - this.setArrayValues('fees', event.fees); - }); - } - - setArrayValues(controlName: string, values: any[]): void { - const array = this.eventForm.get(controlName) as FormArray; - values.forEach(value => array.push(this.fb.group(value))); - } - - get venues(): FormArray { - return this.eventForm.get('venues') as FormArray; - } - - get highlights(): FormArray { - return this.eventForm.get('highlights') as FormArray; - } - - get organisers(): FormArray { - return this.eventForm.get('organisers') as FormArray; - } - - get fees(): FormArray { - return this.eventForm.get('fees') as FormArray; - } - - addVenue() { - this.venues.push(this.fb.group({ - title: [''], - date: [''], - address: [''], - info: [''] - })); - } - - removeVenue(index: number) { - this.venues.removeAt(index); - } - - addHighlight() { - this.highlights.push(this.fb.control('')); - } - - removeHighlight(index: number) { - this.highlights.removeAt(index); - } - - addOrganiser() { - this.organisers.push(this.fb.control('')); - } - - removeOrganiser(index: number) { - this.organisers.removeAt(index); - } - - addFee() { - this.fees.push(this.fb.group({ - desc: [''], - cost: [''] - })); - } - - removeFee(index: number) { - this.fees.removeAt(index); - } - - onSubmit(): void { - if (this.eventForm.valid) { - if (this.eventId) { - this.eventService.updateEvent(this.eventId, this.eventForm.value).subscribe(() => this.router.navigate(['/events'])); - } else { - this.eventService.createEvent(this.eventForm.value).subscribe(() => this.router.navigate(['/events'])); - } + constructor( + private fb: FormBuilder, + private eventService: EventService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + this.eventForm = this.fb.group({ + code: ['', Validators.required], + year: ['', Validators.required], + subject: ['', Validators.required], + title: ['', Validators.required], + subTitle: [''], + description: ['', Validators.required], + detail: ['', Validators.required], + date: ['', Validators.required], + mainImage: [''], + galleryImages: this.fb.array([]), + venues: this.fb.array([]), // Form uses 'venues' (plural) + highlights: this.fb.array([]), + organisers: this.fb.array([]), + fees: this.fb.array([]), // Form uses 'fees' (plural) + phone: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + isActive: [true] + }); + + this.route.paramMap.subscribe(params => { + const idParam = params.get('id'); + this.eventId = idParam ? +idParam : null; + + if (this.eventId !== null) { + this.loadEvent(this.eventId); } + }); + } + + // File selection methods + onMainImageFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.selectedMainImageFile = file; + console.log('Main image file selected:', file.name); + } + } + + onGalleryImagesSelected(event: any): void { + const files = Array.from(event.target.files) as File[]; + if (files.length > 0) { + this.selectedGalleryImageFiles = files; + console.log('Gallery image files selected:', files.map(f => f.name)); + } + } + + // Upload methods + uploadMainImage(): void { + if (!this.selectedMainImageFile) return; + + this.mainImageUploading = true; + this.eventService.uploadImage(this.selectedMainImageFile).subscribe({ + next: (response) => { + console.log('Main image uploaded:', response); + this.eventForm.patchValue({ mainImage: response.url }); + this.selectedMainImageFile = null; + this.mainImageUploading = false; + + // Clear the file input + const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + }, + error: (error) => { + console.error('Error uploading main image:', error); + this.mainImageUploading = false; + alert('Error uploading image. Please try again.'); + } + }); + } + + uploadGalleryImages(): void { + if (!this.selectedGalleryImageFiles || this.selectedGalleryImageFiles.length === 0) return; + + this.galleryImagesUploading = true; + this.eventService.uploadMultipleImages(this.selectedGalleryImageFiles).subscribe({ + next: (responses) => { + console.log('Gallery images uploaded:', responses); + + // Add uploaded image URLs to the form array + const galleryArray = this.galleryImages; + responses.forEach((response: any) => { + if (response && response.url) { + galleryArray.push(this.fb.control(response.url)); + } + }); + + this.selectedGalleryImageFiles = []; + this.galleryImagesUploading = false; + + // Clear the file input + const fileInputs = document.querySelectorAll('input[type="file"]'); + fileInputs.forEach((input: any) => { + if (input.multiple) input.value = ''; + }); + }, + error: (error) => { + console.error('Error uploading gallery images:', error); + this.galleryImagesUploading = false; + alert('Error uploading gallery images. Please try again.'); + } + }); + } + + loadEvent(id: number): void { + this.eventService.getEvent(id).subscribe(event => { + if (event) { + this.eventForm.patchValue({ + code: event.code, + year: event.year, + subject: event.subject, + title: event.title, + subTitle: event.subTitle, + description: event.description, + detail: event.detail, + date: event.date, + mainImage: event.mainImage, + phone: event.phone, + email: event.email, + isActive: event.isActive + }); + + // Map backend 'venue' to form 'venues' + this.setArrayValues('venues', event.venue || []); + + // Map backend 'fee' to form 'fees' + this.setArrayValues('fees', event.fee || []); + + this.setArrayValues('highlights', event.highlights || []); + this.setArrayValues('organisers', event.organisers || []); + this.setStringArrayValues('galleryImages', event.galleryImages || []); + } + }); + } + + setArrayValues(controlName: string, values: any[]): void { + const array = this.eventForm.get(controlName) as FormArray; + array.clear(); + + if (values && values.length > 0) { + values.forEach(value => { + if (controlName === 'venues') { + array.push(this.fb.group({ + title: [value.title || ''], + date: [value.date || ''], + address: [value.address || ''], + info: [value.info || ''] + })); + } else if (controlName === 'fees') { + array.push(this.fb.group({ + description: [value.description || ''], // Changed from 'desc' to 'description' + cost: [value.cost || ''] + })); + } else { + array.push(this.fb.control(value)); + } + }); + } + } + + setStringArrayValues(controlName: string, values: string[]): void { + const array = this.eventForm.get(controlName) as FormArray; + array.clear(); + if (values && values.length > 0) { + values.forEach(value => array.push(this.fb.control(value))); + } + } + + get venues(): FormArray { + return this.eventForm.get('venues') as FormArray; + } + + get highlights(): FormArray { + return this.eventForm.get('highlights') as FormArray; + } + + get organisers(): FormArray { + return this.eventForm.get('organisers') as FormArray; + } + + get fees(): FormArray { + return this.eventForm.get('fees') as FormArray; + } + + get galleryImages(): FormArray { + return this.eventForm.get('galleryImages') as FormArray; + } + + addGalleryImage() { + this.galleryImages.push(this.fb.control('')); + } + + removeGalleryImage(index: number) { + this.galleryImages.removeAt(index); + } + + addVenue() { + this.venues.push(this.fb.group({ + title: [''], + date: [''], + address: [''], + info: [''] + })); + } + + removeVenue(index: number) { + this.venues.removeAt(index); + } + + addHighlight() { + this.highlights.push(this.fb.control('')); + } + + removeHighlight(index: number) { + this.highlights.removeAt(index); + } + + addOrganiser() { + this.organisers.push(this.fb.control('')); + } + + removeOrganiser(index: number) { + this.organisers.removeAt(index); + } + + addFee() { + this.fees.push(this.fb.group({ + description: [''], // Changed from 'desc' to 'description' + cost: [''] + })); + } + + removeFee(index: number) { + this.fees.removeAt(index); + } + + onSubmit(): void { + if (this.eventForm.valid) { + const formValue = this.eventForm.value; + + // Transform form data to match backend structure + const eventData = { + ...formValue, + venue: formValue.venues, // Map 'venues' to 'venue' for backend + fee: formValue.fees, // Map 'fees' to 'fee' for backend + galleryImages: formValue.galleryImages?.filter((img: string) => img?.trim() !== '') || [], + isDeleted: false // Add default value for isDeleted + }; + + // Remove the plural properties that don't exist in backend + delete eventData.venues; + delete eventData.fees; + + console.log('Submitting event data:', eventData); + + if (this.eventId) { + this.eventService.updateEvent(this.eventId, eventData).subscribe({ + next: (response) => { + console.log('Event updated successfully:', response); + this.router.navigate(['/events']); + }, + error: (error) => { + console.error('Error updating event:', error); + alert('Error updating event. Please check the console for details.'); + } + }); + } else { + this.eventService.createEvent(eventData).subscribe({ + next: (response) => { + console.log('Event created successfully:', response); + this.router.navigate(['/events']); + }, + error: (error) => { + console.error('Error creating event:', error); + alert('Error creating event. Please check the console for details.'); + } + }); + } + } else { + console.log('Form is invalid'); + console.log('Form errors:', this.getFormValidationErrors()); + + // Mark all fields as touched to show validation errors + Object.keys(this.eventForm.controls).forEach(key => { + const control = this.eventForm.get(key); + if (control) { + control.markAsTouched(); + if (control instanceof FormArray) { + control.controls.forEach(arrayControl => { + arrayControl.markAsTouched(); + if (arrayControl instanceof FormGroup) { + Object.keys(arrayControl.controls).forEach(innerKey => { + arrayControl.get(innerKey)?.markAsTouched(); + }); + } + }); + } + } + }); + } + } + + // Helper method to debug form validation errors + getFormValidationErrors() { + const formErrors: any = {}; + Object.keys(this.eventForm.controls).forEach(key => { + const controlErrors = this.eventForm.get(key)?.errors; + if (controlErrors) { + formErrors[key] = controlErrors; + } + }); + return formErrors; } } \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/event/event.component.html b/support-portal-frontend/src/app/component/event/event.component.html index 0fb18b2..9bbe3bd 100644 --- a/support-portal-frontend/src/app/component/event/event.component.html +++ b/support-portal-frontend/src/app/component/event/event.component.html @@ -7,41 +7,61 @@ Add New Event - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CodeYearSubjectTitleSubtitleDatePhoneEmailActiveActions
{{ event.code }}{{ event.year }}{{ event.subject }}{{ event.title }}{{ event.subTitle }}{{ event.date }}{{ event.phone }}{{ event.email }}{{ event.isActive ? 'Yes' : 'No' }} - - Edit - - -
- +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeYearSubjectTitleDescriptionDateFeesMain ImageActiveActions
{{ event.code }}{{ event.year }}{{ event.subject }}{{ event.title }}{{ event.description?.substring(0, 50) }}{{ event.description?.length > 50 ? '...' : '' }}{{ event.date }} + + {{ event.fee[0].description }}: ₹{{ event.fee[0].cost }} + (+{{ event.fee.length - 1 }} more) + + No fees + + Event Image + No image + + + {{ event.isActive ? 'Yes' : 'No' }} + + + + Edit + + +
+
+ \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/menu/menu.component.html b/support-portal-frontend/src/app/component/menu/menu.component.html index 2c23897..1d6ad22 100644 --- a/support-portal-frontend/src/app/component/menu/menu.component.html +++ b/support-portal-frontend/src/app/component/menu/menu.component.html @@ -1,52 +1,67 @@
- \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/professor/professor.component.html b/support-portal-frontend/src/app/component/professor/professor.component.html index a87332f..8999626 100644 --- a/support-portal-frontend/src/app/component/professor/professor.component.html +++ b/support-portal-frontend/src/app/component/professor/professor.component.html @@ -39,7 +39,7 @@ data-bs-toggle="tab" href="#profile" > - Welcome, {{ loggedInUser.firstName }} {{ loggedInUser.lastName }} + Welcome, {{ loggedInUser?.firstName }} {{ loggedInUser?.lastName }}
@@ -91,6 +91,7 @@ First Name Last Name Email + Category Status Actions @@ -118,6 +119,18 @@ {{ professor?.email }} + + + {{ getCategoryDisplayName(professor?.category) }} + + + + + +

Settings content goes here...

@@ -188,7 +205,7 @@ aria-labelledby="addProfessorModalLabel" aria-hidden="true" > - -
-
- + \ No newline at end of file diff --git a/support-portal-frontend/src/app/component/professor/professor.component.ts b/support-portal-frontend/src/app/component/professor/professor.component.ts index 29800b1..e6649b1 100644 --- a/support-portal-frontend/src/app/component/professor/professor.component.ts +++ b/support-portal-frontend/src/app/component/professor/professor.component.ts @@ -10,12 +10,19 @@ import { Router } from '@angular/router'; import { FileUploadStatus } from 'src/app/model/file-upload.status'; import { SubSink } from 'subsink'; import { WorkingStatus } from "../../enum/WorkingStatus"; +import { ProfessorCategory } from "../../enum/professor-category.enum"; import { User } from 'src/app/model/user'; -; import { Role } from 'src/app/enum/role.enum'; import { AuthenticationService } from 'src/app/service/authentication.service'; import { ProfessorService } from 'src/app/service/professor.service'; -; + +interface Award { + id?: number; + title: string; + year: string; + description: string; + imageUrl?: string; +} @Component({ selector: 'app-professor', @@ -23,58 +30,120 @@ import { ProfessorService } from 'src/app/service/professor.service'; styleUrls: ['./professor.component.css'] }) export class ProfessorComponent implements OnInit, OnDestroy { - WorkingStatus = WorkingStatus; // Declare enum here + WorkingStatus = WorkingStatus; + ProfessorCategory = ProfessorCategory; private titleSubject = new BehaviorSubject('Professors'); public titleAction$ = this.titleSubject.asObservable(); public loggedInUser: User; - public professors: Professor[] = []; public loggedInProfessor: Professor; public refreshing: boolean; private subs = new SubSink(); + selectedProfessor: Professor = { - professorId: '', // Initialize with empty string or appropriate default - firstName: '', // Initialize with empty string or appropriate default - lastName: '', // Initialize with empty string or appropriate default - email: '', // Initialize with empty string or appropriate default - profileImageUrl: '', // Optional property, can be initialized with empty string or `null` - status: WorkingStatus.ACTIVE, // Initialize with a default value from your enum - department: '', // Initialize with empty string or appropriate default - position: '', // Initialize with empty string or appropriate default - officeLocation: '', // Initialize with empty string or appropriate default - joinDate: new Date() // Initialize with the current date or an appropriate default + professorId: '', + firstName: '', + lastName: '', + email: '', + profileImageUrl: '', + status: WorkingStatus.ACTIVE, + category: ProfessorCategory.FACULTY, + department: '', + position: '', + officeLocation: '', + joinDate: new Date(), + phone: '', + specialty: '', + certification: '', + training: '', + experience: '', + description: '', + designation: '', + workDays: [], + awards: [] }; - public profileImageFileName: string | null; + + public profileImageFileName: string | null; public profileImage: File | null; - // public editProfessor: Professor = new Professor(); 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[] = []; + constructor( private professorService: ProfessorService, private notificationService: NotificationService, private router: Router, private authenticationService: AuthenticationService, - ) {} ngOnInit(): void { - this.getProfessors(true); this.loggedInUser = this.authenticationService.getUserFromLocalStorage(); - + this.initializeAwards(); } ngOnDestroy(): void { this.subs.unsubscribe(); } + private initializeAwards(): void { + this.newProfessorAwards = []; + this.selectedProfessorAwards = []; + } + + // Award management methods + 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); + } + + // Category display helper + public getCategoryDisplayName(category: string): string { + switch (category) { + case 'FACULTY': + return 'Faculty'; + case 'SUPPORT_TEAM': + return 'Support Team'; + case 'TRAINEE_FELLOW': + return 'Trainee/Fellow'; + default: + return 'Unknown'; + } + } + handleTitleChange(title: string): void { this.titleSubject.next(title); } - public changeTitle(title: string): void { this.titleSubject.next(title); } @@ -99,12 +168,24 @@ export class ProfessorComponent implements OnInit, OnDestroy { public onSelectProfessor(selectedProfessor: Professor): void { this.selectedProfessor = selectedProfessor; + this.selectedProfessorAwards = [...(selectedProfessor.awards || [])]; + + // Set up work days for viewing + this.selectedWorkDays = {}; + if (selectedProfessor.workDays && Array.isArray(selectedProfessor.workDays)) { + selectedProfessor.workDays.forEach(day => { + this.selectedWorkDays[day] = true; + }); + } + this.clickButton('openProfessorInfo'); } public onProfileImageChange(fileList: FileList): void { - this.profileImageFileName = fileList[0].name; - this.profileImage = fileList[0]; + if (fileList && fileList.length > 0) { + this.profileImageFileName = fileList[0].name; + this.profileImage = fileList[0]; + } } private sendErrorNotification(message: string): void { @@ -116,13 +197,15 @@ export class ProfessorComponent implements OnInit, OnDestroy { } public onAddNewProfessor(professorForm: NgForm): void { - const formData = this.professorService.createProfessorFormData(professorForm.value, this.profileImage); + const formData = this.createExtendedProfessorFormData(professorForm.value, this.profileImage); this.subs.sink = this.professorService.addProfessor(formData).subscribe( (professor: Professor) => { this.clickButton('new-professor-close'); this.getProfessors(false); this.invalidateVariables(); professorForm.reset(); + this.selectedWorkDays = {}; + this.newProfessorAwards = []; this.notificationService.notify(NotificationType.SUCCESS, `Professor ${professor.firstName} added successfully`); }, (errorResponse: HttpErrorResponse) => { @@ -134,6 +217,9 @@ export class ProfessorComponent implements OnInit, OnDestroy { private invalidateVariables(): void { this.profileImage = null; this.profileImageFileName = null; + this.selectedWorkDays = {}; + this.newProfessorAwards = []; + this.selectedProfessorAwards = []; } public saveNewProfessor(): void { @@ -153,7 +239,10 @@ export class ProfessorComponent implements OnInit, OnDestroy { professor.firstName.toLowerCase().includes(searchTerm) || professor.lastName.toLowerCase().includes(searchTerm) || professor.email.toLowerCase().includes(searchTerm) || - professor.department.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); } @@ -166,19 +255,29 @@ export class ProfessorComponent implements OnInit, OnDestroy { } public onEditProfessor(professor: Professor): void { - // this.editProfessor = professor; - this.selectedProfessor = professor; + this.selectedProfessor = { ...professor }; + + // Set up work days for editing + this.selectedWorkDays = {}; + if (professor.workDays && Array.isArray(professor.workDays)) { + professor.workDays.forEach(day => { + this.selectedWorkDays[day] = true; + }); + } + + // Set up awards for editing + this.selectedProfessorAwards = [...(professor.awards || [])]; + this.clickButton('openProfessorEdit'); } - onUpdateProfessor(form: NgForm) { + public onUpdateProfessor(form: NgForm): void { if (form.invalid) { - // Handle form validation errors if needed this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.'); return; } - const formData = this.professorService.createProfessorFormData(this.selectedProfessor, this.profileImage); + const formData = this.createExtendedProfessorFormData(this.selectedProfessor, this.profileImage); this.subs.add(this.professorService.updateProfessor(this.selectedProfessor.professorId, formData).subscribe( (professor: Professor) => { @@ -193,18 +292,70 @@ 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 || ''); + formData.append('department', professor.department || ''); + formData.append('position', professor.position || ''); + formData.append('officeLocation', professor.officeLocation || ''); + formData.append('status', professor.status || 'ACTIVE'); + formData.append('category', professor.category || 'FACULTY'); + + // 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 || ''); + formData.append('training', professor.training || ''); + + // Work days + const workDays = Object.keys(this.selectedWorkDays).filter(day => this.selectedWorkDays[day]); + if (workDays.length > 0) { + workDays.forEach(day => { + formData.append('workDays', day); + }); + } + + // Awards - determine which awards array to use + const awardsToSubmit = professor.professorId ? this.selectedProfessorAwards : this.newProfessorAwards; + if (awardsToSubmit && awardsToSubmit.length > 0) { + awardsToSubmit.forEach((award, index) => { + if (award.title && award.year) { // Only include awards with title and year + formData.append(`awards[${index}].title`, award.title); + formData.append(`awards[${index}].year`, award.year); + formData.append(`awards[${index}].description`, award.description || ''); + formData.append(`awards[${index}].imageUrl`, award.imageUrl || ''); + } + }); + } + + // Profile image + if (profileImage) { + formData.append('profileImage', profileImage); + } + + return formData; + } public onDeleteProfessor(professor: Professor): void { - this.subs.sink = this.professorService.deleteProfessor(professor.professorId).subscribe( - (response: CustomHttpResponse) => { - this.getProfessors(false); - this.invalidateVariables(); - this.notificationService.notify(NotificationType.SUCCESS, response.message); - }, - (errorResponse: HttpErrorResponse) => { - this.sendErrorNotification(errorResponse.error.message); - } - ); + if (confirm(`Are you sure you want to delete ${professor.firstName} ${professor.lastName}?`)) { + this.subs.sink = this.professorService.deleteProfessor(professor.professorId).subscribe( + (response: CustomHttpResponse) => { + this.getProfessors(false); + this.invalidateVariables(); + this.notificationService.notify(NotificationType.SUCCESS, response.message); + }, + (errorResponse: HttpErrorResponse) => { + this.sendErrorNotification(errorResponse.error.message); + } + ); + } } public updateProfileImage(): void { @@ -241,8 +392,9 @@ export class ProfessorComponent implements OnInit, OnDestroy { break; case HttpEventType.Response: if (event.status === 200) { - // For browser to fetch image when updating (because name left the same) - this.loggedInProfessor.profileImageUrl = `${event.body.profileImageUrl}?time=${new Date().getTime()}`; + if (this.loggedInProfessor) { + this.loggedInProfessor.profileImageUrl = `${event.body.profileImageUrl}?time=${new Date().getTime()}`; + } this.notificationService.notify(NotificationType.SUCCESS, `${event.body.firstName}'s image updated successfully`); this.fileUploadStatus.status = 'done'; } else { @@ -255,10 +407,36 @@ export class ProfessorComponent implements OnInit, OnDestroy { } public get isAdmin(): boolean { - return 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 { - return this.isAdmin || this.loggedInUser.role === Role.MANAGER; + return this.isAdmin || (this.loggedInUser && this.loggedInUser.role === Role.MANAGER); } -} + + public clearFormData(): void { + this.selectedProfessor = { + professorId: '', + firstName: '', + lastName: '', + email: '', + profileImageUrl: '', + status: WorkingStatus.ACTIVE, + category: ProfessorCategory.FACULTY, + department: '', + position: '', + officeLocation: '', + joinDate: new Date(), + phone: '', + specialty: '', + certification: '', + training: '', + experience: '', + description: '', + designation: '', + workDays: [], + awards: [] + }; + this.invalidateVariables(); + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/enum/professor-category.enum.ts b/support-portal-frontend/src/app/enum/professor-category.enum.ts new file mode 100644 index 0000000..6db70e0 --- /dev/null +++ b/support-portal-frontend/src/app/enum/professor-category.enum.ts @@ -0,0 +1,6 @@ +// src/app/enum/professor-category.enum.ts +export enum ProfessorCategory { + FACULTY = 'FACULTY', + SUPPORT_TEAM = 'SUPPORT_TEAM', + TRAINEE_FELLOW = 'TRAINEE_FELLOW' +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/model/Professor.ts b/support-portal-frontend/src/app/model/Professor.ts index a9c027d..fe70dea 100644 --- a/support-portal-frontend/src/app/model/Professor.ts +++ b/support-portal-frontend/src/app/model/Professor.ts @@ -1,16 +1,46 @@ -import { WorkingStatus } from "../enum/WorkingStatus"; +// src/app/model/Professor.ts +import { WorkingStatus } from '../enum/WorkingStatus'; +import { ProfessorCategory } from '../enum/professor-category.enum'; -export class Professor { - // id : number; - professorId: string; // UUID - firstName: string; - lastName: string; - email: string; - department: string; - position: string; - officeLocation: string; - status: WorkingStatus; // Assuming status is represented as a string in the DTO - joinDate: Date; // LocalDateTime as a string - profileImageUrl?: string; // Optional, URL to the profile image - // profileImage?: File; // Optional, used for uploading profile images - } \ No newline at end of file +export interface Professor { + id?: number; + professorId: string; + firstName: string; + lastName: string; + email: string; + department: string; + position: string; + officeLocation: string; + joinDate: Date; + profileImageUrl: string; + status: WorkingStatus; + category: ProfessorCategory; + + // Additional fields for Next.js integration + phone?: string; + specialty?: string; + certification?: string; + training?: string; + experience?: string; + description?: string; + designation?: string; + workDays?: string[]; + + // Awards and skills + awards?: Award[]; + skills?: Skill[]; +} + +export interface Award { + id?: number; + title: string; + year: string; + description: string; + imageUrl?: string; +} + +export interface Skill { + id?: number; + name: string; + level?: number; +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/blog.service.ts b/support-portal-frontend/src/app/service/blog.service.ts index eb4840a..34a77cb 100644 --- a/support-portal-frontend/src/app/service/blog.service.ts +++ b/support-portal-frontend/src/app/service/blog.service.ts @@ -1,27 +1,35 @@ -// post.model.ts -export interface Blog { - id?: number; - title: string; - content: string; - professors: { id: number, name: string }[]; // Adjust this as per your entity structure - tags: string[]; - isPosted: boolean; -} - - import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; +// Update the Blog interface to match your backend +export interface Blog { + id?: number; + title: string; + content: string; + professors: { id: number, firstName?: string, name?: string }[]; + tags: string[]; + posted: boolean; // Changed from isPosted to posted to match backend + imageUrl?: string; // Add imageUrl field +} @Injectable({ providedIn: 'root' }) export class BlogService { - private apiUrl = environment.apiUrl+'/api/posts'; // Updated to match the Spring Boot controller + private apiUrl = environment.apiUrl + '/api/posts'; + private baseApiUrl = environment.apiUrl; // Base API URL for file uploads constructor(private http: HttpClient) {} + + // Fix the upload URL - use baseApiUrl instead of apiUrl + uploadImage(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + + return this.http.post(`${this.baseApiUrl}/api/files/upload`, formData); + } // Get all blogs getBlogs(): Observable { @@ -47,4 +55,4 @@ export class BlogService { deleteBlog(id: number): Observable { return this.http.delete(`${this.apiUrl}/${id}`); } -} +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/career.service.ts b/support-portal-frontend/src/app/service/career.service.ts new file mode 100644 index 0000000..3424387 --- /dev/null +++ b/support-portal-frontend/src/app/service/career.service.ts @@ -0,0 +1,94 @@ +// career.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export interface Job { + id?: number; + title: string; + department: string; + location: string; + type: string; + experience: string; + salary: string; + description: string; + requirements: string[]; + responsibilities: string[]; + isActive: boolean; +} + +export interface JobApplication { + id?: number; + jobId: number; + fullName: string; + email: string; + phone: string; + experience: string; + coverLetter?: string; + resumeUrl?: string; + status?: string; + job?: Job; + createdDate?: string; // Add this line + updatedDate?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class CareerService { + private jobsApiUrl = environment.apiUrl + '/api/jobs'; + private applicationsApiUrl = environment.apiUrl + '/api/job-applications'; + + constructor(private http: HttpClient) {} + + // Job management + getAllJobs(): Observable { + return this.http.get(this.jobsApiUrl); + } + + getActiveJobs(): Observable { + return this.http.get(`${this.jobsApiUrl}/active`); + } + + getJob(id: number): Observable { + return this.http.get(`${this.jobsApiUrl}/${id}`); + } + + createJob(job: Job): Observable { + return this.http.post(this.jobsApiUrl, job); + } + + updateJob(id: number, job: Job): Observable { + return this.http.put(`${this.jobsApiUrl}/${id}`, job); + } + + deleteJob(id: number): Observable { + return this.http.delete(`${this.jobsApiUrl}/${id}`); + } + + // Job application management + getAllApplications(): Observable { + return this.http.get(this.applicationsApiUrl); + } + + getApplicationsByJobId(jobId: number): Observable { + return this.http.get(`${this.applicationsApiUrl}/job/${jobId}`); + } + + getApplication(id: number): Observable { + return this.http.get(`${this.applicationsApiUrl}/${id}`); + } + + createApplication(application: JobApplication): Observable { + return this.http.post(this.applicationsApiUrl, application); + } + + updateApplicationStatus(id: number, status: string): Observable { + return this.http.put(`${this.applicationsApiUrl}/${id}/status?status=${status}`, {}); + } + + deleteApplication(id: number): Observable { + return this.http.delete(`${this.applicationsApiUrl}/${id}`); + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/education.service.ts b/support-portal-frontend/src/app/service/education.service.ts new file mode 100644 index 0000000..9640916 --- /dev/null +++ b/support-portal-frontend/src/app/service/education.service.ts @@ -0,0 +1,98 @@ +// education.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export interface Course { + id?: number; + title: string; + description: string; + duration: string; + seats: number; + category: string; + level: string; + instructor: string; + price?: string; + startDate?: string; + imageUrl?: string; + eligibility: string[]; + objectives: string[]; + isActive: boolean; +} + +export interface CourseApplication { + id?: number; + courseId: number; + fullName: string; + email: string; + phone: string; + qualification: string; + experience?: string; + coverLetter?: string; + resumeUrl?: string; + status?: string; + course?: Course; + createdDate?: string; + updatedDate?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class EducationService { + private coursesApiUrl = environment.apiUrl + '/api/courses'; + private applicationsApiUrl = environment.apiUrl + '/api/course-applications'; + + constructor(private http: HttpClient) {} + + // Course management + getAllCourses(): Observable { + return this.http.get(this.coursesApiUrl); + } + + getActiveCourses(): Observable { + return this.http.get(`${this.coursesApiUrl}/active`); + } + + getCourse(id: number): Observable { + return this.http.get(`${this.coursesApiUrl}/${id}`); + } + + createCourse(course: Course): Observable { + return this.http.post(this.coursesApiUrl, course); + } + + updateCourse(id: number, course: Course): Observable { + return this.http.put(`${this.coursesApiUrl}/${id}`, course); + } + + deleteCourse(id: number): Observable { + return this.http.delete(`${this.coursesApiUrl}/${id}`); + } + + // Course application management + getAllApplications(): Observable { + return this.http.get(this.applicationsApiUrl); + } + + getApplicationsByCourseId(courseId: number): Observable { + return this.http.get(`${this.applicationsApiUrl}/course/${courseId}`); + } + + getApplication(id: number): Observable { + return this.http.get(`${this.applicationsApiUrl}/${id}`); + } + + createApplication(application: CourseApplication): Observable { + return this.http.post(this.applicationsApiUrl, application); + } + + updateApplicationStatus(id: number, status: string): Observable { + return this.http.put(`${this.applicationsApiUrl}/${id}/status?status=${status}`, {}); + } + + deleteApplication(id: number): Observable { + return this.http.delete(`${this.applicationsApiUrl}/${id}`); + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/event.service.ts b/support-portal-frontend/src/app/service/event.service.ts index 1ed556a..54f0682 100644 --- a/support-portal-frontend/src/app/service/event.service.ts +++ b/support-portal-frontend/src/app/service/event.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; @@ -8,14 +8,80 @@ import { environment } from 'src/environments/environment'; providedIn: 'root' }) export class EventService { - private apiUrl = environment.apiUrl+'/api/events'; // Replace with your API endpoint + private apiUrl = environment.apiUrl + '/api/events'; + private fileUploadUrl = environment.apiUrl + '/api/files'; - constructor(private http: HttpClient) { } + private httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + }; + + constructor(private http: HttpClient) { + console.log('EventService initialized with API URL:', this.apiUrl); + } + + // Upload single image file + uploadImage(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + + console.log('Uploading image:', file.name); + + // Don't set Content-Type header for FormData - let browser set it + return this.http.post(`${this.fileUploadUrl}/upload`, formData) + .pipe( + map(response => { + console.log('Image uploaded successfully:', response); + return response; + }), + catchError(this.handleError('uploadImage')) + ); + } + + // Upload multiple images + uploadMultipleImages(files: File[]): Observable { + const uploadPromises = files.map(file => + this.uploadImage(file).toPromise() + ); + + return new Observable(observer => { + Promise.all(uploadPromises) + .then(results => { + observer.next(results); + observer.complete(); + }) + .catch(error => { + observer.error(error); + }); + }); + } + + // Delete image by filename + deleteImage(filename: string): Observable { + const url = `${this.fileUploadUrl}/images/${filename}`; + console.log('Deleting image:', url); + + return this.http.delete(url) + .pipe( + map(response => { + console.log('Image deleted successfully'); + return response; + }), + catchError(this.handleError('deleteImage')) + ); + } // Fetch all events getEvents(): Observable { - return this.http.get(this.apiUrl) + console.log('Fetching events from:', this.apiUrl); + return this.http.get(this.apiUrl, this.httpOptions) .pipe( + map(events => { + console.log('Events received:', events); + return events; + }), catchError(this.handleError('getEvents', [])) ); } @@ -23,16 +89,26 @@ export class EventService { // Fetch a single event by id getEvent(id: number): Observable { const url = `${this.apiUrl}/${id}`; - return this.http.get(url) + console.log('Fetching event from:', url); + return this.http.get(url, this.httpOptions) .pipe( + map(event => { + console.log('Event received:', event); + return event; + }), catchError(this.handleError(`getEvent id=${id}`)) ); } // Create a new event createEvent(event: any): Observable { + console.log('Creating event:', event); return this.http.post(this.apiUrl, event, this.httpOptions) .pipe( + map(response => { + console.log('Event created successfully:', response); + return response; + }), catchError(this.handleError('createEvent')) ); } @@ -40,8 +116,13 @@ export class EventService { // Update an existing event updateEvent(id: number, event: any): Observable { const url = `${this.apiUrl}/${id}`; + console.log('Updating event at:', url, 'with data:', event); return this.http.put(url, event, this.httpOptions) .pipe( + map(response => { + console.log('Event updated successfully:', response); + return response; + }), catchError(this.handleError('updateEvent')) ); } @@ -49,20 +130,53 @@ export class EventService { // Delete an event by id deleteEvent(id: number): Observable { const url = `${this.apiUrl}/${id}`; - return this.http.delete(url) + console.log('Deleting event at:', url); + return this.http.delete(url, this.httpOptions) .pipe( + map(response => { + console.log('Event deleted successfully'); + return response; + }), catchError(this.handleError('deleteEvent')) ); } - private httpOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }) - }; + // Test connection to API + testConnection(): Observable { + console.log('Testing connection to:', this.apiUrl); + return this.http.get(this.apiUrl, this.httpOptions) + .pipe( + map(response => { + console.log('Connection test successful:', response); + return { success: true, data: response }; + }), + catchError(error => { + console.error('Connection test failed:', error); + return of({ success: false, error: error }); + }) + ); + } private handleError(operation = 'operation', result?: T) { return (error: any): Observable => { - console.error(`${operation} failed: ${error.message}`); + console.error(`${operation} failed:`, error); + + // Detailed error logging + if (error instanceof HttpErrorResponse) { + if (error.status === 0) { + console.error('Network error - possible CORS issue or server not running'); + console.error('Check if Spring Boot server is running on:', this.apiUrl); + } else if (error.status === 404) { + console.error('API endpoint not found:', error.url); + } else if (error.status === 500) { + console.error('Server error:', error.error); + } else { + console.error('HTTP error:', error.status, error.statusText); + } + } + + // Return empty result to keep app running return of(result as T); }; } -} +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/file-upload.service.ts b/support-portal-frontend/src/app/service/file-upload.service.ts new file mode 100644 index 0000000..05ae400 --- /dev/null +++ b/support-portal-frontend/src/app/service/file-upload.service.ts @@ -0,0 +1,26 @@ +// file-upload.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export interface FileUploadResponse { + url: string; + filename: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class FileUploadService { + private apiUrl = environment.apiUrl + '/api/files'; + + constructor(private http: HttpClient) {} + + uploadFile(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + + return this.http.post(`${this.apiUrl}/upload`, formData); + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/app/service/upcoming-events.service.ts b/support-portal-frontend/src/app/service/upcoming-events.service.ts new file mode 100644 index 0000000..9724b38 --- /dev/null +++ b/support-portal-frontend/src/app/service/upcoming-events.service.ts @@ -0,0 +1,47 @@ +// upcoming-events.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export interface UpcomingEvent { + id?: number; + title: string; + description: string; + schedule: string; + eventDate?: string; + isActive: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class UpcomingEventsService { + private apiUrl = environment.apiUrl + '/api/upcoming-events'; + + constructor(private http: HttpClient) {} + + getAllUpcomingEvents(): Observable { + return this.http.get(this.apiUrl); + } + + getActiveUpcomingEvents(): Observable { + return this.http.get(`${this.apiUrl}/active`); + } + + getUpcomingEvent(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + createUpcomingEvent(event: UpcomingEvent): Observable { + return this.http.post(this.apiUrl, event); + } + + updateUpcomingEvent(id: number, event: UpcomingEvent): Observable { + return this.http.put(`${this.apiUrl}/${id}`, event); + } + + deleteUpcomingEvent(id: number): Observable { + return this.http.delete(`${this.apiUrl}/${id}`); + } +} \ No newline at end of file diff --git a/support-portal-frontend/src/environments/environment.ts b/support-portal-frontend/src/environments/environment.ts index 86b8bcf..9f57e4f 100644 --- a/support-portal-frontend/src/environments/environment.ts +++ b/support-portal-frontend/src/environments/environment.ts @@ -9,8 +9,8 @@ export const environment = { // apiUrl: 'http://portal-bean.shyshkin.net', // apiUrl: 'http://supportportalbackend-env.eba-wfr5wya3.eu-north-1.elasticbeanstalk.com', // apiUrl: 'http://support-portal.shyshkin.net:5000', - // apiUrl: 'http://localhost:8080', - apiUrl: 'https://cncbackend.techzoos.in', + apiUrl: 'http://localhost:8080', + // apiUrl: 'https://cncbackend.techzoos.in', publicUrls: ['/user/login', '/user/register', '/user/*/profile-image', '/user/*/profile-image/**'] };