feat: multi-select roles in pricing management UI
This commit is contained in:
parent
c628c968ae
commit
a8b1b7cd11
1 changed files with 633 additions and 225 deletions
|
|
@ -1,18 +1,18 @@
|
||||||
import { createSignal, createMemo, onMount, Show, For } from 'solid-js';
|
import { createSignal, createMemo, onMount, Show, For } from "solid-js";
|
||||||
|
|
||||||
const API = '';
|
const API = "";
|
||||||
|
|
||||||
function getToken(): string {
|
function getToken(): string {
|
||||||
return typeof sessionStorage !== 'undefined'
|
return typeof sessionStorage !== "undefined"
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||||
: '';
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function authHeaders(): Record<string, string> {
|
function authHeaders(): Record<string, string> {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
return {
|
return {
|
||||||
Accept: 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -20,71 +20,124 @@ function authHeaders(): Record<string, string> {
|
||||||
type Package = {
|
type Package = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: string;
|
description?: string;
|
||||||
tracecoin_amount: number;
|
package_type: string;
|
||||||
price_inr: number;
|
applicable_roles: string[];
|
||||||
bonus_percentage?: number;
|
tracecoins_amount: number;
|
||||||
|
price: number;
|
||||||
|
duration_days?: number;
|
||||||
|
valid_from?: string;
|
||||||
|
valid_until?: string;
|
||||||
|
is_promotional: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
features?: any;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
is_available?: boolean;
|
||||||
|
is_expired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROLES = [
|
const PACKAGE_TYPES = [
|
||||||
'company', 'customer', 'job_seeker', 'photographer', 'video_editor',
|
{ value: "TRACECOIN_BUNDLE", label: "Tracecoin Bundle" },
|
||||||
'graphic_designer', 'social_media_manager', 'fitness_trainer',
|
{ value: "CONTACT_VIEWS", label: "Contact Views (Company)" },
|
||||||
'catering_services', 'makeup_artist', 'tutor', 'developer', 'ugc_content_creator',
|
{ value: "JOB_POSTING", label: "Job Posting (Company)" },
|
||||||
|
{ value: "LEAD_REQUEST", label: "Lead Request (Professional)" },
|
||||||
|
{ value: "REQUIREMENT_SLOTS", label: "Requirement Slots (Customer)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type SortMode = 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'coins_asc' | 'coins_desc';
|
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> = {
|
const SORT_LABELS: Record<SortMode, string> = {
|
||||||
name_asc: 'Name A→Z', name_desc: 'Name Z→A',
|
name_asc: "Name A→Z",
|
||||||
price_asc: 'Price ↑', price_desc: 'Price ↓',
|
name_desc: "Name Z→A",
|
||||||
coins_asc: 'TraceCoins ↑', coins_desc: 'TraceCoins ↓',
|
price_asc: "Price ↑",
|
||||||
|
price_desc: "Price ↓",
|
||||||
|
coins_asc: "TraceCoins ↑",
|
||||||
|
coins_desc: "TraceCoins ↓",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const [rows, setRows] = createSignal<Package[]>([]);
|
const [rows, setRows] = createSignal<Package[]>([]);
|
||||||
const [loading, setLoading] = createSignal(true);
|
const [loading, setLoading] = createSignal(true);
|
||||||
const [loadError, setLoadError] = createSignal('');
|
const [loadError, setLoadError] = createSignal("");
|
||||||
const [view, setView] = createSignal<'packages' | 'create'>('packages');
|
const [view, setView] = createSignal<"packages" | "create">("packages");
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [search, setSearch] = createSignal('');
|
const [search, setSearch] = createSignal("");
|
||||||
const [roleFilter, setRoleFilter] = createSignal('all');
|
const [typeFilter, setTypeFilter] = createSignal("all");
|
||||||
const [statusFilter, setStatusFilter] = createSignal('all');
|
const [statusFilter, setStatusFilter] = createSignal("all");
|
||||||
const [sortBy, setSortBy] = createSignal<SortMode>('name_asc');
|
const [sortBy, setSortBy] = createSignal<SortMode>("name_asc");
|
||||||
const [sortOpen, setSortOpen] = createSignal(false);
|
const [sortOpen, setSortOpen] = createSignal(false);
|
||||||
|
|
||||||
// Inline edit
|
// Inline edit
|
||||||
const [editingId, setEditingId] = createSignal('');
|
const [editingId, setEditingId] = createSignal("");
|
||||||
const [editName, setEditName] = createSignal('');
|
const [editName, setEditName] = createSignal("");
|
||||||
const [editTracecoins, setEditTracecoins] = createSignal('');
|
const [editTracecoins, setEditTracecoins] = createSignal("");
|
||||||
const [editPrice, setEditPrice] = createSignal('');
|
const [editPrice, setEditPrice] = createSignal("");
|
||||||
const [editSaving, setEditSaving] = createSignal(false);
|
const [editSaving, setEditSaving] = createSignal(false);
|
||||||
const [editError, setEditError] = createSignal('');
|
const [editError, setEditError] = createSignal("");
|
||||||
const [togglingId, setTogglingId] = createSignal('');
|
const [togglingId, setTogglingId] = createSignal("");
|
||||||
|
|
||||||
// Create form
|
// Create form
|
||||||
const [cName, setCName] = createSignal('');
|
const [cName, setCName] = createSignal("");
|
||||||
const [cRole, setCRole] = createSignal(ROLES[0]);
|
const [cDescription, setCDescription] = createSignal("");
|
||||||
const [cTracecoins, setCTracecoins] = createSignal('');
|
const [cType, setCType] = createSignal("TRACECOIN_BUNDLE");
|
||||||
const [cPrice, setCPrice] = createSignal('');
|
const [cRoles, setCRoles] = createSignal<string[]>([]);
|
||||||
const [cBonus, setCBonus] = createSignal('');
|
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 [cSaving, setCsaving] = createSignal(false);
|
||||||
const [cError, setCError] = createSignal('');
|
const [cError, setCError] = createSignal("");
|
||||||
|
const [roleDropdownOpen, setRoleDropdownOpen] = createSignal(false);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError('');
|
setLoadError("");
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API}/api/admin/tracecoin-packages`, {
|
const res = await fetch(`${API}/api/packages`, {
|
||||||
headers: authHeaders(),
|
headers: authHeaders(),
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setRows(Array.isArray(data) ? data : (data.packages ?? []));
|
setRows(Array.isArray(data) ? data : (data.data ?? []));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setLoadError(err.message || 'Could not load packages.');
|
setLoadError(err.message || "Could not load packages.");
|
||||||
setRows([]);
|
setRows([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -96,276 +149,631 @@ export default function PricingPage() {
|
||||||
const filteredRows = createMemo(() => {
|
const filteredRows = createMemo(() => {
|
||||||
let r = rows();
|
let r = rows();
|
||||||
const q = search().toLowerCase();
|
const q = search().toLowerCase();
|
||||||
if (q) r = r.filter((p) => p.name.toLowerCase().includes(q) || p.role.toLowerCase().includes(q));
|
if (q)
|
||||||
if (roleFilter() !== 'all') r = r.filter((p) => p.role === roleFilter());
|
r = r.filter(
|
||||||
if (statusFilter() === 'active') r = r.filter((p) => p.is_active);
|
(p) =>
|
||||||
if (statusFilter() === 'inactive') r = r.filter((p) => !p.is_active);
|
p.name.toLowerCase().includes(q) ||
|
||||||
|
p.package_type.toLowerCase().includes(q) ||
|
||||||
|
p.applicable_roles.some((r) => r.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 sorted = [...r];
|
||||||
const mode = sortBy();
|
const mode = sortBy();
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
if (mode === 'name_desc') return b.name.localeCompare(a.name);
|
if (mode === "name_desc") return b.name.localeCompare(a.name);
|
||||||
if (mode === 'price_asc') return a.price_inr - b.price_inr;
|
if (mode === "price_asc") return a.price - b.price;
|
||||||
if (mode === 'price_desc') return b.price_inr - a.price_inr;
|
if (mode === "price_desc") return b.price - a.price;
|
||||||
if (mode === 'coins_asc') return a.tracecoin_amount - b.tracecoin_amount;
|
if (mode === "coins_asc") return a.tracecoins_amount - b.tracecoins_amount;
|
||||||
if (mode === 'coins_desc') return b.tracecoin_amount - a.tracecoin_amount;
|
if (mode === "coins_desc") return b.tracecoins_amount - a.tracecoins_amount;
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
});
|
});
|
||||||
|
|
||||||
const startEdit = (pkg: Package) => {
|
const startEdit = (pkg: Package) => {
|
||||||
setEditingId(pkg.id); setEditName(pkg.name);
|
setEditingId(pkg.id);
|
||||||
setEditTracecoins(String(pkg.tracecoin_amount)); setEditPrice(String(pkg.price_inr));
|
setEditName(pkg.name);
|
||||||
setEditError('');
|
setEditTracecoins(String(pkg.tracecoins_amount));
|
||||||
|
setEditPrice(String(pkg.price));
|
||||||
|
setEditError("");
|
||||||
|
};
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setEditingId("");
|
||||||
|
setEditError("");
|
||||||
};
|
};
|
||||||
const cancelEdit = () => { setEditingId(''); setEditError(''); };
|
|
||||||
|
|
||||||
const saveEdit = async (id: string) => {
|
const saveEdit = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
setEditSaving(true); setEditError('');
|
setEditSaving(true);
|
||||||
const res = await fetch(`${API}/api/admin/tracecoin-packages/${id}`, {
|
setEditError("");
|
||||||
method: 'PATCH', headers: authHeaders(), credentials: 'include',
|
const res = await fetch(`${API}/api/packages/${id}`, {
|
||||||
body: JSON.stringify({ name: editName(), tracecoin_amount: Number(editTracecoins()), price_inr: Number(editPrice()) }),
|
method: "PATCH",
|
||||||
|
headers: authHeaders(),
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: editName(),
|
||||||
|
tracecoins_amount: Number(editTracecoins()),
|
||||||
|
price: Number(editPrice()),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to save');
|
if (!res.ok) throw new Error("Failed to save");
|
||||||
setEditingId(''); await load();
|
setEditingId("");
|
||||||
} catch (err: any) { setEditError(err.message || 'Failed to save'); }
|
await load();
|
||||||
finally { setEditSaving(false); }
|
} catch (err: any) {
|
||||||
|
setEditError(err.message || "Failed to save");
|
||||||
|
} finally {
|
||||||
|
setEditSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleActive = async (pkg: Package) => {
|
const toggleActive = async (pkg: Package) => {
|
||||||
try {
|
try {
|
||||||
setTogglingId(pkg.id);
|
setTogglingId(pkg.id);
|
||||||
await fetch(`${API}/api/admin/tracecoin-packages/${pkg.id}`, {
|
await fetch(`${API}/api/packages/${pkg.id}`, {
|
||||||
method: 'PATCH', headers: authHeaders(), credentials: 'include',
|
method: "PATCH",
|
||||||
|
headers: authHeaders(),
|
||||||
|
credentials: "include",
|
||||||
body: JSON.stringify({ is_active: !pkg.is_active }),
|
body: JSON.stringify({ is_active: !pkg.is_active }),
|
||||||
});
|
});
|
||||||
await load();
|
await load();
|
||||||
} catch { /* ignore */ } finally { setTogglingId(''); }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
} finally {
|
||||||
|
setTogglingId("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
const handleCreate = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
setCsaving(true); setCError('');
|
setCsaving(true);
|
||||||
|
setCError("");
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
name: cName(), role: cRole(),
|
name: cName(),
|
||||||
tracecoin_amount: Number(cTracecoins()), price_inr: Number(cPrice()),
|
description: cDescription() || undefined,
|
||||||
|
package_type: cType(),
|
||||||
|
applicable_roles: cRoles(),
|
||||||
|
tracecoins_amount: Number(cTracecoins()),
|
||||||
|
price: Number(cPrice()),
|
||||||
|
is_promotional: cPromotional(),
|
||||||
};
|
};
|
||||||
if (cBonus()) body.bonus_percentage = Number(cBonus());
|
if (cDuration()) body.duration_days = Number(cDuration());
|
||||||
const res = await fetch(`${API}/api/admin/tracecoin-packages`, {
|
if (cValidFrom()) body.valid_from = new Date(cValidFrom()).toISOString();
|
||||||
method: 'POST', headers: authHeaders(), credentials: 'include',
|
if (cValidUntil()) body.valid_until = new Date(cValidUntil()).toISOString();
|
||||||
|
|
||||||
|
const res = await fetch(`${API}/api/packages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: authHeaders(),
|
||||||
|
credentials: "include",
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to create package');
|
if (!res.ok) throw new Error("Failed to create package");
|
||||||
setCName(''); setCRole(ROLES[0]); setCTracecoins(''); setCPrice(''); setCBonus('');
|
setCName("");
|
||||||
setView('packages'); await load();
|
setCDescription("");
|
||||||
} catch (err: any) { setCError(err.message || 'Failed to create'); }
|
setCType("TRACECOIN_BUNDLE");
|
||||||
finally { setCsaving(false); }
|
setCRoles([]);
|
||||||
|
setCTracecoins("");
|
||||||
|
setCPrice("");
|
||||||
|
setCDuration("");
|
||||||
|
setCValidFrom("");
|
||||||
|
setCValidUntil("");
|
||||||
|
setCPromotional(false);
|
||||||
|
setView("packages");
|
||||||
|
await load();
|
||||||
|
} catch (err: any) {
|
||||||
|
setCError(err.message || "Failed to create");
|
||||||
|
} finally {
|
||||||
|
setCsaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr?: string) => {
|
||||||
|
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 (
|
return (
|
||||||
<div class="w-full space-y-6 pb-8">
|
<div class="w-full space-y-6 pb-8">
|
||||||
<div style="margin-bottom:1.5rem">
|
<div style="margin-bottom:1.5rem">
|
||||||
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Pricing Management</h1>
|
<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</p>
|
<p class="mt-1 text-[14px] text-[#6B7280]">
|
||||||
|
Create and manage TraceCoin packages for all roles
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
|
<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"] as const).map((t) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={view() === t
|
class={
|
||||||
? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium'
|
view() === t
|
||||||
: 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
? "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)}
|
onClick={() => setView(t)}
|
||||||
>
|
>
|
||||||
{t === 'packages' ? 'Packages' : 'Create Package'}
|
{t === "packages" ? "Packages" : "Create Package"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{/* ── Packages list ── */}
|
{/* ── Packages list ── */}
|
||||||
<Show when={view() === 'packages'}>
|
<Show when={view() === "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="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;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6;flex-wrap:wrap">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or role..."
|
placeholder="Search by name or role..."
|
||||||
value={search()}
|
value={search()}
|
||||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
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"
|
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={roleFilter()} onChange={(e) => setRoleFilter(e.currentTarget.value)} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#374151">
|
<select
|
||||||
<option value="all">All Roles</option>
|
value={typeFilter()}
|
||||||
<For each={ROLES}>{(r) => <option value={r}>{r}</option>}</For>
|
onChange={(e) => setTypeFilter(e.currentTarget.value)}
|
||||||
</select>
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#374151"
|
||||||
<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="all">All Types</option>
|
||||||
<option value="active">Active</option>
|
<For each={PACKAGE_TYPES}>{(t) => <option value={t.value}>{t.label}</option>}</For>
|
||||||
<option value="inactive">Inactive</option>
|
</select>
|
||||||
</select>
|
<select
|
||||||
<div style="position:relative">
|
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()]}
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||||
|
<path
|
||||||
|
d="M3 4.5L6 7.5L9 4.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</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
|
<button
|
||||||
type="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"
|
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={() => setSortOpen(!sortOpen())}
|
onClick={load}
|
||||||
>
|
>
|
||||||
Sort: {SORT_LABELS[sortBy()]}
|
Refresh
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<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={load}>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={loadError()}>
|
<Show when={loadError()}>
|
||||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{loadError()}</div>
|
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||||
</Show>
|
{loadError()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="data-table w-full text-sm">
|
<table class="data-table w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<th>Name</th>
|
||||||
</tr>
|
<th>Type</th>
|
||||||
</thead>
|
<th>Applicable Roles</th>
|
||||||
<tbody>
|
<th>TraceCoins</th>
|
||||||
<Show when={loading()}>
|
<th>Price (₹)</th>
|
||||||
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
<th>Valid Period</th>
|
||||||
</Show>
|
<th>Status</th>
|
||||||
<Show when={!loading() && filteredRows().length === 0}>
|
<th class="text-right">Actions</th>
|
||||||
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No packages found.</td></tr>
|
</tr>
|
||||||
</Show>
|
</thead>
|
||||||
<Show when={!loading() && filteredRows().length > 0}>
|
<tbody>
|
||||||
<For each={filteredRows()}>
|
<Show when={loading()}>
|
||||||
{(pkg) => (
|
<tr>
|
||||||
<>
|
<td colspan="8" style="text-align:center;padding:32px;color:#64748b">
|
||||||
<tr class="hover:bg-slate-50">
|
Loading...
|
||||||
<td class="font-semibold text-slate-900">{pkg.name}</td>
|
</td>
|
||||||
<td><span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;background:#f1f5f9;color:#475569">{pkg.role}</span></td>
|
</tr>
|
||||||
<td class="text-slate-700 font-medium">{pkg.tracecoin_amount}</td>
|
</Show>
|
||||||
<td class="text-slate-700">₹{(pkg.price_inr / 100).toFixed(2)}</td>
|
<Show when={!loading() && filteredRows().length === 0}>
|
||||||
<td class="text-slate-500">{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'}</td>
|
<tr>
|
||||||
<td>
|
<td colspan="8" style="text-align:center;padding:32px;color:#94a3b8">
|
||||||
<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`}>
|
No packages found.
|
||||||
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${pkg.is_active ? '#FF5E13' : '#9CA3AF'};margin-right:5px`} />
|
</td>
|
||||||
{pkg.is_active ? 'Active' : 'Inactive'}
|
</tr>
|
||||||
</span>
|
</Show>
|
||||||
</td>
|
<Show when={!loading() && filteredRows().length > 0}>
|
||||||
<td>
|
<For each={filteredRows()}>
|
||||||
<div class="flex items-center justify-end gap-1">
|
{(pkg) => (
|
||||||
<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
|
<tr class="hover:bg-slate-50">
|
||||||
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'}
|
<td class="font-semibold text-slate-900">
|
||||||
disabled={togglingId() === pkg.id}
|
{pkg.name}
|
||||||
onClick={() => toggleActive(pkg)}
|
{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.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`}
|
||||||
>
|
>
|
||||||
{togglingId() === pkg.id ? '...' : pkg.is_active ? 'Disable' : 'Enable'}
|
<span
|
||||||
</button>
|
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${pkg.is_active ? "#FF5E13" : "#9CA3AF"};margin-right:5px`}
|
||||||
</div>
|
/>
|
||||||
</td>
|
{pkg.is_active ? "Active" : "Inactive"}
|
||||||
</tr>
|
</span>
|
||||||
<Show when={editingId() === pkg.id}>
|
</td>
|
||||||
<tr>
|
<td>
|
||||||
<td colspan="7" style="background:#f8fafc;padding:16px">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<Show when={editError()}>
|
<button
|
||||||
<div class="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">{editError()}</div>
|
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"
|
||||||
</Show>
|
onClick={() => startEdit(pkg)}
|
||||||
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
|
>
|
||||||
<div>
|
Edit
|
||||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
|
</button>
|
||||||
<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" />
|
<button
|
||||||
</div>
|
class={
|
||||||
<div>
|
pkg.is_active
|
||||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">TraceCoins</label>
|
? "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"
|
||||||
<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" />
|
: "btn-primary"
|
||||||
</div>
|
}
|
||||||
<div>
|
disabled={togglingId() === pkg.id}
|
||||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Price (paise)</label>
|
onClick={() => toggleActive(pkg)}
|
||||||
<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>
|
{togglingId() === pkg.id
|
||||||
<div style="display:flex;gap:8px">
|
? "..."
|
||||||
<button class="btn-primary" disabled={editSaving()} onClick={() => saveEdit(pkg.id)}>{editSaving() ? 'Saving...' : 'Save'}</button>
|
: pkg.is_active
|
||||||
<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>
|
? "Disable"
|
||||||
</div>
|
: "Enable"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
<Show when={editingId() === pkg.id}>
|
||||||
</>
|
<tr>
|
||||||
)}
|
<td colspan="8" style="background:#f8fafc;padding:16px">
|
||||||
</For>
|
<Show when={editError()}>
|
||||||
</Show>
|
<div class="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||||
</tbody>
|
{editError()}
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
<Show when={!loading()}>
|
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
|
||||||
<div style="padding:10px 16px;font-size:12px;color:#64748b;border-top:1px solid #f1f5f9">
|
<div>
|
||||||
{filteredRows().length} of {rows().length} packages
|
<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>
|
||||||
|
<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 style="display:flex;gap:8px">
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
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>
|
<Show when={!loading()}>
|
||||||
</div>
|
<div style="padding:10px 16px;font-size:12px;color:#64748b;border-top:1px solid #f1f5f9">
|
||||||
|
{filteredRows().length} of {rows().length} packages
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* ── Create Package ── */}
|
{/* ── Create Package ── */}
|
||||||
<Show when={view() === 'create'}>
|
<Show when={view() === "create"}>
|
||||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6" style="max-width:480px">
|
<section
|
||||||
|
class="rounded-xl border border-gray-200 bg-white shadow-sm p-6"
|
||||||
|
style="max-width:600px"
|
||||||
|
>
|
||||||
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Package</h2>
|
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Package</h2>
|
||||||
<Show when={cError()}>
|
<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>
|
<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>
|
</Show>
|
||||||
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
|
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
|
||||||
<div>
|
<div>
|
||||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
|
||||||
<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;box-sizing:border-box" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Role</label>
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
|
||||||
<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;box-sizing:border-box">
|
Description
|
||||||
<For each={ROLES}>{(r) => <option value={r}>{r}</option>}</For>
|
</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="position:relative">
|
||||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">TraceCoins</label>
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
|
||||||
<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;box-sizing:border-box" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Price INR (paise — e.g. 49900 = ₹499)</label>
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">
|
||||||
<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;box-sizing:border-box" />
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Bonus % <span style="font-weight:400;color:#94a3b8">(optional)</span></label>
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||||
<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;box-sizing:border-box" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn-primary" type="submit" disabled={cSaving()}>{cSaving() ? 'Creating...' : 'Create Package'}</button>
|
<button class="btn-primary" type="submit" disabled={cSaving()}>
|
||||||
|
{cSaving() ? "Creating..." : "Create Package"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue