diff options
Diffstat (limited to 'lib/vendor-rfq-response/vendor-tbe-table')
5 files changed, 0 insertions, 1325 deletions
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx b/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx deleted file mode 100644 index e0bf9727..00000000 --- a/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx +++ /dev/null @@ -1,346 +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 { Loader, 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" - -// DB 스키마에서 필요한 타입들을 가져온다고 가정 -// (실제 프로젝트에 맞춰 import를 수정하세요.) -import { RfqWithAll } from "@/db/schema/rfq" -import { createRfqCommentWithAttachments } from "../../rfqs/service" -import { formatDate } from "@/lib/utils" - -// 코멘트 + 첨부파일 구조 (단순 예시) -// 실제 DB 스키마에 맞춰 조정 -export interface TbeComment { - id: number - commentText: string - commentedBy?: number - createdAt?: string | Date - attachments?: { - id: number - fileName: string - filePath: string - }[] -} - -interface CommentSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { - /** 코멘트를 작성할 RFQ 정보 */ - /** 이미 존재하는 모든 코멘트 목록 (서버에서 불러와 주입) */ - initialComments?: TbeComment[] - - /** 사용자(작성자) ID (로그인 세션 등에서 가져옴) */ - currentUserId: number - rfqId:number - vendorId:number - /** 댓글 저장 후 갱신용 콜백 (옵션) */ - onCommentsUpdated?: (comments: TbeComment[]) => void - isLoading?: boolean // New prop - -} - -// 새 코멘트 작성 폼 스키마 -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, - onCommentsUpdated, - isLoading = false, // Default to false - ...props -}: CommentSheetProps) { - const [comments, setComments] = React.useState<TbeComment[]>(initialComments) - const [isPending, startTransition] = React.useTransition() - - React.useEffect(() => { - setComments(initialComments) - }, [initialComments]) - - - // RHF 세팅 - const form = useForm<CommentFormValues>({ - resolver: zodResolver(commentFormSchema), - defaultValues: { - commentText: "", - newFiles: [] - } - }) - - // formFieldArray 예시 (파일 목록) - const { fields: newFileFields, append, remove } = useFieldArray({ - control: form.control, - name: "newFiles" - }) - - // 1) 기존 코멘트 + 첨부 보여주기 - // 간단히 테이블 하나로 표현 - // 실제로는 Bubble 형태의 UI, Accordion, Timeline 등 다양하게 구성할 수 있음 - 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 || c.attachments.length === 0) && ( - <span className="text-sm text-muted-foreground">No files</span> - )} - {c.attachments && c.attachments.length > 0 && ( - <div className="flex flex-col gap-1"> - {c.attachments.map((att) => ( - <div key={att.id} className="flex items-center gap-2"> - <a - href={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.commentedBy ?? "-"} - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - ) - } - - // 2) 새 파일 Drop - function handleDropAccepted(files: File[]) { - // 드롭된 File[]을 RHF field array에 추가 - const toAppend = files.map((f) => f) - append(toAppend) - } - - - // 3) 저장(Submit) - async function onSubmit(data: CommentFormValues) { - - if (!rfqId) return - startTransition(async () => { - try { - // 서버 액션 호출 - const res = await createRfqCommentWithAttachments({ - rfqId: rfqId, - vendorId: vendorId, // 필요시 세팅 - commentText: data.commentText, - commentedBy: currentUserId, - evaluationId: null, // 필요시 세팅 - files: data.newFiles - }) - - if (!res.ok) { - throw new Error("Failed to create comment") - } - - toast.success("Comment created") - - // 새 코멘트를 다시 불러오거나, - // 여기서는 임시로 "새로운 코멘트가 추가됐다" 라고 가정하여 클라이언트에서 상태 업데이트 - const newComment: TbeComment = { - id: res.commentId, // 서버에서 반환된 commentId - commentText: data.commentText, - commentedBy: currentUserId, - createdAt: new Date().toISOString(), - attachments: (data.newFiles?.map((f, idx) => ({ - id: Math.random() * 100000, - 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}> - <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 (파일 첨부) */} - <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 && <Loader 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-tbe-table/rfq-detail-dialog.tsx b/lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx deleted file mode 100644 index 2056a48f..00000000 --- a/lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx +++ /dev/null @@ -1,86 +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 { formatDateTime } from "@/lib/utils" -import { CalendarClock } from "lucide-react" -import { RfqItemsTable } from "../vendor-cbe-table/rfq-items-table/rfq-items-table" -import { TbeVendorFields } from "@/config/vendorTbeColumnsConfig" - -interface RfqDeailDialogProps { - isOpen: boolean - onOpenChange: (open: boolean) => void - rfqId: number | null - rfq: TbeVendorFields | 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.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.vendorStatus && ( - <Badge variant="outline"> - {rfq.rfqStatus} - </Badge> - )} - {rfq.rfqType && ( - <Badge - variant={ - rfq.rfqType === "BUDGETARY" ? "default" : - rfq.rfqType === "PURCHASE" ? "destructive" : - rfq.rfqType === "PURCHASE_BUDGETARY" ? "secondary" : "outline" - } - > - {rfq.rfqType} - </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-tbe-table/tbe-table-columns.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx deleted file mode 100644 index f664d9a3..00000000 --- a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx +++ /dev/null @@ -1,350 +0,0 @@ -"use client" - -import * as React from "react" -import { type DataTableRowAction } from "@/types/table" -import { type ColumnDef } from "@tanstack/react-table" -import { Download, MessageSquare, Upload } from "lucide-react" -import { toast } from "sonner" - -import { getErrorMessage } from "@/lib/handle-error" -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 { - tbeVendorColumnsConfig, - VendorTbeColumnConfig, - vendorTbeColumnsConfig, - TbeVendorFields, -} from "@/config/vendorTbeColumnsConfig" - -type NextRouter = ReturnType<typeof useRouter> - -interface GetColumnsProps { - setRowAction: React.Dispatch< - React.SetStateAction<DataTableRowAction<TbeVendorFields> | null> - > - router: NextRouter - openCommentSheet: (vendorId: number) => void - handleDownloadTbeTemplate: (tbeId: number, vendorId: number, rfqId: number) => void - handleUploadTbeResponse: (tbeId: number, vendorId: number, rfqId: number, vendorResponseId:number) => void - openVendorContactsDialog: (rfqId: number, rfq: TbeVendorFields) => void // 수정된 시그니처 - -} - -/** - * tanstack table 컬럼 정의 (중첩 헤더 버전) - */ -export function getColumns({ - setRowAction, - router, - openCommentSheet, - handleDownloadTbeTemplate, - handleUploadTbeResponse, - openVendorContactsDialog -}: GetColumnsProps): ColumnDef<TbeVendorFields>[] { - // ---------------------------------------------------------------- - // 1) Select 컬럼 (체크박스) - // ---------------------------------------------------------------- - const selectColumn: ColumnDef<TbeVendorFields> = { - 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<TbeVendorFields>[]> = {} - - tbeVendorColumnsConfig.forEach((cfg) => { - const groupName = cfg.group || "_noGroup" - if (!groupMap[groupName]) { - groupMap[groupName] = [] - } - - // childCol: ColumnDef<TbeVendorFields> - const childCol: ColumnDef<TbeVendorFields> = { - 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 === "vendorStatus") { - const statusVal = row.original.vendorStatus - if (!statusVal) return null - // const Icon = getStatusIcon(statusVal) - return ( - <Badge variant="outline"> - {statusVal} - </Badge> - ) - } - - - 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> - ); - } - if (cfg.id === "rfqVendorStatus") { - const statusVal = row.original.rfqVendorStatus - if (!statusVal) return null - // const Icon = getStatusIcon(statusVal) - const variant = statusVal === "INVITED" ? "default" : statusVal === "REJECTED" ? "destructive" : statusVal === "ACCEPTED" ? "secondary" : "outline" - return ( - <Badge variant={variant}> - {statusVal} - </Badge> - ) - } - - // 예) TBE Updated (날짜) - if (cfg.id === "tbeUpdated") { - const dateVal = val as Date | undefined - if (!dateVal) return null - return formatDate(dateVal) - } - - // 그 외 필드는 기본 값 표시 - return val ?? "" - }, - } - - groupMap[groupName].push(childCol) - }) - - // groupMap → nestedColumns - const nestedColumns: ColumnDef<TbeVendorFields>[] = [] - Object.entries(groupMap).forEach(([groupName, colDefs]) => { - if (groupName === "_noGroup") { - nestedColumns.push(...colDefs) - } else { - nestedColumns.push({ - id: groupName, - header: groupName, - columns: colDefs, - }) - } - }) - - // ---------------------------------------------------------------- - // 3) Comments 컬럼 - // ---------------------------------------------------------------- - const commentsColumn: ColumnDef<TbeVendorFields> = { - 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.tbeId ?? 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 - } - - // ---------------------------------------------------------------- - // 4) TBE 다운로드 컬럼 - 템플릿 다운로드 기능 - // ---------------------------------------------------------------- - const tbeDownloadColumn: ColumnDef<TbeVendorFields> = { - id: "tbeDownload", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="TBE Sheets" /> - ), - cell: ({ row }) => { - const vendor = row.original - const tbeId = vendor.tbeId - const vendorId = vendor.vendorId - const rfqId = vendor.rfqId - const templateFileCount = vendor.templateFileCount || 0 - - if (!tbeId || !vendorId || !rfqId) { - return <div className="text-center text-muted-foreground">-</div> - } - - // 템플릿 파일이 없으면 다운로드 버튼 비활성화 - const isDisabled = templateFileCount <= 0 - - return ( - <Button - variant="ghost" - size="sm" - className="relative h-8 w-8 p-0 group" - onClick={ - isDisabled - ? undefined - : () => handleDownloadTbeTemplate(tbeId, vendorId, rfqId) - } - aria-label={ - templateFileCount > 0 - ? `TBE 템플릿 다운로드 (${templateFileCount}개)` - : "다운로드할 파일 없음" - } - disabled={isDisabled} - > - <Download className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> - - {/* 파일이 1개 이상인 경우 뱃지로 개수 표시 */} - {templateFileCount > 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" - > - {templateFileCount} - </Badge> - )} - - <span className="sr-only"> - {templateFileCount > 0 - ? `TBE 템플릿 다운로드 (${templateFileCount}개)` - : "다운로드할 파일 없음"} - </span> - </Button> - ) - }, - enableSorting: false, - maxSize: 80, - } - // ---------------------------------------------------------------- - // 5) TBE 업로드 컬럼 - 응답 업로드 기능 - // ---------------------------------------------------------------- - const tbeUploadColumn: ColumnDef<TbeVendorFields> = { - id: "tbeUpload", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="Upload Response" /> - ), - cell: ({ row }) => { - const vendor = row.original - const tbeId = vendor.tbeId - const vendorId = vendor.vendorId - const rfqId = vendor.rfqId - const vendorResponseId = vendor.vendorResponseId || 0 - const status = vendor.rfqVendorStatus - const hasResponse = vendor.hasResponse || false - - - if (!tbeId || !vendorId || !rfqId || status === "REJECTED") { - return <div className="text-center text-muted-foreground">-</div> - } - - return ( - <div > - <Button - variant="ghost" - size="sm" - className="h-8 w-8 p-0 group relative" - onClick={() => handleUploadTbeResponse(tbeId, vendorId, rfqId, vendorResponseId)} - aria-label={hasResponse ? "TBE 응답 확인" : "TBE 응답 업로드"} - > - <div className="flex items-center justify-center relative"> - <Upload className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" /> - </div> - {hasResponse && ( - <span className="absolute -top-1 -right-1 inline-flex h-2 w-2 rounded-full" style={{ backgroundColor: '#10B981' }}></span> - )} - <span className="sr-only"> - {"TBE 응답 업로드"} - </span> - </Button> - </div> - ) - }, - enableSorting: false, - maxSize: 80 - } - - // ---------------------------------------------------------------- - // 6) 최종 컬럼 배열 - // ---------------------------------------------------------------- - return [ - selectColumn, - ...nestedColumns, - commentsColumn, - tbeDownloadColumn, - tbeUploadColumn, - ] -}
\ No newline at end of file diff --git a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx deleted file mode 100644 index 13d5dc64..00000000 --- a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx +++ /dev/null @@ -1,188 +0,0 @@ -"use client" - -import * as React from "react" -import { useRouter } from "next/navigation" -import type { - DataTableAdvancedFilterField, - DataTableFilterField, - DataTableRowAction, -} from "@/types/table" -import { toast } from "sonner" -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 "./tbe-table-columns" -import { fetchRfqAttachmentsbyCommentId, getTBEforVendor } from "../../rfqs/service" -import { CommentSheet, TbeComment } from "./comments-sheet" -import { TbeVendorFields } from "@/config/vendorTbeColumnsConfig" -import { useTbeFileHandlers } from "./tbeFileHandler" -import { useSession } from "next-auth/react" -import { RfqDeailDialog } from "./rfq-detail-dialog" - -interface VendorsTableProps { - promises: Promise< - [ - Awaited<ReturnType<typeof getTBEforVendor>>, - ] - > -} - -export function TbeVendorTable({ 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<TbeVendorFields> | null>(null) - - - // router 획득 - const router = useRouter() - - const [initialComments, setInitialComments] = React.useState<TbeComment[]>([]) - const [isLoadingComments, setIsLoadingComments] = React.useState(false) - - const [commentSheetOpen, setCommentSheetOpen] = React.useState(false) - const [selectedRfqIdForComments, setSelectedRfqIdForComments] = React.useState<number | null>(null) - const [isRfqDetailDialogOpen, setIsRfqDetailDialogOpen] = React.useState(false) - - const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null) - const [selectedRfq, setSelectedRfq] = React.useState<TbeVendorFields | null>(null) - - const openVendorContactsDialog = (rfqId: number, rfq: TbeVendorFields) => { - setSelectedRfqId(rfqId) - setSelectedRfq(rfq) - setIsRfqDetailDialogOpen(true) - } - - // TBE 파일 핸들러 훅 사용 - const { - handleDownloadTbeTemplate, - handleUploadTbeResponse, - UploadDialog, - } = useTbeFileHandlers() - - React.useEffect(() => { - if (rowAction?.type === "comments") { - // rowAction가 새로 세팅된 뒤 여기서 openCommentSheet 실행 - openCommentSheet(Number(rowAction.row.original.id)) - } - }, [rowAction]) - - async function openCommentSheet(vendorId: number) { - setInitialComments([]) - setIsLoadingComments(true) - - const comments = rowAction?.row.original.comments - - try { - if (comments && comments.length > 0) { - const commentWithAttachments: TbeComment[] = await Promise.all( - comments.map(async (c) => { - // 서버 액션을 사용하여 코멘트 첨부 파일 가져오기 - const attachments = await fetchRfqAttachmentsbyCommentId(c.id) - - return { - ...c, - commentedBy: userId, // DB나 API 응답에 있다고 가정 - attachments, - } - }) - ) - - setInitialComments(commentWithAttachments) - } - - setSelectedRfqIdForComments(vendorId) - setCommentSheetOpen(true) - - } catch (error) { - console.error("Error loading comments:", error) - toast.error("Failed to load comments") - } finally { - // End loading regardless of success/failure - setIsLoadingComments(false) - } -} - - // getColumns() 호출 시, 필요한 모든 핸들러 함수 주입 - const columns = React.useMemo( - () => getColumns({ - setRowAction, - router, - openCommentSheet, - handleDownloadTbeTemplate, - handleUploadTbeResponse, - openVendorContactsDialog - }), - [setRowAction, router, openCommentSheet, handleDownloadTbeTemplate, handleUploadTbeResponse, openVendorContactsDialog] - ) - - const filterFields: DataTableFilterField<TbeVendorFields>[] = [] - - const advancedFilterFields: DataTableAdvancedFilterField<TbeVendorFields>[] = [ - { id: "rfqCode", label: "RFQ Code", type: "text" }, - { id: "projectCode", label: "Project Code", type: "text" }, - { id: "projectName", label: "Project Name", type: "text" }, - { id: "rfqCode", label: "RFQ Code", type: "text" }, - { id: "tbeResult", label: "TBE Result", type: "text" }, - { id: "tbeNote", label: "TBE Note", type: "text" }, - { id: "rfqCode", label: "RFQ Code", type: "text" }, - { id: "hasResponse", label: "Response?", type: "boolean" }, - { id: "rfqVendorUpdated", label: "Updated at", type: "date" }, - { id: "dueDate", label: "Project Name", type: "date" }, - - ] - - const { table } = useDataTable({ - data, - columns, - pageCount, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "rfqVendorUpdated", desc: true }], - columnPinning: { right: ["comments", "tbeDocuments"] }, // tbeDocuments 컬럼을 우측에 고정 - }, - getRowId: (originalRow) => String(originalRow.id), - shallow: false, - clearOnDefault: true, - }) - - return ( - <> - <DataTable table={table}> - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - shallow={false} - /> - </DataTable> - - {/* 코멘트 시트 */} - {commentSheetOpen && selectedRfqIdForComments && ( - <CommentSheet - open={commentSheetOpen} - onOpenChange={setCommentSheetOpen} - rfqId={selectedRfqIdForComments} - initialComments={initialComments} - vendorId={userVendorId || 0} - currentUserId={userId || 0} - isLoading={isLoadingComments} // Pass the loading state - - /> - )} - - <RfqDeailDialog - isOpen={isRfqDetailDialogOpen} - onOpenChange={setIsRfqDetailDialogOpen} - rfqId={selectedRfqId} - rfq={selectedRfq} - /> - - {/* TBE 파일 다이얼로그 */} - <UploadDialog /> - </> - ) -}
\ No newline at end of file diff --git a/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx deleted file mode 100644 index a0b6f805..00000000 --- a/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx +++ /dev/null @@ -1,355 +0,0 @@ -"use client"; - -import { useCallback, useState, useEffect } from "react"; -import { toast } from "sonner"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { - fetchTbeTemplateFiles, - uploadTbeResponseFile, - getTbeSubmittedFiles, - getFileFromRfqAttachmentsbyid, -} from "../../rfqs/service"; -import { - Dropzone, - DropzoneDescription, - DropzoneInput, - DropzoneTitle, - DropzoneUploadIcon, - DropzoneZone, -} from "@/components/ui/dropzone"; -import { - FileList, - FileListAction, - FileListDescription, - FileListIcon, - FileListInfo, - FileListItem, - FileListName, - FileListSize, -} from "@/components/ui/file-list"; -import { Download, X } from "lucide-react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { formatDateTime } from "@/lib/utils"; - -export function useTbeFileHandlers() { - // 모달 열림 여부, 현재 선택된 IDs - const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); - const [currentTbeId, setCurrentTbeId] = useState<number | null>(null); - const [currentVendorId, setCurrentVendorId] = useState<number | null>(null); - const [currentRfqId, setCurrentRfqId] = useState<number | null>(null); - const [currentvendorResponseId, setCurrentvendorResponseId] = useState<number | null>(null); - - - - // 로딩 상태들 - const [isLoading, setIsLoading] = useState(false); - const [isFetchingFiles, setIsFetchingFiles] = useState(false); - - // 업로드할 파일, 제출된 파일 목록 - const [selectedFile, setSelectedFile] = useState<File | null>(null); - const [submittedFiles, setSubmittedFiles] = useState< - Array<{ id: number; fileName: string; filePath: string; uploadedAt: Date }> - >([]); - - // =================================== - // 1) 제출된 파일 목록 가져오기 - // =================================== - const fetchSubmittedFiles = useCallback(async (vendorResponseId: number) => { - if (!vendorResponseId ) return; - - setIsFetchingFiles(true); - try { - const { files, error } = await getTbeSubmittedFiles(vendorResponseId); - if (error) { - console.error(error); - return; - } - setSubmittedFiles(files); - } catch (error) { - console.error("Failed to fetch submitted files:", error); - } finally { - setIsFetchingFiles(false); - } - }, []); - - // =================================== - // 2) TBE 템플릿 다운로드 - // =================================== - const handleDownloadTbeTemplate = useCallback( - async (tbeId: number, vendorId: number, rfqId: number) => { - setCurrentTbeId(tbeId); - setCurrentVendorId(vendorId); - setCurrentRfqId(rfqId); - setIsLoading(true); - - try { - const { files, error } = await fetchTbeTemplateFiles(tbeId); - if (error) { - toast.error(error); - return; - } - if (files.length === 0) { - toast.warning("다운로드할 템플릿 파일이 없습니다"); - return; - } - // 순차적으로 파일 다운로드 - for (const file of files) { - await downloadFile(file.id); - } - toast.success("모든 템플릿 파일이 다운로드되었습니다"); - } catch (error) { - toast.error("템플릿 파일을 다운로드하는 데 실패했습니다"); - console.error(error); - } finally { - setIsLoading(false); - } - }, - [] - ); - - // 실제 다운로드 로직 - const downloadFile = 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; - } - }, []); - - // =================================== - // 3) 제출된 파일 다운로드 - // =================================== - const downloadSubmittedFile = useCallback((file: { id: number; fileName: string; filePath: string }) => { - try { - const link = document.createElement("a"); - link.href = `/api/tbe-download?path=${encodeURIComponent(file.filePath)}`; - link.download = file.fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - toast.success(`${file.fileName} 다운로드 시작`); - } catch (error) { - console.error("Failed to download file:", error); - toast.error("파일 다운로드에 실패했습니다"); - } - }, []); - - // =================================== - // 4) TBE 응답 업로드 모달 열기 - // (이 시점에서는 데이터 fetch하지 않음) - // =================================== - const handleUploadTbeResponse = useCallback((tbeId: number, vendorId: number, rfqId: number, vendorResponseId:number) => { - setCurrentTbeId(tbeId); - setCurrentVendorId(vendorId); - setCurrentRfqId(rfqId); - setCurrentvendorResponseId(vendorResponseId); - setIsUploadDialogOpen(true); - }, []); - - // =================================== - // 5) Dialog 열고 닫힐 때 상태 초기화 - // 열렸을 때 -> useEffect로 파일 목록 가져오기 - // =================================== - useEffect(() => { - if (!isUploadDialogOpen) { - // 닫힐 때는 파일 상태들 초기화 - setSelectedFile(null); - setSubmittedFiles([]); - } - }, [isUploadDialogOpen]); - - useEffect(() => { - // Dialog가 열렸고, ID들이 유효하면 - if (isUploadDialogOpen &¤tvendorResponseId) { - fetchSubmittedFiles(currentvendorResponseId); - } - }, [isUploadDialogOpen, currentvendorResponseId, fetchSubmittedFiles]); - - // =================================== - // 6) 드롭존 파일 선택 & 제거 - // =================================== - const handleFileDrop = useCallback((files: File[]) => { - if (files && files.length > 0) { - setSelectedFile(files[0]); - } - }, []); - - const handleRemoveFile = useCallback(() => { - setSelectedFile(null); - }, []); - - // =================================== - // 7) 응답 파일 업로드 - // =================================== - const handleSubmitResponse = useCallback(async () => { - if (!selectedFile || !currentTbeId || !currentVendorId || !currentRfqId ||!currentvendorResponseId) { - toast.error("업로드할 파일을 선택해주세요"); - return; - } - - setIsLoading(true); - try { - // FormData 생성 - const formData = new FormData(); - formData.append("file", selectedFile); - formData.append("rfqId", currentRfqId.toString()); - formData.append("vendorId", currentVendorId.toString()); - formData.append("evaluationId", currentTbeId.toString()); - formData.append("vendorResponseId", currentvendorResponseId.toString()); - - const result = await uploadTbeResponseFile(formData); - if (!result.success) { - throw new Error(result.error || "파일 업로드에 실패했습니다"); - } - - toast.success(result.message || "응답이 성공적으로 업로드되었습니다"); - - // 업로드 후 다시 제출된 파일 목록 가져오기 - await fetchSubmittedFiles(currentvendorResponseId); - - // 업로드 성공 시 선택 파일 초기화 - setSelectedFile(null); - } catch (error) { - toast.error(error instanceof Error ? error.message : "응답 업로드에 실패했습니다"); - console.error(error); - } finally { - setIsLoading(false); - } - }, [selectedFile, currentTbeId, currentVendorId, currentRfqId, currentvendorResponseId,fetchSubmittedFiles]); - - // =================================== - // 8) 실제 Dialog 컴포넌트 - // =================================== - const UploadDialog = () => ( - <Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}> - <DialogContent className="sm:max-w-lg"> - <DialogHeader> - <DialogTitle>TBE 응답 파일</DialogTitle> - <DialogDescription>제출된 파일을 확인하거나 새 파일을 업로드하세요.</DialogDescription> - </DialogHeader> - - <Tabs defaultValue="upload" className="w-full"> - <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="upload">새 파일 업로드</TabsTrigger> - <TabsTrigger - value="submitted" - disabled={submittedFiles.length === 0} - className={submittedFiles.length > 0 ? "relative" : ""} - > - 제출된 파일{" "} - {submittedFiles.length > 0 && ( - <span className="ml-2 inline-flex items-center justify-center rounded-full bg-primary w-4 h-4 text-[10px] text-primary-foreground"> - {submittedFiles.length} - </span> - )} - </TabsTrigger> - </TabsList> - - {/* 업로드 탭 */} - <TabsContent value="upload" className="pt-4"> - <div className="grid gap-4"> - {selectedFile ? ( - <FileList> - <FileListItem> - <FileListIcon /> - <FileListInfo> - <FileListName>{selectedFile.name}</FileListName> - <FileListSize>{selectedFile.size}</FileListSize> - </FileListInfo> - <FileListAction> - <Button variant="ghost" size="icon" onClick={handleRemoveFile}> - <X className="h-4 w-4" /> - <span className="sr-only">파일 제거</span> - </Button> - </FileListAction> - </FileListItem> - </FileList> - ) : ( - <Dropzone onDrop={handleFileDrop}> - <DropzoneInput className="sr-only" /> - <DropzoneZone className="flex flex-col items-center justify-center gap-2 p-6"> - <DropzoneUploadIcon className="h-10 w-10 text-muted-foreground" /> - <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle> - <DropzoneDescription>TBE 응답 파일 (XLSX, XLS, DOCX, PDF 등)</DropzoneDescription> - </DropzoneZone> - </Dropzone> - )} - - <DialogFooter className="mt-4"> - <Button type="submit" onClick={handleSubmitResponse} disabled={!selectedFile || isLoading}> - {isLoading ? "업로드 중..." : "응답 업로드"} - </Button> - </DialogFooter> - </div> - </TabsContent> - - {/* 제출된 파일 탭 */} - <TabsContent value="submitted" className="pt-4"> - {isFetchingFiles ? ( - <div className="flex justify-center items-center py-8"> - <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> - </div> - ) : submittedFiles.length > 0 ? ( - <div className="grid gap-2"> - <FileList> - {submittedFiles.map((file) => ( - <FileListItem key={file.id} className="flex items-center justify-between gap-3"> - <div className="flex items-center gap-3 flex-1"> - <FileListIcon className="flex-shrink-0" /> - <FileListInfo className="flex-1 min-w-0"> - <FileListName className="text-sm font-medium truncate">{file.fileName}</FileListName> - <FileListDescription className="text-xs text-muted-foreground"> - {file.uploadedAt ? formatDateTime(file.uploadedAt) : ""} - </FileListDescription> - </FileListInfo> - </div> - <FileListAction className="flex-shrink-0 ml-2"> - <Button variant="ghost" size="icon" onClick={() => downloadSubmittedFile(file)}> - <Download className="h-4 w-4" /> - <span className="sr-only">파일 다운로드</span> - </Button> - </FileListAction> - </FileListItem> - ))} - </FileList> - </div> - ) : ( - <div className="text-center py-8 text-muted-foreground">제출된 파일이 없습니다.</div> - )} - </TabsContent> - </Tabs> - </DialogContent> - </Dialog> - ); - - // =================================== - // 9) Hooks 내보내기 - // =================================== - return { - handleDownloadTbeTemplate, - handleUploadTbeResponse, - UploadDialog, - }; -}
\ No newline at end of file |
