summaryrefslogtreecommitdiff
path: root/lib/rfqs/vendor-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/vendor-table')
-rw-r--r--lib/rfqs/vendor-table/add-vendor-dialog.tsx37
-rw-r--r--lib/rfqs/vendor-table/comments-sheet.tsx318
-rw-r--r--lib/rfqs/vendor-table/feature-flags-provider.tsx108
-rw-r--r--lib/rfqs/vendor-table/invite-vendors-dialog.tsx177
-rw-r--r--lib/rfqs/vendor-table/vendor-list/vendor-list-table-column.tsx154
-rw-r--r--lib/rfqs/vendor-table/vendor-list/vendor-list-table.tsx142
-rw-r--r--lib/rfqs/vendor-table/vendors-table-columns.tsx276
-rw-r--r--lib/rfqs/vendor-table/vendors-table-floating-bar.tsx137
-rw-r--r--lib/rfqs/vendor-table/vendors-table-toolbar-actions.tsx84
-rw-r--r--lib/rfqs/vendor-table/vendors-table.tsx208
10 files changed, 0 insertions, 1641 deletions
diff --git a/lib/rfqs/vendor-table/add-vendor-dialog.tsx b/lib/rfqs/vendor-table/add-vendor-dialog.tsx
deleted file mode 100644
index 8ec5b9f4..00000000
--- a/lib/rfqs/vendor-table/add-vendor-dialog.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
-import { Button } from "@/components/ui/button"
-import { VendorsListTable } from "./vendor-list/vendor-list-table"
-
-interface VendorsListTableProps {
- rfqId: number // so we know which RFQ to insert into
- }
-
-
-/**
- * A dialog that contains a client-side table or infinite scroll
- * for "all vendors," allowing the user to select vendors and add them to the RFQ.
- */
-export function AddVendorDialog({ rfqId }: VendorsListTableProps) {
- const [open, setOpen] = React.useState(false)
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>
- <Button size="sm">
- Add Vendor
- </Button>
- </DialogTrigger>
- <DialogContent className="max-w-[90wv] sm:max-h-[80vh] overflow-auto" style={{maxWidth:1600, height:680}}>
- <DialogHeader>
- <DialogTitle>Add Vendor to RFQ</DialogTitle>
- </DialogHeader>
-
- <VendorsListTable rfqId={rfqId}/>
-
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/vendor-table/comments-sheet.tsx b/lib/rfqs/vendor-table/comments-sheet.tsx
deleted file mode 100644
index 441fdcf1..00000000
--- a/lib/rfqs/vendor-table/comments-sheet.tsx
+++ /dev/null
@@ -1,318 +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 MatchedVendorComment {
- 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?: MatchedVendorComment[]
- currentUserId: number
- rfqId: number
- vendorId: number
- onCommentsUpdated?: (comments: MatchedVendorComment[]) => 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,
- onCommentsUpdated,
- isLoading = false, // Default to false
- ...props
-}: CommentSheetProps) {
-
- const [comments, setComments] = React.useState<MatchedVendorComment[]>(initialComments)
- const [isPending, startTransition] = React.useTransition()
-
- React.useEffect(() => {
- setComments(initialComments)
- }, [initialComments])
-
- const form = useForm<CommentFormValues>({
- resolver: zodResolver(commentFormSchema),
- defaultValues: {
- commentText: "",
- newFiles: [],
- },
- })
-
- const { fields: newFileFields, append, remove } = useFieldArray({
- control: form.control,
- name: "newFiles",
- })
-
- // (A) 기존 코멘트 렌더링
- function renderExistingComments() {
-
- if (isLoading) {
- return (
- <div className="flex justify-center items-center h-32">
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
- <span className="ml-2 text-sm text-muted-foreground">Loading comments...</span>
- </div>
- )
- }
-
- if (comments.length === 0) {
- return <p className="text-sm text-muted-foreground">No comments yet</p>
- }
- return (
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-1/2">Comment</TableHead>
- <TableHead>Attachments</TableHead>
- <TableHead>Created At</TableHead>
- <TableHead>Created By</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {comments.map((c) => (
- <TableRow key={c.id}>
- <TableCell>{c.commentText}</TableCell>
- <TableCell>
- {!c.attachments?.length && (
- <span className="text-sm text-muted-foreground">No files</span>
- )}
- {c.attachments?.length && (
- <div className="flex flex-col gap-1">
- {c.attachments.map((att) => (
- <div key={att.id} className="flex items-center gap-2">
- <a
- href={`/api/rfq-download?path=${encodeURIComponent(att.filePath)}`}
- download
- target="_blank"
- rel="noreferrer"
- className="inline-flex items-center gap-1 text-blue-600 underline"
- >
- <Download className="h-4 w-4" />
- {att.fileName}
- </a>
- </div>
- ))}
- </div>
- )}
- </TableCell>
- <TableCell> {c.createdAt ? formatDate(c.createdAt) : "-"}</TableCell>
- <TableCell>{c.commentedByEmail ?? "-"}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- )
- }
-
- // (B) 파일 드롭
- function handleDropAccepted(files: File[]) {
- append(files)
- }
-
- // (C) Submit
- async function onSubmit(data: CommentFormValues) {
- if (!rfqId) return
- startTransition(async () => {
- try {
- const res = await createRfqCommentWithAttachments({
- rfqId,
- vendorId,
- commentText: data.commentText,
- commentedBy: currentUserId,
- evaluationId: null,
- cbeId: null,
- files: data.newFiles,
- })
-
- if (!res.ok) {
- throw new Error("Failed to create comment")
- }
-
- toast.success("Comment created")
-
- // 임시로 새 코멘트 추가
- const newComment: MatchedVendorComment = {
- 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/vendor-table/feature-flags-provider.tsx b/lib/rfqs/vendor-table/feature-flags-provider.tsx
deleted file mode 100644
index 81131894..00000000
--- a/lib/rfqs/vendor-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/vendor-table/invite-vendors-dialog.tsx b/lib/rfqs/vendor-table/invite-vendors-dialog.tsx
deleted file mode 100644
index 23853e2f..00000000
--- a/lib/rfqs/vendor-table/invite-vendors-dialog.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Row } from "@tanstack/react-table"
-import { Loader, Send, Trash, AlertTriangle } 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 { Alert, AlertDescription } from "@/components/ui/alert"
-
-import { MatchedVendorRow } from "@/config/vendorRfbColumnsConfig"
-import { inviteVendors } from "../service"
-import { RfqType } from "@/lib/rfqs/validations"
-
-interface DeleteTasksDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- vendors: Row<MatchedVendorRow>["original"][]
- rfqId:number
- rfqType: RfqType
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function InviteVendorsDialog({
- vendors,
- rfqId,
- rfqType,
- showTrigger = true,
- onSuccess,
- ...props
-}: DeleteTasksDialogProps) {
- const [isInvitePending, startInviteTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onDelete() {
- startInviteTransition(async () => {
- const { error } = await inviteVendors({
- rfqId,
- vendorIds: vendors.map((vendor) => Number(vendor.id)),
- rfqType
- })
-
- if (error) {
- toast.error(error)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success("Vendor invited")
- onSuccess?.()
- })
- }
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Send className="mr-2 size-4" aria-hidden="true" />
- Invite ({vendors.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Are you absolutely sure?</DialogTitle>
- <DialogDescription>
- This action cannot be undone. This will permanently invite{" "}
- <span className="font-medium">{vendors.length}</span>
- {vendors.length === 1 ? " vendor" : " vendors"}.
- </DialogDescription>
- </DialogHeader>
-
- {/* 편집 제한 경고 메시지 */}
- <Alert variant="destructive" className="mt-4">
- <AlertTriangle className="h-4 w-4" />
- <AlertDescription className="font-medium">
- 한 업체라도 초대를 하고 나면 아이템 편집과 RFQ 문서 첨부 편집은 불가능합니다.
- </AlertDescription>
- </Alert>
-
- <DialogFooter className="gap-2 sm:space-x-0 mt-6">
- <DialogClose asChild>
- <Button variant="outline">Cancel</Button>
- </DialogClose>
- <Button
- aria-label="Invite selected rows"
- variant="destructive"
- onClick={onDelete}
- disabled={isInvitePending}
- >
- {isInvitePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- Invite
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- Invite ({vendors.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>Are you absolutely sure?</DrawerTitle>
- <DrawerDescription>
- This action cannot be undone. This will permanently invite {" "}
- <span className="font-medium">{vendors.length}</span>
- {vendors.length === 1 ? " vendor" : " vendors"} from our servers.
- </DrawerDescription>
- </DrawerHeader>
-
- {/* 편집 제한 경고 메시지 (모바일용) */}
- <div className="px-4">
- <Alert variant="destructive">
- <AlertTriangle className="h-4 w-4" />
- <AlertDescription className="font-medium">
- 한 업체라도 초대를 하고 나면 아이템 편집과 RFQ 문서 첨부 편집은 불가능합니다.
- </AlertDescription>
- </Alert>
- </div>
-
- <DrawerFooter className="gap-2 sm:space-x-0 mt-4">
- <DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
- </DrawerClose>
- <Button
- aria-label="Delete selected rows"
- variant="destructive"
- onClick={onDelete}
- disabled={isInvitePending}
- >
- {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/vendor-table/vendor-list/vendor-list-table-column.tsx b/lib/rfqs/vendor-table/vendor-list/vendor-list-table-column.tsx
deleted file mode 100644
index bfcbe75b..00000000
--- a/lib/rfqs/vendor-table/vendor-list/vendor-list-table-column.tsx
+++ /dev/null
@@ -1,154 +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 { VendorData } from "./vendor-list-table"
-import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"
-import { formatDate } from "@/lib/utils"
-import { Checkbox } from "@/components/ui/checkbox"
-
-export interface DataTableRowAction<TData> {
- row: Row<TData>
- type: "open" | "update" | "delete"
-}
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorData> | null>>
- setSelectedVendorIds: React.Dispatch<React.SetStateAction<number[]>> // Changed to array
-}
-
-/** getColumns: return array of ColumnDef for 'vendors' data */
-export function getColumns({
- setRowAction,
- setSelectedVendorIds, // Changed parameter name
-}: GetColumnsProps): ColumnDef<VendorData>[] {
- return [
- // MULTIPLE SELECT COLUMN
- {
- id: "select",
- enableSorting: false,
- enableHiding: false,
- size: 40,
- // Add checkbox in header for select all functionality
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getFilteredSelectedRowModel().rows.length > 0 &&
- table.getFilteredSelectedRowModel().rows.length === table.getFilteredRowModel().rows.length
- }
- onCheckedChange={(checked) => {
- table.toggleAllRowsSelected(!!checked)
-
- // Update selectedVendorIds based on all rows selection
- if (checked) {
- const allIds = table.getFilteredRowModel().rows.map(row => row.original.id)
- setSelectedVendorIds(allIds)
- } else {
- setSelectedVendorIds([])
- }
- }}
- aria-label="Select all"
- />
- ),
- cell: ({ row }) => {
- const isSelected = row.getIsSelected()
-
- return (
- <Checkbox
- checked={isSelected}
- onCheckedChange={(checked) => {
- row.toggleSelected(!!checked)
-
- // Update the selectedVendorIds state by adding or removing this ID
- setSelectedVendorIds(prevIds => {
- if (checked) {
- // Add this ID if it doesn't exist
- return prevIds.includes(row.original.id)
- ? prevIds
- : [...prevIds, row.original.id]
- } else {
- // Remove this ID
- return prevIds.filter(id => id !== row.original.id)
- }
- })
- }}
- aria-label="Select row"
- />
- )
- },
- },
-
- // Vendor Name
- {
- accessorKey: "vendorName",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Vendor Name" />
- ),
- cell: ({ row }) => row.getValue("vendorName"),
- },
-
- // Vendor Code
- {
- accessorKey: "vendorCode",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Vendor Code" />
- ),
- cell: ({ row }) => row.getValue("vendorCode"),
- },
-
- // Status
- {
- accessorKey: "status",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Status" />
- ),
- cell: ({ row }) => row.getValue("status"),
- },
-
- // Country
- {
- accessorKey: "country",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Country" />
- ),
- cell: ({ row }) => row.getValue("country"),
- },
-
- // Email
- {
- accessorKey: "email",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Email" />
- ),
- cell: ({ row }) => row.getValue("email"),
- },
-
- // Phone
- {
- accessorKey: "phone",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Phone" />
- ),
- cell: ({ row }) => row.getValue("phone"),
- },
-
- // Created At
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Created At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date),
- },
-
- // Updated At
- {
- accessorKey: "updatedAt",
- header: ({ column }) => (
- <ClientDataTableColumnHeaderSimple column={column} title="Updated At" />
- ),
- cell: ({ cell }) => formatDate(cell.getValue() as Date),
- },
- ]
-} \ No newline at end of file
diff --git a/lib/rfqs/vendor-table/vendor-list/vendor-list-table.tsx b/lib/rfqs/vendor-table/vendor-list/vendor-list-table.tsx
deleted file mode 100644
index e34a5052..00000000
--- a/lib/rfqs/vendor-table/vendor-list/vendor-list-table.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { ClientDataTable } from "@/components/client-data-table/data-table"
-import { DataTableRowAction, getColumns } from "./vendor-list-table-column"
-import { DataTableAdvancedFilterField } from "@/types/table"
-import { addItemToVendors, getAllVendors } from "../../service"
-import { Loader2, Plus } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import { useToast } from "@/hooks/use-toast"
-
-export interface VendorData {
- id: number
- vendorName: string
- vendorCode: string | null
- taxId: string
- address: string | null
- country: string | null
- phone: string | null
- email: string | null
- website: string | null
- status: string
- createdAt: Date
- updatedAt: Date
-}
-
-interface VendorsListTableProps {
- rfqId: number
-}
-
-export function VendorsListTable({ rfqId }: VendorsListTableProps) {
- const { toast } = useToast()
- const [rowAction, setRowAction] =
- React.useState<DataTableRowAction<VendorData> | null>(null)
-
- // Changed to array for multiple selection
- const [selectedVendorIds, setSelectedVendorIds] = React.useState<number[]>([])
- const [isSubmitting, setIsSubmitting] = React.useState(false)
-
- const columns = React.useMemo(
- () => getColumns({ setRowAction, setSelectedVendorIds }),
- [setRowAction, setSelectedVendorIds]
- )
-
- const [vendors, setVendors] = React.useState<VendorData[]>([])
- const [isLoading, setIsLoading] = React.useState(false)
-
- React.useEffect(() => {
- async function loadAllVendors() {
- setIsLoading(true)
- try {
- const allVendors = await getAllVendors()
- setVendors(allVendors)
- } catch (error) {
- console.error("협력업체 목록 로드 오류:", error)
- toast({
- title: "Error",
- description: "Failed to load vendors",
- variant: "destructive",
- })
- } finally {
- setIsLoading(false)
- }
- }
- loadAllVendors()
- }, [toast])
-
- const advancedFilterFields: DataTableAdvancedFilterField<VendorData>[] = []
-
- async function handleAddVendors() {
- if (selectedVendorIds.length === 0) return // Safety check
-
- setIsSubmitting(true)
- try {
- // Update to use the multiple vendor service
- const result = await addItemToVendors(rfqId, selectedVendorIds)
-
- if (result.success) {
- toast({
- title: "Success",
- description: `Added items to ${selectedVendorIds.length} vendors`,
- })
- // Reset selection after successful addition
- setSelectedVendorIds([])
- } else {
- toast({
- title: "Error",
- description: result.error || "Failed to add items to vendors",
- variant: "destructive",
- })
- }
- } catch (err) {
- console.error("Failed to add vendors:", err)
- toast({
- title: "Error",
- description: "An unexpected error occurred",
- variant: "destructive",
- })
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 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={vendors}
- columns={columns}
- advancedFilterFields={advancedFilterFields}
- >
- <div className="flex items-center gap-2">
- <Button
- variant="default"
- size="sm"
- onClick={handleAddVendors}
- disabled={selectedVendorIds.length === 0 || isSubmitting}
- >
- {isSubmitting ? (
- <>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- Adding...
- </>
- ) : (
- <>
- <Plus className="mr-2 h-4 w-4" />
- Add Vendors ({selectedVendorIds.length})
- </>
- )}
- </Button>
- </div>
- </ClientDataTable>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/vendor-table/vendors-table-columns.tsx b/lib/rfqs/vendor-table/vendors-table-columns.tsx
deleted file mode 100644
index f152cec5..00000000
--- a/lib/rfqs/vendor-table/vendors-table-columns.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { 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 {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
-import { useRouter } from "next/navigation"
-
-import { vendors } from "@/db/schema/vendors"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { vendorColumnsConfig } from "@/config/vendorColumnsConfig"
-import { Separator } from "@/components/ui/separator"
-import { MatchedVendorRow, vendorRfqColumnsConfig } from "@/config/vendorRfbColumnsConfig"
-
-
-type NextRouter = ReturnType<typeof useRouter>;
-
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<MatchedVendorRow> | null>>;
- router: NextRouter;
- openCommentSheet: (rfqId: number) => void;
-
-}
-
-/**
- * tanstack table 컬럼 정의 (중첩 헤더 버전)
- */
-export function getColumns({ setRowAction, router, openCommentSheet }: GetColumnsProps): ColumnDef<MatchedVendorRow>[] {
- // ----------------------------------------------------------------
- // 1) select 컬럼 (체크박스)
- // ----------------------------------------------------------------
- const selectColumn: ColumnDef<MatchedVendorRow> = {
- 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,
- }
-
- // ----------------------------------------------------------------
- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
- // ----------------------------------------------------------------
- // 3-1) groupMap: { [groupName]: ColumnDef<MatchedVendorRow>[] }
- const groupMap: Record<string, ColumnDef<MatchedVendorRow>[]> = {}
-
- vendorRfqColumnsConfig.forEach((cfg) => {
- // 만약 group가 없으면 "_noGroup" 처리
- const groupName = cfg.group || "_noGroup"
-
- if (!groupMap[groupName]) {
- groupMap[groupName] = []
- }
-
- // child column 정의
- const childCol: ColumnDef<MatchedVendorRow> = {
- 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, cell }) => {
-
-
- 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 === "REJECTED" ? "destructive" : statusVal === "ACCEPTED" ? "secondary" : "outline"
- return (
- <Badge variant={variant}>
- {statusVal}
- </Badge>
- )
- }
-
-
- if (cfg.id === "rfqVendorUpdated") {
- const dateVal = cell.getValue() as Date
- if (!dateVal) return null
- return formatDate(dateVal)
- }
-
-
- // code etc...
- return row.getValue(cfg.id) ?? ""
- },
- }
-
- groupMap[groupName].push(childCol)
- })
-
- const commentsColumn: ColumnDef<MatchedVendorRow> = {
- 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(Number(vendor.id) ?? 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
- }
-
- const actionsColumn: ColumnDef<MatchedVendorRow> = {
- id: "actions",
- cell: ({ row }) => {
- const rfq = row.original
- const status = row.original.rfqVendorStatus
- const isDisabled = !status || status === 'INVITED' || status === 'ACCEPTED'
-
- if (isDisabled) {
- return (
- <div className="relative group">
- <Button
- aria-label="Actions disabled"
- variant="ghost"
- className="flex size-8 p-0 opacity-50 cursor-not-allowed"
- disabled
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- {/* Tooltip explaining why it's disabled */}
- <div className="absolute hidden group-hover:block right-0 -bottom-8 bg-popover text-popover-foreground text-xs p-2 rounded shadow-md whitespace-nowrap z-50">
- 초대 상태에서는 사용할 수 없습니다
- </div>
- </div>
- )
- }
-
-
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- aria-label="Open menu"
- variant="ghost"
- className="flex size-8 p-0 data-[state=open]:bg-muted"
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-40">
- {/* 기존 기능: status가 INVITED일 때만 표시 */}
- {(!status || status === 'INVITED') && (
- <DropdownMenuItem onSelect={() => setRowAction({ row, type: "invite" })}>
- 발행하기
- </DropdownMenuItem>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- )
- },
- size: 40,
- enableSorting: false,
- enableHiding: false,
- }
-
- // ----------------------------------------------------------------
- // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기
- // ----------------------------------------------------------------
- const nestedColumns: ColumnDef<MatchedVendorRow>[] = []
-
- // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함
- // 여기서는 그냥 Object.entries 순서
- Object.entries(groupMap).forEach(([groupName, colDefs]) => {
- if (groupName === "_noGroup") {
- // 그룹 없음 → 그냥 최상위 레벨 컬럼
- nestedColumns.push(...colDefs)
- } else {
- // 상위 컬럼
- nestedColumns.push({
- id: groupName,
- header: groupName, // "Basic Info", "Metadata" 등
- columns: colDefs,
- })
- }
- })
-
- // ----------------------------------------------------------------
- // 4) 최종 컬럼 배열: select, nestedColumns, comments, actions
- // ----------------------------------------------------------------
- return [
- selectColumn,
- ...nestedColumns,
- commentsColumn,
- actionsColumn
- ]
-} \ No newline at end of file
diff --git a/lib/rfqs/vendor-table/vendors-table-floating-bar.tsx b/lib/rfqs/vendor-table/vendors-table-floating-bar.tsx
deleted file mode 100644
index 9b32cf5f..00000000
--- a/lib/rfqs/vendor-table/vendors-table-floating-bar.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { SelectTrigger } from "@radix-ui/react-select"
-import { type Table } from "@tanstack/react-table"
-import {
- ArrowUp,
- CheckCircle2,
- Download,
- Loader,
- Trash2,
- X,
-} from "lucide-react"
-import { toast } from "sonner"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import { Portal } from "@/components/ui/portal"
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
-} from "@/components/ui/select"
-import { Separator } from "@/components/ui/separator"
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-import { Kbd } from "@/components/kbd"
-
-import { ActionConfirmDialog } from "@/components/ui/action-dialog"
-import { vendors } from "@/db/schema/vendors"
-import { MatchedVendorRow } from "@/config/vendorRfbColumnsConfig"
-
-interface VendorsTableFloatingBarProps {
- table: Table<MatchedVendorRow>
-}
-
-
-export function VendorsTableFloatingBar({ table }: VendorsTableFloatingBarProps) {
- const rows = table.getFilteredSelectedRowModel().rows
-
- const [isPending, startTransition] = React.useTransition()
- const [action, setAction] = React.useState<
- "update-status" | "export" | "delete"
- >()
- const [popoverOpen, setPopoverOpen] = React.useState(false)
-
- // Clear selection on Escape key press
- React.useEffect(() => {
- function handleKeyDown(event: KeyboardEvent) {
- if (event.key === "Escape") {
- table.toggleAllRowsSelected(false)
- }
- }
-
- window.addEventListener("keydown", handleKeyDown)
- return () => window.removeEventListener("keydown", handleKeyDown)
- }, [table])
-
-
-
- // 공용 confirm dialog state
- const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
- const [confirmProps, setConfirmProps] = React.useState<{
- title: string
- description?: string
- onConfirm: () => Promise<void> | void
- }>({
- title: "",
- description: "",
- onConfirm: () => { },
- })
-
-
-
-
-
- return (
- <Portal >
- <div className="fixed inset-x-0 bottom-10 z-50 mx-auto w-fit px-2.5" style={{ bottom: '1.5rem' }}>
- <div className="w-full overflow-x-auto">
- <div className="mx-auto flex w-fit items-center gap-2 rounded-md border bg-background p-2 text-foreground shadow">
- <div className="flex h-7 items-center rounded-md border border-dashed pl-2.5 pr-1">
- <span className="whitespace-nowrap text-xs">
- {rows.length} selected
- </span>
- <Separator orientation="vertical" className="ml-2 mr-1" />
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- className="size-5 hover:border"
- onClick={() => table.toggleAllRowsSelected(false)}
- >
- <X className="size-3.5 shrink-0" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent className="flex items-center border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900">
- <p className="mr-2">Clear selection</p>
- <Kbd abbrTitle="Escape" variant="outline">
- Esc
- </Kbd>
- </TooltipContent>
- </Tooltip>
- </div>
-
- </div>
- </div>
- </div>
-
-
- {/* 공용 Confirm Dialog */}
- <ActionConfirmDialog
- open={confirmDialogOpen}
- onOpenChange={setConfirmDialogOpen}
- title={confirmProps.title}
- description={confirmProps.description}
- onConfirm={confirmProps.onConfirm}
- isLoading={isPending && (action === "delete" || action === "update-status")}
- confirmLabel={
- action === "delete"
- ? "Delete"
- : action === "update-status"
- ? "Update"
- : "Confirm"
- }
- confirmVariant={
- action === "delete" ? "destructive" : "default"
- }
- />
- </Portal>
- )
-}
diff --git a/lib/rfqs/vendor-table/vendors-table-toolbar-actions.tsx b/lib/rfqs/vendor-table/vendors-table-toolbar-actions.tsx
deleted file mode 100644
index 864d0f4b..00000000
--- a/lib/rfqs/vendor-table/vendors-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-
-import { MatchedVendorRow } from "@/config/vendorRfbColumnsConfig"
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-import { AddVendorDialog } from "./add-vendor-dialog"
-import { Button } from "@/components/ui/button"
-import { useToast } from "@/hooks/use-toast"
-
-interface VendorsTableToolbarActionsProps {
- table: Table<MatchedVendorRow>
- rfqId: number
-}
-
-export function VendorsTableToolbarActions({ table, rfqId }: VendorsTableToolbarActionsProps) {
- const { toast } = useToast()
- const fileInputRef = React.useRef<HTMLInputElement>(null)
-
- // 선택된 모든 행
- const selectedRows = table.getFilteredSelectedRowModel().rows
-
- // 조건에 맞는 협력업체만 필터링
- const eligibleVendors = React.useMemo(() => {
- return selectedRows
- .map(row => row.original)
- .filter(vendor => !vendor.rfqVendorStatus || vendor.rfqVendorStatus === "INVITED")
- }, [selectedRows])
-
- // 조건에 맞지 않는 협력업체 수
- const ineligibleCount = selectedRows.length - eligibleVendors.length
-
- function handleImportClick() {
- fileInputRef.current?.click()
- }
-
- function handleInviteClick() {
- // 조건에 맞지 않는 협력업체가 있다면 토스트 메시지 표시
- if (ineligibleCount > 0) {
- toast({
- title: "일부 협력업체만 초대됩니다",
- description: `선택한 ${selectedRows.length}개 중 ${eligibleVendors.length}개만 초대 가능합니다. 나머지 ${ineligibleCount}개는 초대 불가능한 상태입니다.`,
- // variant: "warning",
- })
- }
- }
-
- // 다이얼로그 표시 여부 - 적합한 협력업체가 1개 이상 있으면 표시
- const showInviteDialog = eligibleVendors.length > 0
-
- return (
- <div className="flex items-center gap-2">
- {selectedRows.length > 0 && (
- <>
- {showInviteDialog ? (
- <InviteVendorsDialog
- vendors={eligibleVendors}
- rfqId={rfqId}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- onOpenChange={(open) => {
- // 다이얼로그가 열릴 때만 경고 표시
- if (open && ineligibleCount > 0) {
- handleInviteClick()
- }
- }}
- />
- ) : (
- <Button
- variant="default"
- size="sm"
- disabled={true}
- title="선택된 협력업체 중 초대 가능한 협력업체가 없습니다"
- >
- 초대 불가
- </Button>
- )}
- </>
- )}
-
- <AddVendorDialog rfqId={rfqId} />
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/rfqs/vendor-table/vendors-table.tsx b/lib/rfqs/vendor-table/vendors-table.tsx
deleted file mode 100644
index b2e4d5ad..00000000
--- a/lib/rfqs/vendor-table/vendors-table.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import { useSession } from "next-auth/react" // Next-auth session hook 추가
-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 "./vendors-table-columns"
-import { vendors } from "@/db/schema/vendors"
-import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions"
-import { VendorsTableFloatingBar } from "./vendors-table-floating-bar"
-import { fetchRfqAttachmentsbyCommentId, getMatchedVendors } from "../service"
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-import { CommentSheet, MatchedVendorComment } from "./comments-sheet"
-import { MatchedVendorRow } from "@/config/vendorRfbColumnsConfig"
-import { RfqType } from "@/lib/rfqs/validations"
-import { toast } from "sonner"
-
-interface VendorsTableProps {
- promises: Promise<[Awaited<ReturnType<typeof getMatchedVendors>>]>
- rfqId: number
- rfqType: RfqType
-}
-
-export function MatchedVendorsTable({ promises, rfqId, rfqType }: VendorsTableProps) {
- const { featureFlags } = useFeatureFlags()
- const { data: session } = useSession() // 세션 정보 가져오기
-
-
-
- // 1) Suspense로 받아온 데이터
- const [{ data, pageCount }] = React.use(promises)
- // data는 MatchedVendorRow[] 형태 (getMatchedVendors에서 반환)
-
- console.log(data)
-
- // 2) Row 액션 상태
- const [rowAction, setRowAction] = React.useState<
- DataTableRowAction<MatchedVendorRow> | null
- >(null)
-
- // **router** 획득
- const router = useRouter()
-
- // 3) CommentSheet 에 넣을 상태
- // => "댓글"은 MatchedVendorComment[] 로 관리해야 함
- const [initialComments, setInitialComments] = React.useState<
- MatchedVendorComment[]
- >([])
-
- const [isLoadingComments, setIsLoadingComments] = React.useState(false)
-
- const [commentSheetOpen, setCommentSheetOpen] = React.useState(false)
- const [selectedVendorIdForComments, setSelectedVendorIdForComments] =
- React.useState<number | null>(null)
-
- // 4) rowAction이 바뀌면, type이 "comments"인지 확인 후 open
- React.useEffect(() => {
- if (rowAction?.type === "comments") {
- openCommentSheet(rowAction.row.original.id)
- }
- }, [rowAction])
-
- // 5) 댓글 시트 오픈 함수
- async function openCommentSheet(vendorId: number) {
- // Clear previous comments
- setInitialComments([])
-
- // Start loading
- setIsLoadingComments(true)
-
- // Open the sheet immediately with loading state
- setSelectedVendorIdForComments(vendorId)
- setCommentSheetOpen(true)
-
- // (a) 현재 Row의 comments 불러옴
- const comments = rowAction?.row.original.comments
-
- try {
- if (comments && comments.length > 0) {
- // (b) 각 comment마다 첨부파일 fetch
- const commentWithAttachments: MatchedVendorComment[] = await Promise.all(
- comments.map(async (c) => {
- const attachments = await fetchRfqAttachmentsbyCommentId(c.id)
- return {
- ...c,
- attachments,
- }
- })
- )
- setInitialComments(commentWithAttachments)
- }
- } catch (error) {
- console.error("Error loading comments:", error)
- toast.error("Failed to load comments")
- } finally {
- // End loading regardless of success/failure
- setIsLoadingComments(false)
- }
- }
-
- // 6) 컬럼 정의 (memo)
- const columns = React.useMemo(
- () => getColumns({ setRowAction, router, openCommentSheet }),
- [setRowAction, router]
- )
-
- // 7) 필터 정의
- const filterFields: DataTableFilterField<MatchedVendorRow>[] = []
-
- const advancedFilterFields: DataTableAdvancedFilterField<MatchedVendorRow>[] = [
- { 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: "rfqVendorStatus",
- label: "RFQ Status",
- type: "multi-select",
- options: ["INVITED", "ACCEPTED", "REJECTED", "QUOTED"].map((s) => ({
- label: s,
- value: s,
- })),
- },
- { id: "rfqVendorUpdated", label: "Updated at", type: "date" },
- ]
-
- // 8) 테이블 생성
- const { table } = useDataTable({
- data, // MatchedVendorRow[]
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "rfqVendorUpdated", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- // 행의 고유 ID
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- // 세션에서 userId 추출하고 숫자로 변환
- const currentUserId = session?.user?.id ? parseInt(session.user.id, 10) : 0
-
- return (
- <>
- <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}
- rfqType={rfqType}
- />
-
- {/* 댓글 시트 */}
- <CommentSheet
- open={commentSheetOpen}
- onOpenChange={setCommentSheetOpen}
- initialComments={initialComments}
- rfqId={rfqId}
- vendorId={selectedVendorIdForComments ?? 0}
- currentUserId={currentUserId}
- isLoading={isLoadingComments} // Pass the loading state
- onCommentsUpdated={(updatedComments) => {
- // Row 의 comments 필드도 업데이트
- if (!rowAction?.row) return
- rowAction.row.original.comments = updatedComments
- }}
- />
- </>
- )
-} \ No newline at end of file