summaryrefslogtreecommitdiff
path: root/lib/vendor-rfq-response/vendor-cbe-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-rfq-response/vendor-cbe-table')
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/cbe-table-columns.tsx365
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/cbe-table.tsx272
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/comments-sheet.tsx323
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx427
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/rfq-detail-dialog.tsx89
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table-column.tsx62
-rw-r--r--lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table.tsx86
7 files changed, 0 insertions, 1624 deletions
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/cbe-table-columns.tsx b/lib/vendor-rfq-response/vendor-cbe-table/cbe-table-columns.tsx
deleted file mode 100644
index c7be0bf4..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/cbe-table-columns.tsx
+++ /dev/null
@@ -1,365 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { Download, Loader2, MessageSquare, FileEdit } from "lucide-react"
-import { formatDate } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { useRouter } from "next/navigation"
-import { VendorWithCbeFields, vendorResponseCbeColumnsConfig } from "@/config/vendorCbeColumnsConfig"
-import { toast } from "sonner"
-
-
-type NextRouter = ReturnType<typeof useRouter>
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<
- React.SetStateAction<DataTableRowAction<VendorWithCbeFields> | null>
- >
- router: NextRouter
- openCommentSheet: (vendorId: number) => void
- handleDownloadCbeFiles: (vendorId: number, rfqId: number) => void
- loadingVendors: Record<string, boolean>
- openVendorContactsDialog: (rfqId: number, rfq: VendorWithCbeFields) => void
- // New prop for handling commercial response
- openCommercialResponseSheet: (responseId: number, rfq: VendorWithCbeFields) => void
-}
-
-/**
- * tanstack table 컬럼 정의 (중첩 헤더 버전)
- */
-export function getColumns({
- setRowAction,
- router,
- openCommentSheet,
- handleDownloadCbeFiles,
- loadingVendors,
- openVendorContactsDialog,
- openCommercialResponseSheet
-}: GetColumnsProps): ColumnDef<VendorWithCbeFields>[] {
- // ----------------------------------------------------------------
- // 1) Select 컬럼 (체크박스)
- // ----------------------------------------------------------------
- const selectColumn: ColumnDef<VendorWithCbeFields> = {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-0.5"
- />
- ),
- size: 40,
- enableSorting: false,
- enableHiding: false,
- }
-
- // ----------------------------------------------------------------
- // 2) 그룹화(Nested) 컬럼 구성
- // ----------------------------------------------------------------
- const groupMap: Record<string, ColumnDef<VendorWithCbeFields>[]> = {}
-
- vendorResponseCbeColumnsConfig.forEach((cfg) => {
- const groupName = cfg.group || "_noGroup"
- if (!groupMap[groupName]) {
- groupMap[groupName] = []
- }
-
- // childCol: ColumnDef<VendorWithCbeFields>
- const childCol: ColumnDef<VendorWithCbeFields> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- meta: {
- excelHeader: cfg.excelHeader,
- group: cfg.group,
- type: cfg.type,
- },
- maxSize: 120,
- // 셀 렌더링
- cell: ({ row, getValue }) => {
- // 1) 필드값 가져오기
- const val = getValue()
-
-
- if (cfg.id === "rfqCode") {
- const rfq = row.original;
- const rfqId = rfq.rfqId;
-
- // 협력업체 이름을 클릭할 수 있는 버튼으로 렌더링
- const handleVendorNameClick = () => {
- if (rfqId) {
- openVendorContactsDialog(rfqId, rfq); // vendor 전체 객체 전달
- } else {
- toast.error("협력업체 ID를 찾을 수 없습니다.");
- }
- };
-
- return (
- <Button
- variant="link"
- className="p-0 h-auto text-left font-normal justify-start hover:underline"
- onClick={handleVendorNameClick}
- >
- {val as string}
- </Button>
- );
- }
-
- // Commercial Response Status에 배지 적용
- if (cfg.id === "commercialResponseStatus") {
- const status = val as string;
-
- if (!status) return <span className="text-muted-foreground">-</span>;
-
- let variant: "default" | "outline" | "secondary" | "destructive" = "outline";
-
- switch (status) {
- case "SUBMITTED":
- variant = "default"; // Green
- break;
- case "IN_PROGRESS":
- variant = "secondary"; // Orange/Yellow
- break;
- case "PENDING":
- variant = "outline"; // Gray
- break;
- default:
- variant = "outline";
- }
-
- return (
- <Badge variant={variant} className="capitalize">
- {status.toLowerCase().replace("_", " ")}
- </Badge>
- );
- }
-
- // 예) TBE Updated (날짜)
- if (cfg.id === "respondedAt" || cfg.id === "rfqDueDate" ) {
- const dateVal = val as Date | undefined
- if (!dateVal) return null
- return formatDate(dateVal)
- }
-
- // 그 외 필드는 기본 값 표시
- return val ?? ""
- },
- }
-
- groupMap[groupName].push(childCol)
- })
-
- // groupMap → nestedColumns
- const nestedColumns: ColumnDef<VendorWithCbeFields>[] = []
- Object.entries(groupMap).forEach(([groupName, colDefs]) => {
- if (groupName === "_noGroup") {
- nestedColumns.push(...colDefs)
- } else {
- nestedColumns.push({
- id: groupName,
- header: groupName,
- columns: colDefs,
- })
- }
- })
-
- // ----------------------------------------------------------------
- // 3) Respond 컬럼 (새로 추가)
- // ----------------------------------------------------------------
- const respondColumn: ColumnDef<VendorWithCbeFields> = {
- id: "respond",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Response" />
- ),
- cell: ({ row }) => {
- const vendor = row.original
- const responseId = vendor.responseId
-
- if (!responseId) {
- return <div className="text-center text-muted-foreground">-</div>
- }
-
- const handleClick = () => {
- openCommercialResponseSheet(responseId, vendor)
- }
-
- // Status에 따라 버튼 variant 변경
- let variant: "default" | "outline" | "ghost" | "secondary" = "default"
- let buttonText = "Respond"
-
- if (vendor.commercialResponseStatus === "SUBMITTED") {
- variant = "outline"
- buttonText = "Update"
- } else if (vendor.commercialResponseStatus === "IN_PROGRESS") {
- variant = "secondary"
- buttonText = "Continue"
- }
-
- return (
- <Button
- variant={variant}
- size="sm"
- // className="w-20"
- onClick={handleClick}
- >
- <FileEdit className="h-3.5 w-3.5 mr-1" />
- {buttonText}
- </Button>
- )
- },
- enableSorting: false,
- maxSize: 200,
- minSize: 115,
- }
-
- // ----------------------------------------------------------------
- // 4) Comments 컬럼
- // ----------------------------------------------------------------
- const commentsColumn: ColumnDef<VendorWithCbeFields> = {
- id: "comments",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Comments" />
- ),
- cell: ({ row }) => {
- const vendor = row.original
- const commCount = vendor.comments?.length ?? 0
-
- function handleClick() {
- // rowAction + openCommentSheet
- setRowAction({ row, type: "comments" })
- openCommentSheet(vendor.responseId ?? 0)
- }
-
- return (
- <Button
- variant="ghost"
- size="sm"
- className="relative h-8 w-8 p-0 group"
- onClick={handleClick}
- aria-label={
- commCount > 0 ? `View ${commCount} comments` : "No comments"
- }
- >
- <MessageSquare className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
- {commCount > 0 && (
- <Badge
- variant="secondary"
- className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center"
- >
- {commCount}
- </Badge>
- )}
- <span className="sr-only">
- {commCount > 0 ? `${commCount} Comments` : "No Comments"}
- </span>
- </Button>
- )
- },
- enableSorting: false,
- maxSize: 80
- }
-
- // ----------------------------------------------------------------
- // 5) 파일 다운로드 컬럼 (개별 로딩 상태 적용)
- // ----------------------------------------------------------------
- const downloadColumn: ColumnDef<VendorWithCbeFields> = {
- id: "attachDownload",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Attach Download" />
- ),
- cell: ({ row }) => {
- const vendor = row.original
- const vendorId = vendor.vendorId
- const rfqId = vendor.rfqId
- const files = vendor.files?.length || 0
-
- if (!vendorId || !rfqId) {
- return <div className="text-center text-muted-foreground">-</div>
- }
-
- // 각 행별로 로딩 상태 확인 (vendorId_rfqId 형식의 키 사용)
- const rowKey = `${vendorId}_${rfqId}`
- const isRowLoading = loadingVendors[rowKey] === true
-
- // 템플릿 파일이 없으면 다운로드 버튼 비활성화
- const isDisabled = files <= 0 || isRowLoading
-
- return (
- <Button
- variant="ghost"
- size="sm"
- className="relative h-8 w-8 p-0 group"
- onClick={
- isDisabled
- ? undefined
- : () => handleDownloadCbeFiles(vendorId, rfqId)
- }
- aria-label={
- isRowLoading
- ? "다운로드 중..."
- : files > 0
- ? `CBE 첨부 다운로드 (${files}개)`
- : "다운로드할 파일 없음"
- }
- disabled={isDisabled}
- >
- {isRowLoading ? (
- <Loader2 className="h-4 w-4 animate-spin" />
- ) : (
- <Download className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
- )}
-
- {/* 파일이 1개 이상인 경우 뱃지로 개수 표시 (로딩 중이 아닐 때만) */}
- {!isRowLoading && files > 0 && (
- <Badge
- variant="secondary"
- className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center"
- >
- {files}
- </Badge>
- )}
-
- <span className="sr-only">
- {isRowLoading
- ? "다운로드 중..."
- : files > 0
- ? `CBE 첨부 다운로드 (${files}개)`
- : "다운로드할 파일 없음"}
- </span>
- </Button>
- )
- },
- enableSorting: false,
- maxSize: 80,
- }
-
- // ----------------------------------------------------------------
- // 6) 최종 컬럼 배열 (respondColumn 추가)
- // ----------------------------------------------------------------
- return [
- selectColumn,
- ...nestedColumns,
- respondColumn, // 응답 컬럼 추가
- downloadColumn,
- commentsColumn,
- ]
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/cbe-table.tsx b/lib/vendor-rfq-response/vendor-cbe-table/cbe-table.tsx
deleted file mode 100644
index 8477f550..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/cbe-table.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { getColumns } from "./cbe-table-columns"
-import {
- fetchRfqAttachmentsbyCommentId,
- getCBEbyVendorId,
- getFileFromRfqAttachmentsbyid,
- fetchCbeFiles
-} from "../../rfqs/service"
-import { useSession } from "next-auth/react"
-import { CbeComment, CommentSheet } from "./comments-sheet"
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { toast } from "sonner"
-import { RfqDeailDialog } from "./rfq-detail-dialog"
-import { CommercialResponseSheet } from "./respond-cbe-sheet"
-
-interface VendorsTableProps {
- promises: Promise<
- [
- Awaited<ReturnType<typeof getCBEbyVendorId>>,
- ]
- >
-}
-
-export function CbeVendorTable({ promises }: VendorsTableProps) {
- const { data: session } = useSession()
- const userVendorId = session?.user?.companyId
- const userId = Number(session?.user?.id)
- // Suspense로 받아온 데이터
- const [{ data, pageCount }] = React.use(promises)
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorWithCbeFields> | null>(null)
- const [selectedCbeId, setSelectedCbeId] = React.useState<number | null>(null)
-
- // 개별 협력업체별 로딩 상태를 관리하는 맵
- const [loadingVendors, setLoadingVendors] = React.useState<Record<string, boolean>>({})
-
- const router = useRouter()
-
- // 코멘트 관련 상태
- const [initialComments, setInitialComments] = React.useState<CbeComment[]>([])
- const [commentSheetOpen, setCommentSheetOpen] = React.useState(false)
- const [selectedRfqIdForComments, setSelectedRfqIdForComments] = React.useState<number | null>(null)
-
- // 상업 응답 관련 상태
- const [commercialResponseSheetOpen, setCommercialResponseSheetOpen] = React.useState(false)
- const [selectedResponseId, setSelectedResponseId] = React.useState<number | null>(null)
- const [selectedRfq, setSelectedRfq] = React.useState<VendorWithCbeFields | null>(null)
-
- // RFQ 상세 관련 상태
- const [rfqDetailDialogOpen, setRfqDetailDialogOpen] = React.useState(false)
- const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null)
- const [selectedRfqDetail, setSelectedRfqDetail] = React.useState<VendorWithCbeFields | null>(null)
-
- React.useEffect(() => {
- if (rowAction?.type === "comments") {
- // rowAction가 새로 세팅된 뒤 여기서 openCommentSheet 실행
- openCommentSheet(Number(rowAction.row.original.responseId))
- }
- }, [rowAction])
-
- async function openCommentSheet(responseId: number) {
- setInitialComments([])
-
- const comments = rowAction?.row.original.comments
- const rfqId = rowAction?.row.original.rfqId
-
- if (comments && comments.length > 0) {
- const commentWithAttachments: CbeComment[] = await Promise.all(
- comments.map(async (c) => {
- // 서버 액션을 사용하여 코멘트 첨부 파일 가져오기
- const attachments = await fetchRfqAttachmentsbyCommentId(c.id)
-
- return {
- ...c,
- commentedBy: userId, // DB나 API 응답에 있다고 가정
- attachments,
- }
- })
- )
-
- setInitialComments(commentWithAttachments)
- }
-
- if(rfqId) {
- setSelectedRfqIdForComments(rfqId)
- }
- setSelectedCbeId(responseId)
- setCommentSheetOpen(true)
- }
-
- // 상업 응답 시트 열기
- function openCommercialResponseSheet(responseId: number, rfq: VendorWithCbeFields) {
- setSelectedResponseId(responseId)
- setSelectedRfq(rfq)
- setCommercialResponseSheetOpen(true)
- }
-
- // RFQ 상세 대화상자 열기
- function openRfqDetailDialog(rfqId: number, rfq: VendorWithCbeFields) {
- setSelectedRfqId(rfqId)
- setSelectedRfqDetail(rfq)
- setRfqDetailDialogOpen(true)
- }
-
- const handleDownloadCbeFiles = React.useCallback(
- async (vendorId: number, rfqId: number) => {
- // 고유 키 생성: vendorId_rfqId
- const rowKey = `${vendorId}_${rfqId}`
-
- // 해당 협력업체의 로딩 상태만 true로 설정
- setLoadingVendors(prev => ({
- ...prev,
- [rowKey]: true
- }))
-
- try {
- const { files, error } = await fetchCbeFiles(vendorId, rfqId);
- if (error) {
- toast.error(error);
- return;
- }
- if (files.length === 0) {
- toast.warning("다운로드할 CBE 파일이 없습니다");
- return;
- }
- // 순차적으로 파일 다운로드
- for (const file of files) {
- await downloadFile(file.id);
- }
- toast.success(`${files.length}개의 CBE 파일이 다운로드되었습니다`);
- } catch (error) {
- toast.error("CBE 파일을 다운로드하는 데 실패했습니다");
- console.error(error);
- } finally {
- // 해당 협력업체의 로딩 상태만 false로 되돌림
- setLoadingVendors(prev => ({
- ...prev,
- [rowKey]: false
- }))
- }
- },
- []
- );
-
- const downloadFile = React.useCallback(async (fileId: number) => {
- try {
- const { file, error } = await getFileFromRfqAttachmentsbyid(fileId);
- if (error || !file) {
- throw new Error(error || "파일 정보를 가져오는 데 실패했습니다");
- }
-
- const link = document.createElement("a");
- link.href = `/api/rfq-download?path=${encodeURIComponent(file.filePath)}`;
- link.download = file.fileName;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- return true;
- } catch (error) {
- console.error(error);
- return false;
- }
- }, []);
-
- // 응답 성공 후 데이터 갱신
- const handleResponseSuccess = React.useCallback(() => {
- // 필요한 경우 데이터 다시 가져오기
- router.refresh()
- }, [router]);
-
- // getColumns() 호출 시 필요한 핸들러들 주입
- const columns = React.useMemo(
- () => getColumns({
- setRowAction,
- router,
- openCommentSheet,
- handleDownloadCbeFiles,
- loadingVendors,
- openVendorContactsDialog: openRfqDetailDialog,
- openCommercialResponseSheet,
- }),
- [
- setRowAction,
- router,
- openCommentSheet,
- handleDownloadCbeFiles,
- loadingVendors,
- openRfqDetailDialog,
- openCommercialResponseSheet
- ]
- );
-
- // 필터 필드 정의
- const filterFields: DataTableFilterField<VendorWithCbeFields>[] = []
- const advancedFilterFields: DataTableAdvancedFilterField<VendorWithCbeFields>[] = [
-
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "respondedAt", desc: true }],
- columnPinning: { right: ["respond", "comments"] }, // respond 컬럼을 오른쪽에 고정
- },
- getRowId: (originalRow) => String(originalRow.responseId),
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <>
- <DataTable table={table}>
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- />
- </DataTable>
-
- {/* 코멘트 시트 */}
- {commentSheetOpen && selectedRfqIdForComments && selectedCbeId !== null && (
- <CommentSheet
- open={commentSheetOpen}
- onOpenChange={setCommentSheetOpen}
- rfqId={selectedRfqIdForComments}
- initialComments={initialComments}
- vendorId={userVendorId || 0}
- currentUserId={userId || 0}
- cbeId={selectedCbeId}
- />
- )}
-
- {/* 상업 응답 시트 */}
- {commercialResponseSheetOpen && selectedResponseId !== null && selectedRfq && (
- <CommercialResponseSheet
- open={commercialResponseSheetOpen}
- onOpenChange={setCommercialResponseSheetOpen}
- responseId={selectedResponseId}
- rfq={selectedRfq}
- onSuccess={handleResponseSuccess}
- />
- )}
-
- {/* RFQ 상세 대화상자 */}
- {rfqDetailDialogOpen && selectedRfqId !== null && (
- <RfqDeailDialog
- isOpen={rfqDetailDialogOpen}
- onOpenChange={setRfqDetailDialogOpen}
- rfqId={selectedRfqId}
- rfq={selectedRfqDetail}
- />
- )}
- </>
- )
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/comments-sheet.tsx b/lib/vendor-rfq-response/vendor-cbe-table/comments-sheet.tsx
deleted file mode 100644
index 40d38145..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/comments-sheet.tsx
+++ /dev/null
@@ -1,323 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm, useFieldArray } from "react-hook-form"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Download, X, Loader2 } from "lucide-react"
-import prettyBytes from "pretty-bytes"
-import { toast } from "sonner"
-
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import { Button } from "@/components/ui/button"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Textarea } from "@/components/ui/textarea"
-import {
- Dropzone,
- DropzoneZone,
- DropzoneUploadIcon,
- DropzoneTitle,
- DropzoneDescription,
- DropzoneInput,
-} from "@/components/ui/dropzone"
-import {
- Table,
- TableHeader,
- TableRow,
- TableHead,
- TableBody,
- TableCell,
-} from "@/components/ui/table"
-
-import { formatDate } from "@/lib/utils"
-import { createRfqCommentWithAttachments } from "@/lib/rfqs/service"
-
-
-export interface CbeComment {
- id: number
- commentText: string
- commentedBy?: number
- commentedByEmail?: string
- createdAt?: Date
- attachments?: {
- id: number
- fileName: string
- filePath: string
- }[]
-}
-
-// 1) props 정의
-interface CommentSheetProps extends React.ComponentPropsWithRef<typeof Sheet> {
- initialComments?: CbeComment[]
- currentUserId: number
- rfqId: number
- tbeId?: number
- cbeId?: number
- vendorId: number
- onCommentsUpdated?: (comments: CbeComment[]) => void
- isLoading?: boolean // New prop
-}
-
-// 2) 폼 스키마
-const commentFormSchema = z.object({
- commentText: z.string().min(1, "댓글을 입력하세요."),
- newFiles: z.array(z.any()).optional(), // File[]
-})
-type CommentFormValues = z.infer<typeof commentFormSchema>
-
-const MAX_FILE_SIZE = 30e6 // 30MB
-
-export function CommentSheet({
- rfqId,
- vendorId,
- initialComments = [],
- currentUserId,
- tbeId,
- cbeId,
- onCommentsUpdated,
- isLoading = false, // Default to false
- ...props
-}: CommentSheetProps) {
-
-
- const [comments, setComments] = React.useState<CbeComment[]>(initialComments)
- const [isPending, startTransition] = React.useTransition()
-
- React.useEffect(() => {
- setComments(initialComments)
- }, [initialComments])
-
- const form = useForm<CommentFormValues>({
- resolver: zodResolver(commentFormSchema),
- defaultValues: {
- commentText: "",
- newFiles: [],
- },
- })
-
- const { fields: newFileFields, append, remove } = useFieldArray({
- control: form.control,
- name: "newFiles",
- })
-
- // (A) 기존 코멘트 렌더링
- function renderExistingComments() {
-
- if (isLoading) {
- return (
- <div className="flex justify-center items-center h-32">
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
- <span className="ml-2 text-sm text-muted-foreground">Loading comments...</span>
- </div>
- )
- }
-
- if (comments.length === 0) {
- return <p className="text-sm text-muted-foreground">No comments yet</p>
- }
- return (
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-1/2">Comment</TableHead>
- <TableHead>Attachments</TableHead>
- <TableHead>Created At</TableHead>
- <TableHead>Created By</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {comments.map((c) => (
- <TableRow key={c.id}>
- <TableCell>{c.commentText}</TableCell>
- <TableCell>
- {!c.attachments?.length && (
- <span className="text-sm text-muted-foreground">No files</span>
- )}
- {c.attachments?.length && (
- <div className="flex flex-col gap-1">
- {c.attachments.map((att) => (
- <div key={att.id} className="flex items-center gap-2">
- <a
- href={`/api/rfq-download?path=${encodeURIComponent(att.filePath)}`}
- download
- target="_blank"
- rel="noreferrer"
- className="inline-flex items-center gap-1 text-blue-600 underline"
- >
- <Download className="h-4 w-4" />
- {att.fileName}
- </a>
- </div>
- ))}
- </div>
- )}
- </TableCell>
- <TableCell> {c.createdAt ? formatDate(c.createdAt) : "-"}</TableCell>
- <TableCell>{c.commentedByEmail ?? "-"}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- )
- }
-
- // (B) 파일 드롭
- function handleDropAccepted(files: File[]) {
- append(files)
- }
-
- // (C) Submit
- async function onSubmit(data: CommentFormValues) {
- if (!rfqId) return
- startTransition(async () => {
- try {
- const res = await createRfqCommentWithAttachments({
- rfqId,
- vendorId,
- commentText: data.commentText,
- commentedBy: currentUserId,
- evaluationId: null,
- cbeId: cbeId,
- files: data.newFiles,
- })
-
- if (!res.ok) {
- throw new Error("Failed to create comment")
- }
-
- toast.success("Comment created")
-
- // 임시로 새 코멘트 추가
- const newComment: CbeComment = {
- id: res.commentId, // 서버 응답
- commentText: data.commentText,
- commentedBy: currentUserId,
- createdAt: res.createdAt,
- attachments:
- data.newFiles?.map((f) => ({
- id: Math.floor(Math.random() * 1e6),
- fileName: f.name,
- filePath: "/uploads/" + f.name,
- })) || [],
- }
- setComments((prev) => [...prev, newComment])
- onCommentsUpdated?.([...comments, newComment])
-
- form.reset()
- } catch (err: any) {
- console.error(err)
- toast.error("Error: " + err.message)
- }
- })
- }
-
- return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-lg">
- <SheetHeader className="text-left">
- <SheetTitle>Comments</SheetTitle>
- <SheetDescription>
- 필요시 첨부파일과 함께 문의/코멘트를 남길 수 있습니다.
- </SheetDescription>
- </SheetHeader>
-
- <div className="max-h-[300px] overflow-y-auto">{renderExistingComments()}</div>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
- <FormField
- control={form.control}
- name="commentText"
- render={({ field }) => (
- <FormItem>
- <FormLabel>New Comment</FormLabel>
- <FormControl>
- <Textarea placeholder="Enter your comment..." {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <Dropzone
- maxSize={MAX_FILE_SIZE}
- onDropAccepted={handleDropAccepted}
- onDropRejected={(rej) => {
- toast.error("File rejected: " + (rej[0]?.file?.name || ""))
- }}
- >
- {({ maxSize }) => (
- <DropzoneZone className="flex justify-center">
- <DropzoneInput />
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>Drop to attach files</DropzoneTitle>
- <DropzoneDescription>
- Max size: {prettyBytes(maxSize || 0)}
- </DropzoneDescription>
- </div>
- </div>
- </DropzoneZone>
- )}
- </Dropzone>
-
- {newFileFields.length > 0 && (
- <div className="flex flex-col gap-2">
- {newFileFields.map((field, idx) => {
- const file = form.getValues(`newFiles.${idx}`)
- if (!file) return null
- return (
- <div
- key={field.id}
- className="flex items-center justify-between border rounded p-2"
- >
- <span className="text-sm">
- {file.name} ({prettyBytes(file.size)})
- </span>
- <Button
- variant="ghost"
- size="icon"
- type="button"
- onClick={() => remove(idx)}
- >
- <X className="h-4 w-4" />
- </Button>
- </div>
- )
- })}
- </div>
- )}
-
- <SheetFooter className="gap-2 pt-4">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- Cancel
- </Button>
- </SheetClose>
- <Button disabled={isPending}>
- {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- Save
- </Button>
- </SheetFooter>
- </form>
- </Form>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx b/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx
deleted file mode 100644
index 8cc4fa6f..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx
+++ /dev/null
@@ -1,427 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Loader } from "lucide-react"
-import { useForm } from "react-hook-form"
-import { toast } from "sonner"
-import { z } from "zod"
-
-import { Button } from "@/components/ui/button"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Input } from "@/components/ui/input"
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import { Textarea } from "@/components/ui/textarea"
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { getCommercialResponseByResponseId, updateCommercialResponse } from "../service"
-
-// Define schema for form validation (client-side)
-const commercialResponseFormSchema = z.object({
- responseStatus: z.enum(["PENDING", "IN_PROGRESS", "SUBMITTED", "REJECTED", "ACCEPTED"]),
- totalPrice: z.coerce.number().optional(),
- currency: z.string().default("USD"),
- paymentTerms: z.string().optional(),
- incoterms: z.string().optional(),
- deliveryPeriod: z.string().optional(),
- warrantyPeriod: z.string().optional(),
- validityPeriod: z.string().optional(),
- priceBreakdown: z.string().optional(),
- commercialNotes: z.string().optional(),
-})
-
-type CommercialResponseFormInput = z.infer<typeof commercialResponseFormSchema>
-
-interface CommercialResponseSheetProps
- extends React.ComponentPropsWithRef<typeof Sheet> {
- rfq: VendorWithCbeFields | null
- responseId: number | null // This is the vendor_responses.id
- onSuccess?: () => void
-}
-
-export function CommercialResponseSheet({
- rfq,
- responseId,
- onSuccess,
- ...props
-}: CommercialResponseSheetProps) {
- const [isSubmitting, startSubmitTransition] = React.useTransition()
- const [isLoading, setIsLoading] = React.useState(true)
-
- const form = useForm<CommercialResponseFormInput>({
- resolver: zodResolver(commercialResponseFormSchema),
- defaultValues: {
- responseStatus: "PENDING",
- totalPrice: undefined,
- currency: "USD",
- paymentTerms: "",
- incoterms: "",
- deliveryPeriod: "",
- warrantyPeriod: "",
- validityPeriod: "",
- priceBreakdown: "",
- commercialNotes: "",
- },
- })
-
- // Load existing commercial response data when sheet opens
- React.useEffect(() => {
- async function loadCommercialResponse() {
- if (!responseId) return
-
- setIsLoading(true)
- try {
- // Use the helper function to get existing data
- const existingResponse = await getCommercialResponseByResponseId(responseId)
-
- if (existingResponse) {
- // If we found existing data, populate the form
- form.reset({
- responseStatus: existingResponse.responseStatus,
- totalPrice: existingResponse.totalPrice,
- currency: existingResponse.currency || "USD",
- paymentTerms: existingResponse.paymentTerms || "",
- incoterms: existingResponse.incoterms || "",
- deliveryPeriod: existingResponse.deliveryPeriod || "",
- warrantyPeriod: existingResponse.warrantyPeriod || "",
- validityPeriod: existingResponse.validityPeriod || "",
- priceBreakdown: existingResponse.priceBreakdown || "",
- commercialNotes: existingResponse.commercialNotes || "",
- })
- } else if (rfq) {
- // If no existing data but we have rfq data with some values already
- form.reset({
- responseStatus: rfq.commercialResponseStatus as any || "PENDING",
- totalPrice: rfq.totalPrice || undefined,
- currency: rfq.currency || "USD",
- paymentTerms: rfq.paymentTerms || "",
- incoterms: rfq.incoterms || "",
- deliveryPeriod: rfq.deliveryPeriod || "",
- warrantyPeriod: rfq.warrantyPeriod || "",
- validityPeriod: rfq.validityPeriod || "",
- priceBreakdown: "",
- commercialNotes: "",
- })
- }
- } catch (error) {
- console.error("Failed to load commercial response data:", error)
- toast.error("상업 응답 데이터를 불러오는데 실패했습니다")
- } finally {
- setIsLoading(false)
- }
- }
-
- loadCommercialResponse()
- }, [responseId, rfq, form])
-
- function onSubmit(formData: CommercialResponseFormInput) {
- if (!responseId) {
- toast.error("응답 ID를 찾을 수 없습니다")
- return
- }
-
- if (!rfq?.vendorId) {
- toast.error("협력업체 ID를 찾을 수 없습니다")
- return
- }
-
- startSubmitTransition(async () => {
- try {
- // Pass both responseId and vendorId to the server action
- const result = await updateCommercialResponse({
- responseId,
- vendorId: rfq.vendorId, // Include vendorId for revalidateTag
- ...formData,
- })
-
- if (!result.success) {
- toast.error(result.error || "응답 제출 중 오류가 발생했습니다")
- return
- }
-
- toast.success("Commercial response successfully submitted")
- props.onOpenChange?.(false)
-
- if (onSuccess) {
- onSuccess()
- }
- } catch (error) {
- console.error("Error submitting response:", error)
- toast.error("응답 제출 중 오류가 발생했습니다")
- }
- })
- }
-
- return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-md">
- <SheetHeader className="text-left">
- <SheetTitle>Commercial Response</SheetTitle>
- <SheetDescription>
- {rfq?.rfqCode && <span className="font-medium">{rfq.rfqCode}</span>}
- <div className="mt-1">Please provide your commercial response for this RFQ</div>
- </SheetDescription>
- </SheetHeader>
-
- {isLoading ? (
- <div className="flex items-center justify-center py-8">
- <Loader className="h-8 w-8 animate-spin text-muted-foreground" />
- </div>
- ) : (
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-4 overflow-y-auto max-h-[calc(100vh-200px)] pr-2"
- >
- <FormField
- control={form.control}
- name="responseStatus"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Response Status</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- >
- <FormControl>
- <SelectTrigger className="capitalize">
- <SelectValue placeholder="Select response status" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="PENDING">Pending</SelectItem>
- <SelectItem value="IN_PROGRESS">In Progress</SelectItem>
- <SelectItem value="SUBMITTED">Submitted</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <div className="grid grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name="totalPrice"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Total Price</FormLabel>
- <FormControl>
- <Input
- type="number"
- placeholder="0.00"
- {...field}
- value={field.value || ''}
- onChange={(e) => {
- const value = e.target.value === '' ? undefined : parseFloat(e.target.value);
- field.onChange(value);
- }}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="currency"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Currency</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="Select currency" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="USD">USD</SelectItem>
- <SelectItem value="EUR">EUR</SelectItem>
- <SelectItem value="GBP">GBP</SelectItem>
- <SelectItem value="KRW">KRW</SelectItem>
- <SelectItem value="JPY">JPY</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- {/* Other form fields remain the same */}
- <FormField
- control={form.control}
- name="paymentTerms"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Payment Terms</FormLabel>
- <FormControl>
- <Input placeholder="e.g. Net 30" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="incoterms"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Incoterms</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value || ''}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="Select incoterms" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="EXW">EXW (Ex Works)</SelectItem>
- <SelectItem value="FCA">FCA (Free Carrier)</SelectItem>
- <SelectItem value="FOB">FOB (Free On Board)</SelectItem>
- <SelectItem value="CIF">CIF (Cost, Insurance & Freight)</SelectItem>
- <SelectItem value="DAP">DAP (Delivered At Place)</SelectItem>
- <SelectItem value="DDP">DDP (Delivered Duty Paid)</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="deliveryPeriod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Delivery Period</FormLabel>
- <FormControl>
- <Input placeholder="e.g. 4-6 weeks" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="warrantyPeriod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Warranty Period</FormLabel>
- <FormControl>
- <Input placeholder="e.g. 12 months" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="validityPeriod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Validity Period</FormLabel>
- <FormControl>
- <Input placeholder="e.g. 30 days" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="priceBreakdown"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Price Breakdown (Optional)</FormLabel>
- <FormControl>
- <Textarea
- placeholder="Enter price breakdown details here"
- className="min-h-[100px]"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="commercialNotes"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Additional Notes (Optional)</FormLabel>
- <FormControl>
- <Textarea
- placeholder="Any additional comments or notes"
- className="min-h-[100px]"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <SheetFooter className="gap-2 pt-4 sm:space-x-0">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- Cancel
- </Button>
- </SheetClose>
- <Button disabled={isSubmitting} type="submit">
- {isSubmitting && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- Submit Response
- </Button>
- </SheetFooter>
- </form>
- </Form>
- )}
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/rfq-detail-dialog.tsx b/lib/vendor-rfq-response/vendor-cbe-table/rfq-detail-dialog.tsx
deleted file mode 100644
index e9328641..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/rfq-detail-dialog.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-"use client"
-
-import * as React from "react"
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Badge } from "@/components/ui/badge"
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { RfqItemsTable } from "./rfq-items-table/rfq-items-table"
-import { formatDateTime } from "@/lib/utils"
-import { CalendarClock } from "lucide-react"
-
-interface RfqDeailDialogProps {
- isOpen: boolean
- onOpenChange: (open: boolean) => void
- rfqId: number | null
- rfq: VendorWithCbeFields | null
-}
-
-export function RfqDeailDialog({
- isOpen,
- onOpenChange,
- rfqId,
- rfq,
-}: RfqDeailDialogProps) {
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-[90wv] sm:max-h-[80vh] overflow-auto" style={{ maxWidth: 1000, height: 480 }}>
- <DialogHeader>
- <div className="flex flex-col space-y-2">
- <DialogTitle>프로젝트: {rfq && rfq.projectName}({rfq && rfq.projectCode}) / RFQ: {rfq && rfq.rfqCode} Detail</DialogTitle>
- {rfq && (
- <div className="flex flex-col space-y-3 mt-2">
- <div className="text-sm text-muted-foreground">
- <span className="font-medium text-foreground">{rfq.rfqDescription && rfq.rfqDescription}</span>
- </div>
-
- {/* 정보를 두 행으로 나누어 표시 */}
- <div className="flex flex-col space-y-2 sm:space-y-0 sm:flex-row sm:justify-between sm:items-center">
- {/* 첫 번째 행: 상태 배지 */}
- <div className="flex items-center flex-wrap gap-2">
- {rfq.rfqType && (
- <Badge
- variant={
- rfq.rfqType === "BUDGETARY" ? "default" :
- rfq.rfqType === "PURCHASE" ? "destructive" :
- rfq.rfqType === "PURCHASE_BUDGETARY" ? "secondary" : "outline"
- }
- >
- RFQ 유형: {rfq.rfqType}
- </Badge>
- )}
-
-
- {rfq.vendorStatus && (
- <Badge variant="outline">
- RFQ 상태: {rfq.rfqStatus}
- </Badge>
- )}
-
- </div>
-
- {/* 두 번째 행: Due Date를 강조 표시 */}
- {rfq.rfqDueDate && (
- <div className="flex items-center">
- <Badge variant="secondary" className="flex gap-1 text-xs py-1 px-3">
- <CalendarClock className="h-3.5 w-3.5" />
- <span className="font-semibold">Due Date:</span>
- <span>{formatDateTime(rfq.rfqDueDate)}</span>
- </Badge>
- </div>
- )}
- </div>
- </div>
- )}
- </div>
- </DialogHeader>
- {rfqId && (
- <div className="py-4">
- <RfqItemsTable rfqId={rfqId} />
- </div>
- )}
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table-column.tsx b/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table-column.tsx
deleted file mode 100644
index bf4ae709..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table-column.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-"use client"
-// Because columns rely on React state/hooks for row actions
-
-import * as React from "react"
-import { ColumnDef, Row } from "@tanstack/react-table"
-import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"
-import { formatDate } from "@/lib/utils"
-import { Checkbox } from "@/components/ui/checkbox"
-import { ItemData } from "./rfq-items-table"
-
-
-/** getColumns: return array of ColumnDef for 'vendors' data */
-export function getColumns(): ColumnDef<ItemData>[] {
- return [
-
- // Vendor Name
- {
- accessorKey: "itemCode",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Item Code" />
- ),
- cell: ({ row }) => row.getValue("itemCode"),
- },
-
- // Vendor Code
- {
- accessorKey: "description",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Description" />
- ),
- cell: ({ row }) => row.getValue("description"),
- },
-
- // Status
- {
- accessorKey: "quantity",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Quantity" />
- ),
- cell: ({ row }) => row.getValue("quantity"),
- },
-
-
- // Created At
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Created At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date),
- },
-
- // Updated At
- {
- accessorKey: "updatedAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Updated At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date),
- },
- ]
-} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table.tsx b/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table.tsx
deleted file mode 100644
index c5c67e54..00000000
--- a/lib/vendor-rfq-response/vendor-cbe-table/rfq-items-table/rfq-items-table.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-'use client'
-
-import * as React from "react"
-import { ClientDataTable } from "@/components/client-data-table/data-table"
-import { getColumns } from "./rfq-items-table-column"
-import { DataTableAdvancedFilterField } from "@/types/table"
-import { Loader2 } from "lucide-react"
-import { useToast } from "@/hooks/use-toast"
-import { getItemsByRfqId } from "../../service"
-
-export interface ItemData {
- id: number
- itemCode: string
- description: string | null
- quantity: number
- uom: string | null
- createdAt: Date
- updatedAt: Date
-}
-
-interface RFQItemsTableProps {
- rfqId: number
-}
-
-export function RfqItemsTable({ rfqId }: RFQItemsTableProps) {
- const { toast } = useToast()
-
- const columns = React.useMemo(
- () => getColumns(),
- []
- )
-
- const [rfqItems, setRfqItems] = React.useState<ItemData[]>([])
- const [isLoading, setIsLoading] = React.useState(false)
-
- React.useEffect(() => {
- async function loadItems() {
- setIsLoading(true)
- try {
- // Use the correct function name (camelCase)
- const result = await getItemsByRfqId(rfqId)
- if (result.success && result.data) {
- setRfqItems(result.data as ItemData[])
- } else {
- throw new Error(result.error || "Unknown error occurred")
- }
- } catch (error) {
- console.error("RFQ 아이템 로드 오류:", error)
- toast({
- title: "Error",
- description: "Failed to load RFQ items",
- variant: "destructive",
- })
- } finally {
- setIsLoading(false)
- }
- }
- loadItems()
- }, [toast, rfqId])
-
- const advancedFilterFields: DataTableAdvancedFilterField<ItemData>[] = [
- { id: "itemCode", label: "Item Code", type: "text" },
- { id: "description", label: "Description", type: "text" },
- { id: "quantity", label: "Quantity", type: "number" },
- { id: "uom", label: "UoM", type: "text" },
- ]
-
- // If loading, show a flex container that fills the parent and centers the spinner
- if (isLoading) {
- return (
- <div className="flex h-full w-full items-center justify-center">
- <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
- </div>
- )
- }
-
- // Otherwise, show the table
- return (
- <ClientDataTable
- data={rfqItems}
- columns={columns}
- advancedFilterFields={advancedFilterFields}
- >
- </ClientDataTable>
- )
-} \ No newline at end of file