Initial
Takes in a file, process it and output a new file with additional columns.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
(() => {
|
||||
const form = document.getElementById('importForm');
|
||||
const clearButton = document.getElementById('clearButton');
|
||||
const progressBar = document.getElementById('importProgress');
|
||||
const progressLabel = document.getElementById('progressLabel');
|
||||
const progressPercent = document.getElementById('progressPercent');
|
||||
const totalRowsLabel = document.getElementById('importTotalRows');
|
||||
const chunkSizeLabel = document.getElementById('importChunkSize');
|
||||
const statusAlert = document.getElementById('statusAlert');
|
||||
const fileInput = document.getElementById('spreadsheet');
|
||||
const tokenStorageKey = 'warnerImportToken';
|
||||
|
||||
let importToken = window.sessionStorage.getItem(tokenStorageKey) || '';
|
||||
let chunkTimer = null;
|
||||
let chunkInFlight = false;
|
||||
|
||||
if (!form || !progressBar || !progressLabel || !progressPercent || !statusAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setStatus = (type, message) => {
|
||||
const classes = {
|
||||
success: 'border-emerald-200 bg-emerald-50 text-emerald-900',
|
||||
danger: 'border-rose-200 bg-rose-50 text-rose-900',
|
||||
warning: 'border-amber-200 bg-amber-50 text-amber-900',
|
||||
info: 'border-sky-200 bg-sky-50 text-sky-900',
|
||||
};
|
||||
|
||||
statusAlert.className = `mt-4 rounded-2xl border px-4 py-3 text-sm font-medium ${classes[type] || classes.info}`;
|
||||
statusAlert.textContent = message;
|
||||
statusAlert.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const setProgress = (value, label) => {
|
||||
const percent = Math.max(0, Math.min(100, Math.round(value)));
|
||||
progressBar.style.width = `${percent}%`;
|
||||
progressBar.setAttribute('aria-valuenow', String(percent));
|
||||
progressPercent.textContent = `${percent}%`;
|
||||
progressLabel.textContent = label;
|
||||
};
|
||||
|
||||
const setRowMetrics = (totalRows, chunkSize) => {
|
||||
if (totalRowsLabel) {
|
||||
totalRowsLabel.textContent = Number.isFinite(totalRows) ? String(totalRows.toLocaleString()) : '0';
|
||||
}
|
||||
|
||||
if (chunkSizeLabel && Number.isFinite(chunkSize)) {
|
||||
chunkSizeLabel.textContent = String(chunkSize.toLocaleString());
|
||||
}
|
||||
};
|
||||
|
||||
const clearImportState = () => {
|
||||
importToken = '';
|
||||
window.sessionStorage.removeItem(tokenStorageKey);
|
||||
if (chunkTimer !== null) {
|
||||
window.clearTimeout(chunkTimer);
|
||||
chunkTimer = null;
|
||||
}
|
||||
chunkInFlight = false;
|
||||
};
|
||||
|
||||
const scheduleChunk = (offset, delay = 200) => {
|
||||
if (!importToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunkTimer !== null) {
|
||||
window.clearTimeout(chunkTimer);
|
||||
}
|
||||
|
||||
chunkTimer = window.setTimeout(() => {
|
||||
processChunk(offset);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const parseJsonResponse = (xhr) => {
|
||||
try {
|
||||
return JSON.parse(xhr.responseText);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDestructiveAction = (message) => window.confirm(message);
|
||||
|
||||
const deriveProgressPercent = (response) => {
|
||||
if (response && response.progressPercent !== undefined) {
|
||||
return response.progressPercent;
|
||||
}
|
||||
|
||||
const total = Number(response && response.total ? response.total : 0);
|
||||
const offset = Number(response && response.offset ? response.offset : 0);
|
||||
if (total <= 0) {
|
||||
return response && response.done ? 100 : 0;
|
||||
}
|
||||
|
||||
return Math.min(99, Math.round((offset / total) * 100));
|
||||
};
|
||||
|
||||
const processChunk = (offset = 0) => {
|
||||
if (!importToken || chunkInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
chunkInFlight = true;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', window.APP_CONFIG.processChunkUrl, true);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
chunkInFlight = false;
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const response = parseJsonResponse(xhr);
|
||||
if (!response) {
|
||||
setStatus('danger', 'Import response was invalid.');
|
||||
clearImportState();
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = deriveProgressPercent(response);
|
||||
const message = response.message || 'Processing workbook...';
|
||||
setProgress(progress, message);
|
||||
if (response.totalRows !== undefined || response.chunkSize !== undefined) {
|
||||
setRowMetrics(
|
||||
Number(response.totalRows ?? 0),
|
||||
Number(response.chunkSize ?? (window.APP_CONFIG.importChunkSize ?? 0))
|
||||
);
|
||||
}
|
||||
|
||||
if (response.done) {
|
||||
setProgress(100, response.message || 'Import complete');
|
||||
setStatus('success', response.message || 'Import complete. Reloading preview...');
|
||||
clearImportState();
|
||||
window.setTimeout(() => {
|
||||
window.location.href = window.APP_CONFIG.reloadUrl;
|
||||
}, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('info', message);
|
||||
scheduleChunk(response.offset !== undefined ? response.offset : (offset + 1), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = parseJsonResponse(xhr);
|
||||
const responseMessage = response && response.message ? response.message : 'Import failed.';
|
||||
setProgress(0, 'Ready to import');
|
||||
setStatus('danger', responseMessage);
|
||||
clearImportState();
|
||||
};
|
||||
|
||||
xhr.send(
|
||||
`action=process-chunk&token=${encodeURIComponent(importToken)}&offset=${encodeURIComponent(String(offset))}`
|
||||
);
|
||||
};
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
statusAlert.classList.add('hidden');
|
||||
|
||||
if (!fileInput.files || fileInput.files.length === 0) {
|
||||
setStatus('danger', 'Please choose an Excel file before importing.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmDestructiveAction('Importing a new file will wipe the current imported data before processing. Continue?')) {
|
||||
setStatus('info', 'Import cancelled.');
|
||||
setProgress(0, 'Ready to import');
|
||||
return;
|
||||
}
|
||||
|
||||
clearImportState();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', window.APP_CONFIG.importUrl, true);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
setProgress(65, 'Uploading file...');
|
||||
return;
|
||||
}
|
||||
|
||||
const percent = Math.max(1, (event.loaded / event.total) * 70);
|
||||
setProgress(percent, 'Uploading file...');
|
||||
};
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const response = parseJsonResponse(xhr);
|
||||
if (response && response.token) {
|
||||
importToken = response.token;
|
||||
window.sessionStorage.setItem(tokenStorageKey, importToken);
|
||||
setProgress(5, response.message || 'Upload received. Starting chunked import...');
|
||||
setRowMetrics(0, Number(window.APP_CONFIG.importChunkSize || 0));
|
||||
setStatus('info', response.message || 'Upload received. Starting chunked import...');
|
||||
scheduleChunk(0, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('danger', 'Upload succeeded, but the server did not return an import token.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = parseJsonResponse(xhr);
|
||||
const responseMessage = response && response.message ? response.message : 'Import failed.';
|
||||
setProgress(0, 'Ready to import');
|
||||
setStatus('danger', responseMessage);
|
||||
};
|
||||
|
||||
setProgress(5, 'Preparing upload...');
|
||||
xhr.send(formData);
|
||||
setProgress(20, 'Uploading file...');
|
||||
});
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
if (!confirmDestructiveAction('This will permanently wipe all imported rows. Continue?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', window.APP_CONFIG.clearUrl, true);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
setProgress(0, 'Ready to import');
|
||||
setStatus('success', 'Imported data cleared. Reloading page...');
|
||||
clearImportState();
|
||||
window.setTimeout(() => {
|
||||
window.location.href = window.APP_CONFIG.reloadUrl;
|
||||
}, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = parseJsonResponse(xhr);
|
||||
const responseMessage = response && response.message ? response.message : 'Clear failed.';
|
||||
setStatus('danger', responseMessage);
|
||||
};
|
||||
|
||||
xhr.send('action=clear');
|
||||
});
|
||||
}
|
||||
|
||||
if (importToken) {
|
||||
setProgress(10, 'Resuming import...');
|
||||
setStatus('info', 'Resuming the last import job...');
|
||||
setRowMetrics(0, Number(window.APP_CONFIG.importChunkSize || 0));
|
||||
scheduleChunk(0, 200);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user