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 { refreshAppModeMenu } from '@/router'
|
||||
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 {
|
||||
EDITABLE_SETTING_GROUPS,
|
||||
SYSTEM_SETTING_GROUPS,
|
||||
@@ -498,13 +498,12 @@ async function pullSuperAdminAndApplyFlags(): Promise<{
|
||||
ok: boolean
|
||||
message?: string
|
||||
count?: number
|
||||
meta?: { channel?: string; origin?: string; domain?: string }
|
||||
items?: Record<string, string>
|
||||
}> {
|
||||
invalidateSettingsBootstrap()
|
||||
const res = await refreshSuperAdminSettings()
|
||||
if (res?.items && typeof res.items === 'object') {
|
||||
applySuperAdminAccessFlagsFromItems(res.items)
|
||||
}
|
||||
// get_settings کامل — فلگ منو و کارتها با مقدار زنده همتراز شوند
|
||||
// get_settings کامل — فلگهای غیرسوپرادمین و قیمت
|
||||
try {
|
||||
const { getConfiguration } = await import('@/api/configuration')
|
||||
const all = await getConfiguration()
|
||||
@@ -513,23 +512,50 @@ async function pullSuperAdminAndApplyFlags(): Promise<{
|
||||
applyPriceProviderFromRows(rows)
|
||||
applyPriceChannelAccessFromRows(rows)
|
||||
} catch {
|
||||
/* فلگ از items میماند */
|
||||
/* در ادامه از items همگام استفاده میشود */
|
||||
}
|
||||
// items پاسخ refresh منبع حقیقت است — آخر اعمال شود تا get_settings کهنه/دامنهٔ اشتباه فلگ را خاموش نکند
|
||||
if (res?.items && typeof res.items === 'object') {
|
||||
applySuperAdminAccessFlagsFromItems(res.items)
|
||||
}
|
||||
return {
|
||||
ok: res?.ok !== false,
|
||||
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() {
|
||||
if (refreshingSuperAdmin.value) return
|
||||
refreshingSuperAdmin.value = true
|
||||
try {
|
||||
const res = await pullSuperAdminAndApplyFlags()
|
||||
await initConfig()
|
||||
// دوباره items را بعد از initConfig (که ممکن است get_settings ناقص بخواند) اعمال کن
|
||||
if (res?.items && typeof res.items === 'object') {
|
||||
applySuperAdminAccessFlagsFromItems(res.items)
|
||||
}
|
||||
refreshMenuAfterStoreChange()
|
||||
message.success(res?.message || `همگام شد (${res?.count ?? 0} کلید دسترسی از سوپرادمین)`)
|
||||
message.success(syncSuccessMessage(res))
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||||
@@ -546,8 +572,11 @@ async function onRefreshClick() {
|
||||
try {
|
||||
const res = await pullSuperAdminAndApplyFlags()
|
||||
await initConfig()
|
||||
if (res?.items && typeof res.items === 'object') {
|
||||
applySuperAdminAccessFlagsFromItems(res.items)
|
||||
}
|
||||
refreshMenuAfterStoreChange()
|
||||
message.success(res?.message || 'بروزرسانی شد')
|
||||
message.success(syncSuccessMessage(res))
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||||
@@ -580,6 +609,7 @@ async function initConfig() {
|
||||
applySupportChatMenuEnabledFromRows(baseData.value)
|
||||
applyGroupSmsFromRows(baseData.value)
|
||||
applyCustomerSpecialPricingFromRows(baseData.value)
|
||||
applyGoldCreditEnabledFromRows(baseData.value)
|
||||
// صفحهٔ عادی price_provider ندارد (can_edit=0) — همیشه از get_settings کامل بخوان
|
||||
if (
|
||||
!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 === 'price_channel_access') ||
|
||||
!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 {
|
||||
const { getConfiguration } = await import('@/api/configuration')
|
||||
@@ -598,6 +629,7 @@ async function initConfig() {
|
||||
applyPriceChannelAccessFromRows(rows)
|
||||
applyGroupSmsFromRows(rows)
|
||||
applyCustomerSpecialPricingFromRows(rows)
|
||||
applyGoldCreditEnabledFromRows(rows)
|
||||
} catch {
|
||||
/* کش قبلی میماند */
|
||||
}
|
||||
@@ -677,6 +709,9 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
|
||||
if (key === 'customer_special_pricing') {
|
||||
setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
if (key === 'enable_gold_credit') {
|
||||
setGoldCreditEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
}
|
||||
|
||||
function onValueChange(row: Const, value: string) {
|
||||
|
||||
@@ -45,6 +45,12 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
icon: 'vuesax-linear:lock',
|
||||
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',
|
||||
title: 'منبع قیمت و تابلو',
|
||||
@@ -152,7 +158,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
icon: 'vuesax-linear:people',
|
||||
// فقط فلگهای مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادیاند
|
||||
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
|
||||
)
|
||||
},
|
||||
|
||||
@@ -194,6 +194,7 @@ export function isCodeManagedSettingKey(key?: string | null): boolean {
|
||||
export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([
|
||||
'customer_direct_sms',
|
||||
'customer_special_pricing',
|
||||
'enable_gold_credit',
|
||||
'enable_automation',
|
||||
'enable_conditional_order',
|
||||
'enable_customer_level_profit',
|
||||
@@ -366,6 +367,22 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
sms_notif_support_admin: 'switch',
|
||||
group_sms: '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_open_buy: 'switch',
|
||||
montzar_open_sell: 'switch',
|
||||
@@ -443,6 +460,10 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
tahesab: 'تهحساب',
|
||||
zarin: 'زرین'
|
||||
},
|
||||
gold_credit_penalty_type: {
|
||||
rial: 'ریالی',
|
||||
gold: 'طلایی',
|
||||
},
|
||||
support_messenger: {
|
||||
bale: 'بله',
|
||||
telegram: 'تلگرام',
|
||||
@@ -462,6 +483,7 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
|
||||
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||
accounting_provider: 'none,pargar,tahesab,zarin',
|
||||
gold_credit_penalty_type: 'rial,gold',
|
||||
price_provider: 'montzar,zaarbox,main_product',
|
||||
payping_gateway_name: 'sepal,',
|
||||
external_payment_gateway: 'zarinpall,zibal,payping,sepal',
|
||||
@@ -883,6 +905,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'روشن: وقتی مشتری اولین پیام خواندهنشده را در چت پشتیبانی میفرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمینهای دارای «ارسال پیامک» میرود. خاموش: اعلان پیامک چت نمیرود.',
|
||||
group_sms:
|
||||
'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.',
|
||||
enable_gold_credit:
|
||||
'روشن: زیرمنوی اعتبار در مشتریان و صفحه اعتبار در اپ مشتری فعال میشود. خاموش: غیرفعال. فقط از سوپرادمین قابل تغییر است.',
|
||||
customer_special_pricing:
|
||||
'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخابشده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.',
|
||||
profit_gold_api_enabled:
|
||||
@@ -1049,6 +1073,22 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
open_register: 'ثبتنام باز',
|
||||
sms_panel: 'پنل پیامک (کیف پول)',
|
||||
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: 'قیمت ویژه مشتری',
|
||||
charge_notif: 'پیامک شارژ ادمین',
|
||||
sms_notif_order_admin: 'پیامک خرید/فروش به ادمین',
|
||||
@@ -1157,6 +1197,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'referral_bonus_amount',
|
||||
'group_sms',
|
||||
'customer_special_pricing',
|
||||
'enable_gold_credit',
|
||||
'customer_direct_sms',
|
||||
'enable_automation',
|
||||
'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 { useRouterStore } from '@core/stores/router'
|
||||
import { get } from 'lodash'
|
||||
import { customerSpecialPricingEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||
import { customerSpecialPricingEnabledSetting, goldCreditEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||
|
||||
initWalletSettingsFromApi()
|
||||
|
||||
@@ -33,10 +33,11 @@ const menu = computed(() => {
|
||||
meta?: { order?: number }
|
||||
}>
|
||||
return [...children]
|
||||
.filter(
|
||||
(child) =>
|
||||
child.name !== 'customerMain_specialPrice' || customerSpecialPricingEnabledSetting.value
|
||||
)
|
||||
.filter((child) => {
|
||||
if (child.name === 'customerMain_specialPrice') return customerSpecialPricingEnabledSetting.value
|
||||
if (child.name === 'customerMain_goldCredit') return goldCreditEnabledSetting.value
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.meta?.order ?? 99) - (b.meta?.order ?? 99))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -80,6 +80,16 @@ const Customers: RouteRecordRaw = {
|
||||
title: 'قیمت ویژه',
|
||||
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 — زیرمنوی قیمت ویژه مشتری */
|
||||
export const customerSpecialPricingEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — زیرمنوی اعتبار طلایی در مشتریان */
|
||||
export const goldCreditEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — منو و زنگوله اعلان داخل اپ */
|
||||
export const appNotificationEnabledSetting = ref(true)
|
||||
|
||||
@@ -87,8 +90,11 @@ let pargarEnabledFromSettings: boolean | null = null
|
||||
let customerDirectSmsFromSettings: boolean | null = null
|
||||
let accountingProviderFromSettings: AccountingProvider | null = null
|
||||
|
||||
function isSwitchOn(value: string | undefined | null): boolean {
|
||||
const v = (value ?? '').trim().toLowerCase()
|
||||
function isSwitchOn(value: string | number | boolean | undefined | null): boolean {
|
||||
if (value === true || value === 1) return true
|
||||
const v = String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return v === 'true' || v === '1' || v === 'yes' || v === 'activate'
|
||||
}
|
||||
|
||||
@@ -166,6 +172,10 @@ export function setCustomerSpecialPricingEnabledFromSettings(value: boolean): vo
|
||||
customerSpecialPricingEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setGoldCreditEnabledFromSettings(value: boolean): void {
|
||||
goldCreditEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
||||
conditionalOrderEnabledSetting.value = value
|
||||
}
|
||||
@@ -283,13 +293,20 @@ export function applyCustomerSpecialPricingFromRows(
|
||||
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 میخواند */
|
||||
export function invalidateSettingsBootstrap(): void {
|
||||
settingsBootstrapped = false
|
||||
}
|
||||
|
||||
/**
|
||||
* اعمال مستقیم ۱۴ دسترسی سوپرادمین روی فلگهای منو/UI
|
||||
* اعمال مستقیم ۱۵ دسترسی سوپرادمین روی فلگهای منو/UI
|
||||
* (پاسخ refresh-superadmin.items یا ردیفهای get_settings).
|
||||
*/
|
||||
export function applySuperAdminAccessFlagsFromItems(
|
||||
@@ -301,6 +318,7 @@ export function applySuperAdminAccessFlagsFromItems(
|
||||
applyCustomerDirectSmsFromRows(rows)
|
||||
applyGroupSmsFromRows(rows)
|
||||
applyCustomerSpecialPricingFromRows(rows)
|
||||
applyGoldCreditEnabledFromRows(rows)
|
||||
applyConditionalOrderEnabledFromRows(rows)
|
||||
applyAutomationEnabledFromRows(rows)
|
||||
applyProductFundsEnabledFromRows(rows)
|
||||
@@ -386,6 +404,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
||||
applyCustomerDirectSmsFromRows(list)
|
||||
applyGroupSmsFromRows(list)
|
||||
applyCustomerSpecialPricingFromRows(list)
|
||||
applyGoldCreditEnabledFromRows(list)
|
||||
applyConditionalOrderEnabledFromRows(list)
|
||||
applyAutomationEnabledFromRows(list)
|
||||
applyProductFundsEnabledFromRows(list)
|
||||
|
||||
Reference in New Issue
Block a user