Further updates on 03-11-2025

This commit is contained in:
2025-11-03 13:46:43 +05:30
parent 91cb073c78
commit 434d95eeaf
87 changed files with 17792 additions and 3461 deletions

View File

@ -62,15 +62,20 @@ public class CourseController {
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
course.setActive(courseDto.isActive());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
courseRepository.save(course);
return ResponseEntity.status(HttpStatus.CREATED).build();
Course savedCourse = courseRepository.save(course);
return ResponseEntity.status(HttpStatus.CREATED).body(savedCourse);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateCourse(@PathVariable Long id, @RequestBody CourseDto courseDto) {
Course course = courseRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Course not found"));
Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found"));
course.setTitle(courseDto.getTitle());
course.setDescription(courseDto.getDescription());
@ -84,10 +89,14 @@ public class CourseController {
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
course.setActive(courseDto.isActive());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
courseRepository.save(course);
return ResponseEntity.ok().build();
Course updatedCourse = courseRepository.save(course);
return ResponseEntity.ok(updatedCourse);
}
@DeleteMapping("/{id}")

View File

@ -22,7 +22,6 @@ public class JobController {
@Autowired
private JobRepository jobRepository;
// Get all active jobs (for public display)
@GetMapping("/active")
public ResponseEntity<List<Job>> getActiveJobs() {
try {
@ -33,13 +32,11 @@ public class JobController {
}
}
// Get all jobs (for admin)
@GetMapping
public List<Job> getAllJobs() {
return jobRepository.findAll();
}
// Get a single job by ID
@GetMapping("/{id}")
public ResponseEntity<Job> getJobById(@PathVariable Long id) {
return jobRepository.findById(id)
@ -51,8 +48,7 @@ public class JobController {
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
System.out.println("isActive value: " + jobDto.getIsActive());
Job job = new Job();
job.setTitle(jobDto.getTitle());
@ -64,19 +60,22 @@ public class JobController {
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
job.setActive(jobDto.isActive());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
System.out.println("Job before save - isActive: " + job.isActive());
System.out.println("Job before save - isActive: " + job.getIsActive());
Job savedJob = jobRepository.save(job);
System.out.println("Job after save - isActive: " + savedJob.isActive());
System.out.println("Job after save - isActive: " + savedJob.getIsActive());
return ResponseEntity.status(HttpStatus.CREATED).build();
return ResponseEntity.status(HttpStatus.CREATED).body(savedJob);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateJob(@PathVariable Long id, @RequestBody JobDto jobDto) {
Job job = jobRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Job not found"));
Job job = jobRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Job not found"));
job.setTitle(jobDto.getTitle());
job.setDepartment(jobDto.getDepartment());
@ -87,10 +86,12 @@ public class JobController {
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
job.setActive(jobDto.isActive());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
jobRepository.save(job);
return ResponseEntity.ok().build();
Job updatedJob = jobRepository.save(job);
return ResponseEntity.ok(updatedJob);
}
@DeleteMapping("/{id}")

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/milestones")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class MilestoneController {
private final MilestoneService milestoneService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Milestone>> getActiveMilestones() {
return ResponseEntity.ok(milestoneService.getActiveMilestones());
}
// Admin endpoints - NO SECURITY (matching EventController pattern)
@GetMapping
public ResponseEntity<List<Milestone>> getAllMilestones() {
return ResponseEntity.ok(milestoneService.getAllMilestones());
}
@GetMapping("/{id}")
public ResponseEntity<Milestone> getMilestoneById(@PathVariable Long id) {
return ResponseEntity.ok(milestoneService.getMilestoneById(id));
}
@PostMapping
public ResponseEntity<Milestone> createMilestone(@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(milestoneService.createMilestone(milestoneDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Milestone> updateMilestone(
@PathVariable Long id,
@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.ok(milestoneService.updateMilestone(id, milestoneDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMilestone(@PathVariable Long id) {
milestoneService.deleteMilestone(id);
return ResponseEntity.noContent().build();
}
// @PutMapping("/reorder")
// public ResponseEntity<Void> reorderMilestones(@RequestBody List<Long> orderedIds) {
// milestoneService.reorderMilestones(orderedIds);
// return ResponseEntity.ok().build();
// }
}

View File

@ -0,0 +1,31 @@
package com.shyshkin.study.fullstack.supportportal.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Simple test controller to verify Spring Boot is working
* Test URL: http://localhost:8080/api/test
*/
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/test")
public Map<String, String> test() {
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("message", "Spring Boot is working!");
response.put("timestamp", String.valueOf(System.currentTimeMillis()));
return response;
}
@GetMapping("/health")
public String health() {
return "Application is healthy!";
}
}

View File

@ -0,0 +1,57 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/testimonials")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class TestimonialController {
private final TestimonialService testimonialService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Testimonial>> getActiveTestimonials() {
return ResponseEntity.ok(testimonialService.getActiveTestimonials());
}
// Admin endpoints - no security (matching your pattern)
@GetMapping
public ResponseEntity<List<Testimonial>> getAllTestimonials() {
return ResponseEntity.ok(testimonialService.getAllTestimonials());
}
@GetMapping("/{id}")
public ResponseEntity<Testimonial> getTestimonialById(@PathVariable Long id) {
return ResponseEntity.ok(testimonialService.getTestimonialById(id));
}
@PostMapping
public ResponseEntity<Testimonial> createTestimonial(@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(testimonialService.createTestimonial(testimonialDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Testimonial> updateTestimonial(
@PathVariable Long id,
@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.ok(testimonialService.updateTestimonial(id, testimonialDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTestimonial(@PathVariable Long id) {
testimonialService.deleteTestimonial(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -1,3 +1,4 @@
// Course.java - Entity for courses
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
@ -56,6 +57,7 @@ public class Course extends BaseEntity {
@Column(name = "objective", columnDefinition = "TEXT")
private List<String> objectives;
// FIXED: Changed from primitive boolean to Boolean wrapper class
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
private Boolean isActive = true;
}

View File

@ -77,6 +77,10 @@ public class Event {
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;
// NEW FIELD: Book Seat Link
@Column(name = "book_seat_link", length = 500)
private String bookSeatLink;
@ManyToMany
@JoinTable(
name = "event_professors",

View File

@ -49,6 +49,8 @@ public class Job extends BaseEntity {
@Column(name = "responsibility")
private List<String> responsibilities;
// CRITICAL FIX: Changed from primitive boolean to Boolean wrapper class
// This ensures proper handling of the value from JSON and prevents default false
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
private Boolean isActive = true;
}

View File

@ -0,0 +1,58 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "milestones")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Milestone implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, length = 500)
private String description;
// @Column(nullable = false)
// private Integer displayOrder;
@Column(nullable = false)
private Boolean isActive = true;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC")
@Temporal(TemporalType.DATE)
private Date milestoneDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
@Column(nullable = false, updatable = false)
private Date createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
}

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "testimonials")
public class Testimonial {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false)
private Integer age;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String story;
@Column(nullable = false, columnDefinition = "TEXT")
private String outcome;
@Column(nullable = false, columnDefinition = "TEXT")
private String impact;
@Column(nullable = false, length = 100)
private String category;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@ -40,17 +40,10 @@ public class CourseDto {
private String imageUrl;
private List<String> eligibility;
private List<String> 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;
}
// FIXED: Changed from primitive boolean to Boolean wrapper
// Changed field name from "active" to "isActive"
// Removed all manual getter/setter methods
// Lombok @Data will generate: getIsActive() and setIsActive()
private Boolean isActive;
}

View File

@ -37,20 +37,5 @@ public class JobDto {
private List<String> requirements;
private List<String> 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;
}
private Boolean isActive;
}

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MilestoneDTO {
private Long id;
@NotBlank(message = "Title is required")
private String title;
@NotBlank(message = "Description is required")
private String description;
// @NotNull(message = "Display order is required")
// private Integer displayOrder;
@NotNull(message = "Active status is required")
private Boolean isActive;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date milestoneDate;
}

View File

@ -0,0 +1,47 @@
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.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.Size;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TestimonialDTO {
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must not exceed 100 characters")
private String name;
@NotNull(message = "Age is required")
@Min(value = 1, message = "Age must be at least 1")
@Max(value = 120, message = "Age must not exceed 120")
private Integer age;
@NotBlank(message = "Title is required")
@Size(max = 200, message = "Title must not exceed 200 characters")
private String title;
@NotBlank(message = "Story is required")
private String story;
@NotBlank(message = "Outcome is required")
private String outcome;
@NotBlank(message = "Impact is required")
private String impact;
@NotBlank(message = "Category is required")
@Size(max = 100, message = "Category must not exceed 100 characters")
private String category;
private Boolean isActive;
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.persistence.EntityNotFoundException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> handleEntityNotFound(EntityNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

View File

@ -0,0 +1,12 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class MilestoneNotFoundException extends RuntimeException {
public MilestoneNotFoundException(String message) {
super(message);
}
public MilestoneNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,12 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class TestimonialNotFoundException extends RuntimeException {
public TestimonialNotFoundException(String message) {
super(message);
}
public TestimonialNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,16 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MilestoneRepository extends JpaRepository<Milestone, Long> {
// List<Milestone> findAllByIsActiveTrueOrderByDisplayOrderAsc();
// List<Milestone> findAllByOrderByDisplayOrderAsc();
List<Milestone> findAllByIsActiveTrue();
}

View File

@ -0,0 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TestimonialRepository extends JpaRepository<Testimonial, Long> {
// Get all active testimonials
List<Testimonial> findAllByIsActiveTrue();
// Get testimonials by category
List<Testimonial> findAllByIsActiveTrueAndCategory(String category);
}

View File

@ -0,0 +1,23 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import java.util.List;
public interface MilestoneService {
List<Milestone> getAllMilestones();
List<Milestone> getActiveMilestones();
Milestone getMilestoneById(Long id);
Milestone createMilestone(MilestoneDTO milestoneDTO);
Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO);
void deleteMilestone(Long id);
// void reorderMilestones(List<Long> orderedIds);
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import java.util.List;
public interface TestimonialService {
List<Testimonial> getAllTestimonials();
List<Testimonial> getActiveTestimonials();
Testimonial getTestimonialById(Long id);
Testimonial createTestimonial(TestimonialDTO testimonialDTO);
Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO);
void deleteTestimonial(Long id);
}

View File

@ -0,0 +1,141 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.MilestoneNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.MilestoneRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class MilestoneServiceImpl implements MilestoneService {
private final MilestoneRepository milestoneRepository;
@Override
public List<Milestone> getAllMilestones() {
log.info("Fetching all milestones");
List<Milestone> milestones = milestoneRepository.findAll();
return sortMilestonesByMonthYear(milestones);
}
@Override
public List<Milestone> getActiveMilestones() {
log.info("Fetching active milestones");
List<Milestone> milestones = milestoneRepository.findAllByIsActiveTrue();
return sortMilestonesByMonthYear(milestones);
}
@Override
public Milestone getMilestoneById(Long id) {
log.info("Fetching milestone with id: {}", id);
return milestoneRepository.findById(id)
.orElseThrow(() -> new MilestoneNotFoundException("Milestone not found with id: " + id));
}
@Override
@Transactional
public Milestone createMilestone(MilestoneDTO milestoneDTO) {
log.info("Creating new milestone: {}", milestoneDTO.getTitle());
Milestone milestone = Milestone.builder()
.title(milestoneDTO.getTitle())
.description(milestoneDTO.getDescription())
.isActive(milestoneDTO.getIsActive() != null ? milestoneDTO.getIsActive() : true)
.milestoneDate(milestoneDTO.getMilestoneDate())
.build();
Milestone savedMilestone = milestoneRepository.save(milestone);
log.info("Milestone created successfully with id: {}", savedMilestone.getId());
return savedMilestone;
}
@Override
@Transactional
public Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO) {
log.info("Updating milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestone.setTitle(milestoneDTO.getTitle());
milestone.setDescription(milestoneDTO.getDescription());
milestone.setIsActive(milestoneDTO.getIsActive());
milestone.setMilestoneDate(milestoneDTO.getMilestoneDate());
Milestone updatedMilestone = milestoneRepository.save(milestone);
log.info("Milestone updated successfully");
return updatedMilestone;
}
@Override
@Transactional
public void deleteMilestone(Long id) {
log.info("Deleting milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestoneRepository.delete(milestone);
log.info("Milestone deleted successfully");
}
// REMOVE reorderMilestones method
/**
* Sort milestones by year and month extracted from title
* Most recent first (descending order)
*/
private List<Milestone> sortMilestonesByMonthYear(List<Milestone> milestones) {
return milestones.stream()
.sorted(Comparator.comparing(this::extractYearMonth).reversed())
.collect(Collectors.toList());
}
/**
* Extract YearMonth from milestone title
* Supports formats like:
* - "July 2025"
* - "2025"
* - "December 2023"
*/
private YearMonth extractYearMonth(Milestone milestone) {
String title = milestone.getTitle();
try {
// Try parsing "Month Year" format (e.g., "July 2025", "December 2023")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e1) {
try {
// Try parsing "Month Year" with short month (e.g., "Jul 2025")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e2) {
try {
// Try parsing just year (e.g., "2025", "2020")
int year = Integer.parseInt(title.trim());
return YearMonth.of(year, 1); // Default to January
} catch (NumberFormatException e3) {
// If all parsing fails, default to a very old date
log.warn("Could not parse date from title: {}. Using default date.", title);
return YearMonth.of(1900, 1);
}
}
}
}
}

View File

@ -0,0 +1,95 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.TestimonialNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.TestimonialRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class TestimonialServiceImpl implements TestimonialService {
private final TestimonialRepository testimonialRepository;
@Override
public List<Testimonial> getAllTestimonials() {
log.info("Fetching all testimonials");
return testimonialRepository.findAll();
}
@Override
public List<Testimonial> getActiveTestimonials() {
log.info("Fetching active testimonials");
return testimonialRepository.findAllByIsActiveTrue();
}
@Override
public Testimonial getTestimonialById(Long id) {
log.info("Fetching testimonial with id: {}", id);
return testimonialRepository.findById(id)
.orElseThrow(() -> new TestimonialNotFoundException("Testimonial not found with id: " + id));
}
@Override
@Transactional
public Testimonial createTestimonial(TestimonialDTO testimonialDTO) {
log.info("Creating new testimonial for: {}", testimonialDTO.getName());
Testimonial testimonial = Testimonial.builder()
.name(testimonialDTO.getName())
.age(testimonialDTO.getAge())
.title(testimonialDTO.getTitle())
.story(testimonialDTO.getStory())
.outcome(testimonialDTO.getOutcome())
.impact(testimonialDTO.getImpact())
.category(testimonialDTO.getCategory())
.isActive(testimonialDTO.getIsActive() != null ? testimonialDTO.getIsActive() : true)
.build();
Testimonial savedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial created successfully with id: {}", savedTestimonial.getId());
return savedTestimonial;
}
@Override
@Transactional
public Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO) {
log.info("Updating testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonial.setName(testimonialDTO.getName());
testimonial.setAge(testimonialDTO.getAge());
testimonial.setTitle(testimonialDTO.getTitle());
testimonial.setStory(testimonialDTO.getStory());
testimonial.setOutcome(testimonialDTO.getOutcome());
testimonial.setImpact(testimonialDTO.getImpact());
testimonial.setCategory(testimonialDTO.getCategory());
testimonial.setIsActive(testimonialDTO.getIsActive());
Testimonial updatedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial updated successfully");
return updatedTestimonial;
}
@Override
@Transactional
public void deleteTestimonial(Long id) {
log.info("Deleting testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonialRepository.delete(testimonial);
log.info("Testimonial deleted successfully");
}
}

View File

@ -0,0 +1,14 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/support_portal?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: root
file:
upload:
directory: ${user.home}/support-portal-uploads
app:
base-url: http://localhost:8080
cors:
allowed-origins: http://localhost:4200,http://localhost:3000

View File

@ -51,7 +51,7 @@ file:
app:
base-url: ${APP_BASE_URL:http://localhost:8080}
# Fixed public URLs with correct wildcard patterns
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/**,/uploads/**,/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
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/**,/uploads/**,/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,/api/milestones,/api/milestones/**,/api/testimonials,/api/testimonials/**
cors:
allowed-origins: http://localhost:4200,http://localhost:3000,https://maincmc.rootxwire.com,https://dashboad.cmctrauma.com,https://www.dashboad.cmctrauma.com,https://cmctrauma.com,https://www.cmctrauma.com,https://cmcbackend.rootxwire.com,https://cmcadminfrontend.rootxwire.com
jwt: