diff options
Diffstat (limited to 'components/knox/approval/ApprovalCancel.tsx')
| -rw-r--r-- | components/knox/approval/ApprovalCancel.tsx | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/components/knox/approval/ApprovalCancel.tsx b/components/knox/approval/ApprovalCancel.tsx new file mode 100644 index 00000000..d077bfc6 --- /dev/null +++ b/components/knox/approval/ApprovalCancel.tsx @@ -0,0 +1,341 @@ +'use client' + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { toast } from 'sonner'; +import { Loader2, XCircle, AlertTriangle, CheckCircle } from 'lucide-react'; + +// API 함수 및 타입 +import { cancelApproval, getApprovalDetail } from '@/lib/knox-api/approval/approval'; +import type { ApprovalDetailResponse } from '@/lib/knox-api/approval/approval'; + +// Mock 데이터 +import { mockApprovalAPI, getStatusText } from './mocks/approval-mock'; + +interface ApprovalCancelProps { + useFakeData?: boolean; + systemId?: string; + initialApInfId?: string; + onCancelSuccess?: (apInfId: string) => void; +} + +export default function ApprovalCancel({ + useFakeData = false, + systemId = 'EVCP_SYSTEM', + initialApInfId = '', + onCancelSuccess +}: ApprovalCancelProps) { + const [apInfId, setApInfId] = useState(initialApInfId); + const [approvalDetail, setApprovalDetail] = useState<ApprovalDetailResponse['data'] | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + const [error, setError] = useState<string | null>(null); + const [cancelResult, setCancelResult] = useState<{ apInfId: string } | null>(null); + + const fetchApprovalDetail = async () => { + if (!apInfId.trim()) { + toast.error('결재 ID를 입력해주세요.'); + return; + } + + setIsLoading(true); + setError(null); + setApprovalDetail(null); + setCancelResult(null); + + try { + const response = useFakeData + ? await mockApprovalAPI.getApprovalDetail(apInfId) + : await getApprovalDetail(apInfId, systemId); + + if (response.result === 'SUCCESS') { + setApprovalDetail(response.data); + } else { + setError('결재 정보를 가져오는데 실패했습니다.'); + toast.error('결재 정보를 가져오는데 실패했습니다.'); + } + } catch (err) { + console.error('결재 상세 조회 오류:', err); + setError('결재 정보를 가져오는 중 오류가 발생했습니다.'); + toast.error('결재 정보를 가져오는 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const handleCancelApproval = async () => { + if (!approvalDetail) return; + + setIsCancelling(true); + + try { + const response = useFakeData + ? await mockApprovalAPI.cancelApproval(approvalDetail.apInfId) + : await cancelApproval(approvalDetail.apInfId, systemId); + + if (response.result === 'SUCCESS') { + setCancelResult({ apInfId: response.data.apInfId }); + toast.success('결재가 성공적으로 취소되었습니다.'); + onCancelSuccess?.(response.data.apInfId); + + // 상태 업데이트 + setApprovalDetail({ + ...approvalDetail, + status: '4' // 상신취소 + }); + } else { + toast.error('결재 취소에 실패했습니다.'); + } + } catch (err) { + console.error('결재 취소 오류:', err); + toast.error('결재 취소 중 오류가 발생했습니다.'); + } finally { + setIsCancelling(false); + } + }; + + const formatDate = (dateString: string) => { + if (!dateString || dateString.length < 14) return dateString; + const year = dateString.substring(0, 4); + const month = dateString.substring(4, 6); + const day = dateString.substring(6, 8); + const hour = dateString.substring(8, 10); + const minute = dateString.substring(10, 12); + const second = dateString.substring(12, 14); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; + }; + + const getStatusBadgeVariant = (status: string) => { + switch (status) { + case '2': // 완결 + return 'default'; + case '1': // 진행중 + return 'secondary'; + case '3': // 반려 + return 'destructive'; + case '4': // 상신취소 + return 'outline'; + default: + return 'outline'; + } + }; + + const canCancelApproval = (status: string) => { + // 진행중(1), 보류(0) 상태에서만 취소 가능 + return ['0', '1'].includes(status); + }; + + const getCancelabilityMessage = (status: string) => { + if (canCancelApproval(status)) { + return '이 결재는 취소할 수 있습니다.'; + } + + switch (status) { + case '2': + return '완결된 결재는 취소할 수 없습니다.'; + case '3': + return '반려된 결재는 취소할 수 없습니다.'; + case '4': + return '이미 취소된 결재입니다.'; + case '5': + return '전결 처리된 결재는 취소할 수 없습니다.'; + case '6': + return '후완결된 결재는 취소할 수 없습니다.'; + default: + return '현재 상태에서는 취소할 수 없습니다.'; + } + }; + + return ( + <Card className="w-full max-w-4xl"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <XCircle className="w-5 h-5" /> + 결재 취소 + </CardTitle> + <CardDescription> + 결재 ID를 입력하여 상신을 취소합니다. {useFakeData && '(테스트 모드)'} + </CardDescription> + </CardHeader> + + <CardContent className="space-y-6"> + {/* 검색 영역 */} + <div className="flex items-center gap-3"> + <div className="flex-1"> + <Label htmlFor="apInfId">결재 ID</Label> + <Input + id="apInfId" + placeholder="결재 ID를 입력하세요" + value={apInfId} + onChange={(e) => setApInfId(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && fetchApprovalDetail()} + /> + </div> + <Button + onClick={fetchApprovalDetail} + disabled={isLoading} + > + {isLoading ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + 조회 중... + </> + ) : ( + '조회' + )} + </Button> + </div> + + {/* 취소 완료 메시지 */} + {cancelResult && ( + <div className="p-4 bg-green-50 border border-green-200 rounded-lg"> + <div className="flex items-center gap-2 text-green-700"> + <CheckCircle className="w-4 h-4" /> + <span className="font-medium">취소 완료</span> + </div> + <p className="text-sm text-green-600 mt-1"> + 결재 ID: {cancelResult.apInfId}가 성공적으로 취소되었습니다. + </p> + </div> + )} + + {/* 에러 메시지 */} + {error && ( + <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> + <div className="flex items-center gap-2 text-red-700"> + <AlertTriangle className="w-4 h-4" /> + <span className="font-medium">오류</span> + </div> + <p className="text-sm text-red-600 mt-1">{error}</p> + </div> + )} + + {/* 결재 정보 */} + {approvalDetail && ( + <div className="space-y-6"> + <div className="space-y-4"> + <h3 className="text-lg font-semibold">결재 정보</h3> + + <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg"> + <div> + <Label className="text-sm font-medium text-gray-600">결재 ID</Label> + <p className="text-sm font-mono mt-1">{approvalDetail.apInfId}</p> + </div> + <div> + <Label className="text-sm font-medium text-gray-600">제목</Label> + <p className="text-sm mt-1 font-medium">{approvalDetail.subject}</p> + </div> + <div> + <Label className="text-sm font-medium text-gray-600">상신일시</Label> + <p className="text-sm mt-1">{formatDate(approvalDetail.sbmDt)}</p> + </div> + <div> + <Label className="text-sm font-medium text-gray-600">현재 상태</Label> + <div className="mt-1"> + <Badge variant={getStatusBadgeVariant(approvalDetail.status)}> + {getStatusText(approvalDetail.status)} + </Badge> + </div> + </div> + </div> + </div> + + <Separator /> + + {/* 취소 가능 여부 */} + <div className="space-y-4"> + <h3 className="text-lg font-semibold">취소 가능 여부</h3> + + <div className={`p-4 rounded-lg border ${ + canCancelApproval(approvalDetail.status) + ? 'bg-blue-50 border-blue-200' + : 'bg-yellow-50 border-yellow-200' + }`}> + <div className="flex items-center gap-2 mb-2"> + {canCancelApproval(approvalDetail.status) ? ( + <CheckCircle className="w-4 h-4 text-blue-600" /> + ) : ( + <AlertTriangle className="w-4 h-4 text-yellow-600" /> + )} + <span className={`font-medium ${ + canCancelApproval(approvalDetail.status) + ? 'text-blue-700' + : 'text-yellow-700' + }`}> + {canCancelApproval(approvalDetail.status) ? '취소 가능' : '취소 불가'} + </span> + </div> + <p className={`text-sm ${ + canCancelApproval(approvalDetail.status) + ? 'text-blue-600' + : 'text-yellow-600' + }`}> + {getCancelabilityMessage(approvalDetail.status)} + </p> + </div> + </div> + + {/* 취소 버튼 */} + {canCancelApproval(approvalDetail.status) && ( + <> + <Separator /> + + <div className="flex justify-end"> + <AlertDialog> + <AlertDialogTrigger asChild> + <Button variant="destructive" disabled={isCancelling}> + {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> {approvalDetail.apInfId} + <br /> + <strong>제목:</strong> {approvalDetail.subject} + <br /> + <br /> + 이 작업은 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction + onClick={handleCancelApproval} + className="bg-red-600 hover:bg-red-700" + > + 결재 취소 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + </> + )} + </div> + )} + </CardContent> + </Card> + ); +}
\ No newline at end of file |
