garage-erp/apps/dashboard/modules/employees/employee-combobox.tsx
2026-04-06 02:32:47 +03:00

179 lines
6.5 KiB
TypeScript

"use client"
import { useRef, useState } from "react"
import { useQuery } from "@tanstack/react-query"
import { Loader2 } from "lucide-react"
import { useAuthApi } from "@/shared/useApi"
import { EMPLOYEE_ROUTES } from "@garage/api"
import { Badge } from "@/shared/components/ui/badge"
import {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxEmpty,
} from "@/shared/components/ui/combobox"
// ── Types ──
export type EmployeeOption = {
value: string
label: string
first_name?: string
last_name?: string
email?: string
type?: string
status?: string
}
// ── Helpers ──
function buildEmployeeOption(item: any): EmployeeOption {
const label =
[item.first_name, item.last_name].filter(Boolean).join(" ") ||
`Employee #${item.id}`
return {
value: String(item.id),
label,
first_name: item.first_name,
last_name: item.last_name,
email: item.email,
type: item.type,
status: item.status,
}
}
function extractItems(response: unknown): any[] {
if (Array.isArray(response)) return response
const obj = response as any
if (Array.isArray(obj?.data?.data)) return obj.data.data
if (Array.isArray(obj?.data)) return obj.data
return []
}
function getInitials(opt: EmployeeOption): string {
return (
[opt.first_name?.[0], opt.last_name?.[0]].filter(Boolean).join("").toUpperCase() || "E"
)
}
// ── Props ──
type EmployeeComboboxProps = {
value?: EmployeeOption | null
onValueChange: (employee: EmployeeOption | null) => void
disabled?: boolean
placeholder?: string
showClear?: boolean
onBlur?: () => void
"aria-invalid"?: boolean
}
// ── Component ──
export function EmployeeCombobox({
value,
onValueChange,
disabled,
placeholder = "Search by name or email...",
showClear,
onBlur,
"aria-invalid": ariaInvalid,
}: EmployeeComboboxProps) {
const api = useAuthApi()
const anchorRef = useRef<HTMLDivElement>(null)
const [inputValue, setInputValue] = useState("")
const { data: options = [], isLoading } = useQuery<EmployeeOption[]>({
queryKey: [EMPLOYEE_ROUTES.INDEX, "employee-select"],
queryFn: async () => {
const res = await api.employees.list()
return extractItems(res).map(buildEmployeeOption)
},
staleTime: 5 * 60 * 1000,
})
const filtered = inputValue
? options.filter((e) =>
[e.first_name, e.last_name, e.email, e.type]
.filter(Boolean)
.join(" ")
.toLowerCase()
.includes(inputValue.toLowerCase()),
)
: options
return (
<div ref={anchorRef}>
<Combobox
value={value}
onValueChange={(val: EmployeeOption | EmployeeOption[] | null) => {
const single = Array.isArray(val) ? val[0] ?? null : val
onValueChange(single)
}}
disabled={disabled}
onInputValueChange={(val: string, { reason }: { reason: string }) => {
if (reason === "input-change") setInputValue(val)
}}
isItemEqualToValue={(item: EmployeeOption, val: any) =>
item?.value === val?.value
}
>
<ComboboxInput
placeholder={placeholder}
showClear={showClear ?? !!value}
onBlur={onBlur}
aria-invalid={ariaInvalid || undefined}
/>
<ComboboxContent anchor={anchorRef}>
<ComboboxList>
{isLoading && (
<div className="flex items-center justify-center py-6">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
</div>
)}
{!isLoading &&
filtered.map((opt) => (
<ComboboxItem key={opt.value} value={opt}>
<div className="flex items-center gap-3 py-0.5 w-full min-w-0">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary border border-border text-sm font-medium">
{getInitials(opt)}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate text-sm font-medium leading-none">
{opt.label}
</span>
<div className="flex items-center gap-1.5">
{opt.type && (
<Badge
variant="secondary"
className="h-4 px-1.5 text-xs font-normal capitalize"
>
{opt.type}
</Badge>
)}
{opt.status && (
<span
className={`text-xs capitalize ${opt.status === "active" ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`}
>
{opt.status}
</span>
)}
</div>
</div>
</div>
</ComboboxItem>
))}
{!isLoading && filtered.length === 0 && (
<ComboboxEmpty>No employees found</ComboboxEmpty>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}