feat(adminsite): staff licence history, audit and plan editing
Licences and audit are both filterable client-side: the endpoints cap at 500 rows and staff are narrowing a list already in front of them. Plans carry both guard rails spec 4 asks for. The confirmation names each field that changes and states how many licences are already issued and unaffected -- existing licences snapshotted their plan at issue time, and saying so is what stops a well-meaning edit being followed by a panicked reissue. Deployment is displayed and never editable, because moving a tier between cloud and self-hosted would break the cloud-only rule spec 1 leans on; that is a code review, not a form field. The two edit buttons are the concrete changes staff need on day one. A general-purpose limits editor waits until somebody asks for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import type { Plan } from "@/lib/api";
|
||||
import { limitLabel } from "@/lib/format";
|
||||
import { Button } from "./Button";
|
||||
|
||||
/*
|
||||
* Editing a plan changes what every future customer gets, so the confirmation
|
||||
* names each field rather than asking "are you sure". Existing licences
|
||||
* snapshotted their plan at issue time and are genuinely unaffected — saying so
|
||||
* is what stops a well-meaning edit being followed by a panicked reissue.
|
||||
*/
|
||||
export function ConfirmPlanChange({
|
||||
plan,
|
||||
next,
|
||||
issuedCount,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
plan: Plan;
|
||||
next: Plan;
|
||||
issuedCount: number;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const rows: { field: string; was: string; now: string }[] = [];
|
||||
if (plan.limits.max_servers !== next.limits.max_servers)
|
||||
rows.push({
|
||||
field: "max_servers",
|
||||
was: limitLabel(plan.limits.max_servers),
|
||||
now: limitLabel(next.limits.max_servers),
|
||||
});
|
||||
if (plan.limits.max_secret_groups !== next.limits.max_secret_groups)
|
||||
rows.push({
|
||||
field: "max_secret_groups",
|
||||
was: limitLabel(plan.limits.max_secret_groups),
|
||||
now: limitLabel(next.limits.max_secret_groups),
|
||||
});
|
||||
if (plan.limits.max_channels !== next.limits.max_channels)
|
||||
rows.push({
|
||||
field: "max_channels",
|
||||
was: limitLabel(plan.limits.max_channels),
|
||||
now: limitLabel(next.limits.max_channels),
|
||||
});
|
||||
if (plan.features.join(",") !== next.features.join(","))
|
||||
rows.push({
|
||||
field: "features",
|
||||
was: plan.features.join(", ") || "none",
|
||||
now: next.features.join(", ") || "none",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid max-w-xl gap-3 rounded border border-warn bg-panel p-5">
|
||||
<h2 className="text-xl">Change what {plan.name} grants?</h2>
|
||||
<ul className="grid gap-1 font-mono text-[0.82rem]">
|
||||
{rows.map((r) => (
|
||||
<li key={r.field} className="flex flex-wrap gap-2">
|
||||
<span className="text-ink-3">{r.field}</span>
|
||||
<span className="text-ink-3 line-through">{r.was}</span>
|
||||
<span className="font-semibold text-ink">→ {r.now}</span>
|
||||
</li>
|
||||
))}
|
||||
{rows.length === 0 && <li className="text-ink-3">Nothing would change.</li>}
|
||||
</ul>
|
||||
<p className="text-[0.82rem] text-ink-3">
|
||||
This applies to licences issued from now on. The {issuedCount} licences already
|
||||
issued keep what they were signed with until each is reissued.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button type="button" onClick={onConfirm}>
|
||||
Change plan
|
||||
</Button>
|
||||
<Button type="button" variant="line" onClick={onCancel}>
|
||||
Keep as is
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user