126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { ThemeSwitcher } from '../theme'
|
|
import { ROLE_LABEL } from '../auth'
|
|
|
|
/**
|
|
* The app shell's top bar: brand mark, title, and the signed-in user's
|
|
* controls. The board tab strip that used to live below it is now the left
|
|
* sidebar (`Sidebar.tsx`) — this component no longer owns navigation.
|
|
*
|
|
* The gradient is the one saturated surface in the product. Everything on it
|
|
* is white or a white alpha rather than a palette token, because a
|
|
* token-coloured control on the band reads as a hole punched through it —
|
|
* see `.sb-appbar` and `.sb-ghost-btn` in index.css.
|
|
*/
|
|
/** 'admin' is never a single global role any more — computed here from the
|
|
* two per-project flags so the header names exactly what this account can
|
|
* reach, rather than falling back to "Operator" for someone who is in fact
|
|
* an admin of one or both projects. */
|
|
function roleLabel(user: {
|
|
role: string
|
|
ecw_role?: string | null
|
|
tebra_role?: string | null
|
|
}): string {
|
|
if (user.role === 'super_admin') return ROLE_LABEL.super_admin
|
|
const ecw = user.ecw_role === 'admin'
|
|
const tebra = user.tebra_role === 'admin'
|
|
if (ecw && tebra) return 'Admin · Both'
|
|
if (ecw) return 'Admin · ECW'
|
|
if (tebra) return 'Admin · Tebra'
|
|
return ROLE_LABEL.operator
|
|
}
|
|
|
|
export function BrandHeader({
|
|
user,
|
|
onLogout,
|
|
sidebarOpen,
|
|
onToggleSidebar,
|
|
}: {
|
|
user: {
|
|
username: string
|
|
real_name: string | null
|
|
role: string
|
|
ecw_role?: string | null
|
|
tebra_role?: string | null
|
|
}
|
|
onLogout: () => void
|
|
/** Present only on screens that have the left sidebar. */
|
|
sidebarOpen?: boolean
|
|
onToggleSidebar?: () => void
|
|
}) {
|
|
return (
|
|
<header className="sb-appbar shrink-0">
|
|
<ShieldMark />
|
|
{onToggleSidebar && (
|
|
<button
|
|
type="button"
|
|
onClick={onToggleSidebar}
|
|
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
|
aria-label={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
|
aria-expanded={sidebarOpen}
|
|
className="sb-ghost-btn"
|
|
style={{ padding: '3px', lineHeight: 0 }}
|
|
>
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor"
|
|
strokeWidth="1.6" aria-hidden="true">
|
|
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
|
<line x1="9" y1="4.5" x2="9" y2="19.5" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
{/* What the tool is, matching the browser tab and the sign-in screen.
|
|
The brand and EMR edition — "Superbill Automation for ECW" — live in
|
|
the sidebar's own group headers instead (`Sidebar.tsx`), which is
|
|
where the rail has room to state them without competing here. */}
|
|
<div className="min-w-0">
|
|
<div className="sb-appbar-title truncate">Extraction and Automation Tool</div>
|
|
<div className="sb-appbar-sub truncate">Extract · correct · post to eClinicalWorks</div>
|
|
</div>
|
|
<div className="ml-auto flex shrink-0 items-center gap-3">
|
|
<div className="sb-who hidden sm:block">
|
|
<b>{user.real_name ?? user.username}</b>
|
|
<span>{roleLabel(user)}</span>
|
|
</div>
|
|
<button type="button" onClick={onLogout} title="Sign out" className="sb-ghost-btn">
|
|
Sign out
|
|
</button>
|
|
<ThemeSwitcher />
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The product mark: shield plus three ascending bars, drawn as vector.
|
|
*
|
|
* The same artwork the sign-in screen carries, so the two screens read as one
|
|
* product — but flat here, with no bob and no glow: on the sign-in screen it
|
|
* is the subject, and in a header it is a label for the page.
|
|
*
|
|
* Vector rather than the `logo-mark.png` raster it replaced. The raster could
|
|
* not take the header's drop-shadow without fringing along its antialiased
|
|
* edge, and it could not follow a theme.
|
|
*
|
|
* Colours are literal on purpose. The shield and its bars are artwork — the
|
|
* mark is the same mark in light and dark, the way a printed logo is — so
|
|
* these four values are deliberately NOT theme tokens. `--shield`, `--clean`,
|
|
* `--verify` and `--review` hold the same hexes for anything that needs to
|
|
* reference them.
|
|
*
|
|
* `aria-hidden` because the product is named in text immediately beside it.
|
|
*/
|
|
export function ShieldMark({ size = 34 }: { size?: number }) {
|
|
return (
|
|
<span className="sb-appbar-mark" style={{ width: size, height: size }}>
|
|
<svg viewBox="0 0 64 64" fill="none" aria-hidden="true" style={{ width: size, height: size }}>
|
|
<path
|
|
d="M32 5 L53 13 V34 C53 45.5 43.5 53.5 32 58.5 C20.5 53.5 11 45.5 11 34 V13 Z"
|
|
fill="#21395a"
|
|
/>
|
|
<rect x="19.5" y="32" width="7" height="10" rx="1.6" fill="#51a68f" />
|
|
<rect x="30" y="24" width="7" height="18" rx="1.6" fill="#e39b34" />
|
|
<rect x="40.5" y="18" width="7" height="24" rx="1.6" fill="#de5243" />
|
|
</svg>
|
|
</span>
|
|
)
|
|
}
|