fix: separate days now are in separate .xlsx files, NRLs still 1 per sheet.
add: rebuild script for prod. fix: Improved data parsing, now filters out unneeded Lp files and .xlsx files.
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Generate Excel
|
||||
Generate Reports (ZIP)
|
||||
</button>
|
||||
<a href="/api/projects/{{ project_id }}/combined-report-wizard"
|
||||
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm">
|
||||
@@ -238,7 +238,7 @@ async function downloadCombinedReport() {
|
||||
const btn = document.getElementById('download-btn');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> Generating...';
|
||||
btn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> Generating ZIP...';
|
||||
|
||||
try {
|
||||
const locations = allLocationData.map(function(loc) {
|
||||
@@ -268,7 +268,7 @@ async function downloadCombinedReport() {
|
||||
a.href = url;
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'combined_report.xlsx';
|
||||
let filename = 'combined_reports.zip';
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="(.+)"/);
|
||||
if (match) filename = match[1];
|
||||
|
||||
@@ -385,16 +385,27 @@
|
||||
file:text-sm file:font-medium file:bg-seismo-orange file:text-white
|
||||
hover:file:bg-seismo-navy file:cursor-pointer" />
|
||||
<div class="flex items-center gap-3 mt-3">
|
||||
<button onclick="submitUpload()"
|
||||
<button id="upload-btn" onclick="submitUpload()"
|
||||
class="px-4 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
Import Files
|
||||
</button>
|
||||
<button onclick="toggleUploadPanel()"
|
||||
<button id="upload-cancel-btn" onclick="toggleUploadPanel()"
|
||||
class="px-4 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<span id="upload-status" class="text-sm hidden"></span>
|
||||
</div>
|
||||
<!-- Progress bar (hidden until upload starts) -->
|
||||
<div id="upload-progress-wrap" class="hidden mt-3">
|
||||
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
<span id="upload-progress-label">Uploading…</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div id="upload-progress-bar"
|
||||
class="bg-green-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="data-files-list"
|
||||
@@ -629,57 +640,105 @@ function toggleUploadPanel() {
|
||||
const panel = document.getElementById('upload-panel');
|
||||
const status = document.getElementById('upload-status');
|
||||
panel.classList.toggle('hidden');
|
||||
// Reset status when reopening
|
||||
// Reset state when reopening
|
||||
if (!panel.classList.contains('hidden')) {
|
||||
status.textContent = '';
|
||||
status.className = 'text-sm hidden';
|
||||
document.getElementById('upload-input').value = '';
|
||||
document.getElementById('upload-progress-wrap').classList.add('hidden');
|
||||
document.getElementById('upload-progress-bar').style.width = '0%';
|
||||
}
|
||||
}
|
||||
|
||||
async function submitUpload() {
|
||||
function submitUpload() {
|
||||
const input = document.getElementById('upload-input');
|
||||
const status = document.getElementById('upload-status');
|
||||
const btn = document.getElementById('upload-btn');
|
||||
const cancelBtn = document.getElementById('upload-cancel-btn');
|
||||
const progressWrap = document.getElementById('upload-progress-wrap');
|
||||
const progressBar = document.getElementById('upload-progress-bar');
|
||||
const progressLabel = document.getElementById('upload-progress-label');
|
||||
|
||||
if (!input.files.length) {
|
||||
alert('Please select files to upload.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileCount = input.files.length;
|
||||
const formData = new FormData();
|
||||
for (const file of input.files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
status.textContent = 'Uploading\u2026';
|
||||
status.className = 'text-sm text-gray-500';
|
||||
// Disable controls and show progress bar
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Uploading\u2026';
|
||||
btn.classList.add('opacity-60', 'cursor-not-allowed');
|
||||
cancelBtn.disabled = true;
|
||||
cancelBtn.classList.add('opacity-40', 'cursor-not-allowed');
|
||||
status.className = 'text-sm hidden';
|
||||
progressWrap.classList.remove('hidden');
|
||||
progressBar.style.width = '0%';
|
||||
progressLabel.textContent = `Uploading ${fileCount} file${fileCount !== 1 ? 's' : ''}\u2026`;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/projects/${projectId}/nrl/${locationId}/upload-data`,
|
||||
{ method: 'POST', body: formData }
|
||||
);
|
||||
const data = await response.json();
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
if (response.ok) {
|
||||
const parts = [`Imported ${data.files_imported} file${data.files_imported !== 1 ? 's' : ''}`];
|
||||
if (data.leq_files || data.lp_files) {
|
||||
parts.push(`(${data.leq_files} Leq, ${data.lp_files} Lp)`);
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const pct = Math.round((e.loaded / e.total) * 100);
|
||||
progressBar.style.width = pct + '%';
|
||||
progressLabel.textContent = `Uploading ${fileCount} file${fileCount !== 1 ? 's' : ''}\u2026 ${pct}%`;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.upload.addEventListener('load', () => {
|
||||
progressBar.style.width = '100%';
|
||||
progressLabel.textContent = 'Processing files on server\u2026';
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
progressWrap.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Import Files';
|
||||
btn.classList.remove('opacity-60', 'cursor-not-allowed');
|
||||
cancelBtn.disabled = false;
|
||||
cancelBtn.classList.remove('opacity-40', 'cursor-not-allowed');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const parts = [`Imported ${data.files_imported} file${data.files_imported !== 1 ? 's' : ''}`];
|
||||
if (data.leq_files || data.lp_files) {
|
||||
parts.push(`(${data.leq_files} Leq, ${data.lp_files} Lp)`);
|
||||
}
|
||||
if (data.store_name) parts.push(`\u2014 ${data.store_name}`);
|
||||
status.textContent = parts.join(' ');
|
||||
status.className = 'text-sm text-green-600 dark:text-green-400';
|
||||
input.value = '';
|
||||
htmx.trigger(document.getElementById('data-files-list'), 'load');
|
||||
} else {
|
||||
status.textContent = `Error: ${data.detail || 'Upload failed'}`;
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
if (data.store_name) parts.push(`\u2014 ${data.store_name}`);
|
||||
status.textContent = parts.join(' ');
|
||||
status.className = 'text-sm text-green-600 dark:text-green-400';
|
||||
input.value = '';
|
||||
// Refresh the file list
|
||||
htmx.trigger(document.getElementById('data-files-list'), 'load');
|
||||
} else {
|
||||
status.textContent = `Error: ${data.detail || 'Upload failed'}`;
|
||||
} catch {
|
||||
status.textContent = 'Error: Unexpected server response';
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
} catch (err) {
|
||||
status.textContent = `Error: ${err.message}`;
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
progressWrap.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Import Files';
|
||||
btn.classList.remove('opacity-60', 'cursor-not-allowed');
|
||||
cancelBtn.disabled = false;
|
||||
cancelBtn.classList.remove('opacity-40', 'cursor-not-allowed');
|
||||
status.textContent = 'Error: Network error during upload';
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
});
|
||||
|
||||
xhr.open('POST', `/api/projects/${projectId}/nrl/${locationId}/upload-data`);
|
||||
xhr.send(formData);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -264,16 +264,28 @@
|
||||
file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0
|
||||
file:text-sm file:font-medium file:bg-seismo-orange file:text-white
|
||||
hover:file:bg-seismo-navy file:cursor-pointer" />
|
||||
<button onclick="submitUploadAll()"
|
||||
<span id="upload-all-file-count" class="text-xs text-gray-500 dark:text-gray-400 hidden"></span>
|
||||
<button id="upload-all-btn" onclick="submitUploadAll()"
|
||||
class="px-4 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
Import
|
||||
</button>
|
||||
<button onclick="toggleUploadAll()"
|
||||
<button id="upload-all-cancel-btn" onclick="toggleUploadAll()"
|
||||
class="px-4 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<span id="upload-all-status" class="text-sm hidden"></span>
|
||||
</div>
|
||||
<!-- Progress bar -->
|
||||
<div id="upload-all-progress-wrap" class="hidden mt-3">
|
||||
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
<span id="upload-all-progress-label">Uploading…</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div id="upload-all-progress-bar"
|
||||
class="bg-green-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result summary -->
|
||||
<div id="upload-all-results" class="hidden mt-3 text-sm space-y-1"></div>
|
||||
</div>
|
||||
@@ -1642,75 +1654,148 @@ function toggleUploadAll() {
|
||||
document.getElementById('upload-all-results').classList.add('hidden');
|
||||
document.getElementById('upload-all-results').innerHTML = '';
|
||||
document.getElementById('upload-all-input').value = '';
|
||||
document.getElementById('upload-all-file-count').classList.add('hidden');
|
||||
document.getElementById('upload-all-progress-wrap').classList.add('hidden');
|
||||
document.getElementById('upload-all-progress-bar').style.width = '0%';
|
||||
}
|
||||
}
|
||||
|
||||
async function submitUploadAll() {
|
||||
// Show file count and filter info when folder is selected
|
||||
document.getElementById('upload-all-input').addEventListener('change', function() {
|
||||
const countEl = document.getElementById('upload-all-file-count');
|
||||
const total = this.files.length;
|
||||
if (!total) { countEl.classList.add('hidden'); return; }
|
||||
const wanted = Array.from(this.files).filter(_isWantedFile).length;
|
||||
countEl.textContent = `${wanted} of ${total} files will be uploaded (Leq + .rnh only)`;
|
||||
countEl.classList.remove('hidden');
|
||||
});
|
||||
|
||||
function _isWantedFile(f) {
|
||||
const n = (f.webkitRelativePath || f.name).toLowerCase();
|
||||
const base = n.split('/').pop();
|
||||
if (base.endsWith('.rnh')) return true;
|
||||
if (base.endsWith('.rnd')) {
|
||||
if (base.includes('_leq_')) return true; // NL-43 Leq
|
||||
if (base.startsWith('au2_')) return true; // AU2/NL-23 format
|
||||
if (!base.includes('_lp')) return true; // unknown format — keep
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function submitUploadAll() {
|
||||
const input = document.getElementById('upload-all-input');
|
||||
const status = document.getElementById('upload-all-status');
|
||||
const resultsEl = document.getElementById('upload-all-results');
|
||||
const btn = document.getElementById('upload-all-btn');
|
||||
const cancelBtn = document.getElementById('upload-all-cancel-btn');
|
||||
const progressWrap = document.getElementById('upload-all-progress-wrap');
|
||||
const progressBar = document.getElementById('upload-all-progress-bar');
|
||||
const progressLabel = document.getElementById('upload-all-progress-label');
|
||||
|
||||
if (!input.files.length) {
|
||||
alert('Please select a folder to upload.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter client-side — only send Leq .rnd and .rnh files
|
||||
const filesToSend = Array.from(input.files).filter(_isWantedFile);
|
||||
if (!filesToSend.length) {
|
||||
alert('No Leq .rnd or .rnh files found in selected folder.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (const f of input.files) {
|
||||
// webkitRelativePath gives the path relative to the selected folder root
|
||||
for (const f of filesToSend) {
|
||||
formData.append('files', f);
|
||||
formData.append('paths', f.webkitRelativePath || f.name);
|
||||
}
|
||||
|
||||
status.textContent = `Uploading ${input.files.length} files\u2026`;
|
||||
status.className = 'text-sm text-gray-500';
|
||||
// Disable controls and show progress
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Uploading\u2026';
|
||||
btn.classList.add('opacity-60', 'cursor-not-allowed');
|
||||
cancelBtn.disabled = true;
|
||||
cancelBtn.classList.add('opacity-40', 'cursor-not-allowed');
|
||||
status.className = 'text-sm hidden';
|
||||
resultsEl.classList.add('hidden');
|
||||
progressWrap.classList.remove('hidden');
|
||||
progressBar.style.width = '0%';
|
||||
progressLabel.textContent = `Uploading ${filesToSend.length} files\u2026`;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/projects/{{ project_id }}/upload-all`,
|
||||
{ method: 'POST', body: formData }
|
||||
);
|
||||
const data = await response.json();
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
if (response.ok) {
|
||||
const s = data.sessions_created;
|
||||
const f = data.files_imported;
|
||||
status.textContent = `\u2713 Imported ${f} file${f !== 1 ? 's' : ''} across ${s} session${s !== 1 ? 's' : ''}`;
|
||||
status.className = 'text-sm text-green-600 dark:text-green-400';
|
||||
input.value = '';
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const pct = Math.round((e.loaded / e.total) * 100);
|
||||
progressBar.style.width = pct + '%';
|
||||
progressLabel.textContent = `Uploading ${filesToSend.length} files\u2026 ${pct}%`;
|
||||
}
|
||||
});
|
||||
|
||||
// Build results summary
|
||||
let html = '';
|
||||
if (data.sessions && data.sessions.length) {
|
||||
html += '<div class="font-medium text-gray-700 dark:text-gray-300 mb-1">Sessions created:</div>';
|
||||
html += '<ul class="space-y-0.5 ml-2">';
|
||||
for (const sess of data.sessions) {
|
||||
html += `<li class="text-xs text-gray-600 dark:text-gray-400">\u2022 <span class="font-medium">${sess.location_name}</span> — ${sess.files} files`;
|
||||
if (sess.leq_files || sess.lp_files) html += ` (${sess.leq_files} Leq, ${sess.lp_files} Lp)`;
|
||||
if (sess.store_name) html += ` — ${sess.store_name}`;
|
||||
html += '</li>';
|
||||
xhr.upload.addEventListener('load', () => {
|
||||
progressBar.style.width = '100%';
|
||||
progressLabel.textContent = 'Processing files on server\u2026';
|
||||
});
|
||||
|
||||
function _resetControls() {
|
||||
progressWrap.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Import';
|
||||
btn.classList.remove('opacity-60', 'cursor-not-allowed');
|
||||
cancelBtn.disabled = false;
|
||||
cancelBtn.classList.remove('opacity-40', 'cursor-not-allowed');
|
||||
}
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
_resetControls();
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const s = data.sessions_created;
|
||||
const f = data.files_imported;
|
||||
status.textContent = `\u2713 Imported ${f} file${f !== 1 ? 's' : ''} across ${s} session${s !== 1 ? 's' : ''}`;
|
||||
status.className = 'text-sm text-green-600 dark:text-green-400';
|
||||
input.value = '';
|
||||
document.getElementById('upload-all-file-count').classList.add('hidden');
|
||||
|
||||
let html = '';
|
||||
if (data.sessions && data.sessions.length) {
|
||||
html += '<div class="font-medium text-gray-700 dark:text-gray-300 mb-1">Sessions created:</div>';
|
||||
html += '<ul class="space-y-0.5 ml-2">';
|
||||
for (const sess of data.sessions) {
|
||||
html += `<li class="text-xs text-gray-600 dark:text-gray-400">\u2022 <span class="font-medium">${sess.location_name}</span> — ${sess.files} files`;
|
||||
if (sess.leq_files || sess.lp_files) html += ` (${sess.leq_files} Leq, ${sess.lp_files} Lp)`;
|
||||
if (sess.store_name) html += ` — ${sess.store_name}`;
|
||||
html += '</li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
html += '</ul>';
|
||||
if (data.unmatched_folders && data.unmatched_folders.length) {
|
||||
html += `<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">\u26a0 Unmatched folders (no NRL location found): ${data.unmatched_folders.join(', ')}</div>`;
|
||||
}
|
||||
if (html) {
|
||||
resultsEl.innerHTML = html;
|
||||
resultsEl.classList.remove('hidden');
|
||||
}
|
||||
htmx.trigger(document.getElementById('unified-files'), 'refresh');
|
||||
} else {
|
||||
status.textContent = `Error: ${data.detail || 'Upload failed'}`;
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
if (data.unmatched_folders && data.unmatched_folders.length) {
|
||||
html += `<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">\u26a0 Unmatched folders (no NRL location found): ${data.unmatched_folders.join(', ')}</div>`;
|
||||
}
|
||||
if (html) {
|
||||
resultsEl.innerHTML = html;
|
||||
resultsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Refresh the unified files view
|
||||
htmx.trigger(document.getElementById('unified-files'), 'refresh');
|
||||
} else {
|
||||
status.textContent = `Error: ${data.detail || 'Upload failed'}`;
|
||||
} catch {
|
||||
status.textContent = 'Error: Unexpected server response';
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
} catch (err) {
|
||||
status.textContent = `Error: ${err.message}`;
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
_resetControls();
|
||||
status.textContent = 'Error: Network error during upload';
|
||||
status.className = 'text-sm text-red-600 dark:text-red-400';
|
||||
}
|
||||
});
|
||||
|
||||
xhr.open('POST', `/api/projects/{{ project_id }}/upload-all`);
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// Load project details on page load and restore active tab from URL hash
|
||||
|
||||
Reference in New Issue
Block a user