From a6b9cdaf9ea5ed548292632f821e36453f377a83 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 12 May 2025 11:36:25 +0000
Subject: (대표님) procurement-rfq 작업업
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/rfq-table-toolbar-actions.tsx            | 279 +++++++++++++++++++++
 1 file changed, 279 insertions(+)
 create mode 100644 lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx

(limited to 'lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx')

diff --git a/lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx b/lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx
new file mode 100644
index 00000000..26725797
--- /dev/null
+++ b/lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx
@@ -0,0 +1,279 @@
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { ClipboardList, Download, Send, Lock, Upload } from "lucide-react"
+import { toast } from "sonner"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+import { ProcurementRfqsView } from "@/db/schema"
+import { PrDetailsDialog } from "./pr-item-dialog"
+import { sealRfq, sendRfq, getPORfqs, fetchExternalRfqs } from "../services"
+
+// total 필드 추가하여 타입 정의 수정
+type PORfqsReturn = Awaited<ReturnType<typeof getPORfqs>>
+
+interface RFQTableToolbarActionsProps {
+  table: Table<ProcurementRfqsView>;
+  // 타입 수정
+  localData?: PORfqsReturn;
+  setLocalData?: React.Dispatch<React.SetStateAction<PORfqsReturn>>;
+  onSuccess?: () => void;
+}
+
+export function RFQTableToolbarActions({
+  table,
+  localData,
+  setLocalData,
+  onSuccess
+}: RFQTableToolbarActionsProps) {
+  // 다이얼로그 열림/닫힘 상태 관리
+  const [dialogOpen, setDialogOpen] = React.useState(false)
+  const [isProcessing, setIsProcessing] = React.useState(false)
+
+  // 선택된 RFQ 가져오기
+  const getSelectedRfq = (): ProcurementRfqsView | null => {
+    const selectedRows = table.getFilteredSelectedRowModel().rows
+    if (selectedRows.length === 1) {
+      return selectedRows[0].original
+    }
+    return null
+  }
+
+  // 선택된 RFQ
+  const selectedRfq = getSelectedRfq()
+
+  // PR 상세보기 버튼 클릭 핸들러
+  const handleViewPrDetails = () => {
+    const rfq = getSelectedRfq()
+    if (!rfq) {
+      toast.warning("RFQ를 선택해주세요")
+      return
+    }
+
+    if (!rfq.prItemsCount || rfq.prItemsCount <= 0) {
+      toast.warning("선택한 RFQ에 PR 항목이 없습니다")
+      return
+    }
+
+    setDialogOpen(true)
+  }
+
+  // RFQ 밀봉 버튼 클릭 핸들러
+  const handleSealRfq = async () => {
+    const rfq = getSelectedRfq()
+    if (!rfq) {
+      toast.warning("RFQ를 선택해주세요")
+      return
+    }
+
+    // 이미 밀봉된 RFQ인 경우
+    if (rfq.rfqSealedYn) {
+      toast.warning("이미 밀봉된 RFQ입니다")
+      return
+    }
+
+    try {
+      setIsProcessing(true)
+
+      // 낙관적 UI 업데이트 (로컬 데이터 먼저 갱신)
+      if (localData?.data && setLocalData) {
+        // 로컬 데이터에서 해당 행 찾기
+        const rowIndex = localData.data.findIndex(row => row.id === rfq.id);
+        if (rowIndex >= 0) {
+          // 불변성을 유지하면서 로컬 데이터 업데이트 - 타입 안전하게 복사
+          const newData = [...localData.data] as ProcurementRfqsView[];
+          newData[rowIndex] = { ...newData[rowIndex], rfqSealedYn: "Y" };
+
+          // 전체 데이터 구조 복사하여 업데이트, total 필드가 있다면 유지
+          setLocalData({ 
+            ...localData, 
+            data: newData ?? [],
+            pageCount: localData.pageCount,
+            total: localData.total  ?? 0
+          });
+        }
+      }
+
+      const result = await sealRfq(rfq.id)
+
+      if (result.success) {
+        toast.success("RFQ가 성공적으로 밀봉되었습니다")
+        // 데이터 리프레시
+        onSuccess?.()
+      } else {
+        toast.error(result.message || "RFQ 밀봉 중 오류가 발생했습니다")
+
+        // 서버 요청 실패 시 낙관적 업데이트 되돌리기
+        if (localData?.data && setLocalData) {
+          const rowIndex = localData.data.findIndex(row => row.id === rfq.id);
+          if (rowIndex >= 0) {
+            const newData = [...localData.data] as ProcurementRfqsView[];
+            newData[rowIndex] = { ...newData[rowIndex], rfqSealedYn: rfq.rfqSealedYn }; // 원래 값으로 복원
+            setLocalData({ 
+              ...localData, 
+              data: newData ?? [],
+              pageCount: localData.pageCount,
+              total: localData.total ?? 0 
+            });
+          }
+        }
+      }
+    } catch (error) {
+      console.error("RFQ 밀봉 오류:", error)
+      toast.error("RFQ 밀봉 중 오류가 발생했습니다")
+
+      // 에러 발생 시 낙관적 업데이트 되돌리기
+      if (localData?.data && setLocalData) {
+        const rowIndex = localData.data.findIndex(row => row.id === rfq.id);
+        if (rowIndex >= 0) {
+          const newData = [...localData.data] as ProcurementRfqsView[];
+          newData[rowIndex] = { ...newData[rowIndex], rfqSealedYn: rfq.rfqSealedYn }; // 원래 값으로 복원
+          setLocalData({ 
+            ...localData, 
+            data: newData ?? [],
+            pageCount: localData.pageCount,
+            total: localData.total ?? 0
+          });
+        }
+      }
+    } finally {
+      setIsProcessing(false)
+    }
+  }
+
+  // RFQ 전송 버튼 클릭 핸들러
+  const handleSendRfq = async () => {
+    const rfq = getSelectedRfq()
+    if (!rfq) {
+      toast.warning("RFQ를 선택해주세요")
+      return
+    }
+
+    // 전송 가능한 상태인지 확인
+    if (rfq.status !== "RFQ Vendor Assignned" && rfq.status !== "RFQ Sent") {
+      toast.warning("벤더가 할당된 RFQ이거나 전송한 적이 있는 RFQ만 전송할 수 있습니다")
+      return
+    }
+
+    try {
+      setIsProcessing(true)
+
+      const result = await sendRfq(rfq.id)
+
+      if (result.success) {
+        toast.success("RFQ가 성공적으로 전송되었습니다")
+        // 데이터 리프레시
+        onSuccess?.()
+      } else {
+        toast.error(result.message || "RFQ 전송 중 오류가 발생했습니다")
+      }
+    } catch (error) {
+      console.error("RFQ 전송 오류:", error)
+      toast.error("RFQ 전송 중 오류가 발생했습니다")
+    } finally {
+      setIsProcessing(false)
+    }
+  }
+
+  const handleFetchExternalRfqs = async () => {
+    try {
+      setIsProcessing(true);
+
+      const result = await fetchExternalRfqs();
+
+      if (result.success) {
+        toast.success(result.message || "외부 RFQ를 성공적으로 가져왔습니다");
+        // 데이터 리프레시
+        onSuccess?.()
+      } else {
+        toast.error(result.message || "외부 RFQ를 가져오는 중 오류가 발생했습니다");
+      }
+    } catch (error) {
+      console.error("외부 RFQ 가져오기 오류:", error);
+      toast.error("외부 RFQ를 가져오는 중 오류가 발생했습니다");
+    } finally {
+      setIsProcessing(false);
+    }
+  };
+
+  return (
+    <>
+      <div className="flex items-center gap-2">
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={() =>
+            exportTableToExcel(table, {
+              filename: "rfq",
+              excludeColumns: ["select", "actions"],
+            })
+          }
+          className="gap-2"
+        >
+          <Download className="size-4" aria-hidden="true" />
+          <span className="hidden sm:inline">Export</span>
+        </Button>
+        {/* RFQ 가져오기 버튼 */}
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={handleFetchExternalRfqs}
+          className="gap-2"
+          disabled={isProcessing}
+        >
+          <Upload className="size-4" aria-hidden="true" />
+          <span className="hidden sm:inline">RFQ 가져오기</span>
+        </Button>
+
+        {/* PR 상세보기 버튼 */}
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={handleViewPrDetails}
+          className="gap-2"
+          disabled={!selectedRfq || !(selectedRfq.prItemsCount && selectedRfq.prItemsCount > 0)}
+        >
+          <ClipboardList className="size-4" aria-hidden="true" />
+          <span className="hidden sm:inline">PR 상세보기</span>
+        </Button>
+
+        {/* RFQ 밀봉 버튼 */}
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={handleSealRfq}
+          className="gap-2"
+          disabled={!selectedRfq || selectedRfq.rfqSealedYn === "Y" || selectedRfq.status !== "RFQ Sent" || isProcessing}
+        >
+          <Lock className="size-4" aria-hidden="true" />
+          <span className="hidden sm:inline">RFQ 밀봉</span>
+        </Button>
+
+        {/* RFQ 전송 버튼 */}
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={handleSendRfq}
+          className="gap-2"
+          disabled={
+            !selectedRfq || 
+            (selectedRfq.status !== "RFQ Vendor Assignned" && selectedRfq.status !== "RFQ Sent") || 
+            isProcessing
+          }
+        >
+          <Send className="size-4" aria-hidden="true" />
+          <span className="hidden sm:inline">RFQ 전송</span>
+        </Button>
+      </div>
+
+      {/* PR 상세정보 다이얼로그 */}
+      <PrDetailsDialog
+        open={dialogOpen}
+        onOpenChange={setDialogOpen}
+        selectedRfq={selectedRfq}
+      />
+    </>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

