nxtgauge-admin-solid/src/routes/admin/discount.tsx
Ashwin Kumar 04a1079f68 feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00

339 lines
12 KiB
TypeScript

import { createResource, createSignal, Show, For } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
const ROLE_OPTIONS = [
'company',
'customer',
'job_seeker',
'photographer',
'video_editor',
'graphic_designer',
'social_media_manager',
'fitness_trainer',
'catering_services',
'makeup_artist',
'tutor',
'developer',
];
type Discount = {
id: string;
title: string;
scope: 'ROLE' | 'PACKAGE';
role_key?: string;
package_id?: string;
type: 'PERCENT' | 'FIXED';
value: number;
is_active: boolean;
};
type Package = {
id: string;
name: string;
role_key?: string;
};
async function loadDiscounts(): Promise<Discount[]> {
try {
const res = await fetch(`${API}/api/admin/discounts`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.discounts || []);
} catch {
return [];
}
}
async function loadPackages(): Promise<Package[]> {
try {
const res = await fetch(`${API}/api/admin/tracecoin-packages`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.packages || []);
} catch {
return [];
}
}
const defaultForm = () => ({
id: '',
title: '',
scope: 'ROLE' as 'ROLE' | 'PACKAGE',
role_key: 'company',
package_id: '',
type: 'PERCENT' as 'PERCENT' | 'FIXED',
value: 5,
});
export default function DiscountPage() {
const [discounts, { refetch: refetchDiscounts }] = createResource(loadDiscounts);
const [packages] = createResource(loadPackages);
const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list');
const [form, setForm] = createSignal(defaultForm());
const [saving, setSaving] = createSignal(false);
const [toggling, setToggling] = createSignal('');
const [formError, setFormError] = createSignal('');
const resetForm = () => {
setForm(defaultForm());
setFormError('');
};
const getTarget = (item: Discount) => {
if (item.scope === 'ROLE') return item.role_key || '—';
const pkgs = packages();
if (pkgs) {
const pkg = pkgs.find((p) => p.id === item.package_id);
if (pkg) return pkg.name;
}
return item.package_id || '—';
};
const handleSave = async (e: Event) => {
e.preventDefault();
try {
setSaving(true);
setFormError('');
const f = form();
const body: Record<string, unknown> = {
title: f.title,
scope: f.scope,
type: f.type,
value: Number(f.value),
};
if (f.scope === 'ROLE') {
body.role_key = f.role_key;
} else {
body.package_id = f.package_id;
}
const url = f.id ? `${API}/api/admin/discounts/${f.id}` : `${API}/api/admin/discounts`;
const method = f.id ? 'PATCH' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Failed to save discount');
resetForm();
refetchDiscounts();
setActiveTab('list');
} catch (err: unknown) {
setFormError(err instanceof Error ? err.message : 'Failed to save');
} finally {
setSaving(false);
}
};
const handleToggle = async (discount: Discount) => {
try {
setToggling(discount.id);
const res = await fetch(`${API}/api/admin/discounts/${discount.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: !discount.is_active }),
});
if (!res.ok) throw new Error('Failed to toggle');
refetchDiscounts();
} catch {
// ignore
} finally {
setToggling('');
}
};
return (
<AdminShell>
<div class="page-actions">
<div>
<h1 class="page-title">Discount Management</h1>
<p class="page-subtitle">Automatic discounts applied before coupons</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')}
>
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="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<thead>
<tr>
<th>Title</th>
<th>Scope</th>
<th>Target</th>
<th>Type</th>
<th>Value</th>
<th>Status</th>
<th class="align-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={`status-chip ${item.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<button
class="btn"
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="card" 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="error-box" 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="field-grid-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="btn navy" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Discount' : 'Save Discount')}
</button>
<Show when={form().id}>
<button type="button" class="btn" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</AdminShell>
);
}