Made Selects searchable

X-Lovable-Edit-ID: edt-a600b5df-8c93-487f-820f-72979318f7f8
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 17:09:14 +00:00
co-authored by renee-png
14 changed files with 431 additions and 389 deletions
+12 -13
View File
@@ -20,6 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import {
Table,
TableBody,
@@ -1147,19 +1148,17 @@ function AddCollectionDialog({
: "All homeowners on this HOA already have a collection on this matter."}
</p>
) : (
<Select value={homeownerId} onValueChange={setHomeownerId}>
<SelectTrigger>
<SelectValue placeholder="Pick a homeowner…" />
</SelectTrigger>
<SelectContent>
{available.map((h) => (
<SelectItem key={h.id} value={h.id}>
{h.last_name}, {h.first_name}
{h.unit_number ? ` — Unit ${h.unit_number}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
<SearchableSelect
value={homeownerId}
onValueChange={setHomeownerId}
placeholder="Pick a homeowner…"
searchPlaceholder="Search homeowners…"
emptyText="No homeowners found."
options={available.map((h) => {
const label = `${h.last_name}, ${h.first_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`;
return { value: h.id, label, keywords: `${h.first_name} ${h.last_name} ${h.unit_number ?? ""}` };
})}
/>
)}
</div>
<div>
+11 -13
View File
@@ -17,6 +17,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
interface FeeItem {
id: string;
@@ -173,7 +174,7 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
{fees.length > 0 && (
<div className="mb-3 space-y-1.5">
<Label className="text-xs">Fee schedule (optional)</Label>
<Select
<SearchableSelect
value={selectedFeeId}
onValueChange={(v) => {
setSelectedFeeId(v);
@@ -187,18 +188,15 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
}));
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Pick from the fee schedule…" />
</SelectTrigger>
<SelectContent>
{fees.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.name} — {formatCurrency(Number(f.amount))}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Pick from the fee schedule…"
searchPlaceholder="Search fee items…"
emptyText="No fee items found."
options={fees.map((f) => ({
value: f.id,
label: `${f.name} — ${formatCurrency(Number(f.amount))}`,
keywords: `${f.name} ${f.description ?? ""}`,
}))}
/>
</div>
)}
<form onSubmit={submit} className="grid grid-cols-1 md:grid-cols-4 gap-3">
+11 -18
View File
@@ -19,6 +19,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { NewFeeItemDialog } from "@/components/fees/new-fee-item-dialog";
interface FeeItem {
@@ -203,14 +204,12 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
</Button>
</div>
{fees.length > 0 && (
<Select
<SearchableSelect
value={selectedFeeId}
onValueChange={(v) => {
setSelectedFeeId(v);
const fee = fees.find((f) => f.id === v);
if (fee) {
// If the fee item has no rate set (0), fall back to the
// case's default rate or the user's profile hourly rate.
const fallback =
caseRecord.default_hourly_rate ?? profileRate ?? 0;
const rate = Number(fee.amount) > 0 ? fee.amount : fallback;
@@ -222,21 +221,15 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
}));
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Pick a rate from the fee schedule…" />
</SelectTrigger>
<SelectContent>
{fees.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.name}
{Number(f.amount) > 0
? ` — ${formatCurrency(Number(f.amount))}/hr`
: " — uses your default rate"}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Pick a rate from the fee schedule…"
searchPlaceholder="Search fee items…"
emptyText="No fee items found."
options={fees.map((f) => ({
value: f.id,
label: `${f.name}${Number(f.amount) > 0 ? ` — ${formatCurrency(Number(f.amount))}/hr` : " — uses your default rate"}`,
keywords: `${f.name} ${f.description ?? ""}`,
}))}
/>
)}
</div>
<form onSubmit={submit} className="grid grid-cols-1 md:grid-cols-5 gap-3">
+23 -42
View File
@@ -1,12 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { SearchableSelect } from "@/components/ui/searchable-select";
import {
fetchClients,
fetchAccessibleCases,
@@ -42,56 +36,43 @@ export function ClientCasePicker({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Client / Association</Label>
<Select
<SearchableSelect
value={clientId}
onValueChange={(id) => {
const c = clients.find((x) => x.id === id) ?? null;
onClientChange(id, c);
onCaseChange("", null);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a client" />
</SelectTrigger>
<SelectContent>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Select a client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
</div>
<div>
<Label>Case (optional)</Label>
<Select
<SearchableSelect
value={caseId}
onValueChange={(id) => {
const k = cases.find((x) => x.id === id) ?? null;
onCaseChange(id, k);
}}
disabled={!clientId}
>
<SelectTrigger>
<SelectValue placeholder={clientId ? "Select case" : "Pick client first"} />
</SelectTrigger>
<SelectContent>
{filteredCases.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
{clientId ? "No cases for this client" : ""}
</div>
) : (
filteredCases.map((c) => (
<SelectItem key={c.id} value={c.id}>
<span className="font-mono text-xs text-muted-foreground mr-2">
{c.case_number}
</span>
{c.title}
</SelectItem>
))
)}
</SelectContent>
</Select>
placeholder={clientId ? "Select case" : "Pick client first"}
searchPlaceholder="Search cases…"
emptyText={clientId ? "No cases for this client." : "Pick a client first."}
options={filteredCases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
render: (
<span>
<span className="font-mono text-xs text-muted-foreground mr-2">{c.case_number}</span>
{c.title}
</span>
),
}))}
/>
</div>
</div>
);
+23 -42
View File
@@ -9,6 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import {
fetchClients,
fetchHomeowners,
@@ -154,25 +155,17 @@ export function FormFieldsFillPanel({
return (
<div key={f.key}>
{labelEl}
<Select
<SearchableSelect
value={v}
onValueChange={(val) => {
const c = clients.find((x) => x.name === val);
// store the name as the substituted value
set(f.key, c?.name ?? val);
}}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select client" />
</SelectTrigger>
<SelectContent>
{clients.map((c) => (
<SelectItem key={c.id} value={c.name}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Select client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.name, label: c.name, keywords: c.name }))}
/>
</div>
);
}
@@ -182,42 +175,30 @@ export function FormFieldsFillPanel({
return (
<div key={f.key} className="space-y-1">
{labelEl}
<Select
<SearchableSelect
value={filterClientId}
onValueChange={(val) => {
setHoClientByField((p) => ({ ...p, [f.key]: val }));
ensureHomeowners(val);
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="Filter by client" />
</SelectTrigger>
<SelectContent>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
placeholder="Filter by client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
triggerClassName="h-8 text-xs"
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
<SearchableSelect
value={v}
onValueChange={(val) => set(f.key, val)}
disabled={!filterClientId}
>
<SelectTrigger className="h-9">
<SelectValue
placeholder={filterClientId ? "Select homeowner" : "Pick client first"}
/>
</SelectTrigger>
<SelectContent>
{hos.map((h) => {
const name = `${h.first_name} ${h.last_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`;
return (
<SelectItem key={h.id} value={name}>
{name}
</SelectItem>
);
placeholder={filterClientId ? "Select homeowner" : "Pick client first"}
searchPlaceholder="Search homeowners…"
emptyText="No homeowners found."
options={hos.map((h) => {
const name = `${h.first_name} ${h.last_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`;
return { value: name, label: name, keywords: name };
})}
/>
})}
</SelectContent>
</Select>
+16 -34
View File
@@ -1,12 +1,6 @@
import { useEffect, useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { SearchableSelect } from "@/components/ui/searchable-select";
import {
fetchClients,
fetchHomeowners,
@@ -46,49 +40,37 @@ export function ClientHomeownerPicker({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Client / Association</Label>
<Select
<SearchableSelect
value={clientId}
onValueChange={(id) => {
const c = clients.find((x) => x.id === id) ?? null;
onClientChange(id, c);
onHomeownerChange("", null);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a client" />
</SelectTrigger>
<SelectContent>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Select a client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
</div>
{showHomeowner && (
<div>
<Label>Homeowner (optional)</Label>
<Select
<SearchableSelect
value={homeownerId}
onValueChange={(id) => {
const h = homeowners.find((x) => x.id === id) ?? null;
onHomeownerChange(id, h);
}}
disabled={!clientId}
>
<SelectTrigger>
<SelectValue placeholder={clientId ? "Select homeowner" : "Pick client first"} />
</SelectTrigger>
<SelectContent>
{homeowners.map((h) => (
<SelectItem key={h.id} value={h.id}>
{h.last_name}, {h.first_name}
{h.unit_number ? ` — Unit ${h.unit_number}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder={clientId ? "Select homeowner" : "Pick client first"}
searchPlaceholder="Search homeowners…"
emptyText="No homeowners found."
options={homeowners.map((h) => {
const label = `${h.last_name}, ${h.first_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`;
return { value: h.id, label, keywords: `${h.first_name} ${h.last_name} ${h.unit_number ?? ""}` };
})}
/>
</div>
)}
</div>
+33 -37
View File
@@ -11,6 +11,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { FileDown } from "lucide-react";
import {
applyVariables,
@@ -348,22 +349,21 @@ export function LetterGenerator() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Case (optional)</Label>
<Select
<SearchableSelect
value={caseId || "__none"}
onValueChange={(v) => setCaseId(v === "__none" ? "" : v)}
>
<SelectTrigger>
<SelectValue placeholder="Link to a case" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">— No case —</SelectItem>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Link to a case"
searchPlaceholder="Search cases…"
emptyText="No cases found."
options={[
{ value: "__none", label: "— No case —" },
...cases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
})),
]}
/>
</div>
<div>
<Label>
@@ -374,29 +374,25 @@ export function LetterGenerator() {
</span>
) : null}
</Label>
<Select value={recipientId} onValueChange={setRecipientId}>
<SelectTrigger>
<SelectValue placeholder="Select a contact" />
</SelectTrigger>
<SelectContent>
{sortedContacts.linked.length > 0 && (
<>
{sortedContacts.linked.map((c) => (
<SelectItem key={c.id} value={c.id}>
★ {c.name}
{c.company ? ` — ${c.company}` : ""}
</SelectItem>
))}
</>
)}
{sortedContacts.others.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
{c.company ? ` — ${c.company}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
<SearchableSelect
value={recipientId}
onValueChange={setRecipientId}
placeholder="Select a contact"
searchPlaceholder="Search contacts…"
emptyText="No contacts found."
options={[
...sortedContacts.linked.map((c) => ({
value: c.id,
label: `★ ${c.name}${c.company ? ` — ${c.company}` : ""}`,
keywords: `${c.name} ${c.company ?? ""}`,
})),
...sortedContacts.others.map((c) => ({
value: c.id,
label: `${c.name}${c.company ? ` — ${c.company}` : ""}`,
keywords: `${c.name} ${c.company ?? ""}`,
})),
]}
/>
</div>
</div>
<div>
+61 -70
View File
@@ -15,13 +15,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Clock, Loader2, Plus, Receipt as ReceiptIcon } from "lucide-react";
import { toast } from "sonner";
import { roundToTenth } from "@/lib/timer";
@@ -181,40 +175,41 @@ export function QuickAddTime() {
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Client</Label>
<Select
<SearchableSelect
value={form.clientId}
onValueChange={(v) => setForm({ ...form, clientId: v, caseId: "" })}
>
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
<SelectContent>
{clients.map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}
</SelectContent>
</Select>
placeholder="Select"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Case</Label>
<Select
<SearchableSelect
value={form.caseId}
onValueChange={(v) => setForm({ ...form, caseId: v })}
disabled={!form.clientId}
>
<SelectTrigger><SelectValue placeholder={form.clientId ? "Select" : "Pick client"} /></SelectTrigger>
<SelectContent>
{filteredCases.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">No cases.</div>
) : filteredCases.map((c) => (
<SelectItem key={c.id} value={c.id}>
placeholder={form.clientId ? "Select" : "Pick client"}
searchPlaceholder="Search cases…"
emptyText="No cases."
options={filteredCases.map((c) => ({
value: c.id,
label: `${c.case_number} ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
render: (
<span>
<span className="text-muted-foreground mr-2">{c.case_number}</span>{c.title}
</SelectItem>
))}
</SelectContent>
</Select>
</span>
),
}))}
/>
</div>
</div>
{fees.length > 0 && (
<div className="space-y-1.5">
<Label className="text-xs">Fee item (optional)</Label>
<Select
<SearchableSelect
value={feeId}
onValueChange={(v) => {
setFeeId(v);
@@ -230,19 +225,15 @@ export function QuickAddTime() {
}));
}
}}
>
<SelectTrigger><SelectValue placeholder="Pick from fee schedule…" /></SelectTrigger>
<SelectContent>
{fees.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.name}
{Number(f.amount) > 0
? ` — $${Number(f.amount).toFixed(2)}/hr`
: " — uses your default rate"}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Pick from fee schedule…"
searchPlaceholder="Search fee items…"
emptyText="No fee items."
options={fees.map((f) => ({
value: f.id,
label: `${f.name}${Number(f.amount) > 0 ? ` — $${Number(f.amount).toFixed(2)}/hr` : " — uses your default rate"}`,
keywords: f.name,
}))}
/>
</div>
)}
<div className="grid grid-cols-3 gap-3">
@@ -366,40 +357,41 @@ export function QuickAddExpense() {
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Client</Label>
<Select
<SearchableSelect
value={form.clientId}
onValueChange={(v) => setForm({ ...form, clientId: v, caseId: "" })}
>
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
<SelectContent>
{clients.map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}
</SelectContent>
</Select>
placeholder="Select"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Case</Label>
<Select
<SearchableSelect
value={form.caseId}
onValueChange={(v) => setForm({ ...form, caseId: v })}
disabled={!form.clientId}
>
<SelectTrigger><SelectValue placeholder={form.clientId ? "Select" : "Pick client"} /></SelectTrigger>
<SelectContent>
{filteredCases.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">No cases.</div>
) : filteredCases.map((c) => (
<SelectItem key={c.id} value={c.id}>
placeholder={form.clientId ? "Select" : "Pick client"}
searchPlaceholder="Search cases…"
emptyText="No cases."
options={filteredCases.map((c) => ({
value: c.id,
label: `${c.case_number} ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
render: (
<span>
<span className="text-muted-foreground mr-2">{c.case_number}</span>{c.title}
</SelectItem>
))}
</SelectContent>
</Select>
</span>
),
}))}
/>
</div>
</div>
{fees.length > 0 && (
<div className="space-y-1.5">
<Label className="text-xs">Fee item (optional)</Label>
<Select
<SearchableSelect
value={feeId}
onValueChange={(v) => {
setFeeId(v);
@@ -413,16 +405,15 @@ export function QuickAddExpense() {
}));
}
}}
>
<SelectTrigger><SelectValue placeholder="Pick from fee schedule…" /></SelectTrigger>
<SelectContent>
{fees.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.name} — ${Number(f.amount).toFixed(2)}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Pick from fee schedule…"
searchPlaceholder="Search fee items…"
emptyText="No fee items."
options={fees.map((f) => ({
value: f.id,
label: `${f.name} — $${Number(f.amount).toFixed(2)}`,
keywords: f.name,
}))}
/>
</div>
)}
<div className="grid grid-cols-2 gap-3">
+25 -28
View File
@@ -18,6 +18,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import {
Popover,
PopoverContent,
@@ -213,46 +214,42 @@ export function NewTaskDialog({
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs">Client (optional)</Label>
<Select
<SearchableSelect
value={clientId ?? "_none"}
onValueChange={(v) => {
const next = v === "_none" ? null : v;
setClientId(next);
// Clear case if it doesn't belong to selected client
if (next && caseId) {
const k = cases.find((c) => c.id === caseId);
if (k && k.client_id !== next) setCaseId(null);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="No client" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none">No client</SelectItem>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
placeholder="No client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={[
{ value: "_none", label: "No client" },
...clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
]}
/>
</div>
<div>
<Label className="text-xs">Case (optional)</Label>
<Select value={caseId ?? "_none"} onValueChange={(v) => setCaseId(v === "_none" ? null : v)}>
<SelectTrigger>
<SelectValue placeholder="No case" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none">No case</SelectItem>
{filteredCases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
</SelectItem>
))}
</SelectContent>
</Select>
<SearchableSelect
value={caseId ?? "_none"}
onValueChange={(v) => setCaseId(v === "_none" ? null : v)}
placeholder="No case"
searchPlaceholder="Search cases…"
emptyText="No cases found."
options={[
{ value: "_none", label: "No case" },
...filteredCases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
})),
]}
/>
</div>
</div>
<div>
+39 -51
View File
@@ -12,13 +12,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Pause, Play, Square, Timer as TimerIcon, Loader2, X, PlusCircle } from "lucide-react";
import { toast } from "sonner";
import { NewFeeItemDialog } from "@/components/fees/new-fee-item-dialog";
@@ -209,44 +203,37 @@ export function HeaderTimer() {
<div className="p-4 space-y-3">
<div className="space-y-1.5">
<Label className="text-xs">Client</Label>
<Select
<SearchableSelect
value={timer.clientId ?? ""}
onValueChange={(v) => setClient(v || null)}
>
<SelectTrigger>
<SelectValue placeholder="Select client" />
</SelectTrigger>
<SelectContent>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
placeholder="Select client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Case</Label>
<Select
<SearchableSelect
value={timer.caseId ?? ""}
onValueChange={(v) => setCase(v || null)}
disabled={!timer.clientId}
>
<SelectTrigger>
<SelectValue placeholder={timer.clientId ? "Select case" : "Pick a client first"} />
</SelectTrigger>
<SelectContent>
{filteredCases.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">No cases for this client.</div>
) : (
filteredCases.map((c) => (
<SelectItem key={c.id} value={c.id}>
<span className="text-muted-foreground mr-2">{c.case_number}</span>
{c.title}
</SelectItem>
))
)}
</SelectContent>
</Select>
placeholder={timer.clientId ? "Select case" : "Pick a client first"}
searchPlaceholder="Search cases…"
emptyText="No cases for this client."
options={filteredCases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}`,
keywords: `${c.case_number} ${c.title}`,
render: (
<span>
<span className="text-muted-foreground mr-2">{c.case_number}</span>
{c.title}
</span>
),
}))}
/>
</div>
<div className="space-y-1.5">
@@ -263,26 +250,27 @@ export function HeaderTimer() {
</Button>
</div>
{feeItems.length > 0 ? (
<Select
value={feeItemId || "__none"}
onValueChange={(v) => handleSelectFee(v === "__none" ? "" : v)}
>
<SelectTrigger>
<SelectValue placeholder="No fee item" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">No fee item</SelectItem>
{feeItems.map((f) => (
<SelectItem key={f.id} value={f.id}>
<SearchableSelect
value={feeItemId}
onValueChange={(v) => handleSelectFee(v)}
placeholder="No fee item"
searchPlaceholder="Search fee items…"
emptyText="No fee items found."
options={feeItems.map((f) => ({
value: f.id,
label: `${f.name} — ${formatCurrency(f.amount)}${f.pricing_type === "flat" ? " flat" : "/hr"}`,
keywords: `${f.name} ${f.description ?? ""}`,
render: (
<span>
<span className="mr-2">{f.name}</span>
<span className="text-muted-foreground">
{formatCurrency(f.amount)}
{f.pricing_type === "flat" ? " flat" : "/hr"}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</span>
),
}))}
/>
) : (
<p className="text-[11px] text-muted-foreground">
No saved items yet. Click "Add new item" to save one for reuse.
+127
View File
@@ -0,0 +1,127 @@
import * as React from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export interface SearchableSelectOption {
value: string;
/** Visible label in the selected trigger */
label: string;
/** Custom render for the option row (defaults to label) */
render?: React.ReactNode;
/** Extra text used for fuzzy matching (case number, email, etc.) */
keywords?: string;
disabled?: boolean;
}
interface SearchableSelectProps {
value?: string | null;
onValueChange: (value: string) => void;
options: SearchableSelectOption[];
placeholder?: string;
searchPlaceholder?: string;
emptyText?: string;
disabled?: boolean;
className?: string;
triggerClassName?: string;
/** Width of popover content. Default: trigger width via "w-[--radix-popover-trigger-width]" */
contentClassName?: string;
/** Optional element rendered above the list (e.g. "Add new") */
footer?: React.ReactNode;
}
export function SearchableSelect({
value,
onValueChange,
options,
placeholder = "Select…",
searchPlaceholder = "Search…",
emptyText = "No results.",
disabled,
className,
triggerClassName,
contentClassName,
footer,
}: SearchableSelectProps) {
const [open, setOpen] = React.useState(false);
const selected = options.find((o) => o.value === value);
return (
<div className={className}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn(
"w-full justify-between font-normal",
!selected && "text-muted-foreground",
triggerClassName,
)}
>
<span className="truncate text-left">
{selected ? selected.label : placeholder}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className={cn("w-[--radix-popover-trigger-width] p-0", contentClassName)}
align="start"
>
<Command
filter={(itemValue, search) => {
const opt = options.find((o) => o.value === itemValue);
if (!opt) return 0;
const hay = `${opt.label} ${opt.keywords ?? ""}`.toLowerCase();
return hay.includes(search.toLowerCase()) ? 1 : 0;
}}
>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{emptyText}</CommandEmpty>
<CommandGroup>
{options.map((opt) => (
<CommandItem
key={opt.value}
value={opt.value}
disabled={opt.disabled}
onSelect={(v) => {
onValueChange(v);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === opt.value ? "opacity-100" : "opacity-0",
)}
/>
<span className="flex-1 truncate">{opt.render ?? opt.label}</span>
</CommandItem>
))}
</CommandGroup>
{footer}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
}
+12 -7
View File
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { ArrowLeft, Loader2 } from "lucide-react";
@@ -111,13 +112,17 @@ function NewCase() {
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Client <span className="text-muted-foreground font-normal">(optional)</span></Label>
<Select value={form.client_id || "__none"} onValueChange={(v) => setForm({ ...form, client_id: v === "__none" ? "" : v })}>
<SelectTrigger><SelectValue placeholder="No client" /></SelectTrigger>
<SelectContent>
<SelectItem value="__none">No client (attach later)</SelectItem>
{clients.map((c) => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}
</SelectContent>
</Select>
<SearchableSelect
value={form.client_id || "__none"}
onValueChange={(v) => setForm({ ...form, client_id: v === "__none" ? "" : v })}
placeholder="No client"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={[
{ value: "__none", label: "No client (attach later)" },
...clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
]}
/>
</div>
<div className="space-y-2">
<Label>Case number</Label>
+25 -21
View File
@@ -10,6 +10,7 @@ import { Textarea } from "@/components/ui/textarea";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Switch } from "@/components/ui/switch";
import { FL_CIRCUITS } from "@/lib/florida";
import { Packer } from "docx";
@@ -331,7 +332,7 @@ function PleadingNewPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Client</Label>
<Select
<SearchableSelect
value={clientId || "__none__"}
onValueChange={(v) => {
const next = v === "__none__" ? "" : v;
@@ -341,29 +342,32 @@ function PleadingNewPage() {
if (k && k.client_id !== next) setCaseId("");
}
}}
>
<SelectTrigger><SelectValue placeholder="None" /></SelectTrigger>
<SelectContent className="max-h-72">
<SelectItem value="__none__">— None —</SelectItem>
{clientOptions.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
placeholder="None"
searchPlaceholder="Search clients…"
emptyText="No clients found."
options={[
{ value: "__none__", label: "— None —" },
...clientOptions.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
]}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Case</Label>
<Select value={caseId || "__none__"} onValueChange={(v) => setCaseId(v === "__none__" ? "" : v)}>
<SelectTrigger><SelectValue placeholder="None" /></SelectTrigger>
<SelectContent className="max-h-72">
<SelectItem value="__none__">— None —</SelectItem>
{filteredCases.map((k) => (
<SelectItem key={k.id} value={k.id}>
{k.case_number} — {k.title}
</SelectItem>
))}
</SelectContent>
</Select>
<SearchableSelect
value={caseId || "__none__"}
onValueChange={(v) => setCaseId(v === "__none__" ? "" : v)}
placeholder="None"
searchPlaceholder="Search cases…"
emptyText="No cases found."
options={[
{ value: "__none__", label: "— None —" },
...filteredCases.map((k) => ({
value: k.id,
label: `${k.case_number} — ${k.title}`,
keywords: `${k.case_number} ${k.title}`,
})),
]}
/>
</div>
</div>
+13 -13
View File
@@ -14,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Loader2, Send, ExternalLink } from "lucide-react";
@@ -109,19 +110,18 @@ function StatusUpdatesPage() {
<form onSubmit={submit} className="space-y-4">
<div className="space-y-1.5">
<Label>Case</Label>
<Select value={caseId} onValueChange={setCaseId}>
<SelectTrigger>
<SelectValue placeholder="Select a case…" />
</SelectTrigger>
<SelectContent>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
{c.client?.name ? ` · ${c.client.name}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
<SearchableSelect
value={caseId}
onValueChange={setCaseId}
placeholder="Select a case…"
searchPlaceholder="Search cases…"
emptyText="No cases found."
options={cases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}${c.client?.name ? ` · ${c.client.name}` : ""}`,
keywords: `${c.case_number} ${c.title} ${c.client?.name ?? ""}`,
}))}
/>
{selectedCase && (
<Link
to="/cases/$caseId"