Add live progress for historical Purchase and Sales sync
This commit is contained in:
@@ -17,6 +17,25 @@
|
||||
</section>
|
||||
|
||||
{% if selected_client %}
|
||||
{% if latest_run %}
|
||||
<section id="historicalRunProgress"
|
||||
data-status-url="/tools/accounting/historical-learning/run/{{ latest_run.id }}/status"
|
||||
class="rounded-2xl border border-blue-200 bg-white p-5 shadow-soft">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-[0.14em] text-blue-700">Latest Historical Purchase Run</div>
|
||||
<div id="runProgressLabel" class="mt-1 text-lg font-semibold text-slate-900">{{ latest_run.status|replace('_',' ')|title }}</div>
|
||||
<div id="runProgressDetail" class="mt-1 text-sm text-slate-500">{{ latest_run.company_name or latest_run.tally_guid }}</div>
|
||||
</div>
|
||||
<div id="runProgressMeta" class="text-right text-xs text-slate-500">Run #{{ latest_run.id }}</div>
|
||||
</div>
|
||||
<div class="mt-4 h-2 overflow-hidden rounded-full bg-slate-200">
|
||||
<div id="runProgressBar" class="h-full w-1/3 rounded-full bg-blue-600 transition-all duration-500"></div>
|
||||
</div>
|
||||
<div id="runProgressError" class="mt-3 hidden rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Collect Historical Purchase Evidence</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Select the connected ERP Local Agent workstation and currently open mapped Tally company. The agent refreshes the selected period read-only before returning neutral ledger-history facts; classification stays on the ERP server.</p>
|
||||
@@ -59,4 +78,80 @@
|
||||
<script>
|
||||
const s=document.getElementById('wsCompany'); if(s){s.addEventListener('change',()=>{const p=s.value.split('|');document.getElementById('workstationId').value=p[0]||'';document.getElementById('tallyGuid').value=p[1]||'';document.getElementById('companyName').value=p.slice(2).join('|')||'';});}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const box = document.getElementById("historicalRunProgress");
|
||||
if (!box) return;
|
||||
const url = box.dataset.statusUrl;
|
||||
const label = document.getElementById("runProgressLabel");
|
||||
const detail = document.getElementById("runProgressDetail");
|
||||
const meta = document.getElementById("runProgressMeta");
|
||||
const bar = document.getElementById("runProgressBar");
|
||||
const errorBox = document.getElementById("runProgressError");
|
||||
let completedReloaded = false;
|
||||
|
||||
function humanSeconds(seconds) {
|
||||
seconds = Number(seconds || 0);
|
||||
if (seconds < 60) return seconds + " sec";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return mins + " min " + secs + " sec";
|
||||
}
|
||||
|
||||
function paint(data) {
|
||||
label.textContent = data.label || data.status || "Working";
|
||||
detail.textContent = [
|
||||
data.workstation,
|
||||
data.company_name,
|
||||
data.date_from && data.date_to ? (data.date_from + " → " + data.date_to) : ""
|
||||
].filter(Boolean).join(" · ");
|
||||
meta.textContent =
|
||||
"Run #" + data.run_id +
|
||||
" · attempt " + (data.attempts || 0) + "/" + (data.max_attempts || 0) +
|
||||
" · elapsed " + humanSeconds(data.elapsed_seconds);
|
||||
|
||||
if (data.stage === "queued") {
|
||||
bar.className = "h-full w-1/4 rounded-full bg-blue-500 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "processing") {
|
||||
bar.className = "h-full w-2/3 rounded-full bg-blue-600 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "ingesting") {
|
||||
bar.className = "h-full w-5/6 rounded-full bg-indigo-600 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "completed") {
|
||||
bar.className = "h-full w-full rounded-full bg-emerald-600 transition-all duration-500";
|
||||
label.textContent = "Completed · " + (data.evidence_rows || 0) + " evidence row(s)";
|
||||
} else if (data.stage === "failed") {
|
||||
bar.className = "h-full w-full rounded-full bg-red-600 transition-all duration-500";
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
errorBox.textContent = data.error;
|
||||
errorBox.classList.remove("hidden");
|
||||
} else {
|
||||
errorBox.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (data.terminal && !completedReloaded) {
|
||||
completedReloaded = true;
|
||||
window.setTimeout(function () { window.location.reload(); }, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const response = await fetch(url, {headers: {"Accept": "application/json"}, cache: "no-store"});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ok) paint(data);
|
||||
}
|
||||
} catch (e) {
|
||||
// Keep the last visible state; next poll will retry.
|
||||
}
|
||||
if (!completedReloaded) window.setTimeout(poll, 3000);
|
||||
}
|
||||
|
||||
poll();
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -32,6 +32,26 @@
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{% if latest_run %}
|
||||
<section id="historicalRunProgress"
|
||||
data-status-url="/tools/accounting/sales-learning/run/{{ latest_run.id }}/status"
|
||||
data-run-status="{{ latest_run.status }}"
|
||||
class="rounded-2xl border border-blue-200 bg-white p-5 shadow-soft">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-[0.14em] text-blue-700">Latest Historical Sales Run</div>
|
||||
<div id="runProgressLabel" class="mt-1 text-lg font-semibold text-slate-900">{{ latest_run.status|replace('_',' ')|title }}</div>
|
||||
<div id="runProgressDetail" class="mt-1 text-sm text-slate-500">{{ latest_run.company_name or latest_run.tally_guid }}</div>
|
||||
</div>
|
||||
<div id="runProgressMeta" class="text-right text-xs text-slate-500">Run #{{ latest_run.id }}</div>
|
||||
</div>
|
||||
<div class="mt-4 h-2 overflow-hidden rounded-full bg-slate-200">
|
||||
<div id="runProgressBar" class="h-full w-1/3 rounded-full bg-blue-600 transition-all duration-500"></div>
|
||||
</div>
|
||||
<div id="runProgressError" class="mt-3 hidden rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h2 class="font-semibold">Collect Historical Sales Evidence from Tally</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Read-only. The selected workstation refreshes Sales vouchers from the currently open mapped Tally company for the chosen period, then stores neutral historical evidence. It does not create or alter Tally vouchers.</p>
|
||||
@@ -109,4 +129,80 @@
|
||||
syncSelection();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const box = document.getElementById("historicalRunProgress");
|
||||
if (!box) return;
|
||||
const url = box.dataset.statusUrl;
|
||||
const label = document.getElementById("runProgressLabel");
|
||||
const detail = document.getElementById("runProgressDetail");
|
||||
const meta = document.getElementById("runProgressMeta");
|
||||
const bar = document.getElementById("runProgressBar");
|
||||
const errorBox = document.getElementById("runProgressError");
|
||||
let completedReloaded = false;
|
||||
|
||||
function humanSeconds(seconds) {
|
||||
seconds = Number(seconds || 0);
|
||||
if (seconds < 60) return seconds + " sec";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return mins + " min " + secs + " sec";
|
||||
}
|
||||
|
||||
function paint(data) {
|
||||
label.textContent = data.label || data.status || "Working";
|
||||
detail.textContent = [
|
||||
data.workstation,
|
||||
data.company_name,
|
||||
data.date_from && data.date_to ? (data.date_from + " → " + data.date_to) : ""
|
||||
].filter(Boolean).join(" · ");
|
||||
meta.textContent =
|
||||
"Run #" + data.run_id +
|
||||
" · attempt " + (data.attempts || 0) + "/" + (data.max_attempts || 0) +
|
||||
" · elapsed " + humanSeconds(data.elapsed_seconds);
|
||||
|
||||
if (data.stage === "queued") {
|
||||
bar.className = "h-full w-1/4 rounded-full bg-blue-500 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "processing") {
|
||||
bar.className = "h-full w-2/3 rounded-full bg-blue-600 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "ingesting") {
|
||||
bar.className = "h-full w-5/6 rounded-full bg-indigo-600 animate-pulse transition-all duration-500";
|
||||
} else if (data.stage === "completed") {
|
||||
bar.className = "h-full w-full rounded-full bg-emerald-600 transition-all duration-500";
|
||||
label.textContent = "Completed · " + (data.evidence_rows || 0) + " evidence row(s)";
|
||||
} else if (data.stage === "failed") {
|
||||
bar.className = "h-full w-full rounded-full bg-red-600 transition-all duration-500";
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
errorBox.textContent = data.error;
|
||||
errorBox.classList.remove("hidden");
|
||||
} else {
|
||||
errorBox.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (data.terminal && !completedReloaded) {
|
||||
completedReloaded = true;
|
||||
window.setTimeout(function () { window.location.reload(); }, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const response = await fetch(url, {headers: {"Accept": "application/json"}, cache: "no-store"});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ok) paint(data);
|
||||
}
|
||||
} catch (e) {
|
||||
// Keep the last visible state; next poll will retry.
|
||||
}
|
||||
if (!completedReloaded) window.setTimeout(poll, 3000);
|
||||
}
|
||||
|
||||
poll();
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user