Compare commits

..

No commits in common. "e0d51159f6123d4ed2a23243e3927a8026ae0ede" and "42af54b3e2e4a9667b09af7c7d97054a5f968bc1" have entirely different histories.

5 changed files with 15 additions and 135 deletions

View File

@ -42,7 +42,7 @@ function turnsFrom(text) {
return out.filter((t) => t.text) return out.filter((t) => t.text)
} }
export default function CallTranscript({ text, audio, name, onClose }) { export default function CallTranscript({ text, name, onClose }) {
const ref = useRef(null) const ref = useRef(null)
const restore = useRef(null) const restore = useRef(null)
const turns = turnsFrom(text) const turns = turnsFrom(text)
@ -82,16 +82,6 @@ export default function CallTranscript({ text, audio, name, onClose }) {
</button> </button>
</div> </div>
{audio ? (
<div className="conv__audio">
{/* Listen along with the transcript. preload=none so opening the
dialog does not pull a multi-MB WAV until the user hits play. */}
<audio controls preload="none" src={audio}>
Your browser cannot play this recording.
</audio>
</div>
) : null}
<div className="conv__body"> <div className="conv__body">
{turns.length ? turns.map((t, i) => ( {turns.length ? turns.map((t, i) => (
<div key={i} className={'bub bub--' + t.side}> <div key={i} className={'bub bub--' + t.side}>

View File

@ -76,13 +76,3 @@
background: var(--zk-tint); background: var(--zk-tint);
border-radius: var(--r-md); border-radius: var(--r-md);
} }
/* The call recording, played from inside the transcript dialog. */
.conv__audio {
padding: 12px 20px 4px;
border-bottom: 1px solid var(--zk-line-soft);
}
.conv__audio audio {
width: 100%;
height: 38px;
}

View File

@ -838,17 +838,6 @@ blocked ? (
Read what was actually said Read what was actually said
</button> </button>
) : null} ) : null}
{title === 'The call' && row.call_recording_url ? (
/* The recording itself, to play alongside the transcript.
preload=none keeps the multi-MB WAV off the wire until
a person actually presses play. */
<div className="glance__rec">
<span className="glance__rec-l">Call recording</span>
<audio controls preload="none" src={row.call_recording_url}>
Your browser cannot play this recording.
</audio>
</div>
) : null}
</section> </section>
))} ))}
</div> </div>
@ -875,7 +864,7 @@ blocked ? (
<AgentCard agentKey={openAgent} onClose={() => setOpenAgent(null)} /> <AgentCard agentKey={openAgent} onClose={() => setOpenAgent(null)} />
) : null} ) : null}
{showCall ? ( {showCall ? (
<CallTranscript text={row.call_transcript} audio={row.call_recording_url} name={row.customer_name} onClose={() => setShowCall(false)} /> <CallTranscript text={row.call_transcript} name={row.customer_name} onClose={() => setShowCall(false)} />
) : null} ) : null}
{showFile ? ( {showFile ? (

View File

@ -47,16 +47,6 @@ function inr(n) {
return '₹' + Math.round(n).toLocaleString('en-IN') return '₹' + Math.round(n).toLocaleString('en-IN')
} }
/* The renewal-exposure buckets, as predicates on days-to-expiry, so the filter
and the three counts stay one definition. */
const EXPO_BUCKETS = {
lapsed: (d) => d < 0,
week: (d) => d >= 0 && d <= 7,
month: (d) => d > 7 && d <= 30,
}
const EXPO_LABEL = { lapsed: 'Lapsed', week: 'Within 7 days', month: '8 to 30 days' }
function expoLabel(k) { return EXPO_LABEL[k] || '' }
function expiryTone(d) { function expiryTone(d) {
if (d === null) return 'later' if (d === null) return 'later'
if (d < 0) return 'lapsed' if (d < 0) return 'lapsed'
@ -73,8 +63,6 @@ export default function Overview() {
// Filing a lead happens over this page, not instead of it see // Filing a lead happens over this page, not instead of it see
// NewLeadDialog. The overview behind it keeps its counts and its poll. // NewLeadDialog. The overview behind it keeps its counts and its poll.
const [adding, setAdding] = useState(false) const [adding, setAdding] = useState(false)
// Which renewal-exposure bucket the table is narrowed to, or null for all.
const [expo, setExpo] = useState(null)
const state = usePortfolio() const state = usePortfolio()
const m = useMemo(() => { const m = useMemo(() => {
@ -170,10 +158,6 @@ export default function Overview() {
const mine = new Set(visibleStages(roles).map((s) => s.uid)) const mine = new Set(visibleStages(roles).map((s) => s.uid))
// The exposure table, narrowed to the chosen bucket (or the soonest-due
// across all buckets when no filter is set).
const expoRows = expo ? m.attention.filter((r) => EXPO_BUCKETS[expo](r._d)) : m.attention
if (state.status === 'error') { if (state.status === 'error') {
const said = describeError(state.error) const said = describeError(state.error)
return ( return (
@ -318,43 +302,26 @@ export default function Overview() {
</div> </div>
<h2 className="sec">Renewal exposure<span className="sec__hint">open leads by time to expiry</span></h2> <h2 className="sec">Renewal exposure<span className="sec__hint">open leads by time to expiry</span></h2>
{/* The three buckets double as the filter: click one to narrow the <div className="buckets">
table to it, click again (or Clear) to show every open renewal. A <div className="bucket bucket--lapsed">
bucket with nothing in it is not clickable there is nothing to <strong>{m.exposure.lapsed.length}</strong><span>Lapsed</span>
show. */} </div>
<div className="buckets" role="group" aria-label="Filter by time to expiry"> <div className="bucket bucket--urgent">
{[ <strong>{m.exposure.week.length}</strong><span>Within 7 days</span>
['lapsed', 'lapsed', m.exposure.lapsed.length, 'Lapsed'], </div>
['week', 'urgent', m.exposure.week.length, 'Within 7 days'], <div className="bucket bucket--soon">
['month', 'soon', m.exposure.month.length, '8 to 30 days'], <strong>{m.exposure.month.length}</strong><span>8 to 30 days</span>
].map(([key, variant, count, label]) => ( </div>
<button
key={key}
type="button"
className={`bucket bucket--${variant}` + (expo === key ? ' is-active' : '')}
aria-pressed={expo === key}
disabled={!count}
onClick={() => setExpo(expo === key ? null : key)}
>
<strong>{count}</strong><span>{label}</span>
</button>
))}
</div> </div>
{expo ? ( {m.attention.length ? (
<button type="button" className="expo__clear" onClick={() => setExpo(null)}>
Showing {expoLabel(expo)} only · Clear filter
</button>
) : null}
{expoRows.length ? (
<div className="gridwrap"> <div className="gridwrap">
<table className="grid grid--tight"> <table className="grid grid--tight">
<thead> <thead>
<tr><th>Lead</th><th>Expiry</th><th>Stage</th><th className="num">Premium</th></tr> <tr><th>Lead</th><th>Expiry</th><th>Stage</th><th className="num">Premium</th></tr>
</thead> </thead>
<tbody> <tbody>
{expoRows.slice(0, expo ? 50 : 8).map((r) => { {m.attention.slice(0, 8).map((r) => {
const id = r.instance_id ?? r.id const id = r.instance_id ?? r.id
return ( return (
<tr <tr
@ -384,9 +351,7 @@ export default function Overview() {
</table> </table>
</div> </div>
) : ( ) : (
<p className="empty"> <p className="empty">No renewals due within 30 days.</p>
{expo ? 'No open leads in this bucket.' : 'No renewals due within 30 days.'}
</p>
)} )}
</div> </div>

View File

@ -1059,42 +1059,6 @@
.bucket--soon { border-left-color: var(--zk-blue-mid); } .bucket--soon { border-left-color: var(--zk-blue-mid); }
.bucket--soon strong { color: var(--zk-blue-dark); } .bucket--soon strong { color: var(--zk-blue-dark); }
/* The buckets are filter toggles. Reset the button chrome, keep the card look,
and add hover / pressed / disabled states. */
.bucket {
width: 100%;
font: inherit;
text-align: left;
color: inherit;
cursor: pointer;
transition: background var(--t-fast), border-color var(--t-fast),
box-shadow var(--t-fast), transform var(--t-fast);
}
.bucket:hover:not(:disabled) { box-shadow: var(--sh-sm); }
.bucket:active:not(:disabled) { transform: translateY(1px); }
.bucket:focus-visible { outline: none; box-shadow: var(--ring); }
.bucket:disabled { cursor: default; opacity: 0.55; }
/* Pressed: fill with the bucket's own tint so the active filter is unmistakable. */
.bucket--lapsed.is-active { background: var(--zk-danger-tint); border-color: var(--zk-danger-line); border-left-color: var(--zk-danger); }
.bucket--urgent.is-active { background: var(--zk-amber-tint); border-color: var(--zk-amber-line); border-left-color: var(--zk-amber); }
.bucket--soon.is-active { background: var(--zk-tint-blue); border-color: var(--zk-blue-mid); border-left-color: var(--zk-blue-mid); }
/* The clear affordance under the buckets when a filter is on. */
.expo__clear {
margin: 10px 0 0;
padding: 0;
border: 0;
background: none;
cursor: pointer;
font: inherit;
font-size: var(--fs-2xs);
font-weight: 500;
color: var(--zk-blue-dark);
}
.expo__clear:hover { text-decoration: underline; }
.expo__clear:focus-visible { outline: none; text-decoration: underline; box-shadow: var(--ring); border-radius: var(--r-xs); }
/* Stage distribution. A bar per stage, scaled to the largest enough to see /* Stage distribution. A bar per stage, scaled to the largest enough to see
where the book is banked without pulling in a charting library. */ where the book is banked without pulling in a charting library. */
.dist { display: flex; flex-direction: column; gap: 3px; } .dist { display: flex; flex-direction: column; gap: 3px; }
@ -1727,24 +1691,6 @@
/* An action that belongs to one card, not to the panel. Quiet until hovered: /* An action that belongs to one card, not to the panel. Quiet until hovered:
the notes above it are the answer most of the time, and the transcript is the notes above it are the answer most of the time, and the transcript is
the follow-up question. */ the follow-up question. */
.glance__rec {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.glance__rec-l {
font-size: var(--fs-3xs);
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--zk-grey);
}
.glance__rec audio {
width: 100%;
height: 36px;
}
.glance__act { .glance__act {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;