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:
parent
f5d294abbf
commit
64ec515393
1 changed files with 120 additions and 0 deletions
120
src/routes/dashboard/wallet/invoices.tsx
Normal file
120
src/routes/dashboard/wallet/invoices.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue