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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user