diff options
Diffstat (limited to 'lib/b-rfq/attachment/vendor-responses-panel.tsx')
| -rw-r--r-- | lib/b-rfq/attachment/vendor-responses-panel.tsx | 229 |
1 files changed, 205 insertions, 24 deletions
diff --git a/lib/b-rfq/attachment/vendor-responses-panel.tsx b/lib/b-rfq/attachment/vendor-responses-panel.tsx index 901af3bf..0cbe2a08 100644 --- a/lib/b-rfq/attachment/vendor-responses-panel.tsx +++ b/lib/b-rfq/attachment/vendor-responses-panel.tsx @@ -2,8 +2,25 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { RefreshCw, Download, MessageSquare, Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react" -import { formatDate } from "@/lib/utils" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + RefreshCw, + Download, + MessageSquare, + Clock, + CheckCircle2, + XCircle, + AlertCircle, + FileText, + Files, + AlertTriangle +} from "lucide-react" +import { formatDate, formatFileSize } from "@/lib/utils" +import { RequestRevisionDialog } from "./request-revision-dialog" interface VendorResponsesPanelProps { attachment: any @@ -12,12 +29,93 @@ interface VendorResponsesPanelProps { onRefresh: () => void } +// 파일 다운로드 핸들러 +async function handleFileDownload(filePath: string, fileName: string, fileId: number) { + try { + const params = new URLSearchParams({ + path: filePath, + type: "vendor", + responseFileId: fileId.toString(), + }); + + const response = await fetch(`/api/rfq-attachments/download?${params.toString()}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `Download failed: ${response.status}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + console.log("✅ 파일 다운로드 성공:", fileName); + } catch (error) { + console.error("❌ 파일 다운로드 실패:", error); + alert(`파일 다운로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } +} + +// 파일 목록 컴포넌트 +function FilesList({ files }: { files: any[] }) { + if (files.length === 0) { + return ( + <div className="text-center py-4 text-muted-foreground text-sm"> + 업로드된 파일이 없습니다. + </div> + ); + } + + return ( + <div className="space-y-2 max-h-64 overflow-y-auto"> + {files.map((file, index) => ( + <div key={file.id} className="flex items-center justify-between p-3 border rounded-lg bg-green-50 border-green-200"> + <div className="flex items-center gap-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-green-600 flex-shrink-0" /> + <div className="min-w-0 flex-1"> + <div className="font-medium text-sm truncate" title={file.originalFileName}> + {file.originalFileName} + </div> + <div className="text-xs text-muted-foreground"> + {formatFileSize(file.fileSize)} • {formatDate(file.uploadedAt)} + </div> + {file.description && ( + <div className="text-xs text-muted-foreground italic mt-1" title={file.description}> + {file.description} + </div> + )} + </div> + </div> + <Button + size="sm" + variant="ghost" + onClick={() => handleFileDownload(file.filePath, file.originalFileName, file.id)} + className="flex-shrink-0 ml-2" + title="파일 다운로드" + > + <Download className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + ); +} + export function VendorResponsesPanel({ attachment, responses, isLoading, onRefresh }: VendorResponsesPanelProps) { + + console.log(responses) const getStatusIcon = (status: string) => { switch (status) { @@ -114,7 +212,8 @@ export function VendorResponsesPanel({ <TableHead>리비전</TableHead> <TableHead>요청일</TableHead> <TableHead>응답일</TableHead> - <TableHead>벤더 코멘트</TableHead> + <TableHead>응답 파일</TableHead> + <TableHead>코멘트</TableHead> <TableHead className="w-[100px]">액션</TableHead> </TableRow> </TableHeader> @@ -161,37 +260,119 @@ export function VendorResponsesPanel({ <TableCell> {response.respondedAt ? formatDate(response.respondedAt) : '-'} </TableCell> - + + {/* 응답 파일 컬럼 */} <TableCell> - {response.vendorComment ? ( - <div className="max-w-[200px] truncate" title={response.vendorComment}> - {response.vendorComment} + {response.totalFiles > 0 ? ( + <div className="flex items-center gap-2"> + <Badge variant="secondary" className="text-xs"> + {response.totalFiles}개 + </Badge> + {response.totalFiles === 1 ? ( + // 파일이 1개면 바로 다운로드 + <Button + variant="ghost" + size="sm" + className="h-8 w-8 p-0" + onClick={() => { + const file = response.files[0]; + handleFileDownload(file.filePath, file.originalFileName, file.id); + }} + title={response.latestFile?.originalFileName} + > + <Download className="h-4 w-4" /> + </Button> + ) : ( + // 파일이 여러 개면 Popover로 목록 표시 + <Popover> + <PopoverTrigger asChild> + <Button + variant="ghost" + size="sm" + className="h-8 w-8 p-0" + title="파일 목록 보기" + > + <Files className="h-4 w-4" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-96" align="start"> + <div className="space-y-2"> + <div className="font-medium text-sm"> + 응답 파일 목록 ({response.totalFiles}개) + </div> + <FilesList files={response.files} /> + </div> + </PopoverContent> + </Popover> + )} </div> ) : ( - '-' + <span className="text-muted-foreground text-sm">-</span> )} </TableCell> <TableCell> + <div className="space-y-1 max-w-[200px]"> + {/* 벤더 응답 코멘트 */} + {response.responseComment && ( + <div className="flex items-center gap-1"> + <div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" title="벤더 응답 코멘트"></div> + <div className="text-xs text-blue-600 truncate" title={response.responseComment}> + {response.responseComment} + </div> + </div> + )} + + {/* 수정 요청 사유 */} + {response.revisionRequestComment && ( + <div className="flex items-center gap-1"> + <div className="w-2 h-2 rounded-full bg-red-500 flex-shrink-0" title="수정 요청 사유"></div> + <div className="text-xs text-red-600 truncate" title={response.revisionRequestComment}> + {response.revisionRequestComment} + </div> + </div> + )} + + {!response.responseComment && !response.revisionRequestComment && ( + <span className="text-muted-foreground text-sm">-</span> + )} + </div> + </TableCell> + + {/* 액션 컬럼 - 수정 요청 기능으로 변경 */} + <TableCell> <div className="flex items-center gap-1"> {response.responseStatus === 'RESPONDED' && ( - <Button - variant="ghost" - size="sm" - className="h-8 w-8 p-0" - title="첨부파일 다운로드" - > - <Download className="h-4 w-4" /> - </Button> + <RequestRevisionDialog + responseId={response.id} + attachmentType={attachment.attachmentType} + serialNo={attachment.serialNo} + vendorName={response.vendorName} + currentRevision={response.currentRevision} + onSuccess={onRefresh} + trigger={ + <Button + variant="outline" + size="sm" + className="h-8 px-2" + title="수정 요청" + > + <AlertTriangle className="h-3 w-3 mr-1" /> + 수정요청 + </Button> + } + /> + )} + + {response.responseStatus === 'REVISION_REQUESTED' && ( + <Badge variant="secondary" className="text-xs"> + 수정 요청됨 + </Badge> + )} + + {(response.responseStatus === 'NOT_RESPONDED' || response.responseStatus === 'WAIVED') && ( + <span className="text-muted-foreground text-xs">-</span> )} - <Button - variant="ghost" - size="sm" - className="h-8 w-8 p-0" - title="상세 보기" - > - <MessageSquare className="h-4 w-4" /> - </Button> </div> </TableCell> </TableRow> |
