195 lines
9.2 KiB
TypeScript
195 lines
9.2 KiB
TypeScript
import { createMemo, createSignal, For, Show } from 'solid-js';
|
|
|
|
const API = '';
|
|
|
|
type UserReport = {
|
|
total_users?: number;
|
|
new_users?: number;
|
|
active_users?: number;
|
|
};
|
|
|
|
type RevenueReport = {
|
|
total_revenue?: number;
|
|
total_orders?: number;
|
|
total_tracecoins_sold?: number;
|
|
};
|
|
|
|
function getToken(): string {
|
|
return typeof sessionStorage !== 'undefined'
|
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
: '';
|
|
}
|
|
|
|
function authHeaders(): Record<string, string> {
|
|
const token = getToken();
|
|
return {
|
|
Accept: 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
};
|
|
}
|
|
|
|
export default function ReportPage() {
|
|
const [from, setFrom] = createSignal('');
|
|
const [to, setTo] = createSignal('');
|
|
const [tab, setTab] = createSignal<'overview' | 'users' | 'revenue'>('overview');
|
|
const [loading, setLoading] = createSignal(false);
|
|
const [error, setError] = createSignal('');
|
|
const [userReport, setUserReport] = createSignal<UserReport | null>(null);
|
|
const [revenueReport, setRevenueReport] = createSignal<RevenueReport | null>(null);
|
|
|
|
const rows = createMemo(() => {
|
|
if (tab() === 'users') {
|
|
return [
|
|
{ metric: 'Total Users', value: userReport()?.total_users ?? '—' },
|
|
{ metric: 'New Users', value: userReport()?.new_users ?? '—' },
|
|
{ metric: 'Active Users', value: userReport()?.active_users ?? '—' },
|
|
];
|
|
}
|
|
if (tab() === 'revenue') {
|
|
return [
|
|
{ metric: 'Total Revenue', value: revenueReport()?.total_revenue != null ? `₹${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : '—' },
|
|
{ metric: 'Total Orders', value: revenueReport()?.total_orders ?? '—' },
|
|
{ metric: 'TraceCoins Sold', value: revenueReport()?.total_tracecoins_sold ?? '—' },
|
|
];
|
|
}
|
|
return [
|
|
{ metric: 'Total Users', value: userReport()?.total_users ?? '—' },
|
|
{ metric: 'New Users', value: userReport()?.new_users ?? '—' },
|
|
{ metric: 'Active Users', value: userReport()?.active_users ?? '—' },
|
|
{ metric: 'Total Revenue', value: revenueReport()?.total_revenue != null ? `₹${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : '—' },
|
|
{ metric: 'Total Orders', value: revenueReport()?.total_orders ?? '—' },
|
|
{ metric: 'TraceCoins Sold', value: revenueReport()?.total_tracecoins_sold ?? '—' },
|
|
];
|
|
});
|
|
|
|
const exportCsv = () => {
|
|
const csv = ['Metric,Value', ...rows().map((r) => `"${r.metric.replace(/"/g, '""')}","${String(r.value).replace(/"/g, '""')}"`)].join('\n');
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'report-metrics.csv';
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const handleLoad = async (e: Event) => {
|
|
e.preventDefault();
|
|
if (!from() || !to()) return;
|
|
try {
|
|
setLoading(true);
|
|
setError('');
|
|
const [usersRes, revenueRes] = await Promise.all([
|
|
fetch(`${API}/api/admin/reports/users?from=${from()}&to=${to()}`, { headers: authHeaders(), credentials: 'include' }),
|
|
fetch(`${API}/api/admin/reports/revenue?from=${from()}&to=${to()}`, { headers: authHeaders(), credentials: 'include' }),
|
|
]);
|
|
if (!usersRes.ok || !revenueRes.ok) throw new Error('Failed to load report data');
|
|
const [usersData, revenueData] = await Promise.all([usersRes.json(), revenueRes.json()]);
|
|
setUserReport(usersData);
|
|
setRevenueReport(revenueData);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to load reports');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const statCards = createMemo(() => ([
|
|
{ label: 'Total Users', value: userReport()?.total_users ?? '—' },
|
|
{ label: 'New Users', value: userReport()?.new_users ?? '—' },
|
|
{ label: 'Revenue', value: revenueReport()?.total_revenue != null ? `₹${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : '—' },
|
|
{ label: 'Orders', value: revenueReport()?.total_orders ?? '—' },
|
|
]));
|
|
|
|
return (
|
|
<div class="w-full space-y-6 pb-8">
|
|
<div style="margin-bottom:1.5rem">
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Report Management</h1>
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">View platform analytics and export reports.</p>
|
|
</div>
|
|
|
|
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
|
|
<button
|
|
type="button"
|
|
class={tab() === 'overview' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
|
onClick={() => setTab('overview')}
|
|
>
|
|
Overview
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={tab() === 'users' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
|
onClick={() => setTab('users')}
|
|
>
|
|
Users
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class={tab() === 'revenue' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
|
onClick={() => setTab('revenue')}
|
|
>
|
|
Revenue
|
|
</button>
|
|
</div>
|
|
|
|
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
|
|
<form onSubmit={handleLoad} style="display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap">
|
|
<div>
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">From</label>
|
|
<input type="date" value={from()} onInput={(e) => setFrom(e.currentTarget.value)} required style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" />
|
|
</div>
|
|
<div>
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">To</label>
|
|
<input type="date" value={to()} onInput={(e) => setTo(e.currentTarget.value)} required style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" />
|
|
</div>
|
|
<button class="btn-primary" type="submit" disabled={loading()}>{loading() ? 'Loading...' : 'Load Report'}</button>
|
|
</form>
|
|
<Show when={error()}>
|
|
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:12px">{error()}</div>
|
|
</Show>
|
|
</section>
|
|
|
|
<Show when={userReport() || revenueReport()}>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
|
<For each={statCards()}>
|
|
{(card) => (
|
|
<div style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 20px;text-align:center;min-width:140px">
|
|
<div style="font-size:22px;font-weight:700;color:#111827">{card.value}</div>
|
|
<div style="font-size:12px;color:#6b7280">{card.label}</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
|
|
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">Export</button>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr style="background:#0D0D2A;text-align:left">
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Metric</th>
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={rows()}>
|
|
{(row) => (
|
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{row.metric}</td>
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.value}</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
|
<p style="font-size:13px;color:#6B7280">Showing <strong style="font-weight:600;color:#111827">{rows().length}</strong> metrics</p>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
);
|
|
}
|