summaryrefslogtreecommitdiff
path: root/lib/rfq-last/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/table')
-rw-r--r--lib/rfq-last/table/create-general-rfq-dialog.tsx4
-rw-r--r--lib/rfq-last/table/rfq-assign-pic-dialog.tsx311
-rw-r--r--lib/rfq-last/table/rfq-table-toolbar-actions.tsx402
3 files changed, 434 insertions, 283 deletions
diff --git a/lib/rfq-last/table/create-general-rfq-dialog.tsx b/lib/rfq-last/table/create-general-rfq-dialog.tsx
index 14564686..7abf06a3 100644
--- a/lib/rfq-last/table/create-general-rfq-dialog.tsx
+++ b/lib/rfq-last/table/create-general-rfq-dialog.tsx
@@ -60,7 +60,7 @@ import { createGeneralRfqAction, getPUsersForFilter, previewGeneralRfqCode } fro
// 아이템 스키마
const itemSchema = z.object({
- itemCode: z.string().min(1, "자재코드를 입력해주세요"),
+ itemCode: z.string().optional(),
itemName: z.string().min(1, "자재명을 입력해주세요"),
quantity: z.number().min(1, "수량은 1 이상이어야 합니다"),
uom: z.string().min(1, "단위를 입력해주세요"),
@@ -645,7 +645,7 @@ export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProp
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs">
- 자재코드 <span className="text-red-500">*</span>
+ 자재코드
</FormLabel>
<FormControl>
<Input
diff --git a/lib/rfq-last/table/rfq-assign-pic-dialog.tsx b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx
new file mode 100644
index 00000000..89dda979
--- /dev/null
+++ b/lib/rfq-last/table/rfq-assign-pic-dialog.tsx
@@ -0,0 +1,311 @@
+"use client";
+
+import * as React from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Check, ChevronsUpDown, Loader2, User, Users } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { toast } from "sonner";
+import { getPUsersForFilter } from "@/lib/rfq-last/service";
+import { assignPicToRfqs } from "../service";
+import { Badge } from "@/components/ui/badge";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+
+interface User {
+ id: number;
+ name: string;
+ userCode?: string;
+ email?: string;
+}
+
+interface RfqAssignPicDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ selectedRfqIds: number[];
+ selectedRfqCodes: string[];
+ onSuccess?: () => void;
+}
+
+export function RfqAssignPicDialog({
+ open,
+ onOpenChange,
+ selectedRfqIds,
+ selectedRfqCodes,
+ onSuccess,
+}: RfqAssignPicDialogProps) {
+ const [users, setUsers] = React.useState<User[]>([]);
+ const [isLoadingUsers, setIsLoadingUsers] = React.useState(false);
+ const [isAssigning, setIsAssigning] = React.useState(false);
+ const [selectedUser, setSelectedUser] = React.useState<User | null>(null);
+ const [userPopoverOpen, setUserPopoverOpen] = React.useState(false);
+ const [userSearchTerm, setUserSearchTerm] = React.useState("");
+
+ // ITB만 필터링 (rfqCode가 "I"로 시작하는 것)
+ const itbCodes = React.useMemo(() => {
+ return selectedRfqCodes.filter(code => code.startsWith("I"));
+ }, [selectedRfqCodes]);
+
+ const itbIds = React.useMemo(() => {
+ return selectedRfqIds.filter((id, index) => selectedRfqCodes[index]?.startsWith("I"));
+ }, [selectedRfqIds, selectedRfqCodes]);
+
+ // 유저 목록 로드
+ React.useEffect(() => {
+ const loadUsers = async () => {
+ setIsLoadingUsers(true);
+ try {
+ const userList = await getPUsersForFilter();
+ setUsers(userList);
+ } catch (error) {
+ console.log("사용자 목록 로드 오류:", error);
+ toast.error("사용자 목록을 불러오는데 실패했습니다");
+ } finally {
+ setIsLoadingUsers(false);
+ }
+ };
+
+ if (open) {
+ loadUsers();
+ // 다이얼로그 열릴 때 초기화
+ setSelectedUser(null);
+ setUserSearchTerm("");
+ }
+ }, [open]);
+
+ // 유저 검색
+ const filteredUsers = React.useMemo(() => {
+ if (!userSearchTerm) return users;
+
+ const lowerSearchTerm = userSearchTerm.toLowerCase();
+ return users.filter(
+ (user) =>
+ user.name.toLowerCase().includes(lowerSearchTerm) ||
+ user.userCode?.toLowerCase().includes(lowerSearchTerm)
+ );
+ }, [users, userSearchTerm]);
+
+ const handleSelectUser = (user: User) => {
+ setSelectedUser(user);
+ setUserPopoverOpen(false);
+ };
+
+ const handleAssign = async () => {
+ if (!selectedUser) {
+ toast.error("담당자를 선택해주세요");
+ return;
+ }
+
+ if (itbIds.length === 0) {
+ toast.error("선택한 항목 중 ITB가 없습니다");
+ return;
+ }
+
+ setIsAssigning(true);
+ try {
+ const result = await assignPicToRfqs({
+ rfqIds: itbIds,
+ picUserId: selectedUser.id,
+ });
+
+ if (result.success) {
+ toast.success(result.message);
+ onSuccess?.();
+ onOpenChange(false);
+ } else {
+ toast.error(result.message);
+ }
+ } catch (error) {
+ console.error("담당자 지정 오류:", error);
+ toast.error("담당자 지정 중 오류가 발생했습니다");
+ } finally {
+ setIsAssigning(false);
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Users className="h-5 w-5" />
+ 담당자 지정
+ </DialogTitle>
+ <DialogDescription>
+ 선택한 ITB에 구매 담당자를 지정합니다
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* 선택된 ITB 정보 */}
+ <div className="space-y-2">
+ <label className="text-sm font-medium">선택된 ITB</label>
+ <div className="p-3 bg-muted rounded-md">
+ <div className="flex items-center gap-2 mb-2">
+ <Badge variant="secondary">{itbCodes.length}건</Badge>
+ {itbCodes.length !== selectedRfqCodes.length && (
+ <span className="text-xs text-muted-foreground">
+ (전체 {selectedRfqCodes.length}건 중)
+ </span>
+ )}
+ </div>
+ <div className="max-h-[100px] overflow-y-auto">
+ <div className="flex flex-wrap gap-1">
+ {itbCodes.slice(0, 10).map((code, index) => (
+ <Badge key={index} variant="outline" className="text-xs">
+ {code}
+ </Badge>
+ ))}
+ {itbCodes.length > 10 && (
+ <Badge variant="outline" className="text-xs">
+ +{itbCodes.length - 10}개
+ </Badge>
+ )}
+ </div>
+ </div>
+ </div>
+ {itbCodes.length === 0 && (
+ <Alert className="border-orange-200 bg-orange-50">
+ <AlertDescription className="text-orange-800">
+ 선택한 항목 중 ITB (I로 시작하는 코드)가 없습니다.
+ </AlertDescription>
+ </Alert>
+ )}
+ </div>
+
+ {/* 담당자 선택 */}
+ <div className="space-y-2">
+ <label className="text-sm font-medium">구매 담당자</label>
+ <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ type="button"
+ variant="outline"
+ className="w-full justify-between h-10"
+ disabled={isLoadingUsers || itbCodes.length === 0}
+ >
+ {isLoadingUsers ? (
+ <>
+ <span>담당자 로딩 중...</span>
+ <Loader2 className="ml-2 h-4 w-4 animate-spin" />
+ </>
+ ) : (
+ <>
+ <span className="flex items-center gap-2">
+ <User className="h-4 w-4" />
+ {selectedUser ? (
+ <>
+ {selectedUser.name}
+ {selectedUser.userCode && (
+ <span className="text-muted-foreground">
+ ({selectedUser.userCode})
+ </span>
+ )}
+ </>
+ ) : (
+ <span className="text-muted-foreground">
+ 구매 담당자를 선택하세요
+ </span>
+ )}
+ </span>
+ <ChevronsUpDown className="h-4 w-4 opacity-50" />
+ </>
+ )}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[460px] p-0">
+ <Command>
+ <CommandInput
+ placeholder="이름 또는 코드로 검색..."
+ value={userSearchTerm}
+ onValueChange={setUserSearchTerm}
+ />
+ <CommandList className="max-h-[300px]">
+ <CommandEmpty>검색 결과가 없습니다</CommandEmpty>
+ <CommandGroup>
+ {filteredUsers.map((user) => (
+ <CommandItem
+ key={user.id}
+ onSelect={() => handleSelectUser(user)}
+ className="flex items-center justify-between"
+ >
+ <span className="flex items-center gap-2">
+ <User className="h-4 w-4" />
+ {user.name}
+ {user.userCode && (
+ <span className="text-muted-foreground text-sm">
+ ({user.userCode})
+ </span>
+ )}
+ </span>
+ <Check
+ className={cn(
+ "h-4 w-4",
+ selectedUser?.id === user.id
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ {selectedUser && (
+ <p className="text-xs text-muted-foreground">
+ 선택한 담당자: {selectedUser.name}
+ {selectedUser.userCode && ` (${selectedUser.userCode})`}
+ </p>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isAssigning}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ onClick={handleAssign}
+ disabled={!selectedUser || itbCodes.length === 0 || isAssigning}
+ >
+ {isAssigning ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 지정 중...
+ </>
+ ) : (
+ "담당자 지정"
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file
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