"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()}
); }