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.
|
// 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 stage = phaseOf(STAGES.find((s) => s.name === stateName), row)
|
||||||
const holds = holdsOf(stage)
|
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
|
// 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
|
// anyway; showing them a row of buttons that all 403 reads as a broken app
|
||||||
// rather than as a control.
|
// rather than as a control.
|
||||||
@ -441,6 +517,8 @@ export default function Lead() {
|
|||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{statusStrip}
|
||||||
|
|
||||||
{/* Measures, not fields. Age and dwell time are computed — neither is on
|
{/* 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?",
|
the record — and they are the two that answer "is this lead moving?",
|
||||||
which no label/value pair on the page could. */}
|
which no label/value pair on the page could. */}
|
||||||
@ -497,78 +575,6 @@ export default function Lead() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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.
|
{/* WHAT TO DO NEXT, not what is permitted.
|
||||||
Every action carries a role: `do` is the step this stage is
|
Every action carries a role: `do` is the step this stage is
|
||||||
@ -661,6 +667,10 @@ export default function Lead() {
|
|||||||
|
|
||||||
{again.length ? (
|
{again.length ? (
|
||||||
<div className="alt">
|
<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) => (
|
{again.map((a) => (
|
||||||
<button key={a.uid} type="button"
|
<button key={a.uid} type="button"
|
||||||
className={'alt__btn' + (open === a.uid ? ' is-open' : '')}
|
className={'alt__btn' + (open === a.uid ? ' is-open' : '')}
|
||||||
|
|||||||
@ -888,31 +888,32 @@
|
|||||||
margin-bottom: var(--sp-5);
|
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 {
|
.now__cell {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--sp-6) var(--sp-6) var(--sp-5);
|
padding: var(--sp-5) var(--sp-5) var(--sp-5) var(--sp-6);
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-md);
|
||||||
background: var(--zk-navy);
|
background: var(--zk-white);
|
||||||
color: var(--zk-white);
|
color: var(--zk-ink);
|
||||||
|
border: 1px solid var(--zk-line-soft);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--sh-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.now__cell::before {
|
.now__cell::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0 0 auto 0;
|
inset: 0 auto 0 0;
|
||||||
height: 4px;
|
width: 4px;
|
||||||
background: var(--zk-blue-light);
|
background: var(--zk-blue);
|
||||||
}
|
}
|
||||||
.now__cell--person::before { background: var(--zk-amber-line); }
|
.now__cell--person::before { background: var(--zk-amber); }
|
||||||
.now__cell--cust::before { background: var(--zk-cyan); }
|
.now__cell--cust::before { background: var(--zk-teal); }
|
||||||
.now__cell--risk::before { background: var(--zk-danger-line); }
|
.now__cell--risk::before { background: var(--zk-grey); }
|
||||||
|
|
||||||
/* 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__l {
|
.now__l {
|
||||||
display: block;
|
display: block;
|
||||||
@ -921,24 +922,35 @@
|
|||||||
font-weight: var(--fw-semi);
|
font-weight: var(--fw-semi);
|
||||||
letter-spacing: .09em;
|
letter-spacing: .09em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: .78;
|
color: var(--zk-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
.now__v {
|
.now__v {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: var(--sp-2);
|
margin-bottom: var(--sp-2);
|
||||||
font-size: var(--fs-3xl);
|
font-size: var(--fs-2xl);
|
||||||
font-weight: var(--fw-semi);
|
font-weight: var(--fw-semi);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: -.03em;
|
letter-spacing: -.02em;
|
||||||
font-variant-numeric: tabular-nums;
|
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 {
|
.now__s {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: var(--fs-2xs);
|
font-size: var(--fs-2xs);
|
||||||
line-height: var(--lh-snug);
|
line-height: var(--lh-snug);
|
||||||
opacity: .82;
|
color: var(--zk-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KPI strip. Whitespace rather than rules — see .lmetrics for why the cage
|
/* 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__txt b { font-size: var(--fs-sm); font-weight: var(--fw-semi); }
|
||||||
.cta svg { width: 16px; height: 16px; flex: none; }
|
.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