summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
-rw-r--r--lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx157
-rw-r--r--lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx40
-rw-r--r--lib/techsales-rfq/vendor-response/quotation-editor.tsx2
-rw-r--r--lib/techsales-rfq/vendor-response/quotation-item-editor.tsx8
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx71
-rw-r--r--lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx56
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