Add Phase 11B client aware multi bank ownership and engagement archive

This commit is contained in:
A R R R Associates
2026-08-22 20:45:36 +05:30
parent 8748ee9539
commit 016299cf0c
9 changed files with 714 additions and 8 deletions
@@ -13,13 +13,44 @@
<form action="/tools/bank-statement-analyzer/analyze" method="post" enctype="multipart/form-data" class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="grid gap-5 md:grid-cols-2">
{% if analyzer_clients %}
<div class="md:col-span-2 rounded-xl border border-indigo-200 bg-indigo-50 p-4">
<div class="grid gap-4 md:grid-cols-2">
<label class="text-sm font-semibold text-slate-700">
ERP Client <span class="font-normal text-slate-400">(optional)</span>
<select name="client_id" id="bank-analyzer-client" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
<option value="">Standalone analysis — keep current Work-folder storage</option>
{% for client in analyzer_clients %}
<option value="{{ client.id }}" {% if selected_client_id == client.id %}selected{% endif %}>{{ client.client_code }} · {{ client.client_name }}{% if client.trade_name %} · {{ client.trade_name }}{% endif %}</option>
{% endfor %}
</select>
</label>
<label class="text-sm font-semibold text-slate-700">
Engagement document destination
<select name="engagement_id" id="bank-analyzer-engagement" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
<option value="">Select engagement after selecting client</option>
{% for engagement in analyzer_engagements %}
<option value="{{ engagement.id }}" data-client-id="{{ engagement.client_id }}" {% if selected_engagement_id == engagement.id %}selected{% endif %}>
{{ engagement.financial_year }} · {{ engagement.catalogue.service_name if engagement.catalogue else ('Engagement #' ~ engagement.id) }}{% if engagement.period_label %} · {{ engagement.period_label }}{% endif %}
</option>
{% endfor %}
</select>
</label>
</div>
<label class="mt-3 flex items-start gap-2 text-sm text-slate-700">
<input type="checkbox" name="confirm_same_client" value="1" class="mt-1 rounded border-slate-300">
<span>I confirm that all statements uploaded in this analysis belong to the selected client or one of its ERP-recorded legal/trade/business names.</span>
</label>
<p class="mt-2 text-xs text-indigo-700">When a client is selected, the analyzer validates every statement owner against the client name, trade name, business-unit names, registration legal/trade names and branch names. On successful analysis, the source PDFs and generated workbook are archived using the existing Engagement Documents storage pipeline.</p>
</div>
{% endif %}
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Bank</label><select name="bank_selection" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">{% for value, label in bank_options %}<option value="{{ value }}" {% if selected_bank == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">Keep Auto Detect or select a bank for direct parser validation.</p></div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Financial year <span class="font-normal text-slate-400">(optional)</span></label><input name="financial_year" value="{{ financial_year or '' }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Example: 2025-26"></div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Account holder override <span class="font-normal text-slate-400">(optional)</span></label><input name="customer_name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div>
<div><label class="mb-1 block text-sm font-semibold text-slate-700">Account number override <span class="font-normal text-slate-400">(optional)</span></label><input name="account_number" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm" placeholder="Use only when statement extraction needs correction"></div>
</div>
<div class="mt-5 rounded-xl border border-slate-200 bg-slate-50 p-4"><label class="flex items-start gap-3"><input type="checkbox" name="enable_classification" value="1" {% if classification_enabled %}checked{% endif %} class="mt-1 h-4 w-4 rounded border-slate-300"><span><span class="block text-sm font-semibold text-slate-800">Enable narration-based transaction classification</span><span class="mt-1 block text-xs text-slate-500">Adds category, party, review and draft financial helper sheets. Final classification must be verified with books and supporting records.</span></span></label></div>
<div class="mt-5 rounded-xl border border-brand-200 bg-brand-50 p-4 text-sm text-slate-700"><strong>Multi-bank contra detection:</strong> when the client has more than one bank account, upload all relevant statements in the same analysis job. The analyzer pairs only conservative equal-and-opposite transfers across different account numbers and keeps every pair reviewable.</div><div class="mt-5"><label class="mb-1 block text-sm font-semibold text-slate-700">PDF bank statements</label><input type="file" name="statements" accept="application/pdf,.pdf" multiple required class="block w-full rounded-xl border border-slate-300 bg-white px-3 py-3 text-sm"></div>
<div class="mt-5 rounded-xl border border-brand-200 bg-brand-50 p-4 text-sm text-slate-700"><strong>Multi-bank contra detection:</strong> when the client has more than one bank account, upload all relevant statements in the same analysis job. There is no fixed statement-count limit unless your administrator configures one. The analyzer pairs only conservative equal-and-opposite transfers across different account numbers and keeps every pair reviewable.</div><div class="mt-5"><label class="mb-1 block text-sm font-semibold text-slate-700">PDF bank statements — select all accounts/statements for this client</label><input type="file" name="statements" accept="application/pdf,.pdf" multiple required class="block w-full rounded-xl border border-slate-300 bg-white px-3 py-3 text-sm"></div>
<div class="mt-5 rounded-xl bg-slate-50 p-4 text-sm text-slate-600"><div class="font-semibold text-slate-800">Queue limits</div><div class="mt-1">Maximum three processing jobs across all users. Each user may have up to three queued or processing jobs. Completed workbooks and original uploaded statements remain available for 24 hours. Failed-job statements are also retained for 24 hours for debugging.</div></div>
<div class="mt-6 flex flex-wrap gap-3"><button class="rounded-xl bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-700">Submit Analysis</button></div>
</form>
@@ -39,4 +70,29 @@
{% if recent_jobs %}<section class="rounded-2xl border border-slate-200 bg-white p-6 shadow-soft"><div class="flex items-center justify-between"><h2 class="text-lg font-bold text-slate-900">Recent Analyses</h2><a href="/tools/bank-statement-analyzer/jobs" class="text-sm font-semibold text-brand-700">View all</a></div><div class="mt-4 overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="border-b text-left text-slate-500"><th class="py-2 pr-4">Submitted</th><th class="py-2 pr-4">Bank</th><th class="py-2 pr-4">Files</th><th class="py-2 pr-4">Status</th><th class="py-2">Action</th></tr></thead><tbody>{% for item in recent_jobs %}<tr class="border-b border-slate-100"><td class="py-3 pr-4">{{ item.submitted_at }}</td><td class="py-3 pr-4">{{ item.selected_bank|replace('_',' ')|title }}</td><td class="py-3 pr-4">{{ item.file_count }}</td><td class="py-3 pr-4">{{ item.status|title }}{% if item.queue_position %} · Position {{ item.queue_position }}{% endif %}</td><td class="py-3"><a class="font-semibold text-brand-700" href="/tools/bank-statement-analyzer?job={{ item.id }}#analysis-status">View</a></td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
</div>
{% if active_job and active_job.status in ['queued','processing'] %}<script>(function(){const box=document.getElementById('analysis-status');const id=box.dataset.jobId;async function poll(){try{const r=await fetch(`/tools/bank-statement-analyzer/jobs/${id}/status`,{headers:{'Accept':'application/json'}});if(!r.ok)return;const d=await r.json();document.getElementById('job-status-badge').textContent=d.status.charAt(0).toUpperCase()+d.status.slice(1);document.getElementById('queue-position').textContent=d.queue_position||'—';document.getElementById('estimated-wait').textContent=d.estimated_wait||'—';document.getElementById('progress-text').textContent=d.progress_percent+'%';document.getElementById('progress-bar').style.width=d.progress_percent+'%';if(d.status==='completed'||d.status==='failed'){window.location.reload();return;}}catch(e){}setTimeout(poll,5000)}setTimeout(poll,5000)})();</script>{% endif %}
<script>
(function () {
const client = document.getElementById("bank-analyzer-client");
const engagement = document.getElementById("bank-analyzer-engagement");
if (!client || !engagement) return;
const options = Array.from(engagement.querySelectorAll("option[data-client-id]"));
function filterEngagements() {
const selectedClient = client.value;
let selectedStillVisible = false;
options.forEach((option) => {
const visible = !!selectedClient && option.dataset.clientId === selectedClient;
option.hidden = !visible;
option.disabled = !visible;
if (visible && option.selected) selectedStillVisible = true;
});
if (!selectedStillVisible) engagement.value = "";
engagement.required = !!selectedClient;
}
client.addEventListener("change", filterEngagements);
filterEngagements();
})();
</script>
{% endblock %}