feat(product-prices): inputs по справочнику PriceType — без dropdown'a

Раньше каждая цена в карточке товара рендерилась как 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 d2160f8910
commit c7a498cf9a

View file

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