Add customers gold credit menu with request and active tabs.
Gate the submenu on enable_gold_credit and wire settings UI for SuperAdmin and tenant credit options. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
import useAxios from '@core/utils/useAxios'
|
||||||
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
|
|
||||||
|
export interface GoldCreditForm {
|
||||||
|
customer_id?: number
|
||||||
|
gold_amount?: number
|
||||||
|
months?: number
|
||||||
|
interest_type?: 'none' | 'percent' | 'fixed_gram'
|
||||||
|
interest_value?: number
|
||||||
|
delivery_method?: 'auto' | 'manual' | 'in_person'
|
||||||
|
notes?: string
|
||||||
|
has_check?: boolean
|
||||||
|
has_contract?: boolean
|
||||||
|
check_note?: string
|
||||||
|
contract_note?: string
|
||||||
|
penalty_type?: 'rial' | 'gold'
|
||||||
|
penalty_amount?: number
|
||||||
|
grace_hours?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoldCreditInstallment {
|
||||||
|
id: number
|
||||||
|
installment_number: number
|
||||||
|
due_date: string
|
||||||
|
amount_soot: number
|
||||||
|
principal_gold: number
|
||||||
|
interest_gold: number
|
||||||
|
total_gold: number
|
||||||
|
paid_gold: number
|
||||||
|
paid_rial: number
|
||||||
|
status: 'pending' | 'paid' | 'partial' | 'overdue' | 'prepaid'
|
||||||
|
paid_at?: string
|
||||||
|
penalty_days?: number
|
||||||
|
penalty_rial?: number
|
||||||
|
penalty_gold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoldCredit {
|
||||||
|
id: number
|
||||||
|
customer_id: number
|
||||||
|
gold_amount: number
|
||||||
|
months: number
|
||||||
|
interest_type: 'none' | 'percent' | 'fixed_gram'
|
||||||
|
interest_value: number
|
||||||
|
delivery_method: 'auto' | 'manual' | 'in_person'
|
||||||
|
status: string
|
||||||
|
source: string
|
||||||
|
total_interest_gold: number
|
||||||
|
total_payable_gold: number
|
||||||
|
paid_gold: number
|
||||||
|
paid_rial: number
|
||||||
|
notes?: string
|
||||||
|
has_check?: boolean
|
||||||
|
has_contract?: boolean
|
||||||
|
check_note?: string
|
||||||
|
contract_note?: string
|
||||||
|
penalty_type?: 'rial' | 'gold'
|
||||||
|
penalty_amount?: number
|
||||||
|
grace_hours?: number
|
||||||
|
customer?: { id: number; name: string; mobile: string }
|
||||||
|
installments?: GoldCreditInstallment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoldCreditRequest {
|
||||||
|
id: number
|
||||||
|
customer_id: number
|
||||||
|
requested_gold: number
|
||||||
|
requested_months?: number
|
||||||
|
note?: string
|
||||||
|
status: 'pending' | 'approved' | 'rejected'
|
||||||
|
admin_note?: string
|
||||||
|
customer?: { id: number; name: string; mobile: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createGoldCredit = async (data: GoldCreditForm) =>
|
||||||
|
await useAxios.post<GoldCredit>('/admin/gold_credit', data)
|
||||||
|
|
||||||
|
export const updateGoldCredit = async (id: number, data: Partial<GoldCreditForm> & { status?: string }) =>
|
||||||
|
await useAxios.put<GoldCredit>(`/admin/gold_credit/${id}`, data)
|
||||||
|
|
||||||
|
export const previewGoldCredit = async (data: GoldCreditForm) =>
|
||||||
|
await useAxios.post<{ installments: GoldCreditInstallment[]; total_payable_gold: number }>(
|
||||||
|
'/admin/gold_credit/preview',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getGoldCreditTable = async (params: TableInput) =>
|
||||||
|
await useAxios.post('/admin/gold_credit/table', params)
|
||||||
|
|
||||||
|
export const getGoldCreditRequestsTable = async (params: TableInput) =>
|
||||||
|
await useAxios.post('/admin/gold_credit/requests/table', params)
|
||||||
|
|
||||||
|
export const getCustomerGoldCreditInstallments = async (customerId: number) =>
|
||||||
|
await useAxios.post<{ rows: GoldCreditInstallment[]; credit: GoldCredit | null }>(
|
||||||
|
`/admin/customers/${customerId}/gold_credit/installments`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const approveGoldCreditRequest = async (id: number, data: GoldCreditForm) =>
|
||||||
|
await useAxios.post<GoldCredit>(`/admin/gold_credit/requests/${id}/approve`, data)
|
||||||
|
|
||||||
|
export const rejectGoldCreditRequest = async (id: number, adminNote?: string) =>
|
||||||
|
await useAxios.post(`/admin/gold_credit/requests/${id}/reject`, { admin_note: adminNote })
|
||||||
|
|
||||||
|
export const payGoldCreditInstallments = async (data: {
|
||||||
|
gold_credit_id: number
|
||||||
|
installment_ids: number[]
|
||||||
|
pay_method: 'gold' | 'rial' | 'mixed'
|
||||||
|
gold_amount?: number
|
||||||
|
rial_amount?: number
|
||||||
|
}) => await useAxios.post('/admin/gold_credit/installments/pay', data)
|
||||||
@@ -209,7 +209,7 @@ import SettingValueField from './SettingValueField.vue'
|
|||||||
import SettingCard from './SettingCard.vue'
|
import SettingCard from './SettingCard.vue'
|
||||||
import { refreshAppModeMenu } from '@/router'
|
import { refreshAppModeMenu } from '@/router'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings } from '@/utils/config'
|
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyGoldCreditEnabledFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings, setGoldCreditEnabledFromSettings } from '@/utils/config'
|
||||||
import {
|
import {
|
||||||
EDITABLE_SETTING_GROUPS,
|
EDITABLE_SETTING_GROUPS,
|
||||||
SYSTEM_SETTING_GROUPS,
|
SYSTEM_SETTING_GROUPS,
|
||||||
@@ -498,13 +498,12 @@ async function pullSuperAdminAndApplyFlags(): Promise<{
|
|||||||
ok: boolean
|
ok: boolean
|
||||||
message?: string
|
message?: string
|
||||||
count?: number
|
count?: number
|
||||||
|
meta?: { channel?: string; origin?: string; domain?: string }
|
||||||
|
items?: Record<string, string>
|
||||||
}> {
|
}> {
|
||||||
invalidateSettingsBootstrap()
|
invalidateSettingsBootstrap()
|
||||||
const res = await refreshSuperAdminSettings()
|
const res = await refreshSuperAdminSettings()
|
||||||
if (res?.items && typeof res.items === 'object') {
|
// get_settings کامل — فلگهای غیرسوپرادمین و قیمت
|
||||||
applySuperAdminAccessFlagsFromItems(res.items)
|
|
||||||
}
|
|
||||||
// get_settings کامل — فلگ منو و کارتها با مقدار زنده همتراز شوند
|
|
||||||
try {
|
try {
|
||||||
const { getConfiguration } = await import('@/api/configuration')
|
const { getConfiguration } = await import('@/api/configuration')
|
||||||
const all = await getConfiguration()
|
const all = await getConfiguration()
|
||||||
@@ -513,23 +512,50 @@ async function pullSuperAdminAndApplyFlags(): Promise<{
|
|||||||
applyPriceProviderFromRows(rows)
|
applyPriceProviderFromRows(rows)
|
||||||
applyPriceChannelAccessFromRows(rows)
|
applyPriceChannelAccessFromRows(rows)
|
||||||
} catch {
|
} catch {
|
||||||
/* فلگ از items میماند */
|
/* در ادامه از items همگام استفاده میشود */
|
||||||
|
}
|
||||||
|
// items پاسخ refresh منبع حقیقت است — آخر اعمال شود تا get_settings کهنه/دامنهٔ اشتباه فلگ را خاموش نکند
|
||||||
|
if (res?.items && typeof res.items === 'object') {
|
||||||
|
applySuperAdminAccessFlagsFromItems(res.items)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: res?.ok !== false,
|
ok: res?.ok !== false,
|
||||||
message: res?.message,
|
message: res?.message,
|
||||||
count: res?.count
|
count: res?.count,
|
||||||
|
meta: res?.meta,
|
||||||
|
items: res?.items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSuccessMessage(res: {
|
||||||
|
message?: string
|
||||||
|
count?: number
|
||||||
|
meta?: { channel?: string; origin?: string; domain?: string }
|
||||||
|
items?: Record<string, string>
|
||||||
|
}): string {
|
||||||
|
const base = res?.message || `همگام شد (${res?.count ?? 0} کلید دسترسی از سوپرادمین)`
|
||||||
|
const domain = String(res?.meta?.domain || '').trim()
|
||||||
|
const csp = res?.items && typeof res.items === 'object' && !Array.isArray(res.items)
|
||||||
|
? String((res.items as Record<string, string>).customer_special_pricing ?? '')
|
||||||
|
: ''
|
||||||
|
const bits = [base]
|
||||||
|
if (domain) bits.push(`دامنه: ${domain}`)
|
||||||
|
if (csp !== '') bits.push(`قیمت ویژه: ${csp}`)
|
||||||
|
return bits.join(' — ')
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshFromSuperAdmin() {
|
async function refreshFromSuperAdmin() {
|
||||||
if (refreshingSuperAdmin.value) return
|
if (refreshingSuperAdmin.value) return
|
||||||
refreshingSuperAdmin.value = true
|
refreshingSuperAdmin.value = true
|
||||||
try {
|
try {
|
||||||
const res = await pullSuperAdminAndApplyFlags()
|
const res = await pullSuperAdminAndApplyFlags()
|
||||||
await initConfig()
|
await initConfig()
|
||||||
|
// دوباره items را بعد از initConfig (که ممکن است get_settings ناقص بخواند) اعمال کن
|
||||||
|
if (res?.items && typeof res.items === 'object') {
|
||||||
|
applySuperAdminAccessFlagsFromItems(res.items)
|
||||||
|
}
|
||||||
refreshMenuAfterStoreChange()
|
refreshMenuAfterStoreChange()
|
||||||
message.success(res?.message || `همگام شد (${res?.count ?? 0} کلید دسترسی از سوپرادمین)`)
|
message.success(syncSuccessMessage(res))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||||||
@@ -546,8 +572,11 @@ async function onRefreshClick() {
|
|||||||
try {
|
try {
|
||||||
const res = await pullSuperAdminAndApplyFlags()
|
const res = await pullSuperAdminAndApplyFlags()
|
||||||
await initConfig()
|
await initConfig()
|
||||||
|
if (res?.items && typeof res.items === 'object') {
|
||||||
|
applySuperAdminAccessFlagsFromItems(res.items)
|
||||||
|
}
|
||||||
refreshMenuAfterStoreChange()
|
refreshMenuAfterStoreChange()
|
||||||
message.success(res?.message || 'بروزرسانی شد')
|
message.success(syncSuccessMessage(res))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||||||
@@ -580,6 +609,7 @@ async function initConfig() {
|
|||||||
applySupportChatMenuEnabledFromRows(baseData.value)
|
applySupportChatMenuEnabledFromRows(baseData.value)
|
||||||
applyGroupSmsFromRows(baseData.value)
|
applyGroupSmsFromRows(baseData.value)
|
||||||
applyCustomerSpecialPricingFromRows(baseData.value)
|
applyCustomerSpecialPricingFromRows(baseData.value)
|
||||||
|
applyGoldCreditEnabledFromRows(baseData.value)
|
||||||
// صفحهٔ عادی price_provider ندارد (can_edit=0) — همیشه از get_settings کامل بخوان
|
// صفحهٔ عادی price_provider ندارد (can_edit=0) — همیشه از get_settings کامل بخوان
|
||||||
if (
|
if (
|
||||||
!isSystemMode.value ||
|
!isSystemMode.value ||
|
||||||
@@ -587,7 +617,8 @@ async function initConfig() {
|
|||||||
!baseData.value.some((row) => row.key === 'show_to_users_chat') ||
|
!baseData.value.some((row) => row.key === 'show_to_users_chat') ||
|
||||||
!baseData.value.some((row) => row.key === 'price_channel_access') ||
|
!baseData.value.some((row) => row.key === 'price_channel_access') ||
|
||||||
!baseData.value.some((row) => row.key === 'group_sms') ||
|
!baseData.value.some((row) => row.key === 'group_sms') ||
|
||||||
!baseData.value.some((row) => row.key === 'customer_special_pricing')
|
!baseData.value.some((row) => row.key === 'customer_special_pricing') ||
|
||||||
|
!baseData.value.some((row) => row.key === 'enable_gold_credit')
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { getConfiguration } = await import('@/api/configuration')
|
const { getConfiguration } = await import('@/api/configuration')
|
||||||
@@ -598,6 +629,7 @@ async function initConfig() {
|
|||||||
applyPriceChannelAccessFromRows(rows)
|
applyPriceChannelAccessFromRows(rows)
|
||||||
applyGroupSmsFromRows(rows)
|
applyGroupSmsFromRows(rows)
|
||||||
applyCustomerSpecialPricingFromRows(rows)
|
applyCustomerSpecialPricingFromRows(rows)
|
||||||
|
applyGoldCreditEnabledFromRows(rows)
|
||||||
} catch {
|
} catch {
|
||||||
/* کش قبلی میماند */
|
/* کش قبلی میماند */
|
||||||
}
|
}
|
||||||
@@ -677,6 +709,9 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
|
|||||||
if (key === 'customer_special_pricing') {
|
if (key === 'customer_special_pricing') {
|
||||||
setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(value))
|
setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(value))
|
||||||
}
|
}
|
||||||
|
if (key === 'enable_gold_credit') {
|
||||||
|
setGoldCreditEnabledFromSettings(isSwitchOn(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onValueChange(row: Const, value: string) {
|
function onValueChange(row: Const, value: string) {
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
icon: 'vuesax-linear:lock',
|
icon: 'vuesax-linear:lock',
|
||||||
match: (k) => /^(min_|minimum_|balance_report_exclude)/.test(k)
|
match: (k) => /^(min_|minimum_|balance_report_exclude)/.test(k)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'gold_credit',
|
||||||
|
title: 'اعتبار طلایی',
|
||||||
|
icon: 'vuesax-linear:medal-star',
|
||||||
|
match: (k) => k.startsWith('gold_credit_')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'price_source',
|
id: 'price_source',
|
||||||
title: 'منبع قیمت و تابلو',
|
title: 'منبع قیمت و تابلو',
|
||||||
@@ -152,7 +158,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
icon: 'vuesax-linear:people',
|
icon: 'vuesax-linear:people',
|
||||||
// فقط فلگهای مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادیاند
|
// فقط فلگهای مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادیاند
|
||||||
match: (k) =>
|
match: (k) =>
|
||||||
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|permision__enabled|show_to_users_chat)$/.test(
|
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|enable_gold_credit|permision__enabled|show_to_users_chat)$/.test(
|
||||||
k
|
k
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export function isCodeManagedSettingKey(key?: string | null): boolean {
|
|||||||
export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([
|
export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([
|
||||||
'customer_direct_sms',
|
'customer_direct_sms',
|
||||||
'customer_special_pricing',
|
'customer_special_pricing',
|
||||||
|
'enable_gold_credit',
|
||||||
'enable_automation',
|
'enable_automation',
|
||||||
'enable_conditional_order',
|
'enable_conditional_order',
|
||||||
'enable_customer_level_profit',
|
'enable_customer_level_profit',
|
||||||
@@ -366,6 +367,22 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
|||||||
sms_notif_support_admin: 'switch',
|
sms_notif_support_admin: 'switch',
|
||||||
group_sms: 'switch',
|
group_sms: 'switch',
|
||||||
customer_special_pricing: 'switch',
|
customer_special_pricing: 'switch',
|
||||||
|
enable_gold_credit: 'switch',
|
||||||
|
gold_credit_overdue_sms_enabled: 'switch',
|
||||||
|
gold_credit_admin_sms_enabled: 'switch',
|
||||||
|
gold_credit_upcoming_sms_enabled: 'switch',
|
||||||
|
gold_credit_pay_gold_enabled: 'switch',
|
||||||
|
gold_credit_pay_rial_enabled: 'switch',
|
||||||
|
gold_credit_pay_mixed_enabled: 'switch',
|
||||||
|
gold_credit_penalty_amount: 'number',
|
||||||
|
gold_credit_grace_hours: 'number',
|
||||||
|
gold_credit_default_months: 'number',
|
||||||
|
gold_credit_min_balance_gram: 'number',
|
||||||
|
gold_credit_overdue_sms_text: 'longtext',
|
||||||
|
gold_credit_info_text: 'longtext',
|
||||||
|
gold_credit_request_success_text: 'longtext',
|
||||||
|
gold_credit_tiers: 'longtext',
|
||||||
|
gold_credit_penalty_type: 'select',
|
||||||
montzar_closed: 'switch',
|
montzar_closed: 'switch',
|
||||||
montzar_open_buy: 'switch',
|
montzar_open_buy: 'switch',
|
||||||
montzar_open_sell: 'switch',
|
montzar_open_sell: 'switch',
|
||||||
@@ -443,6 +460,10 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
|||||||
tahesab: 'تهحساب',
|
tahesab: 'تهحساب',
|
||||||
zarin: 'زرین'
|
zarin: 'زرین'
|
||||||
},
|
},
|
||||||
|
gold_credit_penalty_type: {
|
||||||
|
rial: 'ریالی',
|
||||||
|
gold: 'طلایی',
|
||||||
|
},
|
||||||
support_messenger: {
|
support_messenger: {
|
||||||
bale: 'بله',
|
bale: 'بله',
|
||||||
telegram: 'تلگرام',
|
telegram: 'تلگرام',
|
||||||
@@ -462,6 +483,7 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
|||||||
|
|
||||||
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||||
accounting_provider: 'none,pargar,tahesab,zarin',
|
accounting_provider: 'none,pargar,tahesab,zarin',
|
||||||
|
gold_credit_penalty_type: 'rial,gold',
|
||||||
price_provider: 'montzar,zaarbox,main_product',
|
price_provider: 'montzar,zaarbox,main_product',
|
||||||
payping_gateway_name: 'sepal,',
|
payping_gateway_name: 'sepal,',
|
||||||
external_payment_gateway: 'zarinpall,zibal,payping,sepal',
|
external_payment_gateway: 'zarinpall,zibal,payping,sepal',
|
||||||
@@ -883,6 +905,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
|||||||
'روشن: وقتی مشتری اولین پیام خواندهنشده را در چت پشتیبانی میفرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمینهای دارای «ارسال پیامک» میرود. خاموش: اعلان پیامک چت نمیرود.',
|
'روشن: وقتی مشتری اولین پیام خواندهنشده را در چت پشتیبانی میفرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمینهای دارای «ارسال پیامک» میرود. خاموش: اعلان پیامک چت نمیرود.',
|
||||||
group_sms:
|
group_sms:
|
||||||
'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.',
|
'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.',
|
||||||
|
enable_gold_credit:
|
||||||
|
'روشن: زیرمنوی اعتبار در مشتریان و صفحه اعتبار در اپ مشتری فعال میشود. خاموش: غیرفعال. فقط از سوپرادمین قابل تغییر است.',
|
||||||
customer_special_pricing:
|
customer_special_pricing:
|
||||||
'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخابشده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.',
|
'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخابشده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.',
|
||||||
profit_gold_api_enabled:
|
profit_gold_api_enabled:
|
||||||
@@ -1049,6 +1073,22 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
|||||||
open_register: 'ثبتنام باز',
|
open_register: 'ثبتنام باز',
|
||||||
sms_panel: 'پنل پیامک (کیف پول)',
|
sms_panel: 'پنل پیامک (کیف پول)',
|
||||||
customer_direct_sms: 'پیامک تکی به مشتری از لیست',
|
customer_direct_sms: 'پیامک تکی به مشتری از لیست',
|
||||||
|
enable_gold_credit: 'اعتبار طلایی',
|
||||||
|
gold_credit_penalty_type: 'نوع جریمه اعتبار طلایی',
|
||||||
|
gold_credit_penalty_amount: 'مبلغ جریمه روزانه اعتبار طلایی',
|
||||||
|
gold_credit_grace_hours: 'مهلت پرداخت بعد از سررسید (ساعت)',
|
||||||
|
gold_credit_default_months: 'تعداد ماه پیشفرض اعتبار طلایی',
|
||||||
|
gold_credit_min_balance_gram: 'حداقل موجودی طلا برای درخواست (گرم)',
|
||||||
|
gold_credit_overdue_sms_enabled: 'SMS هشدار سررسید اعتبار طلایی',
|
||||||
|
gold_credit_admin_sms_enabled: 'پیامک ادمین اعتبار طلایی',
|
||||||
|
gold_credit_upcoming_sms_enabled: 'یادآوری سررسید قسط اعتبار',
|
||||||
|
gold_credit_pay_gold_enabled: 'پرداخت اقساط با طلا',
|
||||||
|
gold_credit_pay_rial_enabled: 'پرداخت اقساط با ریال',
|
||||||
|
gold_credit_pay_mixed_enabled: 'پرداخت اقساط ترکیبی',
|
||||||
|
gold_credit_overdue_sms_text: 'متن SMS سررسید اعتبار طلایی',
|
||||||
|
gold_credit_info_text: 'متن راهنمای اعتبار طلایی',
|
||||||
|
gold_credit_request_success_text: 'متن پس از ثبت درخواست اعتبار',
|
||||||
|
gold_credit_tiers: 'سطوح اعتبار طلایی (JSON)',
|
||||||
customer_special_pricing: 'قیمت ویژه مشتری',
|
customer_special_pricing: 'قیمت ویژه مشتری',
|
||||||
charge_notif: 'پیامک شارژ ادمین',
|
charge_notif: 'پیامک شارژ ادمین',
|
||||||
sms_notif_order_admin: 'پیامک خرید/فروش به ادمین',
|
sms_notif_order_admin: 'پیامک خرید/فروش به ادمین',
|
||||||
@@ -1157,6 +1197,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
|||||||
'referral_bonus_amount',
|
'referral_bonus_amount',
|
||||||
'group_sms',
|
'group_sms',
|
||||||
'customer_special_pricing',
|
'customer_special_pricing',
|
||||||
|
'enable_gold_credit',
|
||||||
'customer_direct_sms',
|
'customer_direct_sms',
|
||||||
'enable_automation',
|
'enable_automation',
|
||||||
'enable_conditional_order',
|
'enable_conditional_order',
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
title="جزئیات اعتبار طلایی"
|
||||||
|
width="52rem"
|
||||||
|
ok-text="بستن"
|
||||||
|
@ok="modalVisible = false"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<template v-if="credit">
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4 text-sm">
|
||||||
|
<span v-if="displayName" class="bg-amber-50 text-amber-900 px-3 py-1.5 rounded-full font-medium">
|
||||||
|
{{ displayName }}
|
||||||
|
</span>
|
||||||
|
<span class="bg-amber-50 text-amber-800 px-3 py-1.5 rounded-full font-medium">
|
||||||
|
اعتبار: {{ credit.gold_amount }} گرم
|
||||||
|
</span>
|
||||||
|
<span class="bg-blue-50 text-blue-800 px-3 py-1.5 rounded-full font-medium">
|
||||||
|
{{ credit.months }} ماه
|
||||||
|
</span>
|
||||||
|
<span class="px-3 py-1.5 rounded-full font-medium" :class="statusClass(credit.status)">
|
||||||
|
{{ statusLabel(credit.status) }}
|
||||||
|
</span>
|
||||||
|
<span class="bg-slate-100 text-slate-700 px-3 py-1.5 rounded-full font-medium">
|
||||||
|
پرداختشده: {{ Number(credit.paid_gold ?? 0).toFixed(4) }} گرم
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="Number(credit.paid_rial) > 0"
|
||||||
|
class="bg-slate-100 text-slate-700 px-3 py-1.5 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
{{ Number(credit.paid_rial).toLocaleString('fa-IR') }} ریال
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="credit.has_check"
|
||||||
|
class="bg-emerald-50 text-emerald-800 px-3 py-1.5 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
چک دریافت شد
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="credit.has_contract"
|
||||||
|
class="bg-indigo-50 text-indigo-800 px-3 py-1.5 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
قرارداد بسته شد
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="credit.penalty_type"
|
||||||
|
class="mb-4 text-sm text-amber-900 bg-amber-50/80 border border-amber-100 rounded-xl px-3 py-2"
|
||||||
|
>
|
||||||
|
جریمه دیرکرد:
|
||||||
|
{{
|
||||||
|
credit.penalty_type === 'gold'
|
||||||
|
? `${credit.penalty_amount} گرم در روز`
|
||||||
|
: `${Number(credit.penalty_amount || 0).toLocaleString('fa-IR')} ریال در روز`
|
||||||
|
}}
|
||||||
|
· مهلت {{ credit.grace_hours ?? 24 }} ساعت
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="credit.notes" class="mb-4 text-sm text-gray-600 bg-gray-50 rounded-xl px-3 py-2">
|
||||||
|
<span class="font-medium text-gray-800">یادداشت: </span>{{ credit.notes }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="font-bold text-sm mb-2 text-gray-700">اقساط</div>
|
||||||
|
<a-table
|
||||||
|
:columns="detailColumns"
|
||||||
|
:data-source="installments"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record: row }">
|
||||||
|
<template v-if="column.key === 'amount_soot'">
|
||||||
|
{{ row.amount_soot?.toLocaleString('fa-IR') }} سوت
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'due_date'">
|
||||||
|
{{ toJalaliNoTime(row.due_date) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<CustomTag
|
||||||
|
:textColor="instStatusStyle(row.status).textColor"
|
||||||
|
:bgColor="instStatusStyle(row.status).bgColor"
|
||||||
|
class="px-2"
|
||||||
|
>
|
||||||
|
{{ instStatusStyle(row.status).text }}
|
||||||
|
</CustomTag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'paid_at'">
|
||||||
|
{{ row.paid_at ? toJalaliNoTime(row.paid_at) : '—' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'penalty'">
|
||||||
|
<span v-if="!row.penalty_days && !row.penalty_rial && !row.penalty_gold">—</span>
|
||||||
|
<span v-else class="text-xs text-red-700">
|
||||||
|
<span v-if="row.penalty_days">{{ row.penalty_days }} روز</span>
|
||||||
|
<span v-if="row.penalty_rial">
|
||||||
|
{{ row.penalty_days ? ' · ' : '' }}{{ Number(row.penalty_rial).toLocaleString('fa-IR') }} ریال
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="!loading" class="text-center text-gray-500 py-8">
|
||||||
|
اطلاعات اعتبار یافت نشد
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { getCustomerGoldCreditInstallments, type GoldCredit, type GoldCreditInstallment } from '@/api/goldCredit'
|
||||||
|
import { toJalaliNoTime } from '@core/utils/useDataTime'
|
||||||
|
import CustomTag from '@/components/CustomTag.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible?: boolean
|
||||||
|
customerId?: number
|
||||||
|
customerName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'update:visible', value: boolean): void }>()
|
||||||
|
|
||||||
|
const modalVisible = computed({
|
||||||
|
get: () => props.visible ?? false,
|
||||||
|
set: (value: boolean) => emit('update:visible', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const credit = ref<GoldCredit | null>(null)
|
||||||
|
const installments = ref<GoldCreditInstallment[]>([])
|
||||||
|
|
||||||
|
const displayName = computed(() => props.customerName ?? credit.value?.customer?.name ?? '')
|
||||||
|
|
||||||
|
const detailColumns = [
|
||||||
|
{ title: 'قسط', dataIndex: 'installment_number', key: 'installment_number', align: 'center' as const },
|
||||||
|
{ title: 'مبلغ (سوت)', dataIndex: 'amount_soot', key: 'amount_soot', align: 'center' as const },
|
||||||
|
{ title: 'سررسید', dataIndex: 'due_date', key: 'due_date', align: 'center' as const },
|
||||||
|
{ title: 'وضعیت', dataIndex: 'status', key: 'status', align: 'center' as const },
|
||||||
|
{ title: 'پرداخت', dataIndex: 'paid_at', key: 'paid_at', align: 'center' as const },
|
||||||
|
{ title: 'جریمه', dataIndex: 'penalty', key: 'penalty', align: 'center' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
const instStatusStyles: Record<string, { text: string; textColor: string; bgColor: string }> = {
|
||||||
|
pending: { text: 'در انتظار', textColor: '#c2410c', bgColor: '#ffedd5' },
|
||||||
|
paid: { text: 'پرداخت شده', textColor: '#15803d', bgColor: '#dcfce7' },
|
||||||
|
prepaid: { text: 'پیشپرداخت', textColor: '#1d4ed8', bgColor: '#dbeafe' },
|
||||||
|
overdue: { text: 'معوق', textColor: '#b91c1c', bgColor: '#fee2e2' },
|
||||||
|
partial: { text: 'ناقص', textColor: '#a16207', bgColor: '#fef9c3' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function instStatusStyle(s: string) {
|
||||||
|
return (
|
||||||
|
instStatusStyles[s] ?? {
|
||||||
|
text: s,
|
||||||
|
textColor: '#374151',
|
||||||
|
bgColor: '#f3f4f6',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(s: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
active: 'فعال',
|
||||||
|
completed: 'تسویه شده',
|
||||||
|
cancelled: 'لغو شده',
|
||||||
|
pending: 'در انتظار',
|
||||||
|
}
|
||||||
|
return map[s] ?? s
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusClass(s: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
active: 'bg-green-50 text-green-800',
|
||||||
|
completed: 'bg-blue-50 text-blue-800',
|
||||||
|
cancelled: 'bg-red-50 text-red-800',
|
||||||
|
pending: 'bg-orange-50 text-orange-800',
|
||||||
|
}
|
||||||
|
return map[s] ?? 'bg-gray-100 text-gray-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (open) => {
|
||||||
|
if (!open || !props.customerId) return
|
||||||
|
loading.value = true
|
||||||
|
credit.value = null
|
||||||
|
installments.value = []
|
||||||
|
try {
|
||||||
|
const res = await getCustomerGoldCreditInstallments(props.customerId)
|
||||||
|
credit.value = res.credit
|
||||||
|
installments.value = res.rows ?? []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-4 flex-wrap gap-2">
|
||||||
|
<div v-if="creditSummary" class="flex gap-2 flex-wrap text-sm">
|
||||||
|
<span class="bg-amber-50 text-amber-800 px-3 py-1 rounded-full font-medium">
|
||||||
|
اعتبار: {{ creditSummary.gold_amount }} گرم
|
||||||
|
</span>
|
||||||
|
<span class="bg-blue-50 text-blue-800 px-3 py-1 rounded-full font-medium">
|
||||||
|
{{ creditSummary.months }} ماه
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full font-medium"
|
||||||
|
:class="
|
||||||
|
creditSummary.status === 'active'
|
||||||
|
? 'bg-green-50 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ statusLabel(creditSummary.status) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="creditSummary.has_check"
|
||||||
|
class="bg-emerald-50 text-emerald-800 px-3 py-1 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
چک گرفته شد
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="creditSummary.has_contract"
|
||||||
|
class="bg-indigo-50 text-indigo-800 px-3 py-1 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
قرارداد بسته شد
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="creditSummary.penalty_type"
|
||||||
|
class="bg-red-50 text-red-800 px-3 py-1 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
جریمه:
|
||||||
|
{{
|
||||||
|
creditSummary.penalty_type === 'gold'
|
||||||
|
? `${creditSummary.penalty_amount} گرم/روز`
|
||||||
|
: `${Number(creditSummary.penalty_amount || 0).toLocaleString('fa-IR')} ریال/روز`
|
||||||
|
}}
|
||||||
|
· مهلت {{ creditSummary.grace_hours ?? 24 }}س
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<BaseButton type="primary" size="small" class="flex mr-auto" @click="emits('assign')">
|
||||||
|
<div>تعیین اعتبار طلایی</div>
|
||||||
|
<BaseIcon icon="vuesax-linear:medal-star" Size="16px" />
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<BasicShowContext title="لیست اقساط">
|
||||||
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl" />
|
||||||
|
</BasicShowContext>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="tsx" setup>
|
||||||
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
|
import { cols } from '@/pages/menuPages/customers/base/hooks/goldCreditColumns'
|
||||||
|
import { getCustomerGoldCreditInstallments, type GoldCredit } from '@/api/goldCredit'
|
||||||
|
|
||||||
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
interface Prop {
|
||||||
|
customerId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const emits = defineEmits(['assign'])
|
||||||
|
const props = defineProps<Prop>()
|
||||||
|
|
||||||
|
const creditSummary = ref<GoldCredit | null>(null)
|
||||||
|
const table = ref(null)
|
||||||
|
|
||||||
|
const tblProps = ref<any>({
|
||||||
|
columns: cols,
|
||||||
|
pagination: false,
|
||||||
|
api: async () => {
|
||||||
|
if (!props.customerId) return { rows: [], total: 0 }
|
||||||
|
const res = await getCustomerGoldCreditInstallments(props.customerId)
|
||||||
|
creditSummary.value = res.credit
|
||||||
|
return { rows: res.rows ?? [], total: res.rows?.length ?? 0 }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusLabel = (s: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
active: 'فعال',
|
||||||
|
completed: 'تسویه شده',
|
||||||
|
cancelled: 'لغو شده',
|
||||||
|
pending: 'در انتظار',
|
||||||
|
}
|
||||||
|
return map[s] ?? s
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetch = () => reload()
|
||||||
|
|
||||||
|
defineExpose({ fetch })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
:title="modalTitle"
|
||||||
|
width="40rem"
|
||||||
|
:loading="loading"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
:ok-text="props.mode === 'approve' ? 'تأیید و ایجاد اعتبار' : 'ثبت'"
|
||||||
|
@ok="submit"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<a-form :model="form" ref="formRef" autocomplete="off" layout="vertical">
|
||||||
|
<div class="grid md:grid-cols-2 gap-3">
|
||||||
|
<a-form-item label="مقدار طلا (گرم)" name="gold_amount" :rules="inputRules('مقدار طلا')">
|
||||||
|
<BasicInput v-model:value="form.gold_amount" type="number" @change="loadPreview" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="تعداد ماه" name="months" :rules="inputRules('تعداد ماه')">
|
||||||
|
<BasicInput v-model:value="form.months" type="number" @change="loadPreview" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="نوع سود" name="interest_type">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="form.interest_type"
|
||||||
|
:items="interestTypes"
|
||||||
|
valueName="value"
|
||||||
|
keyName="key"
|
||||||
|
@change="loadPreview"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="form.interest_type !== 'none'"
|
||||||
|
label="مقدار سود"
|
||||||
|
name="interest_value"
|
||||||
|
:rules="inputRules('مقدار سود')"
|
||||||
|
>
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="form.interest_value"
|
||||||
|
type="number"
|
||||||
|
:addon-before="form.interest_type === 'percent' ? '%' : 'گرم'"
|
||||||
|
@change="loadPreview"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="روش تحویل" name="delivery_method" class="md:col-span-2">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="form.delivery_method"
|
||||||
|
:items="deliveryMethods"
|
||||||
|
valueName="value"
|
||||||
|
keyName="key"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 mb-1 font-bold text-sm text-gray-700">ضمانت</div>
|
||||||
|
<div class="grid md:grid-cols-2 gap-3 rounded-xl bg-slate-50 p-3 border border-slate-100">
|
||||||
|
<a-form-item label="چک ضمانت دریافت شد" name="has_check">
|
||||||
|
<a-switch v-model:checked="form.has_check" checked-children="بله" un-checked-children="خیر" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="قرارداد بسته شد" name="has_contract">
|
||||||
|
<a-switch
|
||||||
|
v-model:checked="form.has_contract"
|
||||||
|
checked-children="بله"
|
||||||
|
un-checked-children="خیر"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="جزئیات چک" name="check_note">
|
||||||
|
<BasicInput v-model:value="form.check_note" place-holder="شماره / بانک / ..." />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="جزئیات قرارداد" name="contract_note">
|
||||||
|
<BasicInput v-model:value="form.contract_note" place-holder="شماره قرارداد / تاریخ" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 mb-1 font-bold text-sm text-gray-700">جریمه دیرکرد (قابل تنظیم برای این اعتبار)</div>
|
||||||
|
<div class="grid md:grid-cols-3 gap-3 rounded-xl bg-amber-50/60 p-3 border border-amber-100">
|
||||||
|
<a-form-item label="نوع جریمه" name="penalty_type">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="form.penalty_type"
|
||||||
|
:items="penaltyTypes"
|
||||||
|
valueName="value"
|
||||||
|
keyName="key"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="مبلغ روزانه" name="penalty_amount">
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="form.penalty_amount"
|
||||||
|
type="number"
|
||||||
|
:addon-before="form.penalty_type === 'gold' ? 'گرم' : 'ریال'"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="مهلت بعد از سررسید (ساعت)" name="grace_hours">
|
||||||
|
<BasicInput v-model:value="form.grace_hours" type="number" />
|
||||||
|
</a-form-item>
|
||||||
|
<div class="md:col-span-3 text-xs text-amber-800/80">
|
||||||
|
پرداخت خودکار نیست. بعد از سررسید SMS هشدار میرود؛ اگر تا پایان مهلت پرداخت نشود، هر روز این
|
||||||
|
مبلغ به بدهی قسط اضافه میشود. مقادیر پیشفرض از پیکربندی سیستم خوانده میشود.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="یادداشت" name="notes" class="mt-3">
|
||||||
|
<BasicInput v-model:value="form.notes" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="preview.length"
|
||||||
|
class="mt-4 rounded-xl border border-amber-100 bg-gradient-to-l from-amber-50 to-white p-4"
|
||||||
|
>
|
||||||
|
<div class="font-bold text-amber-900 mb-3 flex items-center gap-2">
|
||||||
|
<BaseIcon icon="vuesax-linear:calendar" Size="18px" />
|
||||||
|
پیشنمایش اقساط
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2 text-center text-sm mb-2 font-medium text-gray-600">
|
||||||
|
<div>ماه</div>
|
||||||
|
<div>مبلغ (سوت)</div>
|
||||||
|
<div>جمع (گرم)</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="item in preview.slice(0, 6)"
|
||||||
|
:key="item.installment_number"
|
||||||
|
class="grid grid-cols-3 gap-2 text-center py-1.5 border-b border-amber-50 last:border-0"
|
||||||
|
>
|
||||||
|
<div class="font-medium">{{ item.installment_number }}</div>
|
||||||
|
<div class="text-amber-800 font-bold">{{ item.amount_soot?.toLocaleString('fa-IR') }}</div>
|
||||||
|
<div>{{ item.total_gold?.toFixed(4) }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="preview.length > 6" class="text-center text-xs text-gray-500 mt-2">
|
||||||
|
و {{ preview.length - 6 }} قسط دیگر...
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 pt-3 border-t border-amber-100 flex justify-between font-bold text-amber-900">
|
||||||
|
<span>مجموع قابل پرداخت:</span>
|
||||||
|
<span>{{ totalPayable?.toFixed(4) }} گرم</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import { inputRules } from '@/utils/inputRules'
|
||||||
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import type { Item } from '@/model/Item'
|
||||||
|
import {
|
||||||
|
createGoldCredit,
|
||||||
|
updateGoldCredit,
|
||||||
|
previewGoldCredit,
|
||||||
|
approveGoldCreditRequest,
|
||||||
|
type GoldCreditForm,
|
||||||
|
type GoldCreditInstallment,
|
||||||
|
} from '@/api/goldCredit'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible?: boolean
|
||||||
|
customerId?: number
|
||||||
|
editId?: number
|
||||||
|
requestId?: number
|
||||||
|
mode?: 'create' | 'edit' | 'approve'
|
||||||
|
initial?: Partial<GoldCreditForm>
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
mode: 'create',
|
||||||
|
initial: () => ({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
(e: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modalVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value: boolean) => emit('update:visible', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
if (props.mode === 'approve') return 'تأیید درخواست اعتبار'
|
||||||
|
if (props.editId) return 'ویرایش اعتبار طلایی'
|
||||||
|
return 'تعیین اعتبار طلایی'
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const form = ref<GoldCreditForm>(defaultForm())
|
||||||
|
const preview = ref<GoldCreditInstallment[]>([])
|
||||||
|
const totalPayable = ref(0)
|
||||||
|
|
||||||
|
const interestTypes: Item[] = [
|
||||||
|
{ key: 'none', value: 'بدون سود' },
|
||||||
|
{ key: 'percent', value: 'درصد ماهانه' },
|
||||||
|
{ key: 'fixed_gram', value: 'گرم ثابت در ماه' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const deliveryMethods: Item[] = [
|
||||||
|
{ key: 'auto', value: 'خودکار — شارژ کیف پول' },
|
||||||
|
{ key: 'manual', value: 'دستی' },
|
||||||
|
{ key: 'in_person', value: 'تحویل حضوری' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const penaltyTypes: Item[] = [
|
||||||
|
{ key: 'rial', value: 'ریالی (روزانه)' },
|
||||||
|
{ key: 'gold', value: 'طلایی (گرم روزانه)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function defaultForm(): GoldCreditForm {
|
||||||
|
return {
|
||||||
|
interest_type: 'none',
|
||||||
|
interest_value: 0,
|
||||||
|
delivery_method: 'manual',
|
||||||
|
months: 12,
|
||||||
|
has_check: false,
|
||||||
|
has_contract: false,
|
||||||
|
penalty_type: 'rial',
|
||||||
|
penalty_amount: 1000000,
|
||||||
|
grace_hours: 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.value = {
|
||||||
|
...defaultForm(),
|
||||||
|
customer_id: props.customerId,
|
||||||
|
...props.initial,
|
||||||
|
}
|
||||||
|
if (!form.value.months) {
|
||||||
|
form.value.months = 12
|
||||||
|
}
|
||||||
|
preview.value = []
|
||||||
|
totalPayable.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (open) => {
|
||||||
|
if (!open) {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resetForm()
|
||||||
|
await nextTick()
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
await loadPreview()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function loadPreview() {
|
||||||
|
const gold = Number(form.value.gold_amount)
|
||||||
|
const months = Number(form.value.months)
|
||||||
|
if (!gold || !months) {
|
||||||
|
preview.value = []
|
||||||
|
totalPayable.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await previewGoldCredit({
|
||||||
|
...form.value,
|
||||||
|
gold_amount: gold,
|
||||||
|
months,
|
||||||
|
})
|
||||||
|
preview.value = res.installments ?? []
|
||||||
|
totalPayable.value = res.total_payable_gold ?? 0
|
||||||
|
} catch {
|
||||||
|
preview.value = []
|
||||||
|
totalPayable.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validateFields()
|
||||||
|
} catch {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را تکمیل کنید')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode === 'approve' && !props.requestId) {
|
||||||
|
message.error('شناسه درخواست یافت نشد')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode !== 'approve' && props.mode !== 'edit' && !props.customerId) {
|
||||||
|
message.error('شناسه مشتری یافت نشد')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
const gold = Number(form.value.gold_amount)
|
||||||
|
const months = Number(form.value.months)
|
||||||
|
if (!gold || gold <= 0) {
|
||||||
|
message.error('مقدار طلا باید بیشتر از صفر باشد')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
if (!months || months < 1) {
|
||||||
|
message.error('تعداد ماه باید حداقل ۱ باشد')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const payload: GoldCreditForm = {
|
||||||
|
...form.value,
|
||||||
|
customer_id: props.customerId,
|
||||||
|
gold_amount: gold,
|
||||||
|
months,
|
||||||
|
interest_value: Number(form.value.interest_value ?? 0),
|
||||||
|
penalty_amount: Number(form.value.penalty_amount ?? 0),
|
||||||
|
grace_hours: Number(form.value.grace_hours ?? 24),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode === 'approve' && props.requestId) {
|
||||||
|
await approveGoldCreditRequest(props.requestId, payload)
|
||||||
|
message.success('درخواست تأیید و اعتبار ایجاد شد')
|
||||||
|
} else if (props.editId) {
|
||||||
|
await updateGoldCredit(props.editId, payload)
|
||||||
|
message.success('اعتبار با موفقیت ویرایش شد')
|
||||||
|
} else {
|
||||||
|
await createGoldCredit(payload)
|
||||||
|
message.success('اعتبار طلایی با موفقیت ثبت شد')
|
||||||
|
}
|
||||||
|
|
||||||
|
modalVisible.value = false
|
||||||
|
emit('success')
|
||||||
|
} catch {
|
||||||
|
return Promise.reject()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
modalVisible.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
title="تنظیمات اعتبار طلایی"
|
||||||
|
width="48rem"
|
||||||
|
ok-text="بستن"
|
||||||
|
@ok="modalVisible = false"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="text-sm text-gray-500 mb-2">
|
||||||
|
این تنظیمات در اپ کاربر اعمال میشوند. روشهای پرداخت غیرفعال در اپ نمایش داده نمیشوند؛
|
||||||
|
ادمین همچنان میتواند از همه روشها استفاده کند.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="row in rows"
|
||||||
|
:key="row.id ?? row.key"
|
||||||
|
class="rounded-xl border border-slate-100 bg-slate-50/60 p-3"
|
||||||
|
>
|
||||||
|
<div class="font-medium text-sm text-gray-800 mb-1">{{ row.title_fa || row.key }}</div>
|
||||||
|
<div v-if="row.description" class="text-xs text-gray-500 mb-2 whitespace-pre-wrap">
|
||||||
|
{{ displayDescription(row) }}
|
||||||
|
</div>
|
||||||
|
<SettingValueField
|
||||||
|
:model-value="drafts[row.id!] ?? ''"
|
||||||
|
:setting-key="row.key"
|
||||||
|
:setting-type="row.type"
|
||||||
|
:description="row.description"
|
||||||
|
:rows="row.key === 'gold_credit_tiers' ? 6 : 4"
|
||||||
|
:disabled="savingIds.has(row.id!)"
|
||||||
|
@update:value="onDraftChange(row, $event)"
|
||||||
|
/>
|
||||||
|
<div v-if="savedFlashIds.has(row.id!)" class="text-xs text-green-600 mt-1">ذخیره شد</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
||||||
|
import type { Const } from '@/model/configuration'
|
||||||
|
import SettingValueField from '@/pages/menuPages/configuration/SettingValueField.vue'
|
||||||
|
import { formatValueForSave, resolveSettingFieldType } from '@/pages/menuPages/configuration/settingTypes'
|
||||||
|
|
||||||
|
const GOLD_CREDIT_KEYS = [
|
||||||
|
'gold_credit_pay_gold_enabled',
|
||||||
|
'gold_credit_pay_rial_enabled',
|
||||||
|
'gold_credit_pay_mixed_enabled',
|
||||||
|
'gold_credit_min_balance_gram',
|
||||||
|
'gold_credit_default_months',
|
||||||
|
'gold_credit_info_text',
|
||||||
|
'gold_credit_request_success_text',
|
||||||
|
'gold_credit_tiers',
|
||||||
|
'gold_credit_penalty_type',
|
||||||
|
'gold_credit_penalty_amount',
|
||||||
|
'gold_credit_grace_hours',
|
||||||
|
]
|
||||||
|
|
||||||
|
const props = defineProps<{ visible?: boolean }>()
|
||||||
|
const emit = defineEmits<{ (e: 'update:visible', value: boolean): void }>()
|
||||||
|
|
||||||
|
const modalVisible = computed({
|
||||||
|
get: () => props.visible ?? false,
|
||||||
|
set: (v: boolean) => emit('update:visible', v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const rows = ref<Const[]>([])
|
||||||
|
const drafts = ref<Record<number, string>>({})
|
||||||
|
const savingIds = ref(new Set<number>())
|
||||||
|
const savedFlashIds = ref(new Set<number>())
|
||||||
|
const saveTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
||||||
|
const debounceTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
function displayDescription(row: Const) {
|
||||||
|
if (row.type === 'select' && row.description?.includes('|')) {
|
||||||
|
return row.description.split('|')[0]
|
||||||
|
}
|
||||||
|
return row.description
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (open) => {
|
||||||
|
if (!open) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const all = (await getConfiguration()) ?? []
|
||||||
|
rows.value = all
|
||||||
|
.filter((r) => r.key && GOLD_CREDIT_KEYS.includes(r.key))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
GOLD_CREDIT_KEYS.indexOf(a.key!) - GOLD_CREDIT_KEYS.indexOf(b.key!)
|
||||||
|
)
|
||||||
|
const next: Record<number, string> = {}
|
||||||
|
for (const row of rows.value) {
|
||||||
|
if (row.id != null) next[row.id] = String(row.value ?? '')
|
||||||
|
}
|
||||||
|
drafts.value = next
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function onDraftChange(row: Const, value: string) {
|
||||||
|
if (row.id == null) return
|
||||||
|
drafts.value[row.id] = value
|
||||||
|
scheduleSave(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSave(row: Const) {
|
||||||
|
if (row.id == null) return
|
||||||
|
if (debounceTimers.has(row.id)) clearTimeout(debounceTimers.get(row.id))
|
||||||
|
debounceTimers.set(
|
||||||
|
row.id,
|
||||||
|
setTimeout(() => {
|
||||||
|
void saveRow(row)
|
||||||
|
}, 600)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRow(row: Const) {
|
||||||
|
if (row.id == null) return
|
||||||
|
const type = resolveSettingFieldType(row)
|
||||||
|
const raw = drafts.value[row.id] ?? row.value ?? ''
|
||||||
|
const value = formatValueForSave(raw, type)
|
||||||
|
if (value === (row.value ?? '')) return
|
||||||
|
|
||||||
|
savingIds.value.add(row.id)
|
||||||
|
savedFlashIds.value.delete(row.id)
|
||||||
|
try {
|
||||||
|
await setConfiguration({
|
||||||
|
id: row.id,
|
||||||
|
key: row.key,
|
||||||
|
value,
|
||||||
|
description: row.description ?? '',
|
||||||
|
is_active: row.is_active,
|
||||||
|
title_fa: row.title_fa ?? '',
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
row.value = value
|
||||||
|
savedFlashIds.value.add(row.id)
|
||||||
|
if (saveTimers.has(row.id)) clearTimeout(saveTimers.get(row.id))
|
||||||
|
saveTimers.set(
|
||||||
|
row.id,
|
||||||
|
setTimeout(() => {
|
||||||
|
savedFlashIds.value.delete(row.id!)
|
||||||
|
savedFlashIds.value = new Set(savedFlashIds.value)
|
||||||
|
}, 2000)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
drafts.value[row.id] = String(row.value ?? '')
|
||||||
|
message.error(`ذخیره «${row.title_fa || row.key}» ناموفق بود`)
|
||||||
|
} finally {
|
||||||
|
savingIds.value.delete(row.id)
|
||||||
|
savingIds.value = new Set(savingIds.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||||
|
import CustomTag from '@/components/CustomTag.vue'
|
||||||
|
import { toJalaliNoTime } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const statusStyles: Record<string, { text: string; textColor: string; bgColor: string }> = {
|
||||||
|
pending: { text: 'در انتظار', textColor: '#c2410c', bgColor: '#ffedd5' },
|
||||||
|
paid: { text: 'پرداخت شده', textColor: '#15803d', bgColor: '#dcfce7' },
|
||||||
|
prepaid: { text: 'پیشپرداخت', textColor: '#1d4ed8', bgColor: '#dbeafe' },
|
||||||
|
overdue: { text: 'معوق', textColor: '#b91c1c', bgColor: '#fee2e2' },
|
||||||
|
partial: { text: 'ناقص', textColor: '#a16207', bgColor: '#fef9c3' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cols: BaseTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'ردیف',
|
||||||
|
dataIndex: 'installment_number',
|
||||||
|
key: 'installment_number',
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'مبلغ (سوت)',
|
||||||
|
dataIndex: 'amount_soot',
|
||||||
|
key: 'amount_soot',
|
||||||
|
type: 'Number',
|
||||||
|
customRender: (record: any) => `${record.record.amount_soot?.toLocaleString('fa-IR')} سوت`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تاریخ سررسید',
|
||||||
|
dataIndex: 'due_date',
|
||||||
|
key: 'due_date',
|
||||||
|
customRender: (record: any) => toJalaliNoTime(record.record.due_date),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
customRender: (record: any) => {
|
||||||
|
const s = statusStyles[record.record.status] ?? {
|
||||||
|
text: record.record.status,
|
||||||
|
textColor: '#374151',
|
||||||
|
bgColor: '#f3f4f6',
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CustomTag textColor={s.textColor} bgColor={s.bgColor} class="px-2">
|
||||||
|
{s.text}
|
||||||
|
</CustomTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تاریخ پرداخت',
|
||||||
|
dataIndex: 'paid_at',
|
||||||
|
key: 'paid_at',
|
||||||
|
customRender: (record: any) =>
|
||||||
|
record.record.paid_at ? toJalaliNoTime(record.record.paid_at) : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'جریمه',
|
||||||
|
dataIndex: 'penalty_days',
|
||||||
|
key: 'penalty_days',
|
||||||
|
customRender: (record: any) => {
|
||||||
|
const r = record.record
|
||||||
|
const days = r.penalty_days || 0
|
||||||
|
if (!days && !(r.penalty_rial > 0) && !(r.penalty_gold > 0)) return '-'
|
||||||
|
const parts: string[] = []
|
||||||
|
if (days) parts.push(`${days} روز`)
|
||||||
|
if (r.penalty_rial > 0) parts.push(`${Number(r.penalty_rial).toLocaleString('fa-IR')} ریال`)
|
||||||
|
if (r.penalty_gold > 0) parts.push(`${Number(r.penalty_gold).toFixed(4)} گرم`)
|
||||||
|
return parts.join(' / ')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const creditCols: BaseTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'ردیف',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
customRender: (record: any) => record.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'نام مشتری',
|
||||||
|
dataIndex: 'customer.name',
|
||||||
|
key: 'customer.name',
|
||||||
|
type: 'Text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'موبایل',
|
||||||
|
dataIndex: 'customer.mobile',
|
||||||
|
key: 'customer.mobile',
|
||||||
|
type: 'Text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'مقدار (گرم)',
|
||||||
|
dataIndex: 'gold_amount',
|
||||||
|
key: 'gold_amount',
|
||||||
|
type: 'Number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تعداد ماه',
|
||||||
|
dataIndex: 'months',
|
||||||
|
key: 'months',
|
||||||
|
type: 'Number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
customRender: (record: any) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
active: 'فعال',
|
||||||
|
completed: 'تسویه شده',
|
||||||
|
cancelled: 'لغو شده',
|
||||||
|
pending: 'در انتظار',
|
||||||
|
}
|
||||||
|
const styles: Record<string, { textColor: string; bgColor: string }> = {
|
||||||
|
active: { textColor: '#15803d', bgColor: '#dcfce7' },
|
||||||
|
completed: { textColor: '#1d4ed8', bgColor: '#dbeafe' },
|
||||||
|
cancelled: { textColor: '#b91c1c', bgColor: '#fee2e2' },
|
||||||
|
pending: { textColor: '#c2410c', bgColor: '#ffedd5' },
|
||||||
|
}
|
||||||
|
const st = record.record.status
|
||||||
|
const style = styles[st] ?? { textColor: '#374151', bgColor: '#f3f4f6' }
|
||||||
|
return (
|
||||||
|
<CustomTag textColor={style.textColor} bgColor={style.bgColor} class="px-2">
|
||||||
|
{map[st] ?? st}
|
||||||
|
</CustomTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const requestCols: BaseTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'ردیف',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
customRender: (record: any) => record.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'نام مشتری',
|
||||||
|
dataIndex: 'customer.name',
|
||||||
|
key: 'customer.name',
|
||||||
|
type: 'Text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'موبایل',
|
||||||
|
dataIndex: 'customer.mobile',
|
||||||
|
key: 'customer.mobile',
|
||||||
|
type: 'Text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'مقدار درخواست (گرم)',
|
||||||
|
dataIndex: 'requested_gold',
|
||||||
|
key: 'requested_gold',
|
||||||
|
type: 'Number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
customRender: (record: any) => {
|
||||||
|
const map: Record<string, { text: string; textColor: string; bgColor: string }> = {
|
||||||
|
pending: { text: 'در انتظار', textColor: '#c2410c', bgColor: '#ffedd5' },
|
||||||
|
approved: { text: 'تأیید شده', textColor: '#15803d', bgColor: '#dcfce7' },
|
||||||
|
rejected: { text: 'رد شده', textColor: '#b91c1c', bgColor: '#fee2e2' },
|
||||||
|
}
|
||||||
|
const s = map[record.record.status] ?? {
|
||||||
|
text: record.record.status,
|
||||||
|
textColor: '#374151',
|
||||||
|
bgColor: '#f3f4f6',
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CustomTag textColor={s.textColor} bgColor={s.bgColor} class="px-2">
|
||||||
|
{s.text}
|
||||||
|
</CustomTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
|
<template #extraHeader>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<BaseButton type="default" size="small" class="flex" @click="settingsVisible = true">
|
||||||
|
<div>تنظیمات</div>
|
||||||
|
<BaseIcon icon="vuesax-linear:setting-2" Size="16px" />
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton type="primary" size="small" class="flex" @click="openCreate">
|
||||||
|
<div>اعطای اعتبار</div>
|
||||||
|
<BaseIcon icon="vuesax-linear:medal-star" Size="16px" />
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #action="{ record }">
|
||||||
|
<div class="flex gap-3 justify-center items-center">
|
||||||
|
<a-tooltip placement="top">
|
||||||
|
<template #title>جزئیات</template>
|
||||||
|
<Icon
|
||||||
|
icon="vuesax-linear:eye"
|
||||||
|
color="#6366f1"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click.stop="openDetails(record)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip v-if="record.status === 'active'" placement="top">
|
||||||
|
<template #title>ویرایش</template>
|
||||||
|
<Icon
|
||||||
|
icon="vuesax-linear:edit"
|
||||||
|
color="#4572E9"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click.stop="openEdit(record)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseTable>
|
||||||
|
|
||||||
|
<GoldCreditDetailModal
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
:customer-id="detailCustomerId"
|
||||||
|
:customer-name="detailCustomerName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GoldCreditSettingsModal v-model:visible="settingsVisible" />
|
||||||
|
|
||||||
|
<GoldCreditModal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
:mode="modalMode"
|
||||||
|
:edit-id="editRecord?.id"
|
||||||
|
:customer-id="createCustomerId ?? editRecord?.customer_id"
|
||||||
|
:initial="editInitial"
|
||||||
|
@success="reload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="createPickerVisible"
|
||||||
|
title="اعطای اعتبار طلایی"
|
||||||
|
width="28rem"
|
||||||
|
ok-text="ادامه"
|
||||||
|
@ok="confirmCreateCustomer"
|
||||||
|
>
|
||||||
|
<a-form-item label="انتخاب مشتری">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="createCustomerId"
|
||||||
|
:items="customerOptions"
|
||||||
|
valueName="value"
|
||||||
|
keyName="key"
|
||||||
|
placeholder="جستجو نام یا موبایل"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</BasicModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
|
import { creditCols } from '@/pages/menuPages/customers/base/hooks/goldCreditColumns'
|
||||||
|
import { getGoldCreditTable, type GoldCredit } from '@/api/goldCredit'
|
||||||
|
import GoldCreditModal from '@/pages/menuPages/customers/base/components/goldCreditModal.vue'
|
||||||
|
import GoldCreditDetailModal from '@/pages/menuPages/customers/base/components/goldCreditDetailModal.vue'
|
||||||
|
import GoldCreditSettingsModal from '@/pages/menuPages/customers/base/components/goldCreditSettingsModal.vue'
|
||||||
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import { getCustomersList } from '@/api/customer'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import type { Item } from '@/model/Item'
|
||||||
|
|
||||||
|
const [submit, reload] = useBaseTable()
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const modalMode = ref<'create' | 'edit'>('edit')
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const settingsVisible = ref(false)
|
||||||
|
const createPickerVisible = ref(false)
|
||||||
|
const detailCustomerId = ref<number>()
|
||||||
|
const detailCustomerName = ref<string>()
|
||||||
|
const createCustomerId = ref<number>()
|
||||||
|
const customerOptions = ref<Item[]>([])
|
||||||
|
const editRecord = ref<GoldCredit | null>(null)
|
||||||
|
const editInitial = ref<Record<string, unknown>>({})
|
||||||
|
|
||||||
|
const tblProps = ref({
|
||||||
|
columns: [
|
||||||
|
...creditCols,
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'action',
|
||||||
|
dataIndex: 'action',
|
||||||
|
slot: 'action',
|
||||||
|
width: 120,
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
api: (params: unknown) => getGoldCreditTable(params as never),
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const list = await getCustomersList()
|
||||||
|
customerOptions.value = (list ?? []).map((c) => ({
|
||||||
|
key: c.id,
|
||||||
|
value: `${c.name ?? ''} — ${c.mobile ?? ''}`,
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
customerOptions.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function openDetails(record: GoldCredit) {
|
||||||
|
detailCustomerId.value = record.customer_id
|
||||||
|
detailCustomerName.value = record.customer?.name
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
createCustomerId.value = undefined
|
||||||
|
createPickerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmCreateCustomer() {
|
||||||
|
if (!createCustomerId.value) {
|
||||||
|
message.warning('مشتری را انتخاب کنید')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
createPickerVisible.value = false
|
||||||
|
editRecord.value = null
|
||||||
|
editInitial.value = { months: 12 }
|
||||||
|
modalMode.value = 'create'
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(record: GoldCredit) {
|
||||||
|
createCustomerId.value = undefined
|
||||||
|
editRecord.value = record
|
||||||
|
modalMode.value = 'edit'
|
||||||
|
editInitial.value = {
|
||||||
|
gold_amount: record.gold_amount,
|
||||||
|
months: record.months,
|
||||||
|
interest_type: record.interest_type,
|
||||||
|
interest_value: record.interest_value,
|
||||||
|
delivery_method: record.delivery_method,
|
||||||
|
notes: record.notes,
|
||||||
|
has_check: record.has_check,
|
||||||
|
has_contract: record.has_contract,
|
||||||
|
check_note: record.check_note,
|
||||||
|
contract_note: record.contract_note,
|
||||||
|
penalty_type: record.penalty_type,
|
||||||
|
penalty_amount: record.penalty_amount,
|
||||||
|
grace_hours: record.grace_hours,
|
||||||
|
}
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full min-h-0 w-full">
|
||||||
|
<BasicShowContext title="اعتبار">
|
||||||
|
<a-tabs v-model:activeKey="activeTab" class="gold-credit-tabs h-full" destroy-inactive-tab-pane>
|
||||||
|
<a-tab-pane key="requests" tab="درخواستها">
|
||||||
|
<GoldCreditRequestsTab />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="active" tab="اعتبارهای جاری">
|
||||||
|
<GoldCreditActiveTab />
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</BasicShowContext>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import GoldCreditRequestsTab from './requests/index.vue'
|
||||||
|
import GoldCreditActiveTab from './active/index.vue'
|
||||||
|
|
||||||
|
const activeTab = ref('requests')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gold-credit-tabs :deep(.ant-tabs-content),
|
||||||
|
.gold-credit-tabs :deep(.ant-tabs-tabpane) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<BaseTable v-bind="tblProps" @register="registerTable" ref="tbl">
|
||||||
|
<template #action="{ record }">
|
||||||
|
<div v-if="record.status === 'pending'" class="flex gap-2 justify-center">
|
||||||
|
<BaseButton type="primary" size="small" @click.stop="openApprove(record)">
|
||||||
|
تأیید
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton size="small" danger @click.stop="openReject(record)">رد</BaseButton>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-xs">—</span>
|
||||||
|
</template>
|
||||||
|
</BaseTable>
|
||||||
|
|
||||||
|
<GoldCreditModal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
mode="approve"
|
||||||
|
:request-id="selectedRequestId"
|
||||||
|
:customer-id="selectedCustomerId"
|
||||||
|
:initial="modalInitial"
|
||||||
|
@success="reload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="rejectVisible"
|
||||||
|
title="رد درخواست"
|
||||||
|
width="24rem"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
@ok="confirmReject"
|
||||||
|
>
|
||||||
|
<a-form-item label="دلیل رد">
|
||||||
|
<BasicInput v-model:value="rejectNote" />
|
||||||
|
</a-form-item>
|
||||||
|
</BasicModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
|
import { requestCols } from '@/pages/menuPages/customers/base/hooks/goldCreditColumns'
|
||||||
|
import {
|
||||||
|
getGoldCreditRequestsTable,
|
||||||
|
rejectGoldCreditRequest,
|
||||||
|
type GoldCreditRequest,
|
||||||
|
} from '@/api/goldCredit'
|
||||||
|
import GoldCreditModal from '@/pages/menuPages/customers/base/components/goldCreditModal.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const [registerTable, reload] = useBaseTable()
|
||||||
|
const loading = ref(false)
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const rejectVisible = ref(false)
|
||||||
|
const selectedRequestId = ref<number>()
|
||||||
|
const selectedCustomerId = ref<number>()
|
||||||
|
const rejectNote = ref('')
|
||||||
|
const modalInitial = ref<Record<string, unknown>>({})
|
||||||
|
|
||||||
|
const tblProps = ref({
|
||||||
|
columns: [
|
||||||
|
...requestCols,
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'action',
|
||||||
|
dataIndex: 'action',
|
||||||
|
slot: 'action',
|
||||||
|
width: 180,
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
api: (params: unknown) => getGoldCreditRequestsTable(params as never),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function openApprove(record: GoldCreditRequest) {
|
||||||
|
selectedRequestId.value = record.id
|
||||||
|
selectedCustomerId.value = record.customer_id
|
||||||
|
modalInitial.value = {
|
||||||
|
gold_amount: record.requested_gold,
|
||||||
|
months: record.requested_months ?? 12,
|
||||||
|
}
|
||||||
|
await nextTick()
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReject(record: GoldCreditRequest) {
|
||||||
|
selectedRequestId.value = record.id
|
||||||
|
rejectNote.value = ''
|
||||||
|
rejectVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmReject() {
|
||||||
|
if (!selectedRequestId.value) {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await rejectGoldCreditRequest(selectedRequestId.value, rejectNote.value)
|
||||||
|
message.success('درخواست رد شد')
|
||||||
|
rejectVisible.value = false
|
||||||
|
reload()
|
||||||
|
} catch {
|
||||||
|
return Promise.reject()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,7 @@ import { computed } from 'vue'
|
|||||||
import BasicMenu from '@/components/BasicMenu/index.vue'
|
import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
import { customerSpecialPricingEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
import { customerSpecialPricingEnabledSetting, goldCreditEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||||
|
|
||||||
initWalletSettingsFromApi()
|
initWalletSettingsFromApi()
|
||||||
|
|
||||||
@@ -33,10 +33,11 @@ const menu = computed(() => {
|
|||||||
meta?: { order?: number }
|
meta?: { order?: number }
|
||||||
}>
|
}>
|
||||||
return [...children]
|
return [...children]
|
||||||
.filter(
|
.filter((child) => {
|
||||||
(child) =>
|
if (child.name === 'customerMain_specialPrice') return customerSpecialPricingEnabledSetting.value
|
||||||
child.name !== 'customerMain_specialPrice' || customerSpecialPricingEnabledSetting.value
|
if (child.name === 'customerMain_goldCredit') return goldCreditEnabledSetting.value
|
||||||
)
|
return true
|
||||||
|
})
|
||||||
.sort((a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99))
|
.sort((a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -80,6 +80,16 @@ const Customers: RouteRecordRaw = {
|
|||||||
title: 'قیمت ویژه',
|
title: 'قیمت ویژه',
|
||||||
order: 4
|
order: 4
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customerMain_goldCredit',
|
||||||
|
path: 'gold-credit',
|
||||||
|
component: () => import('@/pages/menuPages/customers/goldCredit/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'vuesax-linear:medal-star',
|
||||||
|
title: 'اعتبار',
|
||||||
|
order: 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -64,6 +64,9 @@ export const groupSmsEnabledSetting = ref(false)
|
|||||||
/** reactive — زیرمنوی قیمت ویژه مشتری */
|
/** reactive — زیرمنوی قیمت ویژه مشتری */
|
||||||
export const customerSpecialPricingEnabledSetting = ref(false)
|
export const customerSpecialPricingEnabledSetting = ref(false)
|
||||||
|
|
||||||
|
/** reactive — زیرمنوی اعتبار طلایی در مشتریان */
|
||||||
|
export const goldCreditEnabledSetting = ref(false)
|
||||||
|
|
||||||
/** reactive — منو و زنگوله اعلان داخل اپ */
|
/** reactive — منو و زنگوله اعلان داخل اپ */
|
||||||
export const appNotificationEnabledSetting = ref(true)
|
export const appNotificationEnabledSetting = ref(true)
|
||||||
|
|
||||||
@@ -87,8 +90,11 @@ let pargarEnabledFromSettings: boolean | null = null
|
|||||||
let customerDirectSmsFromSettings: boolean | null = null
|
let customerDirectSmsFromSettings: boolean | null = null
|
||||||
let accountingProviderFromSettings: AccountingProvider | null = null
|
let accountingProviderFromSettings: AccountingProvider | null = null
|
||||||
|
|
||||||
function isSwitchOn(value: string | undefined | null): boolean {
|
function isSwitchOn(value: string | number | boolean | undefined | null): boolean {
|
||||||
const v = (value ?? '').trim().toLowerCase()
|
if (value === true || value === 1) return true
|
||||||
|
const v = String(value ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
return v === 'true' || v === '1' || v === 'yes' || v === 'activate'
|
return v === 'true' || v === '1' || v === 'yes' || v === 'activate'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +172,10 @@ export function setCustomerSpecialPricingEnabledFromSettings(value: boolean): vo
|
|||||||
customerSpecialPricingEnabledSetting.value = value
|
customerSpecialPricingEnabledSetting.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setGoldCreditEnabledFromSettings(value: boolean): void {
|
||||||
|
goldCreditEnabledSetting.value = value
|
||||||
|
}
|
||||||
|
|
||||||
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
||||||
conditionalOrderEnabledSetting.value = value
|
conditionalOrderEnabledSetting.value = value
|
||||||
}
|
}
|
||||||
@@ -283,13 +293,20 @@ export function applyCustomerSpecialPricingFromRows(
|
|||||||
if (row) setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(row.value))
|
if (row) setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(row.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyGoldCreditEnabledFromRows(
|
||||||
|
rows: Array<{ key?: string; value?: string | null }>
|
||||||
|
) {
|
||||||
|
const row = rows.find((item) => item.key === 'enable_gold_credit')
|
||||||
|
if (row) setGoldCreditEnabledFromSettings(isSwitchOn(row.value))
|
||||||
|
}
|
||||||
|
|
||||||
/** بعد از همگامسازی سوپرادمین — bootstrap بعدی دوباره از API میخواند */
|
/** بعد از همگامسازی سوپرادمین — bootstrap بعدی دوباره از API میخواند */
|
||||||
export function invalidateSettingsBootstrap(): void {
|
export function invalidateSettingsBootstrap(): void {
|
||||||
settingsBootstrapped = false
|
settingsBootstrapped = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* اعمال مستقیم ۱۴ دسترسی سوپرادمین روی فلگهای منو/UI
|
* اعمال مستقیم ۱۵ دسترسی سوپرادمین روی فلگهای منو/UI
|
||||||
* (پاسخ refresh-superadmin.items یا ردیفهای get_settings).
|
* (پاسخ refresh-superadmin.items یا ردیفهای get_settings).
|
||||||
*/
|
*/
|
||||||
export function applySuperAdminAccessFlagsFromItems(
|
export function applySuperAdminAccessFlagsFromItems(
|
||||||
@@ -301,6 +318,7 @@ export function applySuperAdminAccessFlagsFromItems(
|
|||||||
applyCustomerDirectSmsFromRows(rows)
|
applyCustomerDirectSmsFromRows(rows)
|
||||||
applyGroupSmsFromRows(rows)
|
applyGroupSmsFromRows(rows)
|
||||||
applyCustomerSpecialPricingFromRows(rows)
|
applyCustomerSpecialPricingFromRows(rows)
|
||||||
|
applyGoldCreditEnabledFromRows(rows)
|
||||||
applyConditionalOrderEnabledFromRows(rows)
|
applyConditionalOrderEnabledFromRows(rows)
|
||||||
applyAutomationEnabledFromRows(rows)
|
applyAutomationEnabledFromRows(rows)
|
||||||
applyProductFundsEnabledFromRows(rows)
|
applyProductFundsEnabledFromRows(rows)
|
||||||
@@ -386,6 +404,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
|||||||
applyCustomerDirectSmsFromRows(list)
|
applyCustomerDirectSmsFromRows(list)
|
||||||
applyGroupSmsFromRows(list)
|
applyGroupSmsFromRows(list)
|
||||||
applyCustomerSpecialPricingFromRows(list)
|
applyCustomerSpecialPricingFromRows(list)
|
||||||
|
applyGoldCreditEnabledFromRows(list)
|
||||||
applyConditionalOrderEnabledFromRows(list)
|
applyConditionalOrderEnabledFromRows(list)
|
||||||
applyAutomationEnabledFromRows(list)
|
applyAutomationEnabledFromRows(list)
|
||||||
applyProductFundsEnabledFromRows(list)
|
applyProductFundsEnabledFromRows(list)
|
||||||
|
|||||||
Reference in New Issue
Block a user