feat(tenant): hide setup/settings surfaces when HELIX_SETUP_MANAGED

Ramaiah's product team owns their setup; end-user admins shouldn't see a dead-end Settings nav + Resume Setup banner. Flag is read from /api/config/ui-flags at app boot.

- use-ui-flags: module-scoped cache + useUiFlags hook + getUiFlags
  helper for non-component callers
- main.tsx: /setup redirects when managed; RequireSelfServeSetup
  guard blocks /settings/*
- resume-setup-banner: suppressed when managed
- login.tsx: skip first-run /setup redirect when managed
- settings.tsx: remove orphan popup-modal scaffolding left over
  from an earlier 'contact product team' approach
- section-card: support onClick-or-href (kept for future use)
This commit is contained in:
2026-04-15 18:56:19 +05:30
parent 196a18fe1a
commit 00c28e642b
5 changed files with 114 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ import { faCircleInfo, faXmark, faArrowRight } from '@fortawesome/pro-duotone-sv
import { Button } from '@/components/base/buttons/button';
import { getSetupState, SETUP_STEP_NAMES, type SetupState } from '@/lib/setup-state';
import { useAuth } from '@/providers/auth-provider';
import { useUiFlags } from '@/hooks/use-ui-flags';
// Dismissible banner shown across the top of authenticated pages when
// the hospital workspace has incomplete setup steps AND the admin has
@@ -19,22 +20,23 @@ import { useAuth } from '@/providers/auth-provider';
// - Not dismissed in the current browser session (resets on reload)
export const ResumeSetupBanner = () => {
const { isAdmin } = useAuth();
const { setupManaged } = useUiFlags();
const [state, setState] = useState<SetupState | null>(null);
const [dismissed, setDismissed] = useState(
() => sessionStorage.getItem('helix_resume_setup_dismissed') === '1',
);
useEffect(() => {
if (!isAdmin || dismissed) return;
if (!isAdmin || dismissed || setupManaged) return;
getSetupState()
.then(setState)
.catch(() => {
// Non-fatal — if setup-state isn't reachable, just
// skip the banner. The wizard still works.
});
}, [isAdmin, dismissed]);
}, [isAdmin, dismissed, setupManaged]);
if (!isAdmin || !state || dismissed) return null;
if (!isAdmin || !state || dismissed || setupManaged) return null;
const incompleteCount = SETUP_STEP_NAMES.filter((s) => !state.steps[s].completed).length;
if (incompleteCount === 0) return null;

View File

@@ -10,27 +10,32 @@ type SectionCardProps = {
description: string;
icon: any;
iconColor?: string;
href: string;
// Either navigate (href) OR intercept the click (onClick). When onClick
// is provided, href is ignored and the card renders as a button. Used
// while self-serve setup is disabled — all clicks go through a
// "contact product team" modal in settings.tsx.
href?: string;
onClick?: () => void;
status?: SectionStatus;
};
// Settings hub card. Each card represents one setup-able section (Branding,
// Clinics, Doctors, Team, Telephony, AI, Widget, Rules) and links to its
// dedicated page. The status badge mirrors the wizard's setup-state so an
// admin can see at a glance which sections still need attention.
// Clinics, Doctors, Team, Telephony, AI, Widget, Rules) and either links to
// its dedicated page or triggers a parent-owned callback.
export const SectionCard = ({
title,
description,
icon,
iconColor = 'text-brand-primary',
href,
onClick,
status = 'unknown',
}: SectionCardProps) => {
return (
<Link
to={href}
className="group block rounded-xl border border-secondary bg-primary p-5 shadow-xs transition hover:border-brand hover:shadow-md"
>
const className = cx(
'group block w-full text-left rounded-xl border border-secondary bg-primary p-5 shadow-xs transition hover:border-brand hover:shadow-md',
);
const body = (
<>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4">
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-secondary">
@@ -62,6 +67,19 @@ export const SectionCard = ({
)}
</div>
)}
</>
);
if (onClick) {
return (
<button type="button" onClick={onClick} className={className}>
{body}
</button>
);
}
return (
<Link to={href ?? '#'} className={className}>
{body}
</Link>
);
};