Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/(admin)/admin/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export default async function AdminSettingsPage() {
{readiness ? (
<TenantReadinessChecklist
title="Tenant readiness"
description="Production launch checks for this company. Items show who owns the next action."
description="Production launch checks your team owns. Platform-managed items are handled by EstateOS."
items={readiness.checklist}
audience="tenant"
/>
) : null}

Expand Down
39 changes: 33 additions & 6 deletions src/app/app/access/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@ import Link from "next/link";

import { Card } from "@/components/ui/card";

function resolveCopy(status: string | undefined, reason: string | undefined) {
type AccessCopy = {
eyebrow: string;
title: string;
body: string;
reason?: string;
actionHref: string;
actionLabel: string;
/** Overrides the default "what this means" bullets (which describe suspension). */
points?: string[];
};

function resolveCopy(status: string | undefined, reason: string | undefined): AccessCopy {
if (status === "superadmin-setup") {
return {
eyebrow: "Platform owner setup required",
Expand All @@ -22,8 +33,18 @@ function resolveCopy(status: string | undefined, reason: string | undefined) {
body:
"Superadmin access is private and requires both an allowlisted email address and a persisted platform role.",
reason: "Use the normal admin or buyer workspace for this account.",
actionHref: "/",
actionLabel: "Return to homepage",
// Send tenant operators back to their own workspace, not the marketing
// homepage — this page is reached by an ordinary admin who simply hit a
// /superadmin URL.
actionHref: "/admin",
actionLabel: "Go to your dashboard",
// The default bullets describe a SUSPENDED company, which is wrong here:
// nothing is blocked for this user, they just aren't a platform owner.
points: [
"Your company workspace is unaffected — admin, portal, and payment actions all still work.",
"The platform owner dashboard is reserved for EstateOS staff.",
"If you reached this by mistake, continue in your own admin dashboard.",
],
};
}

Expand Down Expand Up @@ -80,9 +101,15 @@ export default async function AppAccessPage({
What this means
</h2>
<ul className="mt-4 space-y-3 text-sm leading-7 text-[var(--ink-600)]">
<li>Admin dashboards, portal access, and payment actions are currently blocked.</li>
<li>Super admins can still inspect the company safely from the platform command center.</li>
<li>Once the company is reactivated, normal access resumes automatically.</li>
{(
copy.points ?? [
"Admin dashboards, portal access, and payment actions are currently blocked.",
"Super admins can still inspect the company safely from the platform command center.",
"Once the company is reactivated, normal access resumes automatically.",
]
).map((point) => (
<li key={point}>{point}</li>
))}
</ul>
</div>

Expand Down
9 changes: 7 additions & 2 deletions src/components/shared/logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ export function Logo({
monogram
)}
</div>
<div className="min-w-0 flex-1 overflow-visible sm:min-w-[11rem]">
<div className="tenant-logo-name font-serif text-lg font-semibold leading-tight text-[var(--ink-950)]">
{/* min-w-0 lets this flex child shrink inside narrow shells (e.g. the
dashboard sidebar). A hard min-width here caused long company names
such as "Blueprint Urban Residences LTD" to overflow and get clipped
by the parent's overflow-hidden. The name wraps instead of truncating
so the full company name always stays readable. */}
<div className="min-w-0 flex-1">
<div className="tenant-logo-name break-words font-serif text-lg font-semibold leading-tight text-[var(--ink-950)]">
{name}
</div>
{showTagline && tagline ? (
Expand Down
25 changes: 21 additions & 4 deletions src/components/shared/tenant-readiness-checklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,35 @@ const statusClasses = {
missing: "border-rose-200 bg-rose-50 text-rose-800",
} as const;

/**
* Readiness items owned by EstateOS itself (platform Paystack keys, R2 storage,
* superadmin-only checks). A tenant admin can neither see nor action these — and
* their action links point at /superadmin/*, which tenants cannot open — so they
* are hidden from the tenant audience and shown only to superadmins.
*/
const PLATFORM_OWNERS = new Set(["Platform", "Superadmin"]);

export function isTenantVisibleReadinessItem(item: TenantReadinessItem) {
return !PLATFORM_OWNERS.has(item.owner) && !item.actionLink.startsWith("/superadmin");
}

export function TenantReadinessChecklist({
title = "Go-live readiness",
description = "Resolve these items before onboarding real buyers and payments.",
items,
audience = "superadmin",
}: {
title?: string;
description?: string;
items: TenantReadinessItem[];
/** "tenant" hides platform/superadmin-owned items. Defaults to the full list. */
audience?: "tenant" | "superadmin";
}) {
const missing = items.filter((item) => item.status === "missing").length;
const warnings = items.filter((item) => item.status === "warning").length;
const complete = items.filter((item) => item.status === "complete").length;
const visibleItems =
audience === "tenant" ? items.filter(isTenantVisibleReadinessItem) : items;
const missing = visibleItems.filter((item) => item.status === "missing").length;
const warnings = visibleItems.filter((item) => item.status === "warning").length;
const complete = visibleItems.filter((item) => item.status === "complete").length;

return (
<Card className="p-6">
Expand All @@ -43,7 +60,7 @@ export function TenantReadinessChecklist({
</div>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-2">
{items.map((item) => (
{visibleItems.map((item) => (
<div key={item.id} className="rounded-2xl border border-[var(--line)] bg-white p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
Expand Down
5 changes: 4 additions & 1 deletion src/modules/portal/payments-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ test("payment summary cards wrap long values instead of clipping", () => {
assert.match(logoSource, /showTagline = true/);
assert.match(logoSource, /showTagline && tagline/);
assert.match(dashboardShellSource, /showTagline=\{false\}/);
assert.match(logoSource, /overflow-visible/);
// The name wrapper uses break-words (not a hard min-width) so long company
// names wrap instead of overflowing the sidebar card.
assert.match(logoSource, /break-words/);
assert.doesNotMatch(logoSource, /sm:min-w-\[11rem\]/);
assert.doesNotMatch(logoSource, /tenant-logo-tagline[^\n]+whitespace-nowrap/);
assert.match(globalsSource, /--duration-fast: 120ms/);
assert.match(globalsSource, /--tenant-motion-duration: 160ms/);
Expand Down
Loading