Compare commits

..

2 Commits

Author SHA1 Message Date
Bhanu Prakash Sai Potteri
8f419919bb fix: set router basename to /lead-to-policy for preview deep-links 2026-06-23 13:00:16 +05:30
Bhanu Prakash Sai Potteri
904e1660f1 feat: unify selects on shared searchable picker 2026-06-23 13:00:16 +05:30
11 changed files with 350 additions and 158 deletions

View File

@ -0,0 +1,182 @@
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown, X } from 'lucide-react';
import { cn } from '../../lib/cn';
export interface PickerShellProps<T> {
label?: string;
required?: boolean;
hint?: string;
/** Class for the outer label wrapper (width etc.). */
className?: string;
/** Text shown in the trigger for the current selection ('' = nothing picked). */
displayLabel: string;
placeholder?: string;
/** Rows shown in the open panel (already filtered/fetched by the caller). */
items: T[];
renderItem: (item: T) => ReactNode;
itemKey: (item: T, i: number) => string | number;
onPick: (item: T) => void;
/** When provided and something is selected, a clear (✕) button shows. */
onClear?: () => void;
/** Show the search box (default true). */
searchable?: boolean;
search: string;
onSearch: (s: string) => void;
loading?: boolean;
emptyText?: string;
/** Notifies the caller so it can fetch on open / reset on close. */
onOpenChange?: (open: boolean) => void;
disabled?: boolean;
}
/** Shared searchable-picker chrome: trigger pill + portal dropdown with an
* optional search box and a scrollable option list. Both Select (static
* options) and LookupField (remote rows) render through this so the two are
* visually identical one picker UI everywhere. The panel lives in a portal
* so a Card's `overflow-hidden` can't clip it. */
export function PickerShell<T>({
label,
required,
hint,
className,
displayLabel,
placeholder = 'Select…',
items,
renderItem,
itemKey,
onPick,
onClear,
searchable = true,
search,
onSearch,
loading,
emptyText = 'No matches.',
onOpenChange,
disabled,
}: PickerShellProps<T>) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);
const toggle = (next: boolean) => {
if (disabled) return;
setOpen(next);
onOpenChange?.(next);
};
// Anchor the portal panel under the trigger; track scroll/resize while open.
useLayoutEffect(() => {
if (!open) return;
const place = () => {
const el = triggerRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
setPos({ top: r.bottom + 4, left: r.left, width: r.width });
};
place();
window.addEventListener('scroll', place, true);
window.addEventListener('resize', place);
return () => {
window.removeEventListener('scroll', place, true);
window.removeEventListener('resize', place);
};
}, [open]);
// Close on outside click — ignore clicks inside the trigger or the portal panel.
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
const t = e.target as Node;
if (triggerRef.current?.contains(t) || panelRef.current?.contains(t)) return;
toggle(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
return (
<label className={cn('flex flex-col gap-1.5 font-sans', className)}>
{label && (
<span className="text-sm font-medium text-muted">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>
)}
<div
ref={triggerRef}
className={cn(
'flex items-center gap-2 bg-card rounded-md px-3 h-[42px] border border-border-default focus-ring',
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
)}
onClick={() => toggle(!open)}
>
<span className={cn('flex-1 truncate text-base', displayLabel ? 'text-strong' : 'text-faint')}>
{displayLabel || placeholder}
</span>
{onClear && displayLabel ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onClear();
}}
className="text-faint hover:text-strong"
>
<X size={15} />
</button>
) : (
<ChevronDown size={15} className="text-faint" />
)}
</div>
{hint && <span className="text-xs text-faint">{hint}</span>}
{open &&
pos &&
createPortal(
<div
ref={panelRef}
style={{ position: 'fixed', top: pos.top, left: pos.left, width: pos.width, zIndex: 50 }}
className="bg-card border border-border-default rounded-md shadow-lg overflow-hidden"
>
{searchable && (
<div className="p-2 border-b border-border-subtle">
<input
autoFocus
value={search}
onChange={(e) => onSearch(e.target.value)}
placeholder="Search…"
className="w-full h-9 px-2.5 border border-border-default rounded-md outline-none text-sm focus:border-sunrise-500"
/>
</div>
)}
<div className="max-h-56 overflow-auto">
{loading ? (
<div className="px-3 py-3 text-sm text-faint">Searching</div>
) : items.length ? (
items.map((item, i) => (
<button
type="button"
key={itemKey(item, i)}
onClick={() => {
onPick(item);
toggle(false);
}}
className="w-full text-left px-3 py-2.5 text-sm text-body hover:bg-slate-50 border-none bg-transparent cursor-pointer"
>
{renderItem(item)}
</button>
))
) : (
<div className="px-3 py-3 text-sm text-faint">{emptyText}</div>
)}
</div>
</div>,
document.body,
)}
</label>
);
}

View File

@ -0,0 +1,91 @@
import { useMemo, useState } from 'react';
import { PickerShell } from './PickerShell';
export interface SelectFieldOption {
value: string;
label: string;
}
export interface SelectFieldProps {
label?: string;
required?: boolean;
hint?: string;
className?: string;
/** Either strings or {value,label} objects. */
options?: Array<string | SelectFieldOption>;
value: string;
onChange: (value: string) => void;
placeholder?: string;
/** Force the search box on/off. Defaults to on when there are >7 options. */
searchable?: boolean;
/** Allow clearing back to ''. Defaults to true for non-required fields. */
clearable?: boolean;
disabled?: boolean;
}
const norm = (o: string | SelectFieldOption): SelectFieldOption =>
typeof o === 'string' ? { value: o, label: o } : o;
/** Single-select that renders through the shared PickerShell, so it matches
* LookupField exactly. Static options, searched/filtered locally. An empty-
* value option (e.g. {value:'', label:'Choose…'}) is treated as the
* placeholder rather than a list row, and option values are de-duplicated. */
export function SelectField({
label,
required,
hint,
className,
options = [],
value,
onChange,
placeholder,
searchable,
clearable,
disabled,
}: SelectFieldProps) {
const [search, setSearch] = useState('');
// Normalize, lift any empty-value option's label to the placeholder, dedupe.
const { opts, ph } = useMemo(() => {
const all = options.map(norm);
const empty = all.find((o) => o.value === '');
const seen = new Set<string>();
const opts = all.filter((o) => {
if (o.value === '' || seen.has(o.value)) return false;
seen.add(o.value);
return true;
});
return { opts, ph: placeholder ?? empty?.label };
}, [options, placeholder]);
const items = useMemo(() => {
const q = search.trim().toLowerCase();
return q ? opts.filter((o) => o.label.toLowerCase().includes(q)) : opts;
}, [opts, search]);
const display = opts.find((o) => o.value === value)?.label ?? '';
const canSearch = searchable ?? opts.length > 7;
const canClear = (clearable ?? !required) && value !== '';
return (
<PickerShell<SelectFieldOption>
label={label}
required={required}
hint={hint}
className={className}
displayLabel={display}
placeholder={ph}
items={items}
renderItem={(o) => o.label}
itemKey={(o) => o.value}
onPick={(o) => onChange(o.value)}
onClear={canClear ? () => onChange('') : undefined}
searchable={canSearch}
search={search}
onSearch={setSearch}
onOpenChange={(open) => !open && setSearch('')}
emptyText="No options."
disabled={disabled}
/>
);
}

View File

@ -10,5 +10,9 @@ export { Input } from './Input';
export type { InputProps } from './Input'; export type { InputProps } from './Input';
export { Select } from './Select'; export { Select } from './Select';
export type { SelectProps, SelectOption } from './Select'; export type { SelectProps, SelectOption } from './Select';
export { SelectField } from './SelectField';
export type { SelectFieldProps, SelectFieldOption } from './SelectField';
export { PickerShell } from './PickerShell';
export type { PickerShellProps } from './PickerShell';
export { MultiSelect } from './MultiSelect'; export { MultiSelect } from './MultiSelect';
export type { MultiSelectProps, MultiSelectOption } from './MultiSelect'; export type { MultiSelectProps, MultiSelectOption } from './MultiSelect';

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { CheckCircle2 } from 'lucide-react'; import { CheckCircle2 } from 'lucide-react';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Input } from '../core/Input'; import { Input } from '../core/Input';
import { Select } from '../core/Select'; import { SelectField } from '../core/SelectField';
import { MultiSelect } from '../core/MultiSelect'; import { MultiSelect } from '../core/MultiSelect';
import { LookupField } from './LookupField'; import { LookupField } from './LookupField';
import type { LookupValue } from './LookupField'; import type { LookupValue } from './LookupField';
@ -204,12 +204,13 @@ function Field({
case 'select': case 'select':
return ( return (
<Select <SelectField
label={field.label} label={field.label}
required={field.required} required={field.required}
value={String(value ?? '')} value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)} onChange={onChange}
options={[{ value: '', label: field.required ? 'Select…' : '— None —' }, ...(field.options ?? [])]} options={field.options ?? []}
placeholder={field.required ? 'Select…' : '— None —'}
/> />
); );

View File

@ -1,7 +1,5 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { PickerShell } from '../core/PickerShell';
import { ChevronDown, X } from 'lucide-react';
import { cn } from '../../lib/cn';
import { useZino } from '../../api/provider'; import { useZino } from '../../api/provider';
export interface LookupValue { export interface LookupValue {
@ -23,9 +21,11 @@ export interface LookupFieldProps {
onChange: (v: LookupValue | null) => void; onChange: (v: LookupValue | null) => void;
} }
type Row = Record<string, unknown>;
/** Searchable RDBMS-lookup picker (owner agent etc.). Mirrors the renderer's /** Searchable RDBMS-lookup picker (owner agent etc.). Mirrors the renderer's
* FFLookupField: projects the picked row onto storage + display columns. The * FFLookupField: projects the picked row onto storage + display columns.
* dropdown renders in a portal so it isn't clipped by a Card's overflow. */ * Renders through the shared PickerShell so it's identical to SelectField. */
export function LookupField({ export function LookupField({
label, label,
required, required,
@ -41,13 +41,10 @@ export function LookupField({
const { client } = useZino(); const { client } = useZino();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [rows, setRows] = useState<Array<Record<string, unknown>>>([]); const [rows, setRows] = useState<Row[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);
const labelOf = (src: LookupValue | Record<string, unknown> | null) => const labelOf = (src: LookupValue | Row | null) =>
src src
? displayCols ? displayCols
.map((c) => src[c]) .map((c) => src[c])
@ -58,7 +55,7 @@ export function LookupField({
// Collapse rows that project to the same stored value — e.g. many branches // Collapse rows that project to the same stored value — e.g. many branches
// sharing one region show "North" once. Lookups whose storage carries a // sharing one region show "North" once. Lookups whose storage carries a
// unique id never collapse. // unique id never collapse.
const dedupe = (list: Array<Record<string, unknown>>) => { const dedupe = (list: Row[]) => {
const cols = storageCols.length ? storageCols : displayCols; const cols = storageCols.length ? storageCols : displayCols;
const seen = new Set<string>(); const seen = new Set<string>();
return list.filter((row) => { return list.filter((row) => {
@ -69,24 +66,6 @@ export function LookupField({
}); });
}; };
// Anchor the portal panel under the trigger; track scroll/resize while open.
useLayoutEffect(() => {
if (!open) return;
const place = () => {
const el = triggerRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
setPos({ top: r.bottom + 4, left: r.left, width: r.width });
};
place();
window.addEventListener('scroll', place, true);
window.addEventListener('resize', place);
return () => {
window.removeEventListener('scroll', place, true);
window.removeEventListener('resize', place);
};
}, [open]);
// Debounced fetch while the dropdown is open. // Debounced fetch while the dropdown is open.
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
@ -109,103 +88,35 @@ export function LookupField({
live = false; live = false;
clearTimeout(t); clearTimeout(t);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, search, templateUid, activityId, fieldId, instanceId, client]); }, [open, search, templateUid, activityId, fieldId, instanceId, client]);
// Close on outside click — ignore clicks inside the trigger or the portal panel. function pick(row: Row) {
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
const t = e.target as Node;
if (triggerRef.current?.contains(t) || panelRef.current?.contains(t)) return;
setOpen(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
function pick(row: Record<string, unknown>) {
const stored: LookupValue = {}; const stored: LookupValue = {};
storageCols.forEach((c) => (stored[c] = row[c] ?? null)); storageCols.forEach((c) => (stored[c] = row[c] ?? null));
displayCols.forEach((c) => { displayCols.forEach((c) => {
if (!(c in stored)) stored[c] = row[c] ?? null; if (!(c in stored)) stored[c] = row[c] ?? null;
}); });
onChange(stored); onChange(stored);
setOpen(false);
setSearch('');
} }
const current = labelOf(value);
return ( return (
<label className="flex flex-col gap-1.5 font-sans"> <PickerShell<Row>
{label && ( label={label}
<span className="text-sm font-medium text-muted"> required={required}
{label} displayLabel={labelOf(value)}
{required && <span className="text-ruby-600"> *</span>} items={rows}
</span> renderItem={(row) => labelOf(row) || 'Row'}
)} itemKey={(_row, i) => i}
<div onPick={pick}
ref={triggerRef} onClear={() => onChange(null)}
className="flex items-center gap-2 bg-card rounded-md px-3 h-[42px] border border-border-default focus-ring cursor-pointer" search={search}
onClick={() => setOpen((o) => !o)} onSearch={setSearch}
> loading={loading}
<span className={cn('flex-1 truncate text-base', current ? 'text-strong' : 'text-faint')}> onOpenChange={(o) => {
{current || 'Select…'} setOpen(o);
</span> if (!o) setSearch('');
{value ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onChange(null);
}} }}
className="text-faint hover:text-strong"
>
<X size={15} />
</button>
) : (
<ChevronDown size={15} className="text-faint" />
)}
</div>
{open &&
pos &&
createPortal(
<div
ref={panelRef}
style={{ position: 'fixed', top: pos.top, left: pos.left, width: pos.width, zIndex: 50 }}
className="bg-card border border-border-default rounded-md shadow-lg overflow-hidden"
>
<div className="p-2 border-b border-border-subtle">
<input
autoFocus
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-full h-9 px-2.5 border border-border-default rounded-md outline-none text-sm focus:border-sunrise-500"
/> />
</div>
<div className="max-h-56 overflow-auto">
{loading ? (
<div className="px-3 py-3 text-sm text-faint">Searching</div>
) : rows.length ? (
rows.map((row, i) => (
<button
type="button"
key={i}
onClick={() => pick(row)}
className="w-full text-left px-3 py-2.5 text-sm text-body hover:bg-slate-50 border-none bg-transparent cursor-pointer"
>
{labelOf(row) || `Row ${i + 1}`}
</button>
))
) : (
<div className="px-3 py-3 text-sm text-faint">No matches.</div>
)}
</div>
</div>,
document.body,
)}
</label>
); );
} }

View File

@ -6,11 +6,14 @@ import { ZinoProvider } from './api/provider';
import './styles/styles.css'; import './styles/styles.css';
const baseUrl = import.meta.env.VITE_ZINO_API_URL || 'https://sandbox.getzino.in'; const baseUrl = import.meta.env.VITE_ZINO_API_URL || 'https://sandbox.getzino.in';
// Keep client routes under Vite's base (e.g. /lead-to-policy) so a refresh on
// an in-app route hits the preview's per-slug SPA fallback. '/' in dev.
const routerBase = import.meta.env.BASE_URL.replace(/\/$/, '');
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<ZinoProvider baseUrl={baseUrl}> <ZinoProvider baseUrl={baseUrl}>
<BrowserRouter> <BrowserRouter basename={routerBase}>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</ZinoProvider> </ZinoProvider>

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { CheckCircle2, UserCheck } from 'lucide-react'; import { CheckCircle2, UserCheck } from 'lucide-react';
import { AIDecisionCard, Avatar, Badge, Button, Card, formatINR, Select } from '../components'; import { AIDecisionCard, Avatar, Badge, Button, Card, formatINR, SelectField } from '../components';
import { LookupField } from '../components/form'; import { LookupField } from '../components/form';
import type { LookupValue } from '../components/form'; import type { LookupValue } from '../components/form';
import { useQuery, useZino } from '../api/provider'; import { useQuery, useZino } from '../api/provider';
@ -92,7 +92,7 @@ export function AssignScreen() {
) : ( ) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Qualified stage.</div> <div className="flex-1 text-sm text-faint">Select a lead at the Qualified stage.</div>
)} )}
<Select <SelectField
options={[ options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No qualified leads' }, { value: '', label: eligible.length ? 'Choose lead…' : 'No qualified leads' },
...records.map((r) => ({ ...records.map((r) => ({
@ -101,8 +101,8 @@ export function AssignScreen() {
})), })),
]} ]}
value={selectedId} value={selectedId}
onChange={(e) => { onChange={(v) => {
setSelectedId(e.target.value); setSelectedId(v);
setResult(null); setResult(null);
}} }}
className="w-[260px]" className="w-[260px]"

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { AlertTriangle, CheckCircle2, FileText, Info, ShieldCheck, UploadCloud } from 'lucide-react'; import { AlertTriangle, CheckCircle2, FileText, Info, ShieldCheck, UploadCloud } from 'lucide-react';
import { Avatar, Badge, Button, Card, DocumentUploadCard, formatINR, Input, Select } from '../components'; import { Avatar, Badge, Button, Card, DocumentUploadCard, formatINR, Input, SelectField } from '../components';
import type { BadgeTone, DocStatus, OcrField } from '../components'; import type { BadgeTone, DocStatus, OcrField } from '../components';
import { useQuery, useZino } from '../api/provider'; import { useQuery, useZino } from '../api/provider';
import { useLeads } from '../api/leads'; import { useLeads } from '../api/leads';
@ -244,7 +244,7 @@ export function DocumentsScreen() {
) : ( ) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Product Recommended stage.</div> <div className="flex-1 text-sm text-faint">Select a lead at the Product Recommended stage.</div>
)} )}
<Select <SelectField
options={[ options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' }, { value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' },
...records.map((r) => ({ ...records.map((r) => ({
@ -253,8 +253,8 @@ export function DocumentsScreen() {
})), })),
]} ]}
value={selectedId} value={selectedId}
onChange={(e) => { onChange={(v) => {
setSelectedId(e.target.value); setSelectedId(v);
setResult(null); setResult(null);
}} }}
className="w-[260px]" className="w-[260px]"
@ -342,14 +342,14 @@ export function DocumentsScreen() {
value={verifiedIncome} value={verifiedIncome}
onChange={(e) => setVerifiedIncome(Number(e.target.value) || 0)} onChange={(e) => setVerifiedIncome(Number(e.target.value) || 0)}
/> />
<Select <SelectField
label="Document status" label="Document status"
required required
options={docStatusOptions} options={docStatusOptions}
value={docStatus} value={docStatus}
onChange={(e) => { onChange={(v) => {
statusTouched.current = true; statusTouched.current = true;
setDocStatus(e.target.value); setDocStatus(v as DocStatus);
}} }}
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { CheckCircle2 } from 'lucide-react'; import { CheckCircle2 } from 'lucide-react';
import { Avatar, Button, Card, formatINR, Input } from '../components'; import { Avatar, Button, Card, formatINR, Input, SelectField } from '../components';
import { useLeads } from '../api/leads'; import { useLeads } from '../api/leads';
import { useZino } from '../api/provider'; import { useZino } from '../api/provider';
import { productOf } from '../api/adapters'; import { productOf } from '../api/adapters';
@ -112,21 +112,21 @@ export function IssuePolicyScreen() {
) : ( ) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Underwriting stage.</div> <div className="flex-1 text-sm text-faint">Select a lead at the Underwriting stage.</div>
)} )}
<select <SelectField
options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' },
...records.map((r) => ({
value: String(r.instance_id),
label: `#${r.instance_id} ${r.lead_name ?? ''} · ${r.current_state_name ?? ''}`,
})),
]}
value={selectedId} value={selectedId}
onChange={(e) => { onChange={(v) => {
setSelectedId(e.target.value); setSelectedId(v);
setResult(null); setResult(null);
}} }}
className="w-[260px] font-sans text-base text-strong border border-border-default rounded-md px-3 py-2.5 outline-none focus:border-sunrise-500" className="w-[260px]"
> />
<option value="">{eligible.length ? 'Choose lead…' : 'No eligible leads'}</option>
{records.map((r) => (
<option key={r.instance_id} value={String(r.instance_id)}>
#{r.instance_id} {r.lead_name ?? ''} · {r.current_state_name ?? ''}
</option>
))}
</select>
</div> </div>
{result && ( {result && (

View File

@ -10,7 +10,7 @@ import {
Input, Input,
MultiSelect, MultiSelect,
RupeeAmount, RupeeAmount,
Select, SelectField,
} from '../components'; } from '../components';
import { LookupField } from '../components/form'; import { LookupField } from '../components/form';
import type { LookupValue } from '../components/form'; import type { LookupValue } from '../components/form';
@ -158,7 +158,7 @@ export function RecommendScreen() {
) : ( ) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Meeting Scheduled stage.</div> <div className="flex-1 text-sm text-faint">Select a lead at the Meeting Scheduled stage.</div>
)} )}
<Select <SelectField
options={[ options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' }, { value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' },
...records.map((r) => ({ ...records.map((r) => ({
@ -167,8 +167,8 @@ export function RecommendScreen() {
})), })),
]} ]}
value={selectedId} value={selectedId}
onChange={(e) => { onChange={(v) => {
setSelectedId(e.target.value); setSelectedId(v);
setResult(null); setResult(null);
}} }}
className="w-[260px]" className="w-[260px]"
@ -219,11 +219,11 @@ export function RecommendScreen() {
value={sum} value={sum}
onChange={(e) => setSum(Number(e.target.value) || 0)} onChange={(e) => setSum(Number(e.target.value) || 0)}
/> />
<Select <SelectField
label="Premium frequency" label="Premium frequency"
options={freqOptions} options={freqOptions}
value={freq} value={freq}
onChange={(e) => setFreq(e.target.value)} onChange={setFreq}
/> />
<Input <Input
label="Premium amount" label="Premium amount"

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { CheckCircle2 } from 'lucide-react'; import { CheckCircle2 } from 'lucide-react';
import { Avatar, Button, Card, formatINR, Select } from '../components'; import { Avatar, Button, Card, formatINR, SelectField } from '../components';
import { useLeads } from '../api/leads'; import { useLeads } from '../api/leads';
import { useQuery, useZino } from '../api/provider'; import { useQuery, useZino } from '../api/provider';
import { fieldFromSchema } from '../api/schema'; import { fieldFromSchema } from '../api/schema';
@ -94,7 +94,7 @@ export function UnderwritingScreen() {
) : ( ) : (
<div className="flex-1 text-sm text-faint">Select a lead at the Eligibility Assessed stage.</div> <div className="flex-1 text-sm text-faint">Select a lead at the Eligibility Assessed stage.</div>
)} )}
<Select <SelectField
options={[ options={[
{ value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' }, { value: '', label: eligible.length ? 'Choose lead…' : 'No eligible leads' },
...records.map((r) => ({ ...records.map((r) => ({
@ -103,8 +103,8 @@ export function UnderwritingScreen() {
})), })),
]} ]}
value={selectedId} value={selectedId}
onChange={(e) => { onChange={(v) => {
setSelectedId(e.target.value); setSelectedId(v);
setResult(null); setResult(null);
}} }}
className="w-[260px]" className="w-[260px]"
@ -139,11 +139,11 @@ export function UnderwritingScreen() {
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Select <SelectField
label="Underwriting status" label="Underwriting status"
options={statusOptions} options={statusOptions}
value={status} value={status}
onChange={(e) => setStatus(e.target.value)} onChange={setStatus}
/> />
</div> </div>
<div className="text-xs text-faint mt-2"> <div className="text-xs text-faint mt-2">