feat: add product select to price alerts

Customer can pick a product when creating an alert and see it on the card.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 22:23:25 +03:30
co-authored by Cursor
parent e01192fb5e
commit e578ba521d
8 changed files with 66 additions and 71 deletions
+6
View File
@@ -13,15 +13,20 @@ declare module 'vue' {
ADivider: typeof import('ant-design-vue/es')['Divider']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
@@ -73,5 +78,6 @@ declare module 'vue' {
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanDialog: typeof import('vant/es')['Dialog']
VanField: typeof import('vant/es')['Field']
}
}
-41
View File
@@ -1,41 +0,0 @@
import {
fetchSuperAdminJson,
resolveTenantDomain,
SA_PROGRAM_SETTINGS_PATH,
} from '@/config/superadmin'
export type SuperAdminSettingRow = {
id?: number
key?: string
value?: string | number
title_fa?: string
description?: string
is_active?: boolean | number | string
type?: string
can_edit?: number | boolean | string
}
function parseSettingsPayload(data: unknown): SuperAdminSettingRow[] {
if (Array.isArray(data)) return data as SuperAdminSettingRow[]
if (data && typeof data === 'object') {
const obj = data as Record<string, unknown>
if (Array.isArray(obj.settings)) return obj.settings as SuperAdminSettingRow[]
if (Array.isArray(obj.items)) return obj.items as SuperAdminSettingRow[]
if (Array.isArray(obj.data)) return obj.data as SuperAdminSettingRow[]
}
return []
}
/** کلید قفل‌شده در get_settings محلی — باید از سوپرادمین بیاید */
export function isLockedCanEdit(canEdit: unknown): boolean {
return canEdit === 0 || canEdit === false || canEdit === '0'
}
/** تنظیمات پیشرفته نسخه از سوپرادمین — فقط GET /api/public/program-settings?domain= */
export async function fetchSuperAdminProgramSettings(): Promise<SuperAdminSettingRow[]> {
const domain = encodeURIComponent(resolveTenantDomain())
const data = await fetchSuperAdminJson(
(base) => `${base}${SA_PROGRAM_SETTINGS_PATH}?domain=${domain}`,
)
return parseSettingsPayload(data)
}
-3
View File
@@ -8,9 +8,6 @@ export const SUPERADMIN_BASES = [
/** هر API: سه دامنه × سه دور */
export const SA_RETRY_ROUNDS = 3
/** تنظیمات پیشرفته نسخه — همان دادهٔ /get_settings بدون نیاز به nginx جدا */
export const SA_PROGRAM_SETTINGS_PATH = '/api/public/program-settings'
function normalizeBase(url: string) {
return String(url || '').trim().replace(/\/$/, '')
}
+2
View File
@@ -1,5 +1,7 @@
export interface PriceTrigger {
id?: number
product_id?: number
product?: { id: number; title?: string }
target_price: string
price_condition: string
action: string
+9 -13
View File
@@ -1,7 +1,6 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { getSettings } from '@/api/configuration'
import { fetchSuperAdminProgramSettings, isLockedCanEdit } from '@/api/superadminSettings'
import { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
@@ -13,7 +12,6 @@ export interface AppSetting {
description?: string
is_active?: boolean
type?: string
can_edit?: number | boolean | string
}
export const useSettingsStore = defineStore('settings', () => {
@@ -67,19 +65,17 @@ export const useSettingsStore = defineStore('settings', () => {
fetchPromise = (async () => {
try {
const localRaw = await getSettings().catch(() => [])
const remote = await fetchSuperAdminProgramSettings()
const local = Array.isArray(localRaw) ? localRaw : []
const editable = local.filter((row) => !isLockedCanEdit(row?.can_edit))
settings.value = []
mergeSettings(remote as AppSetting[])
mergeSettings(editable)
loaded.value = true
return settings.value
const data = await getSettings()
if (Array.isArray(data)) {
mergeSettings(data)
}
} catch (error) {
fetchPromise = null
throw error
console.log(error)
} finally {
loaded.value = true
}
return settings.value
})()
return fetchPromise
@@ -3,6 +3,10 @@
<p class="dptr-card__price">{{ usePrice(item.target_price) }}</p>
<div class="dptr-card__meta">
<div class="dptr-card__row">
<span class="dptr-card__label">محصول:</span>
<span class="dptr-card__value">{{ productTitle }}</span>
</div>
<div class="dptr-card__row">
<span class="dptr-card__label">جهت قیمت:</span>
<span class="dptr-card__value">{{ returnPriceCondition(item.price_condition) }}</span>
@@ -76,6 +80,7 @@
</div>
</div>
<div class="flex flex-col gap-2 mt-2">
<BasicInfoItem is-line title="محصول" :value="productTitle" />
<BasicInfoItem is-line title="جهت قیمت" :value="returnPriceCondition(item.price_condition)" />
<BasicInfoItem is-line title="نوع هشدار" :value="returnAction(item.action)" />
</div>
@@ -108,7 +113,7 @@
<script lang="ts" setup>
import { usePrice } from '../../../../utils/usePrice'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { toJalali } from '../../../../utils/useDateTime'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import type { PriceTrigger } from '@/models/PriceTrigger'
@@ -132,6 +137,8 @@ const props = withDefaults(
const emits = defineEmits(['edit', 'refresh'])
const productTitle = computed(() => props.item.product?.title || 'طلا')
onMounted(() => {
isActive.value = props.item.is_active
})
@@ -7,6 +7,16 @@
@finish="ok()"
>
<div class="flex flex-col gap-4 w-full">
<a-form-item :rules="[required()]" label="محصول" name="product_id">
<BasicSelect
v-model:value="form.product_id"
:items="products"
key-name="id"
placeholder="آبشده، سکه، نقره و ..."
size="large"
value-name="title"
/>
</a-form-item>
<a-form-item :rules="[required()]" label="جهت قیمت" name="price_condition">
<BasicSelect
v-model:value="form.price_condition"
@@ -58,6 +68,7 @@ const {
loading,
radioStyle,
typeItems,
products,
ok,
} = usePriceTriggerPageContext()
</script>
@@ -2,12 +2,25 @@ import { inject, onMounted, provide, reactive, ref, type InjectionKey, type Ref
import { cloneDeep } from 'lodash'
import { type FormInstance, message } from 'ant-design-vue'
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
import { getProducts } from '@/api/products'
import type { SelectItem } from '@/models/SelectItem'
import type { PriceTrigger } from '@/models/PriceTrigger'
import type { Product } from '@/models/products'
import { loading } from '@/utils'
function emptyForm(): PriceTrigger {
return {
product_id: undefined,
price_condition: '',
is_active: true,
action: '',
target_price: '',
}
}
export interface PriceTriggerPageContext {
items: Ref<PriceTrigger[]>
products: Ref<Product[]>
addTriggerVisible: Ref<boolean>
editId: Ref<number | undefined>
form: Ref<PriceTrigger>
@@ -27,6 +40,7 @@ export const PRICE_TRIGGER_PAGE_CONTEXT_KEY: InjectionKey<PriceTriggerPageContex
export function usePriceTriggerPage() {
const items = ref<PriceTrigger[]>([])
const products = ref<Product[]>([])
const addTriggerVisible = ref(false)
const editId = ref<number | undefined>(undefined)
const formRef = ref<FormInstance>()
@@ -37,12 +51,7 @@ export function usePriceTriggerPage() {
lineHeight: '30px',
})
const form = ref<PriceTrigger>({
price_condition: '',
is_active: true,
action: '',
target_price: '',
})
const form = ref<PriceTrigger>(emptyForm())
const typeItems: SelectItem[] = [
{ value: 'بزرگتر', key: '>' },
@@ -51,12 +60,7 @@ export function usePriceTriggerPage() {
]
function resetFields() {
form.value = {
price_condition: '',
is_active: true,
action: '',
target_price: '',
}
form.value = emptyForm()
}
function openAdd() {
@@ -66,7 +70,10 @@ export function usePriceTriggerPage() {
}
function openEdit(val: PriceTrigger) {
form.value = cloneDeep(val)
form.value = cloneDeep({
...val,
product_id: val.product_id ?? val.product?.id,
})
editId.value = form.value.id
addTriggerVisible.value = true
}
@@ -82,6 +89,14 @@ export function usePriceTriggerPage() {
}
}
async function loadProducts() {
try {
products.value = ((await getProducts()) as Product[]) ?? []
} catch (e) {
console.log(e)
}
}
async function ok() {
await formRef.value?.validateFields().then(async () => {
try {
@@ -104,10 +119,12 @@ export function usePriceTriggerPage() {
onMounted(() => {
initItems()
loadProducts()
})
const context: PriceTriggerPageContext = {
items,
products,
addTriggerVisible,
editId,
form,