summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-24 08:01:37 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-24 08:01:37 +0000
commit1bda7f20f113737f4af32495e7ff24f6022dc283 (patch)
treeadc6e61ff9b546b277804c82bc6ca24db0347fd6 /lib
parent4fe733d7d9d3d873fa395133e9a42cf9fc8c44dc (diff)
(최겸) 구매 피드백 반영(품목 납기일 일괄설정, 마감일 기본값 설정 등)
Diffstat (limited to 'lib')
-rw-r--r--lib/itb/service.ts8
-rw-r--r--lib/rfq-last/attachment/vendor-response-table.tsx2
-rw-r--r--lib/rfq-last/service.ts15
-rw-r--r--lib/rfq-last/vendor-response/editor/commercial-terms-form.tsx11
-rw-r--r--lib/rfq-last/vendor-response/editor/quotation-items-table.tsx174
-rw-r--r--lib/rfq-last/vendor-response/editor/rfq-info-header.tsx2
-rw-r--r--lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx4
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) // 더 상세한 에러 로깅