From bd4ced41acf09a05f3faa48915e1571c2faa9d66 Mon Sep 17 00:00:00 2001 From: Yashas Date: Wed, 9 Sep 2026 16:05:21 +0530 Subject: [PATCH] console: make the renewal-exposure buckets filter the table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three buckets (Lapsed / Within 7 days / 8 to 30 days) were static counts above a combined table. They now double as the filter: click one to narrow the table to that bucket, click again — or use the Clear affordance — to show every open renewal by soonest due. The active bucket fills with its own tint, an empty bucket is disabled, and the empty state is filter-aware. One predicate map drives both the counts and the filter so they cannot drift. Co-Authored-By: Claude Opus 4.8 --- src/screens/Overview.jsx | 61 +++++++++++++++++++++++++++++++--------- src/screens/screens.css | 36 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/screens/Overview.jsx b/src/screens/Overview.jsx index b0db725..f680db3 100644 --- a/src/screens/Overview.jsx +++ b/src/screens/Overview.jsx @@ -47,6 +47,16 @@ function inr(n) { 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) { if (d === null) return 'later' if (d < 0) return 'lapsed' @@ -63,6 +73,8 @@ export default function Overview() { // Filing a lead happens over this page, not instead of it — see // NewLeadDialog. The overview behind it keeps its counts and its poll. 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 m = useMemo(() => { @@ -158,6 +170,10 @@ export default function Overview() { 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') { const said = describeError(state.error) return ( @@ -302,26 +318,43 @@ export default function Overview() {

Renewal exposureopen leads by time to expiry

-
-
- {m.exposure.lapsed.length}Lapsed -
-
- {m.exposure.week.length}Within 7 days -
-
- {m.exposure.month.length}8 to 30 days -
+ {/* The three buckets double as the filter: click one to narrow the + table to it, click again (or Clear) to show every open renewal. A + bucket with nothing in it is not clickable — there is nothing to + show. */} +
+ {[ + ['lapsed', 'lapsed', m.exposure.lapsed.length, 'Lapsed'], + ['week', 'urgent', m.exposure.week.length, 'Within 7 days'], + ['month', 'soon', m.exposure.month.length, '8 to 30 days'], + ].map(([key, variant, count, label]) => ( + + ))}
- {m.attention.length ? ( + {expo ? ( + + ) : null} + + {expoRows.length ? (
- {m.attention.slice(0, 8).map((r) => { + {expoRows.slice(0, expo ? 50 : 8).map((r) => { const id = r.instance_id ?? r.id return ( ) : ( -

No renewals due within 30 days.

+

+ {expo ? 'No open leads in this bucket.' : 'No renewals due within 30 days.'} +

)} diff --git a/src/screens/screens.css b/src/screens/screens.css index 7a222f2..3523b94 100644 --- a/src/screens/screens.css +++ b/src/screens/screens.css @@ -1059,6 +1059,42 @@ .bucket--soon { border-left-color: var(--zk-blue-mid); } .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 where the book is banked without pulling in a charting library. */ .dist { display: flex; flex-direction: column; gap: 3px; }
LeadExpiryStagePremium