feat: back button + reopen for approved/rejected tickets
This commit is contained in:
@@ -135,6 +135,13 @@ public class TicketController {
|
||||
return ResponseEntity.ok(ApiResponse.ok("반려되었습니다", ticketService.rejectTicket(ticketId, currentUser.getEmpId(), body.get("reason"))));
|
||||
}
|
||||
|
||||
@Operation(summary = "티켓 재오픈", description = "APPROVED/REJECTED 상태를 PENDING_APPROVAL로 되돌립니다")
|
||||
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
|
||||
@PostMapping("/{ticketId}/reopen")
|
||||
public ResponseEntity<ApiResponse<IssueTicket>> reopenTicket(@PathVariable Long ticketId) {
|
||||
return ResponseEntity.ok(ApiResponse.ok("재오픈되었습니다", ticketService.reopenTicket(ticketId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "댓글 목록 조회", description = "티켓의 댓글 목록을 조회합니다")
|
||||
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
||||
@GetMapping("/{ticketId}/comments")
|
||||
|
||||
@@ -234,6 +234,19 @@ public class TicketService {
|
||||
return toIssueTicket(ticketRepository.save(ticket));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public IssueTicket reopenTicket(Long ticketId) {
|
||||
Ticket ticket = ticketRepository.findById(ticketId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND));
|
||||
if (ticket.getStatus() != TicketStatus.APPROVED && ticket.getStatus() != TicketStatus.REJECTED) {
|
||||
throw new BusinessException(ErrorCode.INVALID_STATUS);
|
||||
}
|
||||
ticket.setStatus(TicketStatus.PENDING_APPROVAL);
|
||||
ticket.setApprovedBy(null);
|
||||
ticket.setApprovedAt(null);
|
||||
return toIssueTicket(ticketRepository.save(ticket));
|
||||
}
|
||||
|
||||
private IssueTicket toIssueTicket(Ticket ticket) {
|
||||
return new IssueTicket(
|
||||
ticket.getTicketId(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronLeft, Clock, User, MessageSquare, Paperclip, Send, CheckCircle2, ArrowRight } from 'lucide-react';
|
||||
import { ChevronLeft, Clock, User, MessageSquare, Paperclip, Send, CheckCircle2, ArrowRight, RotateCcw } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import ticketService from '@/services/ticketService';
|
||||
import apiClient from '@/services/apiClient';
|
||||
@@ -72,7 +72,10 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
|
||||
|
||||
const currentIdx = statusFlow.indexOf(status);
|
||||
const canAdvance = currentIdx < statusFlow.length - 1;
|
||||
const canGoBack = currentIdx > 0;
|
||||
const isClosed = status === 'CLOSED';
|
||||
const isApprovalTicket = ['VACATION', 'EXPENSE', 'EQUIPMENT_REQUEST', 'GENERAL'].includes(ticket?.category || '');
|
||||
const isMgmt = user?.roleCode !== 'EMPLOYEE';
|
||||
|
||||
const handleAdvance = async () => {
|
||||
if (!canAdvance) return;
|
||||
@@ -85,6 +88,24 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = async () => {
|
||||
if (!canGoBack) return;
|
||||
const prevStatus = statusFlow[currentIdx - 1];
|
||||
try {
|
||||
await ticketService.updateStatus(ticketId, prevStatus);
|
||||
setStatus(prevStatus);
|
||||
} catch (err: unknown) {
|
||||
alert((err as { message?: string })?.message || '상태 변경 실패');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReopen = async () => {
|
||||
try {
|
||||
const res: any = await apiClient.post(`/tickets/${ticketId}/reopen`);
|
||||
if (res.success) setTicket(res.data);
|
||||
} catch (err: any) { alert(err.message || '재오픈 실패'); }
|
||||
};
|
||||
|
||||
const handlePostComment = async () => {
|
||||
if (!newComment.trim()) return;
|
||||
try {
|
||||
@@ -124,10 +145,27 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`text-xs font-semibold px-3 py-1.5 rounded-full ${statusColor[status]}`}>{statusLabel[status]}</span>
|
||||
{!isClosed && (
|
||||
{!isClosed && statusFlow.includes(status) && (
|
||||
<>
|
||||
{canGoBack && (
|
||||
<button onClick={handleBack} className="bg-gray-100 hover:bg-gray-200 text-text-muted text-xs font-semibold px-3 py-1.5 rounded-lg transition-all flex items-center">
|
||||
<ChevronLeft className="w-3.5 h-3.5 mr-1" /> Back
|
||||
</button>
|
||||
)}
|
||||
<button onClick={handleAdvance} disabled={!canAdvance} className="bg-primary hover:bg-primary-hover disabled:opacity-40 disabled:cursor-not-allowed text-white text-xs font-semibold px-4 py-1.5 rounded-lg transition-all flex items-center">
|
||||
{canAdvance ? <>Advance <ArrowRight className="w-3.5 h-3.5 ml-1.5" /></> : 'Completed'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{status === 'APPROVED' && isMgmt && (
|
||||
<button onClick={handleReopen} className="bg-amber-50 hover:bg-amber-100 text-amber-700 border border-amber-200 text-xs font-semibold px-3 py-1.5 rounded-lg transition-all">
|
||||
승인 취소
|
||||
</button>
|
||||
)}
|
||||
{status === 'REJECTED' && (
|
||||
<button onClick={handleReopen} className="bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-200 text-xs font-semibold px-3 py-1.5 rounded-lg transition-all">
|
||||
재오픈
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user