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/rfq-filter-sheet.tsx | 759 +++++++++++++++++++++++++++ 1 file changed, 759 insertions(+) create mode 100644 lib/techsales-rfq/table/rfq-filter-sheet.tsx (limited to 'lib/techsales-rfq/table/rfq-filter-sheet.tsx') diff --git a/lib/techsales-rfq/table/rfq-filter-sheet.tsx b/lib/techsales-rfq/table/rfq-filter-sheet.tsx new file mode 100644 index 00000000..6021699f --- /dev/null +++ b/lib/techsales-rfq/table/rfq-filter-sheet.tsx @@ -0,0 +1,759 @@ +"use client" + +import { useEffect, useTransition, useState, useRef } from "react" +import { useRouter, useParams } from "next/navigation" +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Search, X } from "lucide-react" +import { customAlphabet } from "nanoid" +import { parseAsStringEnum, useQueryState } from "nuqs" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { DateRangePicker } from "@/components/date-range-picker" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" +import { useTranslation } from '@/i18n/client' +import { getFiltersStateParser } from "@/lib/parsers" + +// nanoid 생성기 +const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6) + +// 필터 스키마 정의 (TechSales RFQ에 맞게 수정) +const filterSchema = z.object({ + rfqCode: z.string().optional(), + materialCode: z.string().optional(), + itemName: z.string().optional(), + pspid: z.string().optional(), + projNm: z.string().optional(), + ptypeNm: z.string().optional(), + createdByName: z.string().optional(), + status: z.string().optional(), + dateRange: z.object({ + from: z.date().optional(), + to: z.date().optional(), + }).optional(), +}) + +// 상태 옵션 정의 (TechSales RFQ 상태에 맞게 수정) +const statusOptions = [ + { value: "RFQ Created", label: "RFQ Created" }, + { value: "RFQ Vendor Assignned", label: "RFQ Vendor Assignned" }, + { value: "RFQ Sent", label: "RFQ Sent" }, + { value: "Quotation Analysis", label: "Quotation Analysis" }, + { value: "Closed", label: "Closed" }, +] + +type FilterFormValues = z.infer + +interface RFQFilterSheetProps { + isOpen: boolean; + onClose: () => void; + onSearch?: () => void; + isLoading?: boolean; +} + +// Updated component for inline use (not a sheet anymore) +export function RFQFilterSheet({ + isOpen, + onClose, + onSearch, + isLoading = false +}: RFQFilterSheetProps) { + const router = useRouter() + const params = useParams(); + const lng = params ? (params.lng as string) : 'ko'; + const { t } = useTranslation(lng); + + const [isPending, startTransition] = useTransition() + + // 초기화 상태 추가 - 폼 초기화 중에는 상태 변경을 방지 + const [isInitializing, setIsInitializing] = useState(false) + // 마지막으로 적용된 필터를 추적하기 위한 ref + const lastAppliedFilters = useRef("") + + // nuqs로 URL 상태 관리 - 파라미터명을 'basicFilters'로 변경 + const [filters, setFilters] = useQueryState( + "basicFilters", + getFiltersStateParser().withDefault([]) + ) + + // joinOperator 설정 + const [joinOperator, setJoinOperator] = useQueryState( + "basicJoinOperator", + parseAsStringEnum(["and", "or"]).withDefault("and") + ) + + // 현재 URL의 페이지 파라미터도 가져옴 + const [page, setPage] = useQueryState("page", { defaultValue: "1" }) + + // 폼 상태 초기화 + const form = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + rfqCode: "", + materialCode: "", + itemName: "", + pspid: "", + projNm: "", + ptypeNm: "", + createdByName: "", + status: "", + dateRange: { + from: undefined, + to: undefined, + }, + }, + }) + + // URL 필터에서 초기 폼 상태 설정 - 개선된 버전 + useEffect(() => { + // 현재 필터를 문자열로 직렬화 + const currentFiltersString = JSON.stringify(filters); + + // 패널이 열렸고, 필터가 있고, 마지막에 적용된 필터와 다를 때만 업데이트 + if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) { + setIsInitializing(true); + + const formValues = { ...form.getValues() }; + let formUpdated = false; + + filters.forEach(filter => { + if (filter.id === "rfqSendDate" && Array.isArray(filter.value) && filter.value.length > 0) { + formValues.dateRange = { + from: filter.value[0] ? new Date(filter.value[0]) : undefined, + to: filter.value[1] ? new Date(filter.value[1]) : undefined, + }; + formUpdated = true; + } else if (filter.id in formValues) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (formValues as any)[filter.id] = filter.value; + formUpdated = true; + } + }); + + // 폼 값이 변경된 경우에만 reset으로 한 번에 업데이트 + if (formUpdated) { + form.reset(formValues); + lastAppliedFilters.current = currentFiltersString; + } + + setIsInitializing(false); + } + }, [filters, isOpen, form]) // form 의존성 추가 + + // 현재 적용된 필터 카운트 + const getActiveFilterCount = () => { + return filters?.length || 0 + } + + // 조회 버튼 클릭 핸들러 + const handleSearch = () => { + // 필터 패널 닫기 로직이 있다면 여기에 추가 + if (onSearch) { + onSearch(); + } + } + + // 폼 제출 핸들러 - 개선된 버전 + async function onSubmit(data: FilterFormValues) { + // 초기화 중이면 제출 방지 + if (isInitializing) return; + + startTransition(async () => { + try { + // 필터 배열 생성 + const newFilters = [] + + if (data.rfqCode?.trim()) { + newFilters.push({ + id: "rfqCode", + value: data.rfqCode.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.materialCode?.trim()) { + newFilters.push({ + id: "materialCode", + value: data.materialCode.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.itemName?.trim()) { + newFilters.push({ + id: "itemName", + value: data.itemName.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.pspid?.trim()) { + newFilters.push({ + id: "pspid", + value: data.pspid.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.projNm?.trim()) { + newFilters.push({ + id: "projNm", + value: data.projNm.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.ptypeNm?.trim()) { + newFilters.push({ + id: "ptypeNm", + value: data.ptypeNm.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.createdByName?.trim()) { + newFilters.push({ + id: "createdByName", + value: data.createdByName.trim(), + type: "text" as const, + operator: "iLike" as const, + rowId: generateId() + }) + } + + if (data.status?.trim()) { + newFilters.push({ + id: "status", + value: data.status.trim(), + type: "select" as const, + operator: "eq" as const, + rowId: generateId() + }) + } + + // Add date range to params if it exists + if (data.dateRange?.from) { + newFilters.push({ + id: "rfqSendDate", + value: [ + data.dateRange.from.toISOString().split('T')[0], + data.dateRange.to ? data.dateRange.to.toISOString().split('T')[0] : undefined + ].filter(Boolean) as string[], + type: "date" as const, + operator: "isBetween" as const, + rowId: generateId() + }) + } + + console.log("기본 필터 적용:", newFilters); + + // 마지막 적용된 필터 업데이트 + lastAppliedFilters.current = JSON.stringify(newFilters); + + // 먼저 필터를 설정 + await setFilters(newFilters.length > 0 ? newFilters : null); + + // 그 다음 페이지를 1로 설정 + await setPage("1"); + + // 필터 업데이트 후 조회 핸들러 호출 (제공된 경우) + handleSearch(); + + // 페이지 새로고침으로 서버 데이터 다시 가져오기 + setTimeout(() => { + window.location.reload(); + }, 100); + } catch (error) { + console.error("필터 적용 오류:", error); + } + }) + } + + // 필터 초기화 핸들러 - 개선된 버전 + async function handleReset() { + try { + setIsInitializing(true); + + form.reset({ + rfqCode: "", + materialCode: "", + itemName: "", + pspid: "", + projNm: "", + ptypeNm: "", + createdByName: "", + status: "", + dateRange: { from: undefined, to: undefined }, + }); + + // 필터와 조인 연산자를 초기화 + await setFilters(null); + await setJoinOperator("and"); + await setPage("1"); + + // 마지막 적용된 필터 초기화 + lastAppliedFilters.current = ""; + + console.log("필터 초기화 완료"); + setIsInitializing(false); + + // 페이지 새로고침으로 서버 데이터 다시 가져오기 + setTimeout(() => { + window.location.reload(); + }, 100); + } catch (error) { + console.error("필터 초기화 오류:", error); + setIsInitializing(false); + } + } + + // Don't render if not open (for side panel use) + if (!isOpen) { + return null; + } + + return ( +
+ {/* Filter Panel Header - 보더 제거, 배경 색상 적용 */} +
+

검색 필터

+
+ + {/* Join Operator Selection - 보더 제거, 배경 색상 적용 */} +
+ + +
+ +
+ + {/* Scrollable content area - 헤더와 버튼 사이에서 스크롤 */} +
+
+ {/* RFQ NO. */} + ( + + {t("RFQ NO.")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 자재코드 */} + ( + + {t("자재코드")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 자재명 */} + ( + + {t("자재명")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 프로젝트 ID */} + ( + + {t("프로젝트 ID")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 프로젝트명 */} + ( + + {t("프로젝트명")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 선종명 */} + ( + + {t("선종명")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 요청자 */} + ( + + {t("요청자")} + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* Status */} + ( + + {t("Status")} + + + + )} + /> + + {/* RFQ 전송일 */} + ( + + {t("RFQ 전송일")} + +
+ + {(field.value?.from || field.value?.to) && ( + + )} +
+
+ +
+ )} + /> +
+
+ + {/* Fixed buttons at bottom - 보더 제거, 배경 색상 적용 */} +
+
+ + +
+
+
+ +
+ ) +} \ No newline at end of file -- cgit v1.2.3