From 1bda7f20f113737f4af32495e7ff24f6022dc283 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 24 Sep 2025 08:01:37 +0000 Subject: (최겸) 구매 피드백 반영(품목 납기일 일괄설정, 마감일 기본값 설정 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/itb/service.ts | 8 +- lib/rfq-last/attachment/vendor-response-table.tsx | 2 +- lib/rfq-last/service.ts | 15 +- .../editor/commercial-terms-form.tsx | 11 +- .../editor/quotation-items-table.tsx | 174 +++++++++++++++++++-- .../vendor-response/editor/rfq-info-header.tsx | 2 + .../editor/vendor-response-editor.tsx | 4 +- 7 files changed, 197 insertions(+), 19 deletions(-) diff --git a/lib/itb/service.ts b/lib/itb/service.ts index 9664bfca..f649bdf5 100644 --- a/lib/itb/service.ts +++ b/lib/itb/service.ts @@ -10,7 +10,8 @@ import { authOptions } from '@/app/api/auth/[...nextauth]/route' import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { GetPurchaseRequestsSchema } from "./validations"; -import { z } from "zod" +import { z } from "zod"; +import { getDefaultDueDate } from "@/lib/rfq-last/service"; const createRequestSchema = z.object({ requestTitle: z.string().min(1), @@ -640,6 +641,9 @@ export async function approvePurchaseRequestsAndCreateRfqs( const rfqCode = await generateItbRfqCode(purchasePicId) + // 마감일 기본값 설정 (입력값 없으면 생성일 + 7일) + const defaultDueDate = getDefaultDueDate(); + const [rfq] = await tx .insert(rfqsLast) .values({ @@ -652,6 +656,7 @@ export async function approvePurchaseRequestsAndCreateRfqs( EngPicName: request.engPicName, pic: purchasePicId || null, status: "RFQ 생성", + dueDate: defaultDueDate, // 마감일 기본값 설정 projectCompany: request.projectCompany, projectSite: request.projectSite, smCode: request.smCode, @@ -711,6 +716,7 @@ export async function approvePurchaseRequestsAndCreateRfqs( rfqItem: `${index + 1}`.padStart(3, '0'), prItem: `${index + 1}`.padStart(3, '0'), prNo: rfqCode, + materialCategory:request.majorItemMaterialCategory, materialCode: item.itemCode, materialDescription: item.itemName, quantity: item.quantity, diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx index f9388752..8be5210f 100644 --- a/lib/rfq-last/attachment/vendor-response-table.tsx +++ b/lib/rfq-last/attachment/vendor-response-table.tsx @@ -43,7 +43,7 @@ import type { DataTableRowAction, } from "@/types/table"; import { cn } from "@/lib/utils"; -import { getRfqVendorAttachments } from "@/lib/rfq-last/service"; +import { getRfqVendorAttachments, updateAttachmentTypes } from "@/lib/rfq-last/service"; import { downloadFile } from "@/lib/file-download"; import { toast } from "sonner"; import { diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 27e278ab..82f8837a 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -18,6 +18,14 @@ import { generateBasicContractsForVendor } from "../basic-contract/gen-service"; import { writeFile, mkdir } from "fs/promises"; import { generateItbRfqCode } from "../itb/service"; +/** + * RFQ 마감일 기본값 계산 (생성일 + 7일) + */ +export async function getDefaultDueDate(): Promise { + const defaultDueDate = new Date(); + defaultDueDate.setDate(defaultDueDate.getDate() + 7); + return defaultDueDate; +} export async function getRfqs(input: GetRfqsSchema) { unstable_noStore(); @@ -346,7 +354,10 @@ export async function createGeneralRfqAction(input: CreateGeneralRfqInput) { // 4. 대표 아이템 정보 추출 (첫 번째 아이템) const representativeItem = input.items[0]; - // 5. rfqsLast 테이블에 기본 정보 삽입 + // 5. 마감일 기본값 설정 (입력값 없으면 생성일 + 7일) + const dueDate = input.dueDate || getDefaultDueDate(); + + // 6. rfqsLast 테이블에 기본 정보 삽입 const [newRfq] = await tx .insert(rfqsLast) .values({ @@ -354,7 +365,7 @@ export async function createGeneralRfqAction(input: CreateGeneralRfqInput) { rfqType: input.rfqType, rfqTitle: input.rfqTitle, status: "RFQ 생성", - dueDate: input.dueDate, + dueDate: dueDate, // 마감일 기본값 설정 // 대표 아이템 정보 itemCode: representativeItem.itemCode, diff --git a/lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx b/lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx index f0c69d8b..d896ee34 100644 --- a/lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx +++ b/lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx @@ -84,7 +84,16 @@ export default function CommercialTermsForm({ rfqDetail, rfq }: CommercialTermsF const isDifferentCurrency = vendorCurrency !== rfqDetail.currency const isDifferentPaymentTerms = vendorPaymentTermsCode !== rfqDetail.paymentTermsCode const isDifferentIncoterms = vendorIncotermsCode !== rfqDetail.incotermsCode - const isDifferentDeliveryDate = !isFrameContract && vendorDeliveryDate?.toISOString() !== rfqDetail.deliveryDate + + // 날짜만 비교 (년월일만 체크) + const formatDateOnly = (date: Date | string | null) => { + if (!date) return null + const d = new Date(date) + return d.toISOString().split('T')[0] // YYYY-MM-DD 형식으로 변환 + } + const isDifferentDeliveryDate = !isFrameContract && + formatDateOnly(vendorDeliveryDate) !== formatDateOnly(rfqDetail.deliveryDate) + const isDifferentContractDuration = isFrameContract && vendorContractDuration !== rfqDetail.contractDuration // 데이터 로드 함수들 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 08928b4d..d98376c1 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -14,14 +14,26 @@ import { Label } from "@/components/ui/label" import { CalendarIcon, Eye, Calculator, AlertCircle } from "lucide-react" import { format } from "date-fns" import { cn, formatCurrency } from "@/lib/utils" -import { useState } from "react" +import { useState, useEffect } from "react" import { Dialog, DialogContent, DialogDescription, + DialogFooter, 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[] @@ -36,10 +48,30 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp const [selectedItem, setSelectedItem] = useState(null) const [showDetail, setShowDetail] = useState(false) - + const [showBulkDateDialog, setShowBulkDateDialog] = useState(false) + const [bulkDeliveryDate, setBulkDeliveryDate] = useState(undefined) + const currency = watch("vendorCurrency") || "USD" const quotationItems = watch("quotationItems") + // PR 아이템 정보를 quotationItems에 초기화 + useEffect(() => { + if (prItems && prItems.length > 0) { + prItems.forEach((prItem, index) => { + // PR 아이템 정보를 quotationItem에 포함 + setValue(`quotationItems.${index}.prNo`, prItem.prNo) + setValue(`quotationItems.${index}.materialCode`, prItem.materialCode) + setValue(`quotationItems.${index}.materialDescription`, prItem.materialDescription) + setValue(`quotationItems.${index}.quantity`, prItem.quantity) + setValue(`quotationItems.${index}.uom`, prItem.uom) + setValue(`quotationItems.${index}.rfqPrItemId`, prItem.id) + + // currency는 vendorCurrency를 따름 + setValue(`quotationItems.${index}.currency`, currency) + }) + } + }, [prItems, setValue, currency]) + // 단가 * 수량 계산 const calculateTotal = (index: number) => { const item = quotationItems[index] @@ -47,6 +79,13 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp if (item && prItem) { const total = (item.unitPrice || 0) * (prItem.quantity || 0) setValue(`quotationItems.${index}.totalPrice`, total) + + // PR 아이템 정보도 함께 업데이트 (값이 변경되었을 수 있음) + setValue(`quotationItems.${index}.prNo`, prItem.prNo) + setValue(`quotationItems.${index}.materialCode`, prItem.materialCode) + setValue(`quotationItems.${index}.materialDescription`, prItem.materialDescription) + setValue(`quotationItems.${index}.quantity`, prItem.quantity) + setValue(`quotationItems.${index}.uom`, prItem.uom) } } @@ -59,8 +98,27 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp const discountAmount = originalTotal * (item.discountRate / 100) const finalTotal = originalTotal - discountAmount setValue(`quotationItems.${index}.totalPrice`, finalTotal) + setValue(`quotationItems.${index}.discountAmount`, discountAmount) + } + } + + // 일괄 납기일 적용 + const applyBulkDeliveryDate = () => { + if (bulkDeliveryDate && fields.length > 0) { + fields.forEach((_, index) => { + setValue(`quotationItems.${index}.vendorDeliveryDate`, bulkDeliveryDate) + }) + setShowBulkDateDialog(false) + setBulkDeliveryDate(undefined) } } + + // 납기일 초기화 + const clearAllDeliveryDates = () => { + fields.forEach((_, index) => { + setValue(`quotationItems.${index}.vendorDeliveryDate`, undefined) + }) + } const totalAmount = quotationItems?.reduce( (sum: number, item: any) => sum + (item.totalPrice || 0), 0 @@ -118,7 +176,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp {/* 제조사 정보 */} - + {/* 제조사 정보 @@ -150,7 +208,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp /> - + */} {/* 기술 준수 및 대안 */} @@ -198,7 +256,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp {/* 할인 정보 */} - + {/* 할인 정보 @@ -231,7 +289,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp - + */} {/* 비고 */} @@ -261,11 +319,32 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp 각 PR 아이템에 대한 견적 단가와 정보를 입력하세요 -
-

총 견적금액

-

- {formatCurrency(totalAmount, currency)} -

+
+
+ + +
+
+

총 견적금액

+

+ {formatCurrency(totalAmount, currency)} +

+
@@ -329,10 +408,12 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
{ - setValue(`quotationItems.${index}.unitPrice`, parseFloat(e.target.value)) + const value = Math.max(0, parseFloat(e.target.value) || 0) + setValue(`quotationItems.${index}.unitPrice`, value) calculateTotal(index) }} className="w-[120px]" @@ -438,12 +519,79 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp {/* 상세 다이얼로그 */} {selectedItem && ( - )} + + {/* 일괄 납기일 설정 다이얼로그 */} + + + + 전체 납기일 설정 + + 모든 PR 아이템에 동일한 납기일을 적용합니다. + + + +
+
+ + + + + + + + + +
+ +
+

+ 선택된 날짜가 {fields.length}개의 모든 PR 아이템에 적용됩니다. + 기존에 설정된 납기일은 모두 교체됩니다. +

+
+
+ + + + + +
+
) } \ No newline at end of file diff --git a/lib/rfq-last/vendor-response/editor/rfq-info-header.tsx b/lib/rfq-last/vendor-response/editor/rfq-info-header.tsx index fe8c5508..fbbaff80 100644 --- a/lib/rfq-last/vendor-response/editor/rfq-info-header.tsx +++ b/lib/rfq-last/vendor-response/editor/rfq-info-header.tsx @@ -17,6 +17,8 @@ export default function RfqInfoHeader({ rfq, rfqDetail, vendor }: RfqInfoHeaderP const majorMaterial = rfq.rfqPrItems?.find(v => v.majorYn) const router = useRouter() + console.log(rfq,"rfq") + const handleGoBack = () => { router.push("/partners/rfq-last"); }; diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx index c6e81f32..6da704cd 100644 --- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx +++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx @@ -297,7 +297,9 @@ export default function VendorResponseEditor({ await uploadPromise toast.success(isSubmit ? "견적서가 제출되었습니다." : "견적서가 저장되었습니다.") - router.push('/partners/rfq-last') + if (isSubmit) { + router.push('/partners/rfq-last') + } router.refresh() } catch (error) { console.error('Submit error:', error) // 더 상세한 에러 로깅 -- cgit v1.2.3