summaryrefslogtreecommitdiff
path: root/lib/rfq-last/table/rfq-table-toolbar-actions.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
commitcf8dac0c6490469dab88a560004b0c07dbd48612 (patch)
treeb9e76061e80d868331e6b4277deecb9086f845f3 /lib/rfq-last/table/rfq-table-toolbar-actions.tsx
parente5745fc0268bbb5770bc14a55fd58a0ec30b466e (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.tsx402
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