diff --git a/components.d.ts b/components.d.ts index 3d0dca9..4645ff3 100644 --- a/components.d.ts +++ b/components.d.ts @@ -20,6 +20,7 @@ declare module '@vue/runtime-core' { ACollapse: typeof import('ant-design-vue/es')['Collapse'] ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] + ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADivider: typeof import('ant-design-vue/es')['Divider'] AdminFieldHelp: typeof import('./src/components/AdminFieldHelp.vue')['default'] AdminHelpTooltipContent: typeof import('./src/components/AdminHelpTooltipContent.vue')['default'] diff --git a/src/api/conditionalOrders.ts b/src/api/conditionalOrders.ts new file mode 100644 index 0000000..39919dc --- /dev/null +++ b/src/api/conditionalOrders.ts @@ -0,0 +1,18 @@ +import type { TableInput } from '@core/type/baseTable' +import useAxios from '@core/utils/useAxios' +import type { ConditionalOrder } from '@/model/conditionalOrders' + +export const getConditionalOrdersTable = async (params: TableInput) => + await useAxios.post>('/admin/conditional_orders/table', params) + +export const createConditionalOrder = async (payload: Partial) => + await useAxios.post('/admin/conditional_orders', payload) + +export const updateConditionalOrder = async (id: number, payload: Partial) => + await useAxios.put(`/admin/conditional_orders/${id}`, payload) + +export const toggleConditionalOrderActive = async (id: number) => + await useAxios.put<{ is_active: boolean }>(`/admin/conditional_orders/${id}/toggle`) + +export const deleteConditionalOrder = async (id: number) => + await useAxios.delete(`/admin/conditional_orders/${id}`) diff --git a/src/api/customer.ts b/src/api/customer.ts index 8e63f2f..5fa9ef4 100644 --- a/src/api/customer.ts +++ b/src/api/customer.ts @@ -25,6 +25,17 @@ export const changeCustomerActivation = async (id: number) => export const changeCustomerOrderActivation = async (id: number) => await useAxios.get(`/admin/customer/trade/${id}`) +export const changeCustomerConditionalOrderActive = async (id: number) => + await useAxios.get(`/admin/customer/conditional_order/${id}`) + +export const updateCustomerConditionalOrderLimit = async ( + id: number, + conditional_order_limit: number | null +) => + await useAxios.put(`/admin/customer/conditional_order_limit/${id}`, { + conditional_order_limit, + }) + export const changeCustomerArchive = async (id: number) => await useAxios.get(`/admin/customer/archive/${id}`) diff --git a/src/api/pargar.ts b/src/api/pargar.ts index 4875f4b..fcbb5e7 100644 --- a/src/api/pargar.ts +++ b/src/api/pargar.ts @@ -39,7 +39,13 @@ export interface PargarSyncWalletResult { ok: boolean message: string result?: { - documents: { applied: number; skipped: number; errors: string[]; logs: number[] } + documents: { + applied: number + skipped: number + batches?: number + acknowledged?: number + errors: string[] + } mande: { applied: boolean; gold_delta?: number; rial_delta?: number; message: string } } } diff --git a/src/components/customer/CustomerWalletBalancePanel.vue b/src/components/customer/CustomerWalletBalancePanel.vue index 73c13cb..7a17f42 100644 --- a/src/components/customer/CustomerWalletBalancePanel.vue +++ b/src/components/customer/CustomerWalletBalancePanel.vue @@ -80,7 +80,7 @@
ریالی - {{ usePrice(summary.total_rial_value ?? 0, true, '') }} + {{ usePrice(summary.cash_wallet ?? 0, true, '') }}
diff --git a/src/model/conditionalOrders.ts b/src/model/conditionalOrders.ts new file mode 100644 index 0000000..73c92a4 --- /dev/null +++ b/src/model/conditionalOrders.ts @@ -0,0 +1,23 @@ +export interface ConditionalOrder { + id: number + customer_id: number + product_id: number + direction: 1 | 2 + price_condition: 'gte' | 'lte' + trigger_price: number + amount?: number | null + number?: number | null + expires_at?: string | null + description?: string | null + is_active: boolean + status: 'pending' | 'executed' | 'failed' | 'expired' + fail_reason?: string | null + order_id?: number | null + fee_charged?: number + triggered_price?: number | null + executed_at?: string | null + created_at?: string + updated_at?: string + customer?: { id: number; name?: string; mobile?: string } + product?: { id: number; title?: string } +} diff --git a/src/model/customer.ts b/src/model/customer.ts index c61f9d5..427eb06 100644 --- a/src/model/customer.ts +++ b/src/model/customer.ts @@ -34,6 +34,8 @@ export interface Customer { profit_amount: number add_user_limit: number trade_active: boolean + conditional_order_active?: boolean + conditional_order_limit?: number | null type?: number type_str: string gold: number diff --git a/src/model/transaction.ts b/src/model/transaction.ts index c7eaedf..b77d651 100644 --- a/src/model/transaction.ts +++ b/src/model/transaction.ts @@ -17,6 +17,9 @@ export interface Transaction { metal_gold?: Record transaction_metal_id?: number transaction_metal_label?: string + product_id?: number | null + product_title?: string | null + product?: { id?: number; title?: string | null; group_type?: string | null; metal_id?: number | null } | null order_id: number transaction_type: string type: number diff --git a/src/pages/menuPages/conditionalOrders/components/ConditionalOrderEditorFormBody.vue b/src/pages/menuPages/conditionalOrders/components/ConditionalOrderEditorFormBody.vue new file mode 100644 index 0000000..3b2ccea --- /dev/null +++ b/src/pages/menuPages/conditionalOrders/components/ConditionalOrderEditorFormBody.vue @@ -0,0 +1,97 @@ + + + diff --git a/src/pages/menuPages/conditionalOrders/components/table.vue b/src/pages/menuPages/conditionalOrders/components/table.vue new file mode 100644 index 0000000..ce102ec --- /dev/null +++ b/src/pages/menuPages/conditionalOrders/components/table.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/pages/menuPages/conditionalOrders/hooks/conditionalOrdersColumns.tsx b/src/pages/menuPages/conditionalOrders/hooks/conditionalOrdersColumns.tsx new file mode 100644 index 0000000..0d89ed9 --- /dev/null +++ b/src/pages/menuPages/conditionalOrders/hooks/conditionalOrdersColumns.tsx @@ -0,0 +1,122 @@ +import type { BaseTableColumn } from '@core/type/baseTable' +import { toJalali } from '@core/utils/useDataTime' +import CustomTag from '@/components/CustomTag.vue' + +export const cols: BaseTableColumn[] = [ + { + title: 'ردیف', + dataIndex: 'id', + key: 'id', + customRender: (record: any) => record.index + 1, + }, + { + title: 'مشتری', + dataIndex: 'customer', + key: 'customer', + type: 'Text', + customRender: ({ record }) => { + const name = record.customer?.name?.trim() + return name ? `${name} (${record.customer_id})` : String(record.customer_id ?? '-') + }, + }, + { + title: 'محصول', + dataIndex: 'product', + key: 'product', + type: 'Text', + customRender: ({ record }) => record.product?.title || record.product_id || '-', + }, + { + title: 'جهت', + dataIndex: 'direction', + key: 'direction', + customRender: ({ record }) => + Number(record.direction) === 1 ? ( + + خرید + + ) : ( + + فروش + + ), + }, + { + title: 'شرط', + dataIndex: 'price_condition', + key: 'price_condition', + customRender: ({ record }) => + record.price_condition === 'gte' ? ( + یا بالاتر + ) : ( + یا پایین‌تر + ), + }, + { + title: 'مظنه تریگر', + dataIndex: 'trigger_price', + key: 'trigger_price', + type: 'Number', + }, + { + title: 'مقدار', + dataIndex: 'amount', + key: 'amount', + customRender: ({ record }) => { + if (record.number != null && Number(record.number) > 0) return `${record.number} عدد` + if (record.amount != null && Number(record.amount) > 0) return `${record.amount} گرم` + return '-' + }, + }, + { + title: 'وضعیت اجرا', + dataIndex: 'status', + key: 'status', + customRender: ({ record }) => { + const map: Record = { + pending: { label: 'در انتظار', text: '#1E40AF', bg: '#DBEAFE' }, + executed: { label: 'انجام شد', text: '#25855A', bg: '#E3F4E9' }, + failed: { label: 'ناموفق', text: '#C53030', bg: '#FECACA' }, + expired: { label: 'منقضی', text: '#6B7280', bg: '#F3F4F6' }, + } + const s = map[record.status] || map.pending + return ( +
+ + {s.label} + +
+ ) + }, + }, + { + title: 'اعتبار تا', + dataIndex: 'expires_at', + key: 'expires_at', + customRender: ({ record }) => (record.expires_at ? toJalali(record.expires_at) : '—'), + }, + { + title: 'فعال', + dataIndex: 'is_active', + key: 'is_active', + type: 'Boolean', + customRender: ({ record }) => + record.is_active ? ( + + فعال + + ) : ( + + غیرفعال + + ), + }, + { + title: 'عملیات', + dataIndex: 'row_action', + key: 'row_action', + slot: 'action', + fixed: 'right', + filterable: false, + }, +] diff --git a/src/pages/menuPages/conditionalOrders/index.vue b/src/pages/menuPages/conditionalOrders/index.vue new file mode 100644 index 0000000..07120ff --- /dev/null +++ b/src/pages/menuPages/conditionalOrders/index.vue @@ -0,0 +1,194 @@ + + + diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index efbd644..e3f7e9e 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -119,7 +119,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [ title: 'عمومی و برند', icon: 'vuesax-linear:home-2', match: (k) => - /^(name_fa|name_en|app_url|admin_url|apk_download_link|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|customer_direct_sms|has_page_earn_money|sms_panel|main_wallet_products)/.test( + /^(name_fa|name_en|app_url|admin_url|apk_download_link|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|customer_direct_sms|has_page_earn_money|sms_panel|main_wallet_products|enable_conditional_order)/.test( k ) }, diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index 487e7cf..4e3cf39 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -236,6 +236,8 @@ const KEY_TYPE_FALLBACK: Record = { buy_fee: 'price', sell_fee: 'price', fee_free: 'number', + fee_conditional_order: 'price', + enable_conditional_order: 'switch', min_withdraw: 'price', min_deposit: 'price', min_first_buy: 'price', @@ -555,6 +557,10 @@ const KEY_HELP_FALLBACK: Record = { 'روشن: اگر مانده طلای کیف اصلی صفر یا زیر صفر باشد (بدهی)، مشتری ابتدا باید آبشده/مانده کیف اصلی را صفر کند و بعد می‌تواند محصول دیگر بخرد. خاموش: بدون این محدودیت. معامله خودِ محصولات کیف اصلی همیشه مجاز است.', fee_free: 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. صفر یعنی غیرفعال.', + fee_conditional_order: + 'مبلغ ریالی که با هر بار ثبت تریگر سفارش شرطی از کیف پول ریالی مشتری کم می‌شود. صفر = رایگان.', + enable_conditional_order: + 'روشن: قابلیت سفارش شرطی سراسری فعال است. خاموش: منو و ارزیابی اجرا نمی‌شود. فقط از صفحه دیپلوی قابل تغییر است.', min_withdraw: 'حداقل مبلغ درخواست برداشت (ریال). درخواست‌های کمتر پذیرفته نمی‌شوند.', min_deposit: @@ -783,6 +789,8 @@ export const KEY_LABEL_FALLBACK: Record = { fee_deduct_from_gold: 'اعمال مبلغ دقیق در معامله مشتری', require_zero_main_gold_before_other_buy: 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر', fee_free: 'معافیت کارمزد سفارش اول', + fee_conditional_order: 'کارمزد سفارش شرطی', + enable_conditional_order: 'سفارش شرطی', min_withdraw: 'حداقل مبلغ برداشت', min_deposit: 'حداقل مبلغ واریز کیف پول', min_first_buy: 'حداقل اولین خرید', diff --git a/src/pages/menuPages/customers/base/components/PargarWalletSyncAction.vue b/src/pages/menuPages/customers/base/components/PargarWalletSyncAction.vue index 5f407b1..dbb63e1 100644 --- a/src/pages/menuPages/customers/base/components/PargarWalletSyncAction.vue +++ b/src/pages/menuPages/customers/base/components/PargarWalletSyncAction.vue @@ -7,11 +7,11 @@

همگام‌سازی با پرگار

- {{ - canSync - ? 'اول اسنادِ اعمال‌نشده، بعد هم‌ترازی مانده — خودکار از پرگار کیف عوض نمی‌شود' - : 'ابتدا کد حسابداری پرگار را برای این مشتری ثبت کنید' - }} + {{ + canSync + ? 'موجودی را با ماندهٔ پرگار یکی می‌کند — تاریخچهٔ اسناد روی کیف replay نمی‌شود' + : 'ابتدا کد حسابداری پرگار را برای این مشتری ثبت کنید' + }}

@@ -21,10 +21,10 @@ size="small" class="pargar-wallet-sync__btn" :loading="loading" - :disabled="!canSync" + :disabled="!canSync || loading" @click="$emit('sync')" > - همگام‌سازی مانده و اسناد + همگام‌سازی مانده از پرگار diff --git a/src/pages/menuPages/customers/base/components/table.vue b/src/pages/menuPages/customers/base/components/table.vue index b301a4b..4bda007 100644 --- a/src/pages/menuPages/customers/base/components/table.vue +++ b/src/pages/menuPages/customers/base/components/table.vue @@ -113,6 +113,47 @@ :color="record.trade_active === 1 ? '#54AD06' : '#ad0627'" /> + + + + + + + + { + const raw = window.prompt( + 'سقف تعداد تریگر pending (خالی = نامحدود):', + current != null ? String(current) : '' + ) + if (raw === null) return + const trimmed = raw.trim() + const limit = trimmed === '' ? null : Number(trimmed) + if (limit !== null && (!Number.isFinite(limit) || limit < 1)) { + message.error('عدد نامعتبر است') + return Promise.reject() + } + await updateCustomerConditionalOrderLimit(customer.id, limit) + await fetch() + message.success('سقف سفارش شرطی ذخیره شد') + }, + }) +}