garage-erp/apps/dashboard/modules/employees/rhf-employee-select-field.tsx
2026-04-15 04:59:05 +03:00

131 lines
4.3 KiB
TypeScript

"use client"
import { useState } from "react"
import { useFormContext, useController, type FieldValues, type FieldPath } from "react-hook-form"
import { useQueryClient } from "@tanstack/react-query"
import { PlusIcon } from "lucide-react"
import { EMPLOYEE_ROUTES } from "@garage/api"
import { Field, FieldLabel, FieldError, FieldDescription } from "@/shared/components/ui/field"
import { FieldShell } from "@/shared/components/form/field-shell"
import { Button } from "@/shared/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import { EmployeeCombobox } from "./employee-combobox"
import { EmployeeForm } from "./employee-form"
// ── Props ──
export type RhfEmployeeSelectFieldProps<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
> = {
name: TName
label?: string
description?: string
required?: boolean
disabled?: boolean
placeholder?: string
/** Show a "+" button to create a new employee inline */
showCreate?: boolean
}
// ── Component ──
export function RhfEmployeeSelectField<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
>({
name,
label = "Employee",
description,
required,
disabled,
placeholder = "Search by name or email...",
showCreate,
}: RhfEmployeeSelectFieldProps<TValues, TName>) {
const { control } = useFormContext<TValues>()
const {
field,
fieldState: { error },
} = useController({ name, control, disabled })
const queryClient = useQueryClient()
const [isCreateOpen, setIsCreateOpen] = useState(false)
const handleCreateSuccess = () => {
queryClient.invalidateQueries({ queryKey: [EMPLOYEE_ROUTES.INDEX] })
setIsCreateOpen(false)
}
const combobox = (
<EmployeeCombobox
value={field.value}
onValueChange={(emp) => {
field.onChange(emp ? { value: emp.value, label: emp.label } : null)
}}
disabled={field.disabled}
placeholder={placeholder}
showClear={!!field.value}
onBlur={field.onBlur}
aria-invalid={!!error || undefined}
/>
)
if (showCreate) {
return (
<>
<Field data-invalid={!!error || undefined}>
{label && (
<div className="flex items-center justify-between">
<FieldLabel>
{label}
{required && <span className="text-destructive ms-0.5">*</span>}
</FieldLabel>
<Button
type="button"
size="icon"
variant="ghost"
className="h-5 w-5"
onClick={() => setIsCreateOpen(true)}
title="Add new employee"
>
<PlusIcon className="h-3.5 w-3.5" />
</Button>
</div>
)}
{combobox}
{description && <FieldDescription>{description}</FieldDescription>}
{error && <FieldError>{error.message}</FieldError>}
</Field>
<Dialog open={isCreateOpen} onOpenChange={(v) => { if (!v) setIsCreateOpen(false) }}>
<DialogContent className="min-w-xl">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">Add Employee</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh] px-4">
<EmployeeForm onSuccess={handleCreateSuccess} />
</ScrollArea>
</DialogContent>
</Dialog>
</>
)
}
return (
<FieldShell
label={label}
error={error?.message}
description={description}
required={required}
>
{combobox}
</FieldShell>
)
}