zurich_kotak/src/layout/Shell.jsx
Yashas 141b1b2452 console: the real mark in the masthead, not type set to look like it
The brand was being drawn: a circled "Z" in Source Sans beside "ZURICH kotak"
in letter-spaced caps with "General Insurance" under it. That is a passable
imitation of a logo and not the logo — the real mark's proportions, its weight
contrast and its plate are all in the asset, which the login page has been
using all along.

So the masthead carries the artwork, at half the bar's height, and nothing is
set in type beside it: the asset already says both lines, and the wordmark
next to it was the same sentence twice. Bundled from src/assets rather than
public/, so Vite rewrites the URL relative and it resolves under the
/zurich-kotak/ mount.

On a phone the mark shrinks rather than shedding its second line — that line
is inside the artwork now, so it cannot be dropped on its own the way the type
lockup's could.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 18:55:52 +05:30

160 lines
7.0 KiB
JavaScript

import { useState } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import { ASK_DESK_ROLES, NAV_GROUPS, STAGES } from '../api/config.js'
import { rolesOf, visibleStages } from '../api/permissions.js'
import { useStageCounts } from '../api/portfolio.jsx'
import { useZino } from '../api/provider.jsx'
import AskDesk from '../components/AskDesk.jsx'
import UserMenu from './UserMenu.jsx'
import logo from '../assets/brand/zurich_logo.webp'
import './Shell.css'
/**
* The frame: a navy masthead, a white rail, a grey canvas.
*
* The rail groups the pipeline by WHO IS HOLDING each lead — a person, the
* customer, the calendar — because "is anything waiting on me?" is the question
* anyone signing in actually has. The count on each row is what needs a person;
* its colour says how urgently, on the one colour rule the whole console keeps:
* red for risk, amber for a person, teal for the customer, blue for the AI.
*
* Closed lives under History and stays folded: it is reporting, not work.
*/
/** The badge colour for a queue: the colour of whoever is being waited on. */
function tone(stage) {
if (stage.kind === 'needs') return 'red'
if (stage.kind === 'customer') return 'amber'
return 'grey'
}
export default function Shell() {
const { user } = useZino()
const roles = rolesOf(user)
const stages = visibleStages(roles)
const { counts, ready } = useStageCounts()
// On a phone the rail becomes a drawer. It is closed on every navigation,
// because a menu that stays open over the page you just chose is a menu you
// have to dismiss twice. Desktop never reads this — the rail is always there.
const [navOpen, setNavOpen] = useState(false)
// The support desk reads the WHOLE book — every partner, every premium — so
// it is an ops tool, not something a partner agent may open. The platform
// enforces this too (the desk is assigned per user); this only decides
// whether the button is drawn.
const [deskOpen, setDeskOpen] = useState(false)
const canAskDesk = ASK_DESK_ROLES.some((r) => roles.includes(r))
// Closing on the link rather than on the route: a menu that stays open over
// the page you just chose is a menu you have to dismiss twice. Keyed off the
// anchor so opening the Closed fold — which is not navigation — leaves it up.
const closeIfLink = (e) => { if (e.target.closest('a')) setNavOpen(false) }
const count = (s) => counts[s.uid] || { total: 0, waiting: 0, working: 0, urgent: 0 }
// "All leads" carries the open book, not the whole book.
const openTotal = STAGES
.filter((s) => s.kind !== 'end')
.reduce((t, s) => t + count(s).total, 0)
const row = (s) => {
const c = count(s)
const n = s.kind === 'end' ? c.total : c.waiting
const hint = c.working ? `${c.working} being worked by the AI` : undefined
return (
<NavLink
key={s.uid}
to={`/stage/${s.uid}`}
title={hint}
className={({ isActive }) => 'nav__link' + (isActive ? ' is-on' : '')}
>
<span className="nav__name">{s.need ?? s.name}</span>
{ready ? <span className={`nav__n nav__n--${n ? tone(s) : 'grey'}`}>{n}</span> : null}
</NavLink>
)
}
return (
<div className="shell">
<header className="shell__top">
<button
type="button" className="mast__burger"
aria-label={navOpen ? 'Close the menu' : 'Open the menu'} aria-expanded={navOpen}
onClick={() => setNavOpen((o) => !o)}
>
<svg viewBox="0 0 20 20" aria-hidden="true">
{navOpen
? <path d="M5 5l10 10M15 5L5 15" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
: <path d="M3 6h14M3 10h14M3 14h14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />}
</svg>
</button>
{/* The lockup as ARTWORK, not as type. It was set in Source Sans with
a drawn Z, which is a passable imitation of a brand and not the
brand: the real mark's proportions, its weight contrast and its
plate are all in the asset. Bundled from src/assets so Vite
rewrites the URL and it resolves under the sub-path mount. */}
<img className="mast__logo" src={logo} alt="Zurich Kotak General Insurance" />
<span className="mast__sep" aria-hidden="true" />
<div className="mast__app">Lead Desk <span>Lead to policy</span></div>
{canAskDesk ? (
<button type="button" className="mast__desk" onClick={() => setDeskOpen(true)}>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z" fill="none" stroke="currentColor"
strokeWidth="1.3" strokeLinejoin="round" />
</svg>
Ask the desk
</button>
) : null}
<div className="mast__user"><UserMenu /></div>
</header>
<div className="shell__body">
{/* The scrim only exists while the drawer is open, and only on a phone:
tapping the page you can see should put the menu away. */}
{navOpen ? <button type="button" className="shell__scrim" aria-label="Close the menu" onClick={() => setNavOpen(false)} /> : null}
<nav className={'shell__nav' + (navOpen ? ' is-open' : '')} aria-label="Pipeline" onClick={closeIfLink}>
<NavLink to="/" end className={({ isActive }) => 'nav__link' + (isActive ? ' is-on' : '')}>
<span className="nav__name">Overview</span>
</NavLink>
<NavLink to="/stage/all" className={({ isActive }) => 'nav__link' + (isActive ? ' is-on' : '')}>
<span className="nav__name">All leads</span>
{ready ? <span className="nav__n nav__n--grey">{openTotal}</span> : null}
</NavLink>
{NAV_GROUPS.map((group) => {
const inGroup = stages.filter((s) => group.kinds.includes(s.kind))
if (!inGroup.length) return null
if (group.kinds.includes('end')) {
const closed = inGroup.reduce((t, s) => t + count(s).total, 0)
return (
<div className="nav__group" key={group.label}>
<h6 className="nav__h">{group.label}</h6>
<details className="nav__fold">
<summary className="nav__link">
<span className="nav__name">Closed</span>
{ready ? <span className="nav__n nav__n--grey">{closed}</span> : null}
</summary>
<div className="nav__sub">{inGroup.map(row)}</div>
</details>
</div>
)
}
return (
<div className="nav__group" key={group.label}>
<h6 className="nav__h">{group.label}</h6>
{inGroup.map(row)}
</div>
)
})}
</nav>
<main className="shell__main">
<Outlet />
</main>
</div>
{deskOpen ? <AskDesk onClose={() => setDeskOpen(false)} /> : null}
</div>
)
}