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
|
const allowDecimal = options.allowDecimal !== false
|
||||||
let s = persian2english(String(raw ?? '').trim())
|
let s = persian2english(String(raw ?? '').trim())
|
||||||
.replace(/[٬,\s]/g, '')
|
.replace(/[٬,\s]/g, '')
|
||||||
|
.replace(/[−–—]/g, '-')
|
||||||
.replace(/٫/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 = 'ریال') {
|
export function usePrice(price: number | string, digit = true, unit = 'ریال') {
|
||||||
const n = typeof price === 'number' ? price : parseNumericValue(price)
|
const n = typeof price === 'number' ? price : parseNumericValue(price)
|
||||||
let _price = Number.isFinite(n) ? String(n) : persian2english(String(price ?? ''))
|
const finite = Number.isFinite(n)
|
||||||
if (digit && 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, ',')
|
_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 {
|
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>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="grid gap-3 md:grid-cols-2">
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
<a-form-item label="سود فروش" name="profit_sell">
|
<a-form-item label="سود / تخفیف فروش" name="profit_sell">
|
||||||
<BasicInput v-model:value="form.profit_sell" type="price" addon-before="ریال" />
|
<SignedProfitInput v-model:value="form.profit_sell" kind="sell" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="سود خرید" name="profit_buy">
|
<a-form-item label="سود / تخفیف خرید" name="profit_buy">
|
||||||
<BasicInput v-model:value="form.profit_buy" type="price" addon-before="ریال" />
|
<SignedProfitInput v-model:value="form.profit_buy" kind="buy" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ import { debounce } from 'lodash'
|
|||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import BasicInput from '@/components/BasicInput.vue'
|
import BasicInput from '@/components/BasicInput.vue'
|
||||||
import BasicSelect from '@/components/BasicSelect.vue'
|
import BasicSelect from '@/components/BasicSelect.vue'
|
||||||
|
import SignedProfitInput from './SignedProfitInput.vue'
|
||||||
import { syncAndLoadCategories, type CategorySelectItem } from '@/utils/categorySelect'
|
import { syncAndLoadCategories, type CategorySelectItem } from '@/utils/categorySelect'
|
||||||
import {
|
import {
|
||||||
getProductsByCategories,
|
getProductsByCategories,
|
||||||
@@ -87,8 +88,8 @@ export type SpecialPriceForm = {
|
|||||||
category_names: string[]
|
category_names: string[]
|
||||||
product_ids: number[]
|
product_ids: number[]
|
||||||
customer_ids: number[]
|
customer_ids: number[]
|
||||||
profit_buy: number | string
|
profit_buy: number
|
||||||
profit_sell: number | string
|
profit_sell: number
|
||||||
show_product: boolean
|
show_product: boolean
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import type { BaseTableColumn } from '@core/type/baseTable'
|
|||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
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[] {
|
export function getSpecialPricePackageColumns(): BaseTableColumn[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -37,14 +46,14 @@ export function getSpecialPricePackageColumns(): BaseTableColumn[] {
|
|||||||
dataIndex: 'profit_buy',
|
dataIndex: 'profit_buy',
|
||||||
key: 'profit_buy',
|
key: 'profit_buy',
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
customRender: ({ record }) => usePrice(record.profit_buy ?? 0, true, '')
|
customRender: ({ record }) => formatSignedProfit(record.profit_buy)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'سود فروش',
|
title: 'سود فروش',
|
||||||
dataIndex: 'profit_sell',
|
dataIndex: 'profit_sell',
|
||||||
key: 'profit_sell',
|
key: 'profit_sell',
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
customRender: ({ record }) => usePrice(record.profit_sell ?? 0, true, '')
|
customRender: ({ record }) => formatSignedProfit(record.profit_sell)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'نمایش محصول',
|
title: 'نمایش محصول',
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ async function openEdit(record: any) {
|
|||||||
category_names: data?.category_names ?? [],
|
category_names: data?.category_names ?? [],
|
||||||
product_ids: (data?.product_ids ?? []).map(Number),
|
product_ids: (data?.product_ids ?? []).map(Number),
|
||||||
customer_ids: (data?.customer_ids ?? []).map(Number),
|
customer_ids: (data?.customer_ids ?? []).map(Number),
|
||||||
profit_buy: data?.package?.profit_buy ?? 0,
|
profit_buy: Number(data?.package?.profit_buy ?? 0),
|
||||||
profit_sell: data?.package?.profit_sell ?? 0,
|
profit_sell: Number(data?.package?.profit_sell ?? 0),
|
||||||
show_product: Boolean(data?.package?.show_product ?? true),
|
show_product: Boolean(data?.package?.show_product ?? true),
|
||||||
is_active: Boolean(data?.package?.is_active ?? true)
|
is_active: Boolean(data?.package?.is_active ?? true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user