feat(admin): let price alerts pick a product

Show the product on the form and table so each alert is tied to one item.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 22:23:22 +03:30
co-authored by Cursor
parent 14c9843abb
commit c332e9df24
6 changed files with 38 additions and 98 deletions
-82
View File
@@ -1,82 +0,0 @@
export type SuperAdminSettingRow = {
id?: number
key?: string
value?: string | number | null
title_fa?: string
description?: string
is_active?: boolean | number | string
type?: string
can_edit?: number | boolean | string
}
const SUPERADMIN_BASES = [
'https://adm.montzar.ir',
'https://adm.montech.ir',
'https://adm.montest.ir',
] as const
const SA_RETRY_ROUNDS = 3
const SA_PROGRAM_SETTINGS_PATH = '/api/public/program-settings'
function resolveTenantDomain() {
const host = String(window.location.hostname || '').trim()
if (!host || host === 'localhost' || host === '127.0.0.1') {
return 'k.montzar.ir'
}
return host
}
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'
}
async function fetchFromUrl(url: string, timeout = 5000): Promise<SuperAdminSettingRow[]> {
const controller = new AbortController()
const timer = window.setTimeout(() => controller.abort(), timeout)
try {
const res = await fetch(url, {
method: 'GET',
cache: 'no-store',
signal: controller.signal,
headers: { Accept: 'application/json' },
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const contentType = res.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
throw new Error('SuperAdmin response is not JSON')
}
return parseSettingsPayload(await res.json())
} finally {
window.clearTimeout(timer)
}
}
/** تنظیمات پیشرفته نسخه از سوپرادمین — فقط GET /api/public/program-settings?domain= */
export async function fetchSuperAdminProgramSettings(): Promise<SuperAdminSettingRow[]> {
const domain = encodeURIComponent(resolveTenantDomain())
let lastError: unknown
for (let round = 1; round <= SA_RETRY_ROUNDS; round++) {
for (const base of SUPERADMIN_BASES) {
try {
return await fetchFromUrl(`${base}${SA_PROGRAM_SETTINGS_PATH}?domain=${domain}`)
} catch (error) {
lastError = error
}
}
}
throw lastError instanceof Error ? lastError : new Error('SuperAdmin program-settings unreachable')
}
+2
View File
@@ -1,6 +1,8 @@
export interface PriceTriggers{
id?: number
customer_id: number
product_id?: number
product?: { id: number; title?: string }
target_price: number
price_condition: string
action: string
@@ -11,6 +11,16 @@
/>
</a-form-item>
<a-form-item label="محصول" name="product_id" :rules="inputRules()">
<BasicSelect
v-model:value="form.product_id"
:items="productItems"
key-name="id"
value-name="label"
placeholder="آبشده، سکه، نقره و ..."
/>
</a-form-item>
<a-form-item label="شرط قیمت" name="price_condition" :rules="inputRules()">
<BasicSelect
v-model:value="form.price_condition"
@@ -48,6 +58,7 @@ import { inputRules, inputRulesNumber } from '@/utils/inputRules'
defineProps<{
form: Partial<PriceTriggers>
customerItems: Array<{ id: number; label: string }>
productItems: Array<{ id: number; label: string }>
editId?: number
}>()
@@ -39,6 +39,15 @@ export const cols: BaseTableColumn[] = [
return name ? `${name} (${record.customer_id})` : String(record.customer_id ?? '-')
},
},
{
title: 'محصول',
dataIndex: 'product',
key: 'product',
type: 'Text',
customRender: ({ record }) => {
return record.product?.title || (record.product_id ? `محصول ${record.product_id}` : 'طلا')
},
},
{
title: 'کد مشتری',
dataIndex: 'customer_id',
@@ -22,6 +22,7 @@
ref="editorRef"
:form="form"
:customer-items="customerItems"
:product-items="productItems"
:edit-id="editId"
/>
</BasicActionSheet>
@@ -38,6 +39,7 @@ import PriceTriggerEditorFormBody from './components/PriceTriggerEditorFormBody.
import type { PriceTriggers } from '@/model/priceTriggers'
import type { Customer } from '@/model/customer'
import { getCustomersList } from '@/api/customer'
import { getCustomersList as getProductsList } from '@/api/product'
import {
createPriceTrigger,
deletePriceTrigger,
@@ -57,9 +59,11 @@ const loading = ref(false)
const editorVisible = ref(false)
const editId = ref<number | undefined>()
const customers = ref<Customer[]>([])
const products = ref<Array<{ id: number; title?: string }>>([])
const defaultForm = (): Partial<PriceTriggers> => ({
customer_id: undefined,
product_id: undefined,
target_price: undefined,
price_condition: '>',
action: 'sms',
@@ -77,6 +81,13 @@ const customerItems = computed(() =>
}))
)
const productItems = computed(() =>
products.value.map((p) => ({
id: p.id,
label: p.title || String(p.id),
}))
)
function refreshTable() {
tableRef.value?.fetch()
}
@@ -96,6 +107,7 @@ function openEdit(record: PriceTriggers) {
editId.value = record.id
form.value = cloneDeep({
customer_id: record.customer_id,
product_id: record.product_id ?? record.product?.id,
target_price: record.target_price,
price_condition: record.price_condition,
action: record.action,
@@ -111,6 +123,7 @@ async function submitForm() {
const payload = {
customer_id: Number(form.value.customer_id),
product_id: Number(form.value.product_id),
target_price: Number(form.value.target_price),
price_condition: form.value.price_condition,
action: form.value.action,
@@ -156,6 +169,7 @@ function deleteItem(id: number) {
onMounted(async () => {
try {
customers.value = (await getCustomersList()) ?? []
products.value = ((await getProductsList()) as Array<{ id: number; title?: string }>) ?? []
} catch (e) {
console.error(e)
}
+2 -16
View File
@@ -332,23 +332,9 @@ async function bootstrapSettingsFromApi(): Promise<void> {
try {
const { getConfiguration } = await import('@/api/configuration')
const { fetchSuperAdminProgramSettings, isLockedCanEdit } = await import('@/api/superadminSettings')
const { parseMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
const localRows = await getConfiguration().catch(() => [])
const saRows = await fetchSuperAdminProgramSettings()
const local = Array.isArray(localRows) ? localRows : []
const editable = local.filter((row) => !isLockedCanEdit(row?.can_edit))
const merged = [...saRows] as typeof local
for (const item of editable) {
if (!item?.key) continue
const index = merged.findIndex((row) => row.key === item.key)
if (index >= 0) {
merged[index] = { ...merged[index], ...item }
} else {
merged.push(item)
}
}
const list = merged
const rows = await getConfiguration()
const list = Array.isArray(rows) ? rows : []
const isStoreRow = list.find((item) => item.key === 'is_store')
const mainWalletRow = list.find((item) => item.key === 'main_wallet_products')
const mainMetalRow = list.find((item) => item.key === 'main_metal')