ui(step-3): apply reference layout to 14 more management pages

- pricing, credit, coupon, discount, tax, order, invoice: white header,
  data-table/table-card, navy buttons, inline styles removed
- review, leads, jobs, notifications, support, report, ledger: same
  pattern + orange tab underlines where applicable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-24 05:20:55 +01:00
parent 0bb59b99ef
commit 5cfa4b89be
14 changed files with 1906 additions and 1859 deletions

View file

@ -142,194 +142,196 @@ export default function CouponPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Coupon Management</h1>
<p class="mt-1 text-sm text-gray-500">Reusable coupon codes for package checkout</p>
<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">Coupon Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Reusable coupon codes for package checkout</p>
</div>
{/* Tabs */}
<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={activeTab() === 'list' ? '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={() => setActiveTab('list')}
>
Coupons
</button>
<button
type="button"
class={activeTab() === 'create' ? '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={() => { resetForm(); setActiveTab('create'); }}
>
{form().id ? 'Edit Coupon' : 'Create Coupon'}
</button>
</div>
<div class="flex-1 p-6">
<Show when={activeTab() === 'list'}>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Code</th>
<th>Title</th>
<th>Type</th>
<th>Value</th>
<th>Max Uses</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={coupons.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!coupons.loading && coupons.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!coupons.loading && !coupons.error && coupons()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No coupons found.</td></tr>
</Show>
<Show when={!coupons.loading && !coupons.error && (coupons()?.length ?? 0) > 0}>
<For each={coupons()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900" style="font-family:monospace">{item.code}</td>
<td class="text-slate-500">{item.title || '—'}</td>
<td class="text-slate-500">{item.type}</td>
<td class="text-slate-500">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</td>
<td class="text-slate-500">{item.usage_limit != null ? item.usage_limit : '—'}</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.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button 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" onClick={() => startEdit(item)}>Edit</button>
<button
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"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
{toggling() === item.id ? '...' : (item.is_active ? 'Disable' : 'Enable')}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Coupon' : 'Create Coupon'}</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Code</label>
<input
type="text"
value={form().code}
onInput={(e) => setForm({ ...form(), code: e.currentTarget.value.toUpperCase() })}
required
placeholder="e.g. SAVE10"
style="text-transform:uppercase"
/>
</div>
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="e.g. 10% off for companies"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
value={form().type}
onChange={(e) => setForm({ ...form(), type: e.currentTarget.value as 'PERCENT' | 'FIXED' })}
>
<option value="PERCENT">Percent (%)</option>
<option value="FIXED">Fixed ()</option>
</select>
</div>
<div class="field">
<label>Value</label>
<input
type="number"
value={form().value}
onInput={(e) => setForm({ ...form(), value: Number(e.currentTarget.value) })}
required
min="1"
/>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Min Order Amount ()</label>
<input
type="number"
value={form().min_order_amount}
onInput={(e) => setForm({ ...form(), min_order_amount: Number(e.currentTarget.value) })}
min="0"
placeholder="0"
/>
</div>
<div class="field">
<label>Max Uses (blank = unlimited)</label>
<input
type="number"
value={form().max_uses}
onInput={(e) => setForm({ ...form(), max_uses: e.currentTarget.value })}
min="1"
placeholder="Unlimited"
/>
</div>
</div>
<div>
<p style="font-size:13px;font-weight:600;margin:0 0 8px;color:#1e293b">Applicable Roles</p>
<div style="display:flex;flex-wrap:wrap;gap:8px">
<For each={ROLE_OPTIONS}>
{(role) => {
const active = () => form().role_keys.includes(role);
return (
<button
type="button"
onClick={() => toggleRole(role)}
style={`border-radius:999px;padding:4px 14px;font-size:13px;cursor:pointer;border:1px solid ${active() ? '#fdba74' : '#cbd5e1'};background:${active() ? '#fff7ed' : '#fff'};color:${active() ? '#c2410c' : '#475569'}`}
>
{role}
</button>
);
}}
</For>
</div>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Coupon' : 'Save Coupon')}
</button>
<Show when={form().id}>
<button type="button" 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" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</div>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${activeTab() === 'list' ? ' active' : ''}`}
onClick={() => setActiveTab('list')}
>
Coupons
</button>
<button
type="button"
class={`admin-tab${activeTab() === 'create' ? ' active' : ''}`}
onClick={() => { resetForm(); setActiveTab('create'); }}
>
{form().id ? 'Edit Coupon' : 'Create Coupon'}
</button>
</div>
<Show when={activeTab() === 'list'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Code</th>
<th>Title</th>
<th>Type</th>
<th>Value</th>
<th>Max Uses</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={coupons.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!coupons.loading && coupons.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!coupons.loading && !coupons.error && coupons()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No coupons found.</td></tr>
</Show>
<Show when={!coupons.loading && !coupons.error && (coupons()?.length ?? 0) > 0}>
<For each={coupons()}>
{(item) => (
<tr>
<td style="font-weight:600;color:#0f172a;font-family:monospace">{item.code}</td>
<td style="color:#475569">{item.title || '—'}</td>
<td style="color:#475569">{item.type}</td>
<td style="color:#475569">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</td>
<td style="color:#475569">{item.usage_limit != null ? item.usage_limit : '—'}</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.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button 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" onClick={() => startEdit(item)}>Edit</button>
<button
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"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
{toggling() === item.id ? '...' : (item.is_active ? 'Disable' : 'Enable')}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Coupon' : 'Create Coupon'}</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Code</label>
<input
type="text"
value={form().code}
onInput={(e) => setForm({ ...form(), code: e.currentTarget.value.toUpperCase() })}
required
placeholder="e.g. SAVE10"
style="text-transform:uppercase"
/>
</div>
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="e.g. 10% off for companies"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
value={form().type}
onChange={(e) => setForm({ ...form(), type: e.currentTarget.value as 'PERCENT' | 'FIXED' })}
>
<option value="PERCENT">Percent (%)</option>
<option value="FIXED">Fixed ()</option>
</select>
</div>
<div class="field">
<label>Value</label>
<input
type="number"
value={form().value}
onInput={(e) => setForm({ ...form(), value: Number(e.currentTarget.value) })}
required
min="1"
/>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Min Order Amount ()</label>
<input
type="number"
value={form().min_order_amount}
onInput={(e) => setForm({ ...form(), min_order_amount: Number(e.currentTarget.value) })}
min="0"
placeholder="0"
/>
</div>
<div class="field">
<label>Max Uses (blank = unlimited)</label>
<input
type="number"
value={form().max_uses}
onInput={(e) => setForm({ ...form(), max_uses: e.currentTarget.value })}
min="1"
placeholder="Unlimited"
/>
</div>
</div>
<div>
<p style="font-size:13px;font-weight:600;margin:0 0 8px;color:#1e293b">Applicable Roles</p>
<div style="display:flex;flex-wrap:wrap;gap:8px">
<For each={ROLE_OPTIONS}>
{(role) => {
const active = () => form().role_keys.includes(role);
return (
<button
type="button"
onClick={() => toggleRole(role)}
style={`border-radius:999px;padding:4px 14px;font-size:13px;cursor:pointer;border:1px solid ${active() ? '#fdba74' : '#cbd5e1'};background:${active() ? '#fff7ed' : '#fff'};color:${active() ? '#c2410c' : '#475569'}`}
>
{role}
</button>
);
}}
</For>
</div>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Coupon' : 'Save Coupon')}
</button>
<Show when={form().id}>
<button type="button" 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" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</AdminShell>
);
}

View file

@ -136,270 +136,273 @@ export default function CreditPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Credit Management</h1>
<p class="mt-1 text-sm text-gray-500">Audit TraceCoin balances and adjust credits</p>
<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">Credit Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Audit TraceCoin balances and adjust credits</p>
</div>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<For each={tabs}>
{(tab) => (
<button
type="button"
class={`admin-tab${activeTab() === tab.key ? ' active' : ''}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
)}
</For>
</div>
{/* Balance & Ledger Tab */}
<Show when={activeTab() === 'ledger'}>
<div style="display:flex;flex-direction:column;gap:24px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Search Account Balance</h2>
<div style="display:flex;gap:10px">
<input
type="text"
placeholder="Enter User ID..."
value={userId()}
onInput={(e) => setUserId(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
style="flex:1;padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
/>
{/* Tabs */}
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
<For each={tabs}>
{(tab) => (
<button
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={handleSearch}
disabled={searchLoading()}
type="button"
class={activeTab() === tab.key ? '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={() => setActiveTab(tab.key)}
>
{searchLoading() ? 'Searching...' : 'Search'}
{tab.label}
</button>
</div>
<Show when={searchError()}>
<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:10px">{searchError()}</div>
</Show>
</section>
)}
</For>
</div>
<Show when={balance() !== null}>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px">
<div style="background:#2563eb;border-radius:12px;padding:24px;color:#fff">
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#bfdbfe;margin:0 0 8px">Current Balance</p>
<p style="font-size:36px;font-weight:900;margin:0">{balance()} TraceCoins</p>
<p style="font-size:12px;color:#bfdbfe;margin:8px 0 0">User: {searchedUserId()}</p>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:20px;overflow:hidden">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#0f172a">TraceCoin Ledger</h3>
<Show when={ledger().length === 0}>
<p style="text-align:center;padding:32px;color:#94a3b8;font-style:italic">No transactions found for this account.</p>
<div class="flex-1 p-6">
{/* Balance & Ledger Tab */}
<Show when={activeTab() === 'ledger'}>
<div style="display:flex;flex-direction:column;gap:24px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Search Account Balance</h2>
<div style="display:flex;gap:10px">
<input
type="text"
placeholder="Enter User ID..."
value={userId()}
onInput={(e) => setUserId(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
class="rounded-lg border border-gray-200 px-3 py-2 text-sm"
style="flex:1"
/>
<button
class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors"
onClick={handleSearch}
disabled={searchLoading()}
>
{searchLoading() ? 'Searching...' : 'Search'}
</button>
</div>
<Show when={searchError()}>
<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:10px">{searchError()}</div>
</Show>
<Show when={ledger().length > 0}>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Type</th>
<th>Amount</th>
<th>Ref ID</th>
<th>Expires At</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<For each={ledger()}>
{(entry) => (
</section>
<Show when={balance() !== null}>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px">
<div style="background:#2563eb;border-radius:12px;padding:24px;color:#fff">
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#bfdbfe;margin:0 0 8px">Current Balance</p>
<p style="font-size:36px;font-weight:900;margin:0">{balance()} TraceCoins</p>
<p style="font-size:12px;color:#bfdbfe;margin:8px 0 0">User: {searchedUserId()}</p>
</div>
<div class="table-card" style="overflow:hidden">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#0f172a">TraceCoin Ledger</h3>
<Show when={ledger().length === 0}>
<p style="text-align:center;padding:32px;color:#94a3b8;font-style:italic">No transactions found for this account.</p>
</Show>
<Show when={ledger().length > 0}>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<td>
<span
style={`display:inline-block;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:700;text-transform:uppercase;background:${entry.transactionType === 'ADD' ? '#dcfce7' : '#fee2e2'};color:${entry.transactionType === 'ADD' ? '#15803d' : '#b91c1c'}`}
>
{entry.transactionType}
</span>
</td>
<td style={`font-weight:700;${entry.transactionType === 'ADD' ? 'color:#16a34a' : 'color:#dc2626'}`}>
{entry.transactionType === 'ADD' ? '+' : '-'}{entry.amount}
</td>
<td style="font-size:12px;color:#64748b;font-family:monospace">
{entry.referenceId ? entry.referenceId.substring(0, 18) : '—'}
</td>
<td style="font-size:12px;color:#94a3b8">
{entry.expiresAt ? new Date(entry.expiresAt).toLocaleDateString() : '—'}
</td>
<td style="font-size:12px;color:#94a3b8">
{entry.createdAt ? new Date(entry.createdAt).toLocaleString() : '—'}
</td>
<th>Type</th>
<th>Amount</th>
<th>Ref ID</th>
<th>Expires At</th>
<th>Date</th>
</tr>
)}
</For>
</tbody>
</table>
</thead>
<tbody>
<For each={ledger()}>
{(entry) => (
<tr class="hover:bg-slate-50">
<td>
<span
style={`display:inline-block;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:700;text-transform:uppercase;background:${entry.transactionType === 'ADD' ? '#dcfce7' : '#fee2e2'};color:${entry.transactionType === 'ADD' ? '#15803d' : '#b91c1c'}`}
>
{entry.transactionType}
</span>
</td>
<td class={entry.transactionType === 'ADD' ? 'font-semibold text-slate-900' : 'font-semibold text-slate-900'} style={entry.transactionType === 'ADD' ? 'color:#16a34a' : 'color:#dc2626'}>
{entry.transactionType === 'ADD' ? '+' : '-'}{entry.amount}
</td>
<td class="text-slate-500" style="font-size:12px;font-family:monospace">
{entry.referenceId ? entry.referenceId.substring(0, 18) : '—'}
</td>
<td class="text-slate-500" style="font-size:12px">
{entry.expiresAt ? new Date(entry.expiresAt).toLocaleDateString() : '—'}
</td>
<td class="text-slate-500" style="font-size:12px">
{entry.createdAt ? new Date(entry.createdAt).toLocaleString() : '—'}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</div>
</div>
</Show>
</div>
</Show>
{/* Reward / Deduct Tab */}
<Show when={activeTab() === 'adjust'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Reward or Adjust TraceCoins</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Use this to reward TraceCoins for a valid support case, process a refund, or correct a balance manually.
</p>
<Show when={adjSuccess()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:14px;color:#15803d;font-weight:600">
{adjSuccess()}
</div>
</Show>
<Show when={adjError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{adjError()}</div>
</Show>
<form onSubmit={handleAdjust} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">User ID</label>
<input
type="text"
required
value={adjUserId()}
onInput={(e) => setAdjUserId(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Amount</label>
<input
type="number"
min="1"
required
value={adjAmount()}
onInput={(e) => setAdjAmount(Number(e.currentTarget.value))}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
value={adjType()}
onChange={(e) => setAdjType(e.currentTarget.value as 'ADD' | 'DEDUCT')}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<option value="ADD">ADD</option>
<option value="DEDUCT">DEDUCT</option>
</select>
</div>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Reason</label>
<input
type="text"
required
value={adjReason()}
onInput={(e) => setAdjReason(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Reference ID <span style="font-weight:400;color:#94a3b8">(optional)</span></label>
<input
type="text"
value={adjRefId()}
onInput={(e) => setAdjRefId(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={adjLoading()}>
{adjLoading() ? 'Adjusting...' : 'Apply Adjustment'}
</button>
</div>
</form>
</section>
</Show>
{/* Reconcile Tab */}
<Show when={activeTab() === 'reconcile'}>
<div style="display:flex;flex-direction:column;gap:20px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Ledger Reconciliation</h2>
<Show when={reconError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{reconError()}</div>
</Show>
<form onSubmit={handleReconcile} style="display:flex;flex-direction:column;gap:14px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Start Date</label>
<input
type="date"
required
value={reconFrom()}
onInput={(e) => setReconFrom(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">End Date</label>
<input
type="date"
required
value={reconTo()}
onInput={(e) => setReconTo(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={reconLoading()}>
{reconLoading() ? 'Running...' : 'Run Reconciliation'}
</button>
</div>
</form>
</section>
<Show when={reconResults() !== null}>
<Show when={(reconResults()?.length ?? 0) === 0}>
<p style="color:#16a34a;font-weight:600;font-size:14px">No discrepancies found.</p>
</Show>
<Show when={(reconResults()?.length ?? 0) > 0}>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>User ID</th>
<th>Expected Balance</th>
<th>Actual Balance</th>
<th>Discrepancy</th>
</tr>
</thead>
<tbody>
<For each={reconResults()!}>
{(row) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900" style="font-family:monospace;font-size:13px">{row.userId}</td>
<td class="text-slate-500">{row.expectedBalance}</td>
<td class="text-slate-500">{row.actualBalance}</td>
<td style={row.discrepancy !== 0 ? 'color:#dc2626;font-weight:700' : 'color:#16a34a'}>
{row.discrepancy}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</Show>
</section>
</Show>
</div>
</Show>
</div>
</Show>
{/* Reward / Deduct Tab */}
<Show when={activeTab() === 'adjust'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Reward or Adjust TraceCoins</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Use this to reward TraceCoins for a valid support case, process a refund, or correct a balance manually.
</p>
<Show when={adjSuccess()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:14px;color:#15803d;font-weight:600">
{adjSuccess()}
</div>
</Show>
<Show when={adjError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{adjError()}</div>
</Show>
<form onSubmit={handleAdjust} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">User ID</label>
<input
type="text"
required
value={adjUserId()}
onInput={(e) => setAdjUserId(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Amount</label>
<input
type="number"
min="1"
required
value={adjAmount()}
onInput={(e) => setAdjAmount(Number(e.currentTarget.value))}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
value={adjType()}
onChange={(e) => setAdjType(e.currentTarget.value as 'ADD' | 'DEDUCT')}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<option value="ADD">ADD</option>
<option value="DEDUCT">DEDUCT</option>
</select>
</div>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Reason</label>
<input
type="text"
required
value={adjReason()}
onInput={(e) => setAdjReason(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Reference ID <span style="font-weight:400;color:#94a3b8">(optional)</span></label>
<input
type="text"
value={adjRefId()}
onInput={(e) => setAdjRefId(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={adjLoading()}>
{adjLoading() ? 'Adjusting...' : 'Apply Adjustment'}
</button>
</div>
</form>
</section>
</Show>
{/* Reconcile Tab */}
<Show when={activeTab() === 'reconcile'}>
<div style="display:flex;flex-direction:column;gap:20px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Ledger Reconciliation</h2>
<Show when={reconError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{reconError()}</div>
</Show>
<form onSubmit={handleReconcile} style="display:flex;flex-direction:column;gap:14px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Start Date</label>
<input
type="date"
required
value={reconFrom()}
onInput={(e) => setReconFrom(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">End Date</label>
<input
type="date"
required
value={reconTo()}
onInput={(e) => setReconTo(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={reconLoading()}>
{reconLoading() ? 'Running...' : 'Run Reconciliation'}
</button>
</div>
</form>
</section>
<Show when={reconResults() !== null}>
<Show when={(reconResults()?.length ?? 0) === 0}>
<p style="color:#16a34a;font-weight:600;font-size:14px">No discrepancies found.</p>
</Show>
<Show when={(reconResults()?.length ?? 0) > 0}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>User ID</th>
<th>Expected Balance</th>
<th>Actual Balance</th>
<th>Discrepancy</th>
</tr>
</thead>
<tbody>
<For each={reconResults()!}>
{(row) => (
<tr>
<td style="font-family:monospace;font-size:13px">{row.userId}</td>
<td>{row.expectedBalance}</td>
<td>{row.actualBalance}</td>
<td style={row.discrepancy !== 0 ? 'color:#dc2626;font-weight:700' : 'color:#16a34a'}>
{row.discrepancy}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</section>
</Show>
</Show>
</div>
</Show>
</div>
</AdminShell>
);
}

View file

@ -146,194 +146,196 @@ export default function DiscountPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Discount Management</h1>
<p class="mt-1 text-sm text-gray-500">Automatic discounts applied before coupons</p>
<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">Discount Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Automatic discounts applied before coupons</p>
</div>
{/* Tabs */}
<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={activeTab() === 'list' ? '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={() => setActiveTab('list')}
>
Discounts
</button>
<button
type="button"
class={activeTab() === 'create' ? '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={() => { resetForm(); setActiveTab('create'); }}
>
{form().id ? 'Edit Discount' : 'Create Discount'}
</button>
</div>
<div class="flex-1 p-6">
<Show when={activeTab() === 'list'}>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Scope</th>
<th>Target</th>
<th>Type</th>
<th>Value</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={discounts.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!discounts.loading && discounts.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!discounts.loading && !discounts.error && discounts()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No discounts found.</td></tr>
</Show>
<Show when={!discounts.loading && !discounts.error && (discounts()?.length ?? 0) > 0}>
<For each={discounts()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{item.title || '—'}</td>
<td class="text-slate-500">{item.scope}</td>
<td class="text-slate-500">{getTarget(item)}</td>
<td class="text-slate-500">{item.type}</td>
<td class="text-slate-500">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</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.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button
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"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
{toggling() === item.id ? '...' : (item.is_active ? 'Disable' : 'Enable')}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Discount' : 'Create Discount'}</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="e.g. New user 10% off"
/>
</div>
<div class="field">
<label>Scope</label>
<div style="display:flex;gap:24px;margin-top:4px">
<label style="display:flex;align-items:center;gap:6px;font-size:14px;font-weight:400;cursor:pointer">
<input
type="radio"
name="scope"
value="ROLE"
checked={form().scope === 'ROLE'}
onChange={() => setForm({ ...form(), scope: 'ROLE' })}
/>
ROLE
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:14px;font-weight:400;cursor:pointer">
<input
type="radio"
name="scope"
value="PACKAGE"
checked={form().scope === 'PACKAGE'}
onChange={() => setForm({ ...form(), scope: 'PACKAGE' })}
/>
PACKAGE
</label>
</div>
</div>
<div class="field">
<label>Target</label>
<Show when={form().scope === 'ROLE'}>
<select
value={form().role_key}
onChange={(e) => setForm({ ...form(), role_key: e.currentTarget.value })}
>
<For each={ROLE_OPTIONS}>
{(role) => <option value={role}>{role}</option>}
</For>
</select>
</Show>
<Show when={form().scope === 'PACKAGE'}>
<select
value={form().package_id}
onChange={(e) => setForm({ ...form(), package_id: e.currentTarget.value })}
>
<Show when={packages.loading}>
<option value="">Loading packages...</option>
</Show>
<Show when={!packages.loading}>
<For each={packages()}>
{(pkg) => <option value={pkg.id}>{pkg.name}</option>}
</For>
</Show>
</select>
</Show>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
value={form().type}
onChange={(e) => setForm({ ...form(), type: e.currentTarget.value as 'PERCENT' | 'FIXED' })}
>
<option value="PERCENT">Percent (%)</option>
<option value="FIXED">Fixed ()</option>
</select>
</div>
<div class="field">
<label>Value</label>
<input
type="number"
value={form().value}
onInput={(e) => setForm({ ...form(), value: Number(e.currentTarget.value) })}
required
min="1"
/>
</div>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Discount' : 'Save Discount')}
</button>
<Show when={form().id}>
<button type="button" 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" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</div>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${activeTab() === 'list' ? ' active' : ''}`}
onClick={() => setActiveTab('list')}
>
Discounts
</button>
<button
type="button"
class={`admin-tab${activeTab() === 'create' ? ' active' : ''}`}
onClick={() => { resetForm(); setActiveTab('create'); }}
>
{form().id ? 'Edit Discount' : 'Create Discount'}
</button>
</div>
<Show when={activeTab() === 'list'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Scope</th>
<th>Target</th>
<th>Type</th>
<th>Value</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={discounts.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!discounts.loading && discounts.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!discounts.loading && !discounts.error && discounts()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No discounts found.</td></tr>
</Show>
<Show when={!discounts.loading && !discounts.error && (discounts()?.length ?? 0) > 0}>
<For each={discounts()}>
{(item) => (
<tr>
<td style="font-weight:600;color:#0f172a">{item.title || '—'}</td>
<td style="color:#475569">{item.scope}</td>
<td style="color:#475569">{getTarget(item)}</td>
<td style="color:#475569">{item.type}</td>
<td style="color:#475569">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</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.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button
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"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
{toggling() === item.id ? '...' : (item.is_active ? 'Disable' : 'Enable')}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Discount' : 'Create Discount'}</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="e.g. New user 10% off"
/>
</div>
<div class="field">
<label>Scope</label>
<div style="display:flex;gap:24px;margin-top:4px">
<label style="display:flex;align-items:center;gap:6px;font-size:14px;font-weight:400;cursor:pointer">
<input
type="radio"
name="scope"
value="ROLE"
checked={form().scope === 'ROLE'}
onChange={() => setForm({ ...form(), scope: 'ROLE' })}
/>
ROLE
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:14px;font-weight:400;cursor:pointer">
<input
type="radio"
name="scope"
value="PACKAGE"
checked={form().scope === 'PACKAGE'}
onChange={() => setForm({ ...form(), scope: 'PACKAGE' })}
/>
PACKAGE
</label>
</div>
</div>
<div class="field">
<label>Target</label>
<Show when={form().scope === 'ROLE'}>
<select
value={form().role_key}
onChange={(e) => setForm({ ...form(), role_key: e.currentTarget.value })}
>
<For each={ROLE_OPTIONS}>
{(role) => <option value={role}>{role}</option>}
</For>
</select>
</Show>
<Show when={form().scope === 'PACKAGE'}>
<select
value={form().package_id}
onChange={(e) => setForm({ ...form(), package_id: e.currentTarget.value })}
>
<Show when={packages.loading}>
<option value="">Loading packages...</option>
</Show>
<Show when={!packages.loading}>
<For each={packages()}>
{(pkg) => <option value={pkg.id}>{pkg.name}</option>}
</For>
</Show>
</select>
</Show>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
value={form().type}
onChange={(e) => setForm({ ...form(), type: e.currentTarget.value as 'PERCENT' | 'FIXED' })}
>
<option value="PERCENT">Percent (%)</option>
<option value="FIXED">Fixed ()</option>
</select>
</div>
<div class="field">
<label>Value</label>
<input
type="number"
value={form().value}
onInput={(e) => setForm({ ...form(), value: Number(e.currentTarget.value) })}
required
min="1"
/>
</div>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Discount' : 'Save Discount')}
</button>
<Show when={form().id}>
<button type="button" 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" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</AdminShell>
);
}

View file

@ -32,87 +32,90 @@ export default function InvoicePage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Invoice Management</h1>
<p class="mt-1 text-sm text-gray-500">View and download all platform invoices.</p>
<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>
<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)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:320px"
/>
</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>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="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) => (
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<td style="font-weight:600;color:#0f172a;font-family:monospace">{item.invoice_number || item.id}</td>
<td style="color:#475569">{item.user_id || '—'}</td>
<td style="color:#475569">{item.package_name || '—'}</td>
<td style="color:#475569">{item.total != null ? (item.total / 100).toFixed(2) : '—'}</td>
<td style="color:#475569">{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 style="color:#475569">{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>
<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>
))}
</Show>
</tbody>
</table>
</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>
</section>
</div>
</AdminShell>
);
}

View file

@ -63,95 +63,97 @@ export default function JobsPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Jobs Management</h1>
<p class="mt-1 text-sm text-gray-500">Review live company job postings</p>
<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">Jobs Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Review live company job postings</p>
</div>
<div class="flex-1 p-6">
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<input
type="text"
placeholder="Search by title or company..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:260px"
/>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<For each={STATUS_OPTIONS}>{(opt) => <option value={opt}>{opt === 'All' ? 'All Statuses' : opt}</option>}</For>
</select>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Skills</th>
<th>Company / Client</th>
<th>Rate</th>
<th>Location</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={jobs.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!jobs.loading && jobs.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No jobs found.</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length > 0}>
<For each={filtered()}>
{(job) => (
<tr class="hover:bg-slate-50">
<td>
<div class="font-semibold text-slate-900">{job.title || '—'}</div>
<Show when={job.description}>
<div style="font-size:12px;color:#64748b;margin-top:2px;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
{job.description}
</div>
</Show>
</td>
<td class="text-slate-500">
{job.required_skills?.join(', ') || job.experience_level || '—'}
</td>
<td class="text-slate-500">{job.client_name || job.company_name || '—'}</td>
<td class="text-slate-500">
{job.hourly_rate_min != null
? `${job.hourly_rate_min}–₹${job.hourly_rate_max ?? job.hourly_rate_min}/hr`
: '—'}
</td>
<td class="text-slate-500">{job.location || '—'}</td>
<td>
<span class={statusChipClass(job.status)}>{job.status || '—'}</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/jobs/${job.id}`}>View</A>
<Show when={job.status === 'PENDING_APPROVAL'}>
<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="/admin/approval">Review</A>
</Show>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<input
type="text"
placeholder="Search by title or company..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:260px"
/>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<For each={STATUS_OPTIONS}>{(opt) => <option value={opt}>{opt === 'All' ? 'All Statuses' : opt}</option>}</For>
</select>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Skills</th>
<th>Company / Client</th>
<th>Rate</th>
<th>Location</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={jobs.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!jobs.loading && jobs.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No jobs found.</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length > 0}>
<For each={filtered()}>
{(job) => (
<tr>
<td>
<div style="font-weight:600;color:#0f172a">{job.title || '—'}</div>
<Show when={job.description}>
<div style="font-size:12px;color:#64748b;margin-top:2px;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
{job.description}
</div>
</Show>
</td>
<td style="color:#475569">
{job.required_skills?.join(', ') || job.experience_level || '—'}
</td>
<td style="color:#475569">{job.client_name || job.company_name || '—'}</td>
<td style="color:#475569">
{job.hourly_rate_min != null
? `${job.hourly_rate_min}–₹${job.hourly_rate_max ?? job.hourly_rate_min}/hr`
: '—'}
</td>
<td style="color:#475569">{job.location || '—'}</td>
<td>
<span class={statusChipClass(job.status)}>{job.status || '—'}</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/jobs/${job.id}`}>View</A>
<Show when={job.status === 'PENDING_APPROVAL'}>
<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="/admin/approval">Review</A>
</Show>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}

View file

@ -49,107 +49,109 @@ export default function LeadsPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Leads Management</h1>
<p class="mt-1 text-sm text-gray-500">View all requirements and lead requests from customers.</p>
<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">Leads Management</h1>
<p class="text-sm text-gray-500 mt-0.5">View all requirements and lead requests from customers.</p>
</div>
<div class="flex-1 p-6">
{/* Filters */}
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;align-items:center;">
<input
type="text"
placeholder="Search by title or location..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;outline:none;min-width:220px;flex:1;max-width:320px"
/>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;background:#fff;outline:none;"
>
<option value="">All Statuses</option>
<option value="OPEN">Open</option>
<option value="ACTIVE">Active</option>
<option value="PENDING">Pending</option>
<option value="CLOSED">Closed</option>
<option value="CANCELLED">Cancelled</option>
</select>
<select
value={roleFilter()}
onChange={(e) => setRoleFilter(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;background:#fff;outline:none;"
>
<option value="">All Roles</option>
<For each={ROLE_OPTIONS}>
{(r) => <option value={r}>{r.replace(/_/g, ' ')}</option>}
</For>
</select>
<Show when={search() || statusFilter() || roleFilter()}>
<span style="font-size:13px;color:#64748b">{filtered().length} result{filtered().length !== 1 ? 's' : ''}</span>
</Show>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Role</th>
<th>Budget</th>
<th>Location</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={leads.loading}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading leads...</td></tr>
</Show>
<Show when={!leads.loading && leads.error}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!leads.loading && !leads.error && filtered().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No leads found.</td></tr>
</Show>
<Show when={!leads.loading && !leads.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td>
<div class="font-semibold text-slate-900">{item.title || '—'}</div>
<Show when={item.description}>
<div style="font-size:12px;color:#64748b;margin-top:2px">
{String(item.description).slice(0, 60)}{String(item.description).length > 60 ? '…' : ''}
</div>
</Show>
</td>
<td class="text-slate-500">{item.profession || item.role || '—'}</td>
<td class="text-slate-500">
{item.budget_range || (item.budget_min != null ? `${item.budget_min}–₹${item.budget_max}` : '—')}
</td>
<td class="text-slate-500">{item.location || '—'}</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 === 'ACTIVE' || item.status === 'OPEN') ? 'active' : ''}`}>
{item.status || '—'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/leads/${item.id}`}>View</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</div>
{/* Filters */}
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;align-items:center;">
<input
type="text"
placeholder="Search by title or location..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;outline:none;min-width:220px;flex:1;max-width:320px"
/>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;background:#fff;outline:none;"
>
<option value="">All Statuses</option>
<option value="OPEN">Open</option>
<option value="ACTIVE">Active</option>
<option value="PENDING">Pending</option>
<option value="CLOSED">Closed</option>
<option value="CANCELLED">Cancelled</option>
</select>
<select
value={roleFilter()}
onChange={(e) => setRoleFilter(e.currentTarget.value)}
style="border:1px solid #cbd5e1;border-radius:8px;padding:8px 12px;font-size:14px;background:#fff;outline:none;"
>
<option value="">All Roles</option>
<For each={ROLE_OPTIONS}>
{(r) => <option value={r}>{r.replace(/_/g, ' ')}</option>}
</For>
</select>
<Show when={search() || statusFilter() || roleFilter()}>
<span style="font-size:13px;color:#64748b">{filtered().length} result{filtered().length !== 1 ? 's' : ''}</span>
</Show>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Role</th>
<th>Budget</th>
<th>Location</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={leads.loading}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading leads...</td></tr>
</Show>
<Show when={!leads.loading && leads.error}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!leads.loading && !leads.error && filtered().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No leads found.</td></tr>
</Show>
<Show when={!leads.loading && !leads.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<tr>
<td>
<div style="font-weight:600;color:#0f172a">{item.title || '—'}</div>
<Show when={item.description}>
<div style="font-size:12px;color:#64748b;margin-top:2px">
{String(item.description).slice(0, 60)}{String(item.description).length > 60 ? '…' : ''}
</div>
</Show>
</td>
<td style="color:#475569">{item.profession || item.role || '—'}</td>
<td style="color:#475569">
{item.budget_range || (item.budget_min != null ? `${item.budget_min}–₹${item.budget_max}` : '—')}
</td>
<td style="color:#475569">{item.location || '—'}</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 === 'ACTIVE' || item.status === 'OPEN') ? 'active' : ''}`}>
{item.status || '—'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/leads/${item.id}`}>View</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}

View file

@ -56,63 +56,65 @@ export default function LedgerPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Ledger Management</h1>
<p class="mt-1 text-sm text-gray-500">Platform financial ledger</p>
<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">Ledger Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Platform financial ledger</p>
</div>
<div class="flex-1 p-6">
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Type</th>
<th>Order ID</th>
<th>Invoice ID</th>
<th>User ID</th>
<th>Amount</th>
<th>Note</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<Show when={entries.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!entries.loading && entries.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!entries.loading && !entries.error && entries()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No ledger entries found.</td></tr>
</Show>
<Show when={!entries.loading && !entries.error && (entries()?.length ?? 0) > 0}>
<For each={entries()}>
{(item) => {
const entryType = item.entry_type || item.type || '—';
return (
<tr class="hover:bg-slate-50">
<td>
<span style={`${typeBadgeStyle(entryType)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid;display:inline-block`}>
{entryType}
</span>
</td>
<td class="text-slate-500" style="font-size:12px;font-family:monospace">{item.order_id || '—'}</td>
<td class="text-slate-500" style="font-size:12px;font-family:monospace">{item.invoice_id || '—'}</td>
<td class="text-slate-500" style="font-size:12px;font-family:monospace">{item.user_id || '—'}</td>
<td class="font-semibold text-slate-900">{formatAmount(item)}</td>
<td class="text-slate-500">{item.note || '—'}</td>
<td class="text-slate-500">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Type</th>
<th>Order ID</th>
<th>Invoice ID</th>
<th>User ID</th>
<th>Amount</th>
<th>Note</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<Show when={entries.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!entries.loading && entries.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!entries.loading && !entries.error && entries()?.length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No ledger entries found.</td></tr>
</Show>
<Show when={!entries.loading && !entries.error && (entries()?.length ?? 0) > 0}>
<For each={entries()}>
{(item) => {
const entryType = item.entry_type || item.type || '—';
return (
<tr>
<td>
<span style={`${typeBadgeStyle(entryType)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid;display:inline-block`}>
{entryType}
</span>
</td>
<td style="color:#475569;font-size:12px;font-family:monospace">{item.order_id || '—'}</td>
<td style="color:#475569;font-size:12px;font-family:monospace">{item.invoice_id || '—'}</td>
<td style="color:#475569;font-size:12px;font-family:monospace">{item.user_id || '—'}</td>
<td style="font-weight:600;color:#0f172a">{formatAmount(item)}</td>
<td style="color:#475569">{item.note || '—'}</td>
<td style="color:#475569">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}

View file

@ -83,105 +83,113 @@ export default function NotificationsPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Notifications</h1>
<p class="mt-1 text-sm text-gray-500">Approval outcomes and action-required updates</p>
</div>
<button
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"
onClick={handleMarkAllRead}
disabled={markingAllRead()}
>
{markingAllRead() ? 'Marking...' : 'Mark All Read'}
</button>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${activeTab() === 'all' ? ' active' : ''}`}
onClick={() => setActiveTab('all')}
>
All
</button>
<button
type="button"
class={`admin-tab${activeTab() === 'unread' ? ' active' : ''}`}
onClick={() => setActiveTab('unread')}
>
Unread ({unreadCount()})
</button>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Message</th>
<th>Event Type</th>
<th>Created At</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={loading() && rows().length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!loading() && visibleRows().length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No notifications.</td></tr>
</Show>
<For each={visibleRows()}>
{(item) => (
<tr style={item.read_at === null ? 'background:#eff6ff' : ''}>
<td style="font-weight:600;color:#0f172a;min-width:160px">{item.title}</td>
<td style="font-size:13px;color:#475569;max-width:320px">{truncate(item.message, 80)}</td>
<td>
<span style={`${BADGE_STYLE};${eventTypeBadgeStyle(item.event_type)}`}>
{item.event_type}
</span>
</td>
<td style="font-size:12px;color:#64748b;white-space:nowrap">
{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
<Show when={item.read_at === null}>
<button
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"
disabled={markingId() === item.id}
onClick={() => handleMarkRead(item.id)}
>
{markingId() === item.id ? '...' : 'Mark Read'}
</button>
</Show>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</section>
<Show when={loading() && rows().length > 0}>
<p style="text-align:center;padding:12px;font-size:13px;color:#64748b">Loading...</p>
</Show>
<Show when={cursor() && !loading()}>
<div style="margin-top:12px">
<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 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Notifications</h1>
<p class="text-sm text-gray-500 mt-0.5">Approval outcomes and action-required updates</p>
</div>
<button
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"
onClick={() => load(cursor(), false)}
onClick={handleMarkAllRead}
disabled={markingAllRead()}
>
Load More
{markingAllRead() ? 'Marking...' : 'Mark All Read'}
</button>
</div>
</Show>
{/* Tabs */}
<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={activeTab() === 'all'
? '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={() => setActiveTab('all')}
>
All
</button>
<button
type="button"
class={activeTab() === 'unread'
? '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={() => setActiveTab('unread')}
>
Unread ({unreadCount()})
</button>
</div>
<div class="flex-1 p-6">
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Message</th>
<th>Event Type</th>
<th>Created At</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={loading() && rows().length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!loading() && visibleRows().length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No notifications.</td></tr>
</Show>
<For each={visibleRows()}>
{(item) => (
<tr class="hover:bg-slate-50" style={item.read_at === null ? 'background:#eff6ff' : ''}>
<td class="font-semibold text-slate-900" style="min-width:160px">{item.title}</td>
<td class="text-slate-500" style="max-width:320px">{truncate(item.message, 80)}</td>
<td>
<span style={`${BADGE_STYLE};${eventTypeBadgeStyle(item.event_type)}`}>
{item.event_type}
</span>
</td>
<td class="text-slate-500" style="font-size:12px;white-space:nowrap">
{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
<Show when={item.read_at === null}>
<button
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"
disabled={markingId() === item.id}
onClick={() => handleMarkRead(item.id)}
>
{markingId() === item.id ? '...' : 'Mark Read'}
</button>
</Show>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
<Show when={loading() && rows().length > 0}>
<p style="text-align:center;padding:12px;font-size:13px;color:#64748b">Loading...</p>
</Show>
<Show when={cursor() && !loading()}>
<div style="margin-top:12px">
<button
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"
onClick={() => load(cursor(), false)}
>
Load More
</button>
</div>
</Show>
</div>
</div>
</AdminShell>
);
}

View file

@ -70,75 +70,78 @@ export default function OrderPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Order Management</h1>
<p class="mt-1 text-sm text-gray-500">TraceCoin package purchase orders</p>
<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">Order Management</h1>
<p class="text-sm text-gray-500 mt-0.5">TraceCoin package purchase orders</p>
</div>
<div class="flex-1 p-6">
<div style="margin-bottom:16px">
<input
type="text"
placeholder="Search by order number, user email, or status..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
class="rounded-lg border border-gray-200 px-3 py-2 text-sm"
style="width:100%;max-width:420px"
/>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Order #</th>
<th>User</th>
<th>Package</th>
<th>TraceCoins</th>
<th>Coupon</th>
<th>Total</th>
<th>Status</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
<Show when={orders.loading}>
<tr><td colspan="8" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!orders.loading && orders.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={!orders.loading && !orders.error && filtered().length === 0}>
<tr><td colspan="8" style="text-align:center;padding:32px;color:#94a3b8">No orders found.</td></tr>
</Show>
<Show when={!orders.loading && !orders.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900" style="font-family:monospace">{item.order_number || item.id}</td>
<td class="text-slate-500">{item.user_name || item.user_email || '—'}</td>
<td class="text-slate-500">{item.package_name || '—'}</td>
<td class="text-slate-500">{item.tracecoin_amount ?? '—'}</td>
<td class="text-slate-500">{item.coupon_code || '—'}</td>
<td class="text-slate-500">{formatAmount(item)}</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"
style={`${statusStyle(item.status)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid`}
>
{item.status || '—'}
</span>
</td>
<td class="text-slate-500">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<input
type="text"
placeholder="Search by order number, user email, or status..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="width:100%;max-width:420px;padding:8px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px"
/>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Order #</th>
<th>User</th>
<th>Package</th>
<th>TraceCoins</th>
<th>Coupon</th>
<th>Total</th>
<th>Status</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
<Show when={orders.loading}>
<tr><td colspan="8" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!orders.loading && orders.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={!orders.loading && !orders.error && filtered().length === 0}>
<tr><td colspan="8" style="text-align:center;padding:32px;color:#94a3b8">No orders found.</td></tr>
</Show>
<Show when={!orders.loading && !orders.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<tr>
<td style="font-weight:600;color:#0f172a;font-family:monospace">{item.order_number || item.id}</td>
<td style="color:#475569">{item.user_name || item.user_email || '—'}</td>
<td style="color:#475569">{item.package_name || '—'}</td>
<td style="color:#475569">{item.tracecoin_amount ?? '—'}</td>
<td style="color:#475569">{item.coupon_code || '—'}</td>
<td style="color:#475569">{formatAmount(item)}</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"
style={`${statusStyle(item.status)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid`}
>
{item.status || '—'}
</span>
</td>
<td style="color:#475569">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}

View file

@ -150,212 +150,214 @@ export default function PricingPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Pricing Management</h1>
<p class="mt-1 text-sm text-gray-500">Create and manage TraceCoin packages</p>
<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">Pricing Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Create and manage TraceCoin packages</p>
</div>
{/* Tabs */}
<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={view() === 'packages' ? '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={() => setView('packages')}
>
Packages
</button>
<button
type="button"
class={view() === 'create' ? '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={() => setView('create')}
>
Create Package
</button>
</div>
<div class="flex-1 p-6">
{/* Packages list tab */}
<Show when={view() === 'packages'}>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>TraceCoins</th>
<th>Price ()</th>
<th>Bonus (%)</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={packages.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!packages.loading && packages.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!packages.loading && !packages.error && (packages()?.length ?? 0) === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No packages found.</td></tr>
</Show>
<Show when={!packages.loading && !packages.error && (packages()?.length ?? 0) > 0}>
<For each={packages()}>
{(pkg) => (
<>
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{pkg.name}</td>
<td class="text-slate-500">{pkg.role}</td>
<td class="text-slate-500">{pkg.tracecoin_amount}</td>
<td class="text-slate-500">{(pkg.price_inr / 100).toFixed(2)}</td>
<td class="text-slate-500">{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'}</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 ${pkg.is_active ? 'active' : ''}`}>
{pkg.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button 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" onClick={() => startEdit(pkg)}>Edit</button>
<button
class={pkg.is_active ? 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors' : 'inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors'}
disabled={togglingId() === pkg.id}
onClick={() => toggleActive(pkg)}
>
{togglingId() === pkg.id ? '...' : pkg.is_active ? 'Disable' : 'Enable'}
</button>
</div>
</td>
</tr>
<Show when={editingId() === pkg.id}>
<tr>
<td colspan="7" style="background:#f8fafc;padding:16px">
<Show when={editError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{editError()}</div>
</Show>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={editName()}
onInput={(e) => setEditName(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
/>
</div>
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">TraceCoins</label>
<input
type="number"
value={editTracecoins()}
onInput={(e) => setEditTracecoins(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Price (paise)</label>
<input
type="number"
value={editPrice()}
onInput={(e) => setEditPrice(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div style="display:flex;gap:8px">
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" disabled={editSaving()} onClick={() => saveEdit(pkg.id)}>
{editSaving() ? 'Saving...' : 'Save'}
</button>
<button 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" onClick={cancelEdit}>Cancel</button>
</div>
</div>
</td>
</tr>
</Show>
</>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</Show>
{/* Create Package tab */}
<Show when={view() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:480px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Package</h2>
<Show when={cError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{cError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={cName()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Role</label>
<select
value={cRole()}
onChange={(e) => setCRole(e.currentTarget.value)}
required
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<For each={ROLES}>{(r) => <option value={r}>{r}</option>}</For>
</select>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">TraceCoins</label>
<input
type="number"
value={cTracecoins()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Price INR (paise, e.g. 49900 = 499)</label>
<input
type="number"
value={cPrice()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Bonus Percentage (optional, e.g. 10 = 10% bonus coins)</label>
<input
type="number"
value={cBonus()}
onInput={(e) => 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"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={cSaving()}>
{cSaving() ? 'Creating...' : 'Create Package'}
</button>
</div>
</form>
</section>
</Show>
</div>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${view() === 'packages' ? ' active' : ''}`}
onClick={() => setView('packages')}
>
Packages
</button>
<button
type="button"
class={`admin-tab${view() === 'create' ? ' active' : ''}`}
onClick={() => setView('create')}
>
Create Package
</button>
</div>
{/* Packages list tab */}
<Show when={view() === 'packages'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>TraceCoins</th>
<th>Price ()</th>
<th>Bonus (%)</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={packages.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!packages.loading && packages.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!packages.loading && !packages.error && (packages()?.length ?? 0) === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No packages found.</td></tr>
</Show>
<Show when={!packages.loading && !packages.error && (packages()?.length ?? 0) > 0}>
<For each={packages()}>
{(pkg) => (
<>
<tr>
<td style="font-weight:600;color:#0f172a">{pkg.name}</td>
<td style="color:#475569">{pkg.role}</td>
<td style="color:#475569">{pkg.tracecoin_amount}</td>
<td style="color:#475569">{(pkg.price_inr / 100).toFixed(2)}</td>
<td style="color:#475569">{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'}</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 ${pkg.is_active ? 'active' : ''}`}>
{pkg.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<button 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" onClick={() => startEdit(pkg)}>Edit</button>
<button
class={pkg.is_active ? 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors' : 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'}
disabled={togglingId() === pkg.id}
onClick={() => toggleActive(pkg)}
>
{togglingId() === pkg.id ? '...' : pkg.is_active ? 'Disable' : 'Enable'}
</button>
</div>
</td>
</tr>
<Show when={editingId() === pkg.id}>
<tr>
<td colspan="7" style="background:#f8fafc;padding:16px">
<Show when={editError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{editError()}</div>
</Show>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={editName()}
onInput={(e) => setEditName(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
/>
</div>
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">TraceCoins</label>
<input
type="number"
value={editTracecoins()}
onInput={(e) => setEditTracecoins(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div class="field">
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Price (paise)</label>
<input
type="number"
value={editPrice()}
onInput={(e) => setEditPrice(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div style="display:flex;gap:8px">
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={editSaving()} onClick={() => saveEdit(pkg.id)}>
{editSaving() ? 'Saving...' : 'Save'}
</button>
<button 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" onClick={cancelEdit}>Cancel</button>
</div>
</div>
</td>
</tr>
</Show>
</>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</Show>
{/* Create Package tab */}
<Show when={view() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:480px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Package</h2>
<Show when={cError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{cError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={cName()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Role</label>
<select
value={cRole()}
onChange={(e) => setCRole(e.currentTarget.value)}
required
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<For each={ROLES}>{(r) => <option value={r}>{r}</option>}</For>
</select>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">TraceCoins</label>
<input
type="number"
value={cTracecoins()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Price INR (paise, e.g. 49900 = 499)</label>
<input
type="number"
value={cPrice()}
onInput={(e) => 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"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Bonus Percentage (optional, e.g. 10 = 10% bonus coins)</label>
<input
type="number"
value={cBonus()}
onInput={(e) => 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"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={cSaving()}>
{cSaving() ? 'Creating...' : 'Create Package'}
</button>
</div>
</form>
</section>
</Show>
</AdminShell>
);
}

View file

@ -46,75 +46,77 @@ export default function ReportPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Report Management</h1>
<p class="mt-1 text-sm text-gray-500">View platform analytics and generate reports.</p>
<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">Report Management</h1>
<p class="text-sm text-gray-500 mt-0.5">View platform analytics and generate reports.</p>
</div>
<div class="flex-1 p-6">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">Date Range</h2>
<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="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" 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:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.total_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">New Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.new_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Active Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.active_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Revenue</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">
{revenueReport()?.total_revenue != null ? `${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : ''}
</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Orders</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_orders ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">TraceCoins Sold</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_tracecoins_sold ?? '—'}</p>
</div>
</div>
</Show>
</div>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">Date Range</h2>
<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="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" 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:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.total_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">New Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.new_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Active Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.active_users ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Revenue</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">
{revenueReport()?.total_revenue != null ? `${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : ''}
</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Orders</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_orders ?? '—'}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">TraceCoins Sold</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_tracecoins_sold ?? '—'}</p>
</div>
</div>
</Show>
</AdminShell>
);
}

View file

@ -120,199 +120,205 @@ export default function ReviewPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Review Management</h1>
<p class="mt-1 text-sm text-gray-500">Moderate platform reviews</p>
<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">Review Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Moderate platform reviews</p>
</div>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${activeTab() === 'list' ? ' active' : ''}`}
onClick={() => setActiveTab('list')}
>
Reviews
</button>
<button
type="button"
class={`admin-tab${activeTab() === 'create' ? ' active' : ''}`}
onClick={() => { resetForm(); setActiveTab('create'); }}
>
Create Review
</button>
</div>
<Show when={activeTab() === 'list'}>
<div style="margin-bottom:16px">
<input
type="text"
placeholder="Search by reviewer, title, type, or status..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:320px"
/>
{/* Tabs */}
<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={activeTab() === 'list'
? '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={() => setActiveTab('list')}
>
Reviews
</button>
<button
type="button"
class={activeTab() === 'create'
? '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={() => { resetForm(); setActiveTab('create'); }}
>
Create Review
</button>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Reviewer</th>
<th>Type</th>
<th>Rating</th>
<th>Title</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={reviews.loading}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!reviews.loading && reviews.error}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!reviews.loading && !reviews.error && filteredReviews().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No reviews found.</td></tr>
</Show>
<Show when={!reviews.loading && !reviews.error && filteredReviews().length > 0}>
<For each={filteredReviews()}>
{(item) => {
const subjectType = item.subject_type || '—';
const isPublished = item.status === 'PUBLISHED';
return (
<tr>
<td style="font-weight:600;color:#0f172a">{item.reviewer_name || item.reviewer_id || '—'}</td>
<td>
<span style={`${typeBadgeStyle(subjectType)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid;display:inline-block`}>
{subjectType}
</span>
</td>
<td style="color:#475569">
{item.rating != null ? `${item.rating}` : '—'}
</td>
<td style="color:#475569">{item.title || '—'}</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 ${isPublished ? 'active' : ''}`}
style={isPublished ? '' : 'background:#f1f5f9;color:#475569;border-color:#e2e8f0'}
>
{isPublished ? 'Published' : 'Hidden'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<Show when={isPublished}>
<button
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"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'HIDDEN')}
>
{toggling() === item.id ? '...' : 'Hide'}
</button>
</Show>
<Show when={!isPublished}>
<button
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'PUBLISHED')}
>
{toggling() === item.id ? '...' : 'Publish'}
</button>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">Create Review</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
<div class="flex-1 p-6">
<Show when={activeTab() === 'list'}>
<div style="margin-bottom:16px">
<input
type="text"
placeholder="Search by reviewer, title, type, or status..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:320px"
/>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Reviewer</th>
<th>Type</th>
<th>Rating</th>
<th>Title</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={reviews.loading}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!reviews.loading && reviews.error}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!reviews.loading && !reviews.error && filteredReviews().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No reviews found.</td></tr>
</Show>
<Show when={!reviews.loading && !reviews.error && filteredReviews().length > 0}>
<For each={filteredReviews()}>
{(item) => {
const subjectType = item.subject_type || '—';
const isPublished = item.status === 'PUBLISHED';
return (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{item.reviewer_name || item.reviewer_id || '—'}</td>
<td>
<span style={`${typeBadgeStyle(subjectType)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid;display:inline-block`}>
{subjectType}
</span>
</td>
<td class="text-slate-500">
{item.rating != null ? `${item.rating}` : '—'}
</td>
<td class="text-slate-500">{item.title || '—'}</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 ${isPublished ? 'active' : ''}`}
style={isPublished ? '' : 'background:#f1f5f9;color:#475569;border-color:#e2e8f0'}
>
{isPublished ? 'Published' : 'Hidden'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<Show when={isPublished}>
<button
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"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'HIDDEN')}
>
{toggling() === item.id ? '...' : 'Hide'}
</button>
</Show>
<Show when={!isPublished}>
<button
class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'PUBLISHED')}
>
{toggling() === item.id ? '...' : 'Publish'}
</button>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Subject Type</label>
<select
value={form().subject_type}
onChange={(e) => setForm({ ...form(), subject_type: e.currentTarget.value })}
>
<option value="PLATFORM">PLATFORM</option>
<option value="PACKAGE">PACKAGE</option>
<option value="SUPPORT">SUPPORT</option>
</select>
</div>
<div class="field">
<label>Subject ID</label>
<input
type="text"
value={form().subject_id}
onInput={(e) => setForm({ ...form(), subject_id: e.currentTarget.value })}
placeholder="e.g. general or package UUID"
/>
</div>
<div class="field">
<label>Reviewer Name</label>
<input
type="text"
value={form().reviewer_name}
onInput={(e) => setForm({ ...form(), reviewer_name: e.currentTarget.value })}
required
placeholder="Full name"
/>
</div>
<div class="field">
<label>Rating</label>
<select
value={form().rating}
onChange={(e) => setForm({ ...form(), rating: Number(e.currentTarget.value) })}
>
<option value={5}>5 Excellent</option>
<option value={4}>4 Good</option>
<option value={3}>3 Average</option>
<option value={2}>2 Poor</option>
<option value={1}>1 Terrible</option>
</select>
</div>
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="Review title"
/>
</div>
<div class="field">
<label>Comment</label>
<textarea
value={form().comment}
onInput={(e) => setForm({ ...form(), comment: e.currentTarget.value })}
rows={4}
placeholder="Detailed review..."
style="resize:vertical"
/>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Review'}
</button>
</div>
</form>
</section>
</Show>
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">Create Review</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label>Subject Type</label>
<select
value={form().subject_type}
onChange={(e) => setForm({ ...form(), subject_type: e.currentTarget.value })}
>
<option value="PLATFORM">PLATFORM</option>
<option value="PACKAGE">PACKAGE</option>
<option value="SUPPORT">SUPPORT</option>
</select>
</div>
<div class="field">
<label>Subject ID</label>
<input
type="text"
value={form().subject_id}
onInput={(e) => setForm({ ...form(), subject_id: e.currentTarget.value })}
placeholder="e.g. general or package UUID"
/>
</div>
<div class="field">
<label>Reviewer Name</label>
<input
type="text"
value={form().reviewer_name}
onInput={(e) => setForm({ ...form(), reviewer_name: e.currentTarget.value })}
required
placeholder="Full name"
/>
</div>
<div class="field">
<label>Rating</label>
<select
value={form().rating}
onChange={(e) => setForm({ ...form(), rating: Number(e.currentTarget.value) })}
>
<option value={5}>5 Excellent</option>
<option value={4}>4 Good</option>
<option value={3}>3 Average</option>
<option value={2}>2 Poor</option>
<option value={1}>1 Terrible</option>
</select>
</div>
<div class="field">
<label>Title</label>
<input
type="text"
value={form().title}
onInput={(e) => setForm({ ...form(), title: e.currentTarget.value })}
required
placeholder="Review title"
/>
</div>
<div class="field">
<label>Comment</label>
<textarea
value={form().comment}
onInput={(e) => setForm({ ...form(), comment: e.currentTarget.value })}
rows={4}
placeholder="Detailed review..."
style="resize:vertical"
/>
</div>
<div class="actions">
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Review'}
</button>
</div>
</form>
</section>
</Show>
</div>
</div>
</AdminShell>
);
}

View file

@ -158,213 +158,219 @@ export default function SupportPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Support Management</h1>
<p class="mt-1 text-sm text-gray-500">Handle platform issues and customer queries</p>
<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">Support Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Handle platform issues and customer queries</p>
</div>
</div>
{/* Stats bar */}
<div style="display:flex;gap:12px;margin-bottom:16px">
<For each={statCards}>
{(card) => (
<div style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 20px;text-align:center">
<div style="font-size:24px;font-weight:700">{card.getValue()}</div>
<div style="font-size:12px;color:#6b7280">{card.label}</div>
</div>
)}
</For>
</div>
{/* Tabs */}
<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={activeTab() === 'queue'
? '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={() => setActiveTab('queue')}
>
Support Queue
</button>
<button
type="button"
class={activeTab() === 'create'
? '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={() => setActiveTab('create')}
>
Create Case
</button>
</div>
{/* Tabs */}
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
<button
type="button"
class={`admin-tab${activeTab() === 'queue' ? ' active' : ''}`}
onClick={() => setActiveTab('queue')}
>
Support Queue
</button>
<button
type="button"
class={`admin-tab${activeTab() === 'create' ? ' active' : ''}`}
onClick={() => setActiveTab('create')}
>
Create Case
</button>
</div>
{/* Support Queue Tab */}
<Show when={activeTab() === 'queue'}>
<div style="display:flex;flex-direction:column;gap:16px">
<div style="display:flex;align-items:center;justify-content:flex-end">
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value as typeof statusFilter extends () => infer R ? R : never)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<option value="all">All statuses</option>
<For each={STATUS_OPTIONS}>
{(s) => <option value={s}>{formatValue(s)}</option>}
</For>
</select>
<div class="flex-1 p-6">
{/* Stats bar */}
<div style="display:flex;gap:12px;margin-bottom:16px">
<For each={statCards}>
{(card) => (
<div style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 20px;text-align:center">
<div style="font-size:24px;font-weight:700">{card.getValue()}</div>
<div style="font-size:12px;color:#6b7280">{card.label}</div>
</div>
)}
</For>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Issue</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Requester</th>
<th>Updated At</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={cases.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!cases.loading && cases.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load cases.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No support cases found.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length > 0}>
<For each={filteredCases()}>
{(item) => (
<tr style="cursor:pointer" onClick={() => {}}>
<td>
<div style="font-weight:600;color:#0f172a">{item.title}</div>
<div style="font-size:12px;color:#64748b;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{item.description}</div>
</td>
<td>
<span style={`${BADGE_STYLE};${typeBadgeStyle(item.type)}`}>{formatValue(item.type)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${priorityBadgeStyle(item.priority)}`}>{formatValue(item.priority)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${statusBadgeStyle(item.status)}`}>{formatValue(item.status)}</span>
</td>
<td>
<div style="font-size:13px">{item.requesterName || '—'}</div>
<div style="font-size:11px;color:#64748b">{item.requesterEmail || ''}</div>
</td>
<td style="font-size:12px;color:#64748b">
{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/support/${item.id}`}>View</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</div>
</Show>
{/* Support Queue Tab */}
<Show when={activeTab() === 'queue'}>
<div style="display:flex;flex-direction:column;gap:16px">
<div style="display:flex;align-items:center;justify-content:flex-end">
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value as typeof statusFilter extends () => infer R ? R : never)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<option value="all">All statuses</option>
<For each={STATUS_OPTIONS}>
{(s) => <option value={s}>{formatValue(s)}</option>}
</For>
</select>
</div>
{/* Create Case Tab */}
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:600px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Create Support Case</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Create an internal support record for platform issues, customer concerns, or compensation-related reviews.
</p>
<Show when={createSuccess()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:14px;color:#15803d;font-weight:600">
{createSuccess()}
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Issue</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Requester</th>
<th>Updated At</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={cases.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!cases.loading && cases.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load cases.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No support cases found.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length > 0}>
<For each={filteredCases()}>
{(item) => (
<tr class="hover:bg-slate-50" style="cursor:pointer" onClick={() => {}}>
<td>
<div class="font-semibold text-slate-900">{item.title}</div>
<div style="font-size:12px;color:#64748b;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{item.description}</div>
</td>
<td>
<span style={`${BADGE_STYLE};${typeBadgeStyle(item.type)}`}>{formatValue(item.type)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${priorityBadgeStyle(item.priority)}`}>{formatValue(item.priority)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${statusBadgeStyle(item.status)}`}>{formatValue(item.status)}</span>
</td>
<td>
<div style="font-size:13px">{item.requesterName || '—'}</div>
<div style="font-size:11px;color:#64748b">{item.requesterEmail || ''}</div>
</td>
<td class="text-slate-500" style="font-size:12px">
{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/support/${item.id}`}>View</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</Show>
<Show when={createError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{createError()}</div>
{/* Create Case Tab */}
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:600px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Create Support Case</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Create an internal support record for platform issues, customer concerns, or compensation-related reviews.
</p>
<Show when={createSuccess()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:14px;color:#15803d;font-weight:600">
{createSuccess()}
</div>
</Show>
<Show when={createError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{createError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
<input
type="text"
required
value={fTitle()}
onInput={(e) => setFTitle(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
<textarea
required
rows="4"
value={fDesc()}
onInput={(e) => setFDesc(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical;box-sizing:border-box"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
value={fType()}
onChange={(e) => setFType(e.currentTarget.value as SupportCase['type'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={TYPE_OPTIONS}>
{(t) => <option value={t}>{formatValue(t)}</option>}
</For>
</select>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Priority</label>
<select
value={fPriority()}
onChange={(e) => setFPriority(e.currentTarget.value as SupportCase['priority'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={PRIORITY_OPTIONS}>
{(p) => <option value={p}>{formatValue(p)}</option>}
</For>
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Name</label>
<input
type="text"
value={fRequesterName()}
onInput={(e) => setFRequesterName(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Email</label>
<input
type="email"
value={fRequesterEmail()}
onInput={(e) => setFRequesterEmail(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={createLoading()}>
{createLoading() ? 'Creating...' : 'Create Support Case'}
</button>
</div>
</form>
</section>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
<input
type="text"
required
value={fTitle()}
onInput={(e) => setFTitle(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
<textarea
required
rows="4"
value={fDesc()}
onInput={(e) => setFDesc(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical;box-sizing:border-box"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
value={fType()}
onChange={(e) => setFType(e.currentTarget.value as SupportCase['type'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={TYPE_OPTIONS}>
{(t) => <option value={t}>{formatValue(t)}</option>}
</For>
</select>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Priority</label>
<select
value={fPriority()}
onChange={(e) => setFPriority(e.currentTarget.value as SupportCase['priority'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={PRIORITY_OPTIONS}>
{(p) => <option value={p}>{formatValue(p)}</option>}
</For>
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Name</label>
<input
type="text"
value={fRequesterName()}
onInput={(e) => setFRequesterName(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Email</label>
<input
type="email"
value={fRequesterEmail()}
onInput={(e) => setFRequesterEmail(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={createLoading()}>
{createLoading() ? 'Creating...' : 'Create Support Case'}
</button>
</div>
</form>
</section>
</Show>
</div>
</div>
</AdminShell>
);
}

View file

@ -64,116 +64,120 @@ export default function TaxPage() {
return (
<AdminShell>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Tax Management</h1>
<p class="mt-1 text-sm text-gray-500">Configure tax rates for platform transactions.</p>
<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 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Tax Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Configure tax rates for platform transactions.</p>
</div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" onClick={() => setShowForm(!showForm())}>
{showForm() ? 'Cancel' : 'Add Tax'}
</button>
</div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => setShowForm(!showForm())}>
{showForm() ? 'Cancel' : 'Add Tax'}
</button>
</div>
<Show when={showForm()}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">New Tax</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
<div class="flex-1 p-6">
<Show when={showForm()}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">New Tax</h2>
<Show when={formError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:12px;max-width:400px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
required
style="width:100%;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">Rate (%)</label>
<input
type="number"
value={rate()}
onInput={(e) => setRate(e.currentTarget.value)}
required
min="0"
max="100"
step="0.01"
style="width:100%;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">Description</label>
<input
type="text"
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Tax'}
</button>
</div>
</form>
</section>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:12px;max-width:400px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
required
style="width:100%;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">Rate (%)</label>
<input
type="number"
value={rate()}
onInput={(e) => setRate(e.currentTarget.value)}
required
min="0"
max="100"
step="0.01"
style="width:100%;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">Description</label>
<input
type="text"
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
/>
</div>
<div>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Tax'}
</button>
</div>
</form>
</section>
</Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Rate (%)</th>
<th>Description</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={taxes.loading}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!taxes.loading && taxes.error}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!taxes.loading && !taxes.error && taxes()?.length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No records found.</td></tr>
</Show>
<Show when={!taxes.loading && !taxes.error && (taxes()?.length ?? 0) > 0}>
{taxes()!.map((item) => (
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<td style="font-weight:600;color:#0f172a">{item.name}</td>
<td style="color:#475569">{item.rate}%</td>
<td style="color:#475569">{item.description || '—'}</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.is_active !== false ? 'active' : ''}`}>
{item.is_active !== false ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/tax/${item.id}/edit`}>Edit</a>
<button
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
{deleting() === item.id ? '...' : 'Delete'}
</button>
</div>
</td>
<th>Name</th>
<th>Rate (%)</th>
<th>Description</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
))}
</Show>
</tbody>
</table>
</thead>
<tbody>
<Show when={taxes.loading}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!taxes.loading && taxes.error}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!taxes.loading && !taxes.error && taxes()?.length === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No records found.</td></tr>
</Show>
<Show when={!taxes.loading && !taxes.error && (taxes()?.length ?? 0) > 0}>
{taxes()!.map((item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{item.name}</td>
<td class="text-slate-500">{item.rate}%</td>
<td class="text-slate-500">{item.description || '—'}</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.is_active !== false ? 'active' : ''}`}>
{item.is_active !== false ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<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={`/admin/tax/${item.id}/edit`}>Edit</a>
<button
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
{deleting() === item.id ? '...' : 'Delete'}
</button>
</div>
</td>
</tr>
))}
</Show>
</tbody>
</table>
</div>
</div>
</div>
</section>
</div>
</AdminShell>
);
}