summaryrefslogtreecommitdiff
path: root/lib/vendor-rfq-response/vendor-tbe-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-rfq-response/vendor-tbe-table')
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx14
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/feature-flags-provider.tsx108
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx86
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx65
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx72
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx4
6 files changed, 199 insertions, 150 deletions
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx b/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx
index 1eee54f5..e0bf9727 100644
--- a/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx
+++ b/lib/vendor-rfq-response/vendor-tbe-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 { Loader, Download, X, Loader2 } from "lucide-react"
import prettyBytes from "pretty-bytes"
import { toast } from "sonner"
@@ -79,6 +79,8 @@ interface CommentSheetProps extends React.ComponentPropsWithRef<typeof Sheet> {
vendorId:number
/** 댓글 저장 후 갱신용 콜백 (옵션) */
onCommentsUpdated?: (comments: TbeComment[]) => void
+ isLoading?: boolean // New prop
+
}
// 새 코멘트 작성 폼 스키마
@@ -96,6 +98,7 @@ export function CommentSheet({
initialComments = [],
currentUserId,
onCommentsUpdated,
+ isLoading = false, // Default to false
...props
}: CommentSheetProps) {
const [comments, setComments] = React.useState<TbeComment[]>(initialComments)
@@ -125,6 +128,15 @@ export function CommentSheet({
// 간단히 테이블 하나로 표현
// 실제로는 Bubble 형태의 UI, Accordion, Timeline 등 다양하게 구성할 수 있음
function renderExistingComments() {
+ if (isLoading) {
+ return (
+ <div className="flex justify-center items-center h-32">
+ <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+ <span className="ml-2 text-sm text-muted-foreground">Loading comments...</span>
+ </div>
+ )
+ }
+
if (comments.length === 0) {
return <p className="text-sm text-muted-foreground">No comments yet</p>
}
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/feature-flags-provider.tsx b/lib/vendor-rfq-response/vendor-tbe-table/feature-flags-provider.tsx
deleted file mode 100644
index 81131894..00000000
--- a/lib/vendor-rfq-response/vendor-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/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx b/lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx
new file mode 100644
index 00000000..2056a48f
--- /dev/null
+++ b/lib/vendor-rfq-response/vendor-tbe-table/rfq-detail-dialog.tsx
@@ -0,0 +1,86 @@
+"use client"
+
+import * as React from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Badge } from "@/components/ui/badge"
+import { formatDateTime } from "@/lib/utils"
+import { CalendarClock } from "lucide-react"
+import { RfqItemsTable } from "../vendor-cbe-table/rfq-items-table/rfq-items-table"
+import { TbeVendorFields } from "@/config/vendorTbeColumnsConfig"
+
+interface RfqDeailDialogProps {
+ isOpen: boolean
+ onOpenChange: (open: boolean) => void
+ rfqId: number | null
+ rfq: TbeVendorFields | null
+}
+
+export function RfqDeailDialog({
+ isOpen,
+ onOpenChange,
+ rfqId,
+ rfq,
+}: RfqDeailDialogProps) {
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-[90wv] sm:max-h-[80vh] overflow-auto" style={{maxWidth:1000, height:480}}>
+ <DialogHeader>
+ <div className="flex flex-col space-y-2">
+ <DialogTitle>{rfq && rfq.rfqCode} Detail</DialogTitle>
+ {rfq && (
+ <div className="flex flex-col space-y-3 mt-2">
+ <div className="text-sm text-muted-foreground">
+ <span className="font-medium text-foreground">{rfq.rfqDescription && rfq.rfqDescription}</span>
+ </div>
+
+ {/* 정보를 두 행으로 나누어 표시 */}
+ <div className="flex flex-col space-y-2 sm:space-y-0 sm:flex-row sm:justify-between sm:items-center">
+ {/* 첫 번째 행: 상태 배지 */}
+ <div className="flex items-center flex-wrap gap-2">
+ {rfq.vendorStatus && (
+ <Badge variant="outline">
+ {rfq.rfqStatus}
+ </Badge>
+ )}
+ {rfq.rfqType && (
+ <Badge
+ variant={
+ rfq.rfqType === "BUDGETARY" ? "default" :
+ rfq.rfqType === "PURCHASE" ? "destructive" :
+ rfq.rfqType === "PURCHASE_BUDGETARY" ? "secondary" : "outline"
+ }
+ >
+ {rfq.rfqType}
+ </Badge>
+ )}
+ </div>
+
+ {/* 두 번째 행: Due Date를 강조 표시 */}
+ {rfq.rfqDueDate && (
+ <div className="flex items-center">
+ <Badge variant="secondary" className="flex gap-1 text-xs py-1 px-3">
+ <CalendarClock className="h-3.5 w-3.5" />
+ <span className="font-semibold">Due Date:</span>
+ <span>{formatDateTime(rfq.rfqDueDate)}</span>
+ </Badge>
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </DialogHeader>
+ {rfqId && (
+ <div className="py-4">
+ <RfqItemsTable rfqId={rfqId} />
+ </div>
+ )}
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx
index 7a95d7ed..f664d9a3 100644
--- a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx
+++ b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table-columns.tsx
@@ -31,6 +31,8 @@ interface GetColumnsProps {
openCommentSheet: (vendorId: number) => void
handleDownloadTbeTemplate: (tbeId: number, vendorId: number, rfqId: number) => void
handleUploadTbeResponse: (tbeId: number, vendorId: number, rfqId: number, vendorResponseId:number) => void
+ openVendorContactsDialog: (rfqId: number, rfq: TbeVendorFields) => void // 수정된 시그니처
+
}
/**
@@ -42,6 +44,7 @@ export function getColumns({
openCommentSheet,
handleDownloadTbeTemplate,
handleUploadTbeResponse,
+ openVendorContactsDialog
}: GetColumnsProps): ColumnDef<TbeVendorFields>[] {
// ----------------------------------------------------------------
// 1) Select 컬럼 (체크박스)
@@ -112,7 +115,30 @@ export function getColumns({
)
}
-
+
+ if (cfg.id === "rfqCode") {
+ const rfq = row.original;
+ const rfqId = rfq.rfqId;
+
+ // 협력업체 이름을 클릭할 수 있는 버튼으로 렌더링
+ const handleVendorNameClick = () => {
+ if (rfqId) {
+ openVendorContactsDialog(rfqId, rfq); // vendor 전체 객체 전달
+ } else {
+ toast.error("협력업체 ID를 찾을 수 없습니다.");
+ }
+ };
+
+ return (
+ <Button
+ variant="link"
+ className="p-0 h-auto text-left font-normal justify-start hover:underline"
+ onClick={handleVendorNameClick}
+ >
+ {val as string}
+ </Button>
+ );
+ }
if (cfg.id === "rfqVendorStatus") {
const statusVal = row.original.rfqVendorStatus
if (!statusVal) return null
@@ -173,21 +199,28 @@ export function getColumns({
}
return (
- <div>
- <Button
- variant="ghost"
- size="sm"
- className="h-8 w-8 p-0 group relative"
- onClick={handleClick}
- aria-label={commCount > 0 ? `View ${commCount} comments` : "Add comment"}
- >
- <div className="flex items-center justify-center relative">
- <MessageSquare className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
- </div>
- {commCount > 0 && <span className="absolute -top-1 -right-1 inline-flex h-2 w-2 rounded-full bg-red-500"></span>}
- <span className="sr-only">{commCount > 0 ? `${commCount} Comments` : "Add Comment"}</span>
- </Button>
- </div>
+ <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,
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx
index 3450a643..13d5dc64 100644
--- a/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx
+++ b/lib/vendor-rfq-response/vendor-tbe-table/tbe-table.tsx
@@ -7,19 +7,17 @@ import type {
DataTableFilterField,
DataTableRowAction,
} from "@/types/table"
-
-import { toSentenceCase } from "@/lib/utils"
+import { toast } from "sonner"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { useFeatureFlags } from "./feature-flags-provider"
import { getColumns } from "./tbe-table-columns"
-import { Vendor, vendors } from "@/db/schema/vendors"
import { fetchRfqAttachmentsbyCommentId, getTBEforVendor } from "../../rfqs/service"
import { CommentSheet, TbeComment } from "./comments-sheet"
import { TbeVendorFields } from "@/config/vendorTbeColumnsConfig"
import { useTbeFileHandlers } from "./tbeFileHandler"
import { useSession } from "next-auth/react"
+import { RfqDeailDialog } from "./rfq-detail-dialog"
interface VendorsTableProps {
promises: Promise<
@@ -30,7 +28,6 @@ interface VendorsTableProps {
}
export function TbeVendorTable({ promises }: VendorsTableProps) {
- const { featureFlags } = useFeatureFlags()
const { data: session } = useSession()
const userVendorId = session?.user?.companyId
const userId = Number(session?.user?.id)
@@ -43,8 +40,20 @@ export function TbeVendorTable({ promises }: VendorsTableProps) {
const router = useRouter()
const [initialComments, setInitialComments] = React.useState<TbeComment[]>([])
+ const [isLoadingComments, setIsLoadingComments] = React.useState(false)
+
const [commentSheetOpen, setCommentSheetOpen] = React.useState(false)
const [selectedRfqIdForComments, setSelectedRfqIdForComments] = React.useState<number | null>(null)
+ const [isRfqDetailDialogOpen, setIsRfqDetailDialogOpen] = React.useState(false)
+
+ const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null)
+ const [selectedRfq, setSelectedRfq] = React.useState<TbeVendorFields | null>(null)
+
+ const openVendorContactsDialog = (rfqId: number, rfq: TbeVendorFields) => {
+ setSelectedRfqId(rfqId)
+ setSelectedRfq(rfq)
+ setIsRfqDetailDialogOpen(true)
+ }
// TBE 파일 핸들러 훅 사용
const {
@@ -62,9 +71,11 @@ export function TbeVendorTable({ promises }: VendorsTableProps) {
async function openCommentSheet(vendorId: number) {
setInitialComments([])
+ setIsLoadingComments(true)
const comments = rowAction?.row.original.comments
+ try {
if (comments && comments.length > 0) {
const commentWithAttachments: TbeComment[] = await Promise.all(
comments.map(async (c) => {
@@ -73,18 +84,26 @@ export function TbeVendorTable({ promises }: VendorsTableProps) {
return {
...c,
- commentedBy: 1, // DB나 API 응답에 있다고 가정
+ commentedBy: userId, // DB나 API 응답에 있다고 가정
attachments,
}
})
)
-
+
setInitialComments(commentWithAttachments)
}
setSelectedRfqIdForComments(vendorId)
setCommentSheetOpen(true)
+
+ } catch (error) {
+ console.error("Error loading comments:", error)
+ toast.error("Failed to load comments")
+ } finally {
+ // End loading regardless of success/failure
+ setIsLoadingComments(false)
}
+}
// getColumns() 호출 시, 필요한 모든 핸들러 함수 주입
const columns = React.useMemo(
@@ -94,27 +113,25 @@ export function TbeVendorTable({ promises }: VendorsTableProps) {
openCommentSheet,
handleDownloadTbeTemplate,
handleUploadTbeResponse,
+ openVendorContactsDialog
}),
- [setRowAction, router, openCommentSheet, handleDownloadTbeTemplate, handleUploadTbeResponse]
+ [setRowAction, router, openCommentSheet, handleDownloadTbeTemplate, handleUploadTbeResponse, openVendorContactsDialog]
)
const filterFields: DataTableFilterField<TbeVendorFields>[] = []
const advancedFilterFields: DataTableAdvancedFilterField<TbeVendorFields>[] = [
- { 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: "rfqCode", label: "RFQ Code", type: "text" },
+ { id: "projectCode", label: "Project Code", type: "text" },
+ { id: "projectName", label: "Project Name", type: "text" },
+ { id: "rfqCode", label: "RFQ Code", type: "text" },
+ { id: "tbeResult", label: "TBE Result", type: "text" },
+ { id: "tbeNote", label: "TBE Note", type: "text" },
+ { id: "rfqCode", label: "RFQ Code", type: "text" },
+ { id: "hasResponse", label: "Response?", type: "boolean" },
{ id: "rfqVendorUpdated", label: "Updated at", type: "date" },
+ { id: "dueDate", label: "Project Name", type: "date" },
+
]
const { table } = useDataTable({
@@ -150,11 +167,20 @@ export function TbeVendorTable({ promises }: VendorsTableProps) {
onOpenChange={setCommentSheetOpen}
rfqId={selectedRfqIdForComments}
initialComments={initialComments}
- vendorId={userVendorId||0}
- currentUserId={userId||0}
+ vendorId={userVendorId || 0}
+ currentUserId={userId || 0}
+ isLoading={isLoadingComments} // Pass the loading state
+
/>
)}
+ <RfqDeailDialog
+ isOpen={isRfqDetailDialogOpen}
+ onOpenChange={setIsRfqDetailDialogOpen}
+ rfqId={selectedRfqId}
+ rfq={selectedRfq}
+ />
+
{/* TBE 파일 다이얼로그 */}
<UploadDialog />
</>
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx b/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
index 4efaee77..a0b6f805 100644
--- a/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
+++ b/lib/vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
@@ -13,9 +13,9 @@ import {
import { Button } from "@/components/ui/button";
import {
fetchTbeTemplateFiles,
- getTbeTemplateFileInfo,
uploadTbeResponseFile,
getTbeSubmittedFiles,
+ getFileFromRfqAttachmentsbyid,
} from "../../rfqs/service";
import {
Dropzone,
@@ -118,7 +118,7 @@ export function useTbeFileHandlers() {
// 실제 다운로드 로직
const downloadFile = useCallback(async (fileId: number) => {
try {
- const { file, error } = await getTbeTemplateFileInfo(fileId);
+ const { file, error } = await getFileFromRfqAttachmentsbyid(fileId);
if (error || !file) {
throw new Error(error || "파일 정보를 가져오는 데 실패했습니다");
}