From 7bf90e71ee98abe2d65e18eaf20a449cd1bd097c Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 4 Nov 2025 16:28:17 +0900 Subject: (김준회) 파트너 RFQ 응답: RFQ PR 아이템 다이얼로그와 유사하게 변경, 기존 액션들은 유지 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/quotation-items-table.tsx | 515 +++++++++++++++------ 1 file changed, 363 insertions(+), 152 deletions(-) (limited to 'lib/rfq-last') diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index 54866822..23ddc924 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -11,10 +11,15 @@ import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Badge } from "@/components/ui/badge" import { Label } from "@/components/ui/label" -import { CalendarIcon, Eye, Calculator, AlertCircle } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { ScrollArea } from "@/components/ui/scroll-area" +import { CalendarIcon, Eye, FileText, Download, ExternalLink } from "lucide-react" import { format } from "date-fns" import { cn, formatCurrency } from "@/lib/utils" import { useState, useEffect } from "react" +import { toast } from "sonner" +import { checkPosFileExists, getDownloadUrlByMaterialCode } from "@/lib/pos" +import { PosFileSelectionDialog } from "@/lib/pos/components/pos-file-selection-dialog" import { Dialog, DialogContent, @@ -23,17 +28,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog" interface QuotationItemsTableProps { prItems: any[] @@ -50,12 +44,35 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp const [showDetail, setShowDetail] = useState(false) const [showBulkDateDialog, setShowBulkDateDialog] = useState(false) const [bulkDeliveryDate, setBulkDeliveryDate] = useState(undefined) + + // POS 파일 관련 상태 + const [posDialogOpen, setPosDialogOpen] = useState(false) + const [selectedMaterialCode, setSelectedMaterialCode] = useState("") + const [posFiles, setPosFiles] = useState>([]) + const [loadingPosFiles, setLoadingPosFiles] = useState(false) + const [downloadingFileIndex, setDownloadingFileIndex] = useState(null) const currency = watch("vendorCurrency") || "USD" const quotationItems = watch("quotationItems") console.log(prItems,"prItems") + // 통계 정보 계산 + const statistics = { + total: prItems.length, + regular: prItems.filter(item => !item.majorYn).length, + major: prItems.filter(item => item.majorYn).length, + totalQuantity: prItems.reduce((sum, item) => sum + (item.quantity || 0), 0), + totalWeight: prItems.reduce((sum, item) => sum + (item.grossWeight || 0), 0), + } + // PR 아이템 정보를 quotationItems에 초기화 useEffect(() => { if (prItems && prItems.length > 0) { @@ -90,19 +107,6 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp setValue(`quotationItems.${index}.uom`, prItem.uom) } } - - // 할인 적용 - const applyDiscount = (index: number) => { - const item = quotationItems[index] - const prItem = prItems[index] - if (item && prItem && item.discountRate) { - const originalTotal = (item.unitPrice || 0) * (prItem.quantity || 0) - const discountAmount = originalTotal * (item.discountRate / 100) - const finalTotal = originalTotal - discountAmount - setValue(`quotationItems.${index}.totalPrice`, finalTotal) - setValue(`quotationItems.${index}.discountAmount`, discountAmount) - } - } // 일괄 납기일 적용 const applyBulkDeliveryDate = () => { @@ -122,6 +126,79 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp }) } + // 사양서 링크 열기 + const handleOpenSpec = (specUrl: string) => { + window.open(specUrl, '_blank', 'noopener,noreferrer') + } + + // POS 파일 목록 조회 및 다이얼로그 열기 + const handleOpenPosDialog = async (materialCode: string) => { + if (!materialCode) { + toast.error("자재코드가 없습니다") + return + } + + setLoadingPosFiles(true) + setSelectedMaterialCode(materialCode) + + try { + toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` }) + + const result = await checkPosFileExists(materialCode) + + if (result.exists && result.files && result.files.length > 0) { + const detailResult = await getDownloadUrlByMaterialCode(materialCode) + + if (detailResult.success && detailResult.availableFiles) { + setPosFiles(detailResult.availableFiles) + setPosDialogOpen(true) + toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` }) + } else { + toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } else { + toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` }) + } + } catch (error) { + console.error("POS 파일 조회 오류:", error) + toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` }) + } finally { + setLoadingPosFiles(false) + } + } + + // POS 파일 다운로드 실행 + const handleDownloadPosFile = async (fileIndex: number, fileName: string) => { + if (!selectedMaterialCode) return + + setDownloadingFileIndex(fileIndex) + + try { + toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` }) + + const downloadUrl = `/api/pos/download-on-demand?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}` + + toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` }) + window.open(downloadUrl, '_blank', 'noopener,noreferrer') + + setTimeout(() => { + setDownloadingFileIndex(null) + }, 1000) + } catch (error) { + console.error("POS 파일 다운로드 오류:", error) + toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` }) + setDownloadingFileIndex(null) + } + } + + // POS 다이얼로그 닫기 + const handleClosePosDialog = () => { + setPosDialogOpen(false) + setSelectedMaterialCode("") + setPosFiles([]) + setDownloadingFileIndex(null) + } + const totalAmount = quotationItems?.reduce( (sum: number, item: any) => sum + (item.totalPrice || 0), 0 ) || 0 @@ -397,139 +474,263 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp + + {/* 통계 정보 */} +
+
+
+
{statistics.total}
+
전체 품목
+
+
+
{statistics.regular}
+
일반 품목
+
+
+
{statistics.totalQuantity.toLocaleString()}
+
총 수량
+
+
+
{statistics.totalWeight.toLocaleString()}
+
총 중량 (KG)
+
+
+ +
-
- - - - No - PR No - 자재코드 - 자재명 - 수량 - 단가 - 총액 - 납기일 - 작업 - - - - {fields.map((field, index) => { - const prItem = prItems[index] - const quotationItem = quotationItems[index] - const isMajor = prItem?.majorYn - - return ( - - -
- {prItem?.rfqItem || index + 1} - {isMajor && ( - - 주요 - - )} -
-
- - {prItem?.prNo} - - - {prItem?.materialCode} - - -
-

- {prItem?.materialDescription} -

- {prItem?.size && ( -

- 사이즈: {prItem.size} + +

+
+ + + No + PR No + PR 아이템 + 자재코드 + 자재명 + 수량 + 단위 + 중량 + 중량단위 + 단가 + 총액 + 납기일 + 사양/POS + 프로젝트 + 상세 + + + + {fields.map((field, index) => { + const prItem = prItems[index] + const quotationItem = quotationItems[index] + const isMajor = prItem?.majorYn + + return ( + + +
+ #{index + 1} + {isMajor && ( + + 주요 + + )} +
+
+ + {prItem?.prNo || "-"} + + + {prItem?.prItem || prItem?.rfqItem || "-"} + + +
+ {prItem?.materialCode || "-"} + {prItem?.acc && ( + + ACC: {prItem.acc} + + )} +
+
+ +
+

+ {prItem?.materialDescription || "-"}

- )} -
-
- - {prItem?.quantity} {prItem?.uom} - - -
- { - const value = Math.max(0, Math.floor(parseFloat(e.target.value) || 0)) - setValue(`quotationItems.${index}.unitPrice`, value) - calculateTotal(index) - }} - className="w-[120px]" - placeholder="0" - /> - - {currency} + {prItem?.materialCategory && ( + + {prItem.materialCategory} + + )} + {prItem?.size && ( + + 크기: {prItem.size} + + )} +
+
+ + + {prItem?.quantity ? prItem.quantity.toLocaleString() : "-"} + + + + + {prItem?.uom || "-"} - - - - {formatCurrency(quotationItem?.totalPrice || 0, currency)} - - - - - - - - setValue(`quotationItems.${index}.vendorDeliveryDate`, date)} - initialFocus + + + + {prItem?.grossWeight ? prItem.grossWeight.toLocaleString() : "-"} + + + + + {prItem?.gwUom || "-"} + + + +
+ { + const value = Math.max(0, Math.floor(parseFloat(e.target.value) || 0)) + setValue(`quotationItems.${index}.unitPrice`, value) + calculateTotal(index) + }} + className="w-[120px]" + placeholder="0" /> - - - {prItem?.deliveryDate && quotationItem?.vendorDeliveryDate && - new Date(quotationItem.vendorDeliveryDate) > new Date(prItem.deliveryDate) && ( -
- - 지연 - + + {currency} +
- )} - - - - - - ) - })} - -
-
+ + + {formatCurrency(quotationItem?.totalPrice || 0, currency)} + + + + + + + + setValue(`quotationItems.${index}.vendorDeliveryDate`, date)} + initialFocus + /> + + + {prItem?.deliveryDate && quotationItem?.vendorDeliveryDate && + new Date(quotationItem.vendorDeliveryDate) > new Date(prItem.deliveryDate) && ( +
+ + 지연 + +
+ )} +
+ +
+ {/* 사양서 정보 */} + {(prItem?.specNo || prItem?.specUrl) && ( +
+ {prItem.specNo && ( + {prItem.specNo} + )} + {prItem.specUrl && ( + + )} +
+ )} + + {/* POS 파일 다운로드 */} + {prItem?.materialCode && ( +
+ + +
+ )} + + {/* 트래킹 번호 */} + {prItem?.trackingNo && ( +
+ TRK: {prItem.trackingNo} +
+ )} +
+
+ +
+ {[ + prItem?.projectDef && `${prItem.projectDef}`, + prItem?.projectSc && `SC: ${prItem.projectSc}`, + prItem?.projectKl && `KL: ${prItem.projectKl}`, + prItem?.projectLc && `LC: ${prItem.projectLc}`, + prItem?.projectDl && `DL: ${prItem.projectDl}` + ].filter(Boolean).join(" | ") || "-"} +
+
+ + + + + ) + })} + + + + {/* 총액 요약 */}
@@ -633,6 +834,16 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp + + {/* POS 파일 선택 다이얼로그 */} + ) } \ No newline at end of file -- cgit v1.2.3