From c8cccaf1198ae48754ac036b579732018f5b448a Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 23 Oct 2025 03:30:01 +0000 Subject: (최겸) 기술영업 조선 rfq 수정(벤더, 담당자 임시삭제기능 추가) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/service.ts | 28 ++- lib/techsales-rfq/table/create-rfq-ship-dialog.tsx | 9 +- .../table/detail-table/add-vendor-dialog.tsx | 213 ++++++++++++--------- .../quotation-contacts-view-dialog.tsx | 94 ++++----- .../detail-table/quotation-history-dialog.tsx | 26 ++- .../table/detail-table/rfq-detail-table.tsx | 1 + lib/techsales-rfq/table/project-detail-dialog.tsx | 87 +++++++++ lib/techsales-rfq/table/update-rfq-sheet.tsx | 156 ++++++++++++++- .../detail/quotation-response-tab.tsx | 1 + 9 files changed, 454 insertions(+), 161 deletions(-) (limited to 'lib/techsales-rfq') diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 058ef48b..9a198ee5 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -2,8 +2,8 @@ import { unstable_noStore, revalidateTag, revalidatePath } from "next/cache"; import db from "@/db/db"; -import { - techSalesRfqs, +import { + techSalesRfqs, techSalesVendorQuotations, techSalesVendorQuotationRevisions, techSalesAttachments, @@ -13,7 +13,8 @@ import { users, techSalesRfqComments, techSalesRfqItems, - biddingProjects + biddingProjects, + projectSeries } from "@/db/schema"; import { and, desc, eq, ilike, or, sql, inArray, count, asc, lt, ne } from "drizzle-orm"; import { unstable_cache } from "@/lib/unstable-cache"; @@ -4209,6 +4210,7 @@ export async function getQuotationContacts(quotationId: number) { contactId: techSalesVendorQuotationContacts.contactId, contactName: techVendorContacts.contactName, contactPosition: techVendorContacts.contactPosition, + contactTitle: techVendorContacts.contactTitle, contactEmail: techVendorContacts.contactEmail, contactPhone: techVendorContacts.contactPhone, contactCountry: techVendorContacts.contactCountry, @@ -4320,15 +4322,23 @@ export async function getTechSalesRfqById(id: number) { pjtType: biddingProjects.pjtType, ptypeNm: biddingProjects.ptypeNm, projMsrm: biddingProjects.projMsrm, + pspid: biddingProjects.pspid, }) .from(biddingProjects) .where(eq(biddingProjects.id, rfq?.biddingProjectId ?? 0)); - + + // 시리즈 정보 가져오기 + const series = await db + .select() + .from(projectSeries) + .where(eq(projectSeries.pspid, project?.pspid ?? "")) + .orderBy(projectSeries.sersNo); + if (!rfq) { return { data: null, error: "RFQ를 찾을 수 없습니다." }; } - - return { data: { ...rfq, project }, error: null }; + + return { data: { ...rfq, project, series }, error: null }; } catch (err) { console.error("Error fetching RFQ:", err); return { data: null, error: getErrorMessage(err) }; @@ -4339,6 +4349,7 @@ export async function getTechSalesRfqById(id: number) { export async function updateTechSalesRfq(data: { id: number; description: string; + remark: string; dueDate: Date; updatedBy: number; }) { @@ -4347,15 +4358,16 @@ export async function updateTechSalesRfq(data: { const rfq = await tx.query.techSalesRfqs.findFirst({ where: eq(techSalesRfqs.id, data.id), }); - + if (!rfq) { return { data: null, error: "RFQ를 찾을 수 없습니다." }; } - + const [updatedRfq] = await tx .update(techSalesRfqs) .set({ description: data.description, // description 필드로 업데이트 + remark: data.remark, // remark 필드로 업데이트 dueDate: data.dueDate, updatedAt: new Date(), }) diff --git a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx index b851f7e8..035cd97e 100644 --- a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { toast } from "sonner" import { ArrowUpDown, CheckSquare, Plus, Search, Square, X, Loader2 } from "lucide-react" import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { CalendarIcon } from "lucide-react" @@ -228,9 +229,10 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { return filtered }, [allItems, itemSearchQuery, selectedWorkType, selectedShipType]) - // 사용 가능한 선종 목록 가져오기 (OPTION 제외) + // 사용 가능한 선종 목록 가져오기 (OPTION 제외, others는 맨 밑으로) const availableShipTypes = React.useMemo(() => { - return shipTypes.filter(shipType => shipType !== "OPTION") + const filtered = shipTypes.filter(shipType => shipType !== "OPTION") + return [...filtered.filter(type => type !== "OTHERS"), ...filtered.filter(type => type === "OTHERS")] }, [shipTypes]) // 프로젝트 선택 처리 @@ -408,8 +410,9 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { RFQ Context - diff --git a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx index ea982407..438ee840 100644 --- a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx @@ -362,99 +362,128 @@ export function AddVendorDialog({ ) // 벤더 구분자 설정 UI - const renderVendorFlagsStep = () => ( -
-
- 선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다. -
- - {selectedVendorData.length > 0 ? ( -
- {selectedVendorData.map((vendor) => ( - - - {vendor.vendorName} - - {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`} - - - -
-
- - handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean) - } - /> - -
- -
- - handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean) - } - /> - -
- -
- - handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean) - } - /> - -
- -
- - handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) - } - /> - -
-
-
-
- ))} -
- ) : ( -
- 선택된 벤더가 없습니다 + const renderVendorFlagsStep = () => { + const isShipRfq = selectedRfq?.rfqType === 'SHIP' + + return ( +
+
+ 선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다.
- )} -
- ) + + {selectedVendorData.length > 0 ? ( +
+ {selectedVendorData.map((vendor) => ( + + + {vendor.vendorName} + + {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`} + + + +
+ {/* 조선 RFQ인 경우: 고객 선호벤더, 신규발굴벤더, SHI Proposal Vendor 표시 */} + {isShipRfq && ( + <> +
+ + handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean) + } + /> + +
+ +
+ + handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean) + } + /> + +
+
+ + handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) + } + /> + +
+ + )} + + {/* 조선 RFQ가 아닌 경우: Project Approved Vendor, SHI Proposal Vendor 표시 */} + {!isShipRfq && ( + <> +
+ + handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean) + } + /> + +
+ +
+ + handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) + } + /> + +
+ + )} +
+
+
+ ))} +
+ ) : ( +
+ 선택된 벤더가 없습니다 +
+ )} +
+ ) + } return ( diff --git a/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx index 61c97b1b..608b5670 100644 --- a/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx @@ -109,60 +109,62 @@ export function QuotationContactsViewDialog({
) : (
- {contacts.map((contact) => ( -
-
- -
-
- {contact.contactName} - {contact.isPrimary && ( - - 주담당자 - + {contacts + .filter((contact) => contact.contactTitle) // contactTitle이 있는 담당자만 필터링 (체크표시된 담당자) + .map((contact) => ( +
+
+ +
+
+ {contact.contactName} + {contact.isPrimary && ( + + 주담당자 + + )} +
+ {contact.contactPosition && ( +

+ {contact.contactPosition} +

+ )} + {contact.contactTitle && ( +

+ {contact.contactTitle} +

+ )} + {contact.contactCountry && ( +

+ {contact.contactCountry} +

)}
- {contact.contactPosition && ( -

- {contact.contactPosition} -

- )} - {contact.contactTitle && ( -

- {contact.contactTitle} -

- )} - {contact.contactCountry && ( -

- {contact.contactCountry} -

- )} -
-
- -
-
- - {contact.contactEmail}
- {contact.contactPhone && ( + +
- - {contact.contactPhone} + + {contact.contactEmail} +
+ {contact.contactPhone && ( +
+ + {contact.contactPhone} +
+ )} +
+ 발송일: {new Date(contact.createdAt).toLocaleDateString('ko-KR')}
- )} -
- 발송일: {new Date(contact.createdAt).toLocaleDateString('ko-KR')}
-
- ))} - + ))} +
- 총 {contacts.length}명의 담당자에게 발송됨 + 총 {contacts.filter((contact) => contact.contactTitle).length}명의 담당자에게 발송됨
)} diff --git a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx index 7d972b91..023d3599 100644 --- a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx @@ -78,6 +78,7 @@ interface QuotationHistoryDialogProps { open: boolean onOpenChange: (open: boolean) => void quotationId: number | null + isInternal?: boolean // 내부 사용자인지 여부 (partners가 아니면 내부) } const statusConfig = { @@ -88,15 +89,16 @@ const statusConfig = { "Rejected": { label: "거절됨", color: "bg-red-100 text-red-800" }, } -function QuotationCard({ - data, - version, - isCurrent = false, - revisedBy, +function QuotationCard({ + data, + version, + isCurrent = false, + revisedBy, revisedAt, attachments, revisionId, revisionNote, + isInternal = false, }: { data: QuotationSnapshot | QuotationHistoryData["current"] version: number @@ -106,6 +108,7 @@ function QuotationCard({ attachments?: QuotationAttachment[] revisionId?: number revisionNote?: string | null + isInternal?: boolean }) { const statusInfo = statusConfig[data.status as keyof typeof statusConfig] || { label: data.status || "알 수 없음", color: "bg-gray-100 text-gray-800" } @@ -171,7 +174,7 @@ function QuotationCard({
)} - {revisionId && ( + {revisionId && isInternal && (

SHI Comment