feat(portal): M10.1 — fill the 10 customer-area shells
Four real surfaces wired to tenant-registry (settings, settings/api-keys CRUD, audit pagination, products live entitlements), five forward-looking empty states with CTAs. 56 vitest tests + 10 Playwright canaries. lib/format.ts consolidates date helpers. Refs: M10.1
This commit was merged in pull request #12.
This commit is contained in:
@@ -1,16 +1,120 @@
|
||||
import { auth } from "@/auth";
|
||||
import { NotAuthorized, ShellEmpty } from "@/components/ShellEmpty";
|
||||
import { NotAuthorized } from "@/components/ShellEmpty";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import type { SessionWithExtras } from "@/lib/session";
|
||||
import { canSee } from "@/lib/session";
|
||||
import { fetchTenantBySlug } from "@/lib/tenant-registry";
|
||||
|
||||
export default async function Page() {
|
||||
export default async function SettingsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const session = (await auth()) as SessionWithExtras | null;
|
||||
if (!canSee(session, "settings")) return <NotAuthorized />;
|
||||
|
||||
const tenant = await fetchTenantBySlug(slug);
|
||||
if (!tenant) {
|
||||
return (
|
||||
<section>
|
||||
<h1 style={{ fontSize: 28 }}>Settings</h1>
|
||||
<p style={{ color: "#a82626" }}>Tenant not found.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ShellEmpty
|
||||
title="Settings"
|
||||
description="Tenant identity, SSO, organization defaults."
|
||||
milestone="M10.1"
|
||||
/>
|
||||
<section>
|
||||
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Settings</h1>
|
||||
<p style={{ color: "#444", marginBottom: 24 }}>
|
||||
Tenant identity and lifecycle metadata. Editing these lands in the
|
||||
M10.1 follow-up; for now contact <a href={`/${slug}/support`}>support</a>.
|
||||
</p>
|
||||
|
||||
<h2 style={{ fontSize: 18, marginTop: 16, marginBottom: 8 }}>Identity</h2>
|
||||
<Field label="Tenant ID" value={tenant.id} mono />
|
||||
<Field label="Slug" value={tenant.slug} mono />
|
||||
<Field label="Name" value={tenant.name} />
|
||||
<Field label="Kind" value={tenant.kind} />
|
||||
|
||||
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>Plan & status</h2>
|
||||
<Field label="Plan" value={tenant.plan} />
|
||||
<Field label="Status" value={tenant.status} badge={statusColor(tenant.status)} />
|
||||
{tenant.trial_ends_at && (
|
||||
<Field label="Trial ends at" value={formatDateTime(tenant.trial_ends_at)} mono />
|
||||
)}
|
||||
|
||||
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>Audit</h2>
|
||||
<Field label="Created" value={formatDateTime(tenant.created_at)} mono />
|
||||
<Field label="Last updated" value={formatDateTime(tenant.updated_at)} mono />
|
||||
|
||||
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>External links</h2>
|
||||
<p style={{ fontSize: 13, color: "#666" }}>
|
||||
ERPNext customer + Polar subscription land in M8.3; rendered here when
|
||||
the IDs land on the tenant row.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
mono,
|
||||
badge,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
badge?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", padding: "6px 0", borderBottom: "1px solid #f0f0f0" }}>
|
||||
<span style={{ width: 160, color: "#666", fontSize: 13 }}>{label}</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono
|
||||
? "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
||||
: "inherit",
|
||||
fontSize: mono ? 13 : 14,
|
||||
}}
|
||||
>
|
||||
{badge ? (
|
||||
<span
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
background: badge,
|
||||
color: "#fff",
|
||||
fontSize: 12,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.4,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusColor(s: string): string {
|
||||
switch (s) {
|
||||
case "active":
|
||||
return "#1a7a3e";
|
||||
case "trial":
|
||||
return "#a87a00";
|
||||
case "frozen":
|
||||
return "#a82626";
|
||||
case "archived":
|
||||
return "#666";
|
||||
case "demo":
|
||||
return "#0070f3";
|
||||
default:
|
||||
return "#444";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user