zurich_kotak/src/layout/Shell.jsx
Yashas e90e4204ec console: redesign the UI — one card language, icons, filters, live trail
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>
2026-09-09 11:37:42 +05:30

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>
)
}