nxtgauge-admin-solid/src/routes/admin/pricing.tsx
Tracewebstudio Dev bed7035ec5
All checks were successful
build-and-release / build (push) Successful in 1m19s
fix: TypeScript errors, test quality, and admin panel correctness
- tsconfig.json: add @types/node for playwright/test configs
- lib/api.ts: handle both array and wrapped packages response from tracecoin endpoint
- lib/server/gateway.ts: use import.meta.env instead of process.env
- routes/admin/pricing.tsx: fix packages() usage after api.ts fix
- routes/admin/roles/*.tsx: fix Set<string> types and module filter types
- routes/admin/users/[id]/edit.tsx: add phone? to local User type
- routes/admin/users/details/[id].tsx: fix roleFilter param type narrowing
- components/admin/DashboardDesignPreview.tsx: fix budget→budget, responses→responseTag
- vite.config.ts: type proxy callbacks as any
- test/setup.ts: fix IntersectionObserver mock, add vitest/globals reference
- tests/e2e: fix PNG type mismatches, add as any for mock overrides
- tests/vitest: fix global.createElement mock type, vitest globals reference
- tests/tsconfig.json: test-specific tsconfig with node types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 00:47:43 +02:00

1176 lines
No EOL
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createMemo, createResource, createSignal, onMount, Show, For } from 'solid-js';
import {
api,
AdminPackage,
CreatePackageInput,
UpdatePackageInput,
listAdminPackages,
createAdminPackage,
updateAdminPackage,
deleteAdminPackage,
} from '~/lib/api';
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)' },
{ value: 'JOB_POSTING', label: 'Job Posting (Company)' },
{ value: 'LEAD_REQUEST', label: 'Lead Request (Professional)' },
{ value: 'REQUIREMENT_SLOTS', label: 'Requirement Slots (Customer)' },
];
const ALL_ROLES = [
'COMPANY',
'CUSTOMER',
'JOB_SEEKER',
'PHOTOGRAPHER',
'VIDEO_EDITOR',
'GRAPHIC_DESIGNER',
'SOCIAL_MEDIA_MANAGER',
'FITNESS_TRAINER',
'CATERING_SERVICE',
'MAKEUP_ARTIST',
'TUTOR',
'DEVELOPER',
'UGC_CONTENT_CREATOR',
];
const ROLE_LABELS: Record<string, string> = {
COMPANY: 'Company',
CUSTOMER: 'Customer',
JOB_SEEKER: 'Job Seeker',
PHOTOGRAPHER: 'Photographer',
VIDEO_EDITOR: 'Video Editor',
GRAPHIC_DESIGNER: 'Graphic Designer',
SOCIAL_MEDIA_MANAGER: 'Social Media Manager',
FITNESS_TRAINER: 'Fitness Trainer',
CATERING_SERVICE: 'Catering Service',
MAKEUP_ARTIST: 'Makeup Artist',
TUTOR: 'Tutor',
DEVELOPER: 'Developer',
UGC_CONTENT_CREATOR: 'UGC Creator',
};
type SortMode = 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'coins_asc' | 'coins_desc';
const SORT_LABELS: Record<SortMode, string> = {
name_asc: 'Name A→Z',
name_desc: 'Name Z→A',
price_asc: 'Price ↑',
price_desc: 'Price ↓',
coins_asc: 'TraceCoins ↑',
coins_desc: 'TraceCoins ↓',
};
const API = '';
function authHeaders(): Record<string, string> {
const token = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
}
export default function PricingPage() {
const [packages, { refetch }] = createResource(listAdminPackages);
const [rows, setRows] = createSignal<AdminPackage[]>([]);
const [loading, setLoading] = createSignal(true);
const [loadError, setLoadError] = createSignal("");
const [view, setView] = createSignal<"packages" | "create" | "ai_packages">("packages");
const [search, setSearch] = createSignal('');
const [typeFilter, setTypeFilter] = createSignal('all');
const [statusFilter, setStatusFilter] = createSignal('all');
const [sortBy, setSortBy] = createSignal<SortMode>('name_asc');
const [sortOpen, setSortOpen] = createSignal(false);
const [editingId, setEditingId] = createSignal('');
const [editName, setEditName] = createSignal('');
const [editTracecoins, setEditTracecoins] = createSignal('');
const [editPrice, setEditPrice] = createSignal('');
const [editActive, setEditActive] = createSignal(true);
const [editSaving, setEditSaving] = createSignal(false);
const [editError, setEditError] = createSignal('');
const [togglingId, setTogglingId] = createSignal('');
const [deletingId, setDeletingId] = createSignal('');
const [cName, setCName] = createSignal('');
const [cDescription, setCDescription] = createSignal('');
const [cType, setCType] = createSignal('TRACECOIN_BUNDLE');
const [cRoles, setCRoles] = createSignal<string[]>([]);
const [cTracecoins, setCTracecoins] = createSignal('');
const [cPrice, setCPrice] = createSignal('');
const [cDuration, setCDuration] = createSignal('');
const [cValidFrom, setCValidFrom] = createSignal('');
const [cValidUntil, setCValidUntil] = createSignal('');
const [cPromotional, setCPromotional] = createSignal(false);
const [cSaving, setCsaving] = createSignal(false);
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("");
try {
const res = await fetch(`${API}/api/packages`, {
headers: authHeaders(),
credentials: "include",
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
const data = await res.json();
setRows(Array.isArray(data) ? data : (data.data ?? []));
} catch (err: any) {
setLoadError(err.message || "Could not load packages.");
setRows([]);
} finally {
setLoading(false);
}
};
// 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(); void loadAiPackages(); });
const filteredRows = createMemo(() => {
let r = packages() ?? [];
const q = search().toLowerCase();
if (q)
r = r.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
p.package_type.toLowerCase().includes(q) ||
(p.applicable_roles ?? []).some((rl) => rl.toLowerCase().includes(q)),
);
if (typeFilter() !== 'all') r = r.filter((p) => p.package_type === typeFilter());
if (statusFilter() === 'active') r = r.filter((p) => p.is_active);
if (statusFilter() === 'inactive') r = r.filter((p) => !p.is_active);
const sorted = [...r];
const mode = sortBy();
sorted.sort((a, b) => {
if (mode === 'name_desc') return b.name.localeCompare(a.name);
if (mode === 'price_asc') return a.price - b.price;
if (mode === 'price_desc') return b.price - a.price;
if (mode === 'coins_asc') return a.tracecoins_amount - b.tracecoins_amount;
if (mode === 'coins_desc') return b.tracecoins_amount - a.tracecoins_amount;
return a.name.localeCompare(b.name);
});
return sorted;
});
const startEdit = (pkg: AdminPackage) => {
setEditingId(pkg.id);
setEditName(pkg.name);
setEditTracecoins(String(pkg.tracecoins_amount));
setEditPrice(String(pkg.price_inr ?? pkg.price));
setEditActive(!!pkg.is_active);
setEditError('');
};
const cancelEdit = () => {
setEditingId('');
setEditError('');
};
const saveEdit = async (id: string) => {
setEditSaving(true);
setEditError('');
try {
const update: UpdatePackageInput = {
name: editName(),
tracecoins_amount: Number(editTracecoins()),
price_inr: Number(editPrice()),
is_active: editActive(),
};
await updateAdminPackage(id, update);
setEditingId('');
await refetch();
} catch (err: any) {
setEditError(err?.message || 'Failed to save');
} finally {
setEditSaving(false);
}
};
const toggleActive = async (pkg: AdminPackage) => {
setTogglingId(pkg.id);
try {
await updateAdminPackage(pkg.id, { is_active: !pkg.is_active });
await refetch();
} catch (err: any) {
console.error('toggleActive failed', err);
} finally {
setTogglingId('');
}
};
const deletePackage = async (pkg: AdminPackage) => {
if (!confirm(`Delete package "${pkg.name}"? This cannot be undone.`)) return;
setDeletingId(pkg.id);
try {
await deleteAdminPackage(pkg.id);
await refetch();
} catch (err: any) {
alert(err?.message || 'Failed to delete package.');
} finally {
setDeletingId('');
}
};
const toggleRole = (role: string) => {
const current = cRoles();
if (current.includes(role)) {
setCRoles(current.filter((r) => r !== role));
} else {
setCRoles([...current, role]);
}
};
const handleCreate = async (e: Event) => {
e.preventDefault();
setCsaving(true);
setCError('');
try {
const body: CreatePackageInput = {
name: cName(),
description: cDescription() || undefined,
package_type: cType(),
applicable_roles: cRoles(),
tracecoins_amount: Number(cTracecoins()),
price_inr: Number(cPrice()),
price: Number(cPrice()), // legacy field for old clients
is_promotional: cPromotional(),
is_active: true,
};
if (cDuration()) body.duration_days = Number(cDuration());
if (cValidFrom()) body.valid_from = new Date(cValidFrom()).toISOString();
if (cValidUntil()) body.valid_until = new Date(cValidUntil()).toISOString();
await createAdminPackage(body);
setCName('');
setCDescription('');
setCType('TRACECOIN_BUNDLE');
setCRoles([]);
setCTracecoins('');
setCPrice('');
setCDuration('');
setCValidFrom('');
setCValidUntil('');
setCPromotional(false);
setView('packages');
await refetch();
} catch (err: any) {
setCError(err?.message || 'Failed to create');
} finally {
setCsaving(false);
}
};
const formatDate = (dateStr?: string | null) => {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
const getTypeLabel = (type: string) => {
return PACKAGE_TYPES.find((t) => t.value === type)?.label || type;
};
return (
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom:1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Pricing Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">
Create and manage TraceCoin packages for all roles
</p>
</div>
<Show when={packages.error}>
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Failed to load packages: {String(packages.error)}
</div>
</Show>
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
{(["packages", "create", "ai_packages"] as const).map((t) => (
<button
type="button"
class={
view() === t
? "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(t)}
>
{t === "packages" ? "Packages" : t === "create" ? "Create Package" : "AI Credit Packages"}
</button>
))}
</div>
<div>
<Show when={view() === 'packages'}>
<section
class="rounded-xl border border-gray-200 bg-white shadow-sm"
style="margin-top:1.5rem"
>
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6;flex-wrap:wrap">
<input
type="text"
placeholder="Search by name or role..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="height:34px;flex:1;min-width:220px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
<select
value={typeFilter()}
onChange={(e) => setTypeFilter(e.currentTarget.value)}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#374151"
>
<option value="all">All Types</option>
<For each={PACKAGE_TYPES}>{(t) => <option value={t.value}>{t.label}</option>}</For>
</select>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#374151"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<div style="position:relative">
<button
type="button"
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
onClick={() => setSortOpen(!sortOpen())}
>
Sort: {SORT_LABELS[sortBy()]}
</button>
<Show when={sortOpen()}>
<div style="position:absolute;top:38px;right:0;background:white;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.1);z-index:50;min-width:190px;padding:6px">
<For each={Object.entries(SORT_LABELS) as [SortMode, string][]}>
{([key, label]) => (
<button
type="button"
onClick={() => {
setSortBy(key);
setSortOpen(false);
}}
style={`display:block;width:100%;text-align:left;padding:8px 12px;font-size:13px;border-radius:8px;border:none;cursor:pointer;background:${sortBy() === key ? '#FFF1EB' : 'transparent'};color:${sortBy() === key ? '#FF5E13' : '#374151'};font-weight:${sortBy() === key ? '600' : '400'}`}
>
{label}
</button>
)}
</For>
</div>
</Show>
</div>
<button
type="button"
class="btn-primary"
style="height:34px;padding:0 12px;font-size:12px"
onClick={() => void refetch()}
>
Refresh
</button>
</div>
<Show
when={!packages.loading}
fallback={
<p style="padding:32px;text-align:center;color:#64748b">Loading packages...</p>
}
>
<Show
when={filteredRows().length > 0}
fallback={
<p style="padding:32px;text-align:center;color:#94a3b8">
No packages found.
</p>
}
>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Applicable Roles</th>
<th>TraceCoins</th>
<th>Price ()</th>
<th>Valid Period</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<For each={filteredRows()}>
{(pkg) => (
<>
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">
{pkg.name}
{pkg.is_promotional && (
<span
style="margin-left:6px;font-size:10px;background:#FEF3C7;color:#D97706;padding:1px 6px;border-radius:4px"
>
PROMO
</span>
)}
</td>
<td>
<span
style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;background:#e0f2fe;color:#0369a1"
>
{getTypeLabel(pkg.package_type)}
</span>
</td>
<td>
<div style="display:flex;flex-wrap:wrap;gap:4px">
<For each={(pkg.applicable_roles ?? []).slice(0, 3)}>
{(role) => (
<span
style="display:inline-block;padding:1px 6px;border-radius:4px;font-size:10px;background:#f1f5f9;color:#475569"
>
{ROLE_LABELS[role] || role}
</span>
)}
</For>
{(pkg.applicable_roles ?? []).length > 3 && (
<span style="font-size:10px;color:#64748b">
+{pkg.applicable_roles.length - 3}
</span>
)}
</div>
</td>
<td class="text-slate-700 font-medium">{pkg.tracecoins_amount}</td>
<td class="text-slate-700">{(pkg.price_inr ?? pkg.price).toLocaleString('en-IN')}</td>
<td style="font-size:12px;color:#64748b">
{pkg.valid_from || pkg.valid_until ? (
<>
{formatDate(pkg.valid_from)} - {formatDate(pkg.valid_until)}
</>
) : (
'Always'
)}
</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
type="button"
class="btn-secondary"
style="padding:6px 10px;font-size:12px"
onClick={() => startEdit(pkg)}
>
Edit
</button>
<button
type="button"
class={pkg.is_active ? 'btn-secondary' : 'btn-primary'}
style={
pkg.is_active
? 'padding:6px 10px;font-size:12px;color:#b91c1c;border-color:#fca5a5'
: 'padding:6px 10px;font-size:12px'
}
disabled={togglingId() === pkg.id}
onClick={() => toggleActive(pkg)}
>
{togglingId() === pkg.id
? '...'
: pkg.is_active
? 'Disable'
: 'Enable'}
</button>
<button
type="button"
class="btn-secondary"
style="padding:6px 10px;font-size:12px;color:#b91c1c;border-color:#fca5a5"
disabled={deletingId() === pkg.id}
onClick={() => deletePackage(pkg)}
>
{deletingId() === pkg.id ? '...' : 'Delete'}
</button>
</div>
</td>
</tr>
<Show when={editingId() === pkg.id}>
<tr>
<td colspan="8" style="background:#f8fafc;padding:16px">
<Show when={editError()}>
<div
class="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700"
>
{editError()}
</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={editName()}
onInput={(e) => setEditName(e.currentTarget.value)}
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:220px"
/>
</div>
<div>
<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>
<label
style="display:block;font-size:12px;font-weight:600;margin-bottom:4px"
>
Price ()
</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>
<label
style="display:block;font-size:12px;font-weight:600;margin-bottom:4px"
>
Active
</label>
<input
type="checkbox"
checked={editActive()}
onChange={(e) => setEditActive(e.currentTarget.checked)}
/>
</div>
<div style="display:flex;gap:8px">
<button
type="button"
class="btn-primary"
style="padding:8px 14px;font-size:13px"
disabled={editSaving()}
onClick={() => saveEdit(pkg.id)}
>
{editSaving() ? 'Saving...' : 'Save'}
</button>
<button
type="button"
class="btn-secondary"
style="padding:8px 14px;font-size:13px"
onClick={cancelEdit}
>
Cancel
</button>
</div>
</div>
</td>
</tr>
</Show>
</>
)}
</For>
</tbody>
</table>
</div>
<div style="padding:10px 16px;font-size:12px;color:#64748b;border-top:1px solid #f1f5f9">
{filteredRows().length} of {(packages() ?? []).length} packages
</div>
</Show>
</Show>
</section>
</Show>
<Show when={view() === 'create'}>
<section
class="rounded-xl border border-gray-200 bg-white shadow-sm p-6"
style="max-width:600px;margin-top:1.5rem"
>
<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">
{cError()}
</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Package Name *
</label>
<input
type="text"
value={cName()}
onInput={(e) => setCName(e.currentTarget.value)}
required
placeholder="e.g. Christmas Special - 50 Tracecoins"
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Description
</label>
<textarea
value={cDescription()}
onInput={(e) => setCDescription(e.currentTarget.value)}
placeholder="Optional description..."
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box;min-height:60px;resize:vertical"
/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Package Type *
</label>
<select
value={cType()}
onChange={(e) => setCType(e.currentTarget.value)}
required
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={PACKAGE_TYPES}>
{(t) => <option value={t.value}>{t.label}</option>}
</For>
</select>
</div>
<div style="position:relative">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Applicable Roles *
</label>
<button
type="button"
onClick={() => setRoleDropdownOpen(!roleDropdownOpen())}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box;text-align:left;background:white;cursor:pointer"
>
{cRoles().length === 0
? 'Select roles...'
: `${cRoles().length} role(s) selected`}
<span style="float:right"></span>
</button>
<Show when={roleDropdownOpen()}>
<div style="position:absolute;top:100%;left:0;right:0;background:white;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.1);z-index:20;max-height:200px;overflow-y:auto;margin-top:4px">
<For each={ALL_ROLES}>
{(role) => (
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px">
<input
type="checkbox"
checked={cRoles().includes(role)}
onChange={() => toggleRole(role)}
/>
{ROLE_LABELS[role] || role}
</label>
)}
</For>
</div>
</Show>
<Show when={cRoles().length > 0}>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:8px">
<For each={cRoles()}>
{(role) => (
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 8px;background:#e0f2fe;color:#0369a1;border-radius:4px;font-size:12px">
{ROLE_LABELS[role] || role}
<button
type="button"
onClick={() => toggleRole(role)}
style="background:none;border:none;cursor:pointer;font-size:14px;padding:0;line-height:1"
>
×
</button>
</span>
)}
</For>
</div>
</Show>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Tracecoins Amount *
</label>
<input
type="number"
value={cTracecoins()}
onInput={(e) => setCTracecoins(e.currentTarget.value)}
required
min="1"
placeholder="e.g. 50"
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Price () *
</label>
<input
type="number"
value={cPrice()}
onInput={(e) => setCPrice(e.currentTarget.value)}
required
min="1"
placeholder="e.g. 499"
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Duration (days)
</label>
<input
type="number"
value={cDuration()}
onInput={(e) => setCDuration(e.currentTarget.value)}
min="1"
placeholder="e.g. 30 (leave empty for unlimited)"
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Valid From
</label>
<input
type="date"
value={cValidFrom()}
onInput={(e) => setCValidFrom(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>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
Valid Until
</label>
<input
type="date"
value={cValidUntil()}
onInput={(e) => setCValidUntil(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>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input
type="checkbox"
checked={cPromotional()}
onChange={(e) => setCPromotional(e.currentTarget.checked)}
/>
<span style="font-size:13px;font-weight:600">Promotional Package</span>
</label>
<p style="font-size:12px;color:#64748b;margin-top:4px">
Promotional packages appear first in listings
</p>
</div>
<div>
<button class="btn-primary" type="submit" disabled={cSaving()}>
{cSaving() ? 'Creating...' : 'Create Package'}
</button>
</div>
</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)">
<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>
);
}