init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
@@ -0,0 +1,87 @@
package com.srsystem.service;
import com.srsystem.config.jwt.JwtTokenProvider;
import com.srsystem.dto.EmployeeSummary;
import com.srsystem.dto.LoginRequest;
import com.srsystem.dto.LoginResponse;
import com.srsystem.entity.Employee;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.EmployeeRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final EmployeeRepository employeeRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
public LoginResponse login(LoginRequest request) {
Employee employee = employeeRepository.findByLoginId(request.loginId())
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_CREDENTIALS));
if (!passwordEncoder.matches(request.password(), employee.getPassword())) {
throw new BusinessException(ErrorCode.INVALID_CREDENTIALS);
}
String accessToken = jwtTokenProvider.generateAccessToken(employee.getLoginId(), employee.getRoleCode().name());
String refreshToken = jwtTokenProvider.generateRefreshToken(employee.getLoginId());
EmployeeSummary summary = new EmployeeSummary(
employee.getEmpId(),
employee.getName(),
employee.getEmail(),
employee.getPhone(),
employee.getRoleCode(),
employee.getEmpStatus(),
employee.getDispatchStatus(),
employee.getJoinedDate(),
employee.getLeftDate()
);
return new LoginResponse(accessToken, refreshToken, summary);
}
@Transactional
public LoginResponse refresh(String refreshToken) {
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_CREDENTIALS);
}
String loginId = jwtTokenProvider.getLoginId(refreshToken);
Employee employee = employeeRepository.findByLoginId(loginId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
String newAccessToken = jwtTokenProvider.generateAccessToken(employee.getLoginId(), employee.getRoleCode().name());
String newRefreshToken = jwtTokenProvider.generateRefreshToken(employee.getLoginId());
EmployeeSummary summary = new EmployeeSummary(
employee.getEmpId(),
employee.getName(),
employee.getEmail(),
employee.getPhone(),
employee.getRoleCode(),
employee.getEmpStatus(),
employee.getDispatchStatus(),
employee.getJoinedDate(),
employee.getLeftDate()
);
return new LoginResponse(newAccessToken, newRefreshToken, summary);
}
@Transactional(readOnly = true)
public Employee getEmployeeByLoginId(String loginId) {
return employeeRepository.findByLoginId(loginId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
}
}
@@ -0,0 +1,88 @@
package com.srsystem.service;
import com.srsystem.dto.CalendarEvent;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Report;
import com.srsystem.entity.Ticket;
import com.srsystem.entity.enums.RoleCode;
import com.srsystem.repository.DispatchHistoryRepository;
import com.srsystem.repository.ReportRepository;
import com.srsystem.repository.TicketRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CalendarService {
private final TicketRepository ticketRepository;
private final ReportRepository reportRepository;
private final DispatchHistoryRepository dispatchHistoryRepository;
public List<CalendarEvent> getEvents(Long userId, RoleCode role, LocalDate start, LocalDate end) {
List<CalendarEvent> events = new ArrayList<>();
LocalDateTime startDT = start.atStartOfDay();
LocalDateTime endDT = end.atTime(23, 59, 59);
// Tickets with due dates in range
List<Ticket> tickets;
if (role == RoleCode.EMPLOYEE) {
tickets = ticketRepository.findByRequester_EmpIdAndDueDateBetween(userId, startDT, endDT);
} else {
tickets = ticketRepository.findByDueDateBetween(startDT, endDT);
}
for (Ticket t : tickets) {
events.add(new CalendarEvent(
"ticket-" + t.getTicketId(),
t.getTitle(),
t.getDueDate() != null ? t.getDueDate().toLocalDate().toString() : "",
"ticket",
t.getPriority() != null ? t.getPriority().name() : "NORMAL",
t.getStatus() != null ? t.getStatus().name() : "OPEN"
));
}
// Reports with target dates in range
List<Report> reports;
if (role == RoleCode.EMPLOYEE) {
reports = reportRepository.findByUser_EmpIdAndTargetDateBetween(userId, start, end);
} else {
reports = reportRepository.findByTargetDateBetween(start, end);
}
for (Report r : reports) {
events.add(new CalendarEvent(
"report-" + r.getReportId(),
r.getReportType() + " Report",
r.getTargetDate().toString(),
"report",
r.getStatus().name(),
""
));
}
// Dispatch history for this user
List<DispatchHistory> dispatches = dispatchHistoryRepository.findByEmployee_EmpId(userId);
for (DispatchHistory d : dispatches) {
if (d.getStartDate() != null && !d.getStartDate().isAfter(end) && (d.getEndDate() == null || !d.getEndDate().isBefore(start))) {
events.add(new CalendarEvent(
"dispatch-" + d.getDispatchId(),
d.getProject().getProjectName() + " (" + d.getRoleTitle() + ")",
d.getStartDate().toString(),
"dispatch",
d.getEndDate() != null ? "ended" : "active",
d.getEndDate() != null ? d.getEndDate().toString() : ""
));
}
}
return events;
}
}
@@ -0,0 +1,76 @@
package com.srsystem.service;
import com.srsystem.dto.CommonCodeRequest;
import com.srsystem.entity.CommonCode;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.CommonCodeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeService {
private final CommonCodeRepository commonCodeRepository;
public List<CommonCode> getCodes(String groupCode) {
if (groupCode != null) {
return commonCodeRepository.findByGroupCodeAndIsActiveOrderBySortOrder(groupCode, 'Y');
}
return commonCodeRepository.findAll().stream()
.filter(c -> c.getIsActive() == 'Y')
.sorted(java.util.Comparator.comparing(CommonCode::getGroupCode)
.thenComparing(CommonCode::getSortOrder))
.collect(java.util.stream.Collectors.toList());
}
public List<String> getGroupCodes() {
return commonCodeRepository.findAll().stream()
.filter(c -> c.getIsActive() == 'Y')
.map(CommonCode::getGroupCode)
.distinct()
.sorted()
.collect(java.util.stream.Collectors.toList());
}
@Transactional
public CommonCode createCode(CommonCodeRequest request) {
CommonCode code = CommonCode.builder()
.groupCode(request.groupCode())
.codeValue(request.codeValue())
.codeName(request.codeName())
.optionVal1(request.optionVal1())
.optionVal2(request.optionVal2())
.sortOrder(request.sortOrder())
.isActive(request.isActive())
.build();
return commonCodeRepository.save(code);
}
@Transactional
public CommonCode updateCode(Long id, CommonCodeRequest request) {
CommonCode code = commonCodeRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.CODE_NOT_FOUND));
code.setGroupCode(request.groupCode());
code.setCodeValue(request.codeValue());
code.setCodeName(request.codeName());
code.setOptionVal1(request.optionVal1());
code.setOptionVal2(request.optionVal2());
code.setSortOrder(request.sortOrder());
code.setIsActive(request.isActive());
return commonCodeRepository.save(code);
}
@Transactional
public void deleteCode(Long id) {
CommonCode code = commonCodeRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.CODE_NOT_FOUND));
code.setIsDeleted('Y');
commonCodeRepository.save(code);
}
}
@@ -0,0 +1,91 @@
package com.srsystem.service;
import com.srsystem.dto.CreateDispatchRequest;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Project;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.DispatchHistoryRepository;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DispatchHistoryService {
private final DispatchHistoryRepository dispatchHistoryRepository;
private final EmployeeRepository employeeRepository;
private final ProjectRepository projectRepository;
public List<DispatchHistory> findByEmployeeId(Long empId) {
employeeRepository.findById(empId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
return dispatchHistoryRepository.findByEmployee_EmpId(empId);
}
public List<DispatchHistory> getActiveDispatches() {
return dispatchHistoryRepository.findByEndDateIsNull();
}
public Page<DispatchHistory> findAll(Pageable pageable, Long empId, Long projectId) {
if (empId != null && projectId != null) {
return dispatchHistoryRepository.findByEmployee_EmpIdAndProject_ProjectId(empId, projectId, pageable);
}
if (empId != null) {
return dispatchHistoryRepository.findByEmployee_EmpId(empId, pageable);
}
if (projectId != null) {
return dispatchHistoryRepository.findByProject_ProjectId(projectId, pageable);
}
return dispatchHistoryRepository.findAll(pageable);
}
public DispatchHistory findById(Long dispatchId) {
return dispatchHistoryRepository.findById(dispatchId)
.orElseThrow(() -> new BusinessException(ErrorCode.DISPATCH_NOT_FOUND));
}
@Transactional
public void delete(Long dispatchId) {
DispatchHistory dh = findById(dispatchId);
dispatchHistoryRepository.delete(dh);
}
@Transactional
public DispatchHistory createDispatch(CreateDispatchRequest request) {
Employee employee = employeeRepository.findById(request.employeeId())
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
Project project = projectRepository.findById(request.projectId())
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
DispatchHistory history = DispatchHistory.builder()
.employee(employee)
.project(project)
.roleTitle(request.roleTitle())
.contributionRate(request.contributionRate())
.individualTech(request.individualTech())
.performedTasks(request.performedTasks())
.startDate(request.startDate() != null ? request.startDate() : LocalDate.now())
.build();
return dispatchHistoryRepository.save(history);
}
@Transactional
public DispatchHistory endDispatch(Long dispatchId) {
DispatchHistory history = dispatchHistoryRepository.findById(dispatchId)
.orElseThrow(() -> new BusinessException(ErrorCode.DISPATCH_NOT_FOUND));
history.setEndDate(LocalDate.now());
return dispatchHistoryRepository.save(history);
}
}
@@ -0,0 +1,146 @@
package com.srsystem.service;
import com.srsystem.dto.ChangePasswordRequest;
import com.srsystem.dto.EmployeeSearchResult;
import com.srsystem.dto.EmployeeSummary;
import com.srsystem.entity.Employee;
import com.srsystem.entity.enums.EmpStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.DispatchHistoryRepository;
import com.srsystem.repository.EmployeeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EmployeeService {
private final EmployeeRepository employeeRepository;
private final DispatchHistoryRepository dispatchHistoryRepository;
private final PasswordEncoder passwordEncoder;
public Page<Employee> findAll(EmpStatus status, Pageable pageable) {
if (status != null) {
return employeeRepository.findByEmpStatus(status, pageable);
}
return employeeRepository.findAll(pageable);
}
public Employee findById(Long empId) {
return employeeRepository.findById(empId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
}
public Employee findByLoginId(String loginId) {
return employeeRepository.findByLoginId(loginId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
}
public List<EmployeeSearchResult> searchByName(String keyword) {
List<Employee> byName = employeeRepository.findByNameContainingIgnoreCase(keyword);
List<EmployeeSearchResult> results = new ArrayList<>();
Set<Long> seen = new HashSet<>();
for (Employee e : byName) {
if (seen.add(e.getEmpId())) {
results.add(toSearchResult(e));
}
}
return results;
}
private EmployeeSearchResult toSearchResult(Employee e) {
return new EmployeeSearchResult(
e.getEmpId(),
e.getName(),
e.getEmail(),
e.getDispatchStatus() != null ? e.getDispatchStatus().name() : null
);
}
@Transactional
public Employee create(Employee employee) {
if (employeeRepository.findByLoginId(employee.getLoginId()).isPresent()) {
throw new BusinessException(ErrorCode.DUPLICATE_LOGIN_ID);
}
employee.setPassword(passwordEncoder.encode(employee.getPassword()));
return employeeRepository.save(employee);
}
@Transactional
public Employee update(Long empId, Employee updateRequest) {
Employee employee = findById(empId);
if (employee.getEmpStatus() == EmpStatus.RESIGNED) {
throw new BusinessException(ErrorCode.RESIGNED_EMPLOYEE);
}
if (!employee.getLoginId().equals(updateRequest.getLoginId())) {
if (employeeRepository.findByLoginId(updateRequest.getLoginId()).isPresent()) {
throw new BusinessException(ErrorCode.DUPLICATE_LOGIN_ID);
}
employee.setLoginId(updateRequest.getLoginId());
}
employee.setName(updateRequest.getName());
employee.setEmail(updateRequest.getEmail());
employee.setPhone(updateRequest.getPhone());
employee.setRoleCode(updateRequest.getRoleCode());
employee.setEmpStatus(updateRequest.getEmpStatus());
employee.setDispatchStatus(updateRequest.getDispatchStatus());
employee.setJoinedDate(updateRequest.getJoinedDate());
employee.setLeftDate(updateRequest.getLeftDate());
if (updateRequest.getPassword() != null && !updateRequest.getPassword().isBlank()) {
employee.setPassword(passwordEncoder.encode(updateRequest.getPassword()));
}
return employee;
}
@Transactional
public void delete(Long empId) {
Employee employee = findById(empId);
if (employee.getEmpStatus() == EmpStatus.RESIGNED) {
throw new BusinessException(ErrorCode.RESIGNED_EMPLOYEE);
}
employeeRepository.delete(employee);
}
public EmployeeSummary toSummary(Employee employee) {
return new EmployeeSummary(
employee.getEmpId(),
employee.getName(),
employee.getEmail(),
employee.getPhone(),
employee.getRoleCode(),
employee.getEmpStatus(),
employee.getDispatchStatus(),
employee.getJoinedDate(),
employee.getLeftDate()
);
}
@Transactional
public void changePassword(Long empId, ChangePasswordRequest request) {
Employee employee = findById(empId);
if (!passwordEncoder.matches(request.currentPassword(), employee.getPassword())) {
throw new BusinessException(ErrorCode.AUTH_INVALID_CREDENTIALS);
}
employee.setPassword(passwordEncoder.encode(request.newPassword()));
employeeRepository.save(employee);
}
}
@@ -0,0 +1,279 @@
package com.srsystem.service;
import com.srsystem.dto.CreateEquipmentRequest;
import com.srsystem.dto.CreateMaintenanceRequest;
import com.srsystem.dto.EquipmentSummary;
import com.srsystem.dto.UpdateEquipmentRequest;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Equipment;
import com.srsystem.entity.EquipmentAssignment;
import com.srsystem.entity.EquipmentMaintenance;
import com.srsystem.entity.enums.EmpStatus;
import com.srsystem.entity.enums.EquipmentStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.EquipmentAssignmentRepository;
import com.srsystem.repository.EquipmentMaintenanceRepository;
import com.srsystem.repository.EquipmentRepository;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.OptimisticLockException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EquipmentService {
private final EquipmentRepository equipmentRepository;
private final EquipmentAssignmentRepository equipmentAssignmentRepository;
private final EquipmentMaintenanceRepository equipmentMaintenanceRepository;
private final EmployeeRepository employeeRepository;
@Value("${file.upload-dir:./uploads}")
private String uploadDir;
@PostConstruct
public void initUploadDir() {
File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs();
}
public Page<Equipment> getEquipment(String type, EquipmentStatus status, String keyword, Pageable pageable) {
if (status != null) {
return equipmentRepository.findByStatus(status, pageable);
}
return equipmentRepository.findAll(pageable);
}
public Equipment getEquipment(Long equipId) {
return equipmentRepository.findById(equipId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
}
@Transactional
public Equipment createEquipment(CreateEquipmentRequest request) {
Equipment equipment = Equipment.builder()
.equipmentType(request.equipmentType())
.manufacturer(request.manufacturer())
.modelName(request.modelName())
.serialNumber(request.serialNumber())
.assetCode(request.assetCode())
.specifications(request.specifications())
.status(request.status() != null ? request.status() : EquipmentStatus.AVAILABLE)
.purchaseDate(request.purchaseDate())
.warrantyExpDate(request.warrantyExpDate())
.build();
return equipmentRepository.save(equipment);
}
@Transactional
public Equipment updateEquipment(Long equipId, UpdateEquipmentRequest request) {
Equipment equipment = getEquipment(equipId);
if (request.equipmentType() != null) {
equipment.setEquipmentType(request.equipmentType());
}
if (request.manufacturer() != null) {
equipment.setManufacturer(request.manufacturer());
}
if (request.modelName() != null) {
equipment.setModelName(request.modelName());
}
if (request.serialNumber() != null) {
equipment.setSerialNumber(request.serialNumber());
}
if (request.assetCode() != null) {
equipment.setAssetCode(request.assetCode());
}
if (request.specifications() != null) {
equipment.setSpecifications(request.specifications());
}
if (request.status() != null) {
equipment.setStatus(request.status());
}
if (request.purchaseDate() != null) {
equipment.setPurchaseDate(request.purchaseDate());
}
if (request.warrantyExpDate() != null) {
equipment.setWarrantyExpDate(request.warrantyExpDate());
}
return equipment;
}
@Transactional
public void deleteEquipment(Long equipId) {
Equipment equipment = getEquipment(equipId);
equipmentRepository.delete(equipment);
}
@Transactional
public EquipmentAssignment assignEquipment(Long equipId, Long userId, String condition) {
Equipment equipment = equipmentRepository.findById(equipId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
if (equipment.getStatus() != EquipmentStatus.AVAILABLE) {
throw new BusinessException(ErrorCode.EQUIP_NOT_AVAILABLE);
}
Employee user = employeeRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
if (user.getEmpStatus() != EmpStatus.ACTIVE) {
throw new BusinessException(ErrorCode.INVALID_ASSIGNEE);
}
try {
equipment.setStatus(EquipmentStatus.IN_USE);
equipmentRepository.save(equipment);
EquipmentAssignment assignment = EquipmentAssignment.builder()
.equipment(equipment)
.user(user)
.assignedDate(LocalDateTime.now())
.condOnAssign(condition)
.build();
return equipmentAssignmentRepository.save(assignment);
} catch (OptimisticLockException e) {
throw new BusinessException(ErrorCode.SYSTEM_CONCURRENCY);
}
}
@Transactional
public EquipmentAssignment returnEquipment(Long equipId, String condition) {
Equipment equipment = equipmentRepository.findById(equipId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
EquipmentAssignment activeAssignment = equipmentAssignmentRepository
.findByEquipment_EquipmentIdAndReturnedDateIsNull(equipId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
activeAssignment.setReturnedDate(LocalDateTime.now());
activeAssignment.setCondOnReturn(condition);
equipment.setStatus(EquipmentStatus.AVAILABLE);
equipmentRepository.save(equipment);
return equipmentAssignmentRepository.save(activeAssignment);
}
public List<EquipmentAssignment> getAssignments(Long equipId) {
return equipmentAssignmentRepository.findByEquipment_EquipmentId(equipId);
}
@Transactional
public EquipmentMaintenance addMaintenance(Long equipId, CreateMaintenanceRequest request) {
Equipment equipment = getEquipment(equipId);
Employee reportedBy = employeeRepository.findByLoginId(request.reportedBy())
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
EquipmentMaintenance maintenance = EquipmentMaintenance.builder()
.equipment(equipment)
.reportedBy(reportedBy)
.maintenanceType(request.maintenanceType())
.issueDescription(request.issueDescription())
.actionTaken(request.actionTaken())
.cost(request.cost())
.reportedDate(LocalDateTime.now())
.build();
return equipmentMaintenanceRepository.save(maintenance);
}
public List<EquipmentMaintenance> getMaintenanceHistory(Long equipId) {
return equipmentMaintenanceRepository.findByEquipment_EquipmentId(equipId);
}
public Map<String, Long> getSummary() {
List<Equipment> all = equipmentRepository.findAll();
Map<String, Long> counts = new HashMap<>();
for (EquipmentStatus status : EquipmentStatus.values()) {
long count = all.stream()
.filter(e -> e.getStatus() == status)
.count();
counts.put(status.name(), count);
}
return counts;
}
@Transactional
public Equipment addImage(Long equipmentId, MultipartFile file) {
Equipment eq = equipmentRepository.findById(equipmentId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
try {
String originalName = file.getOriginalFilename();
String ext = "";
if (originalName != null && originalName.contains(".")) {
ext = originalName.substring(originalName.lastIndexOf("."));
}
String storedName = UUID.randomUUID().toString() + ext;
Path targetPath = Paths.get(uploadDir, storedName);
Files.copy(file.getInputStream(), targetPath);
String url = "/uploads/" + storedName;
ObjectMapper mapper = new ObjectMapper();
List<Map<String, String>> images = new ArrayList<>();
if (eq.getImages() != null && !eq.getImages().isEmpty() && !eq.getImages().equals("[]")) {
images = mapper.readValue(eq.getImages(), new TypeReference<List<Map<String, String>>>() {});
}
Map<String, String> newImage = new HashMap<>();
newImage.put("url", url);
newImage.put("name", originalName != null ? originalName : "unknown");
newImage.put("uploadedAt", LocalDateTime.now().toString());
images.add(newImage);
eq.setImages(mapper.writeValueAsString(images));
return equipmentRepository.save(eq);
} catch (IOException e) {
throw new RuntimeException("Failed to upload image", e);
}
}
@Transactional
public Equipment removeImage(Long equipmentId, int imageIndex) {
Equipment eq = equipmentRepository.findById(equipmentId)
.orElseThrow(() -> new BusinessException(ErrorCode.EQUIPMENT_NOT_FOUND));
try {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, String>> images = new ArrayList<>();
if (eq.getImages() != null && !eq.getImages().isEmpty()) {
images = mapper.readValue(eq.getImages(), new TypeReference<List<Map<String, String>>>() {});
}
if (imageIndex >= 0 && imageIndex < images.size()) {
images.remove(imageIndex);
}
eq.setImages(mapper.writeValueAsString(images));
return equipmentRepository.save(eq);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse images", e);
}
}
}
@@ -0,0 +1,54 @@
package com.srsystem.service;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Notification;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.NotificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NotificationService {
private final NotificationRepository notificationRepository;
private final EmployeeRepository employeeRepository;
public List<Notification> getMyNotifications(Long userId) {
return notificationRepository.findByUser_EmpIdOrderByCreatedAtDesc(userId);
}
public long getUnreadCount(Long userId) {
return notificationRepository.countByUser_EmpIdAndIsRead(userId, 'N');
}
@Transactional
public void markAsRead(Long notificationId) {
Notification notification = notificationRepository.findById(notificationId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
notification.setIsRead('Y');
notificationRepository.save(notification);
}
@Transactional
public Notification createNotification(Long userId, String type, String title, String message, String linkUrl) {
Employee user = employeeRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
Notification notification = Notification.builder()
.user(user)
.type(type)
.title(title)
.message(message)
.linkUrl(linkUrl)
.isRead('N')
.build();
return notificationRepository.save(notification);
}
}
@@ -0,0 +1,92 @@
package com.srsystem.service;
import com.srsystem.dto.HealthResponse;
import com.srsystem.dto.WakeRequest;
import com.srsystem.dto.WakeResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Service
public class ParkingService {
private static final Logger log = LoggerFactory.getLogger(ParkingService.class);
@Value("${parking.enabled:true}")
private boolean parkingEnabled;
@Value("${parking.schedule.weekday-start:20:00}")
private String weekdayStart;
@Value("${parking.schedule.weekday-end:08:00}")
private String weekdayEnd;
@Value("${parking.schedule.weekends:true}")
private boolean parkingOnWeekends;
public HealthResponse checkStatus() {
if (!parkingEnabled) {
return new HealthResponse(false, "Server is running (parking disabled)", null, "Parking disabled");
}
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
DayOfWeek dayOfWeek = today.getDayOfWeek();
boolean isWeekend = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
LocalTime startTime = LocalTime.parse(weekdayStart, DateTimeFormatter.ofPattern("HH:mm"));
LocalTime endTime = LocalTime.parse(weekdayEnd, DateTimeFormatter.ofPattern("HH:mm"));
boolean isParkingTime;
if (isWeekend) {
isParkingTime = parkingOnWeekends;
} else {
if (startTime.isBefore(endTime)) {
isParkingTime = !now.isBefore(startTime) && now.isBefore(endTime);
} else {
isParkingTime = !now.isBefore(startTime) || now.isBefore(endTime);
}
}
String message = isParkingTime ? "Server is currently parked" : "Server is running";
String resumeTime = isParkingTime ? calculateResumeTime(today, now) : null;
String schedule = String.format("Weekdays: %s-%s, Weekends: %s", weekdayStart, weekdayEnd, parkingOnWeekends ? "Parked" : "Running");
return new HealthResponse(isParkingTime, message, resumeTime, schedule);
}
private String calculateResumeTime(LocalDate today, LocalTime now) {
DayOfWeek dayOfWeek = today.getDayOfWeek();
LocalTime endTime = LocalTime.parse(weekdayEnd, DateTimeFormatter.ofPattern("HH:mm"));
if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
if (!parkingOnWeekends) {
return today.atTime(endTime).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
}
LocalDate nextWeekday = today.with(DayOfWeek.MONDAY);
return nextWeekday.atTime(endTime).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
} else {
return today.atTime(endTime).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
}
}
public WakeResponse requestWake(WakeRequest request) {
log.info("Wake request received - requestedBy: {}, reason: {}, requestedAt: {}",
request.requestedBy(), request.reason(), request.requestedAt());
String estimatedTime = LocalTime.now().plusMinutes(5)
.format(DateTimeFormatter.ofPattern("HH:mm"));
return new WakeResponse(
true,
"iLO4 remote boot initiated. Server will be available shortly.",
estimatedTime
);
}
}
@@ -0,0 +1,94 @@
package com.srsystem.service;
import com.srsystem.dto.PortfolioResponse;
import com.srsystem.entity.*;
import com.srsystem.entity.enums.TicketStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PortfolioService {
private final DispatchHistoryRepository dispatchHistoryRepository;
private final ReportRepository reportRepository;
private final TicketRepository ticketRepository;
public List<PortfolioResponse> getPortfolio(Long empId) {
List<DispatchHistory> histories = dispatchHistoryRepository.findByEmployee_EmpId(empId);
return histories.stream().map(dh -> {
PortfolioResponse resp = new PortfolioResponse();
resp.setDispatchId(dh.getDispatchId());
resp.setProjectName(dh.getProject().getProjectName());
resp.setRoleTitle(dh.getRoleTitle());
resp.setContributionRate(dh.getContributionRate());
resp.setIndividualTech(dh.getIndividualTech());
resp.setPerformedTasks(dh.getPerformedTasks());
resp.setAchievements(dh.getAchievements());
resp.setStartDate(dh.getStartDate());
resp.setEndDate(dh.getEndDate());
resp.setInternalRating(dh.getInternalRating());
// Bind reports for this dispatch
List<PortfolioResponse.BoundReport> reports = reportRepository
.findByDispatch_DispatchId(dh.getDispatchId())
.stream().map(r -> {
PortfolioResponse.BoundReport br = new PortfolioResponse.BoundReport();
br.targetDate = r.getTargetDate() != null ? r.getTargetDate().toString() : "";
br.reportType = r.getReportType().name();
br.completedTasks = r.getCompletedTasks();
return br;
}).collect(Collectors.toList());
resp.setReports(reports);
// Bind resolved + closed tickets for this project, by this employee
List<PortfolioResponse.BoundTicket> tickets = new ArrayList<>();
ticketRepository
.findByProject_ProjectIdAndRequester_EmpIdAndStatus(
dh.getProject().getProjectId(), empId, TicketStatus.RESOLVED)
.forEach(t -> {
PortfolioResponse.BoundTicket bt = new PortfolioResponse.BoundTicket();
bt.ticketId = t.getTicketId();
bt.title = t.getTitle();
bt.category = t.getCategory().name();
bt.resolutionNote = t.getResolutionNote();
tickets.add(bt);
});
ticketRepository
.findByProject_ProjectIdAndRequester_EmpIdAndStatus(
dh.getProject().getProjectId(), empId, TicketStatus.CLOSED)
.forEach(t -> {
PortfolioResponse.BoundTicket bt = new PortfolioResponse.BoundTicket();
bt.ticketId = t.getTicketId();
bt.title = t.getTitle();
bt.category = t.getCategory().name();
bt.resolutionNote = t.getResolutionNote();
tickets.add(bt);
});
resp.setResolvedTickets(tickets);
return resp;
}).collect(Collectors.toList());
}
@Transactional
public void updateAchievements(Long dispatchId, String achievements) {
DispatchHistory dh = dispatchHistoryRepository.findById(dispatchId)
.orElseThrow(() -> new BusinessException(ErrorCode.DISPATCH_NOT_FOUND));
dh.setAchievements(achievements);
dispatchHistoryRepository.save(dh);
}
}
@@ -0,0 +1,112 @@
package com.srsystem.service;
import com.srsystem.dto.CreateProjectRequest;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Project;
import com.srsystem.entity.enums.ProjectStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.DispatchHistoryRepository;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProjectService {
private final ProjectRepository projectRepository;
private final DispatchHistoryRepository dispatchHistoryRepository;
private final EmployeeRepository employeeRepository;
public List<Project> getMyProjects(Long empId) {
List<DispatchHistory> histories = dispatchHistoryRepository.findByEmployee_EmpId(empId);
Set<Long> projectIds = histories.stream()
.map(h -> h.getProject().getProjectId())
.collect(Collectors.toSet());
return projectRepository.findAllById(projectIds);
}
public Project getProject(Long projectId) {
return projectRepository.findById(projectId)
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
}
@Transactional(readOnly = true)
public List<Project> findAll(ProjectStatus status) {
if (status != null) {
return projectRepository.findByStatus(status);
}
return projectRepository.findAll();
}
@Transactional
public Project create(CreateProjectRequest req) {
Employee internalManager = null;
if (req.internalManagerId() != null) {
internalManager = employeeRepository.findById(req.internalManagerId())
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
}
Project project = Project.builder()
.projectName(req.projectName())
.projectType(req.projectType())
.contractCompany(req.contractCompany())
.endUserCompany(req.endUserCompany())
.contractType(req.contractType())
.status(ProjectStatus.PREPARING)
.startDate(req.startDate())
.endDate(req.endDate())
.workLocationType(req.workLocationType())
.workAddress(req.workAddress())
.techEnvironment(req.techEnvironment())
.mainTaskDesc(req.mainTaskDesc())
.internalManager(internalManager)
.build();
return projectRepository.save(project);
}
@Transactional
public Project update(Long projectId, CreateProjectRequest req) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
Employee internalManager = null;
if (req.internalManagerId() != null) {
internalManager = employeeRepository.findById(req.internalManagerId())
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
}
project.setProjectName(req.projectName());
project.setProjectType(req.projectType());
project.setContractCompany(req.contractCompany());
project.setEndUserCompany(req.endUserCompany());
project.setContractType(req.contractType());
project.setStartDate(req.startDate());
project.setEndDate(req.endDate());
project.setWorkLocationType(req.workLocationType());
project.setWorkAddress(req.workAddress());
project.setTechEnvironment(req.techEnvironment());
project.setMainTaskDesc(req.mainTaskDesc());
project.setInternalManager(internalManager);
return projectRepository.save(project);
}
@Transactional
public void delete(Long projectId) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
projectRepository.delete(project);
}
}
@@ -0,0 +1,246 @@
package com.srsystem.service;
import com.srsystem.dto.CreateReportRequest;
import com.srsystem.dto.ReportSummary;
import com.srsystem.dto.ReviewReportRequest;
import com.srsystem.dto.TicketSummary;
import com.srsystem.dto.WorkReportResponse;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Notification;
import com.srsystem.entity.Report;
import com.srsystem.entity.Ticket;
import com.srsystem.entity.WeeklyPlan;
import com.srsystem.entity.enums.ReportStatus;
import com.srsystem.entity.enums.ReportType;
import com.srsystem.entity.enums.TicketStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.DispatchHistoryRepository;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.NotificationRepository;
import com.srsystem.repository.ReportRepository;
import com.srsystem.repository.TicketRepository;
import com.srsystem.repository.WeeklyPlanRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReportService {
private final ReportRepository reportRepository;
private final EmployeeRepository employeeRepository;
private final DispatchHistoryRepository dispatchHistoryRepository;
private final TicketRepository ticketRepository;
private final NotificationRepository notificationRepository;
private final WeeklyPlanRepository weeklyPlanRepository;
public Page<WorkReportResponse> getReports(ReportStatus status, String type, LocalDate targetDate, Long userId, Pageable pageable) {
Page<Report> reports;
if (userId != null) {
reports = reportRepository.findByUser_EmpId(userId, pageable);
} else if (status != null) {
reports = reportRepository.findByStatus(status, pageable);
} else {
reports = reportRepository.findAll(pageable);
}
return reports.map(this::toWorkReportResponse);
}
public Page<WorkReportResponse> findByUserId(Long userId, ReportStatus status, String type, LocalDate targetDate, Pageable pageable) {
ReportType reportType = type != null ? ReportType.valueOf(type) : null;
Page<Report> reports = reportRepository.findByUser_EmpIdAndStatusAndReportType(userId, status, reportType, targetDate, pageable);
return reports.map(this::toWorkReportResponse);
}
public WorkReportResponse getReport(Long reportId) {
Report report = reportRepository.findById(reportId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
return toWorkReportResponse(report);
}
@Transactional
public WorkReportResponse createReport(Long userId, CreateReportRequest request) {
if (request.targetDate().isAfter(LocalDate.now())) {
throw new BusinessException(ErrorCode.FUTURE_REPORT_DATE);
}
Employee user = employeeRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
DispatchHistory dispatch = null;
if (request.dispatchId() != null) {
dispatch = dispatchHistoryRepository.findById(request.dispatchId())
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
}
Report report = Report.builder()
.dispatch(dispatch)
.user(user)
.reportType(request.reportType())
.targetDate(request.targetDate())
.completedTasks(request.completedTasks())
.plannedTasks(request.plannedTasks())
.issuesAndRisks(request.issuesAndRisks())
.supportRequested(request.supportRequested())
.status(ReportStatus.DRAFT)
.build();
if (request.workHours() != null) report.setWorkHours(request.workHours());
if (request.blockerFlag() != null) report.setBlockerFlag(request.blockerFlag().charAt(0));
if (request.artifacts() != null) report.setArtifacts(request.artifacts());
if (request.weeklyPlanId() != null) {
weeklyPlanRepository.findById(request.weeklyPlanId())
.ifPresent(report::setWeeklyPlan);
}
Report saved = reportRepository.save(report);
return toWorkReportResponse(saved);
}
@Transactional
public WorkReportResponse updateStatus(Long reportId, Long userId, ReportStatus newStatus) {
Report report = reportRepository.findById(reportId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
if (report.getStatus() == ReportStatus.APPROVED || report.getStatus() == ReportStatus.REJECTED) {
throw new BusinessException(ErrorCode.LOCKED_REPORT);
}
if (newStatus == ReportStatus.SUBMITTED) {
report.setStatus(newStatus);
}
Report updated = reportRepository.save(report);
return toWorkReportResponse(updated);
}
@Transactional
public WorkReportResponse reviewReport(Long reportId, Long reviewerId, ReviewReportRequest request) {
Report report = reportRepository.findById(reportId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
if (report.getStatus() == ReportStatus.APPROVED || report.getStatus() == ReportStatus.REJECTED) {
throw new BusinessException(ErrorCode.LOCKED_REPORT);
}
if (request.status() == ReportStatus.REJECTED &&
(request.reviewComment() == null || request.reviewComment().isBlank())) {
throw new BusinessException(ErrorCode.LOCKED_REPORT);
}
Employee reviewer = employeeRepository.findById(reviewerId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
report.setStatus(request.status());
report.setReviewer(reviewer);
report.setReviewComment(request.reviewComment());
report.setReviewedAt(LocalDateTime.now());
Report updated = reportRepository.save(report);
// Send notification if rejected
if (request.status() == ReportStatus.REJECTED) {
Notification notification = Notification.builder()
.user(report.getUser())
.type("REPORT_REJECTED")
.title("Daily report rejected")
.message(String.format("Your report for %s was rejected. Reason: %s", report.getTargetDate(), request.reviewComment()))
.isRead('N')
.build();
notificationRepository.save(notification);
}
return toWorkReportResponse(updated);
}
@Transactional
public void deleteReport(Long reportId) {
Report report = reportRepository.findById(reportId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
if (report.getStatus() == ReportStatus.APPROVED || report.getStatus() == ReportStatus.REJECTED) {
throw new BusinessException(ErrorCode.LOCKED_REPORT);
}
reportRepository.delete(report);
}
private WorkReportResponse toWorkReportResponse(Report report) {
String projectName = report.getDispatch() != null
? report.getDispatch().getProject().getProjectName()
: null;
String reviewerName = report.getReviewer() != null
? report.getReviewer().getName()
: null;
return new WorkReportResponse(
report.getReportId(),
report.getUser().getName(),
projectName,
report.getReportType(),
report.getTargetDate(),
report.getCompletedTasks(),
report.getPlannedTasks(),
report.getIssuesAndRisks(),
report.getSupportRequested(),
report.getStatus(),
reviewerName,
report.getReviewComment(),
report.getReviewedAt(),
report.getCreatedAt(),
report.getWorkHours(),
String.valueOf(report.getBlockerFlag()),
report.getArtifacts()
);
}
public ReportSummary getSummary() {
return new ReportSummary(
reportRepository.countByStatus(ReportStatus.SUBMITTED),
reportRepository.countByStatus(ReportStatus.APPROVED),
reportRepository.countByStatus(ReportStatus.REJECTED),
reportRepository.countByStatus(ReportStatus.DRAFT)
);
}
public List<TicketSummary> getResolvedTicketsForDate(Long userId, String date) {
LocalDate targetDate = LocalDate.parse(date);
LocalDateTime startOfDay = targetDate.atStartOfDay();
LocalDateTime endOfDay = targetDate.atTime(LocalTime.MAX);
List<Ticket> resolvedTickets = ticketRepository
.findByRequester_EmpIdAndStatusAndResolvedAtBetween(userId, TicketStatus.RESOLVED, startOfDay, endOfDay);
List<Ticket> closedTickets = ticketRepository
.findByRequester_EmpIdAndStatusAndResolvedAtBetween(userId, TicketStatus.CLOSED, startOfDay, endOfDay);
List<Ticket> all = new ArrayList<>();
all.addAll(resolvedTickets);
all.addAll(closedTickets);
return all.stream().map(t -> new TicketSummary(
t.getTicketId(),
t.getTitle(),
t.getCategory().name(),
t.getPriority().name(),
t.getResolutionNote(),
t.getResolvedAt() != null ? t.getResolvedAt().toString() : null
)).collect(Collectors.toList());
}
}
@@ -0,0 +1,203 @@
package com.srsystem.service;
import com.srsystem.dto.CreateCommentRequest;
import com.srsystem.dto.CreateTicketRequest;
import com.srsystem.dto.IssueTicket;
import com.srsystem.dto.TicketCommentResponse;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Project;
import com.srsystem.entity.Ticket;
import com.srsystem.entity.TicketComment;
import com.srsystem.entity.enums.TicketStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.ProjectRepository;
import com.srsystem.repository.TicketCommentRepository;
import com.srsystem.repository.TicketRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TicketService {
private final TicketRepository ticketRepository;
private final TicketCommentRepository ticketCommentRepository;
private final EmployeeRepository employeeRepository;
private final ProjectRepository projectRepository;
public Page<IssueTicket> getTickets(TicketStatus status, String category, String priority, String keyword, Pageable pageable, Long requesterId) {
Page<Ticket> tickets;
if (requesterId != null) {
tickets = ticketRepository.findByRequester_EmpId(requesterId, pageable);
} else if (status != null) {
tickets = ticketRepository.findByStatus(status, pageable);
} else {
tickets = ticketRepository.findAll(pageable);
}
return tickets.map(this::toIssueTicket);
}
public IssueTicket getTicket(Long ticketId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
return toIssueTicket(ticket);
}
@Transactional
public IssueTicket createTicket(Long requesterId, CreateTicketRequest request) {
Employee requester = employeeRepository.findById(requesterId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
Project project = projectRepository.findById(request.projectId())
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_NOT_FOUND));
Ticket ticket = Ticket.builder()
.project(project)
.requester(requester)
.category(request.category())
.priority(request.priority())
.title(request.title())
.description(request.description())
.status(TicketStatus.OPEN)
.slaWarnFlag('N')
.build();
Ticket saved = ticketRepository.save(ticket);
return toIssueTicket(saved);
}
@Transactional
public IssueTicket updateStatus(Long ticketId, Long userId, TicketStatus newStatus) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
if (ticket.getStatus() == TicketStatus.CLOSED) {
throw new BusinessException(ErrorCode.CLOSED_TICKET_MOD);
}
if (newStatus == TicketStatus.IN_PROGRESS && ticket.getAssignee() == null) {
throw new BusinessException(ErrorCode.ASSIGNEE_REQUIRED);
}
if (newStatus == TicketStatus.RESOLVED && ticket.getResolvedAt() == null) {
ticket.setResolvedAt(LocalDateTime.now());
}
ticket.setStatus(newStatus);
Ticket updated = ticketRepository.save(ticket);
return toIssueTicket(updated);
}
@Transactional
public IssueTicket assignTicket(Long ticketId, Long assigneeId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
Employee assignee = employeeRepository.findById(assigneeId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
ticket.setAssignee(assignee);
if (ticket.getStatus() == TicketStatus.OPEN) {
ticket.setStatus(TicketStatus.ASSIGNED);
}
Ticket updated = ticketRepository.save(ticket);
return toIssueTicket(updated);
}
@Transactional
public IssueTicket resolveTicket(Long ticketId, String resolutionNote) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
if (ticket.getStatus() == TicketStatus.CLOSED) {
throw new BusinessException(ErrorCode.CLOSED_TICKET_MOD);
}
ticket.setStatus(TicketStatus.RESOLVED);
ticket.setResolvedAt(LocalDateTime.now());
ticket.setResolutionNote(resolutionNote);
Ticket updated = ticketRepository.save(ticket);
return toIssueTicket(updated);
}
@Transactional
public TicketCommentResponse addComment(Long ticketId, Long userId, CreateCommentRequest request) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
if (ticket.getStatus() == TicketStatus.CLOSED) {
throw new BusinessException(ErrorCode.CLOSED_TICKET_MOD);
}
Employee user = employeeRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
TicketComment comment = TicketComment.builder()
.ticket(ticket)
.user(user)
.commentText(request.commentText())
.attachedFileUrl(request.attachedFileUrl())
.build();
TicketComment saved = ticketCommentRepository.save(comment);
return new TicketCommentResponse(
saved.getCommentId(),
saved.getUser().getName(),
saved.getCommentText(),
saved.getAttachedFileUrl(),
saved.getCreatedAt()
);
}
public List<TicketCommentResponse> getComments(Long ticketId) {
List<TicketComment> comments = ticketCommentRepository.findByTicket_TicketIdOrderByCreatedAtAsc(ticketId);
return comments.stream()
.map(c -> new TicketCommentResponse(
c.getCommentId(),
c.getUser().getName(),
c.getCommentText(),
c.getAttachedFileUrl(),
c.getCreatedAt()
))
.toList();
}
@Transactional
public void deleteTicket(Long ticketId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
ticketRepository.delete(ticket);
}
private IssueTicket toIssueTicket(Ticket ticket) {
return new IssueTicket(
ticket.getTicketId(),
ticket.getProject().getProjectName(),
ticket.getRequester().getName(),
ticket.getAssignee() != null ? ticket.getAssignee().getName() : null,
ticket.getCategory(),
ticket.getPriority(),
ticket.getTitle(),
ticket.getDescription(),
ticket.getStatus(),
ticket.getDueDate(),
ticket.getResolvedAt(),
ticket.getResolutionNote(),
ticket.getSlaWarnFlag() == 'Y',
ticket.getCreatedAt()
);
}
}
@@ -0,0 +1,76 @@
package com.srsystem.service;
import com.srsystem.dto.TrainingSummary;
import com.srsystem.dto.UpdateProgressRequest;
import com.srsystem.entity.Employee;
import com.srsystem.entity.TrainingProgress;
import com.srsystem.entity.enums.TrainingStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.TrainingProgressRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TrainingService {
private final TrainingProgressRepository trainingProgressRepository;
private final EmployeeRepository employeeRepository;
public List<TrainingProgress> getMyProgress(Long userId) {
return trainingProgressRepository.findByUser_EmpId(userId);
}
@Transactional
public TrainingProgress updateProgress(Long progressId, UpdateProgressRequest request) {
TrainingProgress progress = trainingProgressRepository.findById(progressId)
.orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
if (request.progressRate() != null) {
progress.setProgressRate(request.progressRate());
}
if (request.status() != null) {
progress.setStatus(request.status());
}
if (progress.getProgressRate() == 100) {
progress.setStatus(TrainingStatus.COMPLETED);
}
return trainingProgressRepository.save(progress);
}
public List<TrainingSummary> getSummary() {
List<Employee> employees = employeeRepository.findAll();
return employees.stream()
.map(emp -> {
List<TrainingProgress> progresses = trainingProgressRepository.findByUser_EmpId(emp.getEmpId());
long totalCourses = progresses.size();
long completedCount = progresses.stream()
.filter(p -> p.getStatus() == TrainingStatus.COMPLETED)
.count();
double avgProgressRate = progresses.stream()
.mapToInt(TrainingProgress::getProgressRate)
.average()
.orElse(0.0);
return new TrainingSummary(
emp.getEmpId(),
emp.getName(),
totalCourses,
completedCount,
avgProgressRate
);
})
.collect(Collectors.toList());
}
}
@@ -0,0 +1,61 @@
package com.srsystem.service;
import com.srsystem.entity.Employee;
import com.srsystem.entity.WeeklyPlan;
import com.srsystem.entity.enums.WeeklyPlanStatus;
import com.srsystem.exception.BusinessException;
import com.srsystem.exception.ErrorCode;
import com.srsystem.repository.EmployeeRepository;
import com.srsystem.repository.WeeklyPlanRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class WeeklyPlanService {
private final WeeklyPlanRepository weeklyPlanRepository;
private final EmployeeRepository employeeRepository;
public List<WeeklyPlan> getMyPlans(Long userId) {
return weeklyPlanRepository.findByUser_EmpIdOrderByWeekStartDesc(userId);
}
public WeeklyPlan getPlan(Long planId) {
return weeklyPlanRepository.findById(planId)
.orElseThrow(() -> new BusinessException(ErrorCode.PLAN_NOT_FOUND));
}
@Transactional
public WeeklyPlan createPlan(Long userId, String tasksJson) {
Employee user = employeeRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
LocalDate weekStart = LocalDate.now().with(java.time.DayOfWeek.MONDAY);
WeeklyPlan plan = WeeklyPlan.builder()
.user(user)
.weekStart(weekStart)
.status(WeeklyPlanStatus.DRAFT)
.tasks(tasksJson)
.build();
return weeklyPlanRepository.save(plan);
}
@Transactional
public WeeklyPlan updatePlan(Long planId, String tasksJson, WeeklyPlanStatus status) {
WeeklyPlan plan = getPlan(planId);
if (tasksJson != null) plan.setTasks(tasksJson);
if (status != null) plan.setStatus(status);
return weeklyPlanRepository.save(plan);
}
@Transactional
public void deletePlan(Long planId) {
weeklyPlanRepository.deleteById(planId);
}
}
@@ -0,0 +1,22 @@
package com.srsystem.service.notification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class EmailSender {
@Value("${notification.email.enabled:false}")
private boolean emailEnabled;
public void send(String to, String subject, String body) {
if (!emailEnabled) {
log.debug("Email notification disabled, skipping");
return;
}
// TODO: Implement JavaMailSender or external email API
log.info("Email notification sent: to={}, subject={}", to, subject);
}
}
@@ -0,0 +1,22 @@
package com.srsystem.service.notification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SlackSender {
@Value("${notification.slack.webhook-url:}")
private String webhookUrl;
public void send(String title, String message) {
if (webhookUrl.isBlank()) {
log.debug("Slack webhook URL not configured, skipping Slack notification");
return;
}
// TODO: Implement Slack Webhook API call
log.info("Slack notification sent: title={}", title);
}
}