Skip to main content

Expense Submission Form

A custom HTML form with client-side validation, loading states, and backend processing with database insert and task routing.

from abstra.pages import register_function, get_user
from abstra.tables import insert
from abstra.tasks import send_task

@register_function
def submit_expense(description: str, amount: float, category: str):
user = get_user()
record = insert("expenses", {
"description": description,
"amount_cents": int(amount * 100),
"category": category,
"submitted_by": user.email,
"status": "pending",
})
send_task("expense_submitted", {"expense_id": str(record["id"]), "amount": amount, "submitted_by": user.email})
return {"id": str(record["id"])}

@register_function
def __render__():
user = get_user()
return f"""
<script src="https://cdn.tailwindcss.com"></script>
<style>body {{ margin:0; font-family:'Inter',system-ui,sans-serif; background:#f8fafc; }}</style>
<div class="min-h-screen flex items-start justify-center pt-16 px-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div class="w-14 h-14 rounded-2xl bg-indigo-100 flex items-center justify-center mx-auto mb-4">
<svg class="w-7 h-7 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"/></svg>
</div>
<h1 class="text-xl font-bold text-slate-800">Submit Expense</h1>
<p class="text-sm text-slate-400 mt-1">{user.email}</p>
</div>

<form id="form" class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 space-y-5" onsubmit="handleSubmit(event)">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">Description</label>
<input id="desc" type="text" required minlength="3" placeholder="Lunch with client"
class="w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-shadow">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">Amount (R$)</label>
<input id="amount" type="number" required min="0.01" step="0.01" placeholder="150.00"
class="w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-shadow font-mono">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">Category</label>
<select id="cat" required
class="w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-shadow bg-white">
<option value="">Select category...</option>
<option value="travel">Travel</option>
<option value="meals">Meals & Entertainment</option>
<option value="office">Office Supplies</option>
<option value="software">Software & Subscriptions</option>
<option value="other">Other</option>
</select>
</div>
<button type="submit" id="btn"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2.5 rounded-xl font-medium text-sm transition-all active:scale-[0.98] shadow-sm shadow-indigo-200">
Submit Expense
</button>
</form>

<div id="result" class="mt-4 hidden">
<div class="bg-emerald-50 border border-emerald-200 text-emerald-700 p-4 rounded-xl text-sm flex items-center gap-3">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span id="result-text"></span>
</div>
</div>
</div>
</div>
<script>
async function handleSubmit(e) {{
e.preventDefault();
const btn = document.getElementById('btn');
btn.disabled = true; btn.textContent = 'Submitting...'; btn.classList.add('opacity-75');
try {{
const r = await submit_expense(
document.getElementById('desc').value,
parseFloat(document.getElementById('amount').value),
document.getElementById('cat').value
);
document.getElementById('result').classList.remove('hidden');
document.getElementById('result-text').textContent = 'Expense #' + r.id + ' submitted for approval.';
document.getElementById('form').reset();
}} catch(err) {{ alert(err.message); }}
btn.disabled = false; btn.textContent = 'Submit Expense'; btn.classList.remove('opacity-75');
}}
</script>
"""

Key patterns: Centered card layout, native form validation, loading state, success banner, insert() + send_task() on submit.