"use client"; import * as React from "react"; import { useParams, useRouter } 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, Save, Upload, Plus, Tag, TagsIcon, FileText, FileSpreadsheet, FileOutput, Clipboard, Send, GitCompareIcon, RefreshCcw } from "lucide-react"; import { toast } from "sonner"; import { getProjectCodeById, getReportTempList, sendFormDataToSEDP, syncMissingTags, updateFormDataInDB, } 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, DropdownMenuSeparator, } 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 { getSEDPToken } from "@/lib/sedp/sedp-token"; async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise { try { // Get the token const apiKey = await getSEDPToken(); // Define the API base URL const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; // Make the API call const response = await fetch( `${SEDP_API_BASE_URL}/Data/GetPubData`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, REG_TYPE_ID: formCode, ContainDeleted: false }) } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); return data; } catch (error: any) { console.error('Error calling SEDP API:', error); throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`); } } interface GenericData { [key: string]: any; } 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, "translation"); const [rowAction, setRowAction] = React.useState | null>(null); const [tableData, setTableData] = React.useState(dataJSON); // 배치 선택 관련 상태 const [selectedRows, setSelectedRows] = React.useState>({}); // Update tableData when dataJSON changes React.useEffect(() => { setTableData(dataJSON); // 데이터가 변경되면 선택 상태 초기화 setSelectedRows({}); }, [dataJSON]); // 폴링 상태 관리를 위한 ref const pollingRef = React.useRef(null); const [syncId, setSyncId] = React.useState(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, setIsSaving] = React.useState(false); const [isSendingSEDP, setIsSendingSEDP] = React.useState(false); const [isLoadingTags, setIsLoadingTags] = React.useState(false); // Any operation in progress const isAnyOperationPending = isSyncingTags || isImporting || isExporting || isSaving || isSendingSEDP || isLoadingTags; // 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 [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); // 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]); // Get project code when component mounts React.useEffect(() => { const getProjectCode = async () => { try { const code = await getProjectCodeById(projectId); setProjectCode(code); } catch (error) { console.error("Error fetching project code:", error); toast.error("Failed to fetch project code"); } }; if (projectId) { getProjectCode(); } }, [projectId]); // 선택된 행들의 실제 데이터 가져오기 const getSelectedRowsData = React.useCallback(() => { const selectedIndices = Object.keys(selectedRows).filter(key => selectedRows[key]); return selectedIndices.map(index => tableData[parseInt(index)]).filter(Boolean); }, [selectedRows, tableData]); // 선택된 행 개수 계산 const selectedRowCount = React.useMemo(() => { return Object.values(selectedRows).filter(Boolean).length; }, [selectedRows]); const columns = React.useMemo( () => getColumns({ columnsJSON, setRowAction, setReportData, tempCount, selectedRows, onRowSelectionChange: setSelectedRows, editableFieldsMap }), [columnsJSON, setRowAction, setReportData, tempCount, selectedRows] ); 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]); // 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) { setSyncId(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); setSyncId(null); // 성공 메시지 표시 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); setSyncId(null); 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 - Modified to directly save to DB async function handleImportExcel(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; try { setIsImporting(true); // Call the updated importExcelData function with editableFieldsMap const result = await importExcelData({ file, tableData, columnsJSON, formCode, contractItemId, editableFieldsMap, // 추가: 편집 가능 필드 정보 전달 onPendingChange: setIsImporting, 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); } router.refresh(); } } catch (error) { console.error("Import failed:", error); toast.error("Failed to import Excel data"); } finally { // Always clear the file input value e.target.value = ""; setIsImporting(false); } } // 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) => !item.TAG_NO?.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 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: any) { console.error("SEDP error:", err); // Set error status setSedpStatusData({ status: 'error', message: 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, 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); }; return ( <> {/* 선택된 항목 수 표시 (선택된 항목이 있을 때만) */} {selectedRowCount > 0 && (

{selectedRowCount}개 항목이 선택되었습니다. 배치 문서는 선택된 항목만으로 생성됩니다.

)} {/* 버튼 그룹 */}
{/* 태그 관리 드롭다운 */} {/* 모드에 따라 다른 태그 작업 표시 */} {mode === "IM" ? ( Sync Tags ) : ( Get Tags )} setAddTagDialogOpen(true)} disabled={isAnyOperationPending}> Add Tags {/* 리포트 관리 드롭다운 */} setTempUpDialog(true)} disabled={isAnyOperationPending}> Upload Template Batch Document {selectedRowCount > 0 && ( {selectedRowCount} )} {/* IMPORT 버튼 (파일 선택) */} {/* EXPORT 버튼 */} {/* COMPARE WITH SEDP 버튼 */} {/* SEDP 전송 버튼 */}
{/* Modal dialog for tag update */} { if (!open) setRowAction(null); }} columns={columnsJSON} rowData={rowAction?.row.original ?? null} formCode={formCode} contractItemId={contractItemId} 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 ) ); } }} /> {/* Dialog for adding tags */} {/* 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} fetchTagDataFromSEDP={fetchTagDataFromSEDP} /> {/* Other dialogs */} {tempUpDialog && ( )} {reportData.length > 0 && ( )} {batchDownDialog && ( 0 ? getSelectedRowsData() : tableData} packageId={contractItemId} formCode={formCode} formId={formId} /> )} ); }