Skip to main content

Treasury Dashboard

A real-time financial dashboard with summary cards, cash flow table, exchange rates, and auto-refresh.

Treasury Dashboard

import random
from datetime import datetime, timedelta
from abstra.pages import register_function

def _rate(base, spread=0.02):
return round(base * (1 + random.uniform(-spread, spread)), 4)

def _cash_flow_item():
items = [
("Accounts Receivable", "inflow"), ("Loan Repayment", "outflow"),
("Supplier Payment", "outflow"), ("Investment Return", "inflow"),
("Payroll", "outflow"), ("Service Revenue", "inflow"),
("Tax Payment", "outflow"), ("Client Deposit", "inflow"),
]
name, direction = random.choice(items)
return {
"description": name, "direction": direction,
"amount": round(random.uniform(10_000, 500_000), 2),
"due_date": (datetime.now() + timedelta(days=random.randint(0, 30))).strftime("%Y-%m-%d"),
}

@register_function
def get_dashboard_data():
inflows = round(random.uniform(200_000, 900_000), 2)
outflows = round(random.uniform(100_000, 600_000), 2)
return {
"balance": round(random.uniform(2_000_000, 8_000_000), 2),
"inflows": inflows, "outflows": outflows,
"net": round(inflows - outflows, 2),
"pending_approvals": random.randint(2, 12),
"rates": {"USD/BRL": _rate(5.12), "EUR/BRL": _rate(5.54), "GBP/BRL": _rate(6.41)},
"cash_flow": [_cash_flow_item() for _ in range(6)],
"updated_at": datetime.now().strftime("%H:%M:%S"),
}

@register_function
def approve_payment(index: int):
return {"status": "approved", "index": index}

@register_function
def __render__():
return """
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin:0; font-family:'Inter',system-ui,sans-serif; background:#f8fafc; }
.fade { animation: fadeIn .3s ease-in } @keyframes fadeIn { from{opacity:.5} to{opacity:1} }
</style>
<div class="min-h-screen p-8 max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold text-slate-800">Treasury Dashboard</h1>
<p class="text-sm text-slate-400 mt-1">Updated at <span id="time" class="font-medium text-slate-500">--:--:--</span></p>
</div>
<button onclick="refreshData()" id="refresh-btn"
class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-5 py-2.5 rounded-lg transition-all active:scale-95 flex items-center gap-2 shadow-sm shadow-indigo-200">
<svg id="refresh-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Refresh
</button>
</div>

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center">
<svg class="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Balance</p>
</div>
<p id="balance" class="text-2xl font-bold text-slate-800">--</p>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"/></svg>
</div>
<p class="text-xs font-semibold text-emerald-500 uppercase tracking-wider">Inflows</p>
</div>
<p id="inflows" class="text-2xl font-bold text-emerald-600">--</p>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 rounded-lg bg-rose-50 flex items-center justify-center">
<svg class="w-4 h-4 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13l-5 5m0 0l-5-5m5 5V6"/></svg>
</div>
<p class="text-xs font-semibold text-rose-400 uppercase tracking-wider">Outflows</p>
</div>
<p id="outflows" class="text-2xl font-bold text-rose-600">--</p>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center">
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
</div>
<p class="text-xs font-semibold text-amber-500 uppercase tracking-wider">Pending</p>
</div>
<p id="pending" class="text-2xl font-bold text-amber-600">--</p>
</div>
</div>

<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
<h2 class="font-semibold text-slate-800">Upcoming Cash Flow</h2>
<span id="net-badge" class="text-xs font-bold px-3 py-1 rounded-full">--</span>
</div>
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs font-semibold text-slate-400 uppercase tracking-wider border-b border-slate-50">
<th class="px-6 py-3">Description</th><th class="px-6 py-3">Due</th>
<th class="px-6 py-3 text-right">Amount</th><th class="px-6 py-3 text-center">Action</th>
</tr>
</thead>
<tbody id="cash-flow"></tbody>
</table>
</div>

<div class="bg-white rounded-2xl shadow-sm border border-slate-100">
<div class="px-6 py-4 border-b border-slate-100">
<h2 class="font-semibold text-slate-800">Exchange Rates</h2>
</div>
<div id="rates" class="divide-y divide-slate-50"></div>
</div>
</div>
</div>

<script>
const fmt = n => new Intl.NumberFormat('pt-BR',{style:'currency',currency:'BRL'}).format(n);

async function refreshData() {
document.getElementById('refresh-icon').classList.add('animate-spin');
try {
const d = await get_dashboard_data();
document.getElementById('time').textContent = d.updated_at;
document.getElementById('balance').textContent = fmt(d.balance);
document.getElementById('inflows').textContent = fmt(d.inflows);
document.getElementById('outflows').textContent = fmt(d.outflows);
document.getElementById('pending').textContent = d.pending_approvals;

const badge = document.getElementById('net-badge');
badge.textContent = (d.net>=0?'+':'') + fmt(d.net);
badge.className = d.net>=0
? 'text-xs font-bold px-3 py-1 rounded-full bg-emerald-50 text-emerald-700'
: 'text-xs font-bold px-3 py-1 rounded-full bg-rose-50 text-rose-700';

document.getElementById('cash-flow').innerHTML = d.cash_flow.map((item,i) => `
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors fade">
<td class="px-6 py-3.5 font-medium text-slate-700">${item.description}</td>
<td class="px-6 py-3.5 text-slate-400">${item.due_date}</td>
<td class="px-6 py-3.5 text-right font-mono tabular-nums ${item.direction==='inflow'?'text-emerald-600':'text-rose-600'}">
${item.direction==='inflow'?'+':'-'} ${fmt(item.amount)}
</td>
<td class="px-6 py-3.5 text-center">
${item.direction==='outflow'
? `<button onclick="handleApprove(${i})" class="text-xs bg-indigo-50 text-indigo-700 hover:bg-indigo-100 font-medium px-3 py-1.5 rounded-lg transition-colors">Approve</button>`
: '<span class="text-slate-300">—</span>'}
</td>
</tr>`).join('');

document.getElementById('rates').innerHTML = Object.entries(d.rates).map(([pair,rate]) => `
<div class="px-6 py-4 flex items-center justify-between fade">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-xs font-bold text-slate-500">
${pair.split('/')[0]}
</div>
<span class="text-sm font-medium text-slate-700">${pair}</span>
</div>
<span class="text-sm font-mono font-semibold text-slate-800 tabular-nums">${Number(rate).toFixed(4)}</span>
</div>`).join('');
} finally { document.getElementById('refresh-icon').classList.remove('animate-spin'); }
}

async function handleApprove(i) { await approve_payment(i); refreshData(); }

refreshData();
setInterval(refreshData, 10000);
</script>
"""

Key patterns: setInterval for auto-refresh, Tailwind CSS, summary cards with icons, cash flow table with actions, exchange rate list.