From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendors/table/request-basicContract-dialog.tsx | 548 +++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 lib/vendors/table/request-basicContract-dialog.tsx (limited to 'lib/vendors/table/request-basicContract-dialog.tsx') diff --git a/lib/vendors/table/request-basicContract-dialog.tsx b/lib/vendors/table/request-basicContract-dialog.tsx new file mode 100644 index 00000000..8d05fbbe --- /dev/null +++ b/lib/vendors/table/request-basicContract-dialog.tsx @@ -0,0 +1,548 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Send, AlertCircle, Clock, RefreshCw } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Checkbox } from "@/components/ui/checkbox" +import { Badge } from "@/components/ui/badge" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { Vendor } from "@/db/schema/vendors" +import { useSession } from "next-auth/react" +import { getAllTemplates } from "@/lib/basic-contract/service" +import { useState, useEffect } from "react" +import { requestBasicContractInfo } from "@/lib/basic-contract/service" +import { checkContractRequestStatus } from "@/lib/basic-contract/service" +import { BasicContractTemplate } from "@/db/schema" + +interface RequestInfoDialogProps + extends React.ComponentPropsWithoutRef { + vendors: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +// 계약 요청 상태 인터페이스 +interface VendorTemplateStatus { + vendorId: number; + vendorName: string; + templateId: number; + templateName: string; + status: string; + createdAt: Date; + completedAt?: Date; // 계약 체결 날짜 추가 + isExpired: boolean; // 요청 만료 (30일) + isUpdated: boolean; // 템플릿 업데이트 여부 + isContractExpired: boolean; // 계약 유효기간 만료 여부 (1년) 추가 +} +export function RequestContractDialog({ + vendors, + showTrigger = true, + onSuccess, + ...props +}: RequestInfoDialogProps) { + const [isRequestPending, startRequestTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + const { data: session } = useSession() + const [templates, setTemplates] = useState([]) + const [selectedTemplateIds, setSelectedTemplateIds] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [statusLoading, setStatusLoading] = useState(false) + const [statusData, setStatusData] = useState([]) + const [forceResend, setForceResend] = useState>(new Set()) + + // 템플릿 및 상태 로드 + useEffect(() => { + loadTemplatesAndStatus(); + }, [vendors]); + + // 템플릿과 현재 요청 상태를 로드하는 함수 + const loadTemplatesAndStatus = async () => { + console.log("loadTemplatesAndStatus") + setIsLoading(true); + setStatusLoading(true); + + try { + // 1. 템플릿 로드 + const allTemplates = await getAllTemplates(); + const activeTemplates = allTemplates.filter(t => t.status === 'ACTIVE'); + setTemplates(activeTemplates); + + // 기본 템플릿 선택 설정 + const allActiveTemplateIds = activeTemplates.map(t => t.id); + setSelectedTemplateIds(allActiveTemplateIds); + + // 2. 현재 계약 요청 상태 확인 + if (vendors.length > 0 && allActiveTemplateIds.length > 0) { + const vendorIds = vendors.map(v => v.id); + const { data } = await checkContractRequestStatus(vendorIds, allActiveTemplateIds); + setStatusData(data || []); + } + } catch (error) { + console.error("데이터 로딩 오류:", error); + toast.error("템플릿 또는 상태 정보를 불러오는데 실패했습니다."); + } finally { + setIsLoading(false); + setStatusLoading(false); + } + }; + + // 체크박스 상태 변경 핸들러 + const handleTemplateToggle = (templateId: number, checked: boolean) => { + if (checked) { + setSelectedTemplateIds(prev => [...prev, templateId]); + } else { + setSelectedTemplateIds(prev => prev.filter(id => id !== templateId)); + } + }; + + // 강제 재전송 토글 + const toggleForceResend = (vendorId: number, templateId: number) => { + const key = `${vendorId}-${templateId}`; + const newForceResend = new Set(forceResend); + + if (newForceResend.has(key)) { + newForceResend.delete(key); + } else { + newForceResend.add(key); + } + + setForceResend(newForceResend); + }; + + const renderStatusBadge = (vendorId: number, templateId: number) => { + const status = statusData.find( + s => s.vendorId === vendorId && s.templateId === templateId + ); + + if (!status || status.status === "NONE") return null; + + // 상태에 따른 배지 스타일 설정 + let badgeVariant = "outline"; + let badgeLabel = ""; + let icon = null; + let tooltip = ""; + + switch (status.status) { + case "PENDING": + badgeVariant = "secondary"; + badgeLabel = "대기중"; + + if (status.isExpired) { + icon = ; + tooltip = "요청이 만료되었습니다. 재전송이 필요합니다."; + } else if (status.isUpdated) { + icon = ; + tooltip = "템플릿이 업데이트되었습니다. 재전송이 필요합니다."; + } else { + tooltip = "서명 요청이 진행 중입니다."; + } + break; + + case "COMPLETED": + // 계약 유효기간 만료 확인 + if (status.isContractExpired) { + badgeVariant = "warning"; // 경고 스타일 적용 + badgeLabel = "재계약 필요"; + icon = ; + tooltip = "계약 유효기간이 만료되었습니다. 재계약이 필요합니다."; + } else { + badgeVariant = "success"; + badgeLabel = "완료됨"; + tooltip = "이미 서명이 완료되었습니다."; + } + break; + + case "REJECTED": + badgeVariant = "destructive"; + badgeLabel = "거부됨"; + tooltip = "협력업체가 서명을 거부했습니다."; + break; + } + + return ( + + + + + {icon} + {badgeLabel} + + + +

{tooltip}

+ + {/* 재전송 조건에 계약 유효기간 만료 추가 */} + {(status.isExpired || status.isUpdated || status.status === "REJECTED" || + (status.status === "COMPLETED" && status.isContractExpired)) && ( +

+ +

+ )} +
+
+
+ ); + }; + + // 유효한 요청인지 확인 함수 개선 + const isValidRequest = (vendorId: number, templateId: number) => { + const status = statusData.find( + s => s.vendorId === vendorId && s.templateId === templateId + ); + + if (!status || status.status === "NONE") return true; + + // 만료되었거나 템플릿이 업데이트되었거나 거부된 경우 재전송 가능 + // 계약 유효기간 만료도 조건에 추가 + if (status.isExpired || + status.isUpdated || + status.status === "REJECTED" || + (status.status === "COMPLETED" && status.isContractExpired)) { + return forceResend.has(`${vendorId}-${templateId}`); + } + + // PENDING(비만료) 또는 COMPLETED(유효기간 내)는 재전송 불가 + return false; + }; + + + // 요청 발송 처리 + function onApprove() { + if (!session?.user?.id) { + toast.error("사용자 인증 정보를 찾을 수 없습니다.") + return + } + + if (selectedTemplateIds.length === 0) { + toast.error("최소 하나 이상의 계약서 템플릿을 선택해주세요.") + return + } + + // 모든 협력업체-템플릿 조합 생성 + const validRequests: { vendorId: number, templateId: number }[] = []; + const skippedRequests: { vendorId: number, templateId: number, reason: string }[] = []; + + vendors.forEach(vendor => { + selectedTemplateIds.forEach(templateId => { + if (isValidRequest(vendor.id, templateId)) { + validRequests.push({ + vendorId: vendor.id, + templateId + }); + } else { + // 유효하지 않은 요청은 건너뜀 + const status = statusData.find( + s => s.vendorId === vendor.id && s.templateId === templateId + ); + + let reason = "알 수 없음"; + if (status) { + if (status.status === "PENDING") reason = "이미 대기 중"; + if (status.status === "COMPLETED") reason = "이미 완료됨"; + } + + skippedRequests.push({ + vendorId: vendor.id, + templateId, + reason + }); + } + }); + }); + + if (validRequests.length === 0) { + toast.error("전송 가능한 요청이 없습니다. 재전송이 필요한 항목을 '재전송 하기' 버튼으로 활성화하세요."); + return; + } + + startRequestTransition(async () => { + // 유효한 요청만 처리 + const requests = validRequests.map(req => + requestBasicContractInfo({ + vendorIds: [req.vendorId], + requestedBy: Number(session.user.id), + templateId: req.templateId + }) + ); + + try { + const results = await Promise.all(requests); + + // 오류 확인 + const errors = results.filter(r => r.error); + if (errors.length > 0) { + toast.error(`${errors.length}개의 요청에서 오류가 발생했습니다.`); + return; + } + + // 상태 메시지 생성 + let successMessage = "기본계약서 서명 요청이 성공적으로 발송되었습니다."; + if (skippedRequests.length > 0) { + successMessage += ` (${skippedRequests.length}개 요청 건너뜀)`; + } + + props.onOpenChange?.(false); + toast.success(successMessage); + onSuccess?.(); + } catch (error) { + console.error("요청 처리 중 오류:", error); + toast.error("서명 요청 처리 중 오류가 발생했습니다."); + } + }); + } + + // 선택된 템플릿 수 표시 + const selectedCount = selectedTemplateIds.length; + const totalCount = templates.length; + + // UI 렌더링 + const renderTemplateList = () => ( +
+ {templates.map((template) => ( +
+
+
+ handleTemplateToggle(template.id, checked as boolean)} + /> + + + {/* 상태 배지를 템플릿 이름 옆에 나란히 배치 */} + {vendors.length === 1 && renderStatusBadge(vendors[0].id, template.id)} +
+ + + {vendors.length === 1 && (() => { + const status = statusData.find( + s => s.vendorId === vendors[0].id && s.templateId === template.id + ); + + // 계약 유효기간 만료 조건 추가 + if (status && (status.isExpired || + status.isUpdated || + status.status === "REJECTED" || + (status.status === "COMPLETED" && status.isContractExpired))) { + const key = `${vendors[0].id}-${template.id}`; + + // 계약 유효기간 만료인 경우 다른 텍스트 표시 + const buttonText = status.status === "COMPLETED" && status.isContractExpired + ? (forceResend.has(key) ? "재계약 취소" : "재계약하기") + : (forceResend.has(key) ? "재전송 취소" : "재전송하기"); + + return ( + + ); + } + return null; + })()} + +
+ + {/* 추가 정보 표시 (파일명 등) */} +
+ {template.fileName} +
+
+ ))} +
+ ); + + // 내용 영역 렌더링 + const renderContentArea = () => ( +
+
+
+

계약서 템플릿 선택

+ + {selectedCount}/{totalCount} 선택됨 + +
+ + {isLoading ? ( +
+ + 템플릿 로딩 중... +
+ ) : templates.length === 0 ? ( +
+ 활성 상태의 템플릿이 없습니다. 템플릿을 먼저 등록해주세요. +
+ ) : ( + // ScrollArea 대신 네이티브 스크롤 사용 +
+ {renderTemplateList()} +
+ )} +
+ + {statusLoading && ( +
+ + 계약 상태 확인 중... +
+ )} + + {/* 선택된 템플릿 정보 (ScrollArea 대신 네이티브 스크롤 사용) */} + {selectedTemplateIds.length > 0 && ( +
+

선택된 템플릿 정보

+
+
+ {selectedTemplateIds.map(id => { + const template = templates.find(t => t.id === id); + if (!template) return null; + + return ( +
+

이름: {template.templateName}

+

파일: {template.fileName}

+
+ ); + })} +
+
+
+ )} + +
+ 요청시 협력업체에게 이메일이 발송되며, 협력업체는 별도 페이지에서 기본계약서와 기타 관련 서류들에 대해서 서명을 하게 됩니다. +
+
+ ); + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 협력업체 기본계약서 요청 확인 + + {vendors.length} + {vendors.length === 1 ? "개의 협력업체" : "개의 협력업체들"}에게 기본계약서 서명을 요청하시겠습니까? + + + +
+ {renderContentArea()} +
+ + + + + + + +
+
+ ); + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 협력업체 기본계약서 요청 확인 + + {vendors.length} + {vendors.length === 1 ? "개의 협력업체" : "개의 협력업체들"}에게 기본계약서 서명을 요청하시겠습니까? + + + +
+ {renderContentArea()} +
+ + + + + + + +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3