summaryrefslogtreecommitdiff
path: root/components/knox/approval/ApprovalCancel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/knox/approval/ApprovalCancel.tsx')
-rw-r--r--components/knox/approval/ApprovalCancel.tsx341
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