feat: approval workflow with approver/backupApprover

This commit is contained in:
pray4skylark
2026-06-08 17:49:39 +09:00
parent 9c09a89643
commit 3047354571
17 changed files with 461 additions and 82 deletions
@@ -65,6 +65,11 @@ public class EmployeeService {
return results;
}
public List<EmployeeSearchResult> getApprovers() {
return employeeRepository.findByRoleCodeIn(List.of(RoleCode.MANAGER, RoleCode.ADMIN))
.stream().map(this::toSearchResult).toList();
}
private EmployeeSearchResult toSearchResult(Employee e) {
return new EmployeeSearchResult(
e.getEmpId(),
@@ -107,6 +112,8 @@ public class EmployeeService {
employee.setProfileImage(updateRequest.getProfileImage());
employee.setSchoolName(updateRequest.getSchoolName());
employee.setPosition(updateRequest.getPosition());
employee.setManagerId(updateRequest.getManagerId());
employee.setBackupManagerId(updateRequest.getBackupManagerId());
return employee;
}
@@ -59,13 +59,20 @@ public class TicketService {
Employee requester = employeeRepository.findById(requesterId)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
boolean needsApproval = switch (request.category()) {
case VACATION, EXPENSE, EQUIPMENT_REQUEST, GENERAL -> true;
default -> false;
};
TicketStatus initialStatus = needsApproval ? TicketStatus.PENDING_APPROVAL : TicketStatus.OPEN;
Ticket.TicketBuilder builder = Ticket.builder()
.requester(requester)
.category(request.category())
.priority(request.priority())
.title(request.title())
.description(request.description())
.status(TicketStatus.OPEN)
.status(initialStatus)
.slaWarnFlag('N')
.attachmentUrl(request.attachmentUrl())
.attachmentName(request.attachmentName());
@@ -76,6 +83,15 @@ public class TicketService {
builder.project(project);
}
if (needsApproval) {
Long approverId = request.approverId() != null ? request.approverId() : requester.getManagerId();
builder.approverId(approverId);
Long backupId = request.backupApproverId() != null ? request.backupApproverId() : requester.getBackupManagerId();
if (backupId != null && !backupId.equals(approverId)) {
builder.backupApproverId(backupId);
}
}
Ticket saved = ticketRepository.save(builder.build());
return toIssueTicket(saved);
}
@@ -185,6 +201,39 @@ public class TicketService {
ticketRepository.delete(ticket);
}
public List<IssueTicket> getPendingApprovals(Long approverId) {
List<TicketStatus> statuses = List.of(TicketStatus.PENDING_APPROVAL);
List<Ticket> tickets = ticketRepository.findByStatusInAndApproverIdOrBackupApproverId(statuses, approverId);
return tickets.stream().map(this::toIssueTicket).toList();
}
@Transactional
public IssueTicket approveTicket(Long ticketId, Long approverId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
if (ticket.getStatus() != TicketStatus.PENDING_APPROVAL) {
throw new BusinessException(ErrorCode.INVALID_STATUS);
}
ticket.setStatus(TicketStatus.APPROVED);
ticket.setApprovedBy(approverId);
ticket.setApprovedAt(LocalDateTime.now());
return toIssueTicket(ticketRepository.save(ticket));
}
@Transactional
public IssueTicket rejectTicket(Long ticketId, Long approverId, String reason) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
if (ticket.getStatus() != TicketStatus.PENDING_APPROVAL) {
throw new BusinessException(ErrorCode.INVALID_STATUS);
}
ticket.setStatus(TicketStatus.REJECTED);
ticket.setResolutionNote(reason);
ticket.setApprovedBy(approverId);
ticket.setApprovedAt(LocalDateTime.now());
return toIssueTicket(ticketRepository.save(ticket));
}
private IssueTicket toIssueTicket(Ticket ticket) {
return new IssueTicket(
ticket.getTicketId(),
@@ -202,7 +251,11 @@ public class TicketService {
ticket.getSlaWarnFlag() == 'Y',
ticket.getCreatedAt(),
ticket.getAttachmentUrl(),
ticket.getAttachmentName()
ticket.getAttachmentName(),
ticket.getApproverId(),
ticket.getBackupApproverId(),
ticket.getApprovedBy(),
ticket.getApprovedAt()
);
}
}