feat(ui): AsyncSelect — серверный поиск в дропдаунах вместо pageSize=500
Some checks are pending
Some checks are pending
Добавлен AsyncSelect в Field.tsx: дебаунс 300ms, fetch ?search=&pageSize=20, поддержка onCreate, getLabel для кастомного отображения (path у групп товаров). Переведены на AsyncSelect: - SupplyEditPage: поставщик - RetailSaleEditPage: покупатель - ProductEditPage: группа товара - ProductQuickCreateModal: группа товара useLookups/useOrgInfra: pageSize 500→100 для малых справочников. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f61d8bc178
commit
f54c8bb5b7
|
|
@ -3,8 +3,10 @@ import {
|
|||
type InputHTMLAttributes, type SelectHTMLAttributes, type ReactNode, type TextareaHTMLAttributes,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { api } from '@/lib/api'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { localizeNativeValidation } from '@/lib/validation'
|
||||
|
||||
|
|
@ -275,6 +277,181 @@ export function Select({
|
|||
)
|
||||
}
|
||||
|
||||
/** Select с серверным поиском: при каждом вводе дёргает endpoint с ?search=&pageSize=20.
|
||||
* Не требует заранее загруженных опций — подходит для справочников с неограниченным ростом
|
||||
* (контрагенты, группы товаров). API: value/onChange работают с ID строкой.
|
||||
* getLabel — как извлечь отображаемый текст из пришедшего объекта (дефолт: item.name). */
|
||||
export function AsyncSelect({
|
||||
url, value, onChange, placeholder, disabled, className, name: htmlName,
|
||||
getLabel = (item: Record<string, unknown>) => String(item['name'] ?? ''),
|
||||
onCreate, createLabel = 'Создать',
|
||||
}: {
|
||||
url: string
|
||||
value?: string | null
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
name?: string
|
||||
getLabel?: (item: Record<string, unknown>) => string
|
||||
onCreate?: (label: string) => Promise<string>
|
||||
createLabel?: string
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [debouncedQuery, setDebouncedQuery] = useState('')
|
||||
const [highlight, setHighlight] = useState(0)
|
||||
const [selectedLabel, setSelectedLabel] = useState('')
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
const wrapRef = useRef<HTMLDivElement>(null)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const [pos, setPos] = useState<{ top: number; left: number; width: number; openUp: boolean } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebouncedQuery(query), 300)
|
||||
return () => clearTimeout(t)
|
||||
}, [query])
|
||||
|
||||
const { data: options = [], isFetching } = useQuery<Record<string, unknown>[]>({
|
||||
queryKey: ['async-select', url, debouncedQuery],
|
||||
queryFn: async () => {
|
||||
const sep = url.includes('?') ? '&' : '?'
|
||||
const res = await api.get<{ items: Record<string, unknown>[] }>(
|
||||
`${url}${sep}search=${encodeURIComponent(debouncedQuery.trim())}&pageSize=20`,
|
||||
)
|
||||
return res.data.items
|
||||
},
|
||||
enabled: open,
|
||||
staleTime: 30_000,
|
||||
placeholderData: (prev) => prev,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) { setSelectedLabel(''); return }
|
||||
const found = options.find((o) => o['id'] === value)
|
||||
if (found) setSelectedLabel(getLabel(found))
|
||||
}, [options, value, getLabel])
|
||||
|
||||
const recomputePos = () => {
|
||||
const el = wrapRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - r.bottom
|
||||
const openUp = spaceBelow < 240 && r.top > spaceBelow
|
||||
setPos({ top: openUp ? r.top - 4 : r.bottom + 4, left: r.left, width: r.width, openUp })
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return
|
||||
recomputePos()
|
||||
const fn = () => setOpen(false)
|
||||
window.addEventListener('scroll', fn, true)
|
||||
window.addEventListener('resize', fn)
|
||||
return () => { window.removeEventListener('scroll', fn, true); window.removeEventListener('resize', fn) }
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
const t = e.target as Node
|
||||
if (!wrapRef.current?.contains(t) && !dropdownRef.current?.contains(t)) { setOpen(false); setQuery('') }
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
return () => document.removeEventListener('mousedown', onDoc)
|
||||
}, [open])
|
||||
|
||||
useEffect(() => { if (open) requestAnimationFrame(() => searchRef.current?.focus()) }, [open])
|
||||
useEffect(() => { setHighlight(0) }, [query, open])
|
||||
|
||||
const choose = (id: string, label: string) => {
|
||||
setOpen(false); setQuery(''); setCreateError(null)
|
||||
setSelectedLabel(label); onChange(id)
|
||||
}
|
||||
|
||||
const trimmed = query.trim()
|
||||
const exactMatch = options.some((o) => getLabel(o).toLowerCase() === trimmed.toLowerCase())
|
||||
const canCreate = !!onCreate && trimmed.length > 0 && !exactMatch
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!onCreate || !trimmed) return
|
||||
setCreating(true); setCreateError(null)
|
||||
try { choose(await onCreate(trimmed), trimmed) }
|
||||
catch (e) { setCreateError((e as Error).message || 'Не удалось создать') }
|
||||
finally { setCreating(false) }
|
||||
}
|
||||
|
||||
const triggerLabel = selectedLabel || (value ? '…' : (placeholder ?? '—'))
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className="relative">
|
||||
<button type="button" disabled={disabled} onClick={() => !disabled && setOpen((o) => !o)}
|
||||
data-name={htmlName}
|
||||
className={cn(inputClass, 'flex items-center justify-between text-left pr-9', className)}
|
||||
>
|
||||
<span className={cn('truncate', !selectedLabel && !value && 'text-slate-400')}>{triggerLabel}</span>
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
||||
</button>
|
||||
{open && pos && createPortal(
|
||||
<div ref={dropdownRef}
|
||||
style={{ position: 'fixed', left: pos.left, width: pos.width, ...(pos.openUp ? { bottom: window.innerHeight - pos.top } : { top: pos.top }) }}
|
||||
className="z-[100] max-h-72 overflow-hidden rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg flex flex-col"
|
||||
>
|
||||
<div className="p-2 border-b border-slate-100 dark:border-slate-800">
|
||||
<input ref={searchRef} value={query} onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Поиск…"
|
||||
className="w-full h-8 rounded border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--color-brand)]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') { e.preventDefault(); setOpen(false); setQuery('') }
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight((h) => Math.min(h + 1, Math.max(0, options.length - 1))) }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight((h) => Math.max(0, h - 1)) }
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (options.length === 0 && canCreate) { handleCreate(); return }
|
||||
const opt = options[highlight]
|
||||
if (opt) choose(String(opt['id'] ?? ''), getLabel(opt))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ul className="py-1 overflow-auto" role="listbox">
|
||||
{isFetching ? (
|
||||
<li className="px-3 py-2 text-sm text-slate-400">Загрузка…</li>
|
||||
) : options.length === 0 && !canCreate ? (
|
||||
<li className="px-3 py-2 text-sm text-slate-400">Ничего не найдено</li>
|
||||
) : options.map((opt, i) => {
|
||||
const id = String(opt['id'] ?? ''); const label = getLabel(opt)
|
||||
return (
|
||||
<li key={id}>
|
||||
<button type="button" onMouseEnter={() => setHighlight(i)} onClick={() => choose(id, label)}
|
||||
className={cn('w-full text-left px-3 py-1.5 text-sm block',
|
||||
i === highlight && 'bg-slate-100 dark:bg-slate-800',
|
||||
id === value && 'font-medium text-[var(--color-brand)]',
|
||||
)}
|
||||
>
|
||||
<span className="truncate block">{label}</span>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{canCreate && (
|
||||
<li className="border-t border-slate-100 dark:border-slate-800 mt-1 pt-1">
|
||||
<button type="button" disabled={creating} onClick={handleCreate}
|
||||
className="w-full text-left px-3 py-1.5 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 text-[var(--color-brand)] disabled:opacity-60"
|
||||
>
|
||||
{creating ? 'Создаю…' : `${createLabel} «${trimmed}»`}
|
||||
</button>
|
||||
{createError && <div className="px-3 py-1 text-xs text-red-600">{createError}</div>}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface MoneyInputProps {
|
||||
value: number | null | undefined
|
||||
onChange: (v: number | null) => void
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { useMutation } from '@tanstack/react-query'
|
|||
import { api } from '@/lib/api'
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Field, TextInput, Select } from '@/components/Field'
|
||||
import { useUnits, useProductGroups } from '@/lib/useLookups'
|
||||
import { Field, TextInput, Select, AsyncSelect } from '@/components/Field'
|
||||
import { useUnits } from '@/lib/useLookups'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { BarcodeType, type Product } from '@/lib/types'
|
||||
import { generateEan13InternalPrefix2 } from '@/lib/barcode'
|
||||
|
|
@ -26,7 +26,6 @@ interface Props {
|
|||
* pre-fill'ятся из набранного запроса в зависимости от типа. */
|
||||
export function ProductQuickCreateModal({ open, onClose, prefill, onCreated }: Props) {
|
||||
const units = useUnits()
|
||||
const groups = useProductGroups()
|
||||
const org = useOrgSettings()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
|
|
@ -67,11 +66,7 @@ export function ProductQuickCreateModal({ open, onClose, prefill, onCreated }: P
|
|||
const sht = units.data.find((u) => u.code === '796') ?? units.data[0]
|
||||
setUnitId(sht.id)
|
||||
}
|
||||
if (!groupId && groups.data?.length) {
|
||||
const food = groups.data.find((g) => g.name === 'Продукты питания') ?? groups.data[0]
|
||||
setGroupId(food.id)
|
||||
}
|
||||
}, [open, units.data, groups.data, unitId, groupId])
|
||||
}, [open, units.data, unitId])
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
|
@ -134,9 +129,13 @@ export function ProductQuickCreateModal({ open, onClose, prefill, onCreated }: P
|
|||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Группа *">
|
||||
<Select value={groupId} onChange={(e) => setGroupId(e.target.value)}>
|
||||
{groups.data?.map((g) => <option key={g.id} value={g.id}>{g.path}</option>)}
|
||||
</Select>
|
||||
<AsyncSelect
|
||||
url="/api/catalog/product-groups"
|
||||
value={groupId}
|
||||
onChange={setGroupId}
|
||||
placeholder="Выберите группу…"
|
||||
getLabel={(item) => String(item['path'] ?? item['name'] ?? '')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Единица измерения *">
|
||||
<Select value={unitId} onChange={(e) => setUnitId(e.target.value)}>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
function useLookup<T>(key: string, url: string) {
|
||||
return useQuery({
|
||||
queryKey: [`lookup:${key}`],
|
||||
queryFn: async () => (await api.get<PagedResult<T>>(`${url}?pageSize=500`)).data.items,
|
||||
queryFn: async () => (await api.get<PagedResult<T>>(`${url}?pageSize=100`)).data.items,
|
||||
// staleTime=0 + refetchOnMount: 'always' гарантируют что любая страница
|
||||
// (например ProductEditPage), монтирующая usePriceTypes / useUnits / etc,
|
||||
// сразу подтянет свежий снапшот справочника после правки в его UI —
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function useOrgInfra() {
|
|||
const cashRegisters = useQuery({
|
||||
queryKey: ['lookup:retail-points'],
|
||||
queryFn: async () =>
|
||||
(await api.get<PagedResult<RetailPoint>>('/api/catalog/retail-points?pageSize=500')).data.items,
|
||||
(await api.get<PagedResult<RetailPoint>>('/api/catalog/retail-points?pageSize=100')).data.items,
|
||||
staleTime: 0,
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: true,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
|||
import { ArrowLeft, Plus, Trash2, Save } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Field, TextInput, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import { Field, TextInput, TextArea, Select, AsyncSelect, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import {
|
||||
useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes,
|
||||
useUnits, useCountries, useCurrencies, usePriceTypes,
|
||||
} from '@/lib/useLookups'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
||||
|
|
@ -57,7 +57,6 @@ export function ProductEditPage() {
|
|||
const qc = useQueryClient()
|
||||
|
||||
const units = useUnits()
|
||||
const groups = useProductGroups()
|
||||
const countries = useCountries()
|
||||
const currencies = useCurrencies()
|
||||
const priceTypes = usePriceTypes()
|
||||
|
|
@ -99,11 +98,7 @@ export function ProductEditPage() {
|
|||
const sht = units.data.find((u) => u.code === '796') ?? units.data[0]
|
||||
setForm((f) => ({ ...f, unitOfMeasureId: sht.id }))
|
||||
}
|
||||
if (isNew && form.productGroupId === '' && groups.data?.length) {
|
||||
// Дефолт — «Продукты питания», иначе первая.
|
||||
const food = groups.data.find((g) => g.name === 'Продукты питания') ?? groups.data[0]
|
||||
setForm((f) => ({ ...f, productGroupId: food.id }))
|
||||
}
|
||||
|
||||
if (isNew && form.purchaseCurrencyId === '' && currencies.data?.length) {
|
||||
const def = org.data?.defaultCurrencyId
|
||||
? currencies.data?.find(c => c.id === org.data!.defaultCurrencyId)
|
||||
|
|
@ -129,7 +124,7 @@ export function ProductEditPage() {
|
|||
barcodes: [{ code: generateEan13InternalPrefix2(), type: BarcodeType.Ean13, isPrimary: true }],
|
||||
}))
|
||||
}
|
||||
}, [isNew, units.data, groups.data, currencies.data, org.data, form.unitOfMeasureId, form.productGroupId, form.purchaseCurrencyId, form.vat, form.article, form.barcodes.length])
|
||||
}, [isNew, units.data, currencies.data, org.data, form.unitOfMeasureId, form.purchaseCurrencyId, form.vat, form.article, form.barcodes.length])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
|
@ -448,9 +443,13 @@ export function ProductEditPage() {
|
|||
<Section title="Классификация">
|
||||
<Grid cols={3}>
|
||||
<Field label="Группа *">
|
||||
<Select required value={form.productGroupId} onChange={(e) => setForm({ ...form, productGroupId: e.target.value })}>
|
||||
{groups.data?.map((g) => <option key={g.id} value={g.id}>{g.path}</option>)}
|
||||
</Select>
|
||||
<AsyncSelect
|
||||
url="/api/catalog/product-groups"
|
||||
value={form.productGroupId}
|
||||
onChange={(v) => setForm({ ...form, productGroupId: v })}
|
||||
placeholder="Выберите группу…"
|
||||
getLabel={(item) => String(item['path'] ?? item['name'] ?? '')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Единица измерения *">
|
||||
<Select required value={form.unitOfMeasureId} onChange={(e) => setForm({ ...form, unitOfMeasureId: e.target.value })}>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
|||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle, Undo2 } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Field, TextInput, TextArea, Select, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import { Field, TextInput, TextArea, Select, AsyncSelect, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import { ProductPicker } from '@/components/ProductPicker'
|
||||
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
||||
|
||||
|
|
@ -53,7 +53,6 @@ export function RetailSaleEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const customers = useSuppliers() // we re-use the suppliers hook (returns all counterparties) — fine for MVP
|
||||
|
||||
const [form, setForm] = useState<Form>(empty)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -268,11 +267,13 @@ export function RetailSaleEditPage() {
|
|||
</Field>
|
||||
)}
|
||||
<Field label="Покупатель (опц.)">
|
||||
<Select value={form.customerId} disabled={isPosted}
|
||||
onChange={(e) => setForm({ ...form, customerId: e.target.value })}>
|
||||
<option value="">— анонимный —</option>
|
||||
{customers.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</Select>
|
||||
<AsyncSelect
|
||||
url="/api/catalog/counterparties"
|
||||
value={form.customerId}
|
||||
disabled={isPosted}
|
||||
onChange={(v) => setForm({ ...form, customerId: v })}
|
||||
placeholder="— анонимный —"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Способ оплаты">
|
||||
<Select value={form.payment} disabled={isPosted}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
|||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Field, TextArea, Select, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import { Field, TextArea, Select, AsyncSelect, Checkbox, MoneyInput, NumberInput } from '@/components/Field'
|
||||
import { DateField } from '@/components/DateField'
|
||||
import { ProductPicker } from '@/components/ProductPicker'
|
||||
import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
||||
import { useStores, useCurrencies, useSuppliers, usePriceTypes } from '@/lib/useLookups'
|
||||
import { useStores, useCurrencies, usePriceTypes } from '@/lib/useLookups'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
||||
|
||||
|
|
@ -63,7 +63,6 @@ export function SupplyEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const suppliers = useSuppliers()
|
||||
const priceTypes = usePriceTypes()
|
||||
// Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе.
|
||||
// Заголовок колонки «Розничная» подменяется его именем чтобы соответствовать
|
||||
|
|
@ -131,11 +130,8 @@ export function SupplyEditPage() {
|
|||
: currencies.data.find((c) => c.code === 'KZT')
|
||||
if (def) setForm((f) => ({ ...f, currencyId: def.id }))
|
||||
}
|
||||
if (!form.supplierId && suppliers.data?.length) {
|
||||
setForm((f) => ({ ...f, supplierId: suppliers.data![0].id }))
|
||||
}
|
||||
}
|
||||
}, [isNew, stores.data, currencies.data, suppliers.data, org.data?.defaultCurrencyId, form.storeId, form.currencyId, form.supplierId])
|
||||
}, [isNew, stores.data, currencies.data, org.data?.defaultCurrencyId, form.storeId, form.currencyId])
|
||||
|
||||
const isDraft = isNew || existing.data?.status === SupplyStatus.Draft
|
||||
const isPosted = existing.data?.status === SupplyStatus.Posted
|
||||
|
|
@ -312,27 +308,23 @@ export function SupplyEditPage() {
|
|||
onChange={(iso) => setForm({ ...form, date: iso ?? '' })} />
|
||||
</Field>
|
||||
<Field label="Поставщик *">
|
||||
<Select
|
||||
<AsyncSelect
|
||||
url="/api/catalog/counterparties"
|
||||
value={form.supplierId}
|
||||
disabled={isPosted}
|
||||
onChange={(e) => setForm({ ...form, supplierId: e.target.value })}
|
||||
onChange={(v) => setForm({ ...form, supplierId: v })}
|
||||
placeholder="Выберите поставщика…"
|
||||
createLabel="Создать поставщика"
|
||||
onCreate={async (name) => {
|
||||
// Быстрое создание — только Name + Type=LegalEntity, остальное
|
||||
// редактируется потом в справочнике контрагентов.
|
||||
const created = await api.post<{ id: string }>('/api/catalog/counterparties', {
|
||||
name, legalName: null, type: 1,
|
||||
bin: null, iin: null, taxNumber: null, countryId: null,
|
||||
address: null, phone: null, email: null,
|
||||
bankName: null, bankAccount: null, bik: null, contactPerson: null, notes: null,
|
||||
})
|
||||
await qc.invalidateQueries({ queryKey: ['lookup:counterparties'] })
|
||||
return created.data.id
|
||||
}}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{suppliers.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</Select>
|
||||
/>
|
||||
</Field>
|
||||
{(stores.data?.length ?? 0) > 1 && (
|
||||
<Field label="Склад *">
|
||||
|
|
|
|||
Loading…
Reference in a new issue