nxtgauge-admin-solid/src/routes/admin/coupon.tsx

336 lines
12 KiB
TypeScript
Raw Normal View History

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 Coupon = {
id: string;
code: string;
title: string;
type: 'PERCENT' | 'FIXED';
value: number;
min_order_amount: number;
used_count: number;
usage_limit?: number;
is_active: boolean;
role_keys: string[];
};
async function loadCoupons(): Promise<Coupon[]> {
try {
const res = await fetch(`${API}/api/admin/coupons`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.coupons || []);
} catch {
return [];
}
}
const defaultForm = () => ({
id: '',
code: '',
title: '',
type: 'PERCENT' as 'PERCENT' | 'FIXED',
value: 10,
min_order_amount: 0,
max_uses: '',
role_keys: ['company', 'customer'] as string[],
});
export default function CouponPage() {
const [coupons, { refetch }] = createResource(loadCoupons);
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 startEdit = (coupon: Coupon) => {
setForm({
id: coupon.id,
code: coupon.code,
title: coupon.title || '',
type: coupon.type,
value: coupon.value,
min_order_amount: coupon.min_order_amount || 0,
max_uses: coupon.usage_limit != null ? String(coupon.usage_limit) : '',
role_keys: Array.isArray(coupon.role_keys) ? coupon.role_keys : [],
});
setActiveTab('create');
};
const toggleRole = (role: string) => {
const current = form().role_keys;
if (current.includes(role)) {
setForm({ ...form(), role_keys: current.filter((r) => r !== role) });
} else {
setForm({ ...form(), role_keys: [...current, role] });
}
};
const handleSave = async (e: Event) => {
e.preventDefault();
try {
setSaving(true);
setFormError('');
const f = form();
const body: Record<string, unknown> = {
code: f.code.toUpperCase(),
title: f.title,
type: f.type,
value: Number(f.value),
min_order_amount: Number(f.min_order_amount),
role_keys: f.role_keys,
};
if (f.max_uses) body.max_uses = Number(f.max_uses);
const url = f.id ? `${API}/api/admin/coupons/${f.id}` : `${API}/api/admin/coupons`;
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 coupon');
resetForm();
refetch();
setActiveTab('list');
} catch (err: unknown) {
setFormError(err instanceof Error ? err.message : 'Failed to save');
} finally {
setSaving(false);
}
};
const handleToggle = async (coupon: Coupon) => {
try {
setToggling(coupon.id);
const res = await fetch(`${API}/api/admin/coupons/${coupon.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: !coupon.is_active }),
});
if (!res.ok) throw new Error('Failed to toggle');
refetch();
} catch {
// ignore
} finally {
setToggling('');
}
};
return (
<AdminShell>
<div class="page-actions">
<div>
<h1 class="page-title">Coupon Management</h1>
<p class="page-subtitle">Reusable coupon codes for package checkout</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')}
>
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="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<thead>
<tr>
<th>Code</th>
<th>Title</th>
<th>Type</th>
<th>Value</th>
<th>Max Uses</th>
<th>Status</th>
<th class="align-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={`status-chip ${item.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => startEdit(item)}>Edit</button>
<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 Coupon' : 'Create Coupon'}</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>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="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="field-grid-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="btn navy" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Coupon' : 'Save Coupon')}
</button>
<Show when={form().id}>
<button type="button" class="btn" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>
</section>
</Show>
</AdminShell>
);
}