summaryrefslogtreecommitdiff
path: root/components/knox/approval/ApprovalDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/knox/approval/ApprovalDetail.tsx')
-rw-r--r--components/knox/approval/ApprovalDetail.tsx156
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 />
{/* 결재 내용 */}