Visual and information-architecture pass across every screen. No API call, payload, poll, permission rule or workflow mirror changes; forms still render from the live schema and the timeline's merge/grouping is unchanged (only the display order is reversible). Design language - Ground + white cards with one hairline and one radius; sentence-case headings; shared primitives in index.css (.card .pill .btn .avatar .live). - One inline icon set (components/Icon.jsx); no more filled navy/red tiles. - Read-side labels in api/display.js: roles and enum tokens humanized, customer initials/tones, money and date formatters. Nothing sent changes. Shell - 56px header with brand, global lead search (portfolio rows, "/" to focus) and identity chip with a human role label. - Sidebar rows carry icons and a clear active bar; AI roster at the foot, each opening its remit card. Login - Operator copy instead of retail copy; centred sign-in card with show/hide password; demo persona picker that fills the EMAIL only. Overview - "Who holds the work" as four quiet cards that warm only when owed. - Pipeline flow bar with every stage named beneath it; KPI strip in a card. - Action rows with chevron / view-only eye; expiry bands filter the table. Queues - Filter bar (search, product, channel, expiry band, stalled, hide closed) and sortable columns, all client-side on the fetched page. - Avatars, product/channel chips, stage chips for closed leads, tinted stalled rows, count inline with the title. Lead - Avatar + chips in the header; metrics card with the stage rail inside, and a branch node for Referred / Parked / Lost / Declined. - Timeline newest-first by default (toggle), the worker's face as the node, one footer row per entry, AI markdown rendered as bold. - "AI team on this lead" panel with per-agent step counts; PDF tags on docs. Forms & dialogs - Required marked with an asterisk + legend; "Select…" placeholder; dd Mon yyyy hint under date inputs; open activity form in a titled panel; one-line dialog footnotes. Fixes: horizontal overflow at 1024px, boxed "in N days" figure, raw tokens (sme_underwriter, verbal_call, Rsa), findings cut at "1.". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
149 lines
6.2 KiB
JavaScript
149 lines
6.2 KiB
JavaScript
import { useState } from 'react'
|
|
import { NavLink, Outlet } from 'react-router-dom'
|
|
import { NAV_GROUPS } from '../api/config.js'
|
|
import { AGENTS } from '../api/agents.js'
|
|
import { rolesOf, visibleStages } from '../api/permissions.js'
|
|
import { useStageCounts } from '../api/portfolio.jsx'
|
|
import { useZino } from '../api/provider.jsx'
|
|
import AgentCard from '../components/AgentCard.jsx'
|
|
import AgentChip from '../components/AgentChip.jsx'
|
|
import Icon from '../components/Icon.jsx'
|
|
import { KIND_ICON } from '../api/display.js'
|
|
import GlobalSearch from './GlobalSearch.jsx'
|
|
import UserMenu from './UserMenu.jsx'
|
|
import logo from '../assets/brand/zurich_logo.webp'
|
|
import './Shell.css'
|
|
|
|
/**
|
|
* The sidebar groups the pipeline by WHO IS HOLDING each lead, not by where it
|
|
* sits in the process. Fourteen flat stages answer "what is the process?", which
|
|
* is not the question anyone signing in has; these answer "is anything waiting
|
|
* on me?". Queues that need a person are named by what they want done.
|
|
*
|
|
* The counts come from PortfolioProvider — one fetch shared with the overview,
|
|
* so the two cannot disagree. The badge counts leads whose PHASE needs a
|
|
* person, not leads in the state (see phaseOf); a dot marks a queue holding a
|
|
* renewal inside a week.
|
|
*
|
|
* The closed states are folded away: three of the twelve, reporting rather
|
|
* than work, and at equal weight they made the live queues harder to find.
|
|
*/
|
|
export default function Shell() {
|
|
const { user } = useZino()
|
|
const roles = rolesOf(user)
|
|
// The sidebar shows the queues this role WORKS in. Reporting on the rest
|
|
// lives on the overview, which counts the whole book for everyone.
|
|
const stages = visibleStages(roles)
|
|
const { counts, ready } = useStageCounts()
|
|
const [openAgent, setOpenAgent] = useState(null)
|
|
|
|
// One queue row. Extracted because the live groups and the folded Closed
|
|
// group render the same thing and drifted apart when they did not.
|
|
const queue = (s) => {
|
|
const c = counts[s.uid] || { total: 0, waiting: 0, working: 0, urgent: 0 }
|
|
const n = s.kind === 'end' ? c.total : c.waiting
|
|
return (
|
|
<NavLink
|
|
key={s.uid}
|
|
to={`/stage/${s.uid}`}
|
|
title={s.need ? s.name : undefined}
|
|
className={({ isActive }) =>
|
|
'shell__stage' + (isActive ? ' is-active' : '')
|
|
+ (s.kind === 'needs' ? ' is-needed' : '')
|
|
+ (ready && !n && !c.working ? ' is-empty' : '')
|
|
}
|
|
>
|
|
<Icon name={KIND_ICON[s.kind] || 'list'} className="shell__ico" />
|
|
<span className="shell__stagename">{s.need ?? s.name}</span>
|
|
{ready ? (
|
|
<span className="shell__tally">
|
|
{c.urgent ? <span className="shell__urgent" title={`${c.urgent} expiring within 7 days`} /> : null}
|
|
{/* The agents' share is shown apart rather than added in: it is in
|
|
the queue, it is not your work. */}
|
|
{c.working ? <em className="shell__auto" title={`${c.working} being worked by an agent`}>{c.working}</em> : null}
|
|
<b className={'shell__n' + (n ? '' : ' is-zero')}>{n}</b>
|
|
</span>
|
|
) : null}
|
|
</NavLink>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="shell">
|
|
<header className="shell__top">
|
|
<div className="shell__brand">
|
|
<img src={logo} alt="Zurich Kotak General Insurance" />
|
|
<span className="shell__brandtext">Lead Desk</span>
|
|
</div>
|
|
<GlobalSearch />
|
|
<UserMenu />
|
|
</header>
|
|
|
|
<div className="shell__body">
|
|
<nav className="shell__nav" aria-label="Pipeline">
|
|
<div className="shell__navtop">
|
|
<NavLink to="/" end className={({ isActive }) => 'shell__today' + (isActive ? ' is-active' : '')}>
|
|
<Icon name="home" className="shell__ico" />
|
|
Overview
|
|
</NavLink>
|
|
<NavLink to="/stage/all" className={({ isActive }) => 'shell__today' + (isActive ? ' is-active' : '')}>
|
|
<Icon name="list" className="shell__ico" />
|
|
All leads
|
|
</NavLink>
|
|
</div>
|
|
|
|
{NAV_GROUPS.filter((g) => g.kind !== 'end').map((group) => {
|
|
const inGroup = stages.filter((s) => s.kind === group.kind)
|
|
if (!inGroup.length) return null
|
|
const outstanding = inGroup.reduce((t, s) => t + (counts[s.uid]?.waiting || 0), 0)
|
|
return (
|
|
<div className="shell__group" key={group.kind}>
|
|
<p className="shell__navlabel">
|
|
{group.label}
|
|
{ready && group.kind === 'needs' && outstanding ? (
|
|
<span className="shell__grouptally">{outstanding}</span>
|
|
) : null}
|
|
</p>
|
|
<div className="shell__stages">{inGroup.map(queue)}</div>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{/* Closed. Folded, because it is reporting rather than work. */}
|
|
{stages.some((s) => s.kind === 'end') ? (
|
|
<details className="shell__closed">
|
|
<summary>
|
|
<Icon name="chevronRight" className="shell__caret" />
|
|
Closed
|
|
{ready ? (
|
|
<b>{stages.filter((s) => s.kind === 'end')
|
|
.reduce((t, s) => t + (counts[s.uid]?.total || 0), 0)}</b>
|
|
) : null}
|
|
</summary>
|
|
<div className="shell__stages">{stages.filter((s) => s.kind === 'end').map(queue)}</div>
|
|
</details>
|
|
) : null}
|
|
|
|
{/* The team that carries the work. Five employees, each one click
|
|
from what it does and what it may touch. */}
|
|
<div className="shell__team">
|
|
<p className="shell__navlabel">AI employees</p>
|
|
<div className="shell__teamrow">
|
|
{Object.keys(AGENTS).map((k) => (
|
|
<AgentChip key={k} agentKey={k} size="md" showName={false} onOpen={setOpenAgent} />
|
|
))}
|
|
</div>
|
|
<span className="shell__teamnote">Working every open lead. Click one to see its remit.</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="shell__main">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
|
|
{openAgent ? <AgentCard agentKey={openAgent} onClose={() => setOpenAgent(null)} /> : null}
|
|
</div>
|
|
)
|
|
}
|