console: the nav navigates, and the screens act
"New lead" sat in the sidebar. A sidebar is for moving between places; filing a lead is an action, and an action in a list of destinations is the one item that does not behave like its neighbours — everything above and below it changes what you are looking at, and it did something. It now sits on the overview and on every queue, beside the lists it adds to, which is also where somebody is standing when they realise they have a lead to file. Same role gate as before: shown only to a user who holds an entry door. The sidebar's own button CSS went with it rather than being left for someone to wonder about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
33bf65c5f5
commit
de8b158ca9
@ -195,41 +195,6 @@
|
|||||||
animation: zk-rise 0.28s var(--ease) both;
|
animation: zk-rise 0.28s var(--ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Primary action ────────────────────────────────────────── */
|
|
||||||
.shell__add {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 0 2px 22px;
|
|
||||||
padding: 11px 14px;
|
|
||||||
border-radius: var(--r-md);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--zk-white);
|
|
||||||
background: var(--grad-blue);
|
|
||||||
box-shadow: var(--sh-sm);
|
|
||||||
transition: box-shadow var(--t), transform var(--t-fast), background var(--t);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell__add:hover {
|
|
||||||
background: var(--grad-blue-hover);
|
|
||||||
box-shadow: var(--sh-md);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell__add:active {
|
|
||||||
transform: none;
|
|
||||||
box-shadow: var(--sh-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell__add svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Narrow viewports: the rail lies down and scrolls ──────── */
|
/* ── Narrow viewports: the rail lies down and scrolls ──────── */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.shell__body {
|
.shell__body {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NavLink, Outlet } from 'react-router-dom'
|
import { NavLink, Outlet } from 'react-router-dom'
|
||||||
import { ENTRY, NAV_GROUPS } from '../api/config.js'
|
import { NAV_GROUPS } from '../api/config.js'
|
||||||
import { entryDoorsFor, rolesOf, visibleStages } from '../api/permissions.js'
|
import { rolesOf, visibleStages } from '../api/permissions.js'
|
||||||
import { useStageCounts } from '../api/portfolio.jsx'
|
import { useStageCounts } from '../api/portfolio.jsx'
|
||||||
import { useZino } from '../api/provider.jsx'
|
import { useZino } from '../api/provider.jsx'
|
||||||
import UserMenu from './UserMenu.jsx'
|
import UserMenu from './UserMenu.jsx'
|
||||||
@ -36,7 +36,6 @@ export default function Shell() {
|
|||||||
// The sidebar shows the queues this role WORKS in. Reporting on the rest
|
// The sidebar shows the queues this role WORKS in. Reporting on the rest
|
||||||
// lives on the overview, which counts the whole book for everyone.
|
// lives on the overview, which counts the whole book for everyone.
|
||||||
const stages = visibleStages(roles)
|
const stages = visibleStages(roles)
|
||||||
const canFile = entryDoorsFor(roles, ENTRY).length > 0
|
|
||||||
const { counts, ready } = useStageCounts()
|
const { counts, ready } = useStageCounts()
|
||||||
|
|
||||||
// One queue row. Extracted because the live groups and the folded Closed
|
// One queue row. Extracted because the live groups and the folded Closed
|
||||||
@ -98,15 +97,11 @@ export default function Shell() {
|
|||||||
All leads
|
All leads
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{canFile ? (
|
{/* "New lead" used to sit here. A sidebar is for moving between
|
||||||
<NavLink to="/add" className="shell__add">
|
places; filing a lead is an ACTION, and an action in a list of
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
destinations is the one item that does not behave like its
|
||||||
<path d="M8 3.2v9.6M3.2 8h9.6" fill="none" stroke="currentColor" strokeWidth="1.7"
|
neighbours. It lives on the screens where leads are looked at —
|
||||||
strokeLinecap="round" />
|
the overview and the queues — beside the thing it adds to. */}
|
||||||
</svg>
|
|
||||||
New lead
|
|
||||||
</NavLink>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{NAV_GROUPS.filter((g) => g.kind !== 'end').map((group) => {
|
{NAV_GROUPS.filter((g) => g.kind !== 'end').map((group) => {
|
||||||
const inGroup = stages.filter((s) => s.kind === group.kind)
|
const inGroup = stages.filter((s) => s.kind === group.kind)
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import { useMemo } from 'react'
|
|||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { usePortfolio } from '../api/portfolio.jsx'
|
import { usePortfolio } from '../api/portfolio.jsx'
|
||||||
import { useZino } from '../api/provider.jsx'
|
import { useZino } from '../api/provider.jsx'
|
||||||
import { STAGES, phaseOf } from '../api/config.js'
|
import { ENTRY, STAGES, phaseOf } from '../api/config.js'
|
||||||
import { rolesOf, visibleStages } from '../api/permissions.js'
|
import { entryDoorsFor, rolesOf, visibleStages } from '../api/permissions.js'
|
||||||
import { describeError } from '../api/errors.js'
|
import { describeError } from '../api/errors.js'
|
||||||
import './screens.css'
|
import './screens.css'
|
||||||
|
|
||||||
@ -58,6 +58,7 @@ export default function Overview() {
|
|||||||
const { user } = useZino()
|
const { user } = useZino()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const roles = rolesOf(user)
|
const roles = rolesOf(user)
|
||||||
|
const canFile = entryDoorsFor(roles, ENTRY).length > 0
|
||||||
const state = usePortfolio()
|
const state = usePortfolio()
|
||||||
|
|
||||||
const m = useMemo(() => {
|
const m = useMemo(() => {
|
||||||
@ -167,11 +168,25 @@ export default function Overview() {
|
|||||||
<h1 className="page__title">Portfolio overview</h1>
|
<h1 className="page__title">Portfolio overview</h1>
|
||||||
<p className="page__sub">Motor and SME renewals · organisation-wide</p>
|
<p className="page__sub">Motor and SME renewals · organisation-wide</p>
|
||||||
</div>
|
</div>
|
||||||
{state.at ? (
|
{/* The action sits with the thing it acts on. It was in the sidebar,
|
||||||
<span className="stamp">
|
which is for moving between places — an action among destinations
|
||||||
Updated {state.at.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' })}
|
is the one item that does not behave like its neighbours. */}
|
||||||
</span>
|
<div className="page__right">
|
||||||
) : null}
|
{state.at ? (
|
||||||
|
<span className="stamp">
|
||||||
|
Updated {state.at.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{canFile ? (
|
||||||
|
<Link to="/add" className="newlead">
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 3.2v9.6M3.2 8h9.6" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.7" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
New lead
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="kpis">
|
<div className="kpis">
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useZino } from '../api/provider.jsx'
|
import { useZino } from '../api/provider.jsx'
|
||||||
import { CHANNELS, RV_LEADS, STAGES, phaseOf } from '../api/config.js'
|
import { CHANNELS, ENTRY, RV_LEADS, STAGES, phaseOf } from '../api/config.js'
|
||||||
|
import { entryDoorsFor, rolesOf } from '../api/permissions.js'
|
||||||
import { describeError } from '../api/errors.js'
|
import { describeError } from '../api/errors.js'
|
||||||
|
|
||||||
/** Short absolute date plus how long ago — a queue needs both: the absolute
|
/** Short absolute date plus how long ago — a queue needs both: the absolute
|
||||||
@ -70,7 +71,8 @@ import './screens.css'
|
|||||||
export default function Pipeline() {
|
export default function Pipeline() {
|
||||||
const { stageUid } = useParams()
|
const { stageUid } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { client } = useZino()
|
const { client, user } = useZino()
|
||||||
|
const canFile = entryDoorsFor(rolesOf(user), ENTRY).length > 0
|
||||||
// "all" is not a stage — it is every lead in one list, so an operator
|
// "all" is not a stage — it is every lead in one list, so an operator
|
||||||
// wanting to find one does not have to guess which queue it is in.
|
// wanting to find one does not have to guess which queue it is in.
|
||||||
//
|
//
|
||||||
@ -173,6 +175,15 @@ export default function Pipeline() {
|
|||||||
<strong>{isAll ? shownRows.length : state.total}</strong>
|
<strong>{isAll ? shownRows.length : state.total}</strong>
|
||||||
<span>{(isAll ? shownRows.length : state.total) === 1 ? 'lead' : 'leads'}</span>
|
<span>{(isAll ? shownRows.length : state.total) === 1 ? 'lead' : 'leads'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{canFile ? (
|
||||||
|
<Link to="/add" className="newlead">
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 3.2v9.6M3.2 8h9.6" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.7" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
New lead
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1393,3 +1393,25 @@
|
|||||||
.grid__row.is-done .grid__name { color: var(--zk-muted); font-weight: 400; }
|
.grid__row.is-done .grid__name { color: var(--zk-muted); font-weight: 400; }
|
||||||
.grid__row.is-done:hover { color: var(--zk-ink); }
|
.grid__row.is-done:hover { color: var(--zk-ink); }
|
||||||
.grid__row.is-done:hover .grid__name { color: var(--zk-blue-dark); }
|
.grid__row.is-done:hover .grid__name { color: var(--zk-blue-dark); }
|
||||||
|
|
||||||
|
/* ---- file a lead ----
|
||||||
|
Moved off the sidebar. A nav is for moving between places; this adds
|
||||||
|
something, and it now sits beside the lists it adds to. */
|
||||||
|
.newlead {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: none;
|
||||||
|
padding: 9px 16px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--zk-white);
|
||||||
|
background: var(--grad-blue);
|
||||||
|
box-shadow: var(--sh-sm);
|
||||||
|
transition: box-shadow var(--t), transform var(--t-fast), background var(--t);
|
||||||
|
}
|
||||||
|
.newlead svg { width: 14px; height: 14px; }
|
||||||
|
.newlead:hover { background: var(--grad-blue-hover); box-shadow: var(--sh-md); transform: translateY(-1px); }
|
||||||
|
.newlead:focus-visible { outline: 2px solid var(--zk-blue); outline-offset: 2px; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user