feat(web): product card pricing UI + settings toggles
- OrganizationSettingsPage: 2 новые галки —
«Несколько типов цен (Опт, VIP и т.п.)» (multiplePriceTypesEnabled)
и «Показывать «Эталонную цену» на товаре» (showReferencePriceOnProduct).
- ProductEditPage:
• «Эталонная цена» с подписью «не обязательное поле»; рендерится только
при showReferencePriceOnProduct=true.
• «Себестоимость» — readonly MoneyInput, всегда виден; подпись
«расчётная (скользящее среднее)».
• Заголовок секции цен меняется: «Цены продажи» при multipleEnabled,
иначе «Розничная цена».
• Кнопка «Привести к себестоимости» (только для existing товара) —
POST /api/catalog/products/{id}/recalc-retail. После 200 — обновляем
дефолтный PriceType в form.prices, инвалидируем кэш. После 400 —
показываем сообщение в общий error.
- useOrgSettings.ts: добавлены поля multiplePriceTypesEnabled и
showReferencePriceOnProduct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de23f5fc7a
commit
e74bec3964
|
|
@ -15,6 +15,8 @@ export interface OrgSettings {
|
|||
showMarkedOnProduct: boolean
|
||||
showMinMaxStock: boolean
|
||||
allowFractionalPrices: boolean
|
||||
multiplePriceTypesEnabled: boolean
|
||||
showReferencePriceOnProduct: boolean
|
||||
}
|
||||
|
||||
export function useOrgSettings() {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ export function OrganizationSettingsPage() {
|
|||
showMarkedOnProduct: form.showMarkedOnProduct,
|
||||
showMinMaxStock: form.showMinMaxStock,
|
||||
allowFractionalPrices: form.allowFractionalPrices,
|
||||
multiplePriceTypesEnabled: form.multiplePriceTypesEnabled,
|
||||
showReferencePriceOnProduct: form.showReferencePriceOnProduct,
|
||||
}
|
||||
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
||||
},
|
||||
|
|
@ -150,6 +152,26 @@ export function OrganizationSettingsPage() {
|
|||
Включи если хочешь использовать цены с двумя знаками после запятой (1500.50 ₸).
|
||||
По умолчанию — целые тенге, без копеек.
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
label='Несколько типов цен (Опт, VIP и т.п.)'
|
||||
checked={form.multiplePriceTypesEnabled}
|
||||
onChange={(v) => setForm({ ...form, multiplePriceTypesEnabled: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
Если включено — в меню появляется «Типы цен», на карточке товара —
|
||||
список цен по всем типам. По умолчанию одна розничная цена.
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
label='Показывать «Эталонную цену» на товаре'
|
||||
checked={form.showReferencePriceOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showReferencePriceOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
Справочная цена закупа — необязательное поле. Авто-заполняется первой
|
||||
проведённой приёмкой и через 30 дней без приёмок переписывается на текущую себестоимость.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="mt-4 flex gap-3 items-center">
|
||||
|
|
|
|||
|
|
@ -317,14 +317,26 @@ export function ProductEditPage() {
|
|||
|
||||
<Section title="Закупка">
|
||||
<Grid cols={4}>
|
||||
<Field label="Эталонная цена">
|
||||
{org.data?.showReferencePriceOnProduct && (
|
||||
<Field label="Эталонная цена">
|
||||
<MoneyInput
|
||||
value={form.referencePrice === '' ? null : Number(form.referencePrice)}
|
||||
onChange={(n) => setForm({ ...form, referencePrice: n == null ? '' : String(n) })}
|
||||
currencyCode={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
|
||||
currencySymbol={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">не обязательное поле</p>
|
||||
</Field>
|
||||
)}
|
||||
<Field label="Себестоимость">
|
||||
<MoneyInput
|
||||
value={form.referencePrice === '' ? null : Number(form.referencePrice)}
|
||||
onChange={(n) => setForm({ ...form, referencePrice: n == null ? '' : String(n) })}
|
||||
currencyCode={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
|
||||
currencySymbol={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
|
||||
|
||||
value={existing.data?.cost ?? 0}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
||||
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">расчётная (скользящее среднее)</p>
|
||||
</Field>
|
||||
{org.data?.multiCurrencyEnabled && (
|
||||
<Field label="Валюта закупки">
|
||||
|
|
@ -359,8 +371,40 @@ export function ProductEditPage() {
|
|||
)}
|
||||
|
||||
<Section
|
||||
title="Цены продажи"
|
||||
action={<Button type="button" variant="secondary" size="sm" onClick={addPrice}><Plus className="w-3.5 h-3.5" /> Добавить</Button>}
|
||||
title={org.data?.multiplePriceTypesEnabled ? 'Цены продажи' : 'Розничная цена'}
|
||||
action={
|
||||
<div className="flex items-center gap-2">
|
||||
{!isNew && id && (
|
||||
<Button type="button" variant="secondary" size="sm" onClick={async () => {
|
||||
try {
|
||||
const res = await api.post<{ retail: number }>(`/api/catalog/products/${id}/recalc-retail`)
|
||||
// Обновим UI значение в form.prices для дефолтного PriceType.
|
||||
const def = priceTypes.data?.find((pt) => pt.isDefault) ?? priceTypes.data?.find((pt) => pt.isRetail) ?? priceTypes.data?.[0]
|
||||
if (def) {
|
||||
setForm((f) => {
|
||||
const has = f.prices.some(p => p.priceTypeId === def.id)
|
||||
return {
|
||||
...f,
|
||||
prices: has
|
||||
? f.prices.map(p => p.priceTypeId === def.id ? { ...p, amount: res.data.retail } : p)
|
||||
: [...f.prices, { priceTypeId: def.id, amount: res.data.retail, currencyId: currencies.data?.[0]?.id ?? '' }],
|
||||
}
|
||||
})
|
||||
}
|
||||
await qc.invalidateQueries({ queryKey: ['/api/catalog/products', id] })
|
||||
} catch (e) {
|
||||
const msg = (e as { response?: { data?: { error?: string } } }).response?.data?.error ?? 'Ошибка пересчёта'
|
||||
setError(msg)
|
||||
}
|
||||
}}>
|
||||
Привести к себестоимости
|
||||
</Button>
|
||||
)}
|
||||
{org.data?.multiplePriceTypesEnabled && (
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue