diff options
Diffstat (limited to 'lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx')
| -rw-r--r-- | lib/procurement-rfqs/table/rfq-table-toolbar-actions.tsx | 279 |
1 files changed, 279 insertions, 0 deletions
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 |
