feat(admin): signed profit/discount fields for special price packages
Let buy/sell extras be entered as profit or discount so negative values work in the editor and table. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export function sanitizeNumericString(
|
||||
const allowDecimal = options.allowDecimal !== false
|
||||
let s = persian2english(String(raw ?? '').trim())
|
||||
.replace(/[٬,\s]/g, '')
|
||||
.replace(/[−–—]/g, '-')
|
||||
.replace(/٫/g, '.')
|
||||
.replace(/\//g, '.')
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@ import { persian2english, parseNumericValue, sanitizeNumericString } from '@core
|
||||
|
||||
export function usePrice(price: number | string, digit = true, unit = 'ریال') {
|
||||
const n = typeof price === 'number' ? price : parseNumericValue(price)
|
||||
let _price = Number.isFinite(n) ? String(n) : persian2english(String(price ?? ''))
|
||||
if (digit && Number.isFinite(n)) {
|
||||
const finite = Number.isFinite(n)
|
||||
const sign = finite && n < 0 ? '-' : ''
|
||||
const abs = finite ? Math.abs(n) : NaN
|
||||
let _price = finite ? String(abs) : persian2english(String(price ?? ''))
|
||||
if (digit && Number.isFinite(abs)) {
|
||||
_price = _price.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
return `${_price} ${unit}`
|
||||
const shown = finite ? `${sign}${_price}` : _price
|
||||
return unit ? `${shown} ${unit}` : shown
|
||||
}
|
||||
|
||||
export function useTowDecimal(num: string | number): string | number {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="signed-profit" :class="rootClass">
|
||||
<a-radio-group
|
||||
v-model:value="mode"
|
||||
button-style="solid"
|
||||
class="signed-profit__modes w-full"
|
||||
@change="onModeChange"
|
||||
>
|
||||
<a-radio-button value="profit" class="signed-profit__radio signed-profit__radio--profit">
|
||||
<span class="signed-profit__sign">+</span>
|
||||
سود
|
||||
</a-radio-button>
|
||||
<a-radio-button value="discount" class="signed-profit__radio signed-profit__radio--discount">
|
||||
<span class="signed-profit__sign">−</span>
|
||||
تخفیف
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
|
||||
<BasicInput
|
||||
:value="absDisplay"
|
||||
type="price"
|
||||
addon-before="ریال"
|
||||
place-holder="۰"
|
||||
@update:value="onAmountChange"
|
||||
/>
|
||||
|
||||
<p class="signed-profit__hint">{{ hint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import BasicInput from '@/components/BasicInput.vue'
|
||||
import { parseNumericValue } from '@core/utils/convert'
|
||||
|
||||
const props = defineProps<{
|
||||
value?: number | string | null
|
||||
kind: 'buy' | 'sell'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:value': [value: number]
|
||||
}>()
|
||||
|
||||
const numeric = computed(() => {
|
||||
const n = typeof props.value === 'number' ? props.value : parseNumericValue(props.value)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
})
|
||||
|
||||
const mode = ref<'profit' | 'discount'>('profit')
|
||||
|
||||
watch(
|
||||
numeric,
|
||||
(n) => {
|
||||
if (n < 0) mode.value = 'discount'
|
||||
else if (n > 0) mode.value = 'profit'
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const absDisplay = computed(() => Math.abs(numeric.value))
|
||||
|
||||
const rootClass = computed(() =>
|
||||
mode.value === 'discount' ? 'signed-profit--discount' : 'signed-profit--profit',
|
||||
)
|
||||
|
||||
const hint = computed(() => {
|
||||
if (props.kind === 'buy') {
|
||||
return mode.value === 'discount'
|
||||
? 'تخفیف: از قیمت خرید مشتری کم میشود'
|
||||
: 'سود: به قیمت خرید مشتری اضافه میشود'
|
||||
}
|
||||
return mode.value === 'discount'
|
||||
? 'تخفیف: به قیمت فروش مشتری اضافه میشود'
|
||||
: 'سود: از قیمت فروش مشتری کم میشود'
|
||||
})
|
||||
|
||||
function emitSigned(abs: number, discount: boolean) {
|
||||
const safe = Number.isFinite(abs) ? Math.abs(Math.round(abs)) : 0
|
||||
emit('update:value', discount && safe > 0 ? -safe : safe)
|
||||
}
|
||||
|
||||
function onModeChange() {
|
||||
emitSigned(absDisplay.value, mode.value === 'discount')
|
||||
}
|
||||
|
||||
function onAmountChange(raw: string | number) {
|
||||
const n = typeof raw === 'number' ? raw : parseNumericValue(raw)
|
||||
emitSigned(Number.isFinite(n) ? n : 0, mode.value === 'discount')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.signed-profit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.signed-profit__modes {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signed-profit__modes :deep(.ant-radio-button-wrapper) {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.signed-profit__sign {
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.signed-profit__hint {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: var(--admin-text-secondary, rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
.signed-profit--profit .signed-profit__hint {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.signed-profit--discount .signed-profit__hint {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.signed-profit__modes :deep(.ant-radio-button-wrapper-checked.signed-profit__radio--profit) {
|
||||
background: #15803d !important;
|
||||
border-color: #15803d !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.signed-profit__modes :deep(.ant-radio-button-wrapper-checked.signed-profit__radio--discount) {
|
||||
background: #b91c1c !important;
|
||||
border-color: #b91c1c !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .signed-profit--profit .signed-profit__hint,
|
||||
.dark .signed-profit--profit .signed-profit__hint {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .signed-profit--discount .signed-profit__hint,
|
||||
.dark .signed-profit--discount .signed-profit__hint {
|
||||
color: #fca5a5;
|
||||
}
|
||||
</style>
|
||||
@@ -43,11 +43,11 @@
|
||||
</a-form-item>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<a-form-item label="سود فروش" name="profit_sell">
|
||||
<BasicInput v-model:value="form.profit_sell" type="price" addon-before="ریال" />
|
||||
<a-form-item label="سود / تخفیف فروش" name="profit_sell">
|
||||
<SignedProfitInput v-model:value="form.profit_sell" kind="sell" />
|
||||
</a-form-item>
|
||||
<a-form-item label="سود خرید" name="profit_buy">
|
||||
<BasicInput v-model:value="form.profit_buy" type="price" addon-before="ریال" />
|
||||
<a-form-item label="سود / تخفیف خرید" name="profit_buy">
|
||||
<SignedProfitInput v-model:value="form.profit_buy" kind="buy" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@ import { debounce } from 'lodash'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import BasicInput from '@/components/BasicInput.vue'
|
||||
import BasicSelect from '@/components/BasicSelect.vue'
|
||||
import SignedProfitInput from './SignedProfitInput.vue'
|
||||
import { syncAndLoadCategories, type CategorySelectItem } from '@/utils/categorySelect'
|
||||
import {
|
||||
getProductsByCategories,
|
||||
@@ -87,8 +88,8 @@ export type SpecialPriceForm = {
|
||||
category_names: string[]
|
||||
product_ids: number[]
|
||||
customer_ids: number[]
|
||||
profit_buy: number | string
|
||||
profit_sell: number | string
|
||||
profit_buy: number
|
||||
profit_sell: number
|
||||
show_product: boolean
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@ import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
|
||||
function formatSignedProfit(raw: unknown) {
|
||||
const n = Number(raw ?? 0)
|
||||
if (!Number.isFinite(n) || n === 0) {
|
||||
return usePrice(0, true, '')
|
||||
}
|
||||
const label = n < 0 ? 'تخفیف' : 'سود'
|
||||
return `${label} ${usePrice(Math.abs(n), true, '')}`
|
||||
}
|
||||
|
||||
export function getSpecialPricePackageColumns(): BaseTableColumn[] {
|
||||
return [
|
||||
{
|
||||
@@ -37,14 +46,14 @@ export function getSpecialPricePackageColumns(): BaseTableColumn[] {
|
||||
dataIndex: 'profit_buy',
|
||||
key: 'profit_buy',
|
||||
type: 'Text',
|
||||
customRender: ({ record }) => usePrice(record.profit_buy ?? 0, true, '')
|
||||
customRender: ({ record }) => formatSignedProfit(record.profit_buy)
|
||||
},
|
||||
{
|
||||
title: 'سود فروش',
|
||||
dataIndex: 'profit_sell',
|
||||
key: 'profit_sell',
|
||||
type: 'Text',
|
||||
customRender: ({ record }) => usePrice(record.profit_sell ?? 0, true, '')
|
||||
customRender: ({ record }) => formatSignedProfit(record.profit_sell)
|
||||
},
|
||||
{
|
||||
title: 'نمایش محصول',
|
||||
|
||||
@@ -88,8 +88,8 @@ async function openEdit(record: any) {
|
||||
category_names: data?.category_names ?? [],
|
||||
product_ids: (data?.product_ids ?? []).map(Number),
|
||||
customer_ids: (data?.customer_ids ?? []).map(Number),
|
||||
profit_buy: data?.package?.profit_buy ?? 0,
|
||||
profit_sell: data?.package?.profit_sell ?? 0,
|
||||
profit_buy: Number(data?.package?.profit_buy ?? 0),
|
||||
profit_sell: Number(data?.package?.profit_sell ?? 0),
|
||||
show_product: Boolean(data?.package?.show_product ?? true),
|
||||
is_active: Boolean(data?.package?.is_active ?? true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user