diff options
Diffstat (limited to 'components/knox/approval/ApprovalDetail.tsx')
| -rw-r--r-- | components/knox/approval/ApprovalDetail.tsx | 156 |
1 files changed, 154 insertions, 2 deletions
diff --git a/components/knox/approval/ApprovalDetail.tsx b/components/knox/approval/ApprovalDetail.tsx index 1be58d21..e3197522 100644 --- a/components/knox/approval/ApprovalDetail.tsx +++ b/components/knox/approval/ApprovalDetail.tsx @@ -7,11 +7,13 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; +import { Textarea } from '@/components/ui/textarea'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { toast } from 'sonner'; -import { Loader2, Search, FileText, Clock, User, AlertCircle } from 'lucide-react'; +import { Loader2, Search, FileText, Clock, User, AlertCircle, XCircle } from 'lucide-react'; // API 함수 및 타입 -import { getApprovalDetail, getApprovalContent } from '@/lib/knox-api/approval/approval'; +import { getApprovalDetail, getApprovalContent, cancelApproval } from '@/lib/knox-api/approval/approval'; import type { ApprovalDetailResponse, ApprovalContentResponse, ApprovalLine } from '@/lib/knox-api/approval/approval'; import { formatDate } from '@/lib/utils'; @@ -70,6 +72,8 @@ export default function ApprovalDetail({ const [approvalData, setApprovalData] = useState<ApprovalDetailData | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); + const [isCancelling, setIsCancelling] = useState(false); + const [cancelOpinion, setCancelOpinion] = useState(''); const fetchApprovalDetail = async (id: string) => { if (!id.trim()) { @@ -142,6 +146,49 @@ export default function ApprovalDetail({ } }; + const canCancelApproval = (status: string) => { + // 진행중(1), 보류(0) 상태에서만 취소 가능 + return ['0', '1'].includes(status); + }; + + const handleCancelApproval = async () => { + if (!approvalData) return; + + if (!cancelOpinion.trim()) { + toast.error('상신취소 의견을 입력해주세요.'); + return; + } + + setIsCancelling(true); + + try { + const response = await cancelApproval(approvalData.detail.apInfId, cancelOpinion); + + if (response.result === 'success') { + toast.success('결재가 성공적으로 취소되었습니다.'); + + // 상태 업데이트 + setApprovalData({ + ...approvalData, + detail: { + ...approvalData.detail, + status: '4' // 상신취소 + } + }); + + // 의견 초기화 + setCancelOpinion(''); + } else { + toast.error('결재 취소에 실패했습니다.'); + } + } catch (err) { + console.error('결재 취소 오류:', err); + toast.error('결재 취소 중 오류가 발생했습니다.'); + } finally { + setIsCancelling(false); + } + }; + // 첨부파일 다운로드 헬퍼 const handleDownload = async (attachment: ApprovalAttachment) => { try { @@ -301,6 +348,111 @@ export default function ApprovalDetail({ </div> </div> + {/* 결재 취소 섹션 */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold flex items-center gap-2"> + <XCircle className="w-5 h-5" /> + 결재 취소 + </h3> + + <div className={`p-4 rounded-lg border ${ + canCancelApproval(approvalData.detail.status) + ? 'bg-blue-50 border-blue-200' + : 'bg-yellow-50 border-yellow-200' + }`}> + <div className="flex items-center gap-2 mb-2"> + <span className={`font-medium ${ + canCancelApproval(approvalData.detail.status) + ? 'text-blue-700' + : 'text-yellow-700' + }`}> + {canCancelApproval(approvalData.detail.status) ? '취소 가능' : '취소 불가'} + </span> + </div> + <p className={`text-sm ${ + canCancelApproval(approvalData.detail.status) + ? 'text-blue-600' + : 'text-yellow-600' + }`}> + {canCancelApproval(approvalData.detail.status) + ? '이 결재는 취소할 수 있습니다.' + : '현재 상태에서는 취소할 수 없습니다.'} + </p> + </div> + + {/* 취소 의견 및 버튼 */} + {canCancelApproval(approvalData.detail.status) && ( + <div className="space-y-4"> + <div> + <Label htmlFor="cancelOpinion" className="text-sm font-medium"> + 상신취소 의견 <span className="text-red-500">*</span> + </Label> + <Textarea + id="cancelOpinion" + placeholder="상신취소 사유를 입력해주세요" + value={cancelOpinion} + onChange={(e) => setCancelOpinion(e.target.value)} + className="mt-1" + rows={3} + /> + <p className="text-xs text-gray-500 mt-1"> + 상신취소 의견은 필수 입력 항목입니다. + </p> + </div> + + <div className="flex justify-end"> + <AlertDialog> + <AlertDialogTrigger asChild> + <Button + variant="destructive" + disabled={isCancelling || !cancelOpinion.trim()} + > + {isCancelling ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + 취소 중... + </> + ) : ( + <> + <XCircle className="w-4 h-4 mr-2" /> + 결재 취소 + </> + )} + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>결재 취소 확인</AlertDialogTitle> + <AlertDialogDescription> + 정말로 이 결재를 취소하시겠습니까? + <br /> + <br /> + <strong>결재 ID:</strong> {approvalData.detail.apInfId} + <br /> + <strong>제목:</strong> {approvalData.detail.subject} + <br /> + <strong>취소 의견:</strong> {cancelOpinion} + <br /> + <br /> + 이 작업은 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleCancelApproval} + className="bg-red-600 hover:bg-red-700" + > + 결재 취소 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + </div> + )} + </div> + <Separator /> {/* 결재 내용 */} |
