- Replace all /api/gateway/* with /api/* to match gateway routing - Fix AdminShell.tsx: update UGC route to singular and fix logout URL - Remove Applications and Responses from sidebar (unused) - Move conflicting route files into folders (company, approval, verification, users, jobs, kb, leads, photographer) as index.tsx to avoid catch-all interference - Upgrade ProfessionAdminListPage to match Department Management UI: • Dark headers with white text • Icons on Sort/Filters/Export buttons • Pagination UI • Improved empty state with Create button • Hover effects and consistent spacing - Update all pages using ProfessionAdminListPage to benefit from new UI - Fix jobs admin endpoint to use /api/admin/companies/jobs with auth - Add authentication headers to jobs and leads fetch calls These changes unify the API architecture and bring a consistent, professional look to all management tables.
118 lines
5.3 KiB
TypeScript
118 lines
5.3 KiB
TypeScript
import { createResource, createSignal, createMemo, Show } from 'solid-js';
|
|
|
|
const API = '';
|
|
|
|
async function loadInvoices(): Promise<any[]> {
|
|
try {
|
|
const res = await fetch(`${API}/api/admin/invoices`);
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
return Array.isArray(data) ? data : (data.invoices || []);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export default function InvoicePage() {
|
|
const [invoices] = createResource(loadInvoices);
|
|
const [search, setSearch] = createSignal('');
|
|
|
|
const filteredInvoices = createMemo(() => {
|
|
const q = search().toLowerCase();
|
|
const all = invoices() ?? [];
|
|
if (!q) return all;
|
|
return all.filter((inv) =>
|
|
(inv.invoice_number || inv.id || '').toLowerCase().includes(q) ||
|
|
(inv.user_id || '').toLowerCase().includes(q) ||
|
|
(inv.package_name || '').toLowerCase().includes(q) ||
|
|
(inv.status || '').toLowerCase().includes(q)
|
|
);
|
|
});
|
|
|
|
return (
|
|
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
|
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
|
<h1 class="text-xl font-semibold text-gray-900">Invoice Management</h1>
|
|
<p class="text-sm text-gray-500 mt-0.5">View and download all platform invoices.</p>
|
|
</div>
|
|
|
|
<div class="flex-1 p-6">
|
|
<div style="margin-bottom:16px">
|
|
<input
|
|
type="text"
|
|
placeholder="Search invoices by number, user, package, or status..."
|
|
value={search()}
|
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
class="rounded-lg border border-gray-200 px-3 py-2 text-sm"
|
|
style="min-width:320px"
|
|
/>
|
|
</div>
|
|
|
|
<div class="table-card">
|
|
<div class="overflow-x-auto">
|
|
<table class="data-table w-full text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Invoice #</th>
|
|
<th>User</th>
|
|
<th>Package</th>
|
|
<th>Total (₹)</th>
|
|
<th>Tax (₹)</th>
|
|
<th>Status</th>
|
|
<th>Date</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<Show when={invoices.loading}>
|
|
<tr><td colspan="8" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
|
</Show>
|
|
<Show when={!invoices.loading && invoices.error}>
|
|
<tr><td colspan="8" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
|
</Show>
|
|
<Show when={!invoices.loading && !invoices.error && filteredInvoices().length === 0}>
|
|
<tr><td colspan="8" style="text-align:center;padding:32px;color:#94a3b8">No records found.</td></tr>
|
|
</Show>
|
|
<Show when={!invoices.loading && !invoices.error && filteredInvoices().length > 0}>
|
|
{filteredInvoices().map((item) => (
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="font-semibold text-slate-900" style="font-family:monospace">{item.invoice_number || item.id}</td>
|
|
<td class="text-slate-500">{item.user_id || '—'}</td>
|
|
<td class="text-slate-500">{item.package_name || '—'}</td>
|
|
<td class="text-slate-500">₹{item.total != null ? (item.total / 100).toFixed(2) : '—'}</td>
|
|
<td class="text-slate-500">₹{item.tax != null ? (item.tax / 100).toFixed(2) : '—'}</td>
|
|
<td>
|
|
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${item.status === 'PAID' || item.status === 'ISSUED' ? 'active' : ''}`}>
|
|
{item.status || '—'}
|
|
</span>
|
|
</td>
|
|
<td class="text-slate-500">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
|
|
<td>
|
|
<div class="flex items-center justify-end gap-1">
|
|
<Show when={item.download_url || item.pdf_url}>
|
|
<a
|
|
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
|
href={item.download_url || item.pdf_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
download
|
|
>
|
|
Download
|
|
</a>
|
|
</Show>
|
|
<Show when={!item.download_url && !item.pdf_url}>
|
|
<span style="color:#94a3b8;font-size:12px">—</span>
|
|
</Show>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</Show>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|