Add wallet invoices page

- wallet/invoices.tsx: table of invoices with download link; uses role-specific API prefix; handles loading/empty states

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-04-02 18:14:10 +02:00
parent f5d294abbf
commit 64ec515393

View file

@ -0,0 +1,120 @@
import { createResource, createSignal, Show, For } from 'solid-js';
import { A } from '@solidjs/router';
import { getAuthHeader, authState, getRoleApiPath } from '~/lib/auth';
const API = import.meta.env.VITE_API_URL ?? 'http://localhost:8000';
const STATUS_BADGE: Record<string, string> = {
ISSUED: 'badge--blue',
PAID: 'badge--green',
};
export default function WalletInvoices() {
const [page, setPage] = createSignal(1);
const rc = () => authState().runtime_config;
const rolePrefix = () => getRoleApiPath(rc()?.role);
const [invoices] = createResource(() => page(), async (p) => {
const auth = getAuthHeader();
if (!auth.Authorization || !rc()?.role) return { data: [], pagination: null };
const res = await fetch(`${API}${rolePrefix()}/invoices?page=${p}&limit=20`, { headers: auth });
if (!res.ok) return { data: [], pagination: null };
return res.json();
});
return (
<div style={{ 'max-width': '900px' }}>
<div style={{ 'margin-bottom': '24px', display: 'flex', 'align-items': 'center', 'justify-content': 'space-between' }}>
<div>
<h1 style={{ margin: 0, 'font-size': '22px', 'font-weight': '800' }}>Invoices</h1>
<p style={{ margin: '6px 0 0', color: '#64748b', 'font-size': '14px' }}>
Download receipts for all your purchases.
</p>
</div>
<A href="/dashboard/wallet" class="btn" style={{ 'text-decoration': 'none' }}> Wallet</A>
</div>
<div class="table-card">
<Show when={invoices.loading}>
<div class="loading-spinner">Loading invoices</div>
</Show>
<Show when={!invoices.loading && (invoices()?.data?.length ?? 0) === 0}>
<div style={{ padding: '48px', 'text-align': 'center', color: '#64748b' }}>
<div style={{ 'font-size': '36px', 'margin-bottom': '12px' }}>🧾</div>
<p>No invoices yet. Invoices are generated after a successful payment.</p>
</div>
</Show>
<Show when={(invoices()?.data?.length ?? 0) > 0}>
<table class="data-table">
<thead>
<tr>
<th>Invoice #</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
<th class="text-right">Download</th>
</tr>
</thead>
<tbody>
<For each={invoices()?.data}>
{(inv: any) => (
<tr>
<td style={{ 'font-family': 'monospace', 'font-size': '13px', 'font-weight': '600' }}>
{inv.invoice_number ?? '—'}
</td>
<td style={{ 'font-size': '13px' }}>
{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString('en-IN', { dateStyle: 'medium' }) : '—'}
</td>
<td style={{ 'font-weight': '600' }}>
{((inv.total ?? 0) / 100).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</td>
<td>
<span class={`badge ${STATUS_BADGE[inv.status] ?? 'badge--gray'}`}>
{inv.status ?? '—'}
</span>
</td>
<td style={{ 'text-align': 'right' }}>
<Show when={inv.file_url}>
<a
href={inv.file_url}
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm"
style={{ 'text-decoration': 'none' }}
>
Download PDF
</a>
</Show>
<Show when={!inv.file_url}>
<span style={{ 'font-size': '12px', color: '#94a3b8' }}>Generating</span>
</Show>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
</div>
<Show when={invoices()?.pagination?.total_pages > 1}>
<div style={{ display: 'flex', gap: '8px', 'margin-top': '20px', 'justify-content': 'center' }}>
<button class="btn btn-sm" disabled={page() === 1} onClick={() => setPage(p => p - 1)}> Prev</button>
<span style={{ padding: '6px 12px', 'font-size': '13px', color: '#64748b' }}>
Page {page()} of {invoices()?.pagination?.total_pages}
</span>
<button
class="btn btn-sm"
disabled={page() >= invoices()?.pagination?.total_pages}
onClick={() => setPage(p => p + 1)}
>
Next
</button>
</div>
</Show>
</div>
);
}