"use client" import * as React from "react" import { useRouter, useSearchParams } from "next/navigation" import { Button } from "@/components/ui/button" import { PanelLeftClose, PanelLeftOpen } from "lucide-react" import { toast } from "sonner" import type { DataTableAdvancedFilterField, DataTableFilterField, DataTableRowAction, } from "@/types/table" import { updateInvestigationDetailsAction } from "../service" import { createSiteVisitRequestAction, getSiteVisitRequestAction } from "@/lib/site-visit/service" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { getPQSubmissions } from "../service" import { getColumns, PQSubmission } from "./vendors-table-columns" import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions" import { PQFilterSheet } from "./pq-filter-sheet" import { SiteVisitDialog } from "./site-visit-dialog" import { VendorInfoViewDialog } from "@/lib/site-visit/vendor-info-view-dialog" import { EditInvestigationDialog } from "./edit-investigation-dialog" import { cn } from "@/lib/utils" // TablePresetManager 관련 import 추가 import { useTablePresets } from "@/components/data-table/use-table-presets" import { TablePresetManager } from "@/components/data-table/data-table-preset" import { useMemo } from "react" interface PQSubmissionsTableProps { promises: Promise<[Awaited>]> className?: string } export function PQSubmissionsTable({ promises, className }: PQSubmissionsTableProps) { const [rowAction, setRowAction] = React.useState | null>(null) const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) // 방문실사 다이얼로그 상태 const [isSiteVisitDialogOpen, setIsSiteVisitDialogOpen] = React.useState(false) const [selectedInvestigation, setSelectedInvestigation] = React.useState(null) const [isVendorInfoViewDialogOpen, setIsVendorInfoViewDialogOpen] = React.useState(false) const [selectedSiteVisitRequestId, setSelectedSiteVisitRequestId] = React.useState(null) const [selectedInvestigationId, setSelectedInvestigationId] = React.useState(null) // 실사 정보 수정 다이얼로그 상태 const [isEditInvestigationDialogOpen, setIsEditInvestigationDialogOpen] = React.useState(false) const [selectedInvestigationForEdit, setSelectedInvestigationForEdit] = React.useState(null) const router = useRouter() const searchParams = useSearchParams() // Container wrapper의 위치를 측정하기 위한 ref const containerRef = React.useRef(null) const [containerTop, setContainerTop] = React.useState(0) // Container 위치 측정 함수 - top만 측정 const updateContainerBounds = React.useCallback(() => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect() setContainerTop(rect.top) } }, []) // 컴포넌트 마운트 시와 윈도우 리사이즈 시 위치 업데이트 React.useEffect(() => { updateContainerBounds() const handleResize = () => { updateContainerBounds() } window.addEventListener('resize', handleResize) window.addEventListener('scroll', updateContainerBounds) return () => { window.removeEventListener('resize', handleResize) window.removeEventListener('scroll', updateContainerBounds) } }, [updateContainerBounds]) // Suspense 방식으로 데이터 처리 const [promiseData] = React.use(promises) const tableData = promiseData // 디버깅용 로그 console.log("PQ Table Data:", { dataLength: tableData.data?.length, pageCount: tableData.pageCount, sampleData: tableData.data?.[0] }) // 방문실사 다이얼로그 핸들러 const handleSiteVisitRequest = async (data: { inspectionDuration: number requestedStartDate: Date requestedEndDate: Date shiAttendees: { [key: string]: { checked: boolean; attendees: Array<{ name: string; department?: string; email?: string; }>; }; } shiAttendeeDetails?: string vendorRequests: Record otherVendorRequests?: string additionalRequests?: string }, attachments?: File[]) => { try { console.log("data", data) const result = await createSiteVisitRequestAction({ investigationId: selectedInvestigation?.investigation?.id || 0, ...data, attachments }) if (result.success) { toast.success(result.message || "방문실사 요청이 성공적으로 발송되었습니다.") handleCloseSiteVisitDialog() } else { toast.error(result.error || "방문실사 요청 발송 중 오류가 발생했습니다.") } } catch (error) { console.error("방문실사 요청 오류:", error) toast.error("방문실사 요청 발송 중 오류가 발생했습니다.") } } // 방문실사 다이얼로그 열기 const handleOpenSiteVisitDialog = async (investigation: PQSubmission) => { try { // // 기존 방문실사 요청이 있는지 확인 // const existingRequest = await getSiteVisitRequestAction(investigation.investigation?.id || 0) // if (existingRequest.success && existingRequest.data) { // toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.") // return // } console.log("investigation", investigation) setSelectedInvestigation(investigation) setIsSiteVisitDialogOpen(true) } catch (error) { console.error("방문실사 요청 상태 확인 중 오류:", error) toast.error("방문실사 요청 상태 확인 중 오류가 발생했습니다.") } } // 방문실사 다이얼로그 닫기 const handleCloseSiteVisitDialog = () => { setIsSiteVisitDialogOpen(false) setSelectedInvestigation(null) } // 실사 정보 수정 핸들러 const handleEditInvestigation = async (data: { confirmedAt?: Date evaluationResult?: "APPROVED" | "SUPPLEMENT" | "REJECTED" investigationNotes?: string }) => { if (!selectedInvestigationForEdit?.investigation?.id) { toast.error("실사 정보를 찾을 수 없습니다.") return } try { const result = await updateInvestigationDetailsAction({ investigationId: selectedInvestigationForEdit.investigation.id, ...data }) if (result.success) { toast.success(result.message || "실사 정보가 성공적으로 업데이트되었습니다.") setIsEditInvestigationDialogOpen(false) setSelectedInvestigationForEdit(null) } else { toast.error(result.error || "실사 정보 업데이트 중 오류가 발생했습니다.") } } catch (error) { console.error("실사 정보 업데이트 오류:", error) toast.error("실사 정보 업데이트 중 오류가 발생했습니다.") } } // 실사 정보 수정 다이얼로그 닫기 const handleCloseEditInvestigationDialog = () => { setIsEditInvestigationDialogOpen(false) setSelectedInvestigationForEdit(null) } // rowAction 핸들러 React.useEffect(() => { if (rowAction?.type === "site-visit") { // 방문실사 다이얼로그 열기 handleOpenSiteVisitDialog(rowAction.row) setRowAction(null) } else if (rowAction?.type === "vendor-info-view") { // 협력업체 정보 조회 다이얼로그 열기 setSelectedSiteVisitRequestId(rowAction.row.siteVisitRequestId || null) setSelectedInvestigationId(rowAction.row.investigation?.id || null) setIsVendorInfoViewDialogOpen(true) setRowAction(null) } else if (rowAction?.type === "update") { // 실사 정보 수정 다이얼로그 열기 setSelectedInvestigationForEdit(rowAction.row) setIsEditInvestigationDialogOpen(true) setRowAction(null) } }, [rowAction]) // 초기 설정 정의 (RFQ와 동일한 패턴) const initialSettings = React.useMemo(() => ({ page: parseInt(searchParams.get('page') || '1'), perPage: parseInt(searchParams.get('perPage') || '10'), sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "updatedAt", desc: true }], filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and", basicFilters: searchParams.get('basicFilters') || searchParams.get('pqBasicFilters') ? JSON.parse(searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')!) : [], basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and", search: searchParams.get('search') || '', from: searchParams.get('from') || undefined, to: searchParams.get('to') || undefined, columnVisibility: {}, columnOrder: [], pinnedColumns: { left: [], right: ["actions"] }, // PQ는 actions를 오른쪽에 고정 groupBy: [], expandedRows: [] }), [searchParams]) // DB 기반 프리셋 훅 사용 const { presets, activePresetId, hasUnsavedChanges, isLoading: presetsLoading, createPreset, applyPreset, updatePreset, deletePreset, setDefaultPreset, renamePreset, getCurrentSettings, } = useTablePresets('pq-submissions-table', initialSettings) const columns = React.useMemo( () => getColumns({ setRowAction, router }), [setRowAction, router] ) // PQ 제출 필터링을 위한 필드 정의 const filterFields: DataTableFilterField[] = [ { id: "vendorName", label: "협력업체" }, { id: "projectName", label: "프로젝트" }, { id: "status", label: "상태" }, ] // 고급 필터 필드 정의 const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "requesterName", label: "요청자명", type: "text" }, { id: "pqNumber", label: "PQ 번호", type: "text" }, { id: "vendorName", label: "협력업체명", type: "text" }, { id: "vendorCode", label: "협력업체 코드", type: "text" }, { id: "type", label: "PQ 유형", type: "select", options: [ { label: "일반 PQ", value: "GENERAL" }, { label: "프로젝트 PQ", value: "PROJECT" }, ]}, { id: "projectName", label: "프로젝트명", type: "text" }, { id: "status", label: "PQ 상태", type: "select", options: [ { label: "요청됨", value: "REQUESTED" }, { label: "진행 중", value: "IN_PROGRESS" }, { label: "제출됨", value: "SUBMITTED" }, { label: "승인됨", value: "APPROVED" }, { label: "거부됨", value: "REJECTED" }, ]}, { id: "pqItems", label: "실사품목", type: "text" }, { id: "createdAt", label: "PQ 전송일", type: "date" }, { id: "submittedAt", label: "제출일", type: "date" }, { id: "approvedAt", label: "승인일", type: "date" }, { id: "rejectedAt", label: "거부일", type: "date" }, ] // 현재 설정 가져오기 const currentSettings = useMemo(() => { return getCurrentSettings() }, [getCurrentSettings]) // useDataTable 초기 상태 설정 (RFQ와 동일한 패턴) const initialState = useMemo(() => { return { sorting: initialSettings.sort.filter(sortItem => { const columnExists = columns.some(col => col.accessorKey === sortItem.id) return columnExists }), columnVisibility: currentSettings.columnVisibility, columnPinning: currentSettings.pinnedColumns, // DateRangePicker의 from/to 파라미터를 위한 상태 columnFilters: initialSettings.from || initialSettings.to ? [ { id: "createdAt", value: { from: initialSettings.from ? new Date(initialSettings.from) : undefined, to: initialSettings.to ? new Date(initialSettings.to) : undefined, } } ] : [], } }, [currentSettings, initialSettings.sort, initialSettings.from, initialSettings.to, columns]) const { table } = useDataTable({ data: tableData.data, columns, pageCount: tableData.pageCount, rowCount: tableData.total || tableData.data.length, // total 추가 filterFields, // RFQ와 달리 빈 배열이 아닌 실제 필터 필드 사용 enablePinning: true, enableAdvancedFilter: true, enableRowSelection: true, maxSelections: 1, initialState: { sorting: [{ id: "updatedAt", desc: true }], columnPinning: { right: ["actions"] }, }, getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, }) // 조회 버튼 클릭 핸들러 - PQFilterSheet에 전달 const handleSearch = () => { // Close the panel after search setIsFilterPanelOpen(false) } // Get active basic filter count const getActiveBasicFilterCount = () => { try { const basicFilters = searchParams.get('basicFilters') || searchParams.get('pqBasicFilters') return basicFilters ? JSON.parse(basicFilters).length : 0 } catch { return 0 } } // Filter panel width const FILTER_PANEL_WIDTH = 400; return ( <> {/* Filter Panel - fixed positioning으로 화면 최대 좌측에서 시작 */}
{/* Filter Content */}
setIsFilterPanelOpen(false)} onSearch={handleSearch} isLoading={false} />
{/* Main Content Container */}
{/* Main Content - 너비 조정으로 필터 패널 공간 확보 */}
{/* Header Bar */}
{/* Right side info */}
{tableData && ( 총 {tableData.total || tableData.data.length}건 )}
{/* Table Content Area */}
{/* DB 기반 테이블 프리셋 매니저 추가 */} presets={presets} activePresetId={activePresetId} currentSettings={currentSettings} hasUnsavedChanges={hasUnsavedChanges} isLoading={presetsLoading} onCreatePreset={createPreset} onUpdatePreset={updatePreset} onDeletePreset={deletePreset} onApplyPreset={applyPreset} onSetDefaultPreset={setDefaultPreset} onRenamePreset={renamePreset} /> {/* 기존 툴바 액션들 */}
{/* 방문실사 다이얼로그 */} {selectedInvestigation && ( )} {/* 협력업체 정보 조회 다이얼로그 */} { setIsVendorInfoViewDialogOpen(false) setSelectedSiteVisitRequestId(null) setSelectedInvestigationId(null) }} siteVisitRequestId={selectedSiteVisitRequestId} investigationId={selectedInvestigationId} /> {/* 실사 정보 수정 다이얼로그 */} ) }