// Excel Management System Client Logic document.addEventListener('DOMContentLoaded', () => { // State let currentFileId = null; let currentFileName = ''; let currentSheetNames = []; let currentActiveSheet = ''; let currentSheetRows = []; let currentTotalCols = 0; let renderedRowCount = 0; let isDirty = false; const CHUNK_SIZE = 200; // DOM Elements - Navigation & Header const brandHome = document.getElementById('brand-home'); const viewerNavInfo = document.getElementById('viewer-nav-info'); const currentFilenameText = document.getElementById('current-filename-text'); const btnBackDashboard = document.getElementById('btn-back-dashboard'); const btnDownloadFile = document.getElementById('btn-download-file'); const notificationBar = document.getElementById('notification-bar'); const notificationMessage = document.getElementById('notification-message'); const notificationClose = document.getElementById('notification-close'); // DOM Elements - Views const dashboardView = document.getElementById('dashboard-view'); const spreadsheetView = document.getElementById('spreadsheet-view'); // DOM Elements - Dashboard const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const uploadProgress = document.getElementById('upload-progress'); const filesGrid = document.getElementById('files-grid'); const filesEmpty = document.getElementById('files-empty'); const filesLoading = document.getElementById('files-loading'); const fileCountBadge = document.getElementById('file-count-badge'); // DOM Elements - Spreadsheet Viewer const btnViewerBack = document.getElementById('btn-viewer-back'); const btnViewerDownload = document.getElementById('btn-viewer-download'); const btnSaveSheet = document.getElementById('btn-save-sheet'); const btnAddRow = document.getElementById('btn-add-row'); const unsavedBadge = document.getElementById('unsaved-badge'); const sheetFileTitle = document.getElementById('sheet-file-title'); const sheetStats = document.getElementById('sheet-stats'); const sheetTabsList = document.getElementById('sheet-tabs-list'); const gridViewport = document.getElementById('grid-viewport'); const gridLoading = document.getElementById('grid-loading'); const sheetTable = document.getElementById('spreadsheet-table'); const sheetThead = document.getElementById('sheet-thead'); const sheetTbody = document.getElementById('sheet-tbody'); const sheetEmptyMsg = document.getElementById('sheet-empty-msg'); const renderedRowsInfo = document.getElementById('rendered-rows-info'); // ========================================== // Helper Functions // ========================================== function showNotification(message, isSuccess = false) { notificationBar.className = 'notification-bar' + (isSuccess ? ' success' : ''); notificationMessage.textContent = message; notificationBar.style.display = 'block'; if (isSuccess) { setTimeout(() => { hideNotification(); }, 5000); } } function hideNotification() { notificationBar.style.display = 'none'; } notificationClose.addEventListener('click', hideNotification); function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } function formatDate(isoString) { if (!isoString) return 'Just now'; const date = new Date(isoString); return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } // Convert zero-indexed column number to Excel column letter (0 -> 'A', 25 -> 'Z', 26 -> 'AA') function getExcelColLetter(index) { let letter = ''; let temp = index; while (temp >= 0) { letter = String.fromCharCode((temp % 26) + 65) + letter; temp = Math.floor(temp / 26) - 1; } return letter; } // ========================================== // Navigation & View Switching // ========================================== function navigateToDashboard() { if (isDirty) { if (!confirm('You have unsaved changes in the spreadsheet. Do you want to leave without saving?')) { return; } } isDirty = false; unsavedBadge.style.display = 'none'; currentFileId = null; currentFileName = ''; currentSheetRows = []; renderedRowCount = 0; // Reset views spreadsheetView.style.display = 'none'; spreadsheetView.classList.remove('active'); dashboardView.style.display = 'flex'; dashboardView.classList.add('active'); // Reset top nav viewerNavInfo.style.display = 'none'; btnBackDashboard.style.display = 'none'; btnDownloadFile.style.display = 'none'; window.location.hash = ''; loadFilesList(); } function navigateToSpreadsheet(fileId, fileName) { currentFileId = fileId; currentFileName = fileName; // Update views dashboardView.style.display = 'none'; dashboardView.classList.remove('active'); spreadsheetView.style.display = 'flex'; spreadsheetView.classList.add('active'); // Update top nav currentFilenameText.textContent = fileName; sheetFileTitle.textContent = fileName; viewerNavInfo.style.display = 'inline-flex'; btnBackDashboard.style.display = 'inline-flex'; btnDownloadFile.style.display = 'inline-flex'; window.location.hash = `#file=${encodeURIComponent(fileId)}`; loadFileDetailsAndSheet(fileId); } brandHome.addEventListener('click', navigateToDashboard); btnBackDashboard.addEventListener('click', navigateToDashboard); btnViewerBack.addEventListener('click', navigateToDashboard); // Download Action function triggerDownload() { if (!currentFileId) return; window.location.href = `/api/files/${encodeURIComponent(currentFileId)}/download`; } btnDownloadFile.addEventListener('click', triggerDownload); btnViewerDownload.addEventListener('click', triggerDownload); // ========================================== // Dashboard & File Listing // ========================================== async function loadFilesList() { filesLoading.style.display = 'flex'; filesEmpty.style.display = 'none'; filesGrid.innerHTML = ''; try { const res = await fetch('/api/files'); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to fetch files'); } const files = data.files || []; fileCountBadge.textContent = `${files.length} file${files.length === 1 ? '' : 's'}`; if (files.length === 0) { filesEmpty.style.display = 'block'; } else { renderFileCards(files); } } catch (err) { showNotification('Error loading files: ' + err.message); } finally { filesLoading.style.display = 'none'; } } function renderFileCards(files) { filesGrid.innerHTML = ''; files.forEach(file => { const card = document.createElement('div'); card.className = 'file-card'; const sheetCount = file.sheetNames ? file.sheetNames.length : 1; card.innerHTML = `
${file.originalName}
${formatBytes(file.size)} • ${formatDate(file.uploadedAt)} • ${sheetCount} sheet${sheetCount > 1 ? 's' : ''}
`; // Click on card or "Open" button opens the spreadsheet card.addEventListener('click', (e) => { // Prevent trigger if download button was clicked if (e.target.closest('.btn-card-download')) { e.stopPropagation(); window.location.href = `/api/files/${encodeURIComponent(file.id)}/download`; return; } navigateToSpreadsheet(file.id, file.originalName); }); filesGrid.appendChild(card); }); } // ========================================== // File Upload Handling // ========================================== function validateExcelFile(file) { if (!file) return false; const name = file.name.toLowerCase(); return name.endsWith('.xlsx') || name.endsWith('.xls'); } async function handleFileUpload(file) { if (!file) return; if (!validateExcelFile(file)) { showNotification('Invalid file type. Please upload a valid Excel file (.xlsx or .xls).'); return; } hideNotification(); uploadProgress.style.display = 'flex'; const formData = new FormData(); formData.append('file', file); try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to upload Excel file.'); } showNotification(`"${data.file.originalName}" uploaded successfully!`, true); // Refresh files list await loadFilesList(); } catch (err) { showNotification('Upload Error: ' + err.message); } finally { uploadProgress.style.display = 'none'; fileInput.value = ''; } } fileInput.addEventListener('change', (e) => { if (e.target.files && e.target.files[0]) { handleFileUpload(e.target.files[0]); } }); // Drag and Drop ['dragenter', 'dragover'].forEach(eventName => { dropzone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag-over'); }); }); ['dragleave', 'drop'].forEach(eventName => { dropzone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag-over'); }); }); dropzone.addEventListener('drop', (e) => { const dt = e.dataTransfer; if (dt && dt.files && dt.files[0]) { handleFileUpload(dt.files[0]); } }); // ========================================== // Spreadsheet Viewer & Worksheet Switching // ========================================== async function loadFileDetailsAndSheet(fileId, targetSheet = null) { if (isDirty) { if (!confirm('You have unsaved changes in this sheet. Switch without saving?')) { return; } } isDirty = false; unsavedBadge.style.display = 'none'; gridLoading.style.display = 'flex'; sheetEmptyMsg.style.display = 'none'; sheetThead.innerHTML = ''; sheetTbody.innerHTML = ''; sheetTabsList.innerHTML = ''; renderedRowCount = 0; renderedRowsInfo.textContent = 'Loading...'; try { let url = `/api/files/${encodeURIComponent(fileId)}/sheet`; if (targetSheet) { url += `?name=${encodeURIComponent(targetSheet)}`; } const res = await fetch(url); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to load sheet data.'); } currentSheetNames = data.sheetNames || []; currentActiveSheet = data.sheetName; currentSheetRows = data.rows || []; currentTotalCols = data.totalCols; // Render Sheet Tabs renderSheetTabs(currentSheetNames, currentActiveSheet); // Render Grid renderSpreadsheetGrid(currentSheetRows, data.totalCols); // Update stats badge sheetStats.textContent = `Sheet: ${currentActiveSheet} (${data.totalRows} rows × ${data.totalCols} cols)`; } catch (err) { showNotification('Error loading spreadsheet: ' + err.message); } finally { gridLoading.style.display = 'none'; } } function renderSheetTabs(sheetNames, activeSheet) { sheetTabsList.innerHTML = ''; sheetNames.forEach(name => { const tab = document.createElement('button'); tab.className = 'sheet-tab' + (name === activeSheet ? ' active' : ''); tab.innerHTML = ` ${escapeHtml(name)} `; tab.addEventListener('click', () => { if (name !== currentActiveSheet) { loadFileDetailsAndSheet(currentFileId, name); } }); sheetTabsList.appendChild(tab); }); } function renderSpreadsheetGrid(rows, totalCols) { sheetThead.innerHTML = ''; sheetTbody.innerHTML = ''; renderedRowCount = 0; if (!rows || rows.length === 0 || totalCols === 0) { sheetEmptyMsg.style.display = 'block'; renderedRowsInfo.textContent = '0 rows'; return; } sheetEmptyMsg.style.display = 'none'; // 1. Build Header Row with Excel Column Letters (A, B, C...) const headerRow = document.createElement('tr'); // Corner cell (top-left intersection for row & column headers) const cornerCell = document.createElement('th'); cornerCell.className = 'corner-cell'; cornerCell.textContent = ''; headerRow.appendChild(cornerCell); // Column letters: A, B, C... for (let c = 0; c < totalCols; c++) { const th = document.createElement('th'); th.textContent = getExcelColLetter(c); th.title = `Column ${getExcelColLetter(c)}`; headerRow.appendChild(th); } sheetThead.appendChild(headerRow); // 2. Render initial chunk of rows appendRowChunk(); // Reset scroll position to top gridViewport.scrollTop = 0; gridViewport.scrollLeft = 0; } // Progressive rendering for large datasets function appendRowChunk() { if (renderedRowCount >= currentSheetRows.length) return; const nextBatchEnd = Math.min(renderedRowCount + CHUNK_SIZE, currentSheetRows.length); const fragment = document.createDocumentFragment(); for (let r = renderedRowCount; r < nextBatchEnd; r++) { const rowData = currentSheetRows[r]; const tr = document.createElement('tr'); // Left-hand row number cell (1-indexed) const rowHeader = document.createElement('th'); rowHeader.className = 'row-header'; rowHeader.textContent = r + 1; tr.appendChild(rowHeader); // Data cells (editable) for (let c = 0; c < rowData.length; c++) { const td = document.createElement('td'); const val = rowData[c]; td.textContent = (val !== undefined && val !== null) ? val : ''; td.className = 'editable-cell'; td.contentEditable = 'true'; td.spellcheck = false; td.dataset.row = r; td.dataset.col = c; // Track edits td.addEventListener('input', () => { const newVal = td.innerText; currentSheetRows[r][c] = newVal; td.classList.add('cell-modified'); if (!isDirty) { isDirty = true; unsavedBadge.style.display = 'inline-flex'; } }); // Navigation on Enter td.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const nextRow = r + 1; const nextCell = sheetTbody.querySelector(`td[data-row="${nextRow}"][data-col="${c}"]`); if (nextCell) { nextCell.focus(); } else { td.blur(); } } }); tr.appendChild(td); } fragment.appendChild(tr); } sheetTbody.appendChild(fragment); renderedRowCount = nextBatchEnd; // Update row count footer renderedRowsInfo.textContent = `Showing ${renderedRowCount} of ${currentSheetRows.length} rows`; } // Add Row Button Handler btnAddRow.addEventListener('click', () => { if (!currentFileId) return; const cols = currentTotalCols > 0 ? currentTotalCols : (currentSheetRows[0] ? currentSheetRows[0].length : 5); const newRow = new Array(cols).fill(''); const rowIndex = currentSheetRows.length; currentSheetRows.push(newRow); const tr = document.createElement('tr'); const rowHeader = document.createElement('th'); rowHeader.className = 'row-header'; rowHeader.textContent = rowIndex + 1; tr.appendChild(rowHeader); for (let c = 0; c < cols; c++) { const td = document.createElement('td'); td.textContent = ''; td.className = 'editable-cell cell-modified'; td.contentEditable = 'true'; td.spellcheck = false; td.dataset.row = rowIndex; td.dataset.col = c; td.addEventListener('input', () => { currentSheetRows[rowIndex][c] = td.innerText; td.classList.add('cell-modified'); if (!isDirty) { isDirty = true; unsavedBadge.style.display = 'inline-flex'; } }); td.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); td.blur(); } }); tr.appendChild(td); } sheetTbody.appendChild(tr); renderedRowCount++; sheetStats.textContent = `Sheet: ${currentActiveSheet} (${currentSheetRows.length} rows × ${cols} cols)`; renderedRowsInfo.textContent = `Showing ${renderedRowCount} of ${currentSheetRows.length} rows`; if (!isDirty) { isDirty = true; unsavedBadge.style.display = 'inline-flex'; } // Scroll to bottom & focus the new row's first cell gridViewport.scrollTop = gridViewport.scrollHeight; const firstCell = tr.querySelector('td'); if (firstCell) firstCell.focus(); }); // Save Sheet Button Handler btnSaveSheet.addEventListener('click', async () => { if (!currentFileId || !currentActiveSheet) return; const originalBtnHtml = btnSaveSheet.innerHTML; btnSaveSheet.disabled = true; btnSaveSheet.innerHTML = `
Saving... `; try { const res = await fetch(`/api/files/${encodeURIComponent(currentFileId)}/sheet`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sheetName: currentActiveSheet, rows: currentSheetRows }) }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to save worksheet.'); } isDirty = false; unsavedBadge.style.display = 'none'; // Clear modified styling on all cells sheetTbody.querySelectorAll('.cell-modified').forEach(cell => { cell.classList.remove('cell-modified'); }); showNotification(`"${currentActiveSheet}" saved successfully to Excel file!`, true); } catch (err) { showNotification('Save Error: ' + err.message); } finally { btnSaveSheet.disabled = false; btnSaveSheet.innerHTML = originalBtnHtml; } }); // Infinite scroll listener on table viewport gridViewport.addEventListener('scroll', () => { if (renderedRowCount >= currentSheetRows.length) return; const scrollBottom = gridViewport.scrollHeight - gridViewport.scrollTop - gridViewport.clientHeight; // Trigger next chunk when user scrolls within 300px of bottom if (scrollBottom < 300) { appendRowChunk(); } }); function escapeHtml(text) { if (!text) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Check URL hash on startup for direct link support function checkUrlHash() { const hash = window.location.hash; if (hash && hash.startsWith('#file=')) { const fileId = decodeURIComponent(hash.substring(6)); if (fileId) { navigateToSpreadsheet(fileId, fileId); return; } } loadFilesList(); } window.addEventListener('popstate', () => { checkUrlHash(); }); // Initial load checkUrlHash(); });