feat(web): product card pricing UI + settings toggles
Some checks are pending
Some checks are pending
- 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
b2f589655f
commit
a3cf68eb11
|
|
@ -15,6 +15,8 @@ export interface OrgSettings {
|
||||||
showMarkedOnProduct: boolean
|
showMarkedOnProduct: boolean
|
||||||
showMinMaxStock: boolean
|
showMinMaxStock: boolean
|
||||||
allowFractionalPrices: boolean
|
allowFractionalPrices: boolean
|
||||||
|
multiplePriceTypesEnabled: boolean
|
||||||
|
showReferencePriceOnProduct: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOrgSettings() {
|
export function useOrgSettings() {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ export function OrganizationSettingsPage() {
|
||||||
showMarkedOnProduct: form.showMarkedOnProduct,
|
showMarkedOnProduct: form.showMarkedOnProduct,
|
||||||
showMinMaxStock: form.showMinMaxStock,
|
showMinMaxStock: form.showMinMaxStock,
|
||||||
allowFractionalPrices: form.allowFractionalPrices,
|
allowFractionalPrices: form.allowFractionalPrices,
|
||||||
|
multiplePriceTypesEnabled: form.multiplePriceTypesEnabled,
|
||||||
|
showReferencePriceOnProduct: form.showReferencePriceOnProduct,
|
||||||
}
|
}
|
||||||
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
||||||
},
|
},
|
||||||
|
|
@ -150,6 +152,26 @@ export function OrganizationSettingsPage() {
|
||||||
Включи если хочешь использовать цены с двумя знаками после запятой (1500.50 ₸).
|
Включи если хочешь использовать цены с двумя знаками после запятой (1500.50 ₸).
|
||||||
По умолчанию — целые тенге, без копеек.
|
По умолчанию — целые тенге, без копеек.
|
||||||
</p>
|
</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>
|
</section>
|
||||||
|
|
||||||
<div className="mt-4 flex gap-3 items-center">
|
<div className="mt-4 flex gap-3 items-center">
|
||||||
|
|
|
||||||
|
|
@ -317,14 +317,26 @@ export function ProductEditPage() {
|
||||||
|
|
||||||
<Section title="Закупка">
|
<Section title="Закупка">
|
||||||
<Grid cols={4}>
|
<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
|
<MoneyInput
|
||||||
value={form.referencePrice === '' ? null : Number(form.referencePrice)}
|
value={existing.data?.cost ?? 0}
|
||||||
onChange={(n) => setForm({ ...form, referencePrice: n == null ? '' : String(n) })}
|
onChange={() => {}}
|
||||||
currencyCode={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.code ?? org.data?.defaultCurrencyCode ?? undefined}
|
disabled
|
||||||
currencySymbol={currencies.data?.find((c) => c.id === form.purchaseCurrencyId)?.symbol ?? org.data?.defaultCurrencySymbol ?? undefined}
|
currencyCode={org.data?.defaultCurrencyCode ?? undefined}
|
||||||
|
currencySymbol={org.data?.defaultCurrencySymbol ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">расчётная (скользящее среднее)</p>
|
||||||
</Field>
|
</Field>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Field label="Валюта закупки">
|
<Field label="Валюта закупки">
|
||||||
|
|
@ -359,8 +371,40 @@ export function ProductEditPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
title="Цены продажи"
|
title={org.data?.multiplePriceTypesEnabled ? 'Цены продажи' : 'Розничная цена'}
|
||||||
action={<Button type="button" variant="secondary" size="sm" onClick={addPrice}><Plus className="w-3.5 h-3.5" /> Добавить</Button>}
|
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 ? (
|
{form.prices.length === 0 ? (
|
||||||
<div className="text-sm text-slate-400 py-2">Цен ещё нет. Добавь хотя бы розничную.</div>
|
<div className="text-sm text-slate-400 py-2">Цен ещё нет. Добавь хотя бы розничную.</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue