From cf8dac0c6490469dab88a560004b0c07dbd48612 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 18 Sep 2025 00:23:40 +0000 Subject: (대표님) rfq, 계약, 서명 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/itb/table/view-purchase-request-sheet.tsx | 809 ++++++++++++++++++++++++++ 1 file changed, 809 insertions(+) create mode 100644 lib/itb/table/view-purchase-request-sheet.tsx (limited to 'lib/itb/table/view-purchase-request-sheet.tsx') diff --git a/lib/itb/table/view-purchase-request-sheet.tsx b/lib/itb/table/view-purchase-request-sheet.tsx new file mode 100644 index 00000000..c4ff9416 --- /dev/null +++ b/lib/itb/table/view-purchase-request-sheet.tsx @@ -0,0 +1,809 @@ +// components/purchase-requests/view-purchase-request-sheet.tsx +"use client"; + +import * as React from "react"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetFooter, +} from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableFooter, +} from "@/components/ui/table"; +import { Separator } from "@/components/ui/separator"; +import { + FileList, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list"; +import { + FileText, + Package, + Edit, + Download, + Calendar, + User, + Building, + MapPin, + Hash, + DollarSign, + Clock, + CheckCircle, + XCircle, + AlertCircle, + Layers, + Tag, + Paperclip, + FileIcon, + Eye +} from "lucide-react"; +import { format } from "date-fns"; +import { ko } from "date-fns/locale"; +import type { PurchaseRequestView } from "@/db/schema"; +import { useRouter } from "next/navigation"; +import { getPurchaseRequestAttachments } from "../service"; +import { downloadFile, quickPreview, formatFileSize, getFileInfo } from "@/lib/file-download"; + +interface ViewPurchaseRequestSheetProps { + request: PurchaseRequestView; + open: boolean; + onOpenChange: (open: boolean) => void; + onEditClick?: () => void; +} + +const statusConfig = { + "작성중": { + variant: "secondary" as const, + color: "text-gray-500", + icon: Edit, + bgColor: "bg-gray-100" + }, + "요청완료": { + variant: "default" as const, + color: "text-blue-500", + icon: CheckCircle, + bgColor: "bg-blue-50" + }, + "검토중": { + variant: "warning" as const, + color: "text-yellow-500", + icon: Clock, + bgColor: "bg-yellow-50" + }, + "승인": { + variant: "success" as const, + color: "text-green-500", + icon: CheckCircle, + bgColor: "bg-green-50" + }, + "반려": { + variant: "destructive" as const, + color: "text-red-500", + icon: XCircle, + bgColor: "bg-red-50" + }, + "RFQ생성완료": { + variant: "outline" as const, + color: "text-purple-500", + icon: Package, + bgColor: "bg-purple-50" + }, +}; + +export function ViewPurchaseRequestSheet({ + request, + open, + onOpenChange, + onEditClick, +}: ViewPurchaseRequestSheetProps) { + const router = useRouter(); + const [activeTab, setActiveTab] = React.useState("overview"); + const [attachments, setAttachments] = React.useState([]); + const [isLoadingFiles, setIsLoadingFiles] = React.useState(false); + + // 첨부파일 로드 + React.useEffect(() => { + async function loadAttachments() { + if (open && request.id) { + setIsLoadingFiles(true); + try { + const result = await getPurchaseRequestAttachments(request.id); + if (result.success && result.data) { + setAttachments(result.data); + } else { + console.error("Failed to load attachments:", result.error); + setAttachments([]); + } + } catch (error) { + console.error("Error loading attachments:", error); + setAttachments([]); + } finally { + setIsLoadingFiles(false); + } + } + } + + loadAttachments(); + }, [open, request.id]); + + // 파일 다운로드 핸들러 + const handleFileDownload = async (file: any) => { + const result = await downloadFile(file.filePath, file.originalFileName, { + action: 'download', + showToast: true, + onError: (error) => { + console.error("Download failed:", error); + }, + onSuccess: (fileName, fileSize) => { + console.log(`Successfully downloaded: ${fileName} (${fileSize} bytes)`); + } + }); + + return result; + }; + + // 파일 미리보기 핸들러 + const handleFilePreview = async (file: any) => { + const fileInfo = getFileInfo(file.originalFileName); + + if (!fileInfo.canPreview) { + // 미리보기가 지원되지 않는 파일은 다운로드 + return handleFileDownload(file); + } + + const result = await quickPreview(file.filePath, file.originalFileName); + return result; + }; + + // 전체 다운로드 핸들러 + const handleDownloadAll = async () => { + for (const file of attachments) { + await handleFileDownload(file); + // 여러 파일 다운로드 시 간격 두기 + await new Promise(resolve => setTimeout(resolve, 500)); + } + }; + + // 아이템 총액 계산 + const totalAmount = React.useMemo(() => { + if (!request.items || !Array.isArray(request.items)) return 0; + return request.items.reduce((sum, item) => { + const subtotal = (item.quantity || 0) * (item.estimatedUnitPrice || 0); + return sum + subtotal; + }, 0); + }, [request.items]); + + const handleEdit = () => { + if (onEditClick) { + onEditClick(); + } else { + onOpenChange(false); + } + }; + + const handleExport = () => { + // Export to Excel 기능 + console.log("Export to Excel"); + }; + + const StatusIcon = statusConfig[request.status]?.icon || AlertCircle; + const statusBgColor = statusConfig[request.status]?.bgColor || "bg-gray-50"; + + return ( + + + +
+ 구매요청 상세 +
+ + + {request.status} + +
+
+ + 요청번호: {request.requestCode} | + 작성일: {request.createdAt && format(new Date(request.createdAt), "yyyy-MM-dd")} + +
+ + + + + + 개요 + + + + 품목 정보 + {request.itemCount > 0 && ( + + {request.itemCount} + + )} + + + + 첨부파일 + {attachments.length > 0 && ( + + {attachments.length} + + )} + + + + 처리 이력 + + + +
+ + {/* 기본 정보 */} + + + 기본 정보 + + +
+

요청 제목

+

{request.requestTitle}

+
+ + {request.requestDescription && ( +
+

요청 설명

+

{request.requestDescription}

+
+ )} + + + +
+
+ +
+

희망 납기일

+

+ {request.requestedDeliveryDate + ? format(new Date(request.requestedDeliveryDate), "yyyy-MM-dd") + : "-"} +

+
+
+ +
+ +
+

예상 예산

+

{request.estimatedBudget || "-"}

+
+
+
+
+
+ + {/* 프로젝트 & 패키지 정보 */} + + + 프로젝트 정보 + + +
+
+ +
+

프로젝트 코드

+

{request.projectCode || "-"}

+
+
+ +
+ +
+

프로젝트명

+

{request.projectName || "-"}

+
+
+ +
+ +
+

발주처

+

{request.projectCompany || "-"}

+
+
+ +
+ +
+

현장

+

{request.projectSite || "-"}

+
+
+
+ + + +
+
+ +
+

패키지

+

{request.packageNo} - {request.packageName || "-"}

+
+
+ +
+ +
+

SM 코드

+

{request.smCode || "-"}

+
+
+
+
+
+ + {/* 자재 정보 */} + {(request.majorItemMaterialCategory || request.majorItemMaterialDescription) && ( + + + 자재 정보 + + +
+
+ +
+

자재 그룹

+

{request.majorItemMaterialCategory || "-"}

+
+
+ +
+ +
+

자재 설명

+

{request.majorItemMaterialDescription || "-"}

+
+
+
+
+
+ )} + + {/* 담당자 정보 */} + + + 담당자 정보 + + +
+
+ +
+

설계 담당자

+

{request.engPicName || "-"}

+ {request.engPicEmail && ( +

{request.engPicEmail}

+ )} +
+
+ +
+ +
+

구매 담당자

+

{request.purchasePicName || "미배정"}

+ {request.purchasePicEmail && ( +

{request.purchasePicEmail}

+ )} +
+
+
+
+
+ + {/* 반려 사유 */} + {request.status === "반려" && request.rejectReason && ( + + + + + 반려 사유 + + + +

{request.rejectReason}

+
+
+ )} +
+ + + + +
+
+ 품목 목록 + + 총 {request.itemCount || 0}개 품목 | 총 수량 {request.totalQuantity || 0}개 + +
+ +
+
+ + {(!request.items || request.items.length === 0) ? ( +
+ +

등록된 품목이 없습니다

+
+ ) : ( +
+ + + + 번호 + 아이템 코드 + 아이템명 + 사양 + 수량 + 단위 + 예상 단가 + 예상 금액 + {request.items[0]?.remarks && 비고} + + + + {request.items.map((item: any, index: number) => { + const subtotal = (item.quantity || 0) * (item.estimatedUnitPrice || 0); + return ( + + {index + 1} + {item.itemCode || "-"} + {item.itemName} + {item.specification || "-"} + + {item.quantity?.toLocaleString('ko-KR')} + + {item.unit} + + {item.estimatedUnitPrice + ? item.estimatedUnitPrice.toLocaleString('ko-KR') + "원" + : "-"} + + + {subtotal > 0 + ? subtotal.toLocaleString('ko-KR') + "원" + : "-"} + + {request.items[0]?.remarks && ( + {item.remarks || "-"} + )} + + ); + })} + + + + + 총 합계 + + +
+ + {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(totalAmount)} + +
+
+ {request.items[0]?.remarks && } +
+
+
+
+ )} +
+
+
+ + + + +
+
+ 첨부파일 + + 구매 요청 관련 문서 및 파일 + +
+
+
+ + {isLoadingFiles ? ( +
+

파일 목록을 불러오는 중...

+
+ ) : attachments.length === 0 ? ( +
+ +

첨부된 파일이 없습니다

+
+ ) : ( +
+
+

+ 총 {attachments.length}개의 파일이 첨부되어 있습니다 +

+ +
+ + + {attachments.map((file, index) => { + const fileInfo = getFileInfo(file.originalFileName || file.fileName); + return ( + + + + + + + + {file.originalFileName || file.fileName} + + + {file.fileSize} + + + + {file.createdAt && ( + + {format(new Date(file.createdAt), "yyyy-MM-dd HH:mm")} + + )} + {file.category && ( + + {file.category} + + )} + + +
+ {fileInfo.canPreview && ( + + )} + +
+
+ ); + })} +
+ + {/* 파일 종류별 요약 */} + {attachments.length > 0 && ( +
+

파일 요약

+
+
+

총 파일 수

+

{attachments.length}개

+
+
+

총 용량

+

+ {formatFileSize( + attachments.reduce((sum, file) => sum + (file.fileSize || 0), 0) + )} +

+
+
+

최근 업로드

+

+ {attachments[0]?.createdAt + ? format(new Date(attachments[0].createdAt), "yyyy-MM-dd") + : "-"} +

+
+
+
+ )} +
+ )} +
+
+
+ + + + + 처리 이력 + 요청서의 처리 현황을 시간순으로 확인할 수 있습니다 + + +
+
+ +
+ {/* 생성 */} +
+
+
+
+
+
+
+

요청서 작성

+

+ {request.createdByName} ({request.createdByEmail}) +

+

+ {request.createdAt && format(new Date(request.createdAt), "yyyy-MM-dd HH:mm:ss", { locale: ko })} +

+
+
+ + {/* 확정 */} + {request.confirmedAt && ( +
+
+
+ +
+
+
+

요청 확정

+

+ {request.confirmedByName} +

+

+ {format(new Date(request.confirmedAt), "yyyy-MM-dd HH:mm:ss", { locale: ko })} +

+
+
+ )} + + {/* 반려 */} + {request.status === "반려" && request.rejectReason && ( +
+
+
+ +
+
+
+

반려됨

+

+ {request.rejectReason} +

+

+ {request.updatedAt && format(new Date(request.updatedAt), "yyyy-MM-dd HH:mm:ss", { locale: ko })} +

+
+
+ )} + + {/* RFQ 생성 */} + {request.rfqCreatedAt && ( +
+
+
+ +
+
+
+

RFQ 생성 완료

+

+ RFQ 번호: {request.rfqCode} +

+

+ {format(new Date(request.rfqCreatedAt), "yyyy-MM-dd HH:mm:ss", { locale: ko })} +

+
+
+ )} + + {/* 최종 수정 */} + {request.updatedAt && request.updatedAt !== request.createdAt && !request.rfqCreatedAt && request.status !== "반려" && ( +
+
+
+ +
+
+
+

최종 수정

+

+ {request.updatedByName} ({request.updatedByEmail}) +

+

+ {format(new Date(request.updatedAt), "yyyy-MM-dd HH:mm:ss", { locale: ko })} +

+
+
+ )} +
+
+ + + +
+ + + +
+ + {request.status === "작성중" && ( + + )} +
+
+ + + ); +} \ No newline at end of file -- cgit v1.2.3