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);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,81 @@
|
||||
:root {
|
||||
--app-bg: #f3f6fb;
|
||||
--app-ink: #16202a;
|
||||
--app-muted: #5d6b7a;
|
||||
--app-accent: #0f6efc;
|
||||
--app-accent-soft: rgba(15, 110, 252, 0.12);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--app-ink);
|
||||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15, 110, 252, 0.12), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(13, 202, 240, 0.16), transparent 30%),
|
||||
var(--app-bg);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-navbar {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.datatable-toolbar {
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
|
||||
.column-visibility-menu {
|
||||
min-width: 18rem;
|
||||
max-height: 24rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz {
|
||||
--ag-font-family: inherit;
|
||||
--ag-header-background-color: rgba(255, 255, 255, 0.98);
|
||||
--ag-header-foreground-color: var(--app-ink);
|
||||
--ag-background-color: rgba(255, 255, 255, 0.95);
|
||||
--ag-foreground-color: var(--app-ink);
|
||||
--ag-border-color: rgba(22, 32, 42, 0.12);
|
||||
--ag-row-hover-color: rgba(15, 110, 252, 0.06);
|
||||
--ag-selected-row-background-color: rgba(15, 110, 252, 0.12);
|
||||
--ag-header-column-separator-display: none;
|
||||
--ag-row-border-color: rgba(22, 32, 42, 0.08);
|
||||
--ag-cell-horizontal-padding: 14px;
|
||||
width: 100%;
|
||||
height: 72vh;
|
||||
min-height: 620px;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1rem 2.5rem rgba(22, 32, 42, 0.08);
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz .ag-header {
|
||||
border-bottom: 1px solid rgba(22, 32, 42, 0.12);
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz .ag-header-cell-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz .ag-floating-filter {
|
||||
border-top: 1px solid rgba(22, 32, 42, 0.08);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz .ag-cell {
|
||||
padding-top: 0.2rem;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.datatable-page .ag-theme-quartz .ag-row {
|
||||
border-bottom: 1px solid rgba(22, 32, 42, 0.06);
|
||||
}
|
||||
Reference in New Issue
Block a user