added notif 2
This commit is contained in:
Vendored
+1
@@ -95,5 +95,6 @@ declare module '@vue/runtime-core' {
|
||||
ThemeMenuSection: typeof import('./src/core/components/ThemeMenuSection.vue')['default']
|
||||
ThemeToggle: typeof import('./src/core/components/ThemeToggle.vue')['default']
|
||||
UploadPic: typeof import('./src/components/UploadPic.vue')['default']
|
||||
ZarinRetryModal: typeof import('./src/components/ZarinRetryModal.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useAxios from '@core/utils/useAxios'
|
||||
import type { AppNotification } from '@/model/appNotifications'
|
||||
|
||||
export const getAppNotificationsTable = async (params: TableInput) =>
|
||||
await useAxios.post<Array<AppNotification>>('/admin/app_notifications/table', params)
|
||||
|
||||
export const createAppNotification = async (payload: Partial<AppNotification>) =>
|
||||
await useAxios.post<AppNotification>('/admin/app_notifications', payload)
|
||||
|
||||
export const updateAppNotification = async (id: number, payload: Partial<AppNotification>) =>
|
||||
await useAxios.put(`/admin/app_notifications/${id}`, payload)
|
||||
|
||||
export const toggleAppNotificationActive = async (id: number) =>
|
||||
await useAxios.put<{ is_active: boolean }>(`/admin/app_notifications/${id}/toggle`)
|
||||
|
||||
export const deleteAppNotification = async (id: number) =>
|
||||
await useAxios.delete(`/admin/app_notifications/${id}`)
|
||||
@@ -0,0 +1,93 @@
|
||||
import useAxios from '@core/utils/useAxios'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
|
||||
export interface ZarinRecord {
|
||||
id: number
|
||||
customer_id?: number
|
||||
amount?: number
|
||||
desc?: string
|
||||
type?: string
|
||||
sync_status?: 'pending' | 'success' | 'failed'
|
||||
sync_log?: string | null
|
||||
zarin_id?: string | null
|
||||
data?: string | null
|
||||
res?: string | null
|
||||
created_at?: string
|
||||
customer?: {
|
||||
id?: number
|
||||
name?: string
|
||||
mobile?: string
|
||||
moshtari_code?: string | number | null
|
||||
}
|
||||
}
|
||||
|
||||
export const getZarinTable = async (params: TableInput) =>
|
||||
await useAxios.post<ZarinRecord[]>('/admin/zarin/table', params)
|
||||
|
||||
export const getZarinDetail = async (id: number) =>
|
||||
await useAxios.get<ZarinRecord>(`/admin/zarin/${id}`)
|
||||
|
||||
export const retryZarinSanad = async (id: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; zarin?: ZarinRecord }>(`/admin/zarin/${id}/retry`)
|
||||
|
||||
export const registerCustomerInZarin = async (customerId: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||
`/admin/customer/${customerId}/zarin/register`
|
||||
)
|
||||
|
||||
export interface PanelOnlyCustomer {
|
||||
id: number
|
||||
name: string
|
||||
mobile: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ZarinOnlyCustomer {
|
||||
code: number
|
||||
name: string
|
||||
tel: string
|
||||
gid: number | null
|
||||
group: string
|
||||
}
|
||||
|
||||
export interface CustomerSyncStatus {
|
||||
panel_total: number
|
||||
panel_registered: number
|
||||
panel_only: number
|
||||
zarin_total: number
|
||||
zarin_only_count: number
|
||||
panel_only_list: {
|
||||
data: PanelOnlyCustomer[]
|
||||
current_page: number
|
||||
last_page: number
|
||||
total: number
|
||||
}
|
||||
zarin_only_list: {
|
||||
data: ZarinOnlyCustomer[]
|
||||
current_page: number
|
||||
last_page: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export const getZarinCustomerSyncStatus = async (params?: {
|
||||
search?: string
|
||||
page?: number
|
||||
zarin_page?: number
|
||||
include_zarin?: number | boolean
|
||||
}) => await useAxios.get<CustomerSyncStatus>('/admin/zarin/customer-sync/status', { params })
|
||||
|
||||
export interface SyncPanelResult {
|
||||
ok: boolean
|
||||
success: number
|
||||
failed: number
|
||||
errors: { name: string; mobile: string; error: string }[]
|
||||
}
|
||||
|
||||
export const syncPanelToZarin = async () =>
|
||||
await useAxios.post<SyncPanelResult>('/admin/zarin/customer-sync/panel-to-zarin')
|
||||
|
||||
export const importFromZarin = async (customers: ZarinOnlyCustomer[]) =>
|
||||
await useAxios.post<SyncPanelResult>('/admin/zarin/customer-sync/import-from-zarin', {
|
||||
customers,
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-model:visible="visible"
|
||||
:title="title"
|
||||
:ok-text="confirmText"
|
||||
:loading="loading"
|
||||
width="32rem"
|
||||
@ok="confirm"
|
||||
@close="visible = false"
|
||||
>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div v-for="item in items" :key="item.label" class="flex justify-between gap-3 border-b border-gray-100 pb-2">
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
<span class="text-left break-all">{{ item.value ?? '-' }}</span>
|
||||
</div>
|
||||
<div v-if="detail?.res" class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap">{{ detail.res }}</div>
|
||||
<div v-if="logText" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">{{ logText }}</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import { getZarinDetail, retryZarinSanad, registerCustomerInZarin, type ZarinRecord } from '@/api/zarin'
|
||||
import { zarinStatusLabel } from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
mode: 'retry-sanad' | 'register-customer'
|
||||
zarinId?: number
|
||||
customerId?: number
|
||||
customerName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<ZarinRecord | null>(null)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const title = computed(() =>
|
||||
props.mode === 'register-customer' ? 'ثبت مشتری در زرین' : 'ارسال مجدد سند به زرین'
|
||||
)
|
||||
|
||||
const confirmText = computed(() =>
|
||||
props.mode === 'register-customer' ? 'ثبت در زرین' : 'ارسال مجدد'
|
||||
)
|
||||
|
||||
const logText = computed(() => detail.value?.sync_log || '')
|
||||
|
||||
const items = computed(() => {
|
||||
if (props.mode === 'register-customer') {
|
||||
return [
|
||||
{ label: 'نام مشتری', value: props.customerName || detail.value?.customer?.name },
|
||||
{ label: 'شناسه مشتری', value: props.customerId || detail.value?.customer_id },
|
||||
{ label: 'کد زرین', value: detail.value?.customer?.moshtari_code }
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: 'شناسه سند', value: detail.value?.id },
|
||||
{ label: 'مشتری', value: detail.value?.customer?.name },
|
||||
{ label: 'موبایل', value: detail.value?.customer?.mobile },
|
||||
{ label: 'نوع', value: detail.value?.type },
|
||||
{ label: 'مقدار', value: detail.value?.amount },
|
||||
{ label: 'شرح', value: detail.value?.desc },
|
||||
{ label: 'وضعیت', value: zarinStatusLabel(detail.value?.sync_status, detail.value?.zarin_id) },
|
||||
{ label: 'کد زرین', value: detail.value?.zarin_id }
|
||||
]
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.zarinId] as const,
|
||||
async ([open, id]) => {
|
||||
if (!open || props.mode !== 'retry-sanad' || !id) return
|
||||
loading.value = true
|
||||
try {
|
||||
detail.value = await getZarinDetail(id)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function confirm() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (props.mode === 'register-customer' && props.customerId) {
|
||||
const result = await registerCustomerInZarin(props.customerId)
|
||||
if (!result.ok) {
|
||||
message.error(result.message || 'ثبت مشتری ناموفق بود')
|
||||
return
|
||||
}
|
||||
message.success(result.message || 'مشتری در زرین ثبت شد')
|
||||
} else if (props.mode === 'retry-sanad' && props.zarinId) {
|
||||
const result = await retryZarinSanad(props.zarinId)
|
||||
if (!result.ok) {
|
||||
message.error(result.message || 'ارسال مجدد ناموفق بود')
|
||||
detail.value = result.zarin || detail.value
|
||||
return
|
||||
}
|
||||
message.success(result.message || 'سند با موفقیت ثبت شد')
|
||||
}
|
||||
visible.value = false
|
||||
emit('success')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface AppNotification {
|
||||
id?: number
|
||||
title: string
|
||||
body: string
|
||||
severity: 'info' | 'success' | 'warning' | 'danger'
|
||||
is_active: boolean
|
||||
created_by?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export const APP_NOTIFICATION_SEVERITIES = [
|
||||
{ value: 'info', label: 'اطلاعات (info)' },
|
||||
{ value: 'success', label: 'موفق (success)' },
|
||||
{ value: 'warning', label: 'هشدار (warning)' },
|
||||
{ value: 'danger', label: 'مهم / خطر (danger)' },
|
||||
] as const
|
||||
@@ -0,0 +1,863 @@
|
||||
<template>
|
||||
<div class="cs-wrap">
|
||||
|
||||
<!-- ─── بنر آمار ─── -->
|
||||
<div class="cs-banner">
|
||||
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
||||
|
||||
<!-- آمار اصلی: ردیف بالا -->
|
||||
<div class="cs-stats-main">
|
||||
<div class="cs-stat cs-stat--neutral">
|
||||
<div class="cs-stat__num">
|
||||
<template v-if="status">{{ status.panel_total }}</template>
|
||||
<a-spin v-else size="small" />
|
||||
</div>
|
||||
<div class="cs-stat__label">مشتریان پنل</div>
|
||||
</div>
|
||||
<div class="cs-sep-arrows">
|
||||
<Icon icon="vuesax-linear:arrow-left" :size="16" />
|
||||
<Icon icon="vuesax-linear:arrow-right" :size="16" />
|
||||
</div>
|
||||
<div class="cs-stat cs-stat--zarin">
|
||||
<div class="cs-stat__num">
|
||||
<template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template>
|
||||
<a-spin v-else-if="loadingZarin" size="small" />
|
||||
<span v-else class="cs-stat__hint">تب دوم</span>
|
||||
</div>
|
||||
<div class="cs-stat__label">مشتریان زرین</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- آمار تفاوت: ردیف پایین -->
|
||||
<div class="cs-stats-diff">
|
||||
<div class="cs-stat cs-stat--amber">
|
||||
<div class="cs-stat__num">
|
||||
<template v-if="status">{{ status.panel_only }}</template>
|
||||
<a-spin v-else-if="loadingPanel" size="small" />
|
||||
<span v-else>—</span>
|
||||
</div>
|
||||
<div class="cs-stat__label">پنل — بدون زرین</div>
|
||||
</div>
|
||||
<div class="cs-stat cs-stat--sky">
|
||||
<div class="cs-stat__num">
|
||||
<template v-if="status?.zarin_only_count != null">{{ status.zarin_only_count }}</template>
|
||||
<a-spin v-else-if="loadingZarin" size="small" />
|
||||
<span v-else class="cs-stat__hint">تب دوم</span>
|
||||
</div>
|
||||
<div class="cs-stat__label">زرین — بدون پنل</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- progress -->
|
||||
<div v-if="status" class="cs-progress">
|
||||
<div class="cs-progress__labels">
|
||||
<span>{{ registeredPercent }}٪ پنل در زرین</span>
|
||||
<span>{{ status.panel_registered }} / {{ status.panel_total }}</span>
|
||||
</div>
|
||||
<div class="cs-track"><div class="cs-track__fill" :style="{ width: registeredPercent + '%' }" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── تبها ─── -->
|
||||
<div class="cs-tabs">
|
||||
<button
|
||||
class="cs-tab"
|
||||
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
||||
@click="onTabChange('panel')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:arrow-left" :size="14" />
|
||||
پنل ← زرین
|
||||
<span v-if="status?.panel_only" class="cs-tab__badge cs-tab__badge--amber">{{ status.panel_only }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="cs-tab"
|
||||
:class="{ 'cs-tab--active': activeTab === 'zarin' }"
|
||||
@click="onTabChange('zarin')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:arrow-right" :size="14" />
|
||||
زرین ← پنل
|
||||
<span v-if="status?.zarin_only_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.zarin_only_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── تب ۱: پنل → زرین ─── -->
|
||||
<div v-show="activeTab === 'panel'" class="cs-card">
|
||||
<div class="cs-card__head">
|
||||
<div class="cs-card__head-right">
|
||||
<span class="cs-dot cs-dot--amber" />
|
||||
<span class="cs-card__head-title">مشتریان پنل بدون کد زرین</span>
|
||||
</div>
|
||||
<div class="cs-card__head-actions">
|
||||
<a-input
|
||||
v-model:value="panelSearch"
|
||||
placeholder="جستجو..."
|
||||
allow-clear
|
||||
size="small"
|
||||
class="cs-search"
|
||||
@change="onPanelSearch"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="syncingPanel"
|
||||
:disabled="!status?.panel_only"
|
||||
@click="syncAllPanel"
|
||||
>
|
||||
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
||||
ثبت همه در زرین
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncingPanel || panelSyncResult" class="cs-result">
|
||||
<template v-if="syncingPanel">
|
||||
<a-spin size="small" />
|
||||
<span>در حال ثبت مشتریان پنل در زرین...</span>
|
||||
</template>
|
||||
<template v-else-if="panelSyncResult">
|
||||
<span class="cs-result__ok">✓ {{ panelSyncResult.success }} موفق</span>
|
||||
<span v-if="panelSyncResult.failed" class="cs-result__fail">✗ {{ panelSyncResult.failed }} ناموفق</span>
|
||||
<a-button v-if="panelSyncResult.failed" type="link" size="small" @click="showPanelErrors = !showPanelErrors">
|
||||
{{ showPanelErrors ? 'پنهان' : 'جزئیات' }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="panelSyncResult = null">بستن</a-button>
|
||||
</template>
|
||||
<div v-if="showPanelErrors && panelSyncResult?.errors.length" class="cs-result__errors">
|
||||
<div v-for="e in panelSyncResult.errors" :key="e.mobile"><b>{{ e.name }}</b> — {{ e.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingPanel" class="cs-loading"><a-spin /></div>
|
||||
<template v-else-if="panelList.length">
|
||||
<!-- موبایل -->
|
||||
<div class="cs-mobile-list">
|
||||
<div
|
||||
v-for="c in panelList" :key="c.id"
|
||||
class="cs-mobile-row"
|
||||
:class="{ 'cs-mobile-row--done': panelDone.has(c.id) }"
|
||||
>
|
||||
<div class="cs-mobile-row__info">
|
||||
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||
<div class="cs-mobile-row__mobile">{{ c.mobile }}</div>
|
||||
</div>
|
||||
<span v-if="panelDone.has(c.id)" class="cs-done-badge">✓ ثبت شد</span>
|
||||
<a-button v-else type="primary" ghost size="small"
|
||||
:loading="panelRegisteringId === c.id" @click="registerOnePanel(c)">
|
||||
ارسال به زرین
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- دسکتاپ -->
|
||||
<table class="cs-table">
|
||||
<thead><tr>
|
||||
<th>#</th><th>نام مشتری</th><th>موبایل</th><th>تاریخ عضویت</th><th>عملیات</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(c, idx) in panelList" :key="c.id"
|
||||
:class="{ 'cs-table__row--done': panelDone.has(c.id) }">
|
||||
<td class="cs-table__num">{{ (panelPagination.page - 1) * 20 + idx + 1 }}</td>
|
||||
<td class="cs-table__name">{{ c.name }}</td>
|
||||
<td class="cs-table__mobile">{{ c.mobile }}</td>
|
||||
<td class="cs-table__date">{{ toJalali(c.created_at) }}</td>
|
||||
<td class="cs-table__action">
|
||||
<span v-if="panelDone.has(c.id)" class="cs-done-badge">✓ ثبت شد</span>
|
||||
<a-button v-else type="primary" ghost size="small"
|
||||
:loading="panelRegisteringId === c.id" @click="registerOnePanel(c)">
|
||||
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
||||
ارسال به زرین
|
||||
</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="panelPagination.lastPage > 1" class="cs-pagination">
|
||||
<a-pagination
|
||||
v-model:current="panelPagination.page"
|
||||
:total="panelPagination.total"
|
||||
:page-size="20" size="small" show-less-items
|
||||
@change="p => loadPanel(p)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="cs-empty">
|
||||
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div>
|
||||
<div class="cs-empty__title">همه مشتریان پنل در زرین ثبت شدهاند</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── تب ۲: زرین → پنل ─── -->
|
||||
<div v-show="activeTab === 'zarin'" class="cs-card">
|
||||
<div class="cs-card__head">
|
||||
<div class="cs-card__head-right">
|
||||
<span class="cs-dot cs-dot--sky" />
|
||||
<span class="cs-card__head-title">مشتریان زرین بدون حساب پنل</span>
|
||||
</div>
|
||||
<div class="cs-card__head-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="syncingZarin"
|
||||
:disabled="!status?.zarin_only_count || selectedZarin.size === 0"
|
||||
@click="importSelected"
|
||||
>
|
||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||
وارد کردن انتخابشدهها ({{ selectedZarin.size }})
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:loading="importStore.active"
|
||||
:disabled="!zarinOnlyList.length"
|
||||
@click="importAll"
|
||||
>
|
||||
{{ importStore.active ? 'در حال وارد کردن…' : `وارد کردن همه (${status?.zarin_only_count ?? 0})` }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncingZarin || zarinSyncResult" class="cs-result">
|
||||
<template v-if="syncingZarin">
|
||||
<a-spin size="small" />
|
||||
<span>در حال وارد کردن مشتریان زرین به پنل...</span>
|
||||
</template>
|
||||
<template v-else-if="zarinSyncResult">
|
||||
<span class="cs-result__ok">✓ {{ zarinSyncResult.success }} موفق</span>
|
||||
<span v-if="zarinSyncResult.failed" class="cs-result__fail">✗ {{ zarinSyncResult.failed }} ناموفق</span>
|
||||
<a-button v-if="zarinSyncResult.failed" type="link" size="small" @click="showZarinErrors = !showZarinErrors">
|
||||
{{ showZarinErrors ? 'پنهان' : 'جزئیات' }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="zarinSyncResult = null">بستن</a-button>
|
||||
</template>
|
||||
<div v-if="showZarinErrors && zarinSyncResult?.errors.length" class="cs-result__errors">
|
||||
<div v-for="e in zarinSyncResult.errors" :key="e.name"><b>{{ e.name }}</b> — {{ e.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingZarin" class="cs-loading"><a-spin /></div>
|
||||
<template v-else-if="zarinOnlyList.length">
|
||||
<!-- موبایل -->
|
||||
<div class="cs-mobile-list">
|
||||
<div
|
||||
v-for="c in pagedZarin" :key="c.code"
|
||||
class="cs-mobile-row"
|
||||
:class="{ 'cs-mobile-row--done': zarinDone.has(c.code) }"
|
||||
>
|
||||
<a-checkbox
|
||||
:checked="selectedZarin.has(c.code)"
|
||||
:disabled="zarinDone.has(c.code)"
|
||||
class="cs-mobile-row__check"
|
||||
@change="toggleZarin(c.code)"
|
||||
/>
|
||||
<div class="cs-mobile-row__info">
|
||||
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||
<div class="cs-mobile-row__mobile">{{ c.tel || '—' }} · کد {{ c.code }}</div>
|
||||
</div>
|
||||
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||
<a-button v-else type="primary" ghost size="small"
|
||||
@click="importOne(c)">
|
||||
وارد کردن
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- دسکتاپ -->
|
||||
<table class="cs-table">
|
||||
<thead><tr>
|
||||
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
||||
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>کد زرین</th><th>گروه</th><th>عملیات</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(c, idx) in pagedZarin" :key="c.code"
|
||||
:class="{ 'cs-table__row--done': zarinDone.has(c.code) }">
|
||||
<td>
|
||||
<a-checkbox
|
||||
:checked="selectedZarin.has(c.code)"
|
||||
:disabled="zarinDone.has(c.code)"
|
||||
@change="toggleZarin(c.code)"
|
||||
/>
|
||||
</td>
|
||||
<td class="cs-table__num">{{ (zarinPage - 1) * 20 + idx + 1 }}</td>
|
||||
<td class="cs-table__name">{{ c.name }}</td>
|
||||
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
||||
<td class="cs-table__mobile">{{ c.code }}</td>
|
||||
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
||||
<td class="cs-table__action">
|
||||
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||
وارد کردن
|
||||
</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="zarinLastPage > 1" class="cs-pagination">
|
||||
<a-pagination
|
||||
v-model:current="zarinPage"
|
||||
:total="zarinOnlyList.length"
|
||||
:page-size="20" size="small" show-less-items
|
||||
@change="p => { zarinPage = p; selectedZarin.clear() }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="cs-empty">
|
||||
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div>
|
||||
<div class="cs-empty__title">همه مشتریان زرین در پنل ثبت شدهاند</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import {
|
||||
getZarinCustomerSyncStatus,
|
||||
syncPanelToZarin,
|
||||
importFromZarin,
|
||||
registerCustomerInZarin,
|
||||
type CustomerSyncStatus,
|
||||
type PanelOnlyCustomer,
|
||||
type ZarinOnlyCustomer,
|
||||
type SyncPanelResult,
|
||||
} from '@/api/zarin'
|
||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
||||
|
||||
const importStore = useCustomerImportStore()
|
||||
|
||||
// ─── state ───
|
||||
const loadingPanel = ref(true) // شروع با true تا اولین load کامل بشه
|
||||
const loadingZarin = ref(false)
|
||||
const syncingPanel = ref(false)
|
||||
const syncingZarin = ref(false)
|
||||
const activeTab = ref<'panel' | 'zarin'>('panel')
|
||||
const zarinFetched = ref(false) // آیا مشتریان زرین قبلاً fetch شده؟
|
||||
|
||||
const status = ref<CustomerSyncStatus | null>(null)
|
||||
|
||||
// تب ۱
|
||||
const panelSearch = ref('')
|
||||
const panelList = ref<PanelOnlyCustomer[]>([])
|
||||
const panelPagination = ref({ page: 1, lastPage: 1, total: 0 })
|
||||
const panelDone = reactive(new Set<number>())
|
||||
const panelRegisteringId = ref<number | null>(null)
|
||||
const panelSyncResult = ref<SyncPanelResult | null>(null)
|
||||
const showPanelErrors = ref(false)
|
||||
|
||||
// تب ۲
|
||||
const zarinOnlyList = ref<ZarinOnlyCustomer[]>([])
|
||||
const zarinPage = ref(1)
|
||||
const zarinDone = reactive(new Set<number>())
|
||||
const selectedZarin = reactive(new Set<number>())
|
||||
const zarinSyncResult = ref<SyncPanelResult | null>(null)
|
||||
const showZarinErrors = ref(false)
|
||||
|
||||
// ─── computed ───
|
||||
const registeredPercent = computed(() => {
|
||||
if (!status.value?.panel_total) return 0
|
||||
return Math.round((status.value.panel_registered / status.value.panel_total) * 100)
|
||||
})
|
||||
|
||||
const zarinLastPage = computed(() => Math.max(1, Math.ceil(zarinOnlyList.value.length / 20)))
|
||||
|
||||
const pagedZarin = computed(() => {
|
||||
const s = (zarinPage.value - 1) * 20
|
||||
return zarinOnlyList.value.slice(s, s + 20)
|
||||
})
|
||||
|
||||
const allPageSelected = computed(() =>
|
||||
pagedZarin.value.length > 0 &&
|
||||
pagedZarin.value.every(c => selectedZarin.has(c.code) || zarinDone.has(c.code))
|
||||
)
|
||||
const someSelected = computed(() =>
|
||||
pagedZarin.value.some(c => selectedZarin.has(c.code)) && !allPageSelected.value
|
||||
)
|
||||
|
||||
// ─── load panel only (سریع — بدون API زرین) ───
|
||||
let panelSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function load(panelPage = 1) {
|
||||
loadingPanel.value = true
|
||||
try {
|
||||
const res = await getZarinCustomerSyncStatus({
|
||||
search: panelSearch.value || undefined,
|
||||
page: panelPage,
|
||||
})
|
||||
// این درخواست دادههای زرین را شامل نمیشود؛ پس فیلدهای زرین را دستنخورده نگه میداریم
|
||||
if (status.value) {
|
||||
status.value.panel_total = res.panel_total
|
||||
status.value.panel_registered = res.panel_registered
|
||||
status.value.panel_only = res.panel_only
|
||||
status.value.panel_only_list = res.panel_only_list
|
||||
} else {
|
||||
status.value = res
|
||||
}
|
||||
panelList.value = res.panel_only_list.data
|
||||
panelPagination.value = {
|
||||
page: res.panel_only_list.current_page,
|
||||
lastPage: res.panel_only_list.last_page,
|
||||
total: res.panel_only_list.total,
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loadingPanel.value = false }
|
||||
}
|
||||
|
||||
// ─── load zarin customers (کند — فقط وقتی تب زرین باز میشه) ───
|
||||
async function loadZarin() {
|
||||
if (zarinFetched.value) return
|
||||
loadingZarin.value = true
|
||||
try {
|
||||
const res = await getZarinCustomerSyncStatus({ include_zarin: 1 } as any)
|
||||
if (status.value) {
|
||||
status.value.zarin_total = res.zarin_total ?? 0
|
||||
status.value.zarin_only_count = res.zarin_only_count ?? 0
|
||||
} else {
|
||||
status.value = res
|
||||
}
|
||||
zarinOnlyList.value = res.zarin_only_list?.data ?? []
|
||||
zarinFetched.value = true
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loadingZarin.value = false }
|
||||
}
|
||||
|
||||
function onTabChange(tab: 'panel' | 'zarin') {
|
||||
activeTab.value = tab
|
||||
if (tab === 'zarin') loadZarin()
|
||||
}
|
||||
|
||||
async function loadPanel(page = 1) {
|
||||
loadingPanel.value = true
|
||||
try {
|
||||
const res = await getZarinCustomerSyncStatus({ search: panelSearch.value || undefined, page })
|
||||
if (status.value) {
|
||||
status.value.panel_total = res.panel_total
|
||||
status.value.panel_registered = res.panel_registered
|
||||
status.value.panel_only = res.panel_only
|
||||
}
|
||||
panelList.value = res.panel_only_list.data
|
||||
panelPagination.value = {
|
||||
page: res.panel_only_list.current_page,
|
||||
lastPage: res.panel_only_list.last_page,
|
||||
total: res.panel_only_list.total,
|
||||
}
|
||||
} catch { } finally { loadingPanel.value = false }
|
||||
}
|
||||
|
||||
function onPanelSearch() {
|
||||
if (panelSearchTimer) clearTimeout(panelSearchTimer)
|
||||
panelSearchTimer = setTimeout(() => loadPanel(1), 400)
|
||||
}
|
||||
|
||||
// ─── تب ۱: عملیات ───
|
||||
async function registerOnePanel(customer: PanelOnlyCustomer) {
|
||||
panelRegisteringId.value = customer.id
|
||||
try {
|
||||
const res = await registerCustomerInZarin(customer.id)
|
||||
if (res.ok) {
|
||||
panelDone.add(customer.id)
|
||||
if (status.value) { status.value.panel_registered++; status.value.panel_only-- }
|
||||
message.success(`${customer.name} در زرین ثبت شد`)
|
||||
} else {
|
||||
message.error((res as any).message || 'ثبت ناموفق بود')
|
||||
}
|
||||
} catch { message.error('خطا در ارتباط با سرور') }
|
||||
finally { panelRegisteringId.value = null }
|
||||
}
|
||||
|
||||
async function syncAllPanel() {
|
||||
syncingPanel.value = true
|
||||
panelSyncResult.value = null
|
||||
showPanelErrors.value = false
|
||||
try {
|
||||
const res = await syncPanelToZarin()
|
||||
panelSyncResult.value = res
|
||||
if (res.success) message.success(`${res.success} مشتری ثبت شد`)
|
||||
if (res.failed) message.warning(`${res.failed} مشتری ثبت نشد`)
|
||||
await load(1)
|
||||
panelDone.clear()
|
||||
} catch { message.error('خطا در ارتباط با سرور') }
|
||||
finally { syncingPanel.value = false }
|
||||
}
|
||||
|
||||
// ─── تب ۲: عملیات ───
|
||||
function toggleZarin(code: number) {
|
||||
if (selectedZarin.has(code)) selectedZarin.delete(code)
|
||||
else selectedZarin.add(code)
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
if (allPageSelected.value) {
|
||||
pagedZarin.value.forEach(c => selectedZarin.delete(c.code))
|
||||
} else {
|
||||
pagedZarin.value.forEach(c => { if (!zarinDone.has(c.code)) selectedZarin.add(c.code) })
|
||||
}
|
||||
}
|
||||
|
||||
async function doImport(customers: ZarinOnlyCustomer[]) {
|
||||
syncingZarin.value = true
|
||||
zarinSyncResult.value = null
|
||||
showZarinErrors.value = false
|
||||
try {
|
||||
const res = await importFromZarin(customers)
|
||||
zarinSyncResult.value = res
|
||||
if (res.success) {
|
||||
message.success(`${res.success} مشتری وارد پنل شد`)
|
||||
customers.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) zarinDone.add(c.code) })
|
||||
if (status.value?.zarin_only_count != null) {
|
||||
status.value.zarin_only_count = Math.max(0, status.value.zarin_only_count - res.success)
|
||||
}
|
||||
}
|
||||
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
||||
await loadPanel(panelPagination.value.page)
|
||||
selectedZarin.clear()
|
||||
} catch { message.error('خطا در ارتباط با سرور') }
|
||||
finally { syncingZarin.value = false }
|
||||
}
|
||||
|
||||
async function importOne(customer: ZarinOnlyCustomer) { await doImport([customer]) }
|
||||
async function importSelected() { await doImport(zarinOnlyList.value.filter(c => selectedZarin.has(c.code))) }
|
||||
|
||||
// «وارد کردن همه» با پروگرسبار سراسری و قابل مینیمایز (پسزمینهای)
|
||||
async function importAll() {
|
||||
const pending = zarinOnlyList.value.filter(c => !zarinDone.has(c.code))
|
||||
if (!pending.length) return
|
||||
if (importStore.active) {
|
||||
importStore.expand()
|
||||
return
|
||||
}
|
||||
await importStore.start(pending, 'zarin')
|
||||
}
|
||||
|
||||
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
||||
watch(() => importStore.doneCodes.length, () => {
|
||||
importStore.doneCodes.forEach(code => zarinDone.add(code))
|
||||
})
|
||||
|
||||
// پس از پایان کامل، آمار و لیست را تازه کن
|
||||
watch(() => importStore.status, async (s, prev) => {
|
||||
if (prev === 'running' && (s === 'done' || s === 'canceled')) {
|
||||
selectedZarin.clear()
|
||||
if (status.value?.zarin_only_count != null) {
|
||||
status.value.zarin_only_count = Math.max(0, status.value.zarin_only_count - importStore.success)
|
||||
}
|
||||
await loadPanel(panelPagination.value.page)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
load(1)
|
||||
// اگر عملیات سراسری در حال اجراست، ردیفهای انجامشده را همگام کن
|
||||
importStore.doneCodes.forEach(code => zarinDone.add(code))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.cs-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 8px 4px 24px;
|
||||
}
|
||||
|
||||
/* ─── بنر ─── */
|
||||
.cs-banner {
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 55%, #0ea5e9 100%);
|
||||
padding: 14px 16px;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
||||
|
||||
&__title { font-size: 14px; font-weight: 700; }
|
||||
}
|
||||
|
||||
/* ردیف اصلی: پنل ↔ زرین */
|
||||
.cs-stats-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ردیف تفاوت */
|
||||
.cs-stats-diff {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cs-sep-arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: rgba(255,255,255,0.4);
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cs-stat {
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
|
||||
&--neutral { background: rgba(255,255,255,0.12); }
|
||||
&--zarin { background: rgba(14,165,233,0.25); }
|
||||
&--amber { background: rgba(251,191,36,0.2); }
|
||||
&--sky { background: rgba(14,165,233,0.2); }
|
||||
|
||||
&__num {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
min-height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (min-width: 640px) { font-size: 24px; }
|
||||
}
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.65);
|
||||
margin-top: 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&__hint { font-size: 10px; color: rgba(255,255,255,0.4); font-weight: 400; }
|
||||
}
|
||||
|
||||
/* progress */
|
||||
.cs-progress {
|
||||
&__labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.6);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-track {
|
||||
height: 7px;
|
||||
border-radius: 99px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
overflow: hidden;
|
||||
|
||||
&__fill {
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
background: #34d399;
|
||||
transition: width 0.7s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── تبها ─── */
|
||||
.cs-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--admin-border);
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
.cs-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (min-width: 640px) { padding: 8px 16px; font-size: 13px; }
|
||||
|
||||
&:hover { color: var(--admin-text); }
|
||||
|
||||
&--active {
|
||||
color: #7c3aed;
|
||||
border-bottom-color: #7c3aed;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 99px;
|
||||
|
||||
&--amber { background: rgba(245,158,11,0.15); color: #d97706; }
|
||||
&--sky { background: rgba(14,165,233,0.15); color: #0ea5e9; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── کارت ─── */
|
||||
.cs-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
background: var(--admin-bg-surface);
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
background: var(--admin-bg-muted);
|
||||
|
||||
@media (min-width: 640px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
&__head-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__head-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
&__head-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.ant-btn { flex-shrink: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
/* روی موبایل دکمههای head رو کوچیکتر نشون بده */
|
||||
@media (max-width: 639px) {
|
||||
.cs-card__head-title { font-size: 12px; }
|
||||
}
|
||||
|
||||
.cs-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--amber { background: #f59e0b; }
|
||||
&--sky { background: #0ea5e9; }
|
||||
}
|
||||
|
||||
.cs-search { width: 150px; }
|
||||
@media (max-width: 639px) { .cs-search { flex: 1; min-width: 0; } }
|
||||
|
||||
/* ─── نتیجه sync ─── */
|
||||
.cs-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
background: var(--admin-bg-muted);
|
||||
font-size: 13px;
|
||||
color: var(--admin-text);
|
||||
|
||||
&__ok { background: rgba(52,211,153,0.15); color: #059669; font-weight: 600; border-radius: 99px; padding: 2px 10px; font-size: 12px; }
|
||||
&__fail { background: rgba(239,68,68,0.12); color: #dc2626; font-weight: 600; border-radius: 99px; padding: 2px 10px; font-size: 12px; }
|
||||
&__errors { width: 100%; margin-top: 4px; max-height: 100px; overflow-y: auto; border-radius: 8px; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); padding: 8px 12px; font-size: 11px; color: #dc2626; line-height: 1.8; }
|
||||
}
|
||||
|
||||
.cs-loading { display: flex; justify-content: center; padding: 48px; }
|
||||
|
||||
/* ─── موبایل ─── */
|
||||
.cs-mobile-list {
|
||||
display: block;
|
||||
@media (min-width: 640px) { display: none; }
|
||||
}
|
||||
|
||||
.cs-mobile-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&--done { opacity: 0.45; }
|
||||
|
||||
&__check { flex-shrink: 0; }
|
||||
&__info { flex: 1; min-width: 0; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: var(--admin-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
&__mobile{ font-size: 11px; color: var(--admin-text-secondary); font-family: monospace; margin-top: 2px; }
|
||||
}
|
||||
|
||||
/* ─── جدول دسکتاپ ─── */
|
||||
.cs-table {
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
@media (min-width: 640px) { display: table; }
|
||||
|
||||
thead tr { background: var(--admin-bg-muted); border-bottom: 1px solid var(--admin-border); }
|
||||
th { padding: 10px 14px; text-align: right; font-size: 11px; font-weight: 600; color: var(--admin-text-secondary); white-space: nowrap; }
|
||||
tbody tr { border-bottom: 1px solid var(--admin-border); transition: background 0.15s; }
|
||||
tbody tr:hover { background: var(--admin-bg-hover); }
|
||||
tbody tr.cs-table__row--done { opacity: 0.45; }
|
||||
td { padding: 10px 14px; color: var(--admin-text); }
|
||||
|
||||
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
||||
&__name { font-weight: 600; }
|
||||
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
||||
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
||||
&__action { text-align: center; white-space: nowrap; }
|
||||
}
|
||||
|
||||
.cs-done-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(52,211,153,0.15);
|
||||
color: #059669;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 10px;
|
||||
border-radius: 99px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cs-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 52px 16px;
|
||||
gap: 8px;
|
||||
|
||||
&__icon { width: 52px; height: 52px; border-radius: 50%; background: rgba(52,211,153,0.12); color: #059669; display: flex; align-items: center; justify-content: center; }
|
||||
&__title { font-size: 14px; font-weight: 700; color: var(--admin-text); }
|
||||
}
|
||||
|
||||
.cs-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--admin-border);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<MobileCardList
|
||||
v-model:search="search"
|
||||
:loading="loading"
|
||||
:has-more="hasMore"
|
||||
:show-empty="!loading && items.length === 0"
|
||||
empty-text="سندی یافت نشد"
|
||||
@search="reload"
|
||||
@load-more="loadMore"
|
||||
>
|
||||
<ZarinMobileListItem
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@open="openSheet"
|
||||
/>
|
||||
</MobileCardList>
|
||||
|
||||
<ZarinMobileDetailSheet
|
||||
v-model:visible="sheetVisible"
|
||||
:item="selectedItem"
|
||||
@retry="openRetry"
|
||||
@register-customer="openRegister"
|
||||
/>
|
||||
|
||||
<ZarinRetryModal
|
||||
v-model="retryVisible"
|
||||
mode="retry-sanad"
|
||||
:zarin-id="selectedItem?.id"
|
||||
@success="onRetrySuccess"
|
||||
/>
|
||||
|
||||
<ZarinRetryModal
|
||||
v-model="registerVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="selectedItem?.customer_id"
|
||||
:customer-name="selectedItem?.customer?.name"
|
||||
@success="onRetrySuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getZarinTable, type ZarinRecord } from '@/api/zarin'
|
||||
import MobileCardList from '@/components/mobile/MobileCardList.vue'
|
||||
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
|
||||
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import ZarinMobileDetailSheet from './ZarinMobileDetailSheet.vue'
|
||||
import ZarinMobileListItem from './ZarinMobileListItem.vue'
|
||||
|
||||
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<ZarinRecord>()
|
||||
const retryVisible = ref(false)
|
||||
const registerVisible = ref(false)
|
||||
|
||||
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<ZarinRecord>({
|
||||
fetchApi: getZarinTable,
|
||||
onFetched: (rows) => syncSelectedItem(rows)
|
||||
})
|
||||
|
||||
function openRetry(item: ZarinRecord) {
|
||||
selectedItem.value = item
|
||||
sheetVisible.value = false
|
||||
retryVisible.value = true
|
||||
}
|
||||
|
||||
function openRegister(item: ZarinRecord) {
|
||||
selectedItem.value = item
|
||||
sheetVisible.value = false
|
||||
registerVisible.value = true
|
||||
}
|
||||
|
||||
async function onRetrySuccess() {
|
||||
await fetch()
|
||||
}
|
||||
|
||||
onMounted(() => reload())
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
||||
<template #default="{ item: row }">
|
||||
<MobileSheetHeader :title="sheetTitle" :show-title="isMobile">
|
||||
<template #actions>
|
||||
<MobileIconButton
|
||||
v-if="!row.zarin_id && !row.customer?.moshtari_code"
|
||||
plain
|
||||
icon="vuesax-linear:profile-add"
|
||||
color="#7C3AED"
|
||||
:size="22"
|
||||
@click="$emit('register-customer', row)"
|
||||
/>
|
||||
<MobileIconButton
|
||||
v-if="canRetry(row)"
|
||||
plain
|
||||
icon="vuesax-linear:refresh-circle"
|
||||
color="#CA8A04"
|
||||
:size="22"
|
||||
@click="$emit('retry', row)"
|
||||
/>
|
||||
</template>
|
||||
</MobileSheetHeader>
|
||||
|
||||
<MobileInfoGrid>
|
||||
<MobileDetailRow label="شناسه" :value="row.id" />
|
||||
<MobileDetailRow label="مشتری" :value="row.customer?.name || '-'" />
|
||||
<MobileDetailRow label="موبایل" :value="row.customer?.mobile || '-'" />
|
||||
<MobileDetailRow label="نوع" :value="row.type || '-'" />
|
||||
<MobileDetailRow label="مقدار" :value="row.amount ?? '-'" />
|
||||
<MobileDetailRow label="شرح" full-width :value="row.desc || '-'" />
|
||||
<MobileDetailRow label="وضعیت" :value="zarinStatusLabel(row.sync_status, row.zarin_id)" />
|
||||
<MobileDetailRow label="کد زرین" :value="row.zarin_id || '-'" />
|
||||
<MobileDetailRow label="زمان" :value="row.created_at ? toJalali(row.created_at) : '-'" />
|
||||
<MobileDetailRow v-if="row.res" label="پاسخ API" full-width :value="row.res" />
|
||||
</MobileInfoGrid>
|
||||
|
||||
<div
|
||||
v-if="row.sync_log"
|
||||
class="mt-3 rounded-lg bg-red-50 p-3 text-sm text-red-700 whitespace-pre-wrap"
|
||||
>
|
||||
{{ row.sync_log }}
|
||||
</div>
|
||||
</template>
|
||||
</MobileDetailSheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZarinRecord } from '@/api/zarin'
|
||||
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
||||
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
||||
import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
|
||||
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
|
||||
import { canRetryZarin, zarinStatusLabel } from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
item: ZarinRecord | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'retry', item: ZarinRecord): void
|
||||
(e: 'register-customer', item: ZarinRecord): void
|
||||
}>()
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (value: boolean) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const sheetTitle = computed(() => props.item?.customer?.name || 'سند زرین')
|
||||
|
||||
function canRetry(row: ZarinRecord) {
|
||||
return canRetryZarin(row)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<MobileCard clickable @click="$emit('open', item)">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="truncate text-sm font-700 text-gray-900">{{ customerName }}</div>
|
||||
<span class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-600" :class="statusClass">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-600">
|
||||
<span>نوع: <span class="font-600 text-gray-900">{{ item.type || '-' }}</span></span>
|
||||
<span>مقدار: <span class="font-600 text-gray-900">{{ item.amount ?? '-' }}</span></span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
|
||||
</div>
|
||||
</MobileCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZarinRecord } from '@/api/zarin'
|
||||
import MobileCard from '@/components/mobile/MobileCard.vue'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import { zarinStatusLabel } from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ZarinRecord
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open', item: ZarinRecord): void
|
||||
}>()
|
||||
|
||||
const customerName = computed(() => props.item.customer?.name || '-')
|
||||
const createdAtLabel = computed(() =>
|
||||
props.item.created_at ? toJalali(props.item.created_at) : '-'
|
||||
)
|
||||
const statusText = computed(() =>
|
||||
zarinStatusLabel(props.item.sync_status, props.item.zarin_id)
|
||||
)
|
||||
const statusClass = computed(() => {
|
||||
const text = statusText.value
|
||||
if (text === 'موفق') return 'bg-green-100 text-green-700'
|
||||
if (text === 'ناموفق') return 'bg-red-100 text-red-700'
|
||||
return 'bg-amber-100 text-amber-700'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="اسناد زرین">
|
||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||
<template #title><span>اطلاعات سند</span></template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۲: ثبت مشتری در زرین (فقط اگر سند هنوز ثبت نشده و مشتری کد ندارد) -->
|
||||
<a-tooltip
|
||||
v-if="!record.zarin_id && !record.customer?.moshtari_code"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openRegister(record)"
|
||||
>
|
||||
<template #title><span>ثبت مشتری در زرین</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۳: ارسال به زرین (فقط اگر هنوز در زرین ثبت نشده) -->
|
||||
<a-tooltip
|
||||
v-if="!record.zarin_id"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openSend(record)"
|
||||
>
|
||||
<template #title><span>ارسال به زرین</span></template>
|
||||
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
<ZarinMobileCardList v-else ref="mobileListRef" />
|
||||
</BasicShowContext>
|
||||
|
||||
<!-- مودال اطلاعات کلی -->
|
||||
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div
|
||||
v-for="item in infoItems"
|
||||
:key="item.label"
|
||||
class="flex justify-between gap-3 border-b border-gray-100 py-2"
|
||||
>
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
||||
</div>
|
||||
<div v-if="selected?.res" class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap">
|
||||
{{ selected.res }}
|
||||
</div>
|
||||
<div v-if="selected?.sync_log" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">
|
||||
{{ selected.sync_log }}
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
|
||||
<!-- مودال ارسال به زرین -->
|
||||
<ZarinRetryModal
|
||||
v-model="sendVisible"
|
||||
mode="retry-sanad"
|
||||
:zarin-id="selected?.id"
|
||||
@success="fetch"
|
||||
/>
|
||||
|
||||
<!-- مودال ثبت مشتری در زرین -->
|
||||
<ZarinRetryModal
|
||||
v-model="registerVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="selected?.customer_id"
|
||||
:customer-name="selected?.customer?.name"
|
||||
@success="fetch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import { getZarinTable, type ZarinRecord } from '@/api/zarin'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import ZarinMobileCardList from '@/pages/menuPages/accounting/components/ZarinMobileCardList.vue'
|
||||
import { zarinStatusLabel } from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const infoVisible = ref(false)
|
||||
const sendVisible = ref(false)
|
||||
const registerVisible = ref(false)
|
||||
const selected = ref<ZarinRecord | null>(null)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => r.index + 1
|
||||
},
|
||||
{ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' },
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||
{ title: 'مقدار', dataIndex: 'amount', key: 'amount', type: 'Number' },
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', type: 'Text' },
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'sync_status',
|
||||
key: 'sync_status',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: (r: any) => zarinStatusLabel(r.record.sync_status, r.record.zarin_id)
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'Text',
|
||||
customRender: (r: any) => toJalali(r.record.created_at)
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false
|
||||
}
|
||||
]
|
||||
|
||||
const tblProps = ref({
|
||||
columns,
|
||||
api: (params: TableInput) => getZarinTable(params)
|
||||
})
|
||||
|
||||
/** اطلاعاتی که برای هر سند حسابداری نیاز است (بدون جزئیات زرینمحور) */
|
||||
const infoItems = computed(() => {
|
||||
const row = selected.value
|
||||
if (!row) return []
|
||||
return [
|
||||
{ label: 'شناسه سند', value: row.id },
|
||||
{ label: 'مشتری', value: row.customer?.name },
|
||||
{ label: 'موبایل', value: row.customer?.mobile },
|
||||
{ label: 'نوع', value: row.type },
|
||||
{ label: 'مقدار', value: row.amount },
|
||||
{ label: 'شرح', value: row.desc },
|
||||
{ label: 'زمان ثبت', value: row.created_at ? toJalali(row.created_at) : '-' },
|
||||
{ label: 'وضعیت', value: zarinStatusLabel(row.sync_status, row.zarin_id) },
|
||||
{ label: 'کد زرین', value: row.zarin_id || 'ثبت نشده' }
|
||||
]
|
||||
})
|
||||
|
||||
function openInfo(record: ZarinRecord) {
|
||||
selected.value = record
|
||||
infoVisible.value = true
|
||||
}
|
||||
|
||||
function openSend(record: ZarinRecord) {
|
||||
selected.value = record
|
||||
sendVisible.value = true
|
||||
}
|
||||
|
||||
function openRegister(record: ZarinRecord) {
|
||||
selected.value = record
|
||||
registerVisible.value = true
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (isMobile.value) {
|
||||
await mobileListRef.value?.fetch?.()
|
||||
return
|
||||
}
|
||||
await reload()
|
||||
}
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -3,18 +3,22 @@
|
||||
<BasicMainCard>
|
||||
<div
|
||||
class="flex h-full w-full min-w-0 max-w-full flex-col overflow-x-hidden"
|
||||
:class="activeTab === 'customer-sync' || activeTab === 'pargar-customer-sync' ? 'overflow-y-auto' : 'overflow-hidden'"
|
||||
:class="activeTab === 'customer-sync' || activeTab === 'pargar-customer-sync' || activeTab === 'zarin-customer-sync' ? 'overflow-y-auto' : 'overflow-hidden'"
|
||||
>
|
||||
<ATabs v-model:activeKey="activeTab" class="mb-3 shrink-0">
|
||||
<ATabPane key="reports" tab="گزارشات" />
|
||||
<ATabPane v-if="tahesabEnabled" key="tahesab" tab="اسناد تهحساب" />
|
||||
<ATabPane v-if="tahesabEnabled" key="customer-sync" tab="همگام سازی مشتریان در تهحساب" />
|
||||
<ATabPane v-if="zarinEnabled" key="zarin" tab="اسناد زرین" />
|
||||
<ATabPane v-if="zarinEnabled" key="zarin-customer-sync" tab="همگام سازی مشتریان در زرین" />
|
||||
<ATabPane v-if="pargarEnabled" key="pargar" tab="اسناد پرگار" />
|
||||
<ATabPane v-if="pargarEnabled" key="pargar-customer-sync" tab="همگام سازی مشتریان در پرگار" />
|
||||
</ATabs>
|
||||
<tbl v-if="activeTab === 'reports'" ref="table" class="min-h-0 flex-1" />
|
||||
<tahesabTbl v-else-if="tahesabEnabled && activeTab === 'tahesab'" ref="tahesabTable" class="min-h-0 flex-1" />
|
||||
<customerSync v-else-if="tahesabEnabled && activeTab === 'customer-sync'" class="shrink-0" />
|
||||
<zarinTbl v-else-if="zarinEnabled && activeTab === 'zarin'" ref="zarinTable" class="min-h-0 flex-1" />
|
||||
<zarinCustomerSync v-else-if="zarinEnabled && activeTab === 'zarin-customer-sync'" class="shrink-0" />
|
||||
<pargarTbl v-else-if="pargarEnabled && activeTab === 'pargar'" ref="pargarTable" class="min-h-0 flex-1" />
|
||||
<pargarCustomerSync v-else-if="pargarEnabled && activeTab === 'pargar-customer-sync'" class="shrink-0" />
|
||||
</div>
|
||||
@@ -147,6 +151,8 @@ import { ref } from 'vue'
|
||||
import tbl from './components/table.vue'
|
||||
import tahesabTbl from './components/tahesabTable.vue'
|
||||
import customerSync from './components/TahesabCustomerSync.vue'
|
||||
import zarinTbl from './components/zarinTable.vue'
|
||||
import zarinCustomerSync from './components/ZarinCustomerSync.vue'
|
||||
import pargarTbl from './components/pargarTable.vue'
|
||||
import pargarCustomerSync from './components/PargarCustomerSync.vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
@@ -166,13 +172,14 @@ import type { last } from '@/model/price'
|
||||
import { createOrder } from '@/api/order'
|
||||
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import { initAccountingSettingsFromApi, tahesabEnabledSetting, pargarEnabledSetting } from '@/utils/config'
|
||||
import { initAccountingSettingsFromApi, tahesabEnabledSetting, zarinEnabledSetting, pargarEnabledSetting } from '@/utils/config'
|
||||
|
||||
//loading
|
||||
const loading = ref<boolean>(false)
|
||||
const submitError = ref('')
|
||||
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'pargar' | 'pargar-customer-sync'>('reports')
|
||||
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'zarin' | 'zarin-customer-sync' | 'pargar' | 'pargar-customer-sync'>('reports')
|
||||
const tahesabEnabled = tahesabEnabledSetting
|
||||
const zarinEnabled = zarinEnabledSetting
|
||||
const pargarEnabled = pargarEnabledSetting
|
||||
|
||||
//define store
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function zarinStatusLabel(status?: string, zarinId?: string | null): string {
|
||||
if (zarinId) return 'موفق'
|
||||
if (status === 'failed') return 'ناموفق'
|
||||
if (status === 'success') return 'موفق'
|
||||
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار زرین ثبت نشده (نه «در صف»)
|
||||
return 'ارسال نشده'
|
||||
}
|
||||
|
||||
export function canRetryZarin(record: { zarin_id?: string | null; sync_status?: string }): boolean {
|
||||
return !record.zarin_id || record.sync_status === 'failed'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<a-form :model="form" ref="formRef" layout="vertical" autocomplete="off">
|
||||
<a-form-item label="عنوان" name="title" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.title" placeholder="عنوان اعلان" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="توضیح" name="body" :rules="inputRules()">
|
||||
<a-textarea
|
||||
v-model:value="form.body"
|
||||
:rows="5"
|
||||
placeholder="متن اعلان برای مشتری"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="اهمیت" name="severity" :rules="inputRules()">
|
||||
<BasicSelect
|
||||
v-model:value="form.severity"
|
||||
:items="severityItems"
|
||||
key-name="value"
|
||||
value-name="label"
|
||||
placeholder="انتخاب اهمیت"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="وضعیت" name="is_active">
|
||||
<a-switch
|
||||
v-model:checked="form.is_active"
|
||||
checked-children="فعال"
|
||||
un-checked-children="غیرفعال"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { AppNotification } from '@/model/appNotifications'
|
||||
import { APP_NOTIFICATION_SEVERITIES } from '@/model/appNotifications'
|
||||
import { inputRules } from '@/utils/inputRules'
|
||||
import BasicInput from '@/components/BasicInput.vue'
|
||||
import BasicSelect from '@/components/BasicSelect.vue'
|
||||
|
||||
defineProps<{
|
||||
form: Partial<AppNotification>
|
||||
}>()
|
||||
|
||||
const severityItems = [...APP_NOTIFICATION_SEVERITIES]
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
defineExpose({
|
||||
validateFields: () => formRef.value?.validateFields(),
|
||||
resetFields: () => formRef.value?.resetFields(),
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicShowContext title="اعلانهای اپ">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<AdminFieldHelp
|
||||
text="اعلانهای ثبتشده برای همهٔ مشتریان فعال در اپ نمایش داده میشود. فقط وقتی سوییچ «اعلان داخل اپ» روشن باشد."
|
||||
/>
|
||||
<BaseButton type="primary" size="small" @click="emits('add')">
|
||||
<span>افزودن اعلان</span>
|
||||
<BaseIcon icon="vuesax-linear:add" size="16" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>ویرایش</template>
|
||||
<Icon
|
||||
icon="vuesax-linear:edit"
|
||||
color="#4572E9"
|
||||
class="cursor-pointer"
|
||||
@click="emits('edit', record)"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>{{ record.is_active ? 'غیرفعال کردن' : 'فعال کردن' }}</template>
|
||||
<Icon
|
||||
:icon="record.is_active ? 'vuesax-linear:eye-slash' : 'vuesax-linear:eye'"
|
||||
:color="record.is_active ? '#C53030' : '#25855A'"
|
||||
class="cursor-pointer"
|
||||
@click="emits('toggle', record)"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>حذف</template>
|
||||
<Icon
|
||||
icon="vuesax-linear:trash"
|
||||
color="#C53030"
|
||||
class="cursor-pointer"
|
||||
@click="emits('delete', record.id)"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { cols } from '../hooks/appNotificationsColumns'
|
||||
import { getAppNotificationsTable } from '@/api/appNotifications'
|
||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
|
||||
const emits = defineEmits<{
|
||||
add: []
|
||||
edit: [record: Record<string, unknown>]
|
||||
toggle: [record: Record<string, unknown>]
|
||||
delete: [id: number]
|
||||
}>()
|
||||
|
||||
const tblProps = {
|
||||
columns: cols,
|
||||
api: (params: TableInput) => getAppNotificationsTable(params),
|
||||
}
|
||||
|
||||
function fetch() {
|
||||
reload()
|
||||
}
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
|
||||
const severityLabel: Record<string, string> = {
|
||||
info: 'اطلاعات',
|
||||
success: 'موفق',
|
||||
warning: 'هشدار',
|
||||
danger: 'مهم',
|
||||
}
|
||||
|
||||
const severityColor: Record<string, string> = {
|
||||
info: 'blue',
|
||||
success: 'green',
|
||||
warning: 'orange',
|
||||
danger: 'red',
|
||||
}
|
||||
|
||||
export const cols: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
customRender: (record: any) => record.index + 1,
|
||||
},
|
||||
{
|
||||
title: 'عنوان',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
type: 'Text',
|
||||
},
|
||||
{
|
||||
title: 'اهمیت',
|
||||
dataIndex: 'severity',
|
||||
key: 'severity',
|
||||
customRender: ({ record }) => (
|
||||
<CustomTag color={severityColor[record.severity] || 'default'}>
|
||||
{severityLabel[record.severity] || record.severity}
|
||||
</CustomTag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
customRender: ({ record }) => (
|
||||
<CustomTag color={record.is_active ? 'green' : 'red'}>
|
||||
{record.is_active ? 'فعال' : 'غیرفعال'}
|
||||
</CustomTag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
customRender: ({ record }) => (record.created_at ? toJalali(record.created_at) : '-'),
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="flex h-full w-full">
|
||||
<tbl
|
||||
ref="tableRef"
|
||||
class="h-full w-full"
|
||||
@add="openAdd"
|
||||
@edit="openEdit"
|
||||
@toggle="toggleActive"
|
||||
@delete="deleteItem"
|
||||
/>
|
||||
|
||||
<BasicActionSheet
|
||||
v-model:visible="editorVisible"
|
||||
:title="sheetTitle"
|
||||
modal-width="32rem"
|
||||
drawer-height="85vh"
|
||||
:loading="loading"
|
||||
:show-button="true"
|
||||
:show-cancel="true"
|
||||
:show-ok="true"
|
||||
ok-text="ذخیره"
|
||||
cancel-text="انصراف"
|
||||
@ok="submitForm"
|
||||
@close="closeEditor"
|
||||
@cancel="closeEditor"
|
||||
>
|
||||
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
|
||||
<AppNotificationEditorFormBody ref="editorRef" :form="form" />
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import tbl from './components/table.vue'
|
||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||
import AppNotificationEditorFormBody from './components/AppNotificationEditorFormBody.vue'
|
||||
import type { AppNotification } from '@/model/appNotifications'
|
||||
import {
|
||||
createAppNotification,
|
||||
deleteAppNotification,
|
||||
toggleAppNotificationActive,
|
||||
updateAppNotification,
|
||||
} from '@/api/appNotifications'
|
||||
import { confirmDeleteItem } from '@/utils/confirmModal'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
|
||||
const tableRef = ref<null | { fetch: () => void }>(null)
|
||||
const editorRef = ref<{
|
||||
validateFields?: () => Promise<unknown>
|
||||
resetFields?: () => void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const editorVisible = ref(false)
|
||||
const editId = ref<number | undefined>()
|
||||
|
||||
const defaultForm = (): Partial<AppNotification> => ({
|
||||
title: '',
|
||||
body: '',
|
||||
severity: 'info',
|
||||
is_active: true,
|
||||
})
|
||||
|
||||
const form = ref<Partial<AppNotification>>(defaultForm())
|
||||
|
||||
const sheetTitle = computed(() => (editId.value ? 'ویرایش اعلان' : 'افزودن اعلان'))
|
||||
|
||||
function refreshTable() {
|
||||
tableRef.value?.fetch()
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
editorVisible.value = false
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
editId.value = undefined
|
||||
form.value = defaultForm()
|
||||
editorRef.value?.resetFields?.()
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(record: AppNotification) {
|
||||
editId.value = record.id
|
||||
form.value = cloneDeep({
|
||||
title: record.title,
|
||||
body: record.body,
|
||||
severity: record.severity,
|
||||
is_active: record.is_active,
|
||||
})
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
await editorRef.value?.validateFields?.()
|
||||
loading.value = true
|
||||
|
||||
const payload = {
|
||||
title: String(form.value.title ?? '').trim(),
|
||||
body: String(form.value.body ?? '').trim(),
|
||||
severity: form.value.severity || 'info',
|
||||
is_active: Boolean(form.value.is_active),
|
||||
}
|
||||
|
||||
if (editId.value) {
|
||||
await updateAppNotification(editId.value, payload)
|
||||
} else {
|
||||
await createAppNotification(payload)
|
||||
}
|
||||
|
||||
message.success('اعلان با موفقیت ذخیره شد')
|
||||
editorVisible.value = false
|
||||
refreshTable()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(record: AppNotification) {
|
||||
if (!record.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await toggleAppNotificationActive(record.id)
|
||||
message.success(record.is_active ? 'اعلان غیرفعال شد' : 'اعلان فعال شد')
|
||||
refreshTable()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
message.error('خطا در تغییر وضعیت')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(id: number) {
|
||||
confirmDeleteItem('اعلان', id, deleteAppNotification, refreshTable)
|
||||
}
|
||||
</script>
|
||||
@@ -174,7 +174,7 @@ import SettingValueField from './SettingValueField.vue'
|
||||
import SettingCard from './SettingCard.vue'
|
||||
import { refreshAppModeMenu } from '@/router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyPriceProviderFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setPriceProviderFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
||||
import {
|
||||
EDITABLE_SETTING_GROUPS,
|
||||
SYSTEM_SETTING_GROUPS,
|
||||
@@ -423,6 +423,8 @@ async function initConfig() {
|
||||
applyBannerEnabledFromRows(baseData.value)
|
||||
applyTrackingSystemsEnabledFromRows(baseData.value)
|
||||
applyConditionalOrderEnabledFromRows(baseData.value)
|
||||
applyPriceTriggerEnabledFromRows(baseData.value)
|
||||
applyAppNotificationEnabledFromRows(baseData.value)
|
||||
applyPriceProviderFromRows(baseData.value)
|
||||
// در صفحهٔ عادی price_provider (can_edit=0) نیست — از کل تنظیمات بخوان
|
||||
if (!baseData.value.some((row) => row.key === 'price_provider')) {
|
||||
@@ -559,6 +561,12 @@ async function saveSettingRow(row: Const) {
|
||||
if (row.key === 'enable_conditional_order') {
|
||||
setConditionalOrderEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
if (row.key === 'has_page_price_trigger') {
|
||||
setPriceTriggerEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
if (row.key === 'has_page_notification') {
|
||||
setAppNotificationEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
if (row.key === 'price_provider') {
|
||||
setPriceProviderFromSettings(value)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'دسترسی مشتری به قابلیتها',
|
||||
icon: 'vuesax-linear:people',
|
||||
match: (k) =>
|
||||
/^(enable_conditional_order|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled)$/.test(
|
||||
/^(enable_conditional_order|has_page_price_trigger|has_page_notification|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled)$/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
@@ -172,15 +172,28 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
match: (k) => k.startsWith('payping_')
|
||||
},
|
||||
{
|
||||
id: 'accounting',
|
||||
title: 'حسابداری',
|
||||
id: 'accounting_provider',
|
||||
title: 'انتخاب سیستم حسابداری',
|
||||
icon: 'vuesax-linear:receipt-2',
|
||||
match: (k) =>
|
||||
k === 'accounting_provider'
|
||||
|| k.startsWith('tahesab_')
|
||||
|| k.startsWith('pargar_')
|
||||
|| k === 'send_to_tahesab'
|
||||
|| k === 'send_to_pargar'
|
||||
match: (k) => k === 'accounting_provider'
|
||||
},
|
||||
{
|
||||
id: 'accounting_zarin',
|
||||
title: 'حسابداری زرین',
|
||||
icon: 'vuesax-linear:card-pos',
|
||||
match: (k) => k.startsWith('zarin_')
|
||||
},
|
||||
{
|
||||
id: 'accounting_tahesab',
|
||||
title: 'حسابداری تهحساب',
|
||||
icon: 'vuesax-linear:calculator',
|
||||
match: (k) => k.startsWith('tahesab_') || k === 'send_to_tahesab'
|
||||
},
|
||||
{
|
||||
id: 'accounting_pargar',
|
||||
title: 'حسابداری پرگار',
|
||||
icon: 'vuesax-linear:document-text',
|
||||
match: (k) => k.startsWith('pargar_') || k === 'send_to_pargar'
|
||||
},
|
||||
{
|
||||
id: 'price_channel',
|
||||
|
||||
@@ -243,6 +243,8 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
fee_free: 'number',
|
||||
fee_conditional_order: 'price',
|
||||
enable_conditional_order: 'switch',
|
||||
has_page_price_trigger: 'switch',
|
||||
has_page_notification: 'switch',
|
||||
min_withdraw: 'price',
|
||||
min_deposit: 'price',
|
||||
min_first_buy: 'price',
|
||||
@@ -305,6 +307,12 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
tahesab_token: 'text',
|
||||
tahesab_db: 'text',
|
||||
tahesab_bank_account: 'text',
|
||||
zarin_api: 'text',
|
||||
zarin_token: 'text',
|
||||
zarin_noe_kar: 'number',
|
||||
zarin_gorohe_kar: 'number',
|
||||
zarin_name_g: 'text',
|
||||
zarin_uid_prefix: 'text',
|
||||
pargar_username: 'text',
|
||||
pargar_password: 'text',
|
||||
pargar_api_token: 'text',
|
||||
@@ -342,7 +350,8 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
accounting_provider: {
|
||||
none: 'هیچکدام',
|
||||
pargar: 'پرگار',
|
||||
tahesab: 'تهحساب'
|
||||
tahesab: 'تهحساب',
|
||||
zarin: 'زرین'
|
||||
},
|
||||
support_messenger: {
|
||||
bale: 'بله',
|
||||
@@ -362,7 +371,7 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
}
|
||||
|
||||
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||
accounting_provider: 'none,pargar,tahesab',
|
||||
accounting_provider: 'none,pargar,tahesab,zarin',
|
||||
price_provider: 'montzar,zaarbox,main_product',
|
||||
payping_gateway_name: 'sepal,',
|
||||
unit: 'گرم,کیلوگرم',
|
||||
@@ -567,6 +576,10 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'مبلغ ریالی که با هر بار ثبت تریگر سفارش شرطی از کیف پول ریالی مشتری کم میشود. صفر = رایگان.',
|
||||
enable_conditional_order:
|
||||
'روشن: قابلیت سفارش شرطی سراسری فعال است. خاموش: منو و ارزیابی اجرا نمیشود. فقط از صفحه دیپلوی قابل تغییر است.',
|
||||
has_page_price_trigger:
|
||||
'روشن: منوی هشدار قیمت در اپ مشتری نمایش داده میشود. خاموش: منو و صفحه مخفی است. فقط از صفحه دیپلوی قابل تغییر است.',
|
||||
has_page_notification:
|
||||
'روشن: زنگوله اعلان در اپ مشتری و منوی ثبت اعلان در ادمین فعال است. خاموش: مخفی است. فقط از صفحه دیپلوی قابل تغییر است.',
|
||||
min_withdraw:
|
||||
'حداقل مبلغ درخواست برداشت (ریال). درخواستهای کمتر پذیرفته نمیشوند.',
|
||||
min_deposit:
|
||||
@@ -724,7 +737,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
send_to_tahesab:
|
||||
'تنظیم قدیمی — از «سیستم حسابداری» استفاده کنید.',
|
||||
accounting_provider:
|
||||
'سیستم حسابداری متصل: پرگار (API ورودی)، تهحساب (ارسال خودکار)، یا هیچکدام.',
|
||||
'سیستم حسابداری متصل: پرگار، تهحساب، زرین، یا هیچکدام.',
|
||||
send_to_pargar:
|
||||
'تنظیم قدیمی — از «سیستم حسابداری» استفاده کنید.',
|
||||
pargar_username:
|
||||
@@ -747,6 +760,18 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'نام پایگاه داده در تهحساب که سند به آن ارسال میشود.',
|
||||
tahesab_bank_account:
|
||||
'نام یا کد حساب بانکی در سند واریز/برداشت تهحساب.',
|
||||
zarin_api:
|
||||
'آدرس پایه وبسرویس زرین (بدون /ZarrinService/accounting).',
|
||||
zarin_token:
|
||||
'توکن مکمل API زرین که در نرمافزار زرین تعریف میشود.',
|
||||
zarin_noe_kar:
|
||||
'کد noeKar هنگام ساخت کار ساخته در زرین.',
|
||||
zarin_gorohe_kar:
|
||||
'کد gorooheKar هنگام ساخت/جستجوی کار در زرین. اگر چند پنل یک زرین دارند، برای هر پنل گروه کار متفاوت بگذارید.',
|
||||
zarin_name_g:
|
||||
'نام گروه حساب مشتریان در زرین (nameG). اگر چند پنل یک توکن دارند، برای هر پنل نام گروه متفاوت بگذارید. خالی = نام برند.',
|
||||
zarin_uid_prefix:
|
||||
'پیشوند عددی uid برای جلوگیری از تداخل بین چند پنل روی یک زرین. فقط عدد (مثلاً ۱۱).',
|
||||
montify_name:
|
||||
'نام فروشگاه که در درگاه پرداخت و رسید بانکی به مشتری نشان داده میشود.',
|
||||
app_url:
|
||||
@@ -799,6 +824,8 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
fee_free: 'معافیت کارمزد سفارش اول',
|
||||
fee_conditional_order: 'کارمزد سفارش شرطی',
|
||||
enable_conditional_order: 'سفارش شرطی',
|
||||
has_page_price_trigger: 'هشدار قیمت',
|
||||
has_page_notification: 'اعلان داخل اپ',
|
||||
jibit_enabled: 'احراز هویت جیبیت',
|
||||
jibit_verification: 'تأیید کارت بانکی با جیبیت',
|
||||
register_jibit_verify: 'تأیید کدملی/موبایل در ثبتنام',
|
||||
@@ -842,6 +869,12 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
accounting_provider: 'سیستم حسابداری',
|
||||
send_to_tahesab: 'ارسال خودکار به تهحساب (قدیمی)',
|
||||
send_to_pargar: 'ارسال خودکار به پرگار (قدیمی)',
|
||||
zarin_api: 'آدرس API زرین',
|
||||
zarin_token: 'توکن زرین',
|
||||
zarin_noe_kar: 'نوع کار پیشفرض زرین',
|
||||
zarin_gorohe_kar: 'گروه کار پیشفرض زرین',
|
||||
zarin_name_g: 'نام گروه حساب مشتریان زرین',
|
||||
zarin_uid_prefix: 'پیشوند عددی uid زرین',
|
||||
support_messengers: 'پیامرسانهای پشتیبانی',
|
||||
tracking_systems: 'سایتهای ریگیری',
|
||||
tracking_systems_enabled: 'نمایش سایتهای ریگیری',
|
||||
@@ -881,6 +914,8 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'group_sms',
|
||||
'customer_direct_sms',
|
||||
'enable_conditional_order',
|
||||
'has_page_price_trigger',
|
||||
'has_page_notification',
|
||||
'jibit_enabled',
|
||||
'has_page_earn_money',
|
||||
'is_store',
|
||||
@@ -901,6 +936,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'send_to_tahesab', 'send_to_pargar', 'accounting_provider', 'service_charge_percentage',
|
||||
'sse_path',
|
||||
'tahesab_bank_account', 'tahesab_db', 'tahesab_host', 'tahesab_port', 'tahesab_token',
|
||||
'zarin_api', 'zarin_token', 'zarin_noe_kar', 'zarin_gorohe_kar', 'zarin_name_g', 'zarin_uid_prefix',
|
||||
'tracking_systems', 'tracking_systems_enabled',
|
||||
'pargar_api_token', 'pargar_password', 'pargar_username',
|
||||
'pargar_item_id_rial', 'pargar_item_id_gold',
|
||||
|
||||
@@ -62,7 +62,7 @@ const visibleProxy = computed({
|
||||
function currentCodeOf(customer: Customer | null): string {
|
||||
if (!customer) return ''
|
||||
if (props.provider === 'pargar') return displayAccountingCode(customer.pargar_account_id)
|
||||
if (props.provider === 'tahesab') return displayAccountingCode(customer.moshtari_code)
|
||||
if (props.provider === 'tahesab' || props.provider === 'zarin') return displayAccountingCode(customer.moshtari_code)
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ function close() {
|
||||
|
||||
async function submit() {
|
||||
if (!props.customer?.id) return
|
||||
if (props.provider !== 'pargar' && props.provider !== 'tahesab') {
|
||||
if (props.provider !== 'pargar' && props.provider !== 'tahesab' && props.provider !== 'zarin') {
|
||||
message.error('حسابداری فعال نیست.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,14 +149,14 @@ const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
|
||||
|
||||
const accountingCodeLabel = computed(() => {
|
||||
const provider = accountingProviderSetting.value
|
||||
if (provider === 'pargar' || provider === 'tahesab') return 'کد حسابداری'
|
||||
if (provider === 'pargar' || provider === 'tahesab' || provider === 'zarin') return 'کد حسابداری'
|
||||
return null
|
||||
})
|
||||
|
||||
function accountingCodeOf(row: Customer) {
|
||||
const provider = accountingProviderSetting.value
|
||||
if (provider === 'pargar') return displayAccountingCode(row.pargar_account_id)
|
||||
if (provider === 'tahesab') return displayAccountingCode(row.moshtari_code)
|
||||
if (provider === 'tahesab' || provider === 'zarin') return displayAccountingCode(row.moshtari_code)
|
||||
return '-'
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,15 @@
|
||||
<template #title><span>در تهحساب ثبت نشده — کلیک برای ثبت</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="zarinEnabled && !isValidAccountingCode(record.moshtari_code)"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openZarinRegister(record)"
|
||||
>
|
||||
<template #title><span>در زرین ثبت نشده — کلیک برای ثبت</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#0284C7" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="pargarEnabled && !isValidAccountingCode(record.pargar_account_id)"
|
||||
placement="bottom"
|
||||
@@ -192,6 +201,14 @@
|
||||
:customer-name="tahesabCustomer?.name"
|
||||
@success="fetch"
|
||||
/>
|
||||
<ZarinRetryModal
|
||||
v-if="zarinEnabled"
|
||||
v-model="zarinRegisterVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="zarinCustomer?.id"
|
||||
:customer-name="zarinCustomer?.name"
|
||||
@success="fetch"
|
||||
/>
|
||||
<PargarRetryModal
|
||||
v-if="pargarEnabled"
|
||||
v-model="pargarRegisterVisible"
|
||||
@@ -226,9 +243,11 @@ import type { Customer } from '@/model/customer'
|
||||
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import PargarRetryModal from '@/components/PargarRetryModal.vue'
|
||||
import {
|
||||
tahesabEnabledSetting,
|
||||
zarinEnabledSetting,
|
||||
pargarEnabledSetting,
|
||||
accountingProviderSetting,
|
||||
customerDirectSmsEnabledSetting,
|
||||
@@ -246,6 +265,9 @@ const confirmInfo = ref<Customer>()
|
||||
const tahesabRegisterVisible = ref(false)
|
||||
const tahesabCustomer = ref<Customer>()
|
||||
const tahesabEnabled = tahesabEnabledSetting
|
||||
const zarinRegisterVisible = ref(false)
|
||||
const zarinCustomer = ref<Customer>()
|
||||
const zarinEnabled = zarinEnabledSetting
|
||||
const pargarRegisterVisible = ref(false)
|
||||
const pargarCustomer = ref<Customer>()
|
||||
const pargarEnabled = pargarEnabledSetting
|
||||
@@ -254,13 +276,13 @@ const accountingCodeVisible = ref(false)
|
||||
const accountingCodeCustomer = ref<Customer | null>(null)
|
||||
const accountingEnabled = computed(() => {
|
||||
const provider = accountingProviderSetting.value
|
||||
return provider === 'pargar' || provider === 'tahesab'
|
||||
return provider === 'pargar' || provider === 'tahesab' || provider === 'zarin'
|
||||
})
|
||||
|
||||
function accountingCodeOf(record: Customer) {
|
||||
const provider = accountingProviderSetting.value
|
||||
if (provider === 'pargar') return displayAccountingCode(record.pargar_account_id)
|
||||
if (provider === 'tahesab') return displayAccountingCode(record.moshtari_code)
|
||||
if (provider === 'tahesab' || provider === 'zarin') return displayAccountingCode(record.moshtari_code)
|
||||
return '-'
|
||||
}
|
||||
|
||||
@@ -330,6 +352,11 @@ function openTahesabRegister(user: Customer) {
|
||||
tahesabRegisterVisible.value = true
|
||||
}
|
||||
|
||||
function openZarinRegister(user: Customer) {
|
||||
zarinCustomer.value = user
|
||||
zarinRegisterVisible.value = true
|
||||
}
|
||||
|
||||
function openPargarRegister(user: Customer) {
|
||||
pargarCustomer.value = user
|
||||
pargarRegisterVisible.value = true
|
||||
|
||||
@@ -14,7 +14,7 @@ export type CustomerColumnOptions = {
|
||||
}
|
||||
|
||||
function accountingCodeColumn(provider: AccountingProvider | undefined): BaseTableColumn | null {
|
||||
if (provider === 'pargar' || provider === 'tahesab') {
|
||||
if (provider === 'pargar' || provider === 'tahesab' || provider === 'zarin') {
|
||||
return {
|
||||
title: 'کد حسابداری',
|
||||
dataIndex: provider === 'pargar' ? 'pargar_account_id' : 'moshtari_code',
|
||||
|
||||
@@ -609,7 +609,7 @@ const userInfo = ref<Customer>()
|
||||
|
||||
const accountingCodeVisible = computed(() => {
|
||||
const provider = accountingProviderSetting.value
|
||||
return provider === 'pargar' || provider === 'tahesab'
|
||||
return provider === 'pargar' || provider === 'tahesab' || provider === 'zarin'
|
||||
})
|
||||
|
||||
const conditionalOrderEnabled = computed(() => conditionalOrderEnabledSetting.value)
|
||||
@@ -631,7 +631,7 @@ function onConditionalOrderUpdated(payload: {
|
||||
const accountingCodeValue = computed(() => {
|
||||
const provider = accountingProviderSetting.value
|
||||
if (provider === 'pargar') return displayAccountingCode(userInfo.value?.pargar_account_id)
|
||||
if (provider === 'tahesab') return displayAccountingCode(userInfo.value?.moshtari_code)
|
||||
if (provider === 'tahesab' || provider === 'zarin') return displayAccountingCode(userInfo.value?.moshtari_code)
|
||||
return '-'
|
||||
})
|
||||
|
||||
|
||||
@@ -81,6 +81,15 @@
|
||||
<template #title><span>در تهحساب ثبت نشده — کلیک برای ثبت</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="zarinEnabled && !isValidAccountingCode(row.moshtari_code)"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openZarinRegister(row)"
|
||||
>
|
||||
<template #title><span>در زرین ثبت نشده — کلیک برای ثبت</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#0284C7" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="pargarEnabled && !isValidAccountingCode(row.pargar_account_id)"
|
||||
placement="bottom"
|
||||
@@ -250,6 +259,14 @@
|
||||
:customer-name="tahesabCustomer?.name"
|
||||
@success="load"
|
||||
/>
|
||||
<ZarinRetryModal
|
||||
v-if="zarinEnabled"
|
||||
v-model="zarinRegisterVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="zarinCustomer?.id"
|
||||
:customer-name="zarinCustomer?.name"
|
||||
@success="load"
|
||||
/>
|
||||
<PargarRetryModal
|
||||
v-if="pargarEnabled"
|
||||
v-model="pargarRegisterVisible"
|
||||
@@ -287,6 +304,7 @@ import WalletChargeModal from '@/pages/menuPages/customers/base/components/Walle
|
||||
import CustomerDirectSmsModal from '@/pages/menuPages/customers/base/components/CustomerDirectSmsModal.vue'
|
||||
import CustomerAccountingCodeModal from '@/pages/menuPages/customers/base/components/CustomerAccountingCodeModal.vue'
|
||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import PargarRetryModal from '@/components/PargarRetryModal.vue'
|
||||
import {
|
||||
accountingProviderSetting,
|
||||
@@ -294,7 +312,8 @@ import {
|
||||
initAccountingSettingsFromApi,
|
||||
initWalletSettingsFromApi,
|
||||
pargarEnabledSetting,
|
||||
tahesabEnabledSetting
|
||||
tahesabEnabledSetting,
|
||||
zarinEnabledSetting
|
||||
} from '@/utils/config'
|
||||
import { isValidAccountingCode } from '@/utils/accountingCode'
|
||||
|
||||
@@ -323,6 +342,9 @@ const confirmInfo = ref<Customer>()
|
||||
const tahesabRegisterVisible = ref(false)
|
||||
const tahesabCustomer = ref<Customer>()
|
||||
const tahesabEnabled = tahesabEnabledSetting
|
||||
const zarinRegisterVisible = ref(false)
|
||||
const zarinCustomer = ref<Customer>()
|
||||
const zarinEnabled = zarinEnabledSetting
|
||||
const pargarRegisterVisible = ref(false)
|
||||
const pargarCustomer = ref<Customer>()
|
||||
const pargarEnabled = pargarEnabledSetting
|
||||
@@ -330,7 +352,7 @@ const customerDirectSmsEnabled = customerDirectSmsEnabledSetting
|
||||
|
||||
const accountingEnabled = computed(() => {
|
||||
const provider = accountingProviderSetting.value
|
||||
return provider === 'pargar' || provider === 'tahesab'
|
||||
return provider === 'pargar' || provider === 'tahesab' || provider === 'zarin'
|
||||
})
|
||||
|
||||
const editVisible = ref(false)
|
||||
@@ -404,6 +426,11 @@ function openTahesabRegister(row: Customer) {
|
||||
tahesabRegisterVisible.value = true
|
||||
}
|
||||
|
||||
function openZarinRegister(row: Customer) {
|
||||
zarinCustomer.value = row
|
||||
zarinRegisterVisible.value = true
|
||||
}
|
||||
|
||||
function openPargarRegister(row: Customer) {
|
||||
pargarCustomer.value = row
|
||||
pargarRegisterVisible.value = true
|
||||
|
||||
@@ -24,7 +24,7 @@ import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||
import { useRouterStore } from '@core/stores/router'
|
||||
import { get } from 'lodash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { conditionalOrderEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||
import { conditionalOrderEnabledSetting, initWalletSettingsFromApi, priceTriggerEnabledSetting } from '@/utils/config'
|
||||
|
||||
const routerStore = useRouterStore()
|
||||
const route = useRoute()
|
||||
@@ -39,6 +39,7 @@ const menu = computed(() => {
|
||||
return children
|
||||
.filter((child) => !child.meta?.hideMenu)
|
||||
.filter((child) => child.name !== 'conditionalOrders' || conditionalOrderEnabledSetting.value)
|
||||
.filter((child) => child.name !== 'priceTriggers' || priceTriggerEnabledSetting.value)
|
||||
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,7 @@ import { useRouterStore } from '@core/stores/router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess, isSmsBillingEnabled } from '@/utils/smsPanelAccess'
|
||||
import { bannerEnabledSetting, trackingSystemsEnabledSetting } from '@/utils/config'
|
||||
import { bannerEnabledSetting, trackingSystemsEnabledSetting, appNotificationEnabledSetting } from '@/utils/config'
|
||||
import { get } from 'lodash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
@@ -39,6 +39,7 @@ const menu = computed(() => {
|
||||
return children
|
||||
.filter((child) => !child.meta?.hideMenu)
|
||||
.filter((child) => child.name !== 'banners' || bannerEnabledSetting.value)
|
||||
.filter((child) => child.name !== 'appNotifications' || appNotificationEnabledSetting.value)
|
||||
.filter((child) => child.name !== 'captureSitesMain' || trackingSystemsEnabledSetting.value)
|
||||
.filter(
|
||||
(child) =>
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
<template #title><span>ارسال به تهحساب</span></template>
|
||||
<Icon icon="vuesax-linear:refresh-circle" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="zarinEnabled && record.zarin_failed_id"
|
||||
class="cursor-pointer"
|
||||
@click="openZarinRetry(record)"
|
||||
>
|
||||
<template #title><span>ارسال به زرین</span></template>
|
||||
<Icon icon="vuesax-linear:refresh-circle" color="#0284C7" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
@@ -103,6 +111,13 @@
|
||||
:tahesab-id="selectedTahesabId"
|
||||
@success="fetch"
|
||||
/>
|
||||
<ZarinRetryModal
|
||||
v-if="zarinEnabled"
|
||||
v-model="zarinRetryVisible"
|
||||
mode="retry-sanad"
|
||||
:zarin-id="selectedZarinId"
|
||||
@success="fetch"
|
||||
/>
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
</template>
|
||||
@@ -127,7 +142,8 @@ import {
|
||||
needsWeightAssignment
|
||||
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
|
||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||
import { initAccountingSettingsFromApi, tahesabEnabledSetting } from '@/utils/config'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import { initAccountingSettingsFromApi, tahesabEnabledSetting, zarinEnabledSetting } from '@/utils/config'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -143,6 +159,9 @@ const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const tahesabRetryVisible = ref(false)
|
||||
const selectedTahesabId = ref<number>()
|
||||
const tahesabEnabled = tahesabEnabledSetting
|
||||
const zarinRetryVisible = ref(false)
|
||||
const selectedZarinId = ref<number>()
|
||||
const zarinEnabled = zarinEnabledSetting
|
||||
/*const select = (val) => {
|
||||
val.sts === 1 ? return ['1'] : ['2']
|
||||
}*/
|
||||
@@ -201,6 +220,12 @@ function openTahesabRetry(record: { tahesab_failed_id?: number }) {
|
||||
selectedTahesabId.value = record.tahesab_failed_id
|
||||
tahesabRetryVisible.value = true
|
||||
}
|
||||
|
||||
function openZarinRetry(record: { zarin_failed_id?: number }) {
|
||||
if (!record.zarin_failed_id) return
|
||||
selectedZarinId.value = record.zarin_failed_id
|
||||
zarinRetryVisible.value = true
|
||||
}
|
||||
// const spinTable = (val: boolean) => {
|
||||
// // console.log('sss')
|
||||
// table?.value?.loading.value = val
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { conditionalOrderEnabledSetting } from '@/utils/config'
|
||||
import { conditionalOrderEnabledSetting, priceTriggerEnabledSetting } from '@/utils/config'
|
||||
|
||||
const HesabKetab: RouteRecordRaw = {
|
||||
name: 'hesabKetab',
|
||||
@@ -88,6 +88,11 @@ const HesabKetab: RouteRecordRaw = {
|
||||
icon: 'vuesax-linear:notification-bing',
|
||||
title: 'هشدار قیمت',
|
||||
order: 6
|
||||
},
|
||||
beforeEnter() {
|
||||
if (!priceTriggerEnabledSetting.value) {
|
||||
return { name: 'accountsMain' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'
|
||||
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||
import { hasSmsPanelAccess } from '@/utils/smsPanelAccess'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { bannerEnabledSetting, trackingSystemsEnabledSetting } from '@/utils/config'
|
||||
import { bannerEnabledSetting, trackingSystemsEnabledSetting, appNotificationEnabledSetting } from '@/utils/config'
|
||||
|
||||
const Setting: RouteRecordRaw = {
|
||||
name: 'settings',
|
||||
@@ -109,6 +109,21 @@ const Setting: RouteRecordRaw = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'appNotifications',
|
||||
path: 'app-notifications',
|
||||
component: () => import('@/pages/menuPages/appNotifications/index.vue'),
|
||||
meta: {
|
||||
icon: 'vuesax-linear:notification-bing',
|
||||
title: 'اعلانها',
|
||||
order: 4.5
|
||||
},
|
||||
beforeEnter() {
|
||||
if (!appNotificationEnabledSetting.value) {
|
||||
return { name: 'configuration' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'smsMain',
|
||||
path: 'sms',
|
||||
|
||||
+46
-2
@@ -1,7 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { setCachedMainMetal } from '@/utils/metalLabels'
|
||||
|
||||
export type AccountingProvider = 'none' | 'pargar' | 'tahesab'
|
||||
export type AccountingProvider = 'none' | 'pargar' | 'tahesab' | 'zarin'
|
||||
|
||||
export function getConfigApp(key: string) {
|
||||
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {}
|
||||
@@ -34,6 +34,9 @@ export const trackingSystemsEnabledSetting = ref(false)
|
||||
/** reactive — نمایش UI تهحساب در حسابداری و ... */
|
||||
export const tahesabEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — نمایش UI زرین در حسابداری و ... */
|
||||
export const zarinEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — نمایش UI پرگار در حسابداری و ... */
|
||||
export const pargarEnabledSetting = ref(false)
|
||||
|
||||
@@ -43,12 +46,19 @@ export const customerDirectSmsEnabledSetting = ref(false)
|
||||
/** reactive — منو و UI سفارش شرطی (ادمین + کنترلهای مشتری) */
|
||||
export const conditionalOrderEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — منو و UI هشدار قیمت */
|
||||
export const priceTriggerEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — منو و زنگوله اعلان داخل اپ */
|
||||
export const appNotificationEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — منبع قیمت فعال (montzar / zaarbox / main_product) */
|
||||
export const priceProviderSetting = ref('')
|
||||
|
||||
export const accountingProviderSetting = ref<AccountingProvider>('none')
|
||||
|
||||
let tahesabEnabledFromSettings: boolean | null = null
|
||||
let zarinEnabledFromSettings: boolean | null = null
|
||||
let pargarEnabledFromSettings: boolean | null = null
|
||||
let customerDirectSmsFromSettings: boolean | null = null
|
||||
let accountingProviderFromSettings: AccountingProvider | null = null
|
||||
@@ -60,7 +70,7 @@ function isSwitchOn(value: string | undefined | null): boolean {
|
||||
|
||||
function normalizeAccountingProvider(value: string | undefined | null): AccountingProvider {
|
||||
const v = (value ?? '').trim().toLowerCase()
|
||||
if (v === 'pargar' || v === 'tahesab') return v
|
||||
if (v === 'pargar' || v === 'tahesab' || v === 'zarin') return v
|
||||
return 'none'
|
||||
}
|
||||
|
||||
@@ -88,6 +98,7 @@ export function applyAccountingProviderFromRows(
|
||||
accountingProviderFromSettings = provider
|
||||
accountingProviderSetting.value = provider
|
||||
setTahesabEnabledFromSettings(provider === 'tahesab')
|
||||
setZarinEnabledFromSettings(provider === 'zarin')
|
||||
setPargarEnabledFromSettings(provider === 'pargar')
|
||||
}
|
||||
|
||||
@@ -108,6 +119,11 @@ export function setTahesabEnabledFromSettings(value: boolean): void {
|
||||
tahesabEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setZarinEnabledFromSettings(value: boolean): void {
|
||||
zarinEnabledFromSettings = value
|
||||
zarinEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setPargarEnabledFromSettings(value: boolean): void {
|
||||
pargarEnabledFromSettings = value
|
||||
pargarEnabledSetting.value = value
|
||||
@@ -122,6 +138,14 @@ export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
||||
conditionalOrderEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setPriceTriggerEnabledFromSettings(value: boolean): void {
|
||||
priceTriggerEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setAppNotificationEnabledFromSettings(value: boolean): void {
|
||||
appNotificationEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setPriceProviderFromSettings(value?: string | null): void {
|
||||
priceProviderSetting.value = (value ?? '').trim().toLowerCase()
|
||||
}
|
||||
@@ -150,6 +174,20 @@ export function applyConditionalOrderEnabledFromRows(
|
||||
setConditionalOrderEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||
}
|
||||
|
||||
export function applyPriceTriggerEnabledFromRows(
|
||||
rows: Array<{ key?: string; value?: string | null }>
|
||||
) {
|
||||
const row = rows.find((item) => item.key === 'has_page_price_trigger')
|
||||
setPriceTriggerEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||
}
|
||||
|
||||
export function applyAppNotificationEnabledFromRows(
|
||||
rows: Array<{ key?: string; value?: string | null }>
|
||||
) {
|
||||
const row = rows.find((item) => item.key === 'has_page_notification')
|
||||
setAppNotificationEnabledFromSettings(row ? isSwitchOn(row.value) : false)
|
||||
}
|
||||
|
||||
export function applyPriceProviderFromRows(
|
||||
rows: Array<{ key?: string; value?: string | null }>
|
||||
) {
|
||||
@@ -176,6 +214,8 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
||||
applyTrackingSystemsEnabledFromRows(list)
|
||||
applyCustomerDirectSmsFromRows(list)
|
||||
applyConditionalOrderEnabledFromRows(list)
|
||||
applyPriceTriggerEnabledFromRows(list)
|
||||
applyAppNotificationEnabledFromRows(list)
|
||||
applyPriceProviderFromRows(list)
|
||||
setCachedMainMetal(mainMetalRow?.value)
|
||||
const { setCachedMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
|
||||
@@ -207,6 +247,10 @@ export function isTahesabEnabled(): boolean {
|
||||
return tahesabEnabledFromSettings === true
|
||||
}
|
||||
|
||||
export function isZarinEnabled(): boolean {
|
||||
return zarinEnabledFromSettings === true
|
||||
}
|
||||
|
||||
export function isPargarEnabled(): boolean {
|
||||
return pargarEnabledFromSettings === true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user