diff options
Diffstat (limited to 'lib/rfqs/vendor-table')
| -rw-r--r-- | lib/rfqs/vendor-table/comments-sheet.tsx | 25 | ||||
| -rw-r--r-- | lib/rfqs/vendor-table/vendors-table-columns.tsx | 120 | ||||
| -rw-r--r-- | lib/rfqs/vendor-table/vendors-table.tsx | 77 |
3 files changed, 140 insertions, 82 deletions
diff --git a/lib/rfqs/vendor-table/comments-sheet.tsx b/lib/rfqs/vendor-table/comments-sheet.tsx index 644869c6..3a2a9353 100644 --- a/lib/rfqs/vendor-table/comments-sheet.tsx +++ b/lib/rfqs/vendor-table/comments-sheet.tsx @@ -4,7 +4,7 @@ import * as React from "react" import { useForm, useFieldArray } from "react-hook-form" import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" -import { Loader, Download, X } from "lucide-react" +import { Download, X, Loader2 } from "lucide-react" import prettyBytes from "pretty-bytes" import { toast } from "sonner" @@ -52,6 +52,7 @@ export interface MatchedVendorComment { id: number commentText: string commentedBy?: number + commentedByEmail?: string createdAt?: Date attachments?: { id: number @@ -67,6 +68,7 @@ interface CommentSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { rfqId: number vendorId: number onCommentsUpdated?: (comments: MatchedVendorComment[]) => void + isLoading?: boolean // New prop } // 2) 폼 스키마 @@ -84,8 +86,12 @@ export function CommentSheet({ initialComments = [], currentUserId, onCommentsUpdated, + isLoading = false, // Default to false ...props }: CommentSheetProps) { + + console.log(initialComments) + const [comments, setComments] = React.useState<MatchedVendorComment[]>(initialComments) const [isPending, startTransition] = React.useTransition() @@ -108,6 +114,16 @@ export function CommentSheet({ // (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> } @@ -134,7 +150,7 @@ export function CommentSheet({ {c.attachments.map((att) => ( <div key={att.id} className="flex items-center gap-2"> <a - href={att.filePath} + href={`/api/rfq-download?path=${encodeURIComponent(att.filePath)}`} download target="_blank" rel="noreferrer" @@ -149,7 +165,7 @@ export function CommentSheet({ )} </TableCell> <TableCell> { c.createdAt ? formatDate(c.createdAt): "-"}</TableCell> - <TableCell>{c.commentedBy ?? "-"}</TableCell> + <TableCell>{c.commentedByEmail ?? "-"}</TableCell> </TableRow> ))} </TableBody> @@ -173,6 +189,7 @@ export function CommentSheet({ commentText: data.commentText, commentedBy: currentUserId, evaluationId: null, + cbeId: null, files: data.newFiles, }) @@ -291,7 +308,7 @@ export function CommentSheet({ </Button> </SheetClose> <Button disabled={isPending}> - {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />} + {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} Save </Button> </SheetFooter> diff --git a/lib/rfqs/vendor-table/vendors-table-columns.tsx b/lib/rfqs/vendor-table/vendors-table-columns.tsx index 1220cb9d..f152cec5 100644 --- a/lib/rfqs/vendor-table/vendors-table-columns.tsx +++ b/lib/rfqs/vendor-table/vendors-table-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { Ellipsis } from "lucide-react" +import { Ellipsis, MessageSquare } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" @@ -78,10 +78,6 @@ export function getColumns({ setRowAction, router, openCommentSheet }: GetColumn } // ---------------------------------------------------------------- - // 2) actions 컬럼 (Dropdown 메뉴) - // ---------------------------------------------------------------- - - // ---------------------------------------------------------------- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- // 3-1) groupMap: { [groupName]: ColumnDef<MatchedVendorRow>[] } @@ -149,19 +145,76 @@ export function getColumns({ setRowAction, router, openCommentSheet }: GetColumn 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", - // header: "Actions", cell: ({ row }) => { const rfq = row.original - const commCount = rfq.comments?.length ?? 0 const status = row.original.rfqVendorStatus + const isDisabled = !status || status === 'INVITED' || status === 'ACCEPTED' - // 공통 코멘트 핸들러 - function handleCommentClick() { - setRowAction({ row, type: "comments" }) - openCommentSheet(Number(row.original.id)) + 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> @@ -175,21 +228,12 @@ export function getColumns({ setRowAction, router, openCommentSheet }: GetColumn </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> - {/* 기존 기능: status가 INVITED일 때만 표시 */} {(!status || status === 'INVITED') && ( <DropdownMenuItem onSelect={() => setRowAction({ row, type: "invite" })}> 발행하기 </DropdownMenuItem> )} - {/* 두 기능 사이 구분선 */} - <DropdownMenuSeparator /> - {/* 코멘트 메뉴 항목 */} - <DropdownMenuItem onSelect={handleCommentClick}> - {commCount > 0 ? `${commCount} Comments` : "Add Comment"} - </DropdownMenuItem> - - </DropdownMenuContent> </DropdownMenu> ) @@ -199,37 +243,6 @@ export function getColumns({ setRowAction, router, openCommentSheet }: GetColumn enableHiding: false, } - - // const commentsColumn: ColumnDef<MatchedVendorRow> = { - // id: "comments", - // header: "Comments", - // cell: ({ row }) => { - // const rfq = row.original - // const commCount = rfq.comments?.length ?? 0 - - // // 공통 클릭 핸들러 - // function handleClick() { - // setRowAction({ row, type: "comments" }) - // openCommentSheet(Number(row.original.id)) - // } - - // return commCount > 0 ? ( - // <a - // href="#" - // onClick={(e) => { - // e.preventDefault() - // handleClick() - // }} - // > - // {commCount} Comments - // </a> - // ) : ( - // <Button size="sm" variant="outline" onClick={handleClick}> - // Add Comment - // </Button> - // ) - // }, - // } // ---------------------------------------------------------------- // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 // ---------------------------------------------------------------- @@ -252,13 +265,12 @@ export function getColumns({ setRowAction, router, openCommentSheet }: GetColumn }) // ---------------------------------------------------------------- - // 4) 최종 컬럼 배열: select, nestedColumns, actions + // 4) 최종 컬럼 배열: select, nestedColumns, comments, actions // ---------------------------------------------------------------- return [ selectColumn, ...nestedColumns, - // commentsColumn, + commentsColumn, actionsColumn - ] }
\ No newline at end of file diff --git a/lib/rfqs/vendor-table/vendors-table.tsx b/lib/rfqs/vendor-table/vendors-table.tsx index 838342bf..ae9cba41 100644 --- a/lib/rfqs/vendor-table/vendors-table.tsx +++ b/lib/rfqs/vendor-table/vendors-table.tsx @@ -2,6 +2,7 @@ import * as React from "react" import { useRouter } from "next/navigation" +import { useSession } from "next-auth/react" // Next-auth session hook 추가 import type { DataTableAdvancedFilterField, DataTableFilterField, @@ -22,6 +23,7 @@ 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>>]> @@ -29,8 +31,11 @@ interface VendorsTableProps { rfqType: RfqType } -export function MatchedVendorsTable({ promises, rfqId, rfqType}: VendorsTableProps) { +export function MatchedVendorsTable({ promises, rfqId, rfqType }: VendorsTableProps) { const { featureFlags } = useFeatureFlags() + const { data: session } = useSession() // 세션 정보 가져오기 + + // 1) Suspense로 받아온 데이터 const [{ data, pageCount }] = React.use(promises) @@ -47,10 +52,13 @@ export function MatchedVendorsTable({ promises, rfqId, rfqType}: VendorsTablePr const router = useRouter() // 3) CommentSheet 에 넣을 상태 - // => “댓글”은 MatchedVendorComment[] 로 관리해야 함 + // => "댓글"은 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) @@ -64,29 +72,42 @@ export function MatchedVendorsTable({ promises, rfqId, rfqType}: VendorsTablePr // 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 - 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) + + 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) } - - // (c) vendorId state - setSelectedVendorIdForComments(vendorId) - setCommentSheetOpen(true) } - + // 6) 컬럼 정의 (memo) const columns = React.useMemo( () => getColumns({ setRowAction, router, openCommentSheet }), @@ -140,9 +161,16 @@ export function MatchedVendorsTable({ promises, rfqId, rfqType}: VendorsTablePr clearOnDefault: true, }) + // 세션에서 userId 추출하고 숫자로 변환 + const currentUserId = session?.user?.id ? parseInt(session.user.id, 10) : 0 + + console.log(currentUserId,"currentUserId") + return ( - <> - <DataTable table={table}> + <div style={{ maxWidth: '80vw' }}> + <DataTable + table={table} + > <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields} @@ -169,13 +197,14 @@ export function MatchedVendorsTable({ promises, rfqId, rfqType}: VendorsTablePr initialComments={initialComments} rfqId={rfqId} vendorId={selectedVendorIdForComments ?? 0} - currentUserId={1} + currentUserId={currentUserId} + isLoading={isLoadingComments} // Pass the loading state onCommentsUpdated={(updatedComments) => { // Row 의 comments 필드도 업데이트 if (!rowAction?.row) return rowAction.row.original.comments = updatedComments }} /> - </> + </div> ) }
\ No newline at end of file |
