diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
6 files changed, 117 insertions, 217 deletions
diff --git a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx index e4b1b8c3..a8f44474 100644 --- a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx +++ b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx @@ -4,34 +4,7 @@ import * as React from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" -import { formatDateToQuarter, formatDate } from "@/lib/utils" - -interface ProjectSnapshot { - pspid?: string - projNm?: string - projMsrm?: number - kunnr?: string - kunnrNm?: string - cls1?: string - cls1Nm?: string - ptype?: string - ptypeNm?: string - estmPm?: string - scDt?: string - klDt?: string - lcDt?: string - dlDt?: string - dockNo?: string - dockNm?: string - projNo?: string - ownerNm?: string - pspUpdatedAt?: string | Date -} - -interface SeriesSnapshot { - sersNo?: string - klDt?: string -} +import { formatDate } from "@/lib/utils" interface ProjectInfoTabProps { quotation: { @@ -43,17 +16,13 @@ interface ProjectInfoTabProps { dueDate: Date | null status: string | null remark: string | null - projectSnapshot?: ProjectSnapshot | null - seriesSnapshot?: SeriesSnapshot[] | null - item?: { - id: number - itemCode: string | null - itemList: string | null - } | null biddingProject?: { id: number pspid: string | null projNm: string | null + sector: string | null + projMsrm: string | null + ptypeNm: string | null } | null createdByUser?: { id: number @@ -71,8 +40,6 @@ interface ProjectInfoTabProps { export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { const rfq = quotation.rfq - const projectSnapshot = rfq?.projectSnapshot - const seriesSnapshot = rfq?.seriesSnapshot console.log("rfq: ", rfq) @@ -110,15 +77,10 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { <div className="text-sm">{rfq.rfqCode || "미할당"}</div> </div> <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">자재 코드</div> + <div className="text-sm font-medium text-muted-foreground">자재 그룹</div> <div className="text-sm">{rfq.materialCode || "N/A"}</div> </div> <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">자재명</div> - {/* TODO : 타입 작업 (시연을 위해 빌드 중단 상태임. 추후 수정) */} - <div className="text-sm"><strong>{rfq.itemShipbuilding?.itemList || "N/A"}</strong></div> - </div> - <div className="space-y-2"> <div className="text-sm font-medium text-muted-foreground">마감일</div> <div className="text-sm"> {rfq.dueDate ? formatDate(rfq.dueDate) : "N/A"} @@ -164,108 +126,23 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { <div className="text-sm font-medium text-muted-foreground">프로젝트명</div> <div className="text-sm">{rfq.biddingProject.projNm || "N/A"}</div> </div> - </div> - </CardContent> - </Card> - )} - - {/* 프로젝트 스냅샷 정보 */} - {projectSnapshot && ( - <Card> - <CardHeader> - <CardTitle>프로젝트 스냅샷</CardTitle> - <CardDescription> - RFQ 생성 시점의 프로젝트 상세 정보 - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {projectSnapshot.projNo && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">공사번호</div> - <div className="text-sm">{projectSnapshot.projNo}</div> - </div> - )} - {projectSnapshot.projNm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">공사명</div> - <div className="text-sm">{projectSnapshot.projNm}</div> - </div> - )} - {projectSnapshot.estmPm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">견적 PM</div> - <div className="text-sm">{projectSnapshot.estmPm}</div> - </div> - )} - {projectSnapshot.kunnrNm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">선주</div> - <div className="text-sm">{projectSnapshot.kunnrNm}</div> - </div> - )} - {projectSnapshot.cls1Nm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">선급</div> - <div className="text-sm">{projectSnapshot.cls1Nm}</div> - </div> - )} - {projectSnapshot.projMsrm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">척수</div> - <div className="text-sm">{projectSnapshot.projMsrm}</div> - </div> - )} - {projectSnapshot.ptypeNm && ( - <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">선종</div> - <div className="text-sm">{projectSnapshot.ptypeNm}</div> - </div> - )} - </div> - </CardContent> - </Card> - )} - - {/* 시리즈 스냅샷 정보 */} - {seriesSnapshot && Array.isArray(seriesSnapshot) && seriesSnapshot.length > 0 && ( - <Card> - <CardHeader> - <CardTitle>시리즈 정보 스냅샷</CardTitle> - <CardDescription> - 프로젝트의 시리즈별 K/L 일정 정보 - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - {seriesSnapshot.map((series: SeriesSnapshot, index: number) => ( - <div key={index} className="border rounded-lg p-4 space-y-3"> - <div className="flex items-center gap-2"> - <Badge variant="secondary">시리즈 {series.sersNo || index + 1}</Badge> - </div> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - {series.klDt && ( - <div className="space-y-1"> - <div className="text-xs font-medium text-muted-foreground">K/L</div> - <div className="text-sm">{formatDateToQuarter(series.klDt)}</div> - </div> - )} - </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">프로젝트 섹터</div> + <div className="text-sm">{rfq.biddingProject.sector || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">프로젝트 규모</div> + <div className="text-sm">{rfq.biddingProject.projMsrm || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">프로젝트 타입</div> + <div className="text-sm">{rfq.biddingProject.ptypeNm || "N/A"}</div> </div> - ))} - </CardContent> - </Card> - )} - - {/* 정보가 없는 경우 */} - {!projectSnapshot && !seriesSnapshot && ( - <Card> - <CardContent className="text-center py-8"> - <div className="text-muted-foreground"> - 추가 프로젝트 상세정보가 없습니다. </div> </CardContent> </Card> )} + </div> </ScrollArea> ) diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx index 97bba2bd..2e2f5d70 100644 --- a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx +++ b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx @@ -7,36 +7,6 @@ import { ProjectInfoTab } from "./project-info-tab" import { QuotationResponseTab } from "./quotation-response-tab" import { CommunicationTab } from "./communication-tab" -// 프로젝트 스냅샷 타입 정의 -interface ProjectSnapshot { - scDt?: string - klDt?: string - lcDt?: string - dlDt?: string - dockNo?: string - dockNm?: string - projNo?: string - projNm?: string - ownerNm?: string - kunnrNm?: string - cls1Nm?: string - projMsrm?: number - ptypeNm?: string - sector?: string - estmPm?: string -} - -// 시리즈 스냅샷 타입 정의 -interface SeriesSnapshot { - sersNo?: string - scDt?: string - klDt?: string - lcDt?: string - dlDt?: string - dockNo?: string - dockNm?: string -} - interface QuotationData { id: number status: string @@ -51,17 +21,13 @@ interface QuotationData { dueDate: Date | null status: string | null remark: string | null - projectSnapshot?: ProjectSnapshot | null - seriesSnapshot?: SeriesSnapshot[] | null - item?: { - id: number - itemCode: string | null - itemList: string | null - } | null biddingProject?: { id: number pspid: string | null projNm: string | null + sector: string | null + projMsrm: string | null + ptypeNm: string | null } | null createdByUser?: { id: number diff --git a/lib/techsales-rfq/vendor-response/quotation-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-editor.tsx index b30f612c..54058214 100644 --- a/lib/techsales-rfq/vendor-response/quotation-editor.tsx +++ b/lib/techsales-rfq/vendor-response/quotation-editor.tsx @@ -338,7 +338,7 @@ export default function TechSalesQuotationEditor({ quotation }: TechSalesQuotati <p className="font-mono">{quotation.rfq.rfqCode}</p> </div> <div> - <label className="text-sm font-medium text-muted-foreground">자재 코드</label> + <label className="text-sm font-medium text-muted-foreground">자재 그룹</label> <p>{quotation.rfq.materialCode || "N/A"}</p> </div> <div> diff --git a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx index e11864dc..92bec96a 100644 --- a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx +++ b/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx @@ -365,7 +365,7 @@ export function QuotationItemEditor({ onBlur={(e) => handleBlur(index, field, e.target.value)} disabled={disabled || isSaving || !item.isAlternative} className="w-full" - placeholder={field === 'vendorMaterialCode' ? "벤더 자재코드" : "벤더 자재명"} + placeholder={field === 'vendorMaterialCode' ? "벤더 자재그룹" : "벤더 자재명"} /> ) } else if (field === 'deliveryDate') { @@ -406,14 +406,14 @@ export function QuotationItemEditor({ return ( <div className="mt-2 p-3 bg-blue-50 rounded-md space-y-2 text-sm"> {/* <div className="flex flex-col gap-2"> - <label className="text-xs font-medium text-blue-700">벤더 자재코드</label> + <label className="text-xs font-medium text-blue-700">벤더 자재그룹</label> <Input value={item.vendorMaterialCode || ""} onChange={(e) => handleTextInputChange(index, 'vendorMaterialCode', e)} onBlur={(e) => handleBlur(index, 'vendorMaterialCode', e.target.value)} disabled={disabled || isSaving} className="h-8 text-sm" - placeholder="벤더 자재코드 입력" + placeholder="벤더 자재그룹 입력" /> </div> */} @@ -511,7 +511,7 @@ export function QuotationItemEditor({ <TableHeader className="sticky top-0 bg-background"> <TableRow> <TableHead className="w-[50px]">번호</TableHead> - <TableHead>자재코드</TableHead> + <TableHead>자재그룹</TableHead> <TableHead>자재명</TableHead> <TableHead>수량</TableHead> <TableHead>단위</TableHead> diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index cf1dac42..ddee2317 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { type ColumnDef } from "@tanstack/react-table" -import { Edit, Paperclip } from "lucide-react" +import { Edit, Paperclip, Package } from "lucide-react" import { formatCurrency, formatDate, formatDateTime } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -29,7 +29,8 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { // 아이템 정보 itemName?: string; - itemShipbuildingId?: number; + + itemCount?: number; // 프로젝트 정보 projNm?: string; @@ -44,14 +45,6 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { createdByName?: string | null; updatedByName?: string | null; - // 견적 코드 및 버전 - quotationCode?: string | null; - quotationVersion?: number | null; - - // 추가 상태 정보 - rejectionReason?: string | null; - acceptedAt?: Date | null; - // 첨부파일 개수 attachmentCount?: number; } @@ -59,9 +52,10 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { interface GetColumnsProps { router: AppRouterInstance; openAttachmentsSheet: (rfqId: number) => void; + openItemsDialog: (rfq: { id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; }) => void; } -export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): ColumnDef<QuotationWithRfqCode>[] { +export function getColumns({ router, openAttachmentsSheet, openItemsDialog }: GetColumnsProps): ColumnDef<QuotationWithRfqCode>[] { return [ { id: "select", @@ -151,7 +145,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C // { // accessorKey: "materialCode", // header: ({ column }) => ( - // <DataTableColumnHeaderSimple column={column} title="자재 코드" /> + // <DataTableColumnHeaderSimple column={column} title="자재 그룹" /> // ), // cell: ({ row }) => { // const materialCode = row.getValue("materialCode") as string; @@ -251,6 +245,59 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C // enableHiding: true, // }, { + id: "items", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="아이템" /> + ), + cell: ({ row }) => { + const quotation = row.original + const itemCount = quotation.itemCount || 0 + + const handleClick = () => { + const rfq = { + id: quotation.rfqId, + rfqCode: quotation.rfqCode, + status: quotation.rfqStatus, + rfqType: "SHIP" as const, // 기본값 + } + openItemsDialog(rfq) + } + + return ( + <div className="w-20"> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="sm" + className="relative h-8 w-8 p-0 group" + onClick={handleClick} + aria-label={`View ${itemCount} items`} + > + <Package className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> + {itemCount > 0 && ( + <span className="pointer-events-none absolute -top-1 -right-1 inline-flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-primary px-1 text-[0.625rem] font-medium leading-none text-primary-foreground"> + {itemCount} + </span> + )} + <span className="sr-only"> + {itemCount > 0 ? `${itemCount} 아이템` : "아이템 없음"} + </span> + </Button> + </TooltipTrigger> + <TooltipContent> + <p>{itemCount > 0 ? `${itemCount}개 아이템 보기` : "아이템 없음"}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + ) + }, + enableSorting: false, + enableHiding: true, + }, + { id: "attachments", header: ({ column }) => ( <DataTableColumnHeaderSimple column={column} title="첨부파일" /> diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index e98d6bdc..55dcad92 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -11,6 +11,7 @@ import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QU import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" +import { RfqItemsViewDialog } from "../../table/rfq-items-view-dialog" import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service" import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" @@ -23,14 +24,16 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations { itemName?: string | null; projNm?: string | null; quotationCode?: string | null; - quotationVersion?: number | null; + rejectionReason?: string | null; acceptedAt?: Date | null; attachmentCount?: number; + itemCount?: number; } interface VendorQuotationsTableProps { vendorId: string; + rfqType?: "SHIP" | "TOP" | "HULL"; } // 로딩 스켈레톤 컴포넌트 @@ -92,22 +95,9 @@ function TableLoadingSkeleton() { ) } -// 중앙 로딩 인디케이터 컴포넌트 -function CenterLoadingIndicator() { - return ( - <div className="flex flex-col items-center justify-center py-12 space-y-4"> - <div className="relative"> - <div className="w-12 h-12 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div> - </div> - <div className="text-center space-y-1"> - <p className="text-sm font-medium text-gray-900">데이터를 불러오는 중...</p> - <p className="text-xs text-gray-500">잠시만 기다려주세요.</p> - </div> - </div> - ) -} -export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) { + +export function VendorQuotationsTable({ vendorId, rfqType }: VendorQuotationsTableProps) { const searchParams = useSearchParams() const router = useRouter() @@ -116,6 +106,10 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null) const [attachmentsDefault, setAttachmentsDefault] = React.useState<ExistingTechSalesAttachment[]>([]) + // 아이템 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [selectedRfqForItems, setSelectedRfqForItems] = React.useState<{ id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; } | null>(null) + // 데이터 로딩 상태 const [data, setData] = React.useState<QuotationWithRfqCode[]>([]) const [pageCount, setPageCount] = React.useState(0) @@ -158,6 +152,7 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) search: initialSettings.search, from: initialSettings.from, to: initialSettings.to, + rfqType: rfqType, }, vendorId) console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', { @@ -176,7 +171,7 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) setIsLoading(false) setIsInitialLoad(false) } - }, [vendorId, initialSettings]) + }, [vendorId, initialSettings, rfqType]) // URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함) React.useEffect(() => { @@ -192,8 +187,9 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) searchParams?.get('search'), searchParams?.get('from'), searchParams?.get('to'), - // vendorId 변경도 감지 - vendorId + // vendorId와 rfqType 변경도 감지 + vendorId, + rfqType ]) // 데이터 안정성을 위한 메모이제이션 @@ -246,12 +242,19 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) toast.error("첨부파일 조회 중 오류가 발생했습니다.") } }, [data]) + + // 아이템 다이얼로그 열기 함수 + const openItemsDialog = React.useCallback((rfq: { id: number; rfqCode?: string; status?: string; rfqType?: "SHIP" | "TOP" | "HULL"; }) => { + setSelectedRfqForItems(rfq) + setItemsDialogOpen(true) + }, []) // 테이블 컬럼 정의 const columns = React.useMemo(() => getColumns({ router, openAttachmentsSheet, - }), [router, openAttachmentsSheet]) + openItemsDialog, + }), [router, openAttachmentsSheet, openItemsDialog]) // 필터 필드 const filterFields = React.useMemo<DataTableFilterField<QuotationWithRfqCode>[]>(() => [ @@ -270,8 +273,8 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) }, { id: "materialCode", - label: "자재 코드", - placeholder: "자재 코드 검색...", + label: "자재 그룹", + placeholder: "자재 그룹 검색...", } ], []) @@ -284,7 +287,7 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) }, { id: "materialCode", - label: "자재 코드", + label: "자재 그룹", type: "text", }, { @@ -383,6 +386,13 @@ export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) onAttachmentsUpdated={() => {}} // 읽기 전용이므로 빈 함수 readOnly={true} // 벤더 쪽에서는 항상 읽기 전용 /> + + {/* 아이템 보기 다이얼로그 */} + <RfqItemsViewDialog + open={itemsDialogOpen} + onOpenChange={setItemsDialogOpen} + rfq={selectedRfqForItems} + /> </div> ); }
\ No newline at end of file |
