"use client"; import * as React from "react"; import { useParams, useRouter, usePathname } from "next/navigation"; import { useTranslation } from "@/i18n/client"; import { ClientDataTable } from "../client-data-table/data-table"; import { getColumns, DataTableRowAction, DataTableColumnJSON, ColumnType, } from "./form-data-table-columns"; import type { DataTableAdvancedFilterField } from "@/types/table"; import { Button } from "../ui/button"; import { Download, Loader, Upload, Plus, Tag, TagsIcon, FileOutput, Clipboard, Send, GitCompareIcon, RefreshCcw, Trash2, Eye, FileText, Target, CheckCircle2, AlertCircle, Clock } from "lucide-react"; import { toast } from "sonner"; import { getPackageCodeById, getProjectById, getReportTempList, sendFormDataToSEDP, syncMissingTags, } from "@/lib/forms/services"; import { UpdateTagSheet } from "./update-form-sheet"; import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog"; import { FormDataReportDialog } from "./form-data-report-dialog"; import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AddFormTagDialog } from "./add-formTag-dialog"; import { importExcelData } from "./import-excel-form"; import { exportExcelData } from "./export-excel-form"; import { SEDPConfirmationDialog, SEDPStatusDialog } from "./sedp-components"; import { SEDPCompareDialog } from "./sedp-compare-dialog"; import { DeleteFormDataDialog } from "./delete-form-data-dialog"; import { TemplateViewDialog } from "./spreadJS-dialog"; import { fetchTemplateFromSEDP } from "@/lib/forms/sedp-actions"; import { FormStatusByVendor, getFormStatusByVendor } from "@/lib/forms/stat"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; interface GenericData { [key: string]: unknown; } export interface DynamicTableProps { dataJSON: GenericData[]; columnsJSON: DataTableColumnJSON[]; contractItemId: number; formCode: string; formId: number; projectId: number; formName?: string; objectCode?: string; mode: "IM" | "ENG"; // 모드 속성 editableFieldsMap?: Map; // 새로 추가 } export default function DynamicTable({ dataJSON, columnsJSON, contractItemId, formCode, formId, projectId, mode = "IM", // 기본값 설정 formName = `${formCode}`, // Default form name based on formCode editableFieldsMap = new Map(), // 새로 추가 }: DynamicTableProps) { const params = useParams(); const router = useRouter(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); const [rowAction, setRowAction] = React.useState | null>(null); const [tableData, setTableData] = React.useState(dataJSON); // 배치 선택 관련 상태 const [selectedRowsData, setSelectedRowsData] = React.useState([]); const [clearSelection, setClearSelection] = React.useState(false); // 삭제 관련 상태 간소화 const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); const [deleteTarget, setDeleteTarget] = React.useState([]); const [formStats, setFormStats] = React.useState(null); const [isLoadingStats, setIsLoadingStats] = React.useState(true); React.useEffect(() => { const fetchFormStats = async () => { try { setIsLoadingStats(true); // getFormStatusByVendor 서버 액션 직접 호출 const data = await getFormStatusByVendor(projectId, contractItemId, formCode); if (data && data.length > 0) { setFormStats(data[0]); } } catch (error) { console.error("Failed to fetch form stats:", error); toast.error("통계 데이터를 불러오는데 실패했습니다."); } finally { setIsLoadingStats(false); } }; if (projectId && formCode) { fetchFormStats(); } }, [projectId, formCode]); // Update tableData when dataJSON changes React.useEffect(() => { setTableData(dataJSON); }, [dataJSON]); // 폴링 상태 관리를 위한 ref const pollingRef = React.useRef(null); // Separate loading states for different operations const [isSyncingTags, setIsSyncingTags] = React.useState(false); const [isImporting, setIsImporting] = React.useState(false); const [isExporting, setIsExporting] = React.useState(false); const [isSaving] = React.useState(false); const [isSendingSEDP, setIsSendingSEDP] = React.useState(false); const [isLoadingTags, setIsLoadingTags] = React.useState(false); const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false); // 새로 추가 // Any operation in progress const isAnyOperationPending = isSyncingTags || isImporting || isExporting || isSaving || isSendingSEDP || isLoadingTags || isLoadingTemplate; // SEDP dialogs state const [sedpConfirmOpen, setSedpConfirmOpen] = React.useState(false); const [sedpStatusOpen, setSedpStatusOpen] = React.useState(false); const [sedpStatusData, setSedpStatusData] = React.useState({ status: 'success' as 'success' | 'error' | 'partial', message: '', successCount: 0, errorCount: 0, totalCount: 0 }); // SEDP compare dialog state const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false); const [projectCode, setProjectCode] = React.useState(''); const [projectType, setProjectType] = React.useState('plant'); const [packageCode, setPackageCode] = React.useState(''); // 새로 추가된 Template 다이얼로그 상태 const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false); const [templateData, setTemplateData] = React.useState(null); const [tempUpDialog, setTempUpDialog] = React.useState(false); const [reportData, setReportData] = React.useState([]); const [batchDownDialog, setBatchDownDialog] = React.useState(false); const [tempCount, setTempCount] = React.useState(0); const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false); // TAG_NO가 있는 첫 번째 행의 shi 값 확인 const isAddTagDisabled = React.useMemo(() => { const firstRowWithTagNo = tableData.find(row => row.TAG_NO); return firstRowWithTagNo?.shi === true; }, [tableData]); // Clean up polling on unmount React.useEffect(() => { return () => { if (pollingRef.current) { clearInterval(pollingRef.current); } }; }, []); React.useEffect(() => { const getTempCount = async () => { const tempList = await getReportTempList(contractItemId, formId); setTempCount(tempList.length); }; getTempCount(); }, [contractItemId, formId, tempUpDialog]); React.useEffect(() => { const getPackageCode = async () => { try { const packageCode = await getPackageCodeById(contractItemId); setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값 } catch (error) { console.error('패키지 조회 실패:', error); setPackageCode(''); } }; getPackageCode(); }, [contractItemId]) // Get project code when component mounts React.useEffect(() => { const getProjectCode = async () => { try { const project = await getProjectById(projectId); setProjectCode(project.code); setProjectType(project.type); } catch (error) { console.error("Error fetching project code:", error); toast.error("Failed to fetch project code"); } }; if (projectId) { getProjectCode(); } }, [projectId]); // 선택된 행들의 실제 데이터 가져오기 const getSelectedRowsData = React.useCallback(() => { return selectedRowsData; }, [selectedRowsData]); // 선택된 행 개수 계산 const selectedRowCount = React.useMemo(() => { return selectedRowsData.length; }, [selectedRowsData]); const columns = React.useMemo( () => getColumns({ columnsJSON, setRowAction, setReportData, tempCount, }), [columnsJSON, setRowAction, setReportData, tempCount] ); function mapColumnTypeToAdvancedFilterType( columnType: ColumnType ): DataTableAdvancedFilterField["type"] { switch (columnType) { case "STRING": return "text"; case "NUMBER": return "number"; case "LIST": return "select"; default: return "text"; } } const advancedFilterFields = React.useMemo< DataTableAdvancedFilterField[] >(() => { return columnsJSON.map((col) => ({ id: col.key, label: col.label, type: mapColumnTypeToAdvancedFilterType(col.type), options: col.type === "LIST" ? col.options?.map((v) => ({ label: v, value: v })) : undefined, })); }, [columnsJSON]); // 새로 추가된 Template 가져오기 함수 const handleGetTemplate = async () => { if (!projectCode) { toast.error("Project code is not available"); return; } try { setIsLoadingTemplate(true); const templateResult = await fetchTemplateFromSEDP(projectCode, formCode); // 🔍 전달되는 템플릿 데이터 로깅 console.log('📊 Template data received from SEDP:', { count: Array.isArray(templateResult) ? templateResult.length : 'not array', isArray: Array.isArray(templateResult), data: templateResult }); if (Array.isArray(templateResult)) { templateResult.forEach((tmpl, idx) => { console.log(` [${idx}] TMPL_ID: ${tmpl?.TMPL_ID || 'MISSING'}, NAME: ${tmpl?.NAME || 'N/A'}, TYPE: ${tmpl?.TMPL_TYPE || 'N/A'}`); }); } setTemplateData(templateResult); setTemplateDialogOpen(true); toast.success("Template data loaded successfully"); } catch (error) { console.error("Error fetching template:", error); toast.error("Failed to fetch template from SEDP"); } finally { setIsLoadingTemplate(false); } }; // IM 모드: 태그 동기화 함수 async function handleSyncTags() { try { setIsSyncingTags(true); const result = await syncMissingTags(contractItemId, formCode); // Prepare the toast messages based on what changed const changes = []; if (result.createdCount > 0) changes.push(`${result.createdCount}건 태그 생성`); if (result.updatedCount > 0) changes.push(`${result.updatedCount}건 태그 업데이트`); if (result.deletedCount > 0) changes.push(`${result.deletedCount}건 태그 삭제`); if (changes.length > 0) { // If any changes were made, show success message and reload toast.success(`동기화 완료: ${changes.join(", ")}`); router.refresh(); // Use router.refresh instead of location.reload } else { // If no changes were made, show an info message toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다."); } } catch (err) { console.error(err); toast.error("태그 동기화 중 에러가 발생했습니다."); } finally { setIsSyncingTags(false); } } // ENG 모드: 태그 가져오기 함수 const handleGetTags = async () => { try { setIsLoadingTags(true); // API 엔드포인트 호출 - 작업 시작만 요청 const response = await fetch('/api/cron/form-tags/start', { method: 'POST', body: JSON.stringify({ projectCode, formCode, contractItemId }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to start tag import'); } const data = await response.json(); // 작업 ID 저장 if (data.syncId) { toast.info('Tag import started. This may take a while...'); // 상태 확인을 위한 폴링 시작 startPolling(data.syncId); } else { throw new Error('No import ID returned from server'); } } catch (error) { console.error('Error starting tag import:', error); toast.error( error instanceof Error ? error.message : 'An error occurred while starting tag import' ); setIsLoadingTags(false); } }; const startPolling = (id: string) => { // 이전 폴링이 있다면 제거 if (pollingRef.current) { clearInterval(pollingRef.current); } // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { const response = await fetch(`/api/cron/form-tags/status?id=${id}`); if (!response.ok) { throw new Error('Failed to get tag import status'); } const data = await response.json(); if (data.status === 'completed') { // 폴링 중지 if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; } router.refresh(); // 상태 초기화 setIsLoadingTags(false); // 성공 메시지 표시 toast.success( `Tags imported successfully! ${data.result?.processedCount || 0} items processed.` ); } else if (data.status === 'failed') { // 에러 처리 if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; } setIsLoadingTags(false); toast.error(data.error || 'Import failed'); } else if (data.status === 'processing') { // 진행 상태 업데이트 (선택적) if (data.progress) { toast.info(`Import in progress: ${data.progress}%`, { id: `import-progress-${id}`, }); } } } catch (error) { console.error('Error checking importing status:', error); } }, 5000); // 5초마다 체크 }; // Excel Import - Fixed version with proper loading state management async function handleImportExcel(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; try { // Don't set setIsImporting here - let importExcelData handle it completely // setIsImporting(true); // Remove this line // Call the updated importExcelData function with editableFieldsMap const result = await importExcelData({ file, tableData, columnsJSON, formCode, contractItemId, editableFieldsMap, // 추가: 편집 가능 필드 정보 전달 onPendingChange: setIsImporting, // Let importExcelData handle loading state onDataUpdate: (newData) => { setTableData(Array.isArray(newData) ? newData : newData(tableData)); } }); // If import and save was successful, refresh the page if (result.success) { // Show additional info about skipped fields if any if (result.skippedFields && result.skippedFields.length > 0) { console.log("Import completed with some fields skipped:", result.skippedFields); } // Ensure loading state is cleared before refresh setIsImporting(false); // Add a small delay to ensure state update is processed setTimeout(() => { router.refresh(); }, 100); } } catch (error) { console.error("Import failed:", error); toast.error("Failed to import Excel data"); // Ensure loading state is cleared on error setIsImporting(false); } finally { // Always clear the file input value e.target.value = ""; // Don't set setIsImporting(false) here since we handle it above } } // SEDP Send handler (with confirmation) function handleSEDPSendClick() { if (tableData.length === 0) { toast.error("No data to send to SEDP"); return; } // Open confirmation dialog setSedpConfirmOpen(true); } // Handle SEDP compare button click function handleSEDPCompareClick() { if (tableData.length === 0) { toast.error("No data to compare with SEDP"); return; } if (!projectCode) { toast.error("Project code is not available"); return; } // Open compare dialog setSedpCompareOpen(true); } // Actual SEDP send after confirmation async function handleSEDPSendConfirmed() { try { setIsSendingSEDP(true); // Validate data const invalidData = tableData.filter((item) => { const tagNo = item.TAG_NO; return !tagNo || (typeof tagNo === 'string' && !tagNo.trim()); }); if (invalidData.length > 0) { toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`); setSedpConfirmOpen(false); return; } // Then send to SEDP - pass formCode instead of formName const sedpResult = await sendFormDataToSEDP( formCode, // Send formCode instead of formName projectId, // Project ID contractItemId, tableData, // Table data columnsJSON // Column definitions ); // Close confirmation dialog setSedpConfirmOpen(false); // Set status data based on result if (sedpResult.success) { setSedpStatusData({ status: 'success', message: "Data successfully sent to SEDP", successCount: tableData.length, errorCount: 0, totalCount: tableData.length }); } else { setSedpStatusData({ status: 'error', message: sedpResult.message || "Failed to send data to SEDP", successCount: 0, errorCount: tableData.length, totalCount: tableData.length }); } // Open status dialog to show result setSedpStatusOpen(true); // Refresh the route to get fresh data router.refresh(); } catch (err: unknown) { console.error("SEDP error:", err); // Set error status setSedpStatusData({ status: 'error', message: err instanceof Error ? err.message : "An unexpected error occurred", successCount: 0, errorCount: tableData.length, totalCount: tableData.length }); // Close confirmation and open status setSedpConfirmOpen(false); setSedpStatusOpen(true); } finally { setIsSendingSEDP(false); } } // Template Export async function handleExportExcel() { try { setIsExporting(true); await exportExcelData({ tableData, columnsJSON, formCode, editableFieldsMap, onPendingChange: setIsExporting }); } finally { setIsExporting(false); } } // Handle batch document with smart selection logic const handleBatchDocument = () => { if (tempCount === 0) { toast.error("업로드된 Template File이 없습니다."); return; } // 선택된 항목이 있으면 선택된 것만, 없으면 전체 사용 const selectedData = getSelectedRowsData(); if (selectedData.length > 0) { toast.info(`선택된 ${selectedData.length}개 항목으로 배치 문서를 생성합니다.`); } else { toast.info(`전체 ${tableData.length}개 항목으로 배치 문서를 생성합니다.`); } setBatchDownDialog(true); }; // 개별 행 삭제 핸들러 const handleDeleteRow = (rowData: GenericData) => { setDeleteTarget([rowData]); setDeleteDialogOpen(true); }; // 배치 삭제 핸들러 const handleBatchDelete = () => { const selectedData = getSelectedRowsData(); if (selectedData.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; } setDeleteTarget(selectedData); setDeleteDialogOpen(true); }; // 삭제 성공 후 처리 const handleDeleteSuccess = () => { // 로컬 상태에서 삭제된 항목들 제거 const tagNosToDelete = deleteTarget .map(item => item.TAG_NO) .filter(Boolean); setTableData(prev => prev.filter(item => !tagNosToDelete.includes(item.TAG_NO)) ); // 선택 상태 초기화 setSelectedRowsData([]); setClearSelection(prev => !prev); // ClientDataTable의 선택 상태 초기화 // 삭제 타겟 초기화 setDeleteTarget([]); }; // rowAction 처리 부분 수정 React.useEffect(() => { if (rowAction?.type === "delete") { handleDeleteRow(rowAction.row.original); setRowAction(null); // 액션 초기화 } }, [rowAction]); return ( <>
{/* Tag Count */} Total Tags
{isLoadingStats ? ( - ) : ( formStats?.tagCount || 0 )}

Total Tag Count

{/* Completion Rate */} Completion
{isLoadingStats ? ( - ) : ( `${formStats?.completionRate || 0}%` )}

{formStats ? `${formStats.completedFields} / ${formStats.totalFields}` : '-'}

{/* Completed Fields */} Completed
{isLoadingStats ? ( - ) : ( formStats?.completedFields || 0 )}

Completed Fields

{/* Remaining Fields */} Remaining
{isLoadingStats ? ( - ) : ( (formStats?.totalFields || 0) - (formStats?.completedFields || 0) )}

Remaining Fields

{/* Upcoming (7 days) */} Upcoming
{isLoadingStats ? ( - ) : ( formStats?.upcomingCount || 0 )}

Due in 7 Days

{/* Overdue */} Overdue
{isLoadingStats ? ( - ) : ( formStats?.overdueCount || 0 )}

Overdue

{/* 선택된 항목 수 표시 (선택된 항목이 있을 때만) */} {selectedRowCount > 0 && ( )} {/* 버튼 그룹 */}
{/* 태그 관리 드롭다운 */} {/* 모드에 따라 다른 태그 작업 표시 */} {mode === "IM" ? ( {t("buttons.syncTags")} ) : ( {t("buttons.getTags")} )} setAddTagDialogOpen(true)} disabled={isAnyOperationPending || isAddTagDisabled} > {t("buttons.addTags")} {/* 리포트 관리 드롭다운 */} setTempUpDialog(true)} disabled={isAnyOperationPending}> {t("buttons.uploadTemplate")} {t("buttons.batchDocument")} {selectedRowCount > 0 && ( {selectedRowCount} )} {/* IMPORT 버튼 (파일 선택) */} {/* EXPORT 버튼 */} {/* Template 보기 버튼 */} {/* COMPARE WITH SEDP 버튼 */} {/* SEDP 전송 버튼 */}
{/* Modal dialog for tag update */} { if (!open) setRowAction(null); }} columns={columnsJSON} rowData={rowAction?.row.original ?? null} formCode={formCode} contractItemId={contractItemId} editableFieldsMap={editableFieldsMap} onUpdateSuccess={(updatedValues) => { // Update the specific row in tableData when a single row is updated if (rowAction?.row.original?.TAG_NO) { const tagNo = rowAction.row.original.TAG_NO; setTableData(prev => prev.map(item => item.TAG_NO === tagNo ? updatedValues : item ) ); } }} /> { if (!open) { setDeleteDialogOpen(false); setDeleteTarget([]); } }} onSuccess={handleDeleteSuccess} showTrigger={false} /> {/* Dialog for adding tags */} {/* 새로 추가된 Template 다이얼로그 */} setTemplateDialogOpen(false)} templateData={templateData} selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용 tableData={tableData} // SPR_LST_SETUP용 - 새로 추가 formCode={formCode} contractItemId={contractItemId} editableFieldsMap={editableFieldsMap} columnsJSON={columnsJSON} onUpdateSuccess={(updatedValues) => { // 업데이트 로직도 수정해야 함 - 단일 행 또는 복수 행 처리 if (Array.isArray(updatedValues)) { // SPR_LST_SETUP의 경우 - 복수 행 업데이트 const updatedData = [...tableData]; updatedValues.forEach(updatedItem => { const index = updatedData.findIndex(item => item.TAG_NO === updatedItem.TAG_NO); if (index !== -1) { updatedData[index] = updatedItem; } }); setTableData(updatedData); } else { // SPR_ITM_LST_SETUP의 경우 - 단일 행 업데이트 const tagNo = updatedValues.TAG_NO; if (tagNo) { setTableData(prev => prev.map(item => item.TAG_NO === tagNo ? updatedValues : item ) ); } } }} /> {/* SEDP Confirmation Dialog */} setSedpConfirmOpen(false)} onConfirm={handleSEDPSendConfirmed} formName={formName} tagCount={tableData.length} isLoading={isSendingSEDP} /> {/* SEDP Status Dialog */} setSedpStatusOpen(false)} status={sedpStatusData.status} message={sedpStatusData.message} successCount={sedpStatusData.successCount} errorCount={sedpStatusData.errorCount} totalCount={sedpStatusData.totalCount} /> {/* SEDP Compare Dialog */} setSedpCompareOpen(false)} tableData={tableData} columnsJSON={columnsJSON} projectCode={projectCode} formCode={formCode} projectType={projectType} packageCode={packageCode} /> {/* Other dialogs */} {tempUpDialog && ( )} {reportData.length > 0 && ( )} {batchDownDialog && ( 0 ? getSelectedRowsData() : tableData} packageId={contractItemId} formCode={formCode} formId={formId} /> )} ); }