diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
| commit | cf8dac0c6490469dab88a560004b0c07dbd48612 (patch) | |
| tree | b9e76061e80d868331e6b4277deecb9086f845f3 /lib/rfq-last/table/rfq-table-toolbar-actions.tsx | |
| parent | e5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff) | |
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/rfq-last/table/rfq-table-toolbar-actions.tsx')
| -rw-r--r-- | lib/rfq-last/table/rfq-table-toolbar-actions.tsx | 402 |
1 files changed, 121 insertions, 281 deletions
diff --git a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx index 91b2798f..d933fa95 100644 --- a/lib/rfq-last/table/rfq-table-toolbar-actions.tsx +++ b/lib/rfq-last/table/rfq-table-toolbar-actions.tsx @@ -1,308 +1,148 @@ "use client"; import * as React from "react"; -import { type Table } from "@tanstack/react-table"; -import { Download, RefreshCw, Plus, Lock, LockOpen } from "lucide-react"; - +import { Table } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { toast } from "sonner"; +import { Users, RefreshCw, FileDown, Plus } from "lucide-react"; import { RfqsLastView } from "@/db/schema"; -import { CreateGeneralRfqDialog } from "./create-general-rfq-dialog"; -import { sealMultipleRfqs, unsealMultipleRfqs } from "../service"; - -interface RfqTableToolbarActionsProps { - table: Table<RfqsLastView>; - onRefresh?: () => void; +import { RfqAssignPicDialog } from "./rfq-assign-pic-dialog"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface RfqTableToolbarActionsProps<TData> { + table: Table<TData>; rfqCategory?: "general" | "itb" | "rfq"; + onRefresh?: () => void; } -export function RfqTableToolbarActions({ - table, - onRefresh, +export function RfqTableToolbarActions<TData>({ + table, rfqCategory = "itb", -}: RfqTableToolbarActionsProps) { - const [isExporting, setIsExporting] = React.useState(false); - const [isSealing, setIsSealing] = React.useState(false); - const [sealDialogOpen, setSealDialogOpen] = React.useState(false); - const [sealAction, setSealAction] = React.useState<"seal" | "unseal">("seal"); - + onRefresh +}: RfqTableToolbarActionsProps<TData>) { + const [showAssignDialog, setShowAssignDialog] = React.useState(false); + + // 선택된 행 가져오기 const selectedRows = table.getFilteredSelectedRowModel().rows; - const selectedRfqIds = selectedRows.map(row => row.original.id); - // 선택된 항목들의 밀봉 상태 확인 - const sealedCount = selectedRows.filter(row => row.original.rfqSealedYn).length; - const unsealedCount = selectedRows.filter(row => !row.original.rfqSealedYn).length; - - const handleSealAction = React.useCallback(async (action: "seal" | "unseal") => { - setSealAction(action); - setSealDialogOpen(true); - }, []); - - const confirmSealAction = React.useCallback(async () => { - setIsSealing(true); - try { - const result = sealAction === "seal" - ? await sealMultipleRfqs(selectedRfqIds) - : await unsealMultipleRfqs(selectedRfqIds); - - if (result.success) { - toast.success(result.message); - table.toggleAllRowsSelected(false); // 선택 해제 - onRefresh?.(); // 데이터 새로고침 - } else { - toast.error(result.error); - } - } catch (error) { - toast.error("작업 중 오류가 발생했습니다."); - } finally { - setIsSealing(false); - setSealDialogOpen(false); - } - }, [sealAction, selectedRfqIds, table, onRefresh]); - - const handleExportCSV = React.useCallback(async () => { - setIsExporting(true); - try { - const data = table.getFilteredRowModel().rows.map((row) => { - const original = row.original; - return { - "RFQ 코드": original.rfqCode || "", - "상태": original.status || "", - "밀봉여부": original.rfqSealedYn ? "밀봉" : "미밀봉", - "프로젝트 코드": original.projectCode || "", - "프로젝트명": original.projectName || "", - "자재코드": original.itemCode || "", - "자재명": original.itemName || "", - "패키지 번호": original.packageNo || "", - "패키지명": original.packageName || "", - "구매담당자": original.picUserName || original.picName || "", - "엔지니어링 담당": original.engPicName || "", - "발송일": original.rfqSendDate ? new Date(original.rfqSendDate).toLocaleDateString("ko-KR") : "", - "마감일": original.dueDate ? new Date(original.dueDate).toLocaleDateString("ko-KR") : "", - "업체수": original.vendorCount || 0, - "Short List": original.shortListedVendorCount || 0, - "견적접수": original.quotationReceivedCount || 0, - "PR Items": original.prItemsCount || 0, - "주요 Items": original.majorItemsCount || 0, - "시리즈": original.series || "", - "견적 유형": original.rfqType || "", - "견적 제목": original.rfqTitle || "", - "프로젝트 회사": original.projectCompany || "", - "프로젝트 사이트": original.projectSite || "", - "SM 코드": original.smCode || "", - "PR 번호": original.prNumber || "", - "PR 발행일": original.prIssueDate ? new Date(original.prIssueDate).toLocaleDateString("ko-KR") : "", - "생성자": original.createdByUserName || "", - "생성일": original.createdAt ? new Date(original.createdAt).toLocaleDateString("ko-KR") : "", - "수정자": original.updatedByUserName || "", - "수정일": original.updatedAt ? new Date(original.updatedAt).toLocaleDateString("ko-KR") : "", - }; - }); - - const fileName = `RFQ_목록_${new Date().toISOString().split("T")[0]}.csv`; - exportTableToCSV({ data, filename: fileName }); - } catch (error) { - console.error("Export failed:", error); - } finally { - setIsExporting(false); - } - }, [table]); - - const handleExportSelected = React.useCallback(async () => { - setIsExporting(true); - try { - const selectedRows = table.getFilteredSelectedRowModel().rows; - if (selectedRows.length === 0) { - alert("선택된 항목이 없습니다."); - return; - } - - const data = selectedRows.map((row) => { - const original = row.original; - return { - "RFQ 코드": original.rfqCode || "", - "상태": original.status || "", - "밀봉여부": original.rfqSealedYn ? "밀봉" : "미밀봉", - "프로젝트 코드": original.projectCode || "", - "프로젝트명": original.projectName || "", - "자재코드": original.itemCode || "", - "자재명": original.itemName || "", - "패키지 번호": original.packageNo || "", - "패키지명": original.packageName || "", - "구매담당자": original.picUserName || original.picName || "", - "엔지니어링 담당": original.engPicName || "", - "발송일": original.rfqSendDate ? new Date(original.rfqSendDate).toLocaleDateString("ko-KR") : "", - "마감일": original.dueDate ? new Date(original.dueDate).toLocaleDateString("ko-KR") : "", - "업체수": original.vendorCount || 0, - "Short List": original.shortListedVendorCount || 0, - "견적접수": original.quotationReceivedCount || 0, - }; - }); - - const fileName = `RFQ_선택항목_${new Date().toISOString().split("T")[0]}.csv`; - exportTableToCSV({ data, filename: fileName }); - } catch (error) { - console.error("Export failed:", error); - } finally { - setIsExporting(false); - } - }, [table]); + // 선택된 RFQ의 ID와 코드 추출 + const selectedRfqData = React.useMemo(() => { + const rows = selectedRows.map(row => row.original as RfqsLastView); + return { + ids: rows.map(row => row.id), + codes: rows.map(row => row.rfqCode || ""), + // "I"로 시작하는 ITB만 필터링 + itbCount: rows.filter(row => row.rfqCode?.startsWith("I")).length, + totalCount: rows.length + }; + }, [selectedRows]); + + // 담당자 지정 가능 여부 체크 ("I"로 시작하는 항목이 있는지) + const canAssignPic = selectedRfqData.itbCount > 0; + + const handleAssignSuccess = () => { + // 테이블 선택 초기화 + table.toggleAllPageRowsSelected(false); + // 데이터 새로고침 + onRefresh?.(); + }; return ( <> <div className="flex items-center gap-2"> - {onRefresh && ( - <Button - variant="outline" - size="sm" - onClick={onRefresh} - className="h-8 px-2 lg:px-3" - > - <RefreshCw className="mr-2 h-4 w-4" /> - 새로고침 - </Button> + {/* 담당자 지정 버튼 - 선택된 항목 중 ITB가 있을 때만 표시 */} + {selectedRfqData.totalCount > 0 && canAssignPic && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="default" + size="sm" + onClick={() => setShowAssignDialog(true)} + className="flex items-center gap-2" + > + <Users className="h-4 w-4" /> + 담당자 지정 + <Badge variant="secondary" className="ml-1"> + {selectedRfqData.itbCount}건 + </Badge> + </Button> + </TooltipTrigger> + <TooltipContent> + <p>선택한 ITB에 구매 담당자를 지정합니다</p> + {selectedRfqData.itbCount !== selectedRfqData.totalCount && ( + <p className="text-xs text-muted-foreground mt-1"> + 전체 {selectedRfqData.totalCount}건 중 ITB {selectedRfqData.itbCount}건만 지정됩니다 + </p> + )} + </TooltipContent> + </Tooltip> + </TooltipProvider> )} - {/* 견적 밀봉/해제 버튼 */} - {selectedRfqIds.length > 0 && ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="outline" - size="sm" - className="h-8 px-2 lg:px-3" - disabled={isSealing} - > - <Lock className="mr-2 h-4 w-4" /> - 견적 밀봉 - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem - onClick={() => handleSealAction("seal")} - disabled={unsealedCount === 0} - > - <Lock className="mr-2 h-4 w-4" /> - 선택 항목 밀봉 ({unsealedCount}개) - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => handleSealAction("unseal")} - disabled={sealedCount === 0} - > - <LockOpen className="mr-2 h-4 w-4" /> - 선택 항목 밀봉 해제 ({sealedCount}개) - </DropdownMenuItem> - <DropdownMenuSeparator /> - <div className="px-2 py-1.5 text-xs text-muted-foreground"> - 전체 {selectedRfqIds.length}개 선택됨 - </div> - </DropdownMenuContent> - </DropdownMenu> + {/* 선택된 항목 표시 */} + {selectedRfqData.totalCount > 0 && ( + <div className="flex items-center gap-2 px-3 py-1.5 bg-muted rounded-md"> + <span className="text-sm text-muted-foreground"> + 선택된 항목: + </span> + <Badge variant="secondary"> + {selectedRfqData.totalCount}건 + </Badge> + {selectedRfqData.totalCount !== selectedRfqData.itbCount && ( + <Badge variant="outline" className="text-xs"> + ITB {selectedRfqData.itbCount}건 + </Badge> + )} + </div> )} - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="outline" - size="sm" - className="h-8 px-2 lg:px-3" - disabled={isExporting} - > - <Download className="mr-2 h-4 w-4" /> - {isExporting ? "내보내는 중..." : "내보내기"} - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem onClick={handleExportCSV}> - 전체 데이터 내보내기 - </DropdownMenuItem> - <DropdownMenuItem - onClick={handleExportSelected} - disabled={table.getFilteredSelectedRowModel().rows.length === 0} - > - 선택한 항목 내보내기 ({table.getFilteredSelectedRowModel().rows.length}개) - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> + {/* 기존 버튼들 */} + <Button + variant="outline" + size="sm" + onClick={onRefresh} + className="flex items-center gap-2" + > + <RefreshCw className="h-4 w-4" /> + 새로고침 + </Button> - {/* rfqCategory가 'general'일 때만 일반견적 생성 다이얼로그 표시 */} {rfqCategory === "general" && ( - <CreateGeneralRfqDialog onSuccess={onRefresh} /> + <Button + variant="outline" + size="sm" + className="flex items-center gap-2" + > + <Plus className="h-4 w-4" /> + 일반견적 생성 + </Button> )} + + <Button + variant="outline" + size="sm" + className="flex items-center gap-2" + disabled={selectedRfqData.totalCount === 0} + > + <FileDown className="h-4 w-4" /> + 엑셀 다운로드 + </Button> </div> - {/* 밀봉 확인 다이얼로그 */} - <AlertDialog open={sealDialogOpen} onOpenChange={setSealDialogOpen}> - <AlertDialogContent> - <AlertDialogHeader> - <AlertDialogTitle> - {sealAction === "seal" ? "견적 밀봉 확인" : "견적 밀봉 해제 확인"} - </AlertDialogTitle> - <AlertDialogDescription> - {sealAction === "seal" - ? `선택한 ${unsealedCount}개의 견적을 밀봉하시겠습니까? 밀봉된 견적은 업체에서 수정할 수 없습니다.` - : `선택한 ${sealedCount}개의 견적 밀봉을 해제하시겠습니까? 밀봉이 해제되면 업체에서 견적을 수정할 수 있습니다.`} - </AlertDialogDescription> - </AlertDialogHeader> - <AlertDialogFooter> - <AlertDialogCancel disabled={isSealing}>취소</AlertDialogCancel> - <AlertDialogAction - onClick={confirmSealAction} - disabled={isSealing} - className={sealAction === "seal" ? "bg-red-600 hover:bg-red-700" : ""} - > - {isSealing ? "처리 중..." : "확인"} - </AlertDialogAction> - </AlertDialogFooter> - </AlertDialogContent> - </AlertDialog> + {/* 담당자 지정 다이얼로그 */} + <RfqAssignPicDialog + open={showAssignDialog} + onOpenChange={setShowAssignDialog} + selectedRfqIds={selectedRfqData.ids} + selectedRfqCodes={selectedRfqData.codes} + onSuccess={handleAssignSuccess} + /> </> ); -} - -// CSV 내보내기 유틸리티 함수 -function exportTableToCSV({ data, filename }: { data: any[]; filename: string }) { - if (!data || data.length === 0) { - console.warn("No data to export"); - return; - } - - const headers = Object.keys(data[0]); - const csvContent = [ - headers.join(","), - ...data.map(row => - headers.map(header => { - const value = row[header]; - // 값에 쉼표, 줄바꿈, 따옴표가 있으면 따옴표로 감싸기 - if (typeof value === "string" && (value.includes(",") || value.includes("\n") || value.includes('"'))) { - return `"${value.replace(/"/g, '""')}"`; - } - return value; - }).join(",") - ) - ].join("\n"); - - const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = filename; - link.click(); - URL.revokeObjectURL(link.href); }
\ No newline at end of file |
