feat(product-prices): inputs по справочнику PriceType — без dropdown'a
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 43s
CI / Web (React + Vite) (push) Successful in 36s
Docker Web / Build + push Web (push) Successful in 27s
Docker Web / Deploy Web on stage (push) Successful in 11s

Раньше каждая цена в карточке товара рендерилась как dropdown «выбор
PriceType» + поле ввода + кнопка удаления. Это было избыточно:
типы цен и так фиксированы справочником, выбирать нечего.

Теперь:
- Идём по справочнику PriceType (отсортирован по SortOrder→Name).
- На каждый PriceType — одна строка: label = pt.Name, поле MoneyInput.
- IsRequired запись помечается красной звёздочкой * после имени.
- Стерев значение — строка убирается из form.prices (UI пусто).
- Введя значение — создаётся новая запись (currency = KZT
  fallback из справочника), либо обновляется существующая.
- Кнопка «+ Добавить» и иконка удаления убраны — управление набором
  типов цен теперь только через «Настройки → Типы цен».

addPrice/removePrice вспомогательные функции удалены за ненадобностью.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-25 22:55:56 +05:00
parent 7451996f50
commit 748abf7eff

View file

@ -179,12 +179,6 @@ export function ProductEditPage() {
const onSubmit = (e: FormEvent) => { e.preventDefault(); save.mutate() } const onSubmit = (e: FormEvent) => { e.preventDefault(); save.mutate() }
const addPrice = () => setForm({ ...form, prices: [...form.prices, {
priceTypeId: priceTypes.data?.find(p => !form.prices.some(x => x.priceTypeId === p.id))?.id ?? priceTypes.data?.[0]?.id ?? '',
amount: 0,
currencyId: currencies.data?.find(c => c.code === 'KZT')?.id ?? '',
}] })
const removePrice = (i: number) => setForm({ ...form, prices: form.prices.filter((_, ix) => ix !== i) })
const updatePrice = (i: number, patch: Partial<PriceRow>) => const updatePrice = (i: number, patch: Partial<PriceRow>) =>
setForm({ ...form, prices: form.prices.map((p, ix) => ix === i ? { ...p, ...patch } : p) }) setForm({ ...form, prices: form.prices.map((p, ix) => ix === i ? { ...p, ...patch } : p) })
@ -409,49 +403,51 @@ export function ProductEditPage() {
Привести к себестоимости Привести к себестоимости
</Button> </Button>
)} )}
<Button type="button" variant="secondary" size="sm" onClick={addPrice}><Plus className="w-3.5 h-3.5" /> Добавить</Button>
</div> </div>
} }
> >
{form.prices.length === 0 ? ( {/* Список цен рендерится по справочнику PriceType: одно поле на каждый
<div className="text-sm text-slate-400 py-2">Цен ещё нет. Добавь хотя бы розничную.</div> * тип, без выпадашки выбора. Значение хранится в form.prices,
) : ( * key = priceTypeId. Для отсутствующих записей при наборе создаётся
<div className="space-y-2"> * новая, при стирании null Amount (UI пустое). Системная запись
{form.prices.map((p, i) => ( * (IsSystem) и обязательные (IsRequired) помечаются звёздочкой. */}
<div key={i} className="grid grid-cols-12 gap-2 items-center"> <div className="space-y-3">
<div className={org.data?.multiCurrencyEnabled ? 'col-span-6' : 'col-span-8'}> {priceTypes.data?.slice().sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)).map((pt) => {
<Select value={p.priceTypeId} onChange={(e) => updatePrice(i, { priceTypeId: e.target.value })}> const idx = form.prices.findIndex(p => p.priceTypeId === pt.id)
{priceTypes.data?.map((pt) => <option key={pt.id} value={pt.id}>{pt.name}</option>)} const row = idx >= 0 ? form.prices[idx] : null
</Select> const required = pt.isRequired
</div> return (
<div className="col-span-3"> <div key={pt.id} className="grid grid-cols-1 sm:grid-cols-3 gap-2 items-start">
<label className="text-sm text-slate-700 dark:text-slate-200 sm:pt-2">
{pt.name}{required && <span className="text-red-500"> *</span>}
</label>
<div className="sm:col-span-2">
<MoneyInput <MoneyInput
value={p.amount} value={row?.amount ?? null}
onChange={(n) => updatePrice(i, { amount: n ?? 0 })} onChange={(n) => {
currencyCode={currencies.data?.find((c) => c.id === p.currencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined} if (n == null) {
currencySymbol={currencies.data?.find((c) => c.id === p.currencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined} // Стерли значение — удаляем строку, чтобы не слать 0 как required.
setForm({ ...form, prices: form.prices.filter(x => x.priceTypeId !== pt.id) })
return
}
if (idx >= 0) {
updatePrice(idx, { amount: n })
} else {
const cur = currencies.data?.find(c => c.code === 'KZT')?.id ?? currencies.data?.[0]?.id ?? ''
setForm({ ...form, prices: [...form.prices, { priceTypeId: pt.id, amount: n, currencyId: cur }] })
}
}}
currencyCode={currencies.data?.find((c) => c.id === row?.currencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
currencySymbol={currencies.data?.find((c) => c.id === row?.currencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
/> />
</div> </div>
{org.data?.multiCurrencyEnabled && (
<div className="col-span-2">
<Select value={p.currencyId} onChange={(e) => updatePrice(i, { currencyId: e.target.value })}>
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
</Select>
</div>
)}
<button
type="button"
onClick={() => removePrice(i)}
className="col-span-1 text-slate-400 hover:text-red-600 flex justify-center"
title="Удалить строку"
>
<Trash2 className="w-4 h-4" />
</button>
</div> </div>
))} )
</div> })}
)} {priceTypes.data?.length === 0 && (
<div className="text-sm text-slate-400 py-2">Нет ни одного типа цен. Создай в «Настройки Типы цен».</div>
)}
</div>
</Section> </Section>
{!isNew && id && ( {!isNew && id && (