From 4eb7532f822c821fb6b69bf103bd075fefba769b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 15 Jul 2025 10:07:09 +0000 Subject: (대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/spreadJS-dialog.tsx | 654 +++++++++++++++++++------------ 1 file changed, 396 insertions(+), 258 deletions(-) (limited to 'components/form-data/spreadJS-dialog.tsx') diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 8be9d175..c106f926 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -2,45 +2,31 @@ import * as React from "react"; import dynamic from "next/dynamic"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogDescription, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; import { toast } from "sonner"; import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save } from "lucide-react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css"; - -// Dynamically load the SpreadSheets component (disable SSR) +import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; + +// SpreadSheets를 동적으로 import (SSR 비활성화) const SpreadSheets = dynamic( - () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets), - { + () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), + { ssr: false, loading: () => (
Loading SpreadSheets...
- ), + ) } ); -// Apply license key on the client only -if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { +// 라이센스 키 설정을 클라이언트에서만 실행 +if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } @@ -85,11 +71,12 @@ interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; - selectedRow: GenericData; + selectedRow?: GenericData; // SPR_ITM_LST_SETUP용 + tableData?: GenericData[]; // SPR_LST_SETUP용 formCode: string; contractItemId: number; - editableFieldsMap?: Map; // editable field info per tag - onUpdateSuccess?: (updatedValues: Record) => void; + editableFieldsMap?: Map; + onUpdateSuccess?: (updatedValues: Record | GenericData[]) => void; } export function TemplateViewDialog({ @@ -97,345 +84,496 @@ export function TemplateViewDialog({ onClose, templateData, selectedRow, + tableData = [], formCode, contractItemId, editableFieldsMap = new Map(), - onUpdateSuccess, + onUpdateSuccess }: TemplateViewDialogProps) { - /* ------------------------- local state ------------------------- */ - const [hostStyle] = React.useState({ width: "100%", height: "100%" }); + const [hostStyle, setHostStyle] = React.useState({ + width: '100%', + height: '100%' + }); + const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); - const [currentSpread, setCurrentSpread] = React.useState( - null - ); - const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); - const [cellMappings, setCellMappings] = React.useState< - Array<{ attId: string; cellAddress: string; isEditable: boolean }> - >([]); + const [currentSpread, setCurrentSpread] = React.useState(null); + const [cellMappings, setCellMappings] = React.useState>([]); const [isClient, setIsClient] = React.useState(false); + const [templateType, setTemplateType] = React.useState<'SPR_LST_SETUP' | 'SPR_ITM_LST_SETUP' | null>(null); - // Render only on client side + // 클라이언트 사이드에서만 렌더링되도록 보장 React.useEffect(() => { setIsClient(true); }, []); - /* ------------------------- helpers ------------------------- */ - // Normalize template list and keep only those with CONTENT - const normalizedTemplates = React.useMemo((): TemplateItem[] => { - if (!templateData) return []; - - const list = Array.isArray(templateData) - ? (templateData as TemplateItem[]) - : ([templateData] as TemplateItem[]); - - return list.filter( - (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT - ); + // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것 찾기 + const { normalizedTemplate, detectedTemplateType } = React.useMemo(() => { + if (!templateData) return { normalizedTemplate: null, detectedTemplateType: null }; + + let templates: TemplateItem[]; + if (Array.isArray(templateData)) { + templates = templateData as TemplateItem[]; + } else { + templates = [templateData as TemplateItem]; + } + + // CONTENT가 있는 템플릿 찾기 + for (const template of templates) { + if (template.SPR_LST_SETUP?.CONTENT) { + return { normalizedTemplate: template, detectedTemplateType: 'SPR_LST_SETUP' as const }; + } + if (template.SPR_ITM_LST_SETUP?.CONTENT) { + return { normalizedTemplate: template, detectedTemplateType: 'SPR_ITM_LST_SETUP' as const }; + } + } + + return { normalizedTemplate: null, detectedTemplateType: null }; }, [templateData]); - // Choose currently selected template - const selectedTemplate = React.useMemo(() => { - if (!selectedTemplateId) return normalizedTemplates[0]; - return ( - normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) || - normalizedTemplates[0] - ); - }, [normalizedTemplates, selectedTemplateId]); - - // Editable fields for the current TAG_NO - const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO) return []; - return editableFieldsMap.get(selectedRow.TAG_NO) || []; - }, [selectedRow?.TAG_NO, editableFieldsMap]); - - const isFieldEditable = React.useCallback( - (attId: string) => { - // TAG_NO and TAG_DESC are always editable - if (attId === "TAG_NO" || attId === "TAG_DESC") return true; - if (!selectedRow?.TAG_NO) return false; - return editableFields.includes(attId); - }, - [selectedRow?.TAG_NO, editableFields] - ); - - /** Convert a cell address like "M1" into {row:0,col:12}. */ - const parseCellAddress = (addr: string): { row: number; col: number } | null => { - if (!addr) return null; - const match = addr.match(/^([A-Z]+)(\d+)$/); + // 템플릿 타입 설정 + React.useEffect(() => { + setTemplateType(detectedTemplateType); + }, [detectedTemplateType]); + + // 필드가 편집 가능한지 판별하는 함수 + const isFieldEditable = React.useCallback((attId: string) => { + // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 + if (attId === "TAG_NO" || attId === "TAG_DESC") { + return true; + } + + // SPR_ITM_LST_SETUP인 경우 selectedRow.shi 확인 + if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) { + return selectedRow.shi !== true; + } + + // SPR_LST_SETUP인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인) + return true; + }, [templateType, selectedRow]); + + // 편집 가능한 필드 개수 계산 + const editableFieldsCount = React.useMemo(() => { + return cellMappings.filter(m => m.isEditable).length; + }, [cellMappings]); + + // 셀 주소를 행과 열로 변환하는 함수 + const parseCellAddress = (address: string): {row: number, col: number} | null => { + if (!address || address.trim() === "") return null; + + const match = address.match(/^([A-Z]+)(\d+)$/); if (!match) return null; + const [, colStr, rowStr] = match; + let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; - const row = parseInt(rowStr, 10) - 1; + + const row = parseInt(rowStr) - 1; + return { row, col }; }; - // Auto‑select first template - React.useEffect(() => { - if (normalizedTemplates.length && !selectedTemplateId) { - setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); - } - }, [normalizedTemplates, selectedTemplateId]); - - /* ------------------------- init spread ------------------------- */ - const initSpread = React.useCallback( - (spread: GC.Spread.Sheets.Workbook | undefined) => { - if (!spread || !selectedTemplate || !selectedRow) return; + const initSpread = React.useCallback((spread: any) => { + if (!spread || !normalizedTemplate) return; + try { setCurrentSpread(spread); setHasChanges(false); - // Pick content JSON and data‑sheet mapping - const contentJson = - selectedTemplate.SPR_LST_SETUP?.CONTENT ?? - selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT; - const dataSheets = - selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ?? - selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; - if (!contentJson) return; - - // Prepare shared styles once - const editableStyle = new GC.Spread.Sheets.Style(); - editableStyle.backColor = "#f0fdf4"; - editableStyle.locked = false; + // 템플릿 타입에 따라 CONTENT와 DATA_SHEETS 가져오기 + let contentJson = null; + let dataSheets = null; + + if (templateType === 'SPR_LST_SETUP') { + contentJson = normalizedTemplate.SPR_LST_SETUP.CONTENT; + dataSheets = normalizedTemplate.SPR_LST_SETUP.DATA_SHEETS; + console.log('Using SPR_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME); + } else if (templateType === 'SPR_ITM_LST_SETUP') { + contentJson = normalizedTemplate.SPR_ITM_LST_SETUP.CONTENT; + dataSheets = normalizedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; + console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME); + } - const readOnlyStyle = new GC.Spread.Sheets.Style(); - readOnlyStyle.backColor = "#f9fafb"; - readOnlyStyle.foreColor = "#6b7280"; - readOnlyStyle.locked = true; + if (!contentJson) { + console.warn('No CONTENT found in template:', normalizedTemplate.NAME); + return; + } - const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson; + console.log(`Loading template content for: ${normalizedTemplate.NAME} (Type: ${templateType})`); + + const jsonData = typeof contentJson === 'string' + ? JSON.parse(contentJson) + : contentJson; - const sheet = spread.getActiveSheet(); + // 렌더링 일시 중단 + spread.suspendPaint(); - /* -------- batch load + style -------- */ - sheet.suspendPaint(); - sheet.suspendCalcService(true); try { - spread.fromJSON(jsonObj); - sheet.options.isProtected = false; - - const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = []; - - if (dataSheets?.length) { - dataSheets.forEach((ds) => { - ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => { - if (!IN) return; - const pos = parseCellAddress(IN); - if (!pos) return; - const editable = isFieldEditable(ATT_ID); - mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable }); - }); + // fromJSON으로 템플릿 구조 로드 + spread.fromJSON(jsonData); + + // 활성 시트 가져오기 + const activeSheet = spread.getActiveSheet(); + + // 시트 보호 먼저 해제 + activeSheet.options.isProtected = false; + + // MAP_CELL_ATT 정보를 사용해서 데이터 매핑 + if (dataSheets && dataSheets.length > 0) { + const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = []; + + dataSheets.forEach(dataSheet => { + if (dataSheet.MAP_CELL_ATT) { + dataSheet.MAP_CELL_ATT.forEach(mapping => { + const { ATT_ID, IN } = mapping; + + if (IN && IN.trim() !== "") { + const cellPos = parseCellAddress(IN); + if (cellPos) { + const isEditable = isFieldEditable(ATT_ID); + mappings.push({ + attId: ATT_ID, + cellAddress: IN, + isEditable: isEditable + }); + + // 템플릿 타입에 따라 다른 데이터 처리 + if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) { + // 단일 행 처리 (기존 로직) + const cell = activeSheet.getCell(cellPos.row, cellPos.col); + const value = selectedRow[ATT_ID]; + if (value !== undefined && value !== null) { + cell.value(value); + } + + // 스타일 적용 + cell.locked(!isEditable); + const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); + const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); + + if (isEditable) { + newStyle.backColor = "#f0fdf4"; + } else { + newStyle.backColor = "#f9fafb"; + newStyle.foreColor = "#6b7280"; + } + + activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); + + } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) { + // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움 + tableData.forEach((rowData, index) => { + const targetRow = cellPos.row + index; + const cell = activeSheet.getCell(targetRow, cellPos.col); + const value = rowData[ATT_ID]; + + if (value !== undefined && value !== null) { + cell.value(value); + } + + // 개별 행의 편집 가능 여부 확인 (shi 필드 기준) + const rowEditable = isEditable && (rowData.shi !== true); + cell.locked(!rowEditable); + + // 스타일 적용 + const existingStyle = activeSheet.getStyle(targetRow, cellPos.col); + const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); + + if (rowEditable) { + newStyle.backColor = "#f0fdf4"; + } else { + newStyle.backColor = "#f9fafb"; + newStyle.foreColor = "#6b7280"; + } + + activeSheet.setStyle(targetRow, cellPos.col, newStyle); + }); + } + + console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`); + } + } + }); + } + }); + + setCellMappings(mappings); + + // 시트 보호 설정 + activeSheet.options.isProtected = true; + activeSheet.options.protectionOptions = { + allowSelectLockedCells: true, + allowSelectUnlockedCells: true, + allowSort: false, + allowFilter: false, + allowEditObjects: false, + allowResizeRows: false, + allowResizeColumns: false + }; + + // 이벤트 리스너 추가 + activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { + console.log('Cell changed:', info); + setHasChanges(true); }); - } - // Apply values + style in chunks for large templates - const CHUNK = 500; - let idx = 0; - const applyChunk = () => { - const end = Math.min(idx + CHUNK, mappings.length); - for (; idx < end; idx++) { - const { attId, cellAddress, isEditable } = mappings[idx]; - const pos = parseCellAddress(cellAddress)!; - if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) { - sheet.setValue(pos.row, pos.col, selectedRow[attId]); - } - sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle); - } - if (idx < mappings.length) { - requestAnimationFrame(applyChunk); - } else { - // enable protection & events after styling done - sheet.options.isProtected = true; - sheet.options.protectionOptions = { - allowSelectLockedCells: true, - allowSelectUnlockedCells: true, - } as any; - - // Cell/value change events - sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true)); - sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true)); - - // Prevent editing read‑only fields - sheet.bind( - GC.Spread.Sheets.Events.EditStarting, - (event: any, info: any) => { - const map = mappings.find((m) => { - const pos = parseCellAddress(m.cellAddress); - return pos && pos.row === info.row && pos.col === info.col; - }); - if (map && !map.isEditable) { - toast.warning(`${map.attId} field is read‑only`); + activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { + console.log('Value changed:', info); + setHasChanges(true); + }); + + // 편집 시작 시 읽기 전용 셀 확인 + activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { + const mapping = mappings.find(m => { + const cellPos = parseCellAddress(m.cellAddress); + return cellPos && cellPos.row <= info.row && cellPos.col === info.col; + }); + + if (mapping) { + // SPR_LST_SETUP인 경우 해당 행의 데이터에서 shi 확인 + if (templateType === 'SPR_LST_SETUP') { + const dataRowIndex = info.row - parseCellAddress(mapping.cellAddress)!.row; + const rowData = tableData[dataRowIndex]; + if (rowData && rowData.shi === true) { + toast.warning(`Row ${dataRowIndex + 1}: ${mapping.attId} field is read-only (SHI mode)`); info.cancel = true; + return; } } - ); - - setCellMappings(mappings); - sheet.resumeCalcService(false); - sheet.resumePaint(); - } - }; - applyChunk(); - } catch (err) { - console.error(err); - toast.error("Failed to load template"); - sheet.resumeCalcService(false); - sheet.resumePaint(); + + if (!mapping.isEditable) { + toast.warning(`${mapping.attId} field is read-only`); + info.cancel = true; + } + } + }); + } + } finally { + spread.resumePaint(); } - }, - [selectedTemplate, selectedRow, isFieldEditable] - ); - /* ------------------------- handlers ------------------------- */ - const handleTemplateChange = (id: string) => { - setSelectedTemplateId(id); - setHasChanges(false); - if (currentSpread) { - // re‑init after a short tick so component remounts SpreadSheets - setTimeout(() => initSpread(currentSpread), 50); + } catch (error) { + console.error('Error initializing spread:', error); + toast.error('Failed to load template'); + if (spread && spread.resumePaint) { + spread.resumePaint(); + } } - }; + }, [normalizedTemplate, templateType, selectedRow, tableData, isFieldEditable]); + // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { - if (!currentSpread || !hasChanges || !selectedRow) { + if (!currentSpread || !hasChanges) { toast.info("No changes to save"); return; } - setIsPending(true); - try { - const sheet = currentSpread.getActiveSheet(); - const payload: Record = { ...selectedRow }; + setIsPending(true); + + const activeSheet = currentSpread.getActiveSheet(); - cellMappings.forEach((m) => { - if (m.isEditable) { - const pos = parseCellAddress(m.cellAddress); - if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col); + if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) { + // 단일 행 저장 (기존 로직) + const dataToSave = { ...selectedRow }; + + cellMappings.forEach(mapping => { + if (mapping.isEditable) { + const cellPos = parseCellAddress(mapping.cellAddress); + if (cellPos) { + const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); + dataToSave[mapping.attId] = cellValue; + } + } + }); + + dataToSave.TAG_NO = selectedRow.TAG_NO; + + const { success, message } = await updateFormDataInDB( + formCode, + contractItemId, + dataToSave + ); + + if (!success) { + toast.error(message); + return; } - }); - payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO + toast.success("Changes saved successfully!"); + onUpdateSuccess?.(dataToSave); + + } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) { + // 복수 행 저장 + const updatedRows: GenericData[] = []; + let saveCount = 0; + + for (let i = 0; i < tableData.length; i++) { + const originalRow = tableData[i]; + const dataToSave = { ...originalRow }; + let hasRowChanges = false; + + // 각 매핑에 대해 해당 행의 값 확인 + cellMappings.forEach(mapping => { + if (mapping.isEditable && originalRow.shi !== true) { // shi가 true인 행은 편집 불가 + const cellPos = parseCellAddress(mapping.cellAddress); + if (cellPos) { + const targetRow = cellPos.row + i; + const cellValue = activeSheet.getValue(targetRow, cellPos.col); + + // 값이 변경되었는지 확인 + if (cellValue !== originalRow[mapping.attId]) { + dataToSave[mapping.attId] = cellValue; + hasRowChanges = true; + } + } + } + }); - const { success, message } = await updateFormDataInDB( - formCode, - contractItemId, - payload - ); + // 변경사항이 있는 행만 저장 + if (hasRowChanges) { + dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록 - if (!success) { - toast.error(message); - return; + const { success } = await updateFormDataInDB( + formCode, + contractItemId, + dataToSave + ); + + if (success) { + updatedRows.push(dataToSave); + saveCount++; + } + } else { + updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지 + } + } + + if (saveCount > 0) { + toast.success(`${saveCount} rows saved successfully!`); + onUpdateSuccess?.(updatedRows); + } else { + toast.info("No changes to save"); + } } - toast.success("Changes saved successfully!"); - onUpdateSuccess?.({ ...selectedRow, ...payload }); setHasChanges(false); - } catch (err) { - console.error(err); + + } catch (error) { + console.error("Error saving changes:", error); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } - }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]); + }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings]); - /* ------------------------- render ------------------------- */ if (!isOpen) return null; + // 데이터 유효성 검사 + const isDataValid = templateType === 'SPR_ITM_LST_SETUP' ? !!selectedRow : tableData.length > 0; + const dataCount = templateType === 'SPR_ITM_LST_SETUP' ? 1 : tableData.length; + return ( - + - SEDP Template – {formCode} + SEDP Template - {formCode} - {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || "N/A"}`} - {hasChanges && • Unsaved changes} + {templateType && ( + + Template Type: {templateType === 'SPR_LST_SETUP' ? 'List View' : 'Item View'} + + )} + {templateType === 'SPR_ITM_LST_SETUP' && selectedRow && ( + • Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'} + )} + {templateType === 'SPR_LST_SETUP' && ( + • {dataCount} rows + )} + {hasChanges && ( + + • Unsaved changes + + )}
- + Editable fields - - Read‑only fields + + Read-only fields - {!!cellMappings.length && ( + {cellMappings.length > 0 && ( - {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable + {editableFieldsCount} of {cellMappings.length} fields editable )}
- - {/* Template selector */} - {normalizedTemplates.length > 1 && ( -
-
- - - ({normalizedTemplates.length} templates available) -
-
- )} - - {/* Spreadsheet */} + + {/* SpreadSheets 컴포넌트 영역 */}
- {selectedTemplate && isClient ? ( - + {normalizedTemplate && isClient && isDataValid ? ( + ) : (
{!isClient ? ( <> - Loading... + + Loading... - ) : ( + ) : !normalizedTemplate ? ( "No template available" + ) : !isDataValid ? ( + `No ${templateType === 'SPR_ITM_LST_SETUP' ? 'selected row' : 'data'} available` + ) : ( + "Template not ready" )}
)}
- {/* footer */} + {hasChanges && ( - )} +
); -} +} \ No newline at end of file -- cgit v1.2.3