From 5036cf2908792cef45f06256e71f10920f647f49 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 28 May 2025 19:03:21 +0000 Subject: (김준회) 기술영업 조선 RFQ (SHI/벤더) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/table/create-rfq-dialog.tsx | 537 ++++++++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 lib/techsales-rfq/table/create-rfq-dialog.tsx (limited to 'lib/techsales-rfq/table/create-rfq-dialog.tsx') diff --git a/lib/techsales-rfq/table/create-rfq-dialog.tsx b/lib/techsales-rfq/table/create-rfq-dialog.tsx new file mode 100644 index 00000000..cc652b44 --- /dev/null +++ b/lib/techsales-rfq/table/create-rfq-dialog.tsx @@ -0,0 +1,537 @@ +"use client" + +import * as React from "react" +import { toast } from "sonner" +import { ArrowUpDown, CheckSquare, Plus, Search, Square, X, Loader2 } from "lucide-react" +import { Input } from "@/components/ui/input" +import { Calendar } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { CalendarIcon } from "lucide-react" +import { format } from "date-fns" +import { ko } from "date-fns/locale" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" +import { EstimateProjectSelector } from "@/components/BidProjectSelector" +import { type Project } from "@/lib/rfqs/service" +import { createTechSalesRfq } from "@/lib/techsales-rfq/service" +import { useSession } from "next-auth/react" +import { Separator } from "@/components/ui/separator" +import { Badge } from "@/components/ui/badge" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { cn } from "@/lib/utils" +import { ScrollArea } from "@/components/ui/scroll-area" + +// 실제 데이터 서비스 import +import { + getShipbuildingItemsByWorkType, + searchShipbuildingItems, + getWorkTypes, + type ShipbuildingItem, + type WorkType +} from "@/lib/items-tech/service" + +// 유효성 검증 스키마 - 자재코드(item_code) 배열로 변경 +const createRfqSchema = z.object({ + biddingProjectId: z.number({ + required_error: "프로젝트를 선택해주세요.", + }), + materialCodes: z.array(z.string()).min(1, { + message: "적어도 하나의 자재코드를 선택해야 합니다.", + }), + dueDate: z.date({ + required_error: "마감일을 선택해주세요.", + }), +}) + +// 폼 데이터 타입 +type CreateRfqFormValues = z.infer + +// 공종 타입 정의 +interface WorkTypeOption { + code: WorkType + name: string + description: string +} + +interface CreateRfqDialogProps { + onCreated?: () => void; +} + +export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { + const { data: session } = useSession() + const [isProcessing, setIsProcessing] = React.useState(false) + const [isDialogOpen, setIsDialogOpen] = React.useState(false) + const [selectedProject, setSelectedProject] = React.useState(null) + + // 검색 및 필터링 상태 + const [itemSearchQuery, setItemSearchQuery] = React.useState("") + const [selectedWorkType, setSelectedWorkType] = React.useState(null) + const [selectedItems, setSelectedItems] = React.useState([]) + const [isSearchingItems, setIsSearchingItems] = React.useState(false) + + // 데이터 상태 + const [workTypes, setWorkTypes] = React.useState([]) + const [availableItems, setAvailableItems] = React.useState([]) + const [isLoadingItems, setIsLoadingItems] = React.useState(false) + + // RFQ 생성 폼 + const form = useForm({ + resolver: zodResolver(createRfqSchema), + defaultValues: { + biddingProjectId: undefined, + materialCodes: [], + dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후 + } + }) + + // 공종 목록 로드 + React.useEffect(() => { + const loadWorkTypes = async () => { + const types = await getWorkTypes() + setWorkTypes(types) + } + loadWorkTypes() + }, []) + + // 아이템 데이터 로드 + const loadItems = React.useCallback(async () => { + setIsLoadingItems(true) + try { + let result + if (itemSearchQuery.trim()) { + result = await searchShipbuildingItems(itemSearchQuery, selectedWorkType || undefined) + } else { + result = await getShipbuildingItemsByWorkType(selectedWorkType || undefined) + } + + if (result.error) { + toast.error(`아이템 로드 오류: ${result.error}`) + setAvailableItems([]) + } else { + setAvailableItems(result.data || []) + } + } catch (error) { + console.error("아이템 로드 오류:", error) + toast.error("아이템을 불러오는 중 오류가 발생했습니다") + setAvailableItems([]) + } finally { + setIsLoadingItems(false) + } + }, [itemSearchQuery, selectedWorkType]) + + // 아이템 검색 디바운스 + React.useEffect(() => { + setIsSearchingItems(true) + const timer = setTimeout(() => { + loadItems() + setIsSearchingItems(false) + }, 300) + + return () => clearTimeout(timer) + }, [loadItems]) + + // 프로젝트 선택 처리 + const handleProjectSelect = (project: Project) => { + setSelectedProject(project) + form.setValue("biddingProjectId", project.id) + // 선택 초기화 + setSelectedItems([]) + form.setValue("materialCodes", []) + } + + // 아이템 선택/해제 처리 + const handleItemToggle = (item: ShipbuildingItem) => { + const isSelected = selectedItems.some(selected => selected.id === item.id) + + if (isSelected) { + // 아이템 선택 해제 + const newSelectedItems = selectedItems.filter(selected => selected.id !== item.id) + setSelectedItems(newSelectedItems) + form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) + } else { + // 아이템 선택 추가 + const newSelectedItems = [...selectedItems, item] + setSelectedItems(newSelectedItems) + form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) + } + } + + // 아이템 제거 처리 + const handleRemoveItem = (itemId: number) => { + const newSelectedItems = selectedItems.filter(item => item.id !== itemId) + setSelectedItems(newSelectedItems) + form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) + } + + // RFQ 생성 함수 + const handleCreateRfq = async (data: CreateRfqFormValues) => { + try { + setIsProcessing(true) + + // 사용자 인증 확인 + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다") + } + + // 자재코드(item_code) 배열을 materialGroupCodes로 전달 + const result = await createTechSalesRfq({ + biddingProjectId: data.biddingProjectId, + materialGroupCodes: data.materialCodes, // item_code를 자재코드로 사용 + createdBy: Number(session.user.id), + dueDate: data.dueDate, + }) + + if (result.error) { + throw new Error(result.error) + } + + // 성공적으로 생성되면 다이얼로그 닫기 및 메시지 표시 + toast.success(`${result.data?.length || 0}개의 RFQ가 성공적으로 생성되었습니다`) + setIsDialogOpen(false) + form.reset({ + biddingProjectId: undefined, + materialCodes: [], + dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후로 재설정 + }) + setSelectedProject(null) + setItemSearchQuery("") + setSelectedWorkType(null) + setSelectedItems([]) + setAvailableItems([]) + + // 생성 후 콜백 실행 + if (onCreated) { + onCreated() + } + + } catch (error) { + console.error("RFQ 생성 오류:", error) + toast.error(`RFQ 생성 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) + } finally { + setIsProcessing(false) + } + } + + return ( + { + setIsDialogOpen(open) + if (!open) { + form.reset({ + biddingProjectId: undefined, + materialCodes: [], + dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후로 재설정 + }) + setSelectedProject(null) + setItemSearchQuery("") + setSelectedWorkType(null) + setSelectedItems([]) + setAvailableItems([]) + } + }} + > + + + + + + RFQ 생성 + + +
+
+ + {/* 프로젝트 선택 */} + ( + + 입찰 프로젝트 + + + + + + )} + /> + + + + {/* 마감일 설정 */} + ( + + 마감일 + + + + + + + + + date < new Date() || date < new Date("1900-01-01") + } + initialFocus + /> + + + + 벤더가 견적을 제출해야 하는 마감일입니다. + + + + )} + /> + + + + {!selectedProject ? ( +
+ 먼저 프로젝트를 선택해주세요 +
+ ) : ( +
+ {/* 아이템 선택 영역 */} +
+
+ 조선 아이템 선택 + + 공종별 아이템을 선택하세요 + +
+ + {/* 아이템 검색 및 필터 */} +
+
+
+ + setItemSearchQuery(e.target.value)} + className="pl-8 pr-8" + /> + {itemSearchQuery && ( + + )} + {isSearchingItems && ( + + )} +
+ + {/* 공종 필터 */} + + + + + + setSelectedWorkType(null)} + > + 전체 공종 + + {workTypes.map(workType => ( + setSelectedWorkType(workType.code)} + > + {workType.name} + + ))} + + +
+
+ + {/* 아이템 목록 */} +
+ +
+ {isLoadingItems ? ( +
+ + 아이템을 불러오는 중... +
+ ) : availableItems.length > 0 ? ( + availableItems.map((item) => { + const isSelected = selectedItems.some(selected => selected.id === item.id) + return ( +
handleItemToggle(item)} + > +
+ {isSelected ? ( + + ) : ( + + )} +
+
+ {item.itemList || item.itemName} +
+
+ {item.itemCode} • {item.description || '설명 없음'} +
+
+ 공종: {item.workType} • 선종: {item.shipTypes} +
+
+
+
+ ) + }) + ) : ( +
+ {itemSearchQuery ? "검색 결과가 없습니다" : "아이템이 없습니다"} +
+ )} +
+
+
+ + {/* 선택된 아이템 목록 */} + ( + + 선택된 아이템 ({selectedItems.length}개) +
+ {selectedItems.length > 0 ? ( +
+ {selectedItems.map((item) => ( + + {item.itemList || item.itemName} ({item.itemCode}) + handleRemoveItem(item.id)} + /> + + ))} +
+ ) : ( +
+ 선택된 아이템이 없습니다 +
+ )} +
+ +
+ )} + /> +
+
+ )} + + {/* 안내 메시지 */} + {selectedProject && ( +
+

• 공종별 조선 아이템을 선택하세요.

+

• 선택된 아이템의 자재코드(item_code)별로 개별 RFQ가 생성됩니다.

+

• 아이템 코드가 자재 그룹 코드로 사용됩니다.

+

• 마감일은 벤더가 견적을 제출해야 하는 날짜입니다.

+
+ )} + +
+ + +
+ + +
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3