diff --git a/src/screens/Overview.jsx b/src/screens/Overview.jsx index d71aed8..9d26c30 100644 --- a/src/screens/Overview.jsx +++ b/src/screens/Overview.jsx @@ -57,47 +57,13 @@ const EXPO_BUCKETS = { const EXPO_LABEL = { lapsed: 'Lapsed', week: 'Within 7 days', month: '8 to 30 days' } /* The renewal-exposure table is filterable on the columns it shows. Each field knows how to match a row and how to describe itself as a chip. */ -const FILTER_FIELDS = [ - { key: 'stage', label: 'Stage' }, - { key: 'expiry', label: 'Time to expiry' }, - { key: 'premium', label: 'Premium' }, - { key: 'name', label: 'Lead name' }, +const PREM_RANGES = [ + ['lt10', 'Under ₹10,000', (p) => p > 0 && p < 10000], + ['10-25', '₹10,000 – 25,000', (p) => p >= 10000 && p < 25000], + ['25-50', '₹25,000 – 50,000', (p) => p >= 25000 && p < 50000], + ['gt50', 'Over ₹50,000', (p) => p >= 50000], ] -function startFilter(field) { - return field === 'premium' ? { field, op: 'gte', value: '' } : { field, value: '' } -} -function filterReady(d) { - if (!d) return false - if (d.field === 'premium') return String(d.value).trim() !== '' && Number.isFinite(Number(d.value)) - return String(d.value).trim() !== '' -} -function matchExposure(f, r) { - switch (f.field) { - case 'stage': return r.current_state_name === f.value - case 'expiry': return EXPO_BUCKETS[f.value] ? EXPO_BUCKETS[f.value](r._d) : true - case 'premium': { - const v = Number(f.value) - if (!Number.isFinite(v)) return true - const p = num(r.quoted_premium) - if (!p) return false // a lead with no premium cannot satisfy a premium test - return f.op === 'lte' ? p <= v : p >= v - } - case 'name': { - const hay = ((r.customer_name || '') + ' ' + (r.lead_ref || '')).toLowerCase() - return hay.includes(String(f.value).toLowerCase()) - } - default: return true - } -} -function describeFilter(f) { - switch (f.field) { - case 'stage': return 'Stage: ' + f.value - case 'expiry': return 'Expiry: ' + (EXPO_LABEL[f.value] || f.value) - case 'premium': return 'Premium ' + (f.op === 'lte' ? '\u2264 ' : '\u2265 ') + inr(Number(f.value)) - case 'name': return 'Lead: \u201c' + f.value + '\u201d' - default: return '' - } -} +const PREM_MATCH = Object.fromEntries(PREM_RANGES.map(([k, , fn]) => [k, fn])) function expiryTone(d) { if (d === null) return 'later' @@ -115,10 +81,10 @@ 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) - // User-built filters on the renewal-exposure table, AND-combined, plus the - // one being composed in the add row. - const [filters, setFilters] = useState([]) - const [draft, setDraft] = useState(null) // { field, op?, value } | null + // Renewal-exposure dropdown filters. Empty string = no filter on that field. + const [fStage, setFStage] = useState('') + const [fExpiry, setFExpiry] = useState('') + const [fPrem, setFPrem] = useState('') const state = usePortfolio() const m = useMemo(() => { @@ -216,8 +182,13 @@ export default function Overview() { // Stages present in the exposure set, for the Stage filter's options. const expoStages = [...new Set(m.attention.map((r) => r.current_state_name).filter(Boolean))] - // The exposure table, narrowed by every active filter (AND). - const shown = m.attention.filter((r) => filters.every((f) => matchExposure(f, r))) + const anyFilter = Boolean(fStage || fExpiry || fPrem) + // The exposure table, narrowed by whichever dropdowns are set (AND). + const shown = m.attention.filter((r) => + (!fStage || r.current_state_name === fStage) && + (!fExpiry || (EXPO_BUCKETS[fExpiry] ? EXPO_BUCKETS[fExpiry](r._d) : true)) && + (!fPrem || (PREM_MATCH[fPrem] ? PREM_MATCH[fPrem](num(r.quoted_premium)) : true)), + ) if (state.status === 'error') { const said = describeError(state.error) @@ -369,77 +340,36 @@ export default function Overview() {
{m.exposure.month.length}8 to 30 days
- {/* Build-your-own filters: pick a field, give it a value, add it. Each - lands as a removable chip and they AND together. */} -
-
- {filters.map((f, i) => ( - - {describeFilter(f)} - - - ))} - - {draft ? ( - - - - {draft.field === 'stage' ? ( - - ) : draft.field === 'expiry' ? ( - - ) : draft.field === 'premium' ? ( - <> - - setDraft({ ...draft, value: e.target.value })} /> - - ) : ( - setDraft({ ...draft, value: e.target.value })} /> - )} - - - - - ) : ( - - )} - - {filters.length ? ( - - ) : null} -
+ {/* Plain dropdown filters over the columns the table shows. Empty + option = no filter on that field; they narrow together. */} +
+ + + + {anyFilter ? ( + + ) : null}
{shown.length ? ( @@ -449,7 +379,7 @@ export default function Overview() { LeadExpiryStagePremium - {shown.slice(0, filters.length ? 100 : 8).map((r) => { + {shown.slice(0, anyFilter ? 100 : 8).map((r) => { const id = r.instance_id ?? r.id return ( ) : (

- {filters.length ? 'No open leads match these filters.' : 'No renewals due within 30 days.'} + {anyFilter ? 'No open leads match these filters.' : 'No renewals due within 30 days.'}

)}
diff --git a/src/screens/screens.css b/src/screens/screens.css index c68b120..4559b43 100644 --- a/src/screens/screens.css +++ b/src/screens/screens.css @@ -1888,108 +1888,56 @@ color: var(--zk-grey); } -/* ---- renewal-exposure custom filters ---- */ -.xf { margin-top: 12px; } -.xf__row { +/* ---- renewal-exposure dropdown filters ---- */ +.xfd { + margin-top: 12px; display: flex; flex-wrap: wrap; - align-items: center; - gap: 8px; + align-items: flex-end; + gap: 14px; } -.xf__chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 6px 4px 12px; - border-radius: var(--r-pill); - background: var(--zk-tint-blue); - border: 1px solid var(--zk-blue-light, var(--zk-line)); - font-size: var(--fs-2xs); +.xfd__f { + display: flex; + flex-direction: column; + gap: 5px; +} +.xfd__f > span { + font-size: var(--fs-3xs); font-weight: 500; - color: var(--zk-blue-dark); -} -.xf__chip button { - display: grid; - place-items: center; - width: 18px; - height: 18px; - padding: 0; - border: 0; - border-radius: 50%; - background: rgba(195, 15, 104, 0.10); - color: var(--zk-blue-dark); - cursor: pointer; -} -.xf__chip button svg { width: 10px; height: 10px; } -.xf__chip button:hover { background: rgba(195, 15, 104, 0.20); } - -.xf__add { - display: inline-flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - padding: 5px 6px; - border: 1px solid var(--zk-line); - border-radius: var(--r-md); - background: var(--zk-white); -} -.xf__sel, -.xf__in { - font: inherit; - font-size: var(--fs-2xs); - padding: 5px 8px; - border: 1px solid var(--zk-line); - border-radius: var(--r-sm); - background: var(--zk-white); - color: var(--zk-ink); -} -.xf__in { width: 130px; } -.xf__sel:focus-visible, -.xf__in:focus-visible { outline: none; border-color: var(--zk-blue); box-shadow: var(--ring); } - -.xf__apply { - font: inherit; - font-size: var(--fs-2xs); - font-weight: 500; - padding: 6px 12px; - border: 0; - border-radius: var(--r-sm); - background: var(--zk-blue); - color: var(--zk-white); - cursor: pointer; -} -.xf__apply:hover:not(:disabled) { background: var(--zk-blue-dark); } -.xf__apply:disabled { opacity: 0.5; cursor: default; } - -.xf__cancel, -.xf__clearall { - font: inherit; - font-size: var(--fs-2xs); - font-weight: 500; - padding: 6px 8px; - border: 0; - background: none; + letter-spacing: 0.06em; + text-transform: uppercase; color: var(--zk-grey); - cursor: pointer; } -.xf__cancel:hover, -.xf__clearall:hover { color: var(--zk-blue-dark); text-decoration: underline; } - -.xf__new { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px 6px 10px; - border: 1px dashed var(--zk-line); - border-radius: var(--r-pill); - background: var(--zk-white); +.xfd__f select { + font: inherit; + font-size: var(--fs-2xs); + padding: 8px 30px 8px 11px; + border: 1px solid var(--zk-line); + border-radius: var(--r-sm); + background: var(--zk-white) + url("data:image/svg+xml;utf8,") + no-repeat right 10px center; + color: var(--zk-ink); cursor: pointer; + min-width: 150px; + -webkit-appearance: none; + appearance: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} +.xfd__f select:hover { border-color: var(--zk-blue-mid); } +.xfd__f select:focus-visible { outline: none; border-color: var(--zk-blue); box-shadow: var(--ring); } + +.xfd__clear { font: inherit; font-size: var(--fs-2xs); font-weight: 500; + padding: 8px 12px; + border: 1px solid var(--zk-line); + border-radius: var(--r-sm); + background: var(--zk-white); color: var(--zk-blue-dark); + cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); } -.xf__new svg { width: 13px; height: 13px; } -.xf__new:hover { background: var(--zk-tint-blue); border-color: var(--zk-blue-mid); } -.xf__new:focus-visible { outline: none; box-shadow: var(--ring); } +.xfd__clear:hover { background: var(--zk-tint-blue); border-color: var(--zk-blue-mid); } +.xfd__clear:focus-visible { outline: none; box-shadow: var(--ring); }