| const form = document.querySelector("#uploadForm"); |
| const pdfInput = document.querySelector("#pdfInput"); |
| const fileName = document.querySelector("#fileName"); |
| const startButton = document.querySelector("#startButton"); |
| const statusText = document.querySelector("#statusText"); |
| const batchText = document.querySelector("#batchText"); |
| const progressBar = document.querySelector("#progressBar"); |
| const jobIdText = document.querySelector("#jobIdText"); |
| const pageCounter = document.querySelector("#pageCounter"); |
| const tabCounter = document.querySelector("#tabCounter"); |
| const messageText = document.querySelector("#messageText"); |
| const downloadLink = document.querySelector("#downloadLink"); |
| const downloadCsvLink = document.querySelector("#downloadCsvLink"); |
| const previewBody = document.querySelector("#previewBody"); |
| const rowCount = document.querySelector("#rowCount"); |
| const browserViewStatus = document.querySelector("#browserViewStatus"); |
| const browserViewImage = document.querySelector("#browserViewImage"); |
| const browserViewEmpty = document.querySelector("#browserViewEmpty"); |
|
|
| let pollTimer = null; |
|
|
| pdfInput.addEventListener("change", () => { |
| fileName.textContent = pdfInput.files[0]?.name || "Choose a bank statement PDF"; |
| }); |
|
|
| form.addEventListener("submit", async (event) => { |
| event.preventDefault(); |
| clearInterval(pollTimer); |
| downloadLink.hidden = true; |
| downloadCsvLink.hidden = true; |
| startButton.disabled = true; |
| setStatus({ id: "", status: "uploading", message: "Uploading PDF", progress: 2, completed_batches: 0, total_batches: 0 }); |
|
|
| const formData = new FormData(form); |
| try { |
| const response = await fetch("/api/jobs", { method: "POST", body: formData }); |
| if (!response.ok) throw new Error(await readError(response)); |
| const payload = await response.json(); |
| pollJob(payload.job_id); |
| pollTimer = setInterval(() => pollJob(payload.job_id), 2500); |
| } catch (error) { |
| setStatus({ status: "error", message: error.message, progress: 100, completed_batches: 0, total_batches: 0 }); |
| startButton.disabled = false; |
| } |
| }); |
|
|
| async function pollJob(jobId) { |
| try { |
| const response = await fetch(`/api/jobs/${jobId}`); |
| if (!response.ok) throw new Error(await readError(response)); |
| const job = await response.json(); |
| setStatus(job); |
| updateBrowserView(job); |
|
|
| if (job.status === "done") { |
| clearInterval(pollTimer); |
| startButton.disabled = false; |
| downloadLink.hidden = false; |
| downloadLink.href = `/api/jobs/${jobId}/download`; |
| downloadCsvLink.hidden = false; |
| downloadCsvLink.href = `/api/jobs/${jobId}/download.csv`; |
| renderPreview(job.data_preview || []); |
| } |
|
|
| if (job.status === "error") { |
| clearInterval(pollTimer); |
| startButton.disabled = false; |
| } |
| } catch (error) { |
| clearInterval(pollTimer); |
| startButton.disabled = false; |
| setStatus({ status: "error", message: error.message, progress: 100, completed_batches: 0, total_batches: 0 }); |
| } |
| } |
|
|
| function setStatus(job) { |
| statusText.textContent = titleCase(job.status || "ready"); |
| jobIdText.textContent = job.id ? `Job ${job.id}` : "No job started"; |
| pageCounter.textContent = `${job.completed_pages || 0} / ${job.total_pages || 0} pages`; |
| tabCounter.textContent = `${job.active_tabs || 0} active tabs`; |
| const pageText = job.current_pages ? ` Current pages: ${job.current_pages}.` : ""; |
| messageText.textContent = `${job.error || job.message || ""}${pageText}`; |
| batchText.textContent = `${job.completed_batches || 0} / ${job.total_batches || 0} waves`; |
| progressBar.style.width = `${Math.max(0, Math.min(job.progress || 0, 100))}%`; |
| } |
|
|
| function updateBrowserView(job) { |
| if (!job.id || !job.latest_screenshot) { |
| browserViewStatus.textContent = "Waiting"; |
| return; |
| } |
| browserViewStatus.textContent = "Latest screenshot"; |
| browserViewImage.hidden = false; |
| browserViewEmpty.hidden = true; |
| browserViewImage.src = `/api/jobs/${job.id}/browser-view/latest?t=${Date.now()}`; |
| } |
|
|
| function renderPreview(rows) { |
| rowCount.textContent = rows.length ? `First ${rows.length} rows` : "No rows returned"; |
| if (!rows.length) { |
| previewBody.innerHTML = '<tr><td colspan="5" class="empty">No preview rows were returned.</td></tr>'; |
| return; |
| } |
|
|
| previewBody.innerHTML = rows.map((row) => ` |
| <tr> |
| <td>${escapeHtml(row.date || "")}</td> |
| <td>${escapeHtml(row.description || "")}</td> |
| <td>${escapeHtml(row.voucher_type || "")}</td> |
| <td>${escapeHtml(row.amount || "")}</td> |
| <td>${escapeHtml(row.closing || "")}</td> |
| </tr> |
| `).join(""); |
| } |
|
|
| async function readError(response) { |
| try { |
| const payload = await response.json(); |
| return payload.detail || response.statusText; |
| } catch { |
| return response.statusText; |
| } |
| } |
|
|
| function titleCase(value) { |
| return value.charAt(0).toUpperCase() + value.slice(1); |
| } |
|
|
| function escapeHtml(value) { |
| return String(value) |
| .replaceAll("&", "&") |
| .replaceAll("<", "<") |
| .replaceAll(">", ">") |
| .replaceAll('"', """) |
| .replaceAll("'", "'"); |
| } |
|
|