feat(ai-credits): add role scoping UI to AI Credit Packages admin
Matches the backend's new applicable_roles field (nxtgauge-backend-rust apps/payments/src/ai_credits.rs): a role-toggle chip picker (reusing the same ALL_ROLES/ROLE_LABELS already used by the TraceCoin packages tab in this file) on both the create form and the inline edit row, plus a Roles column in the packages table showing 'All roles' when applicable_roles is empty or up to 3 role badges (+N overflow) otherwise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
bd9666aabf
commit
066b077941
1 changed files with 76 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ type AiCreditPackage = {
|
|||
credits: number;
|
||||
price_inr: number;
|
||||
is_active: boolean;
|
||||
applicable_roles: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
|
@ -127,6 +128,7 @@ export default function PricingPage() {
|
|||
const [aiEditDescription, setAiEditDescription] = createSignal("");
|
||||
const [aiEditCredits, setAiEditCredits] = createSignal("");
|
||||
const [aiEditPrice, setAiEditPrice] = createSignal("");
|
||||
const [aiEditRoles, setAiEditRoles] = createSignal<string[]>([]);
|
||||
const [aiEditSaving, setAiEditSaving] = createSignal(false);
|
||||
const [aiEditError, setAiEditError] = createSignal("");
|
||||
const [aiTogglingId, setAiTogglingId] = createSignal("");
|
||||
|
|
@ -135,10 +137,20 @@ export default function PricingPage() {
|
|||
const [aiCDescription, setAiCDescription] = createSignal("");
|
||||
const [aiCCredits, setAiCCredits] = createSignal("");
|
||||
const [aiCPrice, setAiCPrice] = createSignal("");
|
||||
const [aiCRoles, setAiCRoles] = createSignal<string[]>([]);
|
||||
const [aiCSaving, setAiCSaving] = createSignal(false);
|
||||
const [aiCError, setAiCError] = createSignal("");
|
||||
const [aiCreateView, setAiCreateView] = createSignal(false);
|
||||
|
||||
const toggleAiCRole = (role: string) => {
|
||||
const current = aiCRoles();
|
||||
setAiCRoles(current.includes(role) ? current.filter((r) => r !== role) : [...current, role]);
|
||||
};
|
||||
const toggleAiEditRole = (role: string) => {
|
||||
const current = aiEditRoles();
|
||||
setAiEditRoles(current.includes(role) ? current.filter((r) => r !== role) : [...current, role]);
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
setLoadError("");
|
||||
|
|
@ -184,11 +196,13 @@ export default function PricingPage() {
|
|||
setAiEditDescription(pkg.description || "");
|
||||
setAiEditCredits(String(pkg.credits));
|
||||
setAiEditPrice(String(pkg.price_inr / 100)); // Convert paise to rupees for display
|
||||
setAiEditRoles(pkg.applicable_roles ?? []);
|
||||
setAiEditError("");
|
||||
};
|
||||
|
||||
const cancelAiEdit = () => {
|
||||
setAiEditingId("");
|
||||
setAiEditRoles([]);
|
||||
setAiEditError("");
|
||||
};
|
||||
|
||||
|
|
@ -206,6 +220,7 @@ export default function PricingPage() {
|
|||
description: aiEditDescription() || undefined,
|
||||
credits: Number(aiEditCredits()),
|
||||
price_inr: priceInPaise,
|
||||
applicable_roles: aiEditRoles(),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to save");
|
||||
|
|
@ -250,6 +265,7 @@ export default function PricingPage() {
|
|||
description: aiCDescription() || undefined,
|
||||
credits: Number(aiCCredits()),
|
||||
price_inr: priceInPaise,
|
||||
applicable_roles: aiCRoles(),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to create package");
|
||||
|
|
@ -257,6 +273,7 @@ export default function PricingPage() {
|
|||
setAiCDescription("");
|
||||
setAiCCredits("");
|
||||
setAiCPrice("");
|
||||
setAiCRoles([]);
|
||||
setAiCreateView(false);
|
||||
await loadAiPackages();
|
||||
} catch (err: any) {
|
||||
|
|
@ -1006,6 +1023,24 @@ export default function PricingPage() {
|
|||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
|
||||
/>
|
||||
</div>
|
||||
<div style="width:100%">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">
|
||||
Applicable Roles (none selected = every role)
|
||||
</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
<For each={ALL_ROLES}>
|
||||
{(role) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAiCRole(role)}
|
||||
style={`padding:4px 10px;border-radius:9999px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid ${aiCRoles().includes(role) ? "#FF5E13" : "#D1D5DB"};background:${aiCRoles().includes(role) ? "#FFF1EB" : "#fff"};color:${aiCRoles().includes(role) ? "#FF5E13" : "#4B5563"}`}
|
||||
>
|
||||
{ROLE_LABELS[role] || role}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn-primary" type="submit" disabled={aiCSaving()}>
|
||||
{aiCSaving() ? "Creating..." : "Create"}
|
||||
|
|
@ -1030,6 +1065,7 @@ export default function PricingPage() {
|
|||
<th>Name</th>
|
||||
<th>Credits</th>
|
||||
<th>Price (₹)</th>
|
||||
<th>Roles</th>
|
||||
<th>Status</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -1037,14 +1073,14 @@ export default function PricingPage() {
|
|||
<tbody>
|
||||
<Show when={aiPackagesLoading()}>
|
||||
<tr>
|
||||
<td colspan="5" style="text-align:center;padding:32px;color:#64748b">
|
||||
<td colspan="6" 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">
|
||||
<td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">
|
||||
No AI credit packages found.
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -1059,6 +1095,25 @@ export default function PricingPage() {
|
|||
</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>
|
||||
<Show
|
||||
when={(pkg.applicable_roles ?? []).length > 0}
|
||||
fallback={<span style="font-size:11px;color:#94a3b8">All roles</span>}
|
||||
>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px;max-width:220px">
|
||||
<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>
|
||||
</Show>
|
||||
</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`}
|
||||
|
|
@ -1097,7 +1152,7 @@ export default function PricingPage() {
|
|||
</tr>
|
||||
<Show when={aiEditingId() === pkg.id}>
|
||||
<tr>
|
||||
<td colspan="5" style="background:#f8fafc;padding:16px">
|
||||
<td colspan="6" 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()}
|
||||
|
|
@ -1140,6 +1195,24 @@ export default function PricingPage() {
|
|||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px"
|
||||
/>
|
||||
</div>
|
||||
<div style="width:100%">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">
|
||||
Applicable Roles (none selected = every role)
|
||||
</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
<For each={ALL_ROLES}>
|
||||
{(role) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAiEditRole(role)}
|
||||
style={`padding:4px 10px;border-radius:9999px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid ${aiEditRoles().includes(role) ? "#FF5E13" : "#D1D5DB"};background:${aiEditRoles().includes(role) ? "#FFF1EB" : "#fff"};color:${aiEditRoles().includes(role) ? "#FF5E13" : "#4B5563"}`}
|
||||
>
|
||||
{ROLE_LABELS[role] || role}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn-primary" disabled={aiEditSaving()} onClick={() => saveAiEdit(pkg.id)}>
|
||||
{aiEditSaving() ? "Saving..." : "Save"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue