feat: [quotes] 견적 등록 개선
- 수주처 선택 시 담당자/연락처 자동 입력 - 현장명 직접 입력 가능 (creatable 옵션) - SearchableSelect에 creatable 기능 추가
This commit is contained in:
@@ -775,10 +775,13 @@ export function QuoteRegistration({
|
||||
options={clients.map((c) => ({ value: c.id, label: c.vendorName }))}
|
||||
value={formData.clientId}
|
||||
onChange={(value, option) => {
|
||||
const selectedClient = clients.find((c) => c.id === value);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
clientId: value,
|
||||
clientName: option.label,
|
||||
manager: selectedClient?.managerName || selectedClient?.representativeName || prev.manager,
|
||||
contact: selectedClient?.managerPhone || selectedClient?.mobile || selectedClient?.phone || prev.contact,
|
||||
}));
|
||||
}}
|
||||
placeholder={isLoadingClients ? "로딩 중..." : "수주처를 선택하세요"}
|
||||
@@ -794,10 +797,12 @@ export function QuoteRegistration({
|
||||
options={siteNames.map((name) => ({ value: name, label: name }))}
|
||||
value={formData.siteName}
|
||||
onChange={(value) => handleFieldChange("siteName", value)}
|
||||
placeholder="현장명을 선택하세요"
|
||||
searchPlaceholder="현장명 검색..."
|
||||
placeholder="현장명을 선택 또는 입력하세요"
|
||||
searchPlaceholder="현장명 검색 또는 입력..."
|
||||
emptyText="현장명이 없습니다"
|
||||
disabled={isViewMode}
|
||||
creatable
|
||||
creatableText="직접 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react';
|
||||
import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
|
||||
import { cn } from './utils';
|
||||
import { Button } from './button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||
@@ -45,6 +45,10 @@ interface SearchableSelectProps {
|
||||
isLoading?: boolean;
|
||||
/** 최소 입력 안내 메시지 */
|
||||
minInputText?: string;
|
||||
/** 직접 입력 허용 (목록에 없는 값 생성 가능) */
|
||||
creatable?: boolean;
|
||||
/** 직접 입력 시 안내 텍스트 */
|
||||
creatableText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,12 +82,15 @@ export function SearchableSelect({
|
||||
onSearch,
|
||||
isLoading = false,
|
||||
minInputText = '한글 1자 또는 영문 2자 이상 입력하세요',
|
||||
creatable = false,
|
||||
creatableText = '직접 입력',
|
||||
}: SearchableSelectProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const selectedOption = options.find((opt) => opt.value === value)
|
||||
|| (creatable && value ? { value, label: value } : undefined);
|
||||
const isServerSearch = !!onSearch;
|
||||
const queryValid = isValidSearchQuery(searchQuery);
|
||||
|
||||
@@ -147,6 +154,20 @@ export function SearchableSelect({
|
||||
// 서버 검색 모드에서 검색어 미입력/미충족 시 옵션 숨김
|
||||
const displayOptions = isServerSearch && !queryValid ? [] : options;
|
||||
|
||||
// creatable: 검색어가 기존 옵션에 정확히 매칭되지 않으면 생성 옵션 표시
|
||||
const showCreateOption =
|
||||
creatable &&
|
||||
searchQuery.trim() &&
|
||||
!options.some((opt) => opt.label === searchQuery.trim());
|
||||
|
||||
const handleCreate = () => {
|
||||
const trimmed = searchQuery.trim();
|
||||
const newOption: SearchableSelectOption = { value: trimmed, label: trimmed };
|
||||
onChange(trimmed, newOption);
|
||||
setOpen(false);
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -187,7 +208,7 @@ export function SearchableSelect({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CommandEmpty>{getEmptyMessage()}</CommandEmpty>
|
||||
{!showCreateOption && <CommandEmpty>{getEmptyMessage()}</CommandEmpty>}
|
||||
<CommandGroup>
|
||||
{displayOptions.map((option) => (
|
||||
<CommandItem
|
||||
@@ -213,6 +234,18 @@ export function SearchableSelect({
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{showCreateOption && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value={`__create__${searchQuery.trim()}`}
|
||||
onSelect={handleCreate}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<span>{creatableText}: <strong>{searchQuery.trim()}</strong></span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
|
||||
Reference in New Issue
Block a user