From 411827f8377d93322df71eb0e6ee2f0aa46cb8ca Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 8 Apr 2026 02:11:06 +0200 Subject: [PATCH] Fix auth headers on pricing/credit/invoice/coupon pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All 4 pages now send Bearer token from sessionStorage on every fetch - Pricing: fixed endpoint from /api/admin/packages → /api/admin/tracecoin-packages; added search, role filter, status filter, and sort (name/price/coins) - Coupon: added search by code/title and status filter; fixed refetch to use load() - Invoice: refactored from createResource to onMount+signals for consistent auth - Credit: authenticated balance, ledger, adjust, and reconcile fetch calls Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin/coupon.tsx | 106 +++++-- src/routes/admin/credit.tsx | 27 +- src/routes/admin/invoice.tsx | 56 ++-- src/routes/admin/pricing.tsx | 567 ++++++++++++++++++----------------- 4 files changed, 433 insertions(+), 323 deletions(-) diff --git a/src/routes/admin/coupon.tsx b/src/routes/admin/coupon.tsx index cc7fc14..679ceec 100644 --- a/src/routes/admin/coupon.tsx +++ b/src/routes/admin/coupon.tsx @@ -1,7 +1,22 @@ -import { createResource, createSignal, Show, For } from 'solid-js'; +import { createSignal, createMemo, onMount, Show, For } from 'solid-js'; const API = ''; +function getToken(): string { + return typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; +} + +function authHeaders(): Record { + const token = getToken(); + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + const ROLE_OPTIONS = [ 'company', 'customer', @@ -30,16 +45,6 @@ type Coupon = { role_keys: string[]; }; -async function loadCoupons(): Promise { - try { - const res = await fetch(`${API}/api/admin/coupons`); - if (!res.ok) throw new Error('Failed to load'); - const data = await res.json(); - return Array.isArray(data) ? data : (data.coupons || []); - } catch { - return []; - } -} const defaultForm = () => ({ id: '', @@ -53,13 +58,45 @@ const defaultForm = () => ({ }); export default function CouponPage() { - const [coupons, { refetch }] = createResource(loadCoupons); + const [coupons, setCoupons] = createSignal([]); + const [loading, setLoading] = createSignal(true); + const [loadError, setLoadError] = createSignal(''); const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list'); const [form, setForm] = createSignal(defaultForm()); const [saving, setSaving] = createSignal(false); const [toggling, setToggling] = createSignal(''); const [formError, setFormError] = createSignal(''); + // Filters + const [search, setSearch] = createSignal(''); + const [statusFilter, setStatusFilter] = createSignal('all'); + + const load = async () => { + setLoading(true); setLoadError(''); + try { + const res = await fetch(`${API}/api/admin/coupons`, { headers: authHeaders(), credentials: 'include' }); + if (!res.ok) throw new Error(`Request failed (${res.status})`); + const data = await res.json(); + setCoupons(Array.isArray(data) ? data : (data.coupons ?? [])); + } catch (err: any) { + setLoadError(err.message || 'Could not load coupons.'); + setCoupons([]); + } finally { + setLoading(false); + } + }; + + onMount(() => void load()); + + const filteredCoupons = createMemo(() => { + let r = coupons(); + const q = search().toLowerCase(); + if (q) r = r.filter((c) => c.code.toLowerCase().includes(q) || (c.title || '').toLowerCase().includes(q)); + if (statusFilter() === 'active') r = r.filter((c) => c.is_active); + if (statusFilter() === 'inactive') r = r.filter((c) => !c.is_active); + return r; + }); + const resetForm = () => { setForm(defaultForm()); setFormError(''); @@ -108,12 +145,13 @@ export default function CouponPage() { const method = f.id ? 'PATCH' : 'POST'; const res = await fetch(url, { method, - headers: { 'Content-Type': 'application/json' }, + headers: authHeaders(), + credentials: 'include', body: JSON.stringify(body), }); if (!res.ok) throw new Error('Failed to save coupon'); resetForm(); - refetch(); + await load(); setActiveTab('list'); } catch (err: unknown) { setFormError(err instanceof Error ? err.message : 'Failed to save'); @@ -127,11 +165,12 @@ export default function CouponPage() { setToggling(coupon.id); const res = await fetch(`${API}/api/admin/coupons/${coupon.id}`, { method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + headers: authHeaders(), + credentials: 'include', body: JSON.stringify({ is_active: !coupon.is_active }), }); if (!res.ok) throw new Error('Failed to toggle'); - refetch(); + await load(); } catch { // ignore } finally { @@ -166,6 +205,25 @@ export default function CouponPage() {
+
+ setSearch(e.currentTarget.value)} + class="rounded-lg border border-gray-200 px-3 py-2 text-sm" + style="min-width:200px;flex:1" + /> + + +
+ +
{loadError()}
+
@@ -181,17 +239,14 @@ export default function CouponPage() { - + - - - - + - 0}> - + 0}> + {(item) => ( @@ -223,6 +278,11 @@ export default function CouponPage() {
Loading...
Failed to load. Is the backend running?
No coupons found.
{item.code}
+ +
+ {filteredCoupons().length} of {coupons().length} coupons +
+
diff --git a/src/routes/admin/credit.tsx b/src/routes/admin/credit.tsx index af8199e..9b130f3 100644 --- a/src/routes/admin/credit.tsx +++ b/src/routes/admin/credit.tsx @@ -1,7 +1,22 @@ -import { createSignal, createResource, Show, For } from 'solid-js'; +import { createSignal, Show, For } from 'solid-js'; const API = ''; +function getToken(): string { + return typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; +} + +function authHeaders(): Record { + const token = getToken(); + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + type LedgerEntry = { id: string; transactionType: 'ADD' | 'DEDUCT'; @@ -57,8 +72,8 @@ export default function CreditPage() { setSearchedUserId(uid); try { const [balRes, ledRes] = await Promise.all([ - fetch(`${API}/api/admin/credits/balance?userId=${encodeURIComponent(uid)}`), - fetch(`${API}/api/admin/credits/ledger?userId=${encodeURIComponent(uid)}`), + fetch(`${API}/api/admin/credits/balance?userId=${encodeURIComponent(uid)}`, { headers: authHeaders(), credentials: 'include' }), + fetch(`${API}/api/admin/credits/ledger?userId=${encodeURIComponent(uid)}`, { headers: authHeaders(), credentials: 'include' }), ]); if (!balRes.ok || !ledRes.ok) throw new Error('Failed to fetch'); const balData = await balRes.json(); @@ -82,7 +97,8 @@ export default function CreditPage() { try { const res = await fetch(`${API}/api/admin/credits/adjust`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: authHeaders(), + credentials: 'include', body: JSON.stringify({ user_id: adjUserId(), amount: adjAmount(), @@ -115,7 +131,8 @@ export default function CreditPage() { setReconResults(null); try { const res = await fetch( - `${API}/api/admin/credits/reconcile?from=${encodeURIComponent(reconFrom())}&to=${encodeURIComponent(reconTo())}` + `${API}/api/admin/credits/reconcile?from=${encodeURIComponent(reconFrom())}&to=${encodeURIComponent(reconTo())}`, + { headers: authHeaders(), credentials: 'include' } ); if (!res.ok) throw new Error('Failed to reconcile'); const data = await res.json(); diff --git a/src/routes/admin/invoice.tsx b/src/routes/admin/invoice.tsx index 01dc1e0..8e2e4d9 100644 --- a/src/routes/admin/invoice.tsx +++ b/src/routes/admin/invoice.tsx @@ -1,25 +1,47 @@ -import { createResource, createSignal, createMemo, Show } from 'solid-js'; +import { createSignal, createMemo, onMount, Show } from 'solid-js'; const API = ''; -async function loadInvoices(): Promise { - 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 []; - } +function getToken(): string { + return typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; +} + +function authHeaders(): Record { + const token = getToken(); + return { + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; } export default function InvoicePage() { - const [invoices] = createResource(loadInvoices); + const [invoices, setInvoices] = createSignal([]); + const [loading, setLoading] = createSignal(true); + const [loadError, setLoadError] = createSignal(''); const [search, setSearch] = createSignal(''); + const load = async () => { + setLoading(true); setLoadError(''); + try { + const res = await fetch(`${API}/api/admin/invoices`, { headers: authHeaders(), credentials: 'include' }); + if (!res.ok) throw new Error(`Request failed (${res.status})`); + const data = await res.json(); + setInvoices(Array.isArray(data) ? data : (data.invoices ?? [])); + } catch (err: any) { + setLoadError(err.message || 'Could not load invoices.'); + setInvoices([]); + } finally { + setLoading(false); + } + }; + + onMount(() => void load()); + const filteredInvoices = createMemo(() => { const q = search().toLowerCase(); - const all = invoices() ?? []; + const all = invoices(); if (!q) return all; return all.filter((inv) => (inv.invoice_number || inv.id || '').toLowerCase().includes(q) || @@ -64,16 +86,16 @@ export default function InvoicePage() { - + Loading... - - Failed to load. Is the backend running? + + {loadError()} - + No records found. - 0}> + 0}> {filteredInvoices().map((item) => ( {item.invoice_number || item.id} diff --git a/src/routes/admin/pricing.tsx b/src/routes/admin/pricing.tsx index 16e9c8c..9e6cbb9 100644 --- a/src/routes/admin/pricing.tsx +++ b/src/routes/admin/pricing.tsx @@ -1,7 +1,22 @@ -import { createResource, createSignal, Show, For } from 'solid-js'; +import { createSignal, createMemo, onMount, Show, For } from 'solid-js'; const API = ''; +function getToken(): string { + return typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; +} + +function authHeaders(): Record { + const token = getToken(); + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + type Package = { id: string; name: string; @@ -13,47 +28,42 @@ type Package = { }; const ROLES = [ - 'company', - 'customer', - 'job_seeker', - 'photographer', - 'video_editor', - 'graphic_designer', - 'social_media_manager', - 'fitness_trainer', - 'catering_services', - 'makeup_artist', - 'tutor', - 'developer', + 'company', 'customer', 'job_seeker', 'photographer', 'video_editor', + 'graphic_designer', 'social_media_manager', 'fitness_trainer', + 'catering_services', 'makeup_artist', 'tutor', 'developer', 'ugc_content_creator', ]; -async function loadPackages(): Promise { - try { - const res = await fetch(`${API}/api/admin/packages`); - if (!res.ok) throw new Error('Failed to load'); - const data = await res.json(); - return Array.isArray(data) ? data : (data.packages || []); - } catch { - return []; - } -} +type SortMode = 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'coins_asc' | 'coins_desc'; + +const SORT_LABELS: Record = { + name_asc: 'Name A→Z', name_desc: 'Name Z→A', + price_asc: 'Price ↑', price_desc: 'Price ↓', + coins_asc: 'TraceCoins ↑', coins_desc: 'TraceCoins ↓', +}; export default function PricingPage() { - const [packages, { refetch }] = createResource(loadPackages); + const [rows, setRows] = createSignal([]); + const [loading, setLoading] = createSignal(true); + const [loadError, setLoadError] = createSignal(''); const [view, setView] = createSignal<'packages' | 'create'>('packages'); - // Inline edit state + // Filters + const [search, setSearch] = createSignal(''); + const [roleFilter, setRoleFilter] = createSignal('all'); + const [statusFilter, setStatusFilter] = createSignal('all'); + const [sortBy, setSortBy] = createSignal('name_asc'); + const [sortOpen, setSortOpen] = createSignal(false); + + // Inline edit const [editingId, setEditingId] = createSignal(''); const [editName, setEditName] = createSignal(''); const [editTracecoins, setEditTracecoins] = createSignal(''); const [editPrice, setEditPrice] = createSignal(''); const [editSaving, setEditSaving] = createSignal(false); const [editError, setEditError] = createSignal(''); - - // Toggle active state const [togglingId, setTogglingId] = createSignal(''); - // Create form state + // Create form const [cName, setCName] = createSignal(''); const [cRole, setCRole] = createSignal(ROLES[0]); const [cTracecoins, setCTracecoins] = createSignal(''); @@ -62,299 +72,300 @@ export default function PricingPage() { const [cSaving, setCsaving] = createSignal(false); const [cError, setCError] = createSignal(''); - const startEdit = (pkg: Package) => { - setEditingId(pkg.id); - setEditName(pkg.name); - setEditTracecoins(String(pkg.tracecoin_amount)); - setEditPrice(String(pkg.price_inr)); - setEditError(''); + const load = async () => { + setLoading(true); + setLoadError(''); + try { + const res = await fetch(`${API}/api/admin/tracecoin-packages`, { + headers: authHeaders(), + credentials: 'include', + }); + if (!res.ok) throw new Error(`Request failed (${res.status})`); + const data = await res.json(); + setRows(Array.isArray(data) ? data : (data.packages ?? [])); + } catch (err: any) { + setLoadError(err.message || 'Could not load packages.'); + setRows([]); + } finally { + setLoading(false); + } }; - const cancelEdit = () => { - setEditingId(''); + onMount(() => void load()); + + const filteredRows = createMemo(() => { + let r = rows(); + const q = search().toLowerCase(); + if (q) r = r.filter((p) => p.name.toLowerCase().includes(q) || p.role.toLowerCase().includes(q)); + if (roleFilter() !== 'all') r = r.filter((p) => p.role === roleFilter()); + if (statusFilter() === 'active') r = r.filter((p) => p.is_active); + if (statusFilter() === 'inactive') r = r.filter((p) => !p.is_active); + const sorted = [...r]; + const mode = sortBy(); + sorted.sort((a, b) => { + if (mode === 'name_desc') return b.name.localeCompare(a.name); + if (mode === 'price_asc') return a.price_inr - b.price_inr; + if (mode === 'price_desc') return b.price_inr - a.price_inr; + if (mode === 'coins_asc') return a.tracecoin_amount - b.tracecoin_amount; + if (mode === 'coins_desc') return b.tracecoin_amount - a.tracecoin_amount; + return a.name.localeCompare(b.name); + }); + return sorted; + }); + + const startEdit = (pkg: Package) => { + setEditingId(pkg.id); setEditName(pkg.name); + setEditTracecoins(String(pkg.tracecoin_amount)); setEditPrice(String(pkg.price_inr)); setEditError(''); }; + const cancelEdit = () => { setEditingId(''); setEditError(''); }; const saveEdit = async (id: string) => { try { - setEditSaving(true); - setEditError(''); + setEditSaving(true); setEditError(''); const res = await fetch(`${API}/api/admin/tracecoin-packages/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: editName(), - tracecoin_amount: Number(editTracecoins()), - price_inr: Number(editPrice()), - }), + method: 'PATCH', headers: authHeaders(), credentials: 'include', + body: JSON.stringify({ name: editName(), tracecoin_amount: Number(editTracecoins()), price_inr: Number(editPrice()) }), }); if (!res.ok) throw new Error('Failed to save'); - setEditingId(''); - refetch(); - } catch (err: any) { - setEditError(err.message || 'Failed to save'); - } finally { - setEditSaving(false); - } + setEditingId(''); await load(); + } catch (err: any) { setEditError(err.message || 'Failed to save'); } + finally { setEditSaving(false); } }; const toggleActive = async (pkg: Package) => { try { setTogglingId(pkg.id); - const res = await fetch(`${API}/api/admin/tracecoin-packages/${pkg.id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + await fetch(`${API}/api/admin/tracecoin-packages/${pkg.id}`, { + method: 'PATCH', headers: authHeaders(), credentials: 'include', body: JSON.stringify({ is_active: !pkg.is_active }), }); - if (!res.ok) throw new Error('Failed to update'); - refetch(); - } catch { - // ignore - } finally { - setTogglingId(''); - } + await load(); + } catch { /* ignore */ } finally { setTogglingId(''); } }; const handleCreate = async (e: Event) => { e.preventDefault(); try { - setCsaving(true); - setCError(''); + setCsaving(true); setCError(''); const body: Record = { - name: cName(), - role: cRole(), - tracecoin_amount: Number(cTracecoins()), - price_inr: Number(cPrice()), + name: cName(), role: cRole(), + tracecoin_amount: Number(cTracecoins()), price_inr: Number(cPrice()), }; if (cBonus()) body.bonus_percentage = Number(cBonus()); const res = await fetch(`${API}/api/admin/tracecoin-packages`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: 'POST', headers: authHeaders(), credentials: 'include', body: JSON.stringify(body), }); if (!res.ok) throw new Error('Failed to create package'); - setCName(''); - setCRole(ROLES[0]); - setCTracecoins(''); - setCPrice(''); - setCBonus(''); - setView('packages'); - refetch(); - } catch (err: any) { - setCError(err.message || 'Failed to create'); - } finally { - setCsaving(false); - } + setCName(''); setCRole(ROLES[0]); setCTracecoins(''); setCPrice(''); setCBonus(''); + setView('packages'); await load(); + } catch (err: any) { setCError(err.message || 'Failed to create'); } + finally { setCsaving(false); } }; return ( -
-
-

Pricing Management

-

Create and manage TraceCoin packages

-
+
+
+

Pricing Management

+

Create and manage TraceCoin packages

+
- {/* Tabs */} -
+ {/* Tabs */} +
+ {(['packages', 'create'] as const).map((t) => ( - -
+ ))} +
-
- {/* Packages list tab */} - -
-
- - - - - - - - - - - - - - - - - - - - - - - 0}> - - {(pkg) => ( - <> - - - - - - - - +
NameRoleTraceCoinsPrice (₹)Bonus (%)StatusActions
Loading...
Failed to load. Is the backend running?
No packages found.
{pkg.name}{pkg.role}{pkg.tracecoin_amount}₹{(pkg.price_inr / 100).toFixed(2)}{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'} - - {pkg.is_active ? 'Active' : 'Inactive'} - - -
- - +
+ + {/* ── Packages list ── */} + +
+ setSearch(e.currentTarget.value)} + class="rounded-lg border border-gray-200 px-3 py-2 text-sm" + style="min-width:200px;flex:1" + /> + + +
+ + +
+ + {([key, label]) => ( + + )} + +
+
+
+ +
+ + +
{loadError()}
+
+ +
+
+ + + + + + + + + + + + + + 0}> + + {(pkg) => ( + <> + + + + + + + + + + + + - - - - - - - )} - - - -
NameRoleTraceCoinsPrice (₹)BonusStatusActions
Loading...
No packages found.
{pkg.name}{pkg.role}{pkg.tracecoin_amount}₹{(pkg.price_inr / 100).toFixed(2)}{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'} + + + {pkg.is_active ? 'Active' : 'Inactive'} + + +
+ + +
+
+ +
{editError()}
+
+
+
+ + setEditName(e.currentTarget.value)} style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px" /> +
+
+ + setEditTracecoins(e.currentTarget.value)} style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" /> +
+
+ + setEditPrice(e.currentTarget.value)} style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" /> +
+
+ + +
- -
{editError()}
-
-
-
- - setEditName(e.currentTarget.value)} - style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px" - /> -
-
- - setEditTracecoins(e.currentTarget.value)} - style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" - /> -
-
- - setEditPrice(e.currentTarget.value)} - style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" - /> -
-
- - -
-
-
-
+ + + )} + + +
- + +
+ {filteredRows().length} of {rows().length} packages +
+
+
+
+ + {/* ── Create Package ── */} + +
+

New Package

+ +
{cError()}
+
+
+
+ + setCName(e.currentTarget.value)} required placeholder="e.g. Starter Pack" style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" /> +
+
+ + +
+
+ + setCTracecoins(e.currentTarget.value)} required min="1" placeholder="e.g. 100" style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" /> +
+
+ + setCPrice(e.currentTarget.value)} required min="1" placeholder="e.g. 49900" style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" /> +
+
+ + setCBonus(e.currentTarget.value)} min="0" placeholder="e.g. 10" style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" /> +
+
+ +
+
+
+
- {/* Create Package tab */} - -
-

New Package

- -
{cError()}
-
-
-
- - setCName(e.currentTarget.value)} - required - placeholder="e.g. Starter Pack" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- - -
-
- - setCTracecoins(e.currentTarget.value)} - required - min="1" - placeholder="e.g. 100" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- - setCPrice(e.currentTarget.value)} - required - min="1" - placeholder="e.g. 49900" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- - setCBonus(e.currentTarget.value)} - min="0" - placeholder="e.g. 10" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- -
-
-
-
-
+
); }