feat: Add AI Credits admin UI (Task 2)

- pricing.tsx: AI Credit Packages tab with list, create, edit, toggle
- credit.tsx: AI Credits panel with Balance/Ledger/Adjust/Reconcile sub-tabs
- Coupon code support in order creation
- Integration with backend admin endpoints
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-06 01:48:13 +05:30
parent 575a5749f6
commit c383382f93
2 changed files with 804 additions and 5 deletions

View file

@ -34,7 +34,36 @@ type ReconcileRow = {
};
export default function CreditPage() {
const [activeTab, setActiveTab] = createSignal<'ledger' | 'adjust' | 'reconcile'>('ledger');
const [activeTab, setActiveTab] = createSignal<'ledger' | 'adjust' | 'reconcile' | 'ai_credits'>('ledger');
// AI Credits sub-tab state
const [aiCreditsSubTab, setAiCreditsSubTab] = createSignal<'balance' | 'adjust' | 'reconcile'>('balance');
const [aiUserId, setAiUserId] = createSignal('');
const [aiSearchedUserId, setAiSearchedUserId] = createSignal('');
const [aiBalance, setAiBalance] = createSignal<any>(null);
const [aiLedger, setAiLedger] = createSignal<any[]>([]);
const [aiSearchLoading, setAiSearchLoading] = createSignal(false);
const [aiSearchError, setAiSearchError] = createSignal('');
const [aiLedgerPage, setAiLedgerPage] = createSignal(1);
const [aiLedgerLimit, setAiLedgerLimit] = createSignal(20);
const [aiLedgerTotal, setAiLedgerTotal] = createSignal(0);
// AI Adjust state
const [aiAdjUserId, setAiAdjUserId] = createSignal('');
const [aiAdjAmount, setAiAdjAmount] = createSignal(1);
const [aiAdjType, setAiAdjType] = createSignal<'ADD' | 'DEDUCT'>('ADD');
const [aiAdjReason, setAiAdjReason] = createSignal('');
const [aiAdjIdempotencyKey, setAiAdjIdempotencyKey] = createSignal('');
const [aiAdjLoading, setAiAdjLoading] = createSignal(false);
const [aiAdjSuccess, setAiAdjSuccess] = createSignal('');
const [aiAdjError, setAiAdjError] = createSignal('');
// AI Reconcile state
const [aiReconFrom, setAiReconFrom] = createSignal('');
const [aiReconTo, setAiReconTo] = createSignal('');
const [aiReconLoading, setAiReconLoading] = createSignal(false);
const [aiReconResults, setAiReconResults] = createSignal<any>(null);
const [aiReconError, setAiReconError] = createSignal('');
// Balance & Ledger tab state
const [userId, setUserId] = createSignal('');
@ -149,12 +178,101 @@ export default function CreditPage() {
}
};
const tabs: { key: 'ledger' | 'adjust' | 'reconcile'; label: string }[] = [
const tabs: { key: 'ledger' | 'adjust' | 'reconcile' | 'ai_credits'; label: string }[] = [
{ key: 'ledger', label: 'Balance & Ledger' },
{ key: 'adjust', label: 'Reward / Deduct' },
{ key: 'reconcile', label: 'Reconcile' },
{ key: 'ai_credits', label: 'AI Credits' },
];
// AI Credits handlers
const handleAiSearch = async () => {
const uid = aiUserId().trim();
if (!uid) return;
setAiSearchLoading(true);
setAiSearchError('');
setAiBalance(null);
setAiLedger([]);
setAiSearchedUserId(uid);
try {
const [balRes, ledRes] = await Promise.all([
fetch(`${API}/api/admin/ai-credits/balance?userId=${encodeURIComponent(uid)}`, { headers: authHeaders(), credentials: 'include' }),
fetch(`${API}/api/admin/ai-credits/ledger?userId=${encodeURIComponent(uid)}&page=${aiLedgerPage()}&limit=${aiLedgerLimit()}`, { headers: authHeaders(), credentials: 'include' }),
]);
if (!balRes.ok || !ledRes.ok) throw new Error('Failed to fetch');
const balData = await balRes.json();
const ledData = await ledRes.json();
setAiBalance(balData);
setAiLedger(ledData.data ?? []);
setAiLedgerTotal(ledData.total ?? 0);
} catch {
setAiSearchError('Failed to fetch AI credits data for this user ID.');
setAiBalance(null);
setAiLedger([]);
} finally {
setAiSearchLoading(false);
}
};
const handleAiAdjust = async (e: Event) => {
e.preventDefault();
setAiAdjLoading(true);
setAiAdjSuccess('');
setAiAdjError('');
try {
// Generate idempotency key if not provided
const idempotencyKey = aiAdjIdempotencyKey().trim() || crypto.randomUUID();
const res = await fetch(`${API}/api/admin/ai-credits/adjust`, {
method: 'POST',
headers: authHeaders(),
credentials: 'include',
body: JSON.stringify({
user_id: aiAdjUserId(),
amount: aiAdjAmount(),
type: aiAdjType(),
reason: aiAdjReason(),
idempotency_key: idempotencyKey,
}),
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error((d as any).error || 'Failed to adjust credits');
}
setAiAdjSuccess('AI credits adjusted successfully!');
setAiAdjUserId('');
setAiAdjAmount(1);
setAiAdjType('ADD');
setAiAdjReason('');
setAiAdjIdempotencyKey('');
} catch (err: any) {
setAiAdjError(err.message || 'Failed to adjust credits');
} finally {
setAiAdjLoading(false);
}
};
const handleAiReconcile = async (e: Event) => {
e.preventDefault();
setAiReconLoading(true);
setAiReconError('');
setAiReconResults(null);
try {
const fromDate = new Date(aiReconFrom()).toISOString();
const toDate = new Date(aiReconTo()).toISOString();
const res = await fetch(
`${API}/api/admin/ai-credits/reconcile?from=${encodeURIComponent(fromDate)}&to=${encodeURIComponent(toDate)}`,
{ headers: authHeaders(), credentials: 'include' }
);
if (!res.ok) throw new Error('Failed to reconcile');
const data = await res.json();
setAiReconResults(data);
} catch (err: any) {
setAiReconError(err.message || 'Failed to reconcile');
} finally {
setAiReconLoading(false);
}
};
const filteredLedger = createMemo(() => {
let data = ledger();
const q = ledgerSearch().toLowerCase().trim();
@ -518,6 +636,307 @@ export default function CreditPage() {
</Show>
</div>
</Show>
{/* AI Credits Tab */}
<Show when={activeTab() === 'ai_credits'}>
<div>
{/* AI Credits Sub-tabs */}
<div class="bg-white border-b border-gray-200 px-0 flex items-center gap-6 mb-6">
{(['balance', 'adjust', 'reconcile'] as const).map((t) => (
<button
type="button"
class={
aiCreditsSubTab() === t
? "py-2 border-b-2 border-orange-500 text-orange-600 text-sm font-medium"
: "py-2 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors"
}
onClick={() => setAiCreditsSubTab(t)}
>
{t === 'balance' ? 'Balance & Ledger' : t === 'adjust' ? 'Reward / Deduct' : 'Reconcile'}
</button>
))}
</div>
{/* AI Balance & Ledger */}
<Show when={aiCreditsSubTab() === 'balance'}>
<div style="display:flex;flex-direction:column;gap:24px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Search AI Credits Balance</h2>
<div style="display:flex;gap:10px">
<input
type="text"
placeholder="Enter User ID..."
value={aiUserId()}
onInput={(e) => setAiUserId(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAiSearch()}
class="rounded-lg border border-gray-200 px-3 py-2 text-sm"
style="flex:1"
/>
<button
class="btn-primary"
onClick={handleAiSearch}
disabled={aiSearchLoading()}
>
{aiSearchLoading() ? 'Searching...' : 'Search'}
</button>
</div>
<Show when={aiSearchError()}>
<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">{aiSearchError()}</div>
</Show>
</section>
<Show when={aiBalance() !== null}>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px">
<div style="background:#7c3aed;border-radius:12px;padding:24px;color:#fff">
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#ddd6fe;margin:0 0 8px">Available AI Credits</p>
<p style="font-size:36px;font-weight:900;margin:0">{aiBalance().available_credits ?? 0}</p>
<p style="font-size:12px;color:#ddd6fe;margin:8px 0 0">User: {aiSearchedUserId()}</p>
<div style="margin-top:16px;border-top:1px solid rgba(255,255,255,0.2);padding-top:12px">
<p style="font-size:11px;color:#ddd6fe;margin:0 0 4px">Monthly: {aiBalance().monthly_credits_used ?? 0} / {aiBalance().monthly_credits_total ?? 0}</p>
<p style="font-size:11px;color:#ddd6fe;margin:0 0 4px">Purchased: {aiBalance().purchased_credits_used ?? 0} / {aiBalance().purchased_credits_total ?? 0}</p>
<p style="font-size:11px;color:#ddd6fe;margin:0">Bonus: {aiBalance().bonus_credits_used ?? 0} / {aiBalance().bonus_credits_total ?? 0}</p>
</div>
</div>
<div class="table-card" style="overflow:hidden">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#0f172a">AI Credits Ledger</h3>
<Show when={aiLedger().length === 0}>
<p style="text-align:center;padding:32px;color:#94a3b8;font-style:italic">No transactions found for this account.</p>
</Show>
<Show when={aiLedger().length > 0}>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Type</th>
<th>Credits</th>
<th>Balance After</th>
<th>Description</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<For each={aiLedger()}>
{(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.credits > 0 ? '#dcfce7' : '#fee2e2'};color:${entry.credits > 0 ? '#15803d' : '#b91c1c'}`}
>
{entry.credits > 0 ? 'CREDIT' : 'DEBIT'}
</span>
</td>
<td class="font-semibold text-slate-900" style={entry.credits > 0 ? 'color:#16a34a' : 'color:#dc2626'}>
{entry.credits > 0 ? '+' : ''}{entry.credits}
</td>
<td class="text-slate-700">{entry.balance_after}</td>
<td class="text-slate-500" style="font-size:12px">{entry.description || '—'}</td>
<td class="text-slate-500" style="font-size:12px">
{entry.created_at ? new Date(entry.created_at).toLocaleString() : '—'}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
<div style="padding:10px 16px;font-size:12px;color:#64748b;border-top:1px solid #f1f5f9">
Showing {aiLedger().length} of {aiLedgerTotal()} entries (Page {aiLedgerPage()})
</div>
</Show>
</div>
</div>
</Show>
</div>
</Show>
{/* AI Reward / Deduct */}
<Show when={aiCreditsSubTab() === '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 AI Credits</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Use this to reward AI credits for a valid support case, process a refund, or correct a balance manually.
</p>
<Show when={aiAdjSuccess()}>
<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">
{aiAdjSuccess()}
</div>
</Show>
<Show when={aiAdjError()}>
<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">{aiAdjError()}</div>
</Show>
<form onSubmit={handleAiAdjust} 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={aiAdjUserId()}
onInput={(e) => setAiAdjUserId(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={aiAdjAmount()}
onInput={(e) => setAiAdjAmount(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={aiAdjType()}
onChange={(e) => setAiAdjType(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 <span style="color:#dc2626">*</span></label>
<input
type="text"
required
value={aiAdjReason()}
onInput={(e) => setAiAdjReason(e.currentTarget.value)}
placeholder="Required: Explain why credits are being adjusted"
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">Idempotency Key <span style="font-weight:400;color:#94a3b8">(optional)</span></label>
<input
type="text"
value={aiAdjIdempotencyKey()}
onInput={(e) => setAiAdjIdempotencyKey(e.currentTarget.value)}
placeholder="Leave empty to auto-generate"
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
<p style="font-size:11px;color:#64748b;margin-top:4px">Prevents duplicate adjustments if resubmitted</p>
</div>
<div>
<button class="btn-primary" type="submit" disabled={aiAdjLoading()}>
{aiAdjLoading() ? 'Adjusting...' : 'Apply Adjustment'}
</button>
</div>
</form>
</section>
</Show>
{/* AI Reconcile */}
<Show when={aiCreditsSubTab() === '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">AI Credits Reconciliation</h2>
<Show when={aiReconError()}>
<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">{aiReconError()}</div>
</Show>
<form onSubmit={handleAiReconcile} 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={aiReconFrom()}
onInput={(e) => setAiReconFrom(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={aiReconTo()}
onInput={(e) => setAiReconTo(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="btn-primary" type="submit" disabled={aiReconLoading()}>
{aiReconLoading() ? 'Running...' : 'Run Reconciliation'}
</button>
</div>
</form>
</section>
<Show when={aiReconResults() !== null}>
<div class="table-card">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#0f172a">Summary by Entry Type</h3>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Entry Type</th>
<th>Count</th>
<th>Total Credits</th>
</tr>
</thead>
<tbody>
<For each={aiReconResults()?.summary || []}>
{(row) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{row.entry_type}</td>
<td class="text-slate-700">{row.count}</td>
<td class="text-slate-700">{row.total_credits}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
<h3 style="margin:24px 0 16px;font-size:15px;font-weight:700;color:#0f172a">Drift Check</h3>
<Show when={(aiReconResults()?.drift_check?.issues_found ?? 0) === 0}>
<p style="color:#16a34a;font-weight:600;font-size:14px;padding:16px">No discrepancies found. All wallets reconciled correctly.</p>
</Show>
<Show when={(aiReconResults()?.drift_check?.issues_found ?? 0) > 0}>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>User</th>
<th>Ledger Sum</th>
<th>Computed</th>
<th>Drift</th>
</tr>
</thead>
<tbody>
<For each={aiReconResults()?.drift_check?.issues || []}>
{(row) => (
<tr class="hover:bg-slate-50">
<td>
<div class="font-semibold text-slate-900">{row.user_email}</div>
<div style="font-size:11px;color:#64748b;font-family:monospace">{row.user_id}</div>
</td>
<td class="text-slate-700">{row.ledger_sum}</td>
<td class="text-slate-700">{row.computed_available}</td>
<td style={row.drift !== 0 ? 'color:#dc2626;font-weight:700' : 'color:#16a34a'}>
{row.drift > 0 ? '+' : ''}{row.drift}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</div>
</Show>
</div>
</Show>
</div>
</Show>
</div>
</div>
);

View file

@ -37,6 +37,17 @@ type Package = {
is_expired?: boolean;
};
type AiCreditPackage = {
id: string;
name: string;
description?: string;
credits: number;
price_inr: number;
is_active: boolean;
created_at: string;
updated_at: string;
};
const PACKAGE_TYPES = [
{ value: "TRACECOIN_BUNDLE", label: "Tracecoin Bundle" },
{ value: "CONTACT_VIEWS", label: "Contact Views (Company)" },
@ -92,7 +103,7 @@ export default function PricingPage() {
const [rows, setRows] = createSignal<Package[]>([]);
const [loading, setLoading] = createSignal(true);
const [loadError, setLoadError] = createSignal("");
const [view, setView] = createSignal<"packages" | "create">("packages");
const [view, setView] = createSignal<"packages" | "create" | "ai_packages">("packages");
// Filters
const [search, setSearch] = createSignal("");
@ -125,6 +136,27 @@ export default function PricingPage() {
const [cError, setCError] = createSignal("");
const [roleDropdownOpen, setRoleDropdownOpen] = createSignal(false);
// AI Credit Packages state
const [aiPackages, setAiPackages] = createSignal<AiCreditPackage[]>([]);
const [aiPackagesLoading, setAiPackagesLoading] = createSignal(false);
const [aiPackagesError, setAiPackagesError] = createSignal("");
const [aiEditingId, setAiEditingId] = createSignal("");
const [aiEditName, setAiEditName] = createSignal("");
const [aiEditDescription, setAiEditDescription] = createSignal("");
const [aiEditCredits, setAiEditCredits] = createSignal("");
const [aiEditPrice, setAiEditPrice] = createSignal("");
const [aiEditSaving, setAiEditSaving] = createSignal(false);
const [aiEditError, setAiEditError] = createSignal("");
const [aiTogglingId, setAiTogglingId] = createSignal("");
// AI Create form
const [aiCName, setAiCName] = createSignal("");
const [aiCDescription, setAiCDescription] = createSignal("");
const [aiCCredits, setAiCCredits] = createSignal("");
const [aiCPrice, setAiCPrice] = createSignal("");
const [aiCSaving, setAiCSaving] = createSignal(false);
const [aiCError, setAiCError] = createSignal("");
const [aiCreateView, setAiCreateView] = createSignal(false);
const load = async () => {
setLoading(true);
setLoadError("");
@ -144,6 +176,114 @@ export default function PricingPage() {
}
};
// AI Credit Packages functions
const loadAiPackages = async () => {
setAiPackagesLoading(true);
setAiPackagesError("");
try {
const res = await fetch(`${API}/api/admin/ai-credits/packages`, {
headers: authHeaders(),
credentials: "include",
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
const data = await res.json();
setAiPackages(Array.isArray(data.packages) ? data.packages : []);
} catch (err: any) {
setAiPackagesError(err.message || "Could not load AI credit packages.");
setAiPackages([]);
} finally {
setAiPackagesLoading(false);
}
};
const startAiEdit = (pkg: AiCreditPackage) => {
setAiEditingId(pkg.id);
setAiEditName(pkg.name);
setAiEditDescription(pkg.description || "");
setAiEditCredits(String(pkg.credits));
setAiEditPrice(String(pkg.price_inr / 100)); // Convert paise to rupees for display
setAiEditError("");
};
const cancelAiEdit = () => {
setAiEditingId("");
setAiEditError("");
};
const saveAiEdit = async (id: string) => {
try {
setAiEditSaving(true);
setAiEditError("");
const priceInPaise = Math.round(Number(aiEditPrice()) * 100);
const res = await fetch(`${API}/api/admin/ai-credits/packages/${id}`, {
method: "PATCH",
headers: authHeaders(),
credentials: "include",
body: JSON.stringify({
name: aiEditName(),
description: aiEditDescription() || undefined,
credits: Number(aiEditCredits()),
price_inr: priceInPaise,
}),
});
if (!res.ok) throw new Error("Failed to save");
setAiEditingId("");
await loadAiPackages();
} catch (err: any) {
setAiEditError(err.message || "Failed to save");
} finally {
setAiEditSaving(false);
}
};
const toggleAiActive = async (pkg: AiCreditPackage) => {
try {
setAiTogglingId(pkg.id);
await fetch(`${API}/api/admin/ai-credits/packages/${pkg.id}`, {
method: "PATCH",
headers: authHeaders(),
credentials: "include",
body: JSON.stringify({ is_active: !pkg.is_active }),
});
await loadAiPackages();
} catch {
/* ignore */
} finally {
setAiTogglingId("");
}
};
const handleAiCreate = async (e: Event) => {
e.preventDefault();
try {
setAiCSaving(true);
setAiCError("");
const priceInPaise = Math.round(Number(aiCPrice()) * 100);
const res = await fetch(`${API}/api/admin/ai-credits/packages`, {
method: "POST",
headers: authHeaders(),
credentials: "include",
body: JSON.stringify({
name: aiCName(),
description: aiCDescription() || undefined,
credits: Number(aiCCredits()),
price_inr: priceInPaise,
}),
});
if (!res.ok) throw new Error("Failed to create package");
setAiCName("");
setAiCDescription("");
setAiCCredits("");
setAiCPrice("");
setAiCreateView(false);
await loadAiPackages();
} catch (err: any) {
setAiCError(err.message || "Failed to create");
} finally {
setAiCSaving(false);
}
};
onMount(() => void load());
const filteredRows = createMemo(() => {
@ -302,7 +442,7 @@ export default function PricingPage() {
{/* Tabs */}
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
{(["packages", "create"] as const).map((t) => (
{(["packages", "create", "ai_packages"] as const).map((t) => (
<button
type="button"
class={
@ -312,7 +452,7 @@ export default function PricingPage() {
}
onClick={() => setView(t)}
>
{t === "packages" ? "Packages" : "Create Package"}
{t === "packages" ? "Packages" : t === "create" ? "Create Package" : "AI Credit Packages"}
</button>
))}
</div>
@ -774,6 +914,246 @@ export default function PricingPage() {
</form>
</section>
</Show>
{/* AI Credit Packages Tab */}
<Show when={view() === "ai_packages"}>
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)"
onMount={() => loadAiPackages()}>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6;flex-wrap:wrap">
<h2 style="margin:0;font-size:18px;font-weight:700;color:#111827">AI Credit Packages</h2>
<button
type="button"
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #D1D5DB;background:#fff;padding:0 12px;font-size:12px;font-weight:600;color:#0f172a;cursor:pointer"
onClick={() => setAiCreateView(true)}
>
+ Create Package
</button>
</div>
<Show when={aiPackagesError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin:14px 20px">
{aiPackagesError()}
</div>
</Show>
<Show when={aiCreateView()}>
<div style="background:#f8fafc;padding:16px 20px;border-bottom:1px solid #F3F4F6">
<Show when={aiCError()}>
<div class="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{aiCError()}
</div>
</Show>
<h3 style="margin:0 0 12px;font-size:14px;font-weight:600">Create AI Credit Package</h3>
<form onSubmit={handleAiCreate} style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name *</label>
<input
type="text"
value={aiCName()}
onInput={(e) => setAiCName(e.currentTarget.value)}
required
placeholder="e.g. AI Starter Pack"
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Description</label>
<input
type="text"
value={aiCDescription()}
onInput={(e) => setAiCDescription(e.currentTarget.value)}
placeholder="Optional"
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:200px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Credits *</label>
<input
type="number"
value={aiCCredits()}
onInput={(e) => setAiCCredits(e.currentTarget.value)}
required
min="1"
placeholder="e.g. 100"
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Price () *</label>
<input
type="number"
value={aiCPrice()}
onInput={(e) => setAiCPrice(e.currentTarget.value)}
required
min="1"
placeholder="e.g. 499"
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="btn-primary" type="submit" disabled={aiCSaving()}>
{aiCSaving() ? "Creating..." : "Create"}
</button>
<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={() => setAiCreateView(false)}
>
Cancel
</button>
</div>
</form>
</div>
</Show>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Credits</th>
<th>Price ()</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={aiPackagesLoading()}>
<tr>
<td colspan="5" style="text-align:center;padding:32px;color:#64748b">
Loading...
</td>
</tr>
</Show>
<Show when={!aiPackagesLoading() && aiPackages().length === 0}>
<tr>
<td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">
No AI credit packages found.
</td>
</tr>
</Show>
<Show when={!aiPackagesLoading() && aiPackages().length > 0}>
<For each={aiPackages()}>
{(pkg) => (
<>
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">
{pkg.name}
</td>
<td class="text-slate-700 font-medium">{pkg.credits}</td>
<td class="text-slate-700">{(pkg.price_inr / 100).toLocaleString("en-IN")}</td>
<td>
<span
style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${pkg.is_active ? "#FFD8C2" : "#D1D5DB"};background:${pkg.is_active ? "#FFF1EB" : "#F3F4F6"};color:${pkg.is_active ? "#FF5E13" : "#4B5563"};padding:2px 10px;font-size:12px;font-weight:500`}
>
<span
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${pkg.is_active ? "#FF5E13" : "#9CA3AF"};margin-right:5px`}
/>
{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={() => startAiEdit(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"
: "btn-primary"
}
disabled={aiTogglingId() === pkg.id}
onClick={() => toggleAiActive(pkg)}
>
{aiTogglingId() === pkg.id
? "..."
: pkg.is_active
? "Disable"
: "Enable"}
</button>
</div>
</td>
</tr>
<Show when={aiEditingId() === pkg.id}>
<tr>
<td colspan="5" style="background:#f8fafc;padding:16px">
<Show when={aiEditError()}>
<div class="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{aiEditError()}
</div>
</Show>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
<input
type="text"
value={aiEditName()}
onInput={(e) => setAiEditName(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Description</label>
<input
type="text"
value={aiEditDescription()}
onInput={(e) => setAiEditDescription(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Credits</label>
<input
type="number"
value={aiEditCredits()}
onInput={(e) => setAiEditCredits(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
/>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Price ()</label>
<input
type="number"
value={aiEditPrice()}
onInput={(e) => setAiEditPrice(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="btn-primary" disabled={aiEditSaving()} onClick={() => saveAiEdit(pkg.id)}>
{aiEditSaving() ? "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={cancelAiEdit}
>
Cancel
</button>
</div>
</div>
</td>
</tr>
</Show>
</>
)}
</For>
</Show>
</tbody>
</table>
</div>
<Show when={!aiPackagesLoading()}>
<div style="padding:10px 16px;font-size:12px;color:#64748b;border-top:1px solid #f1f5f9">
{aiPackages().length} AI credit packages
</div>
</Show>
</div>
</div>
</Show>
</div>
</div>
);