diff options
Diffstat (limited to 'components/form-data/form-data-table.tsx')
| -rw-r--r-- | components/form-data/form-data-table.tsx | 948 |
1 files changed, 524 insertions, 424 deletions
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 4caee44f..05278375 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { useTranslation } from "@/i18n/client"; import { ClientDataTable } from "../client-data-table/data-table"; @@ -13,20 +13,88 @@ import { } from "./form-data-table-columns"; import type { DataTableAdvancedFilterField } from "@/types/table"; import { Button } from "../ui/button"; -import { Download, Loader, Save, Upload } from "lucide-react"; +import { + Download, + Loader, + Save, + Upload, + Plus, + Tag, + TagsIcon, + FileText, + FileSpreadsheet, + FileOutput, + Clipboard, + Send, + GitCompareIcon, + RefreshCcw +} from "lucide-react"; import { toast } from "sonner"; -import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services"; +import { + getProjectCodeById, + getReportTempList, + sendFormDataToSEDP, + syncMissingTags, + updateFormDataInDB, +} from "@/lib/forms/services"; import { UpdateTagSheet } from "./update-form-sheet"; -import ExcelJS from "exceljs"; -import { saveAs } from "file-saver"; 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 { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; + 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<any> { + 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; @@ -38,6 +106,10 @@ export interface DynamicTableProps { contractItemId: number; formCode: string; formId: number; + projectId: number; + formName?: string; + objectCode?: string; + mode: "IM" | "ENG"; // 모드 속성 } export default function DynamicTable({ @@ -46,28 +118,98 @@ export default function DynamicTable({ contractItemId, formCode, formId, + projectId, + mode = "IM", // 기본값 설정 + formName = `${formCode}`, // Default form name based on formCode }: DynamicTableProps) { const params = useParams(); + const router = useRouter(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "translation"); const [rowAction, setRowAction] = React.useState<DataTableRowAction<GenericData> | null>(null); - const [tableData, setTableData] = React.useState<GenericData[]>( - () => dataJSON - ); - const [isPending, setIsPending] = React.useState(false); + const [tableData, setTableData] = React.useState<GenericData[]>(dataJSON); + + // Update tableData when dataJSON changes + React.useEffect(() => { + setTableData(dataJSON); + }, [dataJSON]); + + // 폴링 상태 관리를 위한 ref + const pollingRef = React.useRef<NodeJS.Timeout | null>(null); + const [syncId, setSyncId] = React.useState<string | null>(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<string>(''); + const [tempUpDialog, setTempUpDialog] = React.useState(false); const [reportData, setReportData] = React.useState<GenericData[]>([]); 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"); + } + }; - // Reference to the table instance - const tableRef = React.useRef(null); + if (projectId) { + getProjectCode(); + } + }, [projectId]); const columns = React.useMemo( - () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData }), - [columnsJSON, setRowAction, setReportData] + () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData, tempCount }), + [columnsJSON, setRowAction, setReportData, tempCount] ); function mapColumnTypeToAdvancedFilterType( @@ -79,11 +221,8 @@ export default function DynamicTable({ case "NUMBER": return "number"; case "LIST": - // "select"로 하셔도 되고 "multi-select"로 하셔도 됩니다. return "select"; - // 그 외 다른 타입들도 적절히 추가 매핑 default: - // 예: 못 매핑한 경우 기본적으로 "text" 적용 return "text"; } } @@ -102,10 +241,10 @@ export default function DynamicTable({ })); }, [columnsJSON]); - // 1) 태그 불러오기 (기존) + // IM 모드: 태그 동기화 함수 async function handleSyncTags() { try { - setIsPending(true); + setIsSyncingTags(true); const result = await syncMissingTags(contractItemId, formCode); // Prepare the toast messages based on what changed @@ -120,7 +259,7 @@ export default function DynamicTable({ if (changes.length > 0) { // If any changes were made, show success message and reload toast.success(`동기화 완료: ${changes.join(", ")}`); - location.reload(); + router.refresh(); // Use router.refresh instead of location.reload } else { // If no changes were made, show an info message toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다."); @@ -129,487 +268,393 @@ export default function DynamicTable({ console.error(err); toast.error("태그 동기화 중 에러가 발생했습니다."); } finally { - setIsPending(false); + setIsSyncingTags(false); } } - // 2) Excel Import (새로운 기능) - async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) { - const file = e.target.files?.[0]; - if (!file) return; - + + // ENG 모드: 태그 가져오기 함수 + const handleGetTags = async () => { try { - setIsPending(true); - - // 기존 테이블 데이터의 tagNumber 목록 (이미 있는 태그) - const existingTagNumbers = new Set(tableData.map((d) => d.tagNumber)); - - const workbook = new ExcelJS.Workbook(); - const arrayBuffer = await file.arrayBuffer(); - await workbook.xlsx.load(arrayBuffer); - - const worksheet = workbook.worksheets[0]; - - // (A) 헤더 파싱 (Error 열 추가 전에 먼저 체크) - const headerRow = worksheet.getRow(1); - const headerRowValues = headerRow.values as ExcelJS.CellValue[]; - - // 디버깅용 로그 - console.log("원본 헤더 값:", headerRowValues); - - // Excel의 헤더와 columnsJSON의 label 매핑 생성 - // Excel의 행은 1부터 시작하므로 headerRowValues[0]은 undefined - const headerToIndexMap = new Map<string, number>(); - for (let i = 1; i < headerRowValues.length; i++) { - const headerValue = String(headerRowValues[i] || "").trim(); - if (headerValue) { - headerToIndexMap.set(headerValue, i); - } - } - - // (B) 헤더 검사 - let headerErrorMessage = ""; - - // (1) "columnsJSON에 있는데 엑셀에 없는" 라벨 - columnsJSON.forEach((col) => { - const label = col.label; - if (!headerToIndexMap.has(label)) { - headerErrorMessage += `Column "${label}" is missing. `; - } - }); - - // (2) "엑셀에는 있는데 columnsJSON에 없는" 라벨 검사 - headerToIndexMap.forEach((index, headerLabel) => { - const found = columnsJSON.some((col) => col.label === headerLabel); - if (!found) { - headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `; - } + setIsLoadingTags(true); + + // API 엔드포인트 호출 - 작업 시작만 요청 + const response = await fetch('/api/cron/form-tags/start', { + method: 'POST', + body: JSON.stringify({ projectCode ,formCode ,contractItemId }) }); - - // (C) 이제 Error 열 추가 - const lastColIndex = worksheet.columnCount + 1; - worksheet.getRow(1).getCell(lastColIndex).value = "Error"; - - // 헤더 에러가 있으면 기록 후 다운로드하고 중단 - if (headerErrorMessage) { - headerRow.getCell(lastColIndex).value = headerErrorMessage.trim(); - - const outBuffer = await workbook.xlsx.writeBuffer(); - saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); - - toast.error(`Header mismatch found. Please check downloaded file.`); - return; + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to start tag import'); } - - // -- 여기까지 왔다면, 헤더는 문제 없음 -- - - // 컬럼 키-인덱스 매핑 생성 (실제 데이터 행 파싱용) - // columnsJSON의 key와 Excel 열 인덱스 간의 매핑 - const keyToIndexMap = new Map<string, number>(); - columnsJSON.forEach((col) => { - const index = headerToIndexMap.get(col.label); - if (index !== undefined) { - keyToIndexMap.set(col.key, index); + + 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 importedData: GenericData[] = []; - const lastRowNumber = worksheet.lastRow?.number || 1; - let errorCount = 0; - - // 실제 데이터 행 파싱 - for (let rowNum = 2; rowNum <= lastRowNumber; rowNum++) { - const row = worksheet.getRow(rowNum); - const rowValues = row.values as ExcelJS.CellValue[]; - if (!rowValues || rowValues.length <= 1) continue; // 빈 행 스킵 - - let errorMessage = ""; - const rowObj: Record<string, any> = {}; - - // 각 열에 대해 처리 - columnsJSON.forEach((col) => { - const colIndex = keyToIndexMap.get(col.key); - if (colIndex === undefined) return; - - const cellValue = rowValues[colIndex] ?? ""; - let stringVal = String(cellValue).trim(); - - // 타입별 검사 - switch (col.type) { - case "STRING": - if (!stringVal && col.key === "tagNumber") { - errorMessage += `[${col.label}] is empty. `; - } - rowObj[col.key] = stringVal; - break; - - case "NUMBER": - if (stringVal) { - const num = parseFloat(stringVal); - if (isNaN(num)) { - errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `; - } else { - rowObj[col.key] = num; - } - } else { - rowObj[col.key] = null; - } - break; - - case "LIST": - if ( - stringVal && - col.options && - !col.options.includes(stringVal) - ) { - errorMessage += `[${ - col.label - }] '${stringVal}' not in ${col.options.join(", ")}. `; - } - rowObj[col.key] = stringVal; - break; - - default: - rowObj[col.key] = stringVal; - break; + + 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}`, + }); } - }); - - // tagNumber 검사 - const tagNum = rowObj["tagNumber"]; - if (!tagNum) { - errorMessage += `No tagNumber found. `; - } else if (!existingTagNumbers.has(tagNum)) { - errorMessage += `TagNumber '${tagNum}' is not in current data. `; - } - - if (errorMessage) { - row.getCell(lastColIndex).value = errorMessage.trim(); - errorCount++; - } else { - importedData.push(rowObj); } + } 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<HTMLInputElement>) { + const file = e.target.files?.[0]; + if (!file) return; - // 에러가 있으면 재다운로드 후 import 중단 - if (errorCount > 0) { - const outBuffer = await workbook.xlsx.writeBuffer(); - saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); - toast.error( - `There are ${errorCount} error row(s). Please check downloaded file.` - ); - return; - } - - // 에러 없으니 tableData 병합 - setTableData((prev) => { - const newDataMap = new Map<string, GenericData>(); - - // 기존 데이터를 맵에 추가 - prev.forEach((item) => { - if (item.tagNumber) { - newDataMap.set(item.tagNumber, { ...item }); - } - }); - - // 임포트 데이터로 기존 데이터 업데이트 - importedData.forEach((item) => { - const tag = item.tagNumber; - if (!tag) return; - const oldItem = newDataMap.get(tag) || {}; - newDataMap.set(tag, { ...oldItem, ...item }); - }); - - return Array.from(newDataMap.values()); + try { + setIsImporting(true); + + // Call the updated importExcelData function with direct save capability + const result = await importExcelData({ + file, + tableData, + columnsJSON, + formCode, // Pass formCode for direct save + contractItemId, // Pass contractItemId for direct save + onPendingChange: setIsImporting, + onDataUpdate: (newData) => { + // This is called only after successful DB save + setTableData(Array.isArray(newData) ? newData : newData(tableData)); + } }); - - toast.success(`Imported ${importedData.length} rows successfully.`); - } catch (err) { - console.error("Excel import error:", err); - toast.error("Excel import failed."); + + // If import and save was successful, refresh the page + if (result.success) { + router.refresh(); + } + } catch (error) { + console.error("Import failed:", error); + toast.error("Failed to import Excel data"); } finally { - setIsPending(false); + // 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); } - // 3) Save -> 서버에 전체 tableData를 저장 - async function handleSave() { + // Actual SEDP send after confirmation + async function handleSEDPSendConfirmed() { try { - setIsSaving(true); - - // 유효성 검사 - const invalidData = tableData.filter((item) => !item.tagNumber?.trim()); + setIsSendingSEDP(true); + + // Validate data + const invalidData = tableData.filter((item) => !item.TAG_NO?.trim()); if (invalidData.length > 0) { - toast.error( - `태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.` - ); + toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`); + setSedpConfirmOpen(false); return; } - // 서버 액션 호출 - const result = await updateFormDataInDB( - formCode, - contractItemId, - tableData + // 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 ); - if (result.success) { - toast.success(result.message); + // 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 { - toast.error(result.message); + setSedpStatusData({ + status: 'error', + message: sedpResult.message || "Failed to send data to SEDP", + successCount: 0, + errorCount: tableData.length, + totalCount: tableData.length + }); } - } catch (err) { - console.error("Save error:", err); - toast.error("데이터 저장 중 오류가 발생했습니다."); + + // 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 { - setIsSaving(false); + setIsSendingSEDP(false); } } - - // 4) NEW: Excel Export with data validation for select columns using a hidden validation sheet + + // Template Export async function handleExportExcel() { try { - setIsPending(true); - - // Create a new workbook - const workbook = new ExcelJS.Workbook(); - - // 데이터 시트 생성 - const worksheet = workbook.addWorksheet("Data"); - - // 유효성 검사용 숨김 시트 생성 - const validationSheet = workbook.addWorksheet("ValidationData"); - validationSheet.state = "hidden"; // 시트 숨김 처리 - - // 1. 유효성 검사 시트에 select 옵션 추가 - const selectColumns = columnsJSON.filter( - (col) => col.type === "LIST" && col.options && col.options.length > 0 - ); - - // 유효성 검사 범위 저장 맵 (컬럼 키 -> 유효성 검사 범위) - const validationRanges = new Map<string, string>(); - - selectColumns.forEach((col, idx) => { - const colIndex = idx + 1; - const colLetter = validationSheet.getColumn(colIndex).letter; - - // 헤더 추가 (컬럼 레이블) - validationSheet.getCell(`${colLetter}1`).value = col.label; - - // 옵션 추가 - if (col.options) { - col.options.forEach((option, optIdx) => { - validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option; - }); - - // 유효성 검사 범위 저장 (ValidationData!$A$2:$A$4 형식) - validationRanges.set( - col.key, - `ValidationData!${colLetter}$2:${colLetter}${ - col.options.length + 1 - }` - ); - } - }); - - // 2. 데이터 시트에 헤더 추가 - const headers = columnsJSON.map((col) => col.label); - worksheet.addRow(headers); - - // 헤더 스타일 적용 - const headerRow = worksheet.getRow(1); - headerRow.font = { bold: true }; - headerRow.alignment = { horizontal: "center" }; - headerRow.eachCell((cell) => { - cell.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "FFCCCCCC" }, - }; - }); - - // 3. 데이터 행 추가 - tableData.forEach((row) => { - const rowValues = columnsJSON.map((col) => { - const value = row[col.key]; - return value !== undefined && value !== null ? value : ""; - }); - worksheet.addRow(rowValues); - }); - - // 4. 데이터 유효성 검사 적용 - const maxRows = 5000; // 데이터 유효성 검사를 적용할 최대 행 수 - - columnsJSON.forEach((col, idx) => { - if (col.type === "LIST" && validationRanges.has(col.key)) { - const colLetter = worksheet.getColumn(idx + 1).letter; - const validationRange = validationRanges.get(col.key)!; - - // 유효성 검사 정의 - const validation = { - type: "list" as const, - allowBlank: true, - formulae: [validationRange], - showErrorMessage: true, - errorStyle: "warning" as const, - errorTitle: "유효하지 않은 값", - error: "목록에서 값을 선택해주세요.", - }; - - // 모든 데이터 행에 유효성 검사 적용 (최대 maxRows까지) - for ( - let rowIdx = 2; - rowIdx <= Math.min(tableData.length + 1, maxRows); - rowIdx++ - ) { - worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = - validation; - } - - // 빈 행에도 적용 (최대 maxRows까지) - if (tableData.length + 1 < maxRows) { - for ( - let rowIdx = tableData.length + 2; - rowIdx <= maxRows; - rowIdx++ - ) { - worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = - validation; - } - } - } - }); - - // 5. 컬럼 너비 자동 조정 - columnsJSON.forEach((col, idx) => { - const column = worksheet.getColumn(idx + 1); - - // 최적 너비 계산 - let maxLength = col.label.length; - tableData.forEach((row) => { - const value = row[col.key]; - if (value !== undefined && value !== null) { - const valueLength = String(value).length; - if (valueLength > maxLength) { - maxLength = valueLength; - } - } - }); - - // 너비 설정 (최소 10, 최대 50) - column.width = Math.min(Math.max(maxLength + 2, 10), 50); + setIsExporting(true); + await exportExcelData({ + tableData, + columnsJSON, + formCode, + onPendingChange: setIsExporting }); - - // 6. 파일 다운로드 - const buffer = await workbook.xlsx.writeBuffer(); - saveAs( - new Blob([buffer]), - `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx` - ); - - toast.success("Excel 내보내기 완료!"); - } catch (err) { - console.error("Excel export error:", err); - toast.error("Excel 내보내기 실패."); } finally { - setIsPending(false); + setIsExporting(false); } } + // Handle batch document check + const handleBatchDocument = () => { + if (tempCount > 0) { + setBatchDownDialog(true); + } else { + toast.error("업로드된 Template File이 없습니다."); + } + }; + return ( <> <ClientDataTable data={tableData} columns={columns} advancedFilterFields={advancedFilterFields} - // tableRef={tableRef} > {/* 버튼 그룹 */} <div className="flex items-center gap-2"> - {/* 태그 불러오기 버튼 */} - <Popover> - <PopoverTrigger asChild> - <Button variant="default" size="sm"> - Vendor Document + {/* 태그 관리 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm" disabled={isAnyOperationPending}> + {(isSyncingTags || isLoadingTags) && ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + )} + <TagsIcon className="size-4" /> + Tag Operations </Button> - </PopoverTrigger> - <PopoverContent className="flex flex-row gap-2 w-auto"> - <Button - variant="outline" - size="sm" - onClick={() => setTempUpDialog(true)} - > - Template Upload + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {/* 모드에 따라 다른 태그 작업 표시 */} + {mode === "IM" ? ( + <DropdownMenuItem onClick={handleSyncTags} disabled={isAnyOperationPending}> + <Tag className="mr-2 h-4 w-4" /> + Sync Tags + </DropdownMenuItem> + ) : ( + <DropdownMenuItem onClick={handleGetTags} disabled={isAnyOperationPending}> + <RefreshCcw className="mr-2 h-4 w-4" /> + Get Tags + </DropdownMenuItem> + )} + <DropdownMenuItem onClick={() => setAddTagDialogOpen(true)} disabled={isAnyOperationPending}> + <Plus className="mr-2 h-4 w-4" /> + Add Tags + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + + {/* 리포트 관리 드롭다운 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" size="sm" disabled={isAnyOperationPending}> + <Clipboard className="size-4" /> + Report Operations </Button> - <Button - variant="outline" - size="sm" - onClick={() => setBatchDownDialog(true)} - > - Vendor Document Create - </Button> - </PopoverContent> - </Popover> - <Button - variant="default" - size="sm" - onClick={handleSyncTags} - disabled={isPending} - > - {isPending && ( - <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> - )} - Sync Tags - </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem onClick={() => setTempUpDialog(true)} disabled={isAnyOperationPending}> + <Upload className="mr-2 h-4 w-4" /> + Upload Template + </DropdownMenuItem> + <DropdownMenuItem onClick={handleBatchDocument} disabled={isAnyOperationPending}> + <FileOutput className="mr-2 h-4 w-4" /> + Batch Document + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> {/* IMPORT 버튼 (파일 선택) */} - <Button asChild variant="outline" size="sm" disabled={isPending}> + <Button asChild variant="outline" size="sm" disabled={isAnyOperationPending}> <label> - <Upload className="size-4" /> + {isImporting ? ( + <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> + ) : ( + <Upload className="size-4" /> + )} Import <input type="file" accept=".xlsx,.xls" onChange={handleImportExcel} style={{ display: "none" }} + disabled={isAnyOperationPending} /> </label> </Button> - {/* EXPORT 버튼 (새로 추가) */} + {/* EXPORT 버튼 */} <Button variant="outline" size="sm" onClick={handleExportExcel} - disabled={isPending} + disabled={isAnyOperationPending} > - <Download className="mr-2 size-4" /> - Export Template + {isExporting ? ( + <Loader className="mr-2 size-4 animate-spin" /> + ) : ( + <Download className="mr-2 size-4" /> + )} + Export </Button> - {/* SAVE 버튼 */} + {/* COMPARE WITH SEDP 버튼 */} <Button variant="outline" size="sm" - onClick={handleSave} - disabled={isPending || isSaving} + onClick={handleSEDPCompareClick} + disabled={isAnyOperationPending} > - {isSaving ? ( + <GitCompareIcon className="mr-2 size-4" /> + Compare with SEDP + </Button> + + {/* SEDP 전송 버튼 */} + <Button + variant="samsung" + size="sm" + onClick={handleSEDPSendClick} + disabled={isAnyOperationPending} + > + {isSendingSEDP ? ( <> <Loader className="mr-2 size-4 animate-spin" /> - 저장 중... + SEDP 전송 중... </> ) : ( <> - <Save className="mr-2 size-4" /> - Save + <Send className="size-4" /> + Send to SHI </> )} </Button> </div> </ClientDataTable> + {/* Modal dialog for tag update */} <UpdateTagSheet open={rowAction?.type === "update"} onOpenChange={(open) => { @@ -619,7 +664,62 @@ export default function DynamicTable({ 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 */} + <AddFormTagDialog + projectId={projectId} + formCode={formCode} + formName={`Form ${formCode}`} + contractItemId={contractItemId} + open={addTagDialogOpen} + onOpenChange={setAddTagDialogOpen} + /> + + {/* SEDP Confirmation Dialog */} + <SEDPConfirmationDialog + isOpen={sedpConfirmOpen} + onClose={() => setSedpConfirmOpen(false)} + onConfirm={handleSEDPSendConfirmed} + formName={formName} + tagCount={tableData.length} + isLoading={isSendingSEDP} + /> + + {/* SEDP Status Dialog */} + <SEDPStatusDialog + isOpen={sedpStatusOpen} + onClose={() => setSedpStatusOpen(false)} + status={sedpStatusData.status} + message={sedpStatusData.message} + successCount={sedpStatusData.successCount} + errorCount={sedpStatusData.errorCount} + totalCount={sedpStatusData.totalCount} + /> + + {/* SEDP Compare Dialog */} + <SEDPCompareDialog + isOpen={sedpCompareOpen} + onClose={() => setSedpCompareOpen(false)} + tableData={tableData} + columnsJSON={columnsJSON} + projectCode={projectCode} + formCode={formCode} + fetchTagDataFromSEDP={fetchTagDataFromSEDP} + /> + + {/* Other dialogs */} {tempUpDialog && ( <FormDataReportTempUploadDialog columnsJSON={columnsJSON} @@ -656,4 +756,4 @@ export default function DynamicTable({ )} </> ); -} +}
\ No newline at end of file |
