131 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|