diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-24 08:01:37 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-24 08:01:37 +0000 |
| commit | 1bda7f20f113737f4af32495e7ff24f6022dc283 (patch) | |
| tree | adc6e61ff9b546b277804c82bc6ca24db0347fd6 | |
| parent | 4fe733d7d9d3d873fa395133e9a42cf9fc8c44dc (diff) | |
(최겸) 구매 피드백 반영(품목 납기일 일괄설정, 마감일 기본값 설정 등)
| -rw-r--r-- | lib/itb/service.ts | 8 | ||||
| -rw-r--r-- | lib/rfq-last/attachment/vendor-response-table.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 15 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx | 11 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/quotation-items-table.tsx | 174 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/rfq-info-header.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/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<Date> { + 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<any>(null) const [showDetail, setShowDetail] = useState(false) - + const [showBulkDateDialog, setShowBulkDateDialog] = useState(false) + const [bulkDeliveryDate, setBulkDeliveryDate] = useState<Date | undefined>(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 </Card> {/* 제조사 정보 */} - <Card> + {/* <Card> <CardHeader className="pb-3"> <CardTitle className="text-base">제조사 정보</CardTitle> </CardHeader> @@ -150,7 +208,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp /> </div> </CardContent> - </Card> + </Card> */} {/* 기술 준수 및 대안 */} <Card> @@ -198,7 +256,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp </Card> {/* 할인 정보 */} - <Card> + {/* <Card> <CardHeader className="pb-3"> <CardTitle className="text-base">할인 정보</CardTitle> </CardHeader> @@ -231,7 +289,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp </div> </div> </CardContent> - </Card> + </Card> */} {/* 비고 */} <Card> @@ -261,11 +319,32 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp 각 PR 아이템에 대한 견적 단가와 정보를 입력하세요 </CardDescription> </div> - <div className="text-right"> - <p className="text-sm text-muted-foreground">총 견적금액</p> - <p className="text-2xl font-bold text-primary"> - {formatCurrency(totalAmount, currency)} - </p> + <div className="flex items-center gap-2"> + <div className="flex gap-2"> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => setShowBulkDateDialog(true)} + > + <CalendarIcon className="h-4 w-4 mr-1" /> + 전체 납기일 설정 + </Button> + <Button + type="button" + variant="outline" + size="sm" + onClick={clearAllDeliveryDates} + > + 납기일 초기화 + </Button> + </div> + <div className="text-right"> + <p className="text-sm text-muted-foreground">총 견적금액</p> + <p className="text-2xl font-bold text-primary"> + {formatCurrency(totalAmount, currency)} + </p> + </div> </div> </div> </CardHeader> @@ -329,10 +408,12 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp <div className="flex items-center gap-1"> <Input type="number" + min="0" step="0.01" {...register(`quotationItems.${index}.unitPrice`, { valueAsNumber: true })} onChange={(e) => { - 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 && ( - <ItemDetailDialog + <ItemDetailDialog item={selectedItem.item} prItem={selectedItem.prItem} index={selectedItem.index} /> )} + + {/* 일괄 납기일 설정 다이얼로그 */} + <Dialog open={showBulkDateDialog} onOpenChange={setShowBulkDateDialog}> + <DialogContent className="sm:max-w-md"> + <DialogHeader> + <DialogTitle>전체 납기일 설정</DialogTitle> + <DialogDescription> + 모든 PR 아이템에 동일한 납기일을 적용합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + <div className="space-y-2"> + <Label>납기일 선택</Label> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + className={cn( + "w-full justify-start text-left font-normal", + !bulkDeliveryDate && "text-muted-foreground" + )} + > + <CalendarIcon className="mr-2 h-4 w-4" /> + {bulkDeliveryDate ? format(bulkDeliveryDate, "yyyy-MM-dd") : "날짜 선택"} + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={bulkDeliveryDate} + onSelect={setBulkDeliveryDate} + initialFocus + /> + </PopoverContent> + </Popover> + </div> + + <div className="bg-muted/50 rounded-lg p-3"> + <p className="text-sm text-muted-foreground"> + 선택된 날짜가 <strong>{fields.length}개</strong>의 모든 PR 아이템에 적용됩니다. + 기존에 설정된 납기일은 모두 교체됩니다. + </p> + </div> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => { + setShowBulkDateDialog(false) + setBulkDeliveryDate(undefined) + }} + > + 취소 + </Button> + <Button + type="button" + onClick={applyBulkDeliveryDate} + disabled={!bulkDeliveryDate} + > + 전체 적용 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> </Card> ) }
\ 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) // 더 상세한 에러 로깅 |
