summaryrefslogtreecommitdiff
path: root/lib/rfqs/tbe-table
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
commit44b74ff4170090673b6eeacd8c528e0abf47b7aa (patch)
tree3f3824b4e2cb24536c1677188b4cae5b8909d3da /lib/rfqs/tbe-table
parent4953e770929b82ef77da074f77071ebd0f428529 (diff)
(김준회) deprecated code 정리
Diffstat (limited to 'lib/rfqs/tbe-table')
-rw-r--r--lib/rfqs/tbe-table/comments-sheet.tsx325
-rw-r--r--lib/rfqs/tbe-table/feature-flags-provider.tsx108
-rw-r--r--lib/rfqs/tbe-table/file-dialog.tsx141
-rw-r--r--lib/rfqs/tbe-table/invite-vendors-dialog.tsx220
-rw-r--r--lib/rfqs/tbe-table/tbe-result-dialog.tsx208
-rw-r--r--lib/rfqs/tbe-table/tbe-table-columns.tsx373
-rw-r--r--lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx67
-rw-r--r--lib/rfqs/tbe-table/tbe-table.tsx220
-rw-r--r--lib/rfqs/tbe-table/vendor-contact-dialog.tsx71
-rw-r--r--lib/rfqs/tbe-table/vendor-contact/vendor-contact-table-column.tsx70
-rw-r--r--lib/rfqs/tbe-table/vendor-contact/vendor-contact-table.tsx89
11 files changed, 0 insertions, 1892 deletions
diff --git a/lib/rfqs/tbe-table/comments-sheet.tsx b/lib/rfqs/tbe-table/comments-sheet.tsx
deleted file mode 100644
index b3cdbc60..00000000
--- a/lib/rfqs/tbe-table/comments-sheet.tsx
+++ /dev/null
@@ -1,325 +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 { createRfqCommentWithAttachments } from "../service"
-import { formatDate } from "@/lib/utils"
-
-
-export interface TbeComment {
- 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?: TbeComment[]
- currentUserId: number
- rfqId: number
- tbeId: number
- vendorId: number
- onCommentsUpdated?: (comments: TbeComment[]) => 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,
- onCommentsUpdated,
- isLoading = false, // Default to false
- ...props
-}: CommentSheetProps) {
- console.log("tbeId", tbeId)
-
- const [comments, setComments] = React.useState<TbeComment[]>(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, "KR") : "-"}</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 {
- console.log("rfqId", rfqId)
- console.log("vendorId", vendorId)
- console.log("tbeId", tbeId)
- console.log("currentUserId", currentUserId)
- const res = await createRfqCommentWithAttachments({
- rfqId,
- vendorId,
- commentText: data.commentText,
- commentedBy: currentUserId,
- evaluationId: tbeId,
- cbeId: null,
- files: data.newFiles,
- })
-
- if (!res.ok) {
- throw new Error("Failed to create comment")
- }
-
- toast.success("Comment created")
-
- // 임시로 새 코멘트 추가
- const newComment: TbeComment = {
- 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/rfqs/tbe-table/feature-flags-provider.tsx b/lib/rfqs/tbe-table/feature-flags-provider.tsx
deleted file mode 100644
index 81131894..00000000
--- a/lib/rfqs/tbe-table/feature-flags-provider.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useQueryState } from "nuqs"
-
-import { dataTableConfig, type DataTableConfig } from "@/config/data-table"
-import { cn } from "@/lib/utils"
-import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-
-type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"]
-
-interface FeatureFlagsContextProps {
- featureFlags: FeatureFlagValue[]
- setFeatureFlags: (value: FeatureFlagValue[]) => void
-}
-
-const FeatureFlagsContext = React.createContext<FeatureFlagsContextProps>({
- featureFlags: [],
- setFeatureFlags: () => {},
-})
-
-export function useFeatureFlags() {
- const context = React.useContext(FeatureFlagsContext)
- if (!context) {
- throw new Error(
- "useFeatureFlags must be used within a FeatureFlagsProvider"
- )
- }
- return context
-}
-
-interface FeatureFlagsProviderProps {
- children: React.ReactNode
-}
-
-export function FeatureFlagsProvider({ children }: FeatureFlagsProviderProps) {
- const [featureFlags, setFeatureFlags] = useQueryState<FeatureFlagValue[]>(
- "flags",
- {
- defaultValue: [],
- parse: (value) => value.split(",") as FeatureFlagValue[],
- serialize: (value) => value.join(","),
- eq: (a, b) =>
- a.length === b.length && a.every((value, index) => value === b[index]),
- clearOnDefault: true,
- shallow: false,
- }
- )
-
- return (
- <FeatureFlagsContext.Provider
- value={{
- featureFlags,
- setFeatureFlags: (value) => void setFeatureFlags(value),
- }}
- >
- <div className="w-full overflow-x-auto">
- <ToggleGroup
- type="multiple"
- variant="outline"
- size="sm"
- value={featureFlags}
- onValueChange={(value: FeatureFlagValue[]) => setFeatureFlags(value)}
- className="w-fit gap-0"
- >
- {dataTableConfig.featureFlags.map((flag, index) => (
- <Tooltip key={flag.value}>
- <ToggleGroupItem
- value={flag.value}
- className={cn(
- "gap-2 whitespace-nowrap rounded-none px-3 text-xs data-[state=on]:bg-accent/70 data-[state=on]:hover:bg-accent/90",
- {
- "rounded-l-sm border-r-0": index === 0,
- "rounded-r-sm":
- index === dataTableConfig.featureFlags.length - 1,
- }
- )}
- asChild
- >
- <TooltipTrigger>
- <flag.icon className="size-3.5 shrink-0" aria-hidden="true" />
- {flag.label}
- </TooltipTrigger>
- </ToggleGroupItem>
- <TooltipContent
- align="start"
- side="bottom"
- sideOffset={6}
- className="flex max-w-60 flex-col space-y-1.5 border bg-background py-2 font-semibold text-foreground"
- >
- <div>{flag.tooltipTitle}</div>
- <div className="text-xs text-muted-foreground">
- {flag.tooltipDescription}
- </div>
- </TooltipContent>
- </Tooltip>
- ))}
- </ToggleGroup>
- </div>
- {children}
- </FeatureFlagsContext.Provider>
- )
-}
diff --git a/lib/rfqs/tbe-table/file-dialog.tsx b/lib/rfqs/tbe-table/file-dialog.tsx
deleted file mode 100644
index e19430a3..00000000
--- a/lib/rfqs/tbe-table/file-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Download, X } from "lucide-react"
-import { toast } from "sonner"
-
-import { getErrorMessage } from "@/lib/handle-error"
-import { formatDateTime } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-
-import {
- FileList,
- FileListItem,
- FileListIcon,
- FileListInfo,
- FileListName,
- FileListDescription,
- FileListAction,
-} from "@/components/ui/file-list"
-import { getTbeFilesForVendor, getTbeSubmittedFiles } from "../service"
-
-interface TBEFileDialogProps {
- isOpen: boolean
- onOpenChange: (open: boolean) => void
- tbeId: number
- vendorId: number
- rfqId: number
- onRefresh?: () => void
-}
-
-export function TBEFileDialog({
- isOpen,
- onOpenChange,
- vendorId,
- rfqId,
- onRefresh,
-}: TBEFileDialogProps) {
- const [submittedFiles, setSubmittedFiles] = React.useState<any[]>([])
- const [isFetchingFiles, setIsFetchingFiles] = React.useState(false)
-
-
- // Fetch submitted files when dialog opens
- React.useEffect(() => {
- if (isOpen && rfqId && vendorId) {
- fetchSubmittedFiles()
- }
- }, [isOpen, rfqId, vendorId])
-
- // Fetch submitted files using the service function
- const fetchSubmittedFiles = async () => {
- if (!rfqId || !vendorId) return
-
- setIsFetchingFiles(true)
- try {
- const { files, error } = await getTbeFilesForVendor(rfqId, vendorId)
-
- if (error) {
- throw new Error(error)
- }
-
- setSubmittedFiles(files)
- } catch (error) {
- toast.error("Failed to load files: " + getErrorMessage(error))
- } finally {
- setIsFetchingFiles(false)
- }
- }
-
- // Download submitted file
- const downloadSubmittedFile = async (file: any) => {
- try {
- const response = await fetch(`/api/tbe-download?path=${encodeURIComponent(file.filePath)}`)
- if (!response.ok) {
- throw new Error("Failed to download file")
- }
-
- const blob = await response.blob()
- const url = window.URL.createObjectURL(blob)
- const a = document.createElement("a")
- a.href = url
- a.download = file.fileName
- document.body.appendChild(a)
- a.click()
- window.URL.revokeObjectURL(url)
- document.body.removeChild(a)
- } catch (error) {
- toast.error("Failed to download file: " + getErrorMessage(error))
- }
- }
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent className="sm:max-w-lg">
- <DialogHeader>
- <DialogTitle>TBE 응답 파일</DialogTitle>
- <DialogDescription>제출된 파일 목록을 확인하고 다운로드하세요.</DialogDescription>
- </DialogHeader>
-
- {/* 제출된 파일 목록 */}
- {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, "KR") : ""}
- </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>
- )}
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/invite-vendors-dialog.tsx b/lib/rfqs/tbe-table/invite-vendors-dialog.tsx
deleted file mode 100644
index 935d2bf3..00000000
--- a/lib/rfqs/tbe-table/invite-vendors-dialog.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Row } from "@tanstack/react-table"
-import { Loader, Send } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-
-import { Input } from "@/components/ui/input"
-
-import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
-import { inviteTbeVendorsAction } from "../service"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Badge } from "@/components/ui/badge"
-import { Label } from "@/components/ui/label"
-
-interface InviteVendorsDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- vendors: Row<VendorWithTbeFields>["original"][]
- rfqId: number
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function InviteVendorsDialog({
- vendors,
- rfqId,
- showTrigger = true,
- onSuccess,
- ...props
-}: InviteVendorsDialogProps) {
- const [isInvitePending, startInviteTransition] = React.useTransition()
-
-
- // multiple 파일을 받을 state
- const [files, setFiles] = React.useState<FileList | null>(null)
-
- // 미디어쿼리 (desktop 여부)
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onInvite() {
- startInviteTransition(async () => {
- // 파일이 선택되지 않았다면 에러
- if (!files || files.length === 0) {
- toast.error("Please attach TBE files before inviting.")
- return
- }
-
- // FormData 생성
- const formData = new FormData()
- formData.append("rfqId", String(rfqId))
- vendors.forEach((vendor) => {
- formData.append("vendorIds[]", String(vendor.id))
- })
-
- // multiple 파일
- for (let i = 0; i < files.length; i++) {
- formData.append("tbeFiles", files[i]) // key는 동일하게 "tbeFiles"
- }
-
- // 서버 액션 호출
- const { error } = await inviteTbeVendorsAction(formData)
-
- if (error) {
- toast.error(error)
- return
- }
-
- // 성공
- props.onOpenChange?.(false)
- toast.success("Vendors invited with TBE!")
- onSuccess?.()
- })
- }
-
- // 파일 선택 UI
- const fileInput = (
-<>
- <div className="space-y-2">
- <Label>선택된 협력업체 ({vendors.length})</Label>
- <ScrollArea className="h-20 border rounded-md p-2">
- <div className="flex flex-wrap gap-2">
- {vendors.map((vendor, index) => (
- <Badge key={index} variant="secondary" className="py-1">
- {vendor.vendorName || `협력업체 #${vendor.vendorCode}`}
- </Badge>
- ))}
- </div>
- </ScrollArea>
- <p className="text-[0.8rem] font-medium text-muted-foreground">
- 선택된 모든 협력업체의 등록된 연락처에게 TBE 평가 알림이 전송됩니다.
- </p>
- </div>
-
- <div className="mb-4">
- <label className="mb-2 block font-medium">TBE Sheets</label>
- <Input
- type="file"
- multiple
- onChange={(e) => {
- setFiles(e.target.files)
- }}
- />
- </div>
- </>
- )
-
- // Desktop Dialog
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Send className="mr-2 size-4" aria-hidden="true" />
- TBE 평가 생성 ({vendors.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>TBE 평가 시트 전송</DialogTitle>
- <DialogDescription>
- 선택한 {vendors.length}개 협력업체에 대한 기술 평가 시트와 알림을 전송합니다. 파일 첨부가 필수이므로 파일을 첨부해야지 버튼이 활성화됩니다.
- </DialogDescription>
- </DialogHeader>
-
- {/* 파일 첨부 */}
- {fileInput}
-
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">Cancel</Button>
- </DialogClose>
- <Button
- aria-label="Invite selected rows"
- variant="destructive"
- onClick={onInvite}
- // 파일이 없거나 초대 진행중이면 비활성화
- disabled={isInvitePending || !files || files.length === 0}
- >
- {isInvitePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- Invite
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- // Mobile Drawer
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Send className="mr-2 size-4" aria-hidden="true" />
- Invite ({vendors.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DialogTitle>TBE 평가 시트 전송</DialogTitle>
- <DialogDescription>
- 선택한 {vendors.length}개 협력업체에 대한 기술 평가 시트와 알림을 전송합니다. 파일 첨부가 필수이므로 파일을 첨부해야지 버튼이 활성화됩니다.
- </DialogDescription>
- </DrawerHeader>
-
- {/* 파일 첨부 */}
- {fileInput}
-
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
- </DrawerClose>
- <Button
- aria-label="Invite selected rows"
- variant="destructive"
- onClick={onInvite}
- // 파일이 없거나 초대 진행중이면 비활성화
- disabled={isInvitePending || !files || files.length === 0}
- >
- {isInvitePending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- Invite
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/tbe-result-dialog.tsx b/lib/rfqs/tbe-table/tbe-result-dialog.tsx
deleted file mode 100644
index 8400ecac..00000000
--- a/lib/rfqs/tbe-table/tbe-result-dialog.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { toast } from "sonner"
-
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Textarea } from "@/components/ui/textarea"
-import { Label } from "@/components/ui/label"
-import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
-import { getErrorMessage } from "@/lib/handle-error"
-import { saveTbeResult } from "../service"
-
-// Define the props for the TbeResultDialog component
-interface TbeResultDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- tbe: VendorWithTbeFields | null
- onRefresh?: () => void
-}
-
-// Define TBE result options
-const TBE_RESULT_OPTIONS = [
- { value: "pass", label: "Pass", badgeVariant: "default" },
- { value: "non-pass", label: "Non-Pass", badgeVariant: "destructive" },
- { value: "conditional pass", label: "Conditional Pass", badgeVariant: "secondary" },
-] as const
-
-type TbeResultOption = typeof TBE_RESULT_OPTIONS[number]["value"]
-
-export function TbeResultDialog({
- open,
- onOpenChange,
- tbe,
- onRefresh,
-}: TbeResultDialogProps) {
- // Initialize state for form inputs
- const [result, setResult] = React.useState<TbeResultOption | "">("")
- const [note, setNote] = React.useState("")
- const [isSubmitting, setIsSubmitting] = React.useState(false)
-
- // Update form values when the tbe prop changes
- React.useEffect(() => {
- if (tbe) {
- setResult((tbe.tbeResult as TbeResultOption) || "")
- setNote(tbe.tbeNote || "")
- }
- }, [tbe])
-
- // Reset form when dialog closes
- React.useEffect(() => {
- if (!open) {
- // Small delay to avoid visual glitches when dialog is closing
- const timer = setTimeout(() => {
- if (!tbe) {
- setResult("")
- setNote("")
- }
- }, 300)
- return () => clearTimeout(timer)
- }
- }, [open, tbe])
-
- // Handle form submission with server action
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
-
- if (!tbe || !result) return
-
- setIsSubmitting(true)
-
- try {
- // Call the server action to save the TBE result
- const response = await saveTbeResult({
- id: tbe.tbeId ?? 0, // This is the id in the rfq_evaluations table
- vendorId: tbe.vendorId, // This is the vendorId in the rfq_evaluations table
- result: result, // The selected evaluation result
- notes: note, // The evaluation notes
- })
-
- if (!response.success) {
- throw new Error(response.message || "Failed to save TBE result")
- }
-
- // Show success toast
- toast.success("TBE result saved successfully")
-
- // Close the dialog
- onOpenChange(false)
-
- // Refresh the data if refresh callback is provided
- if (onRefresh) {
- onRefresh()
- }
- } catch (error) {
- // Show error toast
- toast.error(`Failed to save: ${getErrorMessage(error)}`)
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // Find the selected result option
- const selectedOption = TBE_RESULT_OPTIONS.find(option => option.value === result)
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle className="text-xl font-semibold">
- {tbe?.tbeResult ? "Edit TBE Result" : "Enter TBE Result"}
- </DialogTitle>
- {tbe && (
- <DialogDescription className="text-sm text-muted-foreground mt-1">
- <div className="flex flex-col gap-1">
- <span>
- <strong>Vendor:</strong> {tbe.vendorName}
- </span>
- <span>
- <strong>RFQ Code:</strong> {tbe.rfqCode}
- </span>
- {tbe.email && (
- <span>
- <strong>Email:</strong> {tbe.email}
- </span>
- )}
- </div>
- </DialogDescription>
- )}
- </DialogHeader>
-
- <form onSubmit={handleSubmit} className="space-y-6 py-2">
- <div className="space-y-2">
- <Label htmlFor="tbe-result" className="text-sm font-medium">
- Evaluation Result
- </Label>
- <Select
- value={result}
- onValueChange={(value) => setResult(value as TbeResultOption)}
- required
- >
- <SelectTrigger id="tbe-result" className="w-full">
- <SelectValue placeholder="Select a result" />
- </SelectTrigger>
- <SelectContent>
- {TBE_RESULT_OPTIONS.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- <div className="flex items-center">
- <Badge variant={option.badgeVariant as any} className="mr-2">
- {option.label}
- </Badge>
- </div>
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="tbe-note" className="text-sm font-medium">
- Evaluation Note
- </Label>
- <Textarea
- id="tbe-note"
- placeholder="Enter evaluation notes..."
- value={note}
- onChange={(e) => setNote(e.target.value)}
- className="min-h-[120px] resize-y"
- />
- </div>
-
- <DialogFooter className="gap-2 sm:gap-0">
- <Button
- type="button"
- variant="outline"
- onClick={() => onOpenChange(false)}
- disabled={isSubmitting}
- >
- Cancel
- </Button>
- <Button
- type="submit"
- disabled={!result || isSubmitting}
- className="min-w-[100px]"
- >
- {isSubmitting ? "Saving..." : "Save"}
- </Button>
- </DialogFooter>
- </form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/tbe-table-columns.tsx b/lib/rfqs/tbe-table/tbe-table-columns.tsx
deleted file mode 100644
index 0538d354..00000000
--- a/lib/rfqs/tbe-table/tbe-table-columns.tsx
+++ /dev/null
@@ -1,373 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { Download, Ellipsis, MessageSquare } 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 {
- vendorTbeColumnsConfig,
- VendorWithTbeFields,
-} from "@/config/vendorTbeColumnsConfig"
-
-type NextRouter = ReturnType<typeof useRouter>
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<
- React.SetStateAction<DataTableRowAction<VendorWithTbeFields> | null>
- >
- router: NextRouter
- openCommentSheet: (vendorId: number) => void
- openFilesDialog: (tbeId:number , vendorId: number) => void
- openVendorContactsDialog: (vendorId: number, vendor: VendorWithTbeFields) => void // 수정된 시그니처
-
-}
-
-/**
- * tanstack table 컬럼 정의 (중첩 헤더 버전)
- */
-export function getColumns({
- setRowAction,
- openCommentSheet,
- openFilesDialog,
- openVendorContactsDialog
-}: GetColumnsProps): ColumnDef<VendorWithTbeFields>[] {
- // ----------------------------------------------------------------
- // 1) Select 컬럼 (체크박스)
- // ----------------------------------------------------------------
- const selectColumn: ColumnDef<VendorWithTbeFields> = {
- 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<VendorWithTbeFields>[]> = {}
-
- vendorTbeColumnsConfig.forEach((cfg) => {
- const groupName = cfg.group || "_noGroup"
- if (!groupMap[groupName]) {
- groupMap[groupName] = []
- }
-
- // childCol: ColumnDef<VendorWithTbeFields>
- const childCol: ColumnDef<VendorWithTbeFields> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- meta: {
- excelHeader: cfg.excelHeader,
- group: cfg.group,
- type: cfg.type,
- },
- // 셀 렌더링
- cell: ({ row, getValue }) => {
- // 1) 필드값 가져오기
- const val = getValue()
-
- if (cfg.id === "vendorName") {
- const vendor = row.original;
- const vendorId = vendor.vendorId;
-
- // 협력업체 이름을 클릭할 수 있는 버튼으로 렌더링
- const handleVendorNameClick = () => {
- if (vendorId) {
- openVendorContactsDialog(vendorId, vendor); // 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 === "tbeResult") {
- const vendor = row.original;
- const tbeResult = vendor.tbeResult;
- const filesCount = vendor.files?.length ?? 0;
-
- // Only show button or link if there are files
- if (filesCount > 0) {
- // Function to handle clicking on the result
- const handleTbeResultClick = () => {
- setRowAction({ row, type: "tbeResult" });
- };
-
- if (!tbeResult) {
- // No result yet, but files exist - show "결과 입력" button
- return (
- <Button
- variant="outline"
- size="sm"
- onClick={handleTbeResultClick}
- >
- 결과 입력
- </Button>
- );
- } else {
- // Result exists - show as a hyperlink
- let badgeVariant: "default" | "outline" | "destructive" | "secondary" = "outline";
-
- // Set badge variant based on result
- if (tbeResult === "pass") {
- badgeVariant = "default";
- } else if (tbeResult === "non-pass") {
- badgeVariant = "destructive";
- } else if (tbeResult === "conditional pass") {
- badgeVariant = "secondary";
- }
-
- return (
- <Button
- variant="link"
- className="p-0 h-auto underline"
- onClick={handleTbeResultClick}
- >
- <Badge variant={badgeVariant}>
- {tbeResult}
- </Badge>
- </Button>
- );
- }
- }
-
- // No files available, return empty cell
- return null;
- }
-
-
- 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 === "rfqVendorStatus") {
- const statusVal = row.original.rfqVendorStatus
- if (!statusVal) return null
- // const Icon = getStatusIcon(statusVal)
- const variant = statusVal ==="INVITED"?"default" :statusVal ==="DECLINED"?"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, "KR")
- }
-
- // 그 외 필드는 기본 값 표시
- return val ?? ""
- },
- }
-
- groupMap[groupName].push(childCol)
- })
-
- // groupMap → nestedColumns
- const nestedColumns: ColumnDef<VendorWithTbeFields>[] = []
- 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<VendorWithTbeFields> = {
- 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) Actions 컬럼 (예: 초대하기 버튼)
- // ----------------------------------------------------------------
- // const actionsColumn: ColumnDef<VendorWithTbeFields> = {
- // id: "actions",
- // cell: ({ row }) => {
- // const status = row.original.tbeResult
- // // 예: 만약 tbeResult가 없을 때만 초대하기 버튼 표시
- // if (status) {
- // return null
- // }
-
- // return (
- // <Button
- // onClick={() => setRowAction({ row, type: "invite" })}
- // size="sm"
- // variant="outline"
- // >
- // 발행하기
- // </Button>
- // )
- // },
- // size: 80,
- // enableSorting: false,
- // enableHiding: false,
- // }
-// ----------------------------------------------------------------
-// 3) Files Column - Add before Comments column
-// ----------------------------------------------------------------
-const filesColumn: ColumnDef<VendorWithTbeFields> = {
- id: "files",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Response Files" />
- ),
- cell: ({ row }) => {
- const vendor = row.original
- // We'll assume that files count will be populated from the backend
- // You'll need to modify your getTBE function to include files
- const filesCount = vendor.files?.length ?? 0
-
- function handleClick() {
- // Open files dialog
- setRowAction({ row, type: "files" })
- openFilesDialog(vendor.tbeId ?? 0, vendor.vendorId ?? 0)
- }
-
- return (
- <div className="flex items-center justify-center">
-<Button
- variant="ghost"
- size="sm"
- className="relative h-8 w-8 p-0 group"
- onClick={handleClick}
- aria-label={filesCount > 0 ? `View ${filesCount} files` : "Upload file"}
->
- {/* 아이콘: 중앙 정렬을 위해 Button 자체가 flex container */}
- <Download className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
-
- {/* 파일 개수가 1개 이상이면 뱃지 표시 */}
- {filesCount > 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"
- >
- {filesCount}
- </Badge>
- )}
-
- <span className="sr-only">
- {filesCount > 0 ? `${filesCount} Files` : "Upload File"}
- </span>
-</Button>
- </div>
- )
- },
- enableSorting: false,
- maxSize: 80
-}
-
-// ----------------------------------------------------------------
-// 5) 최종 컬럼 배열 - Update to include the files column
-// ----------------------------------------------------------------
-return [
- selectColumn,
- ...nestedColumns,
- filesColumn, // Add the files column before comments
- commentsColumn,
- // actionsColumn,
-]
-
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx b/lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx
deleted file mode 100644
index a8f8ea82..00000000
--- a/lib/rfqs/tbe-table/tbe-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, Upload } from "lucide-react"
-import { toast } from "sonner"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-
-
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
-
-interface VendorsTableToolbarActionsProps {
- table: Table<VendorWithTbeFields>
- rfqId: number
-}
-
-export function VendorsTableToolbarActions({ table,rfqId }: VendorsTableToolbarActionsProps) {
- // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
- const fileInputRef = React.useRef<HTMLInputElement>(null)
-
- // 파일이 선택되었을 때 처리
-
- function handleImportClick() {
- // 숨겨진 <input type="file" /> 요소를 클릭
- fileInputRef.current?.click()
- }
-
- const invitationPossibeVendors = React.useMemo(() => {
- return table
- .getFilteredSelectedRowModel()
- .rows
- .map(row => row.original)
- .filter(vendor => vendor.technicalResponseStatus === null);
- }, [table.getFilteredSelectedRowModel().rows]);
-
- return (
- <div className="flex items-center gap-2">
- {invitationPossibeVendors.length > 0 &&
- (
- <InviteVendorsDialog
- vendors={invitationPossibeVendors}
- rfqId = {rfqId}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- />
- )
- }
-
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "tasks",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/tbe-table.tsx b/lib/rfqs/tbe-table/tbe-table.tsx
deleted file mode 100644
index 0add8927..00000000
--- a/lib/rfqs/tbe-table/tbe-table.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { toSentenceCase } from "@/lib/utils"
-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 { useFeatureFlags } from "./feature-flags-provider"
-import { getColumns } from "./tbe-table-columns"
-import { Vendor, vendors } from "@/db/schema/vendors"
-import { VendorsTableToolbarActions } from "./tbe-table-toolbar-actions"
-import { fetchRfqAttachmentsbyCommentId, getTBE } from "../service"
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-import { CommentSheet, TbeComment } from "./comments-sheet"
-import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
-import { TBEFileDialog } from "./file-dialog"
-import { TbeResultDialog } from "./tbe-result-dialog"
-import { VendorContactsDialog } from "./vendor-contact-dialog"
-import { useSession } from "next-auth/react" // Next-auth session hook 추가
-
-interface VendorsTableProps {
- promises: Promise<
- [
- Awaited<ReturnType<typeof getTBE>>,
- ]
- >
- rfqId: number
-}
-
-
-export function TbeTable({ promises, rfqId }: VendorsTableProps) {
- const { featureFlags } = useFeatureFlags()
-
- // Suspense로 받아온 데이터
- const [{ data, pageCount }] = React.use(promises)
- console.log("data", data)
- const { data: session } = useSession() // 세션 정보 가져오기
-
- const currentUserId = session?.user?.id ? parseInt(session.user.id, 10) : 0
-
-
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorWithTbeFields> | null>(null)
-
- // **router** 획득
- const router = useRouter()
-
- const [initialComments, setInitialComments] = React.useState<TbeComment[]>([])
- const [commentSheetOpen, setCommentSheetOpen] = React.useState(false)
- const [selectedRfqIdForComments, setSelectedRfqIdForComments] = React.useState<number | null>(null)
-
- const [isFileDialogOpen, setIsFileDialogOpen] = React.useState(false)
- const [selectedVendorId, setSelectedVendorId] = React.useState<number | null>(null)
- const [selectedTbeId, setSelectedTbeId] = React.useState<number | null>(null)
- const [isContactDialogOpen, setIsContactDialogOpen] = React.useState(false)
- const [selectedVendor, setSelectedVendor] = React.useState<VendorWithTbeFields | null>(null)
-
- // Add handleRefresh function
- const handleRefresh = React.useCallback(() => {
- router.refresh();
- }, [router]);
-
- React.useEffect(() => {
- if (rowAction?.type === "comments") {
- // rowAction가 새로 세팅된 뒤 여기서 openCommentSheet 실행
- openCommentSheet(Number(rowAction.row.original.id))
- } else if (rowAction?.type === "files") {
- // Handle files action
- const vendorId = rowAction.row.original.vendorId;
- const tbeId = rowAction.row.original.tbeId ?? 0;
- openFilesDialog(tbeId, vendorId);
- }
- }, [rowAction])
-
- async function openCommentSheet(a: number) {
- setInitialComments([])
-
- const comments = rowAction?.row.original.comments
- const rfqId = rowAction?.row.original.rfqId
- const vendorId = rowAction?.row.original.vendorId
- const tbeId = rowAction?.row.original.tbeId
- console.log("original", rowAction?.row.original)
- if (comments && comments.length > 0) {
- const commentWithAttachments: TbeComment[] = await Promise.all(
- comments.map(async (c) => {
- const attachments = await fetchRfqAttachmentsbyCommentId(c.id)
-
- return {
- ...c,
- commentedBy: currentUserId, // DB나 API 응답에 있다고 가정
- attachments,
- }
- })
- )
- // 3) state에 저장 -> CommentSheet에서 initialComments로 사용
- setInitialComments(commentWithAttachments)
- }
- setSelectedTbeId(tbeId ?? 0)
- setSelectedVendorId(vendorId ?? 0)
- setSelectedRfqIdForComments(rfqId ?? 0)
- setCommentSheetOpen(true)
- }
-
- const openFilesDialog = (tbeId: number, vendorId: number) => {
- setSelectedTbeId(tbeId)
- setSelectedVendorId(vendorId)
- setIsFileDialogOpen(true)
- }
- const openVendorContactsDialog = (vendorId: number, vendor: VendorWithTbeFields) => {
- setSelectedVendorId(vendorId)
- setSelectedVendor(vendor)
- setIsContactDialogOpen(true)
- }
-
- // getColumns() 호출 시, router를 주입
- const columns = React.useMemo(
- () => getColumns({ setRowAction, router, openCommentSheet, openFilesDialog, openVendorContactsDialog }),
- [setRowAction, router]
- )
-
- const filterFields: DataTableFilterField<VendorWithTbeFields>[] = [
- ]
-
- const advancedFilterFields: DataTableAdvancedFilterField<VendorWithTbeFields>[] = [
- { id: "vendorName", label: "Vendor Name", type: "text" },
- { id: "vendorCode", label: "Vendor Code", type: "text" },
- { id: "email", label: "Email", type: "text" },
- { id: "country", label: "Country", type: "text" },
- {
- id: "vendorStatus",
- label: "Vendor Status",
- type: "multi-select",
- options: vendors.status.enumValues.map((status) => ({
- label: toSentenceCase(status),
- value: status,
- })),
- },
- { id: "rfqVendorUpdated", label: "Updated at", type: "date" },
- ]
-
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "rfqVendorUpdated", desc: true }],
- columnPinning: { right: ["comments"] },
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
-
-
- return (
- <div style={{ maxWidth: '80vw' }}>
- <DataTable
- table={table}
- >
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <VendorsTableToolbarActions table={table} rfqId={rfqId} />
- </DataTableAdvancedToolbar>
- </DataTable>
- <InviteVendorsDialog
- vendors={rowAction?.row.original ? [rowAction?.row.original] : []}
- onOpenChange={() => setRowAction(null)}
- rfqId={rfqId}
- open={rowAction?.type === "invite"}
- showTrigger={false}
- />
- <CommentSheet
- currentUserId={currentUserId}
- open={commentSheetOpen}
- onOpenChange={setCommentSheetOpen}
- rfqId={rfqId}
- tbeId={selectedTbeId ?? 0}
- vendorId={selectedVendorId ?? 0}
- initialComments={initialComments}
- />
-
- <TBEFileDialog
- isOpen={isFileDialogOpen}
- onOpenChange={setIsFileDialogOpen}
- tbeId={selectedTbeId ?? 0}
- vendorId={selectedVendorId ?? 0}
- rfqId={rfqId} // Use the prop directly instead of data[0]?.rfqId
- onRefresh={handleRefresh}
- />
-
- <TbeResultDialog
- open={rowAction?.type === "tbeResult"}
- onOpenChange={() => setRowAction(null)}
- tbe={rowAction?.row.original ?? null}
- />
-
- <VendorContactsDialog
- isOpen={isContactDialogOpen}
- onOpenChange={setIsContactDialogOpen}
- vendorId={selectedVendorId}
- vendor={selectedVendor}
- />
-
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/vendor-contact-dialog.tsx b/lib/rfqs/tbe-table/vendor-contact-dialog.tsx
deleted file mode 100644
index 3619fe77..00000000
--- a/lib/rfqs/tbe-table/vendor-contact-dialog.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-"use client"
-
-import * as React from "react"
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { VendorContactsTable } from "./vendor-contact/vendor-contact-table"
-import { Badge } from "@/components/ui/badge"
-import { VendorWithTbeFields } from "@/config/vendorTbeColumnsConfig"
-
-interface VendorContactsDialogProps {
- isOpen: boolean
- onOpenChange: (open: boolean) => void
- vendorId: number | null
- vendor: VendorWithTbeFields | null
-}
-
-export function VendorContactsDialog({
- isOpen,
- onOpenChange,
- vendorId,
- vendor,
-}: VendorContactsDialogProps) {
- 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>협력업체 연락처</DialogTitle>
- {vendor && (
- <div className="flex flex-col space-y-1 mt-2">
- <div className="text-sm text-muted-foreground">
- <span className="font-medium text-foreground">{vendor.vendorName}</span>
- {vendor.vendorCode && (
- <span className="ml-2 text-xs text-muted-foreground">({vendor.vendorCode})</span>
- )}
- </div>
- <div className="flex items-center">
- {vendor.vendorStatus && (
- <Badge variant="outline" className="mr-2">
- {vendor.vendorStatus}
- </Badge>
- )}
- {vendor.rfqVendorStatus && (
- <Badge
- variant={
- vendor.rfqVendorStatus === "INVITED" ? "default" :
- vendor.rfqVendorStatus === "DECLINED" ? "destructive" :
- vendor.rfqVendorStatus === "ACCEPTED" ? "secondary" : "outline"
- }
- >
- {vendor.rfqVendorStatus}
- </Badge>
- )}
- </div>
- </div>
- )}
- </div>
- </DialogHeader>
- {vendorId && (
- <div className="py-4">
- <VendorContactsTable vendorId={vendorId} />
- </div>
- )}
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table-column.tsx b/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table-column.tsx
deleted file mode 100644
index efc395b4..00000000
--- a/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table-column.tsx
+++ /dev/null
@@ -1,70 +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 { VendorData } from "./vendor-contact-table"
-
-
-/** getColumns: return array of ColumnDef for 'vendors' data */
-export function getColumns(): ColumnDef<VendorData>[] {
- return [
-
- // Vendor Name
- {
- accessorKey: "contactName",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Contact Name" />
- ),
- cell: ({ row }) => row.getValue("contactName"),
- },
-
- // Vendor Code
- {
- accessorKey: "contactPosition",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Position" />
- ),
- cell: ({ row }) => row.getValue("contactPosition"),
- },
-
- // Status
- {
- accessorKey: "contactEmail",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Email" />
- ),
- cell: ({ row }) => row.getValue("contactEmail"),
- },
-
- // Country
- {
- accessorKey: "contactPhone",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Phone" />
- ),
- cell: ({ row }) => row.getValue("contactPhone"),
- },
-
- // Created At
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Created At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date, "KR"),
- },
-
- // Updated At
- {
- accessorKey: "updatedAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Updated At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date, "KR"),
- },
- ]
-} \ No newline at end of file
diff --git a/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table.tsx b/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table.tsx
deleted file mode 100644
index c079da02..00000000
--- a/lib/rfqs/tbe-table/vendor-contact/vendor-contact-table.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-'use client'
-
-import * as React from "react"
-import { ClientDataTable } from "@/components/client-data-table/data-table"
-import { getColumns } from "./vendor-contact-table-column"
-import { DataTableAdvancedFilterField } from "@/types/table"
-import { Loader2 } from "lucide-react"
-import { useToast } from "@/hooks/use-toast"
-import { getVendorContactsByVendorId } from "../../service"
-
-export interface VendorData {
- id: number
- contactName: string
- contactPosition: string | null
- contactEmail: string
- contactPhone: string | null
- isPrimary: boolean | null
- createdAt: Date
- updatedAt: Date
-}
-
-interface VendorContactsTableProps {
- vendorId: number
-}
-
-export function VendorContactsTable({ vendorId }: VendorContactsTableProps) {
- const { toast } = useToast()
-
- const columns = React.useMemo(
- () => getColumns(),
- []
- )
-
- const [vendorContacts, setVendorContacts] = React.useState<VendorData[]>([])
- const [isLoading, setIsLoading] = React.useState(false)
-
- React.useEffect(() => {
- async function loadVendorContacts() {
- setIsLoading(true)
- try {
- const result = await getVendorContactsByVendorId(vendorId)
- if (result.success && result.data) {
- // undefined 체크 추가 및 타입 캐스팅
- setVendorContacts(result.data as VendorData[])
- } else {
- throw new Error(result.error || "Unknown error occurred")
- }
- } catch (error) {
- console.error("협력업체 연락처 로드 오류:", error)
- toast({
- title: "Error",
- description: "Failed to load vendor contacts",
- variant: "destructive",
- })
- } finally {
- setIsLoading(false)
- }
- }
- loadVendorContacts()
- }, [toast, vendorId])
-
- const advancedFilterFields: DataTableAdvancedFilterField<VendorData>[] = [
- { id: "contactName", label: "Contact Name", type: "text" },
- { id: "contactPosition", label: "Posiotion", type: "text" },
- { id: "contactEmail", label: "Email", type: "text" },
- { id: "contactPhone", label: "Phone", 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={vendorContacts}
- columns={columns}
- advancedFilterFields={advancedFilterFields}
- >
- </ClientDataTable>
- )
-} \ No newline at end of file