// lib/tbe-last/table/dialogs/documents-sheet.tsx "use client" import * as React from "react" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ScrollArea } from "@/components/ui/scroll-area" import { formatDate } from "@/lib/utils" import { downloadFile, getFileInfo } from "@/lib/file-download" import { FileText, Eye, Download, MoreHorizontal, Filter, MessageSquare, CheckCircle, XCircle, Clock, AlertCircle, Save, } from "lucide-react" import { toast } from "sonner" import { useRouter } from "next/navigation" interface DocumentsSheetProps { open: boolean onOpenChange: (open: boolean) => void sessionDetail: any isLoading: boolean } type CommentCount = { totalCount: number; openCount: number } type CountMap = Record export function DocumentsSheet({ open, onOpenChange, sessionDetail, isLoading }: DocumentsSheetProps) { console.log(sessionDetail, "sessionDetail") const [sourceFilter, setSourceFilter] = React.useState<"all" | "buyer" | "vendor">("all") const [searchTerm, setSearchTerm] = React.useState("") const [editingReviewId, setEditingReviewId] = React.useState(null) const [reviewData, setReviewData] = React.useState>({}) const [isSaving, setIsSaving] = React.useState>({}) const [commentCounts, setCommentCounts] = React.useState({}) // <-- 추가 const [countLoading, setCountLoading] = React.useState(false) const router = useRouter() const allReviewIds = React.useMemo(() => { const docs = sessionDetail?.documents ?? [] const ids = new Set() for (const d of docs) { const id = Number(d?.documentReviewId) if (Number.isFinite(id)) ids.add(id) } return Array.from(ids) }, [sessionDetail?.documents]) React.useEffect(() => { let aborted = false ; (async () => { if (allReviewIds.length === 0) { setCommentCounts({}) return } setCountLoading(true) try { // 너무 길어질 수 있으니 적당히 나눠서 호출(옵션) const chunkSize = 100 const chunks: number[][] = [] for (let i = 0; i < allReviewIds.length; i += chunkSize) { chunks.push(allReviewIds.slice(i, i + chunkSize)) } const merged: CountMap = {} for (const c of chunks) { const qs = encodeURIComponent(c.join(",")) const res = await fetch(`/api/pdftron-comments/xfdf/count?ids=${qs}`, { credentials: "include", cache: "no-store", }) if (!res.ok) throw new Error(`count api ${res.status}`) const json = await res.json() if (aborted) return const data = (json?.data ?? {}) as Record for (const [k, v] of Object.entries(data)) { const idNum = Number(k) if (Number.isFinite(idNum)) { merged[idNum] = { totalCount: v.totalCount ?? 0, openCount: v.openCount ?? 0 } } } } if (!aborted) setCommentCounts(merged) } catch (e) { console.error("Failed to load comment counts", e) } finally { if (!aborted) setCountLoading(false) } })() return () => { aborted = true } }, [allReviewIds.join(",")]) // 의존성: id 목록이 바뀔 때만 // 문서 초기 데이터 설정 React.useEffect(() => { if (sessionDetail?.documents) { const initialData: Record = {} sessionDetail.documents.forEach((doc: any) => { initialData[doc.documentReviewId] = { reviewStatus: doc.reviewStatus || "미검토", reviewComments: doc.reviewComments || "" } }) setReviewData(initialData) } }, [sessionDetail]) // PDFtron 뷰어 열기 const handleOpenPDFTron = (doc: any) => { if (!doc.filePath) { toast.error("파일 경로를 찾을 수 없습니다") return } const params = new URLSearchParams({ filePath: doc.filePath, documentId: doc.documentId.toString(), documentReviewId: doc.documentReviewId?.toString() || '', sessionId: sessionDetail?.session?.tbeSessionId?.toString() || '', documentName: doc.documentName || '', mode: 'review' }) window.open(`/pdftron-viewer?${params.toString()}`, '_blank') } // 파일이 PDFtron에서 열 수 있는지 확인 const canOpenInPDFTron = (filePath: string) => { if (!filePath) return false const ext = filePath.split('.').pop()?.toLowerCase() const supportedFormats = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'tiff', 'bmp'] return supportedFormats.includes(ext || '') } // 파일 다운로드 const handleDownload = async (doc: any) => { if (!doc.filePath) { toast.error("파일 경로를 찾을 수 없습니다") return } await downloadFile(doc.filePath, doc.originalFileName || doc.documentName, { action: 'download', showToast: true, onError: (error) => { console.error('Download error:', error) } }) } // 리뷰 상태 저장 const handleSaveReview = async (doc: any) => { const reviewId = doc.documentReviewId setIsSaving({ ...isSaving, [reviewId]: true }) try { // API 호출하여 리뷰 상태 저장 const response = await fetch(`/api/document-reviews/${reviewId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reviewStatus: reviewData[reviewId]?.reviewStatus, reviewComments: reviewData[reviewId]?.reviewComments }) }) if (!response.ok) throw new Error('Failed to save review') toast.success("리뷰 저장 완료") router.refresh() setEditingReviewId(null) } catch (error) { console.error('Save review error:', error) toast.error("리뷰 저장 실패") } finally { setIsSaving({ ...isSaving, [reviewId]: false }) } } // 리뷰 상태 아이콘 const getReviewStatusIcon = (status: string) => { switch (status) { case "승인": return case "반려": return case "보류": return default: return } } // 필터링된 문서 목록 const filteredDocuments = React.useMemo(() => { if (!sessionDetail?.documents) return [] return sessionDetail.documents.filter((doc: any) => { // Source 필터 if (sourceFilter !== "all" && doc.documentSource !== sourceFilter) { return false } // 검색어 필터 if (searchTerm) { const searchLower = searchTerm.toLowerCase() return ( doc.documentName?.toLowerCase().includes(searchLower) || doc.documentType?.toLowerCase().includes(searchLower) || doc.reviewComments?.toLowerCase().includes(searchLower) ) } return true }) }, [sessionDetail?.documents, sourceFilter, searchTerm]) return ( Documents & Review Management 문서 검토 및 코멘트 관리 {/* 필터 및 검색 */}
setSearchTerm(e.target.value)} className="max-w-sm" />
Total: {filteredDocuments.length} {sessionDetail?.documents && ( <> Buyer: {sessionDetail.documents.filter((d: any) => d.documentSource === "buyer").length} Vendor: {sessionDetail.documents.filter((d: any) => d.documentSource === "vendor").length} )}
{/* 문서 테이블 */} {isLoading ? (
Loading...
) : ( Source Document Name Type Review Status Comments Review Notes Uploaded Actions {filteredDocuments.length === 0 ? ( No documents found ) : ( filteredDocuments.map((doc: any) => ( {doc.documentSource}
{doc.documentName}
{doc.documentType} {editingReviewId === doc.documentReviewId ? ( ) : (
{getReviewStatusIcon(reviewData[doc.documentReviewId]?.reviewStatus || doc.reviewStatus)} {reviewData[doc.documentReviewId]?.reviewStatus || doc.reviewStatus || "미검토"}
)}
{(() => { const id = Number(doc.documentReviewId) const counts = Number.isFinite(id) ? commentCounts[id] : undefined if (countLoading && !counts) { return Loading… } if (!counts || counts.totalCount === 0) { return - } return (
{counts.totalCount} {counts.openCount > 0 && ( ({counts.openCount} open) )}
) })()}
{editingReviewId === doc.documentReviewId ? (