console: state before numbers; frame optional actions; flatten the overview band
Applying the lens from the status-strip critique across the surfaces. LEAD PAGE - "What is happening now" lifts above the metrics. An operator's first question is what is going on / what do I do, not what the premium is — so the status strip (an alert when blocked or stalled, the live state otherwise) sits directly under the header, before the four numbers, full-width. - The optional "Add a document" action stopped floating: with no required step it read like the next move and sat under "nothing is waiting on you". A quiet "Optional" tag now frames it. OVERVIEW - The "right now" band was four heavy dark-navy shadowed cards — the cards-on-canvas language removed everywhere else, clashing on the flat white page. Flattened to light tiles read by a coloured left rule (blue AI, amber person, teal customer, grey risk), the figure taking that colour; a live person-queue or lapsed renewal warms its whole tile. Prominence from the big number and the accent, not a dark fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d1ed66b7ae
commit
e21e3949f5
@ -300,6 +300,82 @@ export default function Lead() {
|
||||
// are still looked up on the state's own uid; only the presentation moves.
|
||||
const stage = phaseOf(STAGES.find((s) => s.name === stateName), row)
|
||||
const holds = holdsOf(stage)
|
||||
|
||||
/* WHAT IS HAPPENING NOW, lifted above the numbers. An operator's first
|
||||
question about a lead is "what is going on / what do I do", not "what is
|
||||
the premium" — so the status strip (an alert when blocked or stalled, the
|
||||
live state otherwise) sits directly under the header, before the metrics.
|
||||
Rendered full-width there rather than inside the narrow work column. */
|
||||
const statusStrip = (
|
||||
blocked ? (
|
||||
<div className="doing doing--blocked">
|
||||
<span className="doing__stop" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>Stopped — {blocked.what}</strong>
|
||||
<span>
|
||||
{blocked.fix} The agents will pick the lead up again on their own once it
|
||||
is there; nothing else is running in the meantime.
|
||||
</span>
|
||||
<button
|
||||
type="button" className="doing__fix"
|
||||
onClick={() => setOpen(blocked.via)}
|
||||
>
|
||||
Open Collect Documents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : stalled ? (
|
||||
<div className="doing doing--stalled">
|
||||
<span className="doing__stop" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing} — nothing for {stalled.mins} minutes</strong>
|
||||
<span>
|
||||
{stage.by} has not come back. That is usually the model provider being
|
||||
slow or dropping a request, not a problem with this lead — the work so far
|
||||
is safe and nothing has been lost.
|
||||
{stalled.nudge
|
||||
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
|
||||
: ' There is no step to re-run from here; the actions below are the way on.'}
|
||||
</span>
|
||||
{stalled.nudge ? (
|
||||
<button
|
||||
type="button" className="doing__fix"
|
||||
onClick={() => setOpen(stalled.nudge.uid)}
|
||||
>
|
||||
{stalled.nudge.label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : stage?.kind === 'customer' && stage.doing ? (
|
||||
/* Not an agent working, and not a task of yours either — the lead
|
||||
is with someone outside the business. Without this the screen
|
||||
went blank at exactly the stage where an operator is most likely
|
||||
to wonder whether something has broken. */
|
||||
<div className="doing doing--waiting">
|
||||
<span className="doing__wait" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing}</strong>
|
||||
<span>
|
||||
Their answer comes back to this lead on its own — Engage reads it and
|
||||
records the acceptance. Nothing is waiting on you.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : stage?.kind === 'auto' ? (
|
||||
<div className="doing">
|
||||
<span className="doing__pulse" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing}</strong>
|
||||
<span>
|
||||
Handled by {stage.by}.
|
||||
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
|
||||
{' '}Typically completes within two minutes; this view refreshes automatically.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
// Only what THIS user may run. The workflow refuses the rest server-side
|
||||
// anyway; showing them a row of buttons that all 403 reads as a broken app
|
||||
// rather than as a control.
|
||||
@ -441,6 +517,8 @@ export default function Lead() {
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{statusStrip}
|
||||
|
||||
{/* Measures, not fields. Age and dwell time are computed — neither is on
|
||||
the record — and they are the two that answer "is this lead moving?",
|
||||
which no label/value pair on the page could. */}
|
||||
@ -497,78 +575,6 @@ export default function Lead() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* An AI-carried stage has no empty action bar and no idle screen: it
|
||||
says what is happening and who is doing it. The activities below it
|
||||
stay available — ops can run them by hand — but they are not the
|
||||
answer to "why is nothing moving?". */}
|
||||
{blocked ? (
|
||||
<div className="doing doing--blocked">
|
||||
<span className="doing__stop" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>Stopped — {blocked.what}</strong>
|
||||
<span>
|
||||
{blocked.fix} The agents will pick the lead up again on their own once it
|
||||
is there; nothing else is running in the meantime.
|
||||
</span>
|
||||
<button
|
||||
type="button" className="doing__fix"
|
||||
onClick={() => setOpen(blocked.via)}
|
||||
>
|
||||
Open Collect Documents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : stalled ? (
|
||||
<div className="doing doing--stalled">
|
||||
<span className="doing__stop" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing} — nothing for {stalled.mins} minutes</strong>
|
||||
<span>
|
||||
{stage.by} has not come back. That is usually the model provider being
|
||||
slow or dropping a request, not a problem with this lead — the work so far
|
||||
is safe and nothing has been lost.
|
||||
{stalled.nudge
|
||||
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
|
||||
: ' There is no step to re-run from here; the actions below are the way on.'}
|
||||
</span>
|
||||
{stalled.nudge ? (
|
||||
<button
|
||||
type="button" className="doing__fix"
|
||||
onClick={() => setOpen(stalled.nudge.uid)}
|
||||
>
|
||||
{stalled.nudge.label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : stage?.kind === 'customer' && stage.doing ? (
|
||||
/* Not an agent working, and not a task of yours either — the lead
|
||||
is with someone outside the business. Without this the screen
|
||||
went blank at exactly the stage where an operator is most likely
|
||||
to wonder whether something has broken. */
|
||||
<div className="doing doing--waiting">
|
||||
<span className="doing__wait" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing}</strong>
|
||||
<span>
|
||||
Their answer comes back to this lead on its own — Engage reads it and
|
||||
records the acceptance. Nothing is waiting on you.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : stage?.kind === 'auto' ? (
|
||||
<div className="doing">
|
||||
<span className="doing__pulse" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{stage.doing}</strong>
|
||||
<span>
|
||||
Handled by {stage.by}.
|
||||
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
|
||||
{' '}Typically completes within two minutes; this view refreshes automatically.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* WHAT TO DO NEXT, not what is permitted.
|
||||
Every action carries a role: `do` is the step this stage is
|
||||
@ -661,6 +667,10 @@ export default function Lead() {
|
||||
|
||||
{again.length ? (
|
||||
<div className="alt">
|
||||
{/* With no required step, these optional actions floated at
|
||||
the top of the column reading like the next step. A quiet
|
||||
tag frames them as optional without repeating the status. */}
|
||||
{!step.length ? <span className="alt__note">Optional</span> : null}
|
||||
{again.map((a) => (
|
||||
<button key={a.uid} type="button"
|
||||
className={'alt__btn' + (open === a.uid ? ' is-open' : '')}
|
||||
|
||||
@ -888,31 +888,32 @@
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
|
||||
/* FLAT, TO MATCH THE REST. This was four heavy dark-navy cards with shadows —
|
||||
the cards-on-canvas language removed everywhere else, so on the flat white
|
||||
page it clashed. Now light tiles, each read by a coloured left rule: blue
|
||||
for the AI, amber for a person, teal for the customer, red for risk. The
|
||||
number takes that colour, so the row is read as colour before text. Prominence
|
||||
comes from a big figure and the accent, not from a dark fill. */
|
||||
.now__cell {
|
||||
position: relative;
|
||||
padding: var(--sp-6) var(--sp-6) var(--sp-5);
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--zk-navy);
|
||||
color: var(--zk-white);
|
||||
padding: var(--sp-5) var(--sp-5) var(--sp-5) var(--sp-6);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--zk-white);
|
||||
color: var(--zk-ink);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--sh-md);
|
||||
}
|
||||
|
||||
.now__cell::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 4px;
|
||||
background: var(--zk-blue-light);
|
||||
inset: 0 auto 0 0;
|
||||
width: 4px;
|
||||
background: var(--zk-blue);
|
||||
}
|
||||
.now__cell--person::before { background: var(--zk-amber-line); }
|
||||
.now__cell--cust::before { background: var(--zk-cyan); }
|
||||
.now__cell--risk::before { background: var(--zk-danger-line); }
|
||||
|
||||
/* A live person-queue or a lapsed renewal is the one thing on this page that
|
||||
is actually owed. It warms up; the rest stay navy. */
|
||||
.now__cell--person.is-live { background: var(--zk-amber-ink); }
|
||||
.now__cell--risk.is-live { background: var(--zk-danger-ink); }
|
||||
.now__cell--person::before { background: var(--zk-amber); }
|
||||
.now__cell--cust::before { background: var(--zk-teal); }
|
||||
.now__cell--risk::before { background: var(--zk-grey); }
|
||||
|
||||
.now__l {
|
||||
display: block;
|
||||
@ -921,24 +922,35 @@
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: .09em;
|
||||
text-transform: uppercase;
|
||||
opacity: .78;
|
||||
color: var(--zk-grey);
|
||||
}
|
||||
|
||||
.now__v {
|
||||
display: block;
|
||||
margin-bottom: var(--sp-2);
|
||||
font-size: var(--fs-3xl);
|
||||
font-size: var(--fs-2xl);
|
||||
font-weight: var(--fw-semi);
|
||||
line-height: 1;
|
||||
letter-spacing: -.03em;
|
||||
letter-spacing: -.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--zk-blue-dark);
|
||||
}
|
||||
.now__cell--person .now__v { color: var(--zk-amber-ink); }
|
||||
.now__cell--cust .now__v { color: var(--zk-teal-ink); }
|
||||
.now__cell--risk .now__v { color: var(--zk-ink); }
|
||||
|
||||
/* A live person-queue or a lapsed renewal is the one thing actually owed. It
|
||||
warms its whole tile; the rest stay plain white. */
|
||||
.now__cell--person.is-live { background: var(--zk-amber-tint); border-color: var(--zk-amber-line); }
|
||||
.now__cell--risk.is-live { background: var(--zk-danger-tint); border-color: var(--zk-danger-line); }
|
||||
.now__cell--risk.is-live::before { background: var(--zk-danger); }
|
||||
.now__cell--risk.is-live .now__v { color: var(--zk-danger-ink); }
|
||||
|
||||
.now__s {
|
||||
display: block;
|
||||
font-size: var(--fs-2xs);
|
||||
line-height: var(--lh-snug);
|
||||
opacity: .82;
|
||||
color: var(--zk-muted);
|
||||
}
|
||||
|
||||
/* KPI strip. Whitespace rather than rules — see .lmetrics for why the cage
|
||||
@ -1830,3 +1842,16 @@
|
||||
}
|
||||
.cta__txt b { font-size: var(--fs-sm); font-weight: var(--fw-semi); }
|
||||
.cta svg { width: 16px; height: 16px; flex: none; }
|
||||
|
||||
/* Frames optional (again) actions when there is no required step, so a
|
||||
secondary button stops reading as the next move. */
|
||||
.alt__note {
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
margin-right: var(--sp-2);
|
||||
font-size: var(--fs-3xs);
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: .07em;
|
||||
text-transform: uppercase;
|
||||
color: var(--zk-grey);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user