zurich_kotak_pwa/src/layout/Shell.jsx
Yashas 5b30cfd647 The real mark in the app bar
Same change as the console's masthead, in the two places this app draws the
brand: the app bar and the drawer head. The circled "Z" and the letter-spaced
wordmark were type imitating a logo; the asset is the logo, and it carries
both lines of the wordmark on its own plate, so nothing is set beside it.

26px on a 56px bar leaves the burger a thumb's width to its left and the
avatar to its right at 390px.

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

185 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { NavLink, Outlet, useNavigate } 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 logo from '../assets/brand/zurich_logo.webp'
/**
* The frame: a navy bar that stays put, and a drawer behind the hamburger.
*
* On a phone the queues cannot live beside the content, and stacked above it
* they cost a screenful before the first lead. So they go in a drawer, and the
* bar carries the only two things worth permanent space: where you are, and
* who you are.
*/
/** Two letters for the avatar: a name if we have one, else the local part. */
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 badge colour is 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'
}
/** A queue's icon, chosen for what it is waiting on rather than decoration. */
function NavIcon({ kind }) {
if (kind === 'needs') {
return <svg viewBox="0 0 20 20" fill="none"><path d="M6 2.5h5.5L15 6v11.5H6z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/><path d="M11 2.5V6h4" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
}
if (kind === 'customer') {
return <svg viewBox="0 0 20 20" fill="none"><path d="M3 4.5h14v9h-8l-3.5 3v-3H3z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
}
if (kind === 'waiting') {
return <svg viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7" stroke="currentColor" strokeWidth="1.5"/><path d="M10 6v4.2l2.6 1.6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
}
return <svg viewBox="0 0 20 20" fill="none"><path d="M4 10.5 8 14l8-8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
}
export default function Shell() {
const { user, signOut } = useZino()
const navigate = useNavigate()
const roles = rolesOf(user)
const stages = visibleStages(roles)
const { counts, ready } = useStageCounts()
const [open, setOpen] = useState(false)
const [me, setMe] = useState(false)
// The support desk reads the WHOLE book — every partner, every premium — so
// it is an ops tool rather than something a partner agent may open. The
// platform enforces that too (the desk is assigned per user); this only
// decides whether the entry is drawn.
const [desk, setDesk] = useState(false)
const canAskDesk = ASK_DESK_ROLES.some((r) => roles.includes(r))
const count = (s) => counts[s.uid] || { total: 0, waiting: 0, working: 0, urgent: 0 }
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 t = tone(s)
return (
<NavLink
key={s.uid} to={`/stage/${s.uid}`} onClick={() => setOpen(false)}
className={({ isActive }) => 'nav nav--' + (s.kind === 'customer' ? 'teal' : s.kind === 'needs' ? 'red' : 'blue') + (isActive ? ' is-on' : '')}
>
<span className="nav__i" aria-hidden="true"><NavIcon kind={s.kind} /></span>
<span className="nav__t">{s.need ?? s.name}</span>
{ready ? <span className={`nav__n nav__n--${n ? t : 'grey'}`}>{n}</span> : null}
</NavLink>
)
}
return (
<div className="shell">
<header className="appbar">
<button type="button" className="appbar__burger" aria-label="Open the menu" onClick={() => setOpen(true)}>
<svg viewBox="0 0 22 22" fill="none"><path d="M3 6h16M3 11h16M3 16h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
</button>
{/* The real mark rather than type set to look like it — see the
console's masthead. Bundled from src/assets, so the URL is
rewritten relative and resolves under the sub-path mount. */}
<img className="appbar__logo" src={logo} alt="Zurich Kotak General Insurance" />
<button type="button" className="appbar__me" aria-label="Your account" onClick={() => setMe(true)}>
{initials(user)}
</button>
</header>
<main>
<Outlet />
</main>
{open ? (
<>
<div className="scrim" role="presentation" onClick={() => setOpen(false)} />
<nav className="drawer" aria-label="Queues">
<div className="drawer__head">
<button type="button" className="drawer__x" aria-label="Close the menu" onClick={() => setOpen(false)}>
<svg viewBox="0 0 20 20" fill="none"><path d="M5 5l10 10M15 5L5 15" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></svg>
</button>
<img className="appbar__logo" src={logo} alt="Zurich Kotak General Insurance" />
</div>
<div className="drawer__body">
<NavLink to="/" end onClick={() => setOpen(false)} className={({ isActive }) => 'nav' + (isActive ? ' is-on' : '')}>
<span className="nav__i" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none"><path d="M3.5 3.5h6v6h-6zM10.5 3.5h6v6h-6zM3.5 10.5h6v6h-6zM10.5 10.5h6v6h-6z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
</span>
<span className="nav__t">Overview</span>
</NavLink>
<NavLink to="/stage/all" onClick={() => setOpen(false)} className={({ isActive }) => 'nav' + (isActive ? ' is-on' : '')}>
<span className="nav__i" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none"><circle cx="7" cy="7" r="2.6" stroke="currentColor" strokeWidth="1.5"/><path d="M2.5 16a4.5 4.5 0 0 1 9 0" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><path d="M13 5.2a2.6 2.6 0 0 1 0 5M14.5 16a4.4 4.4 0 0 0-2-3.7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
</span>
<span className="nav__t">All leads</span>
{ready ? <span className="nav__n">{openTotal}</span> : null}
</NavLink>
{NAV_GROUPS.map((group) => {
const inGroup = stages.filter((s) => group.kinds.includes(s.kind))
if (!inGroup.length) return null
return (
<div key={group.label}>
<h6 className="drawer__h">{group.label}</h6>
{inGroup.map(row)}
</div>
)
})}
{canAskDesk ? (
<div>
<h6 className="drawer__h">Ask</h6>
<button
type="button" className="nav nav--desk"
onClick={() => { setOpen(false); setDesk(true) }}
>
<span className="nav__i" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none"><path d="M3 4.5h14v9h-8l-3.5 3v-3H3z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
</span>
<span className="nav__t">Ask the desk</span>
</button>
</div>
) : null}
</div>
</nav>
</>
) : null}
{me ? (
<>
<div className="scrim" style={{ zIndex: 60 }} role="presentation" onClick={() => setMe(false)} />
<div className="sheet" role="dialog" aria-label="Your account">
<span className="sheet__grab" aria-hidden="true" />
<div className="sheet__head">
<div>
<h2>{user?.name || user?.email || 'Signed in'}</h2>
<p>{user?.email}</p>
</div>
<button type="button" className="sheet__x" aria-label="Close" onClick={() => setMe(false)}>×</button>
</div>
<div className="sheet__body">
<button
type="button" className="btn btn--ghost btn--block"
onClick={() => { setMe(false); signOut(); navigate('/login') }}
>
Sign out
</button>
</div>
</div>
</>
) : null}
{desk ? <AskDesk onClose={() => setDesk(false)} /> : null}
</div>
)
}