Implements the design in zurich-kotak-leaddesk across every screen. Foundation: Source Sans 3 bundled from src/assets/fonts (no Google Fonts request), 15px root, navy/blue/amber/teal/red palette where blue is the AI, amber a person, teal the customer and red is reserved for risk; flat white 10px cards on a grey canvas, no shadows. Shell: 56px navy masthead (Z wordmark, Lead Desk, user), 232px white nav grouped Needs you / Scheduled / History with coloured count pills and the closed stages folded. Overview: the hero says who holds each open lead (stacked bar + legend) beside the renewals-at-risk card; five KPIs incl. Closed to date; Waiting on a person queue with owner dots and "N more with the AI"; Pipeline by stage with owner-coloured bars; Renewal exposure with tabs, dropdown filters and a Holding it column. Lead: back button, id line, holder pill; one attention banner (amber your turn / blocked, red stalled, blue with the AI, teal with the customer) that carries its button; four facts with a confidence bar; navy steps rail whose current step takes its holder's colour; timeline as an avatar gutter with title, stage pill, by-line + AI tag and text-link actions (See reasoning · N steps, Hear the call, Open thread); side column flows. Lists: card with search + hide-closed toggle, columns Customer / Stage / Renewal due / Holding it / Premium / Last activity, stalled rows get a red inset, closed rows dim, person-gated queues split into Your turn / With the AI group rows. src/api/holder.js is the single rule for holder tone + label so the table cells, the overview and the lead header cannot disagree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
92 lines
3.2 KiB
JavaScript
92 lines
3.2 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useZino } from '../api/provider.jsx'
|
|
import './UserMenu.css'
|
|
|
|
/** First letters of the first two words — a name is all we have for an avatar. */
|
|
function initials(user) {
|
|
const from = user?.name || user?.email || ''
|
|
const parts = from.replace(/@.*$/, '').split(/[\s._-]+/).filter(Boolean)
|
|
if (!parts.length) return '?'
|
|
return (parts[0][0] + (parts[1]?.[0] || '')).toUpperCase()
|
|
}
|
|
|
|
/**
|
|
* The signed-in identity, with sign-out folded inside it.
|
|
*
|
|
* The role stays visible on the trigger rather than only inside the menu: RBAC is
|
|
* the demo, so who you are signed in as should never take a click to find out.
|
|
*/
|
|
export default function UserMenu() {
|
|
const { user, signOut } = useZino()
|
|
const [open, setOpen] = useState(false)
|
|
const wrapRef = useRef(null)
|
|
const triggerRef = useRef(null)
|
|
|
|
// Close on an outside click or Escape. Bound only while open, so the shell
|
|
// costs nothing at rest.
|
|
useEffect(() => {
|
|
if (!open) return
|
|
function onDown(e) {
|
|
if (!wrapRef.current?.contains(e.target)) setOpen(false)
|
|
}
|
|
function onKey(e) {
|
|
if (e.key !== 'Escape') return
|
|
setOpen(false)
|
|
triggerRef.current?.focus()
|
|
}
|
|
document.addEventListener('mousedown', onDown)
|
|
document.addEventListener('keydown', onKey)
|
|
return () => {
|
|
document.removeEventListener('mousedown', onDown)
|
|
document.removeEventListener('keydown', onKey)
|
|
}
|
|
}, [open])
|
|
|
|
const name = user?.name || user?.email || 'Signed in'
|
|
// Role slugs read as titles: partner_agent -> Partner agent.
|
|
const roles = (user?.roles || [])
|
|
// `sme_underwriter` → "SME underwriter": the acronyms the roles here use stay upper.
|
|
.map((r) => String(r).replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
|
|
.replace(/\b(sme|uw|rm|posp|csr|ops|kyc)\b/gi, (m) => m.toUpperCase()))
|
|
.join(', ')
|
|
|
|
return (
|
|
<div className="um" ref={wrapRef}>
|
|
<button
|
|
ref={triggerRef}
|
|
type="button"
|
|
className={'um__trigger' + (open ? ' is-open' : '')}
|
|
onClick={() => setOpen((o) => !o)}
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
>
|
|
<span className="um__avatar" aria-hidden="true">{initials(user)}</span>
|
|
<span className="um__id">
|
|
<strong>{name}</strong>
|
|
<span>{roles || user?.email}</span>
|
|
</span>
|
|
<svg className="um__caret" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
|
|
<path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" strokeWidth="1.4"
|
|
strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</button>
|
|
|
|
{open ? (
|
|
<div className="um__menu" role="menu">
|
|
<div className="um__head">
|
|
<span className="um__avatar um__avatar--lg" aria-hidden="true">{initials(user)}</span>
|
|
<div className="um__headid">
|
|
<strong>{name}</strong>
|
|
{user?.email ? <span>{user.email}</span> : null}
|
|
{roles ? <span className="um__roles">{roles}</span> : null}
|
|
</div>
|
|
</div>
|
|
<button type="button" role="menuitem" className="um__item" onClick={signOut}>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|