From 4863ba5d336297dddcc8d6d4b414beceb5559742 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 8 Dec 2025 03:03:26 +0000 Subject: (최겸) 기술영업 rfq eml첨부기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../possible-items/connect-item-vendor-dialog.tsx | 406 +++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 lib/tech-vendors/possible-items/connect-item-vendor-dialog.tsx (limited to 'lib/tech-vendors/possible-items/connect-item-vendor-dialog.tsx') diff --git a/lib/tech-vendors/possible-items/connect-item-vendor-dialog.tsx b/lib/tech-vendors/possible-items/connect-item-vendor-dialog.tsx new file mode 100644 index 00000000..bd53b3cc --- /dev/null +++ b/lib/tech-vendors/possible-items/connect-item-vendor-dialog.tsx @@ -0,0 +1,406 @@ +"use client"; + +import * as React from "react"; +import { Search, X } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { + getItemsForVendorMapping, + getConnectableVendorsForItem, + connectItemWithVendors, +} from "../service"; + +type ItemType = "SHIP" | "TOP" | "HULL"; + +interface ItemData { + id: number; + itemCode: string | null; + itemList: string | null; + workType: string | null; + shipTypes?: string | null; + subItemList?: string | null; + itemType: ItemType; + createdAt: Date; + updatedAt: Date; +} + +interface VendorData { + id: number; + vendorName: string; + email: string | null; + techVendorType: string; + status: string; +} + +interface ConnectItemVendorDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConnected?: () => void; +} + +export function ConnectItemVendorDialog({ + open, + onOpenChange, + onConnected, +}: ConnectItemVendorDialogProps) { + const [items, setItems] = React.useState([]); + const [filteredItems, setFilteredItems] = React.useState([]); + const [itemSearch, setItemSearch] = React.useState(""); + const [selectedItem, setSelectedItem] = React.useState(null); + + const [vendors, setVendors] = React.useState([]); + const [filteredVendors, setFilteredVendors] = React.useState([]); + const [vendorSearch, setVendorSearch] = React.useState(""); + const [selectedVendorIds, setSelectedVendorIds] = React.useState([]); + + const [isLoadingItems, setIsLoadingItems] = React.useState(false); + const [isLoadingVendors, setIsLoadingVendors] = React.useState(false); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + // 다이얼로그가 열릴 때 전체 아이템 목록 로드 + React.useEffect(() => { + if (open) { + loadItems(); + } + }, [open]); + + // 아이템 검색 필터링 + React.useEffect(() => { + if (!itemSearch) { + setFilteredItems(items); + return; + } + + const lowered = itemSearch.toLowerCase(); + const filtered = items.filter((item) => + [item.itemCode, item.itemList, item.workType, item.shipTypes, item.subItemList] + .filter(Boolean) + .some((value) => value?.toLowerCase().includes(lowered)) + ); + setFilteredItems(filtered); + }, [items, itemSearch]); + + // 벤더 검색 필터링 + React.useEffect(() => { + if (!vendorSearch) { + setFilteredVendors(vendors); + return; + } + + const lowered = vendorSearch.toLowerCase(); + const filtered = vendors.filter((vendor) => + [vendor.vendorName, vendor.email, vendor.techVendorType, vendor.status] + .filter(Boolean) + .some((value) => value?.toLowerCase().includes(lowered)) + ); + setFilteredVendors(filtered); + }, [vendors, vendorSearch]); + + // 특정 아이템 선택 시 연결 가능한 벤더 목록 로드 + React.useEffect(() => { + if (!selectedItem) { + setVendors([]); + setFilteredVendors([]); + setSelectedVendorIds([]); + return; + } + loadVendors(selectedItem); + }, [selectedItem]); + + const loadItems = async () => { + setIsLoadingItems(true); + try { + const result = await getItemsForVendorMapping(); + if (result.error) { + throw new Error(result.error); + } + const validItems = (result.data as ItemData[]).filter((item) => item.itemCode != null); + setItems(validItems); + } catch (error) { + console.error("Failed to load items for mapping:", error); + toast.error("아이템 목록을 불러오는데 실패했습니다."); + } finally { + setIsLoadingItems(false); + } + }; + + const loadVendors = async (item: ItemData) => { + setIsLoadingVendors(true); + try { + const result = await getConnectableVendorsForItem(item.id, item.itemType); + if (result.error) { + throw new Error(result.error); + } + setVendors(result.data as VendorData[]); + } catch (error) { + console.error("Failed to load vendors for item:", error); + toast.error("연결 가능한 벤더 목록을 불러오는데 실패했습니다."); + } finally { + setIsLoadingVendors(false); + } + }; + + const handleItemSelect = (item: ItemData) => { + if (!item.itemCode) return; + setSelectedItem(item); + }; + + const handleVendorToggle = (vendorId: number) => { + setSelectedVendorIds((prev) => + prev.includes(vendorId) + ? prev.filter((id) => id !== vendorId) + : [...prev, vendorId] + ); + }; + + const handleSubmit = async () => { + if (!selectedItem || selectedVendorIds.length === 0) return; + + setIsSubmitting(true); + try { + const result = await connectItemWithVendors({ + itemId: selectedItem.id, + itemType: selectedItem.itemType, + vendorIds: selectedVendorIds, + }); + + if (!result.success) { + throw new Error(result.error || "연결에 실패했습니다."); + } + + const successCount = result.successCount || 0; + const skippedCount = result.skipped?.length || 0; + + toast.success( + `${successCount}개 벤더와 연결되었습니다${ + skippedCount > 0 ? ` (${skippedCount}개 중복 제외)` : "" + }` + ); + + onConnected?.(); + handleClose(); + } catch (error) { + console.error("Failed to connect item with vendors:", error); + toast.error(error instanceof Error ? error.message : "연결 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + + const handleClose = () => { + onOpenChange(false); + setTimeout(() => { + setItemSearch(""); + setVendorSearch(""); + setSelectedItem(null); + setSelectedVendorIds([]); + setItems([]); + setFilteredItems([]); + setVendors([]); + setFilteredVendors([]); + }, 200); + }; + + return ( + + + + 아이템 기준 벤더 연결 + + 연결할 아이템을 먼저 선택한 후, 해당 아이템과 연결할 벤더를 선택하세요. + + + +
+ {/* 아이템 선택 영역 */} +
+
+ +
+ + setItemSearch(e.target.value)} + className="pl-10" + /> +
+
+ + {selectedItem && ( +
+ +
+ + {[selectedItem.itemType, selectedItem.itemCode, selectedItem.shipTypes] + .filter(Boolean) + .join("-")} + { + e.stopPropagation(); + setSelectedItem(null); + }} + /> + +
+
+ )} + +
+
+ {isLoadingItems ? ( +
아이템 로딩 중...
+ ) : filteredItems.length === 0 ? ( +
+ 아이템이 없습니다. +
+ ) : ( +
+ {filteredItems.map((item) => { + if (!item.itemCode) return null; + const isSelected = selectedItem?.id === item.id && selectedItem.itemType === item.itemType; + const itemKey = `${item.itemType}-${item.id}-${item.itemCode}${item.shipTypes ? `-${item.shipTypes}` : ""}`; + return ( +
handleItemSelect(item)} + > +
+ {[`[${item.itemType}]`, item.itemCode, item.shipTypes] + .filter(Boolean) + .join(" ")} +
+
+ {item.itemList || "-"} +
+
+ 공종: {item.workType || "-"} + {item.shipTypes && 선종: {item.shipTypes}} + {item.subItemList && 서브아이템: {item.subItemList}} +
+
+ ); + })} +
+ )} +
+
+
+ + {/* 벤더 선택 영역 */} +
+
+ +
+ + setVendorSearch(e.target.value)} + className="pl-10" + disabled={!selectedItem} + /> +
+
+ + {selectedVendorIds.length > 0 && ( +
+ +
+ {vendors + .filter((vendor) => selectedVendorIds.includes(vendor.id)) + .map((vendor) => ( + + {vendor.vendorName} + { + e.stopPropagation(); + handleVendorToggle(vendor.id); + }} + /> + + ))} +
+
+ )} + +
+
+ {!selectedItem ? ( +
+ 아이템을 먼저 선택해주세요. +
+ ) : isLoadingVendors ? ( +
벤더 로딩 중...
+ ) : filteredVendors.length === 0 ? ( +
+ 연결 가능한 벤더가 없습니다. +
+ ) : ( +
+ {filteredVendors.map((vendor) => { + const isSelected = selectedVendorIds.includes(vendor.id); + return ( +
handleVendorToggle(vendor.id)} + > +
{vendor.vendorName}
+
+ {vendor.email || "-"} +
+
+ 타입: {vendor.techVendorType || "-"} + 상태: {vendor.status || "-"} +
+
+ ); + })} +
+ )} +
+
+
+
+ +
+ + +
+
+
+ ); +} + -- cgit v1.2.3