From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tbe/table/comments-sheet.tsx | 15 +- lib/tbe/table/invite-vendors-dialog.tsx | 8 +- lib/tbe/table/tbe-result-dialog.tsx | 208 ++++++++++++++++++++++++++++ lib/tbe/table/tbe-table-columns.tsx | 103 +++++++++++++- lib/tbe/table/tbe-table-toolbar-actions.tsx | 28 ++-- lib/tbe/table/tbe-table.tsx | 81 ++++++++--- lib/tbe/table/vendor-contact-dialog.tsx | 71 ++++++++++ 7 files changed, 478 insertions(+), 36 deletions(-) create mode 100644 lib/tbe/table/tbe-result-dialog.tsx create mode 100644 lib/tbe/table/vendor-contact-dialog.tsx (limited to 'lib/tbe/table') diff --git a/lib/tbe/table/comments-sheet.tsx b/lib/tbe/table/comments-sheet.tsx index 7fcde35d..0952209d 100644 --- a/lib/tbe/table/comments-sheet.tsx +++ b/lib/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" @@ -50,7 +50,6 @@ import { // DB 스키마에서 필요한 타입들을 가져온다고 가정 // (실제 프로젝트에 맞춰 import를 수정하세요.) -import { RfqWithAll } from "@/db/schema/rfq" import { formatDate } from "@/lib/utils" import { createRfqCommentWithAttachments } from "@/lib/rfqs/service" @@ -77,6 +76,7 @@ interface CommentSheetProps extends React.ComponentPropsWithRef { currentUserId: number rfqId:number vendorId:number + isLoading?: boolean // New prop /** 댓글 저장 후 갱신용 콜백 (옵션) */ onCommentsUpdated?: (comments: TbeComment[]) => void } @@ -96,6 +96,7 @@ export function CommentSheet({ initialComments = [], currentUserId, onCommentsUpdated, + isLoading = false, ...props }: CommentSheetProps) { const [comments, setComments] = React.useState(initialComments) @@ -125,6 +126,16 @@ export function CommentSheet({ // 간단히 테이블 하나로 표현 // 실제로는 Bubble 형태의 UI, Accordion, Timeline 등 다양하게 구성할 수 있음 function renderExistingComments() { + + if (isLoading) { + return ( +
+ + Loading comments... +
+ ) + } + if (comments.length === 0) { return

No comments yet

} diff --git a/lib/tbe/table/invite-vendors-dialog.tsx b/lib/tbe/table/invite-vendors-dialog.tsx index 87467e57..59535278 100644 --- a/lib/tbe/table/invite-vendors-dialog.tsx +++ b/lib/tbe/table/invite-vendors-dialog.tsx @@ -39,6 +39,7 @@ interface InviteVendorsDialogProps rfqId: number showTrigger?: boolean onSuccess?: () => void + hasMultipleRfqIds?: boolean } export function InviteVendorsDialog({ @@ -46,6 +47,7 @@ export function InviteVendorsDialog({ rfqId, showTrigger = true, onSuccess, + hasMultipleRfqIds, ...props }: InviteVendorsDialogProps) { const [isInvitePending, startInviteTransition] = React.useTransition() @@ -105,10 +107,14 @@ export function InviteVendorsDialog({ /> ) - + if (hasMultipleRfqIds) { + toast.error("동일한 RFQ에 대해 선택해주세요"); + return; + } // Desktop Dialog if (isDesktop) { return ( + {showTrigger ? ( diff --git a/lib/tbe/table/tbe-result-dialog.tsx b/lib/tbe/table/tbe-result-dialog.tsx new file mode 100644 index 00000000..59e2f49b --- /dev/null +++ b/lib/tbe/table/tbe-result-dialog.tsx @@ -0,0 +1,208 @@ +"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 "@/lib/rfqs/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("") + 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 ( + + + + + {tbe?.tbeResult ? "Edit TBE Result" : "Enter TBE Result"} + + {tbe && ( + +
+ + Vendor: {tbe.vendorName} + + + RFQ Code: {tbe.rfqCode} + + {tbe.email && ( + + Email: {tbe.email} + + )} +
+
+ )} +
+ +
+
+ + +
+ +
+ +