summaryrefslogtreecommitdiff
path: root/lib/vendor-rfq-response/vendor-tbe-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-rfq-response/vendor-tbe-table')
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx346
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx86
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx350
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx188
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx355
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 &&currentvendorResponseId) {
- 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