Require review before Tahesab wallet sync and harden accounting admin UX.
Single-customer and row sync preview balances with documents before overwrite/keep; bulk sync keeps confirm. Also ship Zarin wallet sync actions, field help, and get_user cache improvements. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
-16
@@ -11,7 +11,6 @@ declare module '@vue/runtime-core' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
||||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
@@ -25,42 +24,27 @@ declare module '@vue/runtime-core' {
|
|||||||
AdminHelpTooltipContent: typeof import('./src/components/AdminHelpTooltipContent.vue')['default']
|
AdminHelpTooltipContent: typeof import('./src/components/AdminHelpTooltipContent.vue')['default']
|
||||||
AdminLabelWithHelp: typeof import('./src/components/AdminLabelWithHelp.vue')['default']
|
AdminLabelWithHelp: typeof import('./src/components/AdminLabelWithHelp.vue')['default']
|
||||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
|
||||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||||
AForm: typeof import('ant-design-vue/es')['Form']
|
AForm: typeof import('ant-design-vue/es')['Form']
|
||||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||||
AImage: typeof import('ant-design-vue/es')['Image']
|
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
|
||||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
|
||||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
|
||||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
|
||||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
|
||||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
|
||||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||||
ATable: typeof import('ant-design-vue/es')['Table']
|
ATable: typeof import('ant-design-vue/es')['Table']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
|
||||||
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
||||||
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
|
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
|
||||||
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
|
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
|
||||||
|
|||||||
+97
-1
@@ -89,6 +89,19 @@ export interface TahesabOnlyCustomer {
|
|||||||
tel: string
|
tel: string
|
||||||
gid: number | null
|
gid: number | null
|
||||||
group: string
|
group: string
|
||||||
|
has_phone?: boolean
|
||||||
|
gold?: number | null
|
||||||
|
cash?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountingPhoneMatch {
|
||||||
|
code: number
|
||||||
|
name: string
|
||||||
|
tel: string
|
||||||
|
panel_id: number
|
||||||
|
panel_name: string
|
||||||
|
panel_code?: string | null
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerSyncStatus {
|
export interface CustomerSyncStatus {
|
||||||
@@ -97,6 +110,13 @@ export interface CustomerSyncStatus {
|
|||||||
panel_only: number
|
panel_only: number
|
||||||
tahesab_total: number
|
tahesab_total: number
|
||||||
tahesab_only_count: number
|
tahesab_only_count: number
|
||||||
|
tahesab_with_phone_count?: number | null
|
||||||
|
tahesab_without_phone_count?: number | null
|
||||||
|
tahesab_importable_count?: number | null
|
||||||
|
phone_match_count?: number | null
|
||||||
|
phone_conflict_count?: number | null
|
||||||
|
phone_matches?: AccountingPhoneMatch[]
|
||||||
|
phone_conflicts?: AccountingPhoneMatch[]
|
||||||
panel_only_list: {
|
panel_only_list: {
|
||||||
data: PanelOnlyCustomer[]
|
data: PanelOnlyCustomer[]
|
||||||
current_page: number
|
current_page: number
|
||||||
@@ -115,6 +135,7 @@ export const getCustomerSyncStatus = async (params?: {
|
|||||||
search?: string
|
search?: string
|
||||||
page?: number
|
page?: number
|
||||||
tahesab_page?: number
|
tahesab_page?: number
|
||||||
|
include_tahesab?: number | boolean
|
||||||
}) => await useAxios.get<CustomerSyncStatus>('/admin/tahesab/customer-sync/status', { params })
|
}) => await useAxios.get<CustomerSyncStatus>('/admin/tahesab/customer-sync/status', { params })
|
||||||
|
|
||||||
export interface SyncPanelResult {
|
export interface SyncPanelResult {
|
||||||
@@ -132,6 +153,33 @@ export const importFromTahesab = async (customers: TahesabOnlyCustomer[]) =>
|
|||||||
customers,
|
customers,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const linkTahesabMatchingPhones = async () =>
|
||||||
|
await useAxios.post<{
|
||||||
|
ok: boolean
|
||||||
|
linked: number
|
||||||
|
conflicts: number
|
||||||
|
message: string
|
||||||
|
linked_rows: AccountingPhoneMatch[]
|
||||||
|
conflict_rows: AccountingPhoneMatch[]
|
||||||
|
}>('/admin/tahesab/customer-sync/link-by-phone')
|
||||||
|
|
||||||
|
export const fetchTahesabMandeByCodes = async (codes: number[]) =>
|
||||||
|
await useAxios.post<{ ok: boolean; mandes: Record<string, { gold: number; cash: number }> }>(
|
||||||
|
'/admin/tahesab/mande-by-codes',
|
||||||
|
{ codes },
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface TahesabPreviewDoc {
|
||||||
|
id: number
|
||||||
|
title?: string
|
||||||
|
gold?: number
|
||||||
|
cash?: number
|
||||||
|
amount?: number
|
||||||
|
type?: string
|
||||||
|
date?: string
|
||||||
|
created_at?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface TahesabWalletSyncResult {
|
export interface TahesabWalletSyncResult {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
needs_decision?: boolean
|
needs_decision?: boolean
|
||||||
@@ -146,6 +194,10 @@ export interface TahesabWalletSyncResult {
|
|||||||
app_cash?: number
|
app_cash?: number
|
||||||
tahesab_gold?: number
|
tahesab_gold?: number
|
||||||
tahesab_cash?: number
|
tahesab_cash?: number
|
||||||
|
zarin_gold?: number
|
||||||
|
zarin_cash?: number
|
||||||
|
wallet_docs?: TahesabPreviewDoc[]
|
||||||
|
tahesab_docs?: TahesabPreviewDoc[]
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +205,7 @@ export interface TahesabWalletSyncResult {
|
|||||||
/** همگامسازی مانده یک مشتری با تهحساب */
|
/** همگامسازی مانده یک مشتری با تهحساب */
|
||||||
export const syncCustomerWalletFromTahesab = async (
|
export const syncCustomerWalletFromTahesab = async (
|
||||||
customerId: number,
|
customerId: number,
|
||||||
opts?: { overwrite?: boolean; keep?: boolean; ask_if_conflict?: boolean }
|
opts?: { overwrite?: boolean; keep?: boolean; ask_if_conflict?: boolean; always_ask?: boolean }
|
||||||
) =>
|
) =>
|
||||||
await useAxios.post<TahesabWalletSyncResult>(
|
await useAxios.post<TahesabWalletSyncResult>(
|
||||||
`/admin/customer/${customerId}/tahesab/sync-wallet`,
|
`/admin/customer/${customerId}/tahesab/sync-wallet`,
|
||||||
@@ -161,9 +213,16 @@ export const syncCustomerWalletFromTahesab = async (
|
|||||||
overwrite: opts?.overwrite ?? false,
|
overwrite: opts?.overwrite ?? false,
|
||||||
keep: opts?.keep ?? false,
|
keep: opts?.keep ?? false,
|
||||||
ask_if_conflict: opts?.ask_if_conflict ?? true,
|
ask_if_conflict: opts?.ask_if_conflict ?? true,
|
||||||
|
always_ask: opts?.always_ask ?? false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const previewCustomerWalletFromTahesab = async (customerId: number, code?: string | number) =>
|
||||||
|
await useAxios.post<TahesabWalletSyncResult>(
|
||||||
|
`/admin/customer/${customerId}/tahesab/preview-wallet`,
|
||||||
|
{ code },
|
||||||
|
)
|
||||||
|
|
||||||
export interface TahesabBulkWalletSyncChunk {
|
export interface TahesabBulkWalletSyncChunk {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
message: string
|
message: string
|
||||||
@@ -200,3 +259,40 @@ export const syncAllWalletsFromTahesabChunk = async (params?: {
|
|||||||
limit: params?.limit ?? 20,
|
limit: params?.limit ?? 20,
|
||||||
overwrite: params?.overwrite ?? false,
|
overwrite: params?.overwrite ?? false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export interface TahesabWalletLinkedRow {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
mobile: string
|
||||||
|
moshtari_code: number | null
|
||||||
|
app_gold: number
|
||||||
|
app_cash: number
|
||||||
|
tahesab_gold: number | null
|
||||||
|
tahesab_cash: number | null
|
||||||
|
gold_delta?: number | null
|
||||||
|
rial_delta?: number | null
|
||||||
|
status: 'same' | 'conflict' | 'ready' | 'missing'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TahesabWalletLinkedList {
|
||||||
|
ok: boolean
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
per_page: number
|
||||||
|
last_page: number
|
||||||
|
rows: TahesabWalletLinkedRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTahesabWalletLinkedList = async (params?: {
|
||||||
|
search?: string
|
||||||
|
page?: number
|
||||||
|
per_page?: number
|
||||||
|
}) =>
|
||||||
|
await useAxios.get<TahesabWalletLinkedList>('/admin/tahesab/wallet-sync/list', {
|
||||||
|
params: {
|
||||||
|
search: params?.search || undefined,
|
||||||
|
page: params?.page ?? 1,
|
||||||
|
per_page: params?.per_page ?? 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ export interface ZarinOnlyCustomer {
|
|||||||
tel: string
|
tel: string
|
||||||
gid: number | null
|
gid: number | null
|
||||||
group: string
|
group: string
|
||||||
|
has_phone?: boolean
|
||||||
|
gold?: number | null
|
||||||
|
cash?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountingPhoneMatch {
|
||||||
|
code: number
|
||||||
|
name: string
|
||||||
|
tel: string
|
||||||
|
panel_id: number
|
||||||
|
panel_name: string
|
||||||
|
panel_code?: string | null
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerSyncStatus {
|
export interface CustomerSyncStatus {
|
||||||
@@ -97,6 +110,13 @@ export interface CustomerSyncStatus {
|
|||||||
panel_only: number
|
panel_only: number
|
||||||
zarin_total: number | null
|
zarin_total: number | null
|
||||||
zarin_only_count: number | null
|
zarin_only_count: number | null
|
||||||
|
zarin_with_phone_count?: number | null
|
||||||
|
zarin_without_phone_count?: number | null
|
||||||
|
zarin_importable_count?: number | null
|
||||||
|
phone_match_count?: number | null
|
||||||
|
phone_conflict_count?: number | null
|
||||||
|
phone_matches?: AccountingPhoneMatch[]
|
||||||
|
phone_conflicts?: AccountingPhoneMatch[]
|
||||||
/** اگر دریافت لیست شکست بخورد */
|
/** اگر دریافت لیست شکست بخورد */
|
||||||
zarin_fetch_error?: string | null
|
zarin_fetch_error?: string | null
|
||||||
zarin_http_status?: number | null
|
zarin_http_status?: number | null
|
||||||
@@ -148,3 +168,89 @@ export const importFromZarin = async (customers: ZarinOnlyCustomer[]) =>
|
|||||||
await useAxios.post<SyncPanelResult>('/admin/zarin/customer-sync/import-from-zarin', {
|
await useAxios.post<SyncPanelResult>('/admin/zarin/customer-sync/import-from-zarin', {
|
||||||
customers,
|
customers,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const linkZarinMatchingPhones = async () =>
|
||||||
|
await useAxios.post<{
|
||||||
|
ok: boolean
|
||||||
|
linked: number
|
||||||
|
conflicts: number
|
||||||
|
message: string
|
||||||
|
linked_rows: AccountingPhoneMatch[]
|
||||||
|
conflict_rows: AccountingPhoneMatch[]
|
||||||
|
}>('/admin/zarin/customer-sync/link-by-phone')
|
||||||
|
|
||||||
|
export interface ZarinWalletSyncResult {
|
||||||
|
ok: boolean
|
||||||
|
needs_decision?: boolean
|
||||||
|
message: string
|
||||||
|
result?: {
|
||||||
|
applied: boolean
|
||||||
|
needs_decision?: boolean
|
||||||
|
skipped?: boolean
|
||||||
|
gold_delta?: number
|
||||||
|
rial_delta?: number
|
||||||
|
app_gold?: number
|
||||||
|
app_cash?: number
|
||||||
|
zarin_gold?: number
|
||||||
|
zarin_cash?: number
|
||||||
|
tahesab_gold?: number
|
||||||
|
tahesab_cash?: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncCustomerWalletFromZarin = async (
|
||||||
|
customerId: number,
|
||||||
|
opts?: { overwrite?: boolean; keep?: boolean; ask_if_conflict?: boolean; always_ask?: boolean }
|
||||||
|
) =>
|
||||||
|
await useAxios.post<ZarinWalletSyncResult>(
|
||||||
|
`/admin/customer/${customerId}/zarin/sync-wallet`,
|
||||||
|
{
|
||||||
|
overwrite: opts?.overwrite ?? false,
|
||||||
|
keep: opts?.keep ?? false,
|
||||||
|
ask_if_conflict: opts?.ask_if_conflict ?? true,
|
||||||
|
always_ask: opts?.always_ask ?? false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const previewCustomerWalletFromZarin = async (customerId: number, code?: string | number) =>
|
||||||
|
await useAxios.post<ZarinWalletSyncResult>(
|
||||||
|
`/admin/customer/${customerId}/zarin/preview-wallet`,
|
||||||
|
{ code },
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ZarinBulkWalletSyncChunk {
|
||||||
|
ok: boolean
|
||||||
|
message: string
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
processed: number
|
||||||
|
next_offset: number
|
||||||
|
done: boolean
|
||||||
|
success: number
|
||||||
|
changed: number
|
||||||
|
needs_decision: number
|
||||||
|
failed: number
|
||||||
|
errors: { id: number; name: string; mobile?: string; error: string }[]
|
||||||
|
decisions: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
mobile?: string
|
||||||
|
app_gold?: number
|
||||||
|
app_cash?: number
|
||||||
|
tahesab_gold?: number
|
||||||
|
tahesab_cash?: number
|
||||||
|
message: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncAllWalletsFromZarinChunk = async (params?: {
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
overwrite?: boolean
|
||||||
|
}) =>
|
||||||
|
await useAxios.post<ZarinBulkWalletSyncChunk>('/admin/zarin/wallet-sync/all', {
|
||||||
|
offset: params?.offset ?? 0,
|
||||||
|
limit: params?.limit ?? 20,
|
||||||
|
overwrite: params?.overwrite ?? false,
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex h-full min-h-0 min-w-0 w-full max-w-full flex-col overflow-hidden py-2 pr-3">
|
<div class="relative flex h-full min-h-0 min-w-0 w-full max-w-full flex-col overflow-hidden py-2 pr-3">
|
||||||
<div class="flex shrink-0 min-w-0 items-center justify-between gap-2">
|
<div class="flex shrink-0 min-w-0 items-center justify-between gap-2">
|
||||||
|
<div class="flex min-w-0 items-center gap-1.5">
|
||||||
<div class="truncate font-bold text-[var(--admin-text)]">{{ props.title }}</div>
|
<div class="truncate font-bold text-[var(--admin-text)]">{{ props.title }}</div>
|
||||||
|
<AdminFieldHelp v-if="props.help" :text="props.help" size="sm" />
|
||||||
|
</div>
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-1">
|
<div class="mt-2 min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-1">
|
||||||
@@ -11,8 +14,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
title: string
|
title: string
|
||||||
|
help?: string
|
||||||
}
|
}
|
||||||
const props = defineProps<Prop>()
|
const props = withDefaults(defineProps<Prop>(), {
|
||||||
|
help: '',
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+149
-25
@@ -14,6 +14,9 @@
|
|||||||
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
|
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL';
|
||||||
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
|
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
|
||||||
const CONFIG_CACHE_KEY = 'APP_CONFIG_CACHE';
|
const CONFIG_CACHE_KEY = 'APP_CONFIG_CACHE';
|
||||||
|
const CONFIG_IDB_NAME = 'abshode_admin_cfg';
|
||||||
|
const CONFIG_IDB_STORE = 'kv';
|
||||||
|
const CONFIG_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
const VERSION_STORAGE_KEY = 'version';
|
const VERSION_STORAGE_KEY = 'version';
|
||||||
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
|
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
|
||||||
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
|
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
|
||||||
@@ -130,13 +133,13 @@
|
|||||||
return readUserVersion(response.user);
|
return readUserVersion(response.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCachedAppConfig() {
|
function readCachedAppConfigPayload() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(CONFIG_CACHE_KEY);
|
const raw = localStorage.getItem(CONFIG_CACHE_KEY);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const payload = JSON.parse(raw);
|
const payload = JSON.parse(raw);
|
||||||
if (!payload || typeof payload !== 'object' || !payload.config) return null;
|
if (!payload || typeof payload !== 'object' || !payload.config) return null;
|
||||||
return payload.config;
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to read cached get_user:', error);
|
console.warn('Failed to read cached get_user:', error);
|
||||||
localStorage.removeItem(CONFIG_CACHE_KEY);
|
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||||
@@ -144,18 +147,125 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeCachedAppConfig(config) {
|
function readCachedAppConfig() {
|
||||||
try {
|
const payload = readCachedAppConfigPayload();
|
||||||
localStorage.setItem(
|
return payload && payload.config ? payload.config : null;
|
||||||
CONFIG_CACHE_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
cached_at: Date.now(),
|
|
||||||
config: config
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to write get_user cache:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFreshGetUserCache(payload, remoteVersion, domain) {
|
||||||
|
if (!payload || !payload.config) return false;
|
||||||
|
if (isServerUnreachable(payload.config)) return false;
|
||||||
|
const cachedDomain = String(payload.domain || '').trim();
|
||||||
|
if (cachedDomain && cachedDomain !== domain) return false;
|
||||||
|
const age = Date.now() - Number(payload.cached_at || 0);
|
||||||
|
if (!payload.cached_at || age < 0 || age > CONFIG_CACHE_MAX_AGE_MS) return false;
|
||||||
|
const cachedVersion = normalizeVersion(payload.version);
|
||||||
|
if (remoteVersion) {
|
||||||
|
return cachedVersion === remoteVersion;
|
||||||
|
}
|
||||||
|
return Boolean(cachedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConfigIdb() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!window.indexedDB) {
|
||||||
|
reject(new Error('indexedDB unavailable'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const req = window.indexedDB.open(CONFIG_IDB_NAME, 1);
|
||||||
|
req.onupgradeneeded = function () {
|
||||||
|
if (!req.result.objectStoreNames.contains(CONFIG_IDB_STORE)) {
|
||||||
|
req.result.createObjectStore(CONFIG_IDB_STORE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.onsuccess = function () {
|
||||||
|
resolve(req.result);
|
||||||
|
};
|
||||||
|
req.onerror = function () {
|
||||||
|
reject(req.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCachedAppConfigToIdb(payload) {
|
||||||
|
openConfigIdb()
|
||||||
|
.then(function (db) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
|
||||||
|
tx.oncomplete = function () {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
tx.onerror = function () {
|
||||||
|
reject(tx.error);
|
||||||
|
};
|
||||||
|
tx.objectStore(CONFIG_IDB_STORE).put(payload, CONFIG_CACHE_KEY);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.warn('Failed to write get_user IndexedDB cache:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCachedAppConfigFromIdb() {
|
||||||
|
return openConfigIdb()
|
||||||
|
.then(function (db) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const tx = db.transaction(CONFIG_IDB_STORE, 'readonly');
|
||||||
|
const req = tx.objectStore(CONFIG_IDB_STORE).get(CONFIG_CACHE_KEY);
|
||||||
|
req.onsuccess = function () {
|
||||||
|
const value = req.result;
|
||||||
|
if (!value || typeof value !== 'object' || !value.config) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
req.onerror = function () {
|
||||||
|
reject(req.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCachedAppConfigIdb() {
|
||||||
|
openConfigIdb()
|
||||||
|
.then(function (db) {
|
||||||
|
const tx = db.transaction(CONFIG_IDB_STORE, 'readwrite');
|
||||||
|
tx.objectStore(CONFIG_IDB_STORE).delete(CONFIG_CACHE_KEY);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
/* ignore */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCachedAppConfig(config, version) {
|
||||||
|
const payload = {
|
||||||
|
cached_at: Date.now(),
|
||||||
|
version: normalizeVersion(version || getStoredAppVersion()),
|
||||||
|
domain: resolveTenantDomain(),
|
||||||
|
config: config
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CONFIG_CACHE_KEY, JSON.stringify(payload));
|
||||||
|
clearCachedAppConfigIdb();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('get_user too large for localStorage; using IndexedDB', error);
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
writeCachedAppConfigToIdb(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCachedPayload() {
|
||||||
|
const fromLs = readCachedAppConfigPayload();
|
||||||
|
if (fromLs) return fromLs;
|
||||||
|
return await readCachedAppConfigFromIdb();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredAppVersion() {
|
function getStoredAppVersion() {
|
||||||
@@ -238,6 +348,7 @@
|
|||||||
localStorage.removeItem(CONFIG_CACHE_KEY);
|
localStorage.removeItem(CONFIG_CACHE_KEY);
|
||||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY);
|
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY);
|
||||||
localStorage.removeItem(VERSION_SUCCESS_URL_KEY);
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY);
|
||||||
|
clearCachedAppConfigIdb();
|
||||||
restoreAuthToken(token);
|
restoreAuthToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +422,7 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchConfig(rounds) {
|
async function fetchConfig(rounds, version) {
|
||||||
rounds = rounds || CONFIG_RETRY_ROUNDS;
|
rounds = rounds || CONFIG_RETRY_ROUNDS;
|
||||||
const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY);
|
const urls = sortUrls(getConfigUrls(), CONFIG_SUCCESS_URL_KEY);
|
||||||
|
|
||||||
@@ -328,7 +439,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
|
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
|
||||||
writeCachedAppConfig(config);
|
writeCachedAppConfig(config, version);
|
||||||
return { config: config, url: url };
|
return { config: config, url: url };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
|
clearStaleSuccessUrl(CONFIG_SUCCESS_URL_KEY, url);
|
||||||
@@ -413,15 +524,15 @@
|
|||||||
startVersionUpdateLock(serverVersion);
|
startVersionUpdateLock(serverVersion);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { config } = await fetchConfig();
|
const { config } = await fetchConfig(undefined, serverVersion);
|
||||||
if (isServerUnreachable(config)) {
|
if (isServerUnreachable(config)) {
|
||||||
writeCachedAppConfig(config);
|
writeCachedAppConfig(config, serverVersion);
|
||||||
showServerUnavailable();
|
showServerUnavailable();
|
||||||
cancelVersionUpdateLock();
|
cancelVersionUpdateLock();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
clearBootstrapCachesKeepToken();
|
clearBootstrapCachesKeepToken();
|
||||||
writeCachedAppConfig(config);
|
writeCachedAppConfig(config, serverVersion);
|
||||||
applyConfig(config);
|
applyConfig(config);
|
||||||
finishVersionUpdateBeforeReload(serverVersion);
|
finishVersionUpdateBeforeReload(serverVersion);
|
||||||
console.log('App version changed — hard refresh', serverVersion);
|
console.log('App version changed — hard refresh', serverVersion);
|
||||||
@@ -441,8 +552,8 @@
|
|||||||
let needsFreshForVersion = false;
|
let needsFreshForVersion = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// کش قدیمی را فقط اگر خودش قفل باشد اعمال/قطع کن؛ وگرنه تا get_user تازه صبر کن
|
const cachedPayload = await resolveCachedPayload();
|
||||||
const cachedConfig = readCachedAppConfig();
|
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
|
||||||
if (cachedConfig && isServerUnreachable(cachedConfig)) {
|
if (cachedConfig && isServerUnreachable(cachedConfig)) {
|
||||||
showServerUnavailable();
|
showServerUnavailable();
|
||||||
return;
|
return;
|
||||||
@@ -461,9 +572,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config, url } = await fetchConfig();
|
const domain = resolveTenantDomain();
|
||||||
|
if (
|
||||||
|
!needsFreshForVersion &&
|
||||||
|
isFreshGetUserCache(cachedPayload, remoteVersion, domain)
|
||||||
|
) {
|
||||||
|
applyConfig(cachedPayload.config);
|
||||||
|
if (remoteVersion) {
|
||||||
|
writeStoredAppVersion(remoteVersion);
|
||||||
|
}
|
||||||
|
console.log('Config loaded from cache; get_user skipped');
|
||||||
|
finishConfigLoad(cachedPayload.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config, url } = await fetchConfig(undefined, remoteVersion);
|
||||||
applyConfig(config);
|
applyConfig(config);
|
||||||
writeCachedAppConfig(config);
|
writeCachedAppConfig(config, remoteVersion);
|
||||||
if (remoteVersion) {
|
if (remoteVersion) {
|
||||||
writeStoredAppVersion(remoteVersion);
|
writeStoredAppVersion(remoteVersion);
|
||||||
}
|
}
|
||||||
@@ -477,13 +602,13 @@
|
|||||||
console.warn('Config loader: Could not fetch config from any endpoint');
|
console.warn('Config loader: Could not fetch config from any endpoint');
|
||||||
console.warn('Error details:', error && error.message ? error.message : error);
|
console.warn('Error details:', error && error.message ? error.message : error);
|
||||||
|
|
||||||
// اگر نسخه عوض شده (قفل/آنلاک)، هرگز با کش قدیمی reachable ادمین را بالا نیاور
|
|
||||||
if (versionChanged || needsFreshForVersion) {
|
if (versionChanged || needsFreshForVersion) {
|
||||||
showServerUnavailable();
|
showServerUnavailable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedConfig = readCachedAppConfig();
|
const cachedPayload = await resolveCachedPayload();
|
||||||
|
const cachedConfig = cachedPayload && cachedPayload.config ? cachedPayload.config : null;
|
||||||
if (cachedConfig) {
|
if (cachedConfig) {
|
||||||
try {
|
try {
|
||||||
applyConfig(cachedConfig);
|
applyConfig(cachedConfig);
|
||||||
@@ -519,7 +644,6 @@
|
|||||||
showServerUnavailable();
|
showServerUnavailable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// بدون تأیید تازه از get_user، ادمین را با کش بالا نیاور (دور زدن قفل)
|
|
||||||
showServerUnavailable();
|
showServerUnavailable();
|
||||||
}
|
}
|
||||||
}, CONFIG_TIMEOUT);
|
}, CONFIG_TIMEOUT);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cs-wrap">
|
<div class="cs-wrap">
|
||||||
|
|
||||||
<PargarWalletBulkSync />
|
|
||||||
|
|
||||||
<ColleagueAccountingAccounts provider-only="pargar" />
|
<ColleagueAccountingAccounts provider-only="pargar" />
|
||||||
|
|
||||||
<!-- ─── بنر آمار ─── -->
|
<!-- ─── بنر آمار ─── -->
|
||||||
<div class="cs-banner">
|
<div class="cs-banner">
|
||||||
|
<div class="cs-banner__title-row">
|
||||||
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
||||||
|
<AdminFieldHelp :text="accountingPargarCustomerSyncIntro" size="sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- آمار اصلی: ردیف بالا -->
|
<!-- آمار اصلی: ردیف بالا -->
|
||||||
<div class="cs-stats-main">
|
<div class="cs-stats-main">
|
||||||
@@ -327,8 +328,9 @@ import {
|
|||||||
type SyncPanelResult,
|
type SyncPanelResult,
|
||||||
} from '@/api/pargar'
|
} from '@/api/pargar'
|
||||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
import { useCustomerImportStore } from '@/store/customerImportStore'
|
||||||
import PargarWalletBulkSync from './PargarWalletBulkSync.vue'
|
|
||||||
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
||||||
|
import { accountingPargarCustomerSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
const importStore = useCustomerImportStore()
|
const importStore = useCustomerImportStore()
|
||||||
|
|
||||||
@@ -580,6 +582,7 @@ onMounted(() => {
|
|||||||
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
||||||
|
|
||||||
&__title { font-size: 14px; font-weight: 700; }
|
&__title { font-size: 14px; font-weight: 700; }
|
||||||
|
&__title-row { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ردیف اصلی: پنل ↔ پرگار */
|
/* ردیف اصلی: پنل ↔ پرگار */
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
<Icon icon="vuesax-linear:refresh-circle" :size="20" color="var(--admin-primary, #10913A)" />
|
<Icon icon="vuesax-linear:refresh-circle" :size="20" color="var(--admin-primary, #10913A)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pw-bulk__copy min-w-0 flex-1">
|
<div class="pw-bulk__copy min-w-0 flex-1">
|
||||||
|
<div class="pw-bulk__title-row">
|
||||||
<p class="pw-bulk__title">همگامسازی ماندهٔ همهٔ مشتریان</p>
|
<p class="pw-bulk__title">همگامسازی ماندهٔ همهٔ مشتریان</p>
|
||||||
|
<AdminFieldHelp :text="accountingPargarBalanceSyncIntro" size="sm" />
|
||||||
|
</div>
|
||||||
<p class="pw-bulk__hint">
|
<p class="pw-bulk__hint">
|
||||||
موجودی کیف همهٔ مشتریان دارای کد پرگار را با ماندهٔ پرگار یکی میکند — تاریخچهٔ اسناد روی کیف
|
موجودی کیف همهٔ مشتریان دارای کد پرگار را با ماندهٔ پرگار یکی میکند — تاریخچهٔ اسناد روی کیف
|
||||||
replay نمیشود
|
replay نمیشود
|
||||||
@@ -64,6 +67,8 @@ import { computed, createVNode, ref } from 'vue'
|
|||||||
import { Modal, message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import { accountingPargarBalanceSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
import {
|
import {
|
||||||
syncAllWalletsFromPargarChunk,
|
syncAllWalletsFromPargarChunk,
|
||||||
type PargarBulkWalletSyncChunk,
|
type PargarBulkWalletSyncChunk,
|
||||||
@@ -184,6 +189,10 @@ async function start() {
|
|||||||
color: var(--admin-text, inherit);
|
color: var(--admin-text, inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pw-bulk__title-row {
|
||||||
|
@apply flex items-center gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.pw-bulk__hint {
|
.pw-bulk__hint {
|
||||||
@apply m-0 mt-0.5 text-xs leading-5;
|
@apply m-0 mt-0.5 text-xs leading-5;
|
||||||
color: var(--admin-text-secondary, #6b7280);
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
|||||||
@@ -1,64 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cs-wrap">
|
<div class="cs-wrap">
|
||||||
|
|
||||||
<TahesabWalletBulkSync />
|
|
||||||
|
|
||||||
<ColleagueAccountingAccounts provider-only="tahesab" />
|
<ColleagueAccountingAccounts provider-only="tahesab" />
|
||||||
|
|
||||||
<!-- ─── بنر آمار ─── -->
|
<!-- ─── آمار فشرده ─── -->
|
||||||
<div class="cs-banner">
|
<div class="cs-banner">
|
||||||
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
<div class="cs-banner__top">
|
||||||
|
<span class="cs-banner__title-row">
|
||||||
|
<span class="cs-banner__title">همگامسازی مشتریان</span>
|
||||||
|
<AdminFieldHelp :text="accountingTahesabCustomerSyncIntro" size="sm" />
|
||||||
|
</span>
|
||||||
|
<span v-if="status" class="cs-banner__meta">
|
||||||
|
{{ registeredPercent }}٪ لینکشده
|
||||||
|
· {{ status.panel_registered }} / {{ status.panel_total }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- آمار اصلی: ردیف بالا -->
|
<div class="cs-stats">
|
||||||
<div class="cs-stats-main">
|
<div class="cs-stat">
|
||||||
<div class="cs-stat cs-stat--neutral">
|
<span class="cs-stat__num">
|
||||||
<div class="cs-stat__num">
|
|
||||||
<template v-if="status">{{ status.panel_total }}</template>
|
<template v-if="status">{{ status.panel_total }}</template>
|
||||||
<a-spin v-else size="small" />
|
<a-spin v-else size="small" />
|
||||||
|
</span>
|
||||||
|
<span class="cs-stat__label">پنل</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">مشتریان پنل</div>
|
<div class="cs-stat">
|
||||||
</div>
|
<span class="cs-stat__num">
|
||||||
<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--tahesab">
|
|
||||||
<div class="cs-stat__num">
|
|
||||||
<template v-if="status?.tahesab_total != null">{{ status.tahesab_total }}</template>
|
<template v-if="status?.tahesab_total != null">{{ status.tahesab_total }}</template>
|
||||||
<a-spin v-else-if="loadingTahesab" size="small" />
|
<a-spin v-else-if="loadingTahesab" size="small" />
|
||||||
<span v-else class="cs-stat__hint">تب دوم</span>
|
<span v-else class="cs-stat__hint">—</span>
|
||||||
|
</span>
|
||||||
|
<span class="cs-stat__label">کل تهحساب</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">مشتریان تهحساب</div>
|
<div class="cs-stat cs-stat--sky">
|
||||||
|
<span class="cs-stat__num">
|
||||||
|
<template v-if="status?.tahesab_with_phone_count != null">{{ status.tahesab_with_phone_count }}</template>
|
||||||
|
<a-spin v-else-if="loadingTahesab" size="small" />
|
||||||
|
<span v-else class="cs-stat__hint">—</span>
|
||||||
|
</span>
|
||||||
|
<span class="cs-stat__label">تهحساب با شماره</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- آمار تفاوت: ردیف پایین -->
|
|
||||||
<div class="cs-stats-diff">
|
|
||||||
<div class="cs-stat cs-stat--amber">
|
<div class="cs-stat cs-stat--amber">
|
||||||
<div class="cs-stat__num">
|
<span class="cs-stat__num">
|
||||||
<template v-if="status">{{ status.panel_only }}</template>
|
<template v-if="status">{{ status.panel_only }}</template>
|
||||||
<a-spin v-else-if="loadingPanel" size="small" />
|
<a-spin v-else-if="loadingPanel" size="small" />
|
||||||
<span v-else>—</span>
|
<span v-else>—</span>
|
||||||
|
</span>
|
||||||
|
<span class="cs-stat__label">فقط پنل</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">پنل — بدون تهحساب</div>
|
<div class="cs-stat">
|
||||||
</div>
|
<span class="cs-stat__num">
|
||||||
<div class="cs-stat cs-stat--sky">
|
<template v-if="status?.tahesab_importable_count != null">{{ status.tahesab_importable_count }}</template>
|
||||||
<div class="cs-stat__num">
|
|
||||||
<template v-if="status?.tahesab_only_count != null">{{ status.tahesab_only_count }}</template>
|
|
||||||
<a-spin v-else-if="loadingTahesab" size="small" />
|
<a-spin v-else-if="loadingTahesab" size="small" />
|
||||||
<span v-else class="cs-stat__hint">تب دوم</span>
|
<span v-else class="cs-stat__hint">—</span>
|
||||||
</div>
|
</span>
|
||||||
<div class="cs-stat__label">تهحساب — بدون پنل</div>
|
<span class="cs-stat__label">قابل وارد کردن</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- progress -->
|
<div v-if="status" class="cs-track">
|
||||||
<div v-if="status" class="cs-progress">
|
<div class="cs-track__fill" :style="{ width: registeredPercent + '%' }" />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
@@ -69,8 +70,7 @@
|
|||||||
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
||||||
@click="onTabChange('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>
|
<span v-if="status?.panel_only" class="cs-tab__badge cs-tab__badge--amber">{{ status.panel_only }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -78,9 +78,8 @@
|
|||||||
:class="{ 'cs-tab--active': activeTab === 'tahesab' }"
|
:class="{ 'cs-tab--active': activeTab === 'tahesab' }"
|
||||||
@click="onTabChange('tahesab')"
|
@click="onTabChange('tahesab')"
|
||||||
>
|
>
|
||||||
<Icon icon="vuesax-linear:arrow-right" :size="14" />
|
از تهحساب به پنل
|
||||||
تهحساب ← پنل
|
<span v-if="status?.tahesab_importable_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.tahesab_importable_count }}</span>
|
||||||
<span v-if="status?.tahesab_only_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.tahesab_only_count }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,6 +99,7 @@
|
|||||||
class="cs-search"
|
class="cs-search"
|
||||||
@change="onPanelSearch"
|
@change="onPanelSearch"
|
||||||
/>
|
/>
|
||||||
|
<div class="cs-card__head-btns">
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -112,6 +112,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="syncingPanel || panelSyncResult" class="cs-result">
|
<div v-if="syncingPanel || panelSyncResult" class="cs-result">
|
||||||
<template v-if="syncingPanel">
|
<template v-if="syncingPanel">
|
||||||
@@ -191,6 +192,28 @@
|
|||||||
|
|
||||||
<!-- ─── تب ۲: تهحساب → پنل ─── -->
|
<!-- ─── تب ۲: تهحساب → پنل ─── -->
|
||||||
<div v-show="activeTab === 'tahesab'" class="cs-card">
|
<div v-show="activeTab === 'tahesab'" class="cs-card">
|
||||||
|
<div
|
||||||
|
v-if="(status?.phone_match_count || 0) > 0 || (status?.phone_conflict_count || 0) > 0"
|
||||||
|
class="cs-link-banner"
|
||||||
|
>
|
||||||
|
<div class="cs-link-banner__copy">
|
||||||
|
<p v-if="status?.phone_match_count" class="cs-link-banner__title">
|
||||||
|
{{ status.phone_match_count }} مشتری با شماره یکسان در پنل هست — کد تهحساب روی همان حساب مینشیند.
|
||||||
|
</p>
|
||||||
|
<p v-if="status?.phone_conflict_count" class="cs-link-banner__warn">
|
||||||
|
{{ status.phone_conflict_count }} مورد تداخل کد (شماره یکی، کد فرق دارد) — از صفحهٔ مشتری تصمیم بگیرید.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
v-if="status?.phone_match_count"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="linkingPhones"
|
||||||
|
@click="confirmLinkPhones"
|
||||||
|
>
|
||||||
|
نشاندن کد روی شمارههای یکسان
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
<div class="cs-card__head">
|
<div class="cs-card__head">
|
||||||
<div class="cs-card__head-right">
|
<div class="cs-card__head-right">
|
||||||
<span class="cs-dot cs-dot--sky" />
|
<span class="cs-dot cs-dot--sky" />
|
||||||
@@ -205,26 +228,28 @@
|
|||||||
class="cs-search"
|
class="cs-search"
|
||||||
@change="onTahesabSearch"
|
@change="onTahesabSearch"
|
||||||
/>
|
/>
|
||||||
|
<div class="cs-card__head-btns">
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
:loading="syncingTahesab"
|
:loading="syncingTahesab"
|
||||||
:disabled="!status?.tahesab_only_count || selectedTahesab.size === 0"
|
:disabled="importableSelectedCount === 0"
|
||||||
@click="importSelected"
|
@click="importSelected"
|
||||||
>
|
>
|
||||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
وارد کردن انتخابشدهها ({{ selectedTahesab.size }})
|
وارد کردن انتخابشده ({{ importableSelectedCount }})
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
size="small"
|
size="small"
|
||||||
:loading="importStore.active"
|
:loading="importStore.active"
|
||||||
:disabled="!filteredTahesabList.length"
|
:disabled="!importableCount"
|
||||||
@click="importAll"
|
@click="importAll"
|
||||||
>
|
>
|
||||||
{{ importStore.active ? 'در حال وارد کردن…' : `وارد کردن همه (${status?.tahesab_only_count ?? 0})` }}
|
{{ importStore.active ? 'در حال…' : `وارد کردن همه (${importableCount})` }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="syncingTahesab || tahesabSyncResult" class="cs-result">
|
<div v-if="syncingTahesab || tahesabSyncResult" class="cs-result">
|
||||||
<template v-if="syncingTahesab">
|
<template v-if="syncingTahesab">
|
||||||
@@ -251,21 +276,24 @@
|
|||||||
<div
|
<div
|
||||||
v-for="c in pagedTahesab" :key="c.code"
|
v-for="c in pagedTahesab" :key="c.code"
|
||||||
class="cs-mobile-row"
|
class="cs-mobile-row"
|
||||||
:class="{ 'cs-mobile-row--done': tahesabDone.has(c.code) }"
|
:class="{
|
||||||
|
'cs-mobile-row--done': tahesabDone.has(c.code),
|
||||||
|
'cs-mobile-row--no-phone': !rowHasPhone(c),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<a-checkbox
|
<a-checkbox
|
||||||
:checked="selectedTahesab.has(c.code)"
|
:checked="selectedTahesab.has(c.code)"
|
||||||
:disabled="tahesabDone.has(c.code)"
|
:disabled="!rowHasPhone(c) || tahesabDone.has(c.code)"
|
||||||
class="cs-mobile-row__check"
|
class="cs-mobile-row__check"
|
||||||
@change="toggleTahesab(c.code)"
|
@change="toggleTahesab(c.code)"
|
||||||
/>
|
/>
|
||||||
<div class="cs-mobile-row__info">
|
<div class="cs-mobile-row__info">
|
||||||
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||||
<div class="cs-mobile-row__mobile">{{ c.tel || '—' }} · کد {{ c.code }}</div>
|
<div class="cs-mobile-row__mobile">{{ c.tel || 'بدون شماره' }} · کد {{ c.code }} · {{ formatMande(c) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
<a-button v-else type="primary" ghost size="small"
|
<span v-else-if="!rowHasPhone(c)" class="cs-skip-badge">شماره لازم است</span>
|
||||||
@click="importOne(c)">
|
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||||
وارد کردن
|
وارد کردن
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,25 +302,30 @@
|
|||||||
<table class="cs-table">
|
<table class="cs-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
||||||
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>کد تهحساب</th><th>گروه</th><th>عملیات</th>
|
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>مانده</th><th>کد تهحساب</th><th>گروه</th><th>عملیات</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(c, idx) in pagedTahesab" :key="c.code"
|
<tr v-for="(c, idx) in pagedTahesab" :key="c.code"
|
||||||
:class="{ 'cs-table__row--done': tahesabDone.has(c.code) }">
|
:class="{
|
||||||
|
'cs-table__row--done': tahesabDone.has(c.code),
|
||||||
|
'cs-table__row--no-phone': !rowHasPhone(c),
|
||||||
|
}">
|
||||||
<td>
|
<td>
|
||||||
<a-checkbox
|
<a-checkbox
|
||||||
:checked="selectedTahesab.has(c.code)"
|
:checked="selectedTahesab.has(c.code)"
|
||||||
:disabled="tahesabDone.has(c.code)"
|
:disabled="!rowHasPhone(c) || tahesabDone.has(c.code)"
|
||||||
@change="toggleTahesab(c.code)"
|
@change="toggleTahesab(c.code)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="cs-table__num">{{ (tahesabPage - 1) * 20 + idx + 1 }}</td>
|
<td class="cs-table__num">{{ (tahesabPage - 1) * 20 + idx + 1 }}</td>
|
||||||
<td class="cs-table__name">{{ c.name }}</td>
|
<td class="cs-table__name">{{ c.name }}</td>
|
||||||
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
||||||
|
<td class="cs-table__mande">{{ formatMande(c) }}</td>
|
||||||
<td class="cs-table__mobile">{{ c.code }}</td>
|
<td class="cs-table__mobile">{{ c.code }}</td>
|
||||||
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
||||||
<td class="cs-table__action">
|
<td class="cs-table__action">
|
||||||
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
|
<span v-else-if="!rowHasPhone(c)" class="cs-skip-badge">شماره لازم است</span>
|
||||||
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
وارد کردن
|
وارد کردن
|
||||||
@@ -322,8 +355,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
import { ref, computed, onMounted, reactive, watch, createVNode } from 'vue'
|
||||||
import { message } from 'ant-design-vue/es'
|
import { message, Modal } from 'ant-design-vue/es'
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
import {
|
import {
|
||||||
@@ -331,6 +365,8 @@ import {
|
|||||||
syncPanelToTahesab,
|
syncPanelToTahesab,
|
||||||
importFromTahesab,
|
importFromTahesab,
|
||||||
registerCustomerInTahesab,
|
registerCustomerInTahesab,
|
||||||
|
linkTahesabMatchingPhones,
|
||||||
|
fetchTahesabMandeByCodes,
|
||||||
type CustomerSyncStatus,
|
type CustomerSyncStatus,
|
||||||
type PanelOnlyCustomer,
|
type PanelOnlyCustomer,
|
||||||
type TahesabOnlyCustomer,
|
type TahesabOnlyCustomer,
|
||||||
@@ -338,7 +374,8 @@ import {
|
|||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
import { useCustomerImportStore } from '@/store/customerImportStore'
|
||||||
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
||||||
import TahesabWalletBulkSync from '@/pages/menuPages/accounting/components/TahesabWalletBulkSync.vue'
|
import { accountingTahesabCustomerSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
const importStore = useCustomerImportStore()
|
const importStore = useCustomerImportStore()
|
||||||
|
|
||||||
@@ -369,6 +406,40 @@ const tahesabDone = reactive(new Set<number>())
|
|||||||
const selectedTahesab = reactive(new Set<number>())
|
const selectedTahesab = reactive(new Set<number>())
|
||||||
const tahesabSyncResult = ref<SyncPanelResult | null>(null)
|
const tahesabSyncResult = ref<SyncPanelResult | null>(null)
|
||||||
const showTahesabErrors = ref(false)
|
const showTahesabErrors = ref(false)
|
||||||
|
const linkingPhones = ref(false)
|
||||||
|
|
||||||
|
function rowHasPhone(c: TahesabOnlyCustomer) {
|
||||||
|
if (c.has_phone === false) return false
|
||||||
|
if (c.has_phone === true) return true
|
||||||
|
const tel = (c.tel || '').replace(/\D/g, '')
|
||||||
|
return tel.length >= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const importableList = computed(() =>
|
||||||
|
filteredTahesabList.value.filter(c => rowHasPhone(c) && !tahesabDone.has(c.code)),
|
||||||
|
)
|
||||||
|
const importableCount = computed(() => importableList.value.length)
|
||||||
|
const importableSelectedCount = computed(() =>
|
||||||
|
[...selectedTahesab].filter(code => {
|
||||||
|
const c = tahesabOnlyList.value.find(x => x.code === code)
|
||||||
|
return c && rowHasPhone(c) && !tahesabDone.has(code)
|
||||||
|
}).length,
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatGold(v?: number | null) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return ''
|
||||||
|
return Number(v).toLocaleString('fa-IR', { maximumFractionDigits: 3 })
|
||||||
|
}
|
||||||
|
function formatCash(v?: number | null) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return ''
|
||||||
|
return Math.round(Number(v)).toLocaleString('fa-IR')
|
||||||
|
}
|
||||||
|
function formatMande(c: TahesabOnlyCustomer) {
|
||||||
|
const g = formatGold(c.gold)
|
||||||
|
const r = formatCash(c.cash)
|
||||||
|
if (!g && !r) return '—'
|
||||||
|
return [g ? `${g} طلا` : '', r ? `${r} ریال` : ''].filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
// ─── computed ───
|
// ─── computed ───
|
||||||
const registeredPercent = computed(() => {
|
const registeredPercent = computed(() => {
|
||||||
@@ -407,12 +478,12 @@ const pagedTahesab = computed(() => {
|
|||||||
return filteredTahesabList.value.slice(s, s + 20)
|
return filteredTahesabList.value.slice(s, s + 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
const allPageSelected = computed(() =>
|
const allPageSelected = computed(() => {
|
||||||
pagedTahesab.value.length > 0 &&
|
const importable = pagedTahesab.value.filter(c => rowHasPhone(c) && !tahesabDone.has(c.code))
|
||||||
pagedTahesab.value.every(c => selectedTahesab.has(c.code) || tahesabDone.has(c.code))
|
return importable.length > 0 && importable.every(c => selectedTahesab.has(c.code))
|
||||||
)
|
})
|
||||||
const someSelected = computed(() =>
|
const someSelected = computed(() =>
|
||||||
pagedTahesab.value.some(c => selectedTahesab.has(c.code)) && !allPageSelected.value
|
pagedTahesab.value.some(c => selectedTahesab.has(c.code) && rowHasPhone(c)) && !allPageSelected.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── load panel only (سریع — بدون Tahesab API) ───
|
// ─── load panel only (سریع — بدون Tahesab API) ───
|
||||||
@@ -453,11 +524,19 @@ async function loadTahesab() {
|
|||||||
if (status.value) {
|
if (status.value) {
|
||||||
status.value.tahesab_total = res.tahesab_total ?? 0
|
status.value.tahesab_total = res.tahesab_total ?? 0
|
||||||
status.value.tahesab_only_count = res.tahesab_only_count ?? 0
|
status.value.tahesab_only_count = res.tahesab_only_count ?? 0
|
||||||
|
status.value.tahesab_with_phone_count = res.tahesab_with_phone_count ?? 0
|
||||||
|
status.value.tahesab_without_phone_count = res.tahesab_without_phone_count ?? 0
|
||||||
|
status.value.tahesab_importable_count = res.tahesab_importable_count ?? 0
|
||||||
|
status.value.phone_match_count = res.phone_match_count ?? 0
|
||||||
|
status.value.phone_conflict_count = res.phone_conflict_count ?? 0
|
||||||
|
status.value.phone_matches = res.phone_matches ?? []
|
||||||
|
status.value.phone_conflicts = res.phone_conflicts ?? []
|
||||||
} else {
|
} else {
|
||||||
status.value = res
|
status.value = res
|
||||||
}
|
}
|
||||||
tahesabOnlyList.value = res.tahesab_only_list?.data ?? []
|
tahesabOnlyList.value = res.tahesab_only_list?.data ?? []
|
||||||
tahesabFetched.value = true
|
tahesabFetched.value = true
|
||||||
|
await enrichPageMande()
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
finally { loadingTahesab.value = false }
|
finally { loadingTahesab.value = false }
|
||||||
}
|
}
|
||||||
@@ -528,31 +607,70 @@ async function syncAllPanel() {
|
|||||||
|
|
||||||
// ─── تب ۲: عملیات ───
|
// ─── تب ۲: عملیات ───
|
||||||
function toggleTahesab(code: number) {
|
function toggleTahesab(code: number) {
|
||||||
|
const row = tahesabOnlyList.value.find(c => c.code === code)
|
||||||
|
if (row && !rowHasPhone(row)) return
|
||||||
if (selectedTahesab.has(code)) selectedTahesab.delete(code)
|
if (selectedTahesab.has(code)) selectedTahesab.delete(code)
|
||||||
else selectedTahesab.add(code)
|
else selectedTahesab.add(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAll() {
|
function toggleAll() {
|
||||||
|
const importable = pagedTahesab.value.filter(c => rowHasPhone(c) && !tahesabDone.has(c.code))
|
||||||
if (allPageSelected.value) {
|
if (allPageSelected.value) {
|
||||||
pagedTahesab.value.forEach(c => selectedTahesab.delete(c.code))
|
importable.forEach(c => selectedTahesab.delete(c.code))
|
||||||
} else {
|
} else {
|
||||||
pagedTahesab.value.forEach(c => { if (!tahesabDone.has(c.code)) selectedTahesab.add(c.code) })
|
importable.forEach(c => selectedTahesab.add(c.code))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importReport(customers: TahesabOnlyCustomer[]) {
|
||||||
|
const withPhone = customers.filter(rowHasPhone)
|
||||||
|
const noPhone = customers.length - withPhone.length
|
||||||
|
return [
|
||||||
|
`${withPhone.length} مشتری با شماره وارد پنل میشوند.`,
|
||||||
|
noPhone ? `${noPhone} بدون شماره وارد نمیشوند.` : '',
|
||||||
|
'مانده تهحساب روی کیف خالی مینشیند. اگر اپ مانده داشته باشد برای تصمیم شما میماند.',
|
||||||
|
'دو مانده با هم جمع نمیشوند.',
|
||||||
|
].filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmImport(customers: TahesabOnlyCustomer[], onOk: () => void) {
|
||||||
|
const ready = customers.filter(c => rowHasPhone(c) && !tahesabDone.has(c.code))
|
||||||
|
if (!ready.length) {
|
||||||
|
message.warning('مشتری دارای شماره برای وارد کردن نیست')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'وارد کردن به پنل؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content: importReport(customers),
|
||||||
|
okText: 'بله، وارد کن',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function doImport(customers: TahesabOnlyCustomer[]) {
|
async function doImport(customers: TahesabOnlyCustomer[]) {
|
||||||
|
const ready = customers.filter(c => rowHasPhone(c) && !tahesabDone.has(c.code))
|
||||||
|
if (!ready.length) {
|
||||||
|
message.warning('بدون شماره وارد پنل نمیشوند')
|
||||||
|
return
|
||||||
|
}
|
||||||
syncingTahesab.value = true
|
syncingTahesab.value = true
|
||||||
tahesabSyncResult.value = null
|
tahesabSyncResult.value = null
|
||||||
showTahesabErrors.value = false
|
showTahesabErrors.value = false
|
||||||
try {
|
try {
|
||||||
const res = await importFromTahesab(customers)
|
const res = await importFromTahesab(ready)
|
||||||
tahesabSyncResult.value = res
|
tahesabSyncResult.value = res
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
message.success(`${res.success} مشتری وارد پنل شد`)
|
message.success(`${res.success} مشتری وارد پنل شد`)
|
||||||
customers.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) tahesabDone.add(c.code) })
|
ready.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) tahesabDone.add(c.code) })
|
||||||
if (status.value?.tahesab_only_count != null) {
|
if (status.value?.tahesab_only_count != null) {
|
||||||
status.value.tahesab_only_count = Math.max(0, status.value.tahesab_only_count - res.success)
|
status.value.tahesab_only_count = Math.max(0, status.value.tahesab_only_count - res.success)
|
||||||
}
|
}
|
||||||
|
if (status.value?.tahesab_importable_count != null) {
|
||||||
|
status.value.tahesab_importable_count = Math.max(0, status.value.tahesab_importable_count - res.success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
||||||
await loadPanel(panelPagination.value.page)
|
await loadPanel(panelPagination.value.page)
|
||||||
@@ -561,20 +679,73 @@ async function doImport(customers: TahesabOnlyCustomer[]) {
|
|||||||
finally { syncingTahesab.value = false }
|
finally { syncingTahesab.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importOne(customer: TahesabOnlyCustomer) { await doImport([customer]) }
|
async function importOne(customer: TahesabOnlyCustomer) {
|
||||||
async function importSelected() { await doImport(filteredTahesabList.value.filter(c => selectedTahesab.has(c.code))) }
|
if (!rowHasPhone(customer)) {
|
||||||
|
message.warning('بدون شماره از تهحساب وارد پنل نمیشود')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmImport([customer], () => doImport([customer]))
|
||||||
|
}
|
||||||
|
async function importSelected() {
|
||||||
|
const rows = filteredTahesabList.value.filter(c => selectedTahesab.has(c.code))
|
||||||
|
confirmImport(rows, () => doImport(rows))
|
||||||
|
}
|
||||||
|
|
||||||
// «وارد کردن همه» با پروگرسبار سراسری و قابل مینیمایز (پسزمینهای)
|
|
||||||
async function importAll() {
|
async function importAll() {
|
||||||
const pending = filteredTahesabList.value.filter(c => !tahesabDone.has(c.code))
|
const pending = importableList.value
|
||||||
if (!pending.length) return
|
if (!pending.length) return
|
||||||
if (importStore.active) {
|
if (importStore.active) {
|
||||||
importStore.expand()
|
importStore.expand()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await importStore.start(pending, 'tahesab')
|
confirmImport(pending, () => importStore.start(pending, 'tahesab'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmLinkPhones() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'نشاندن کد تهحساب روی شماره یکسان؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content: 'کد فقط روی مشتری پنل بدون کد مینشیند. اگر هر دو مانده داشته باشند برای تصمیم میماند. کد متفاوت دستی حل میشود.',
|
||||||
|
okText: 'اعمال کد',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk: linkPhones,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkPhones() {
|
||||||
|
linkingPhones.value = true
|
||||||
|
try {
|
||||||
|
const res = await linkTahesabMatchingPhones()
|
||||||
|
if (res.linked) message.success(res.message)
|
||||||
|
else message.info(res.message)
|
||||||
|
if (res.conflicts) message.warning(`${res.conflicts} تداخل کد — دستی تصمیم بگیرید`)
|
||||||
|
tahesabFetched.value = false
|
||||||
|
await loadTahesab()
|
||||||
|
await loadPanel(panelPagination.value.page)
|
||||||
|
} catch {
|
||||||
|
message.error('لینک شماره یکسان ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
linkingPhones.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enrichPageMande() {
|
||||||
|
const need = pagedTahesab.value.filter(c => c.gold == null && c.cash == null && c.code)
|
||||||
|
if (!need.length) return
|
||||||
|
try {
|
||||||
|
const res = await fetchTahesabMandeByCodes(need.map(c => c.code))
|
||||||
|
const mandes = res.mandes || {}
|
||||||
|
tahesabOnlyList.value = tahesabOnlyList.value.map((c) => {
|
||||||
|
const m = mandes[String(c.code)]
|
||||||
|
if (!m) return c
|
||||||
|
return { ...c, gold: m.gold, cash: m.cash }
|
||||||
|
})
|
||||||
|
} catch { /* ستون مانده اختیاری است */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(pagedTahesab, () => { enrichPageMande() })
|
||||||
|
|
||||||
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
||||||
watch(() => importStore.doneCodes.length, () => {
|
watch(() => importStore.doneCodes.length, () => {
|
||||||
importStore.doneCodes.forEach(code => tahesabDone.add(code))
|
importStore.doneCodes.forEach(code => tahesabDone.add(code))
|
||||||
@@ -602,104 +773,108 @@ onMounted(() => {
|
|||||||
.cs-wrap {
|
.cs-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
padding: 8px 4px 24px;
|
padding: 4px 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── بنر ─── */
|
/* ─── آمار فشرده ─── */
|
||||||
.cs-banner {
|
.cs-banner {
|
||||||
border-radius: 14px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 55%, #0ea5e9 100%);
|
border: 1px solid var(--admin-border);
|
||||||
padding: 14px 16px;
|
background: var(--admin-bg-surface);
|
||||||
color: #fff;
|
padding: 10px 12px;
|
||||||
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.35);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ردیف تفاوت */
|
.cs-banner__top {
|
||||||
.cs-stats-diff {
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-banner__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-banner__title-row {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-banner__meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-sep-arrows {
|
@media (min-width: 640px) {
|
||||||
display: flex;
|
.cs-stats {
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
align-items: center;
|
}
|
||||||
color: rgba(255,255,255,0.4);
|
|
||||||
flex-shrink: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-stat {
|
.cs-stat {
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 8px 10px;
|
border: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-bg-muted);
|
||||||
|
padding: 6px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex: 1;
|
min-width: 0;
|
||||||
|
|
||||||
&--neutral { background: rgba(255,255,255,0.12); }
|
&--amber { border-color: rgba(245, 158, 11, 0.35); }
|
||||||
&--tahesab { background: rgba(14,165,233,0.25); }
|
&--sky { border-color: rgba(14, 165, 233, 0.35); }
|
||||||
&--amber { background: rgba(251,191,36,0.2); }
|
|
||||||
&--sky { background: rgba(14,165,233,0.2); }
|
|
||||||
|
|
||||||
&__num {
|
&__num {
|
||||||
font-size: 20px;
|
font-size: 15px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1.2;
|
||||||
min-height: 26px;
|
color: var(--admin-text);
|
||||||
|
min-height: 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
@media (min-width: 640px) { font-size: 24px; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(255,255,255,0.65);
|
color: var(--admin-text-secondary);
|
||||||
margin-top: 3px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
&__hint { font-size: 10px; color: rgba(255,255,255,0.4); font-weight: 400; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* progress */
|
&__hint {
|
||||||
.cs-progress {
|
font-size: 11px;
|
||||||
&__labels {
|
color: var(--admin-text-secondary);
|
||||||
display: flex;
|
font-weight: 400;
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(255,255,255,0.6);
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-track {
|
.cs-track {
|
||||||
height: 7px;
|
height: 4px;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background: rgba(255,255,255,0.2);
|
background: var(--admin-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__fill {
|
&__fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background: #34d399;
|
background: var(--admin-primary, #d4af37);
|
||||||
transition: width 0.7s ease;
|
transition: width 0.5s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +882,7 @@ onMounted(() => {
|
|||||||
.cs-tabs {
|
.cs-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
border-bottom: 2px solid var(--admin-border);
|
border-bottom: 1px solid var(--admin-border);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
&::-webkit-scrollbar { display: none; }
|
&::-webkit-scrollbar { display: none; }
|
||||||
@@ -717,26 +892,24 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 8px 12px;
|
padding: 7px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--admin-text-secondary);
|
color: var(--admin-text-secondary);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: color 0.15s, border-color 0.15s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
@media (min-width: 640px) { padding: 8px 16px; font-size: 13px; }
|
|
||||||
|
|
||||||
&:hover { color: var(--admin-text); }
|
&:hover { color: var(--admin-text); }
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
color: #7c3aed;
|
color: var(--admin-primary, #d4af37);
|
||||||
border-bottom-color: #7c3aed;
|
border-bottom-color: var(--admin-primary, #d4af37);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__badge {
|
&__badge {
|
||||||
@@ -786,11 +959,41 @@ onMounted(() => {
|
|||||||
|
|
||||||
&__head-actions {
|
&__head-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
flex-wrap: wrap;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.ant-btn { flex-shrink: 0; }
|
@media (min-width: 640px) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
width: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head-btns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-btn) {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,8 +1013,17 @@ onMounted(() => {
|
|||||||
&--sky { background: #0ea5e9; }
|
&--sky { background: #0ea5e9; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-search { width: 150px; }
|
/* سرچ: موبایل تمامعرض؛ دسکتاپ عرض متعادل کنار دکمهها */
|
||||||
@media (max-width: 639px) { .cs-search { flex: 1; min-width: 0; } }
|
.cs-search {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
width: 11rem !important;
|
||||||
|
max-width: 14rem;
|
||||||
|
flex: 1 1 11rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── نتیجه sync ─── */
|
/* ─── نتیجه sync ─── */
|
||||||
.cs-result {
|
.cs-result {
|
||||||
@@ -847,6 +1059,7 @@ onMounted(() => {
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
&--done { opacity: 0.45; }
|
&--done { opacity: 0.45; }
|
||||||
|
&--no-phone { opacity: 0.55; filter: grayscale(0.35); }
|
||||||
|
|
||||||
&__check { flex-shrink: 0; }
|
&__check { flex-shrink: 0; }
|
||||||
&__info { flex: 1; min-width: 0; }
|
&__info { flex: 1; min-width: 0; }
|
||||||
@@ -864,9 +1077,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
thead tr { background: var(--admin-bg-muted); border-bottom: 1px solid var(--admin-border); }
|
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; }
|
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 { border-bottom: 1px solid var(--admin-border); background: var(--admin-bg-surface); transition: background 0.15s; }
|
||||||
|
tbody tr:nth-child(even) { background: var(--admin-bg-surface); }
|
||||||
tbody tr:hover { background: var(--admin-bg-hover); }
|
tbody tr:hover { background: var(--admin-bg-hover); }
|
||||||
tbody tr.cs-table__row--done { opacity: 0.45; }
|
tbody tr.cs-table__row--done { opacity: 0.45; }
|
||||||
|
tbody tr.cs-table__row--no-phone { opacity: 0.55; filter: grayscale(0.35); }
|
||||||
td { padding: 10px 14px; color: var(--admin-text); }
|
td { padding: 10px 14px; color: var(--admin-text); }
|
||||||
|
|
||||||
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
||||||
@@ -874,6 +1089,46 @@ onMounted(() => {
|
|||||||
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
||||||
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
||||||
&__action { text-align: center; white-space: nowrap; }
|
&__action { text-align: center; white-space: nowrap; }
|
||||||
|
&__mande { font-size: 11px; color: var(--admin-text-secondary); white-space: nowrap; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-skip-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(148,163,184,0.18);
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-link-banner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.cs-link-banner {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cs-link-banner__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
.cs-link-banner__warn {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-done-badge {
|
.cs-done-badge {
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tw-bulk">
|
<div class="tw-bulk">
|
||||||
<div class="tw-bulk__head">
|
<div class="tw-bulk__row">
|
||||||
<div class="tw-bulk__icon" aria-hidden="true">
|
|
||||||
<Icon icon="vuesax-linear:refresh-circle" :size="20" color="var(--admin-primary, #10913A)" />
|
|
||||||
</div>
|
|
||||||
<div class="tw-bulk__copy min-w-0 flex-1">
|
<div class="tw-bulk__copy min-w-0 flex-1">
|
||||||
<p class="tw-bulk__title">همگامسازی ماندهٔ مشتریان لینکشده</p>
|
<div class="tw-bulk__title-row">
|
||||||
|
<p class="tw-bulk__title">همگامسازی مانده</p>
|
||||||
|
<AdminFieldHelp :text="accountingTahesabBalanceSyncIntro" size="sm" />
|
||||||
|
</div>
|
||||||
<p class="tw-bulk__hint">
|
<p class="tw-bulk__hint">
|
||||||
موجودی کیف همهٔ مشتریان دارای کد تهحساب را با ماندهٔ تهحساب یکی میکند.
|
همگام تکی: اول سندها و اختلاف را میبینید، بعد تأیید میکنید. دو مانده جمع نمیشوند.
|
||||||
اگر مشتری در اپ مانده داشته باشد و با تهحساب فرق کند، در جزئیات نشان داده میشود تا جداگانه تصمیم بگیرید.
|
همگام همه فقط کیف خالی را مینشاند؛ تداخل برای تصمیم دستی میماند.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-bulk__actions">
|
<div class="tw-bulk__actions">
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
block
|
size="small"
|
||||||
class="tw-bulk__btn"
|
|
||||||
:loading="running && !overwriteMode"
|
:loading="running && !overwriteMode"
|
||||||
:disabled="running"
|
:disabled="running"
|
||||||
@click="confirmAndStart(false)"
|
@click="confirmAndStart(false)"
|
||||||
>
|
>
|
||||||
همگامسازی مانده (بدون بازنویسی تداخلها)
|
همگامسازی
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
block
|
size="small"
|
||||||
danger
|
danger
|
||||||
class="tw-bulk__btn"
|
|
||||||
:loading="running && overwriteMode"
|
:loading="running && overwriteMode"
|
||||||
:disabled="running"
|
:disabled="running"
|
||||||
@click="confirmAndStart(true)"
|
@click="confirmAndStart(true)"
|
||||||
>
|
>
|
||||||
اعمال اجباری مانده تهحساب برای همه
|
اعمال اجباری
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="running || resultVisible" class="tw-bulk__progress">
|
<div v-if="running || resultVisible" class="tw-bulk__progress">
|
||||||
<template v-if="running">
|
<template v-if="running">
|
||||||
@@ -52,8 +49,8 @@
|
|||||||
<template v-else-if="resultVisible">
|
<template v-else-if="resultVisible">
|
||||||
<div class="tw-bulk__progress-row">
|
<div class="tw-bulk__progress-row">
|
||||||
<span class="tw-bulk__ok">✓ {{ success }} موفق</span>
|
<span class="tw-bulk__ok">✓ {{ success }} موفق</span>
|
||||||
<span v-if="changed" class="tw-bulk__changed">{{ changed }} تغییر موجودی</span>
|
<span v-if="changed" class="tw-bulk__changed">{{ changed }} تغییر</span>
|
||||||
<span v-if="needsDecision" class="tw-bulk__decision">{{ needsDecision }} نیازمند تصمیم</span>
|
<span v-if="needsDecision" class="tw-bulk__decision">{{ needsDecision }} تصمیم</span>
|
||||||
<span v-if="failed" class="tw-bulk__fail">✗ {{ failed }} ناموفق</span>
|
<span v-if="failed" class="tw-bulk__fail">✗ {{ failed }} ناموفق</span>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="errors.length || decisions.length"
|
v-if="errors.length || decisions.length"
|
||||||
@@ -66,13 +63,26 @@
|
|||||||
<a-button type="link" size="small" @click="dismissResult">بستن</a-button>
|
<a-button type="link" size="small" @click="dismissResult">بستن</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showDetails && decisions.length" class="tw-bulk__decisions">
|
<div v-if="showDetails && decisions.length" class="tw-bulk__decisions">
|
||||||
<div class="tw-bulk__decisions-title">تداخل موجودی — از صفحه مشتری تصمیم بگیرید:</div>
|
<div class="tw-bulk__decisions-title">تداخل مانده — کدام را نگه داریم؟ (جمع نمیشود)</div>
|
||||||
<div v-for="d in decisions" :key="d.id">
|
<div v-for="d in decisions" :key="d.id" class="tw-bulk__decision-row">
|
||||||
|
<div>
|
||||||
<b>{{ d.name }}</b>
|
<b>{{ d.name }}</b>
|
||||||
<template v-if="d.mobile"> ({{ d.mobile }})</template>
|
<template v-if="d.mobile"> ({{ d.mobile }})</template>
|
||||||
— اپ: {{ formatGold(d.app_gold) }} طلا / {{ formatCash(d.app_cash) }} ریال
|
— اپ: {{ formatGold(d.app_gold) }} طلا / {{ formatCash(d.app_cash) }} ریال
|
||||||
· تهحساب: {{ formatGold(d.tahesab_gold) }} طلا / {{ formatCash(d.tahesab_cash) }} ریال
|
· تهحساب: {{ formatGold(d.tahesab_gold) }} طلا / {{ formatCash(d.tahesab_cash) }} ریال
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tw-bulk__decision-actions">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:loading="syncingId === d.id && syncingMode === 'ask'"
|
||||||
|
:disabled="!!syncingId || !!decidingId"
|
||||||
|
@click="openReview(d.id)"
|
||||||
|
>
|
||||||
|
بررسی و تصمیم
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showDetails && errors.length" class="tw-bulk__errors">
|
<div v-if="showDetails && errors.length" class="tw-bulk__errors">
|
||||||
<div v-for="e in errors" :key="e.id">
|
<div v-for="e in errors" :key="e.id">
|
||||||
@@ -83,18 +93,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-bulk__toolbar">
|
||||||
|
<a-input
|
||||||
|
v-model:value="search"
|
||||||
|
allow-clear
|
||||||
|
size="small"
|
||||||
|
placeholder="جستجوی نام، موبایل یا کد"
|
||||||
|
class="tw-bulk__search"
|
||||||
|
@change="onSearch"
|
||||||
|
/>
|
||||||
|
<span class="tw-bulk__count">{{ listTotal }} مشتری لینکشده</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-bulk__table-wrap">
|
||||||
|
<a-spin :spinning="listLoading">
|
||||||
|
<a-table
|
||||||
|
size="small"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="rows"
|
||||||
|
:row-key="'id'"
|
||||||
|
:pagination="tablePagination"
|
||||||
|
:locale="{ emptyText: search ? 'نتیجهای نیست' : 'مشتری لینکشدهای نیست' }"
|
||||||
|
:scroll="{ x: 960 }"
|
||||||
|
@change="onTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'customer'">
|
||||||
|
<div class="tw-bulk__cell-name">{{ record.name || '—' }}</div>
|
||||||
|
<div class="tw-bulk__cell-sub">{{ record.mobile || '—' }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'app_gold'">{{ formatGold(record.app_gold) }}</template>
|
||||||
|
<template v-else-if="column.key === 'th_gold'">{{ formatGold(record.tahesab_gold) }}</template>
|
||||||
|
<template v-else-if="column.key === 'app_cash'">{{ formatCash(record.app_cash) }}</template>
|
||||||
|
<template v-else-if="column.key === 'th_cash'">{{ formatCash(record.tahesab_cash) }}</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="statusColor(record.status)">{{ statusLabel(record.status) }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<div class="tw-bulk__row-actions">
|
||||||
|
<a-button
|
||||||
|
v-if="record.status === 'ready' || record.status === 'same' || record.status === 'conflict'"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:loading="syncingId === record.id && syncingMode === 'ask'"
|
||||||
|
:disabled="!!syncingId || running"
|
||||||
|
@click="openReview(record.id)"
|
||||||
|
>
|
||||||
|
بررسی و تصمیم
|
||||||
|
</a-button>
|
||||||
|
<span v-if="record.status === 'missing'" class="tw-bulk__cell-sub">مانده نیست</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TahesabMandeDecisionModal
|
||||||
|
v-model:visible="decisionVisible"
|
||||||
|
source-label="تهحساب"
|
||||||
|
:app-gold="decisionResult?.app_gold"
|
||||||
|
:app-cash="decisionResult?.app_cash"
|
||||||
|
:tahesab-gold="decisionResult?.tahesab_gold"
|
||||||
|
:tahesab-cash="decisionResult?.tahesab_cash"
|
||||||
|
:gold-delta="decisionResult?.gold_delta"
|
||||||
|
:rial-delta="decisionResult?.rial_delta"
|
||||||
|
:wallet-docs="decisionResult?.wallet_docs"
|
||||||
|
:tahesab-docs="decisionResult?.tahesab_docs"
|
||||||
|
@decide="onReviewDecide"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, createVNode, ref } from 'vue'
|
import { computed, createVNode, onMounted, ref } from 'vue'
|
||||||
import { Modal, message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
|
||||||
import {
|
import {
|
||||||
|
getTahesabWalletLinkedList,
|
||||||
|
previewCustomerWalletFromTahesab,
|
||||||
syncAllWalletsFromTahesabChunk,
|
syncAllWalletsFromTahesabChunk,
|
||||||
|
syncCustomerWalletFromTahesab,
|
||||||
type TahesabBulkWalletSyncChunk,
|
type TahesabBulkWalletSyncChunk,
|
||||||
|
type TahesabWalletLinkedRow,
|
||||||
|
type TahesabWalletSyncResult,
|
||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import { accountingTahesabBalanceSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
import TahesabMandeDecisionModal from '@/pages/menuPages/customers/base/components/TahesabMandeDecisionModal.vue'
|
||||||
|
|
||||||
const CHUNK_LIMIT = 20
|
const CHUNK_LIMIT = 20
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
@@ -112,26 +199,168 @@ const needsDecision = ref(0)
|
|||||||
const failed = ref(0)
|
const failed = ref(0)
|
||||||
const errors = ref<TahesabBulkWalletSyncChunk['errors']>([])
|
const errors = ref<TahesabBulkWalletSyncChunk['errors']>([])
|
||||||
const decisions = ref<TahesabBulkWalletSyncChunk['decisions']>([])
|
const decisions = ref<TahesabBulkWalletSyncChunk['decisions']>([])
|
||||||
|
const decidingId = ref<number | null>(null)
|
||||||
|
const decidingMode = ref<'overwrite' | 'keep' | null>(null)
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const page = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
const listTotal = ref(0)
|
||||||
|
const listLoading = ref(false)
|
||||||
|
const rows = ref<TahesabWalletLinkedRow[]>([])
|
||||||
|
const syncingId = ref<number | null>(null)
|
||||||
|
const syncingMode = ref<'ask' | 'overwrite' | 'keep' | null>(null)
|
||||||
|
const decisionVisible = ref(false)
|
||||||
|
const decisionCustomerId = ref<number | null>(null)
|
||||||
|
const decisionResult = ref<TahesabWalletSyncResult['result'] | null>(null)
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'مشتری', key: 'customer', width: 160 },
|
||||||
|
{ title: 'کد', dataIndex: 'moshtari_code', key: 'code', width: 72 },
|
||||||
|
{ title: 'طلا اپ', key: 'app_gold', width: 100 },
|
||||||
|
{ title: 'طلا تهحساب', key: 'th_gold', width: 110 },
|
||||||
|
{ title: 'ریال اپ', key: 'app_cash', width: 110 },
|
||||||
|
{ title: 'ریال تهحساب', key: 'th_cash', width: 120 },
|
||||||
|
{ title: 'وضعیت', key: 'status', width: 110 },
|
||||||
|
{ title: 'عمل', key: 'action', width: 180 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const tablePagination = computed(() => ({
|
||||||
|
current: page.value,
|
||||||
|
pageSize: perPage.value,
|
||||||
|
total: listTotal.value,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50'],
|
||||||
|
showTotal: (n: number) => `${n} ردیف`,
|
||||||
|
}))
|
||||||
|
|
||||||
const percent = computed(() =>
|
const percent = computed(() =>
|
||||||
total.value > 0 ? Math.min(100, Math.round((processed.value / total.value) * 100)) : 0,
|
total.value > 0 ? Math.min(100, Math.round((processed.value / total.value) * 100)) : 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
function formatGold(v?: number) {
|
function formatGold(v?: number | null) {
|
||||||
if (v == null || Number.isNaN(v)) return '—'
|
if (v == null || Number.isNaN(v)) return '—'
|
||||||
return Number(v).toLocaleString('fa-IR', { maximumFractionDigits: 3 })
|
return Number(v).toLocaleString('fa-IR', { maximumFractionDigits: 3 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCash(v?: number) {
|
function formatCash(v?: number | null) {
|
||||||
if (v == null || Number.isNaN(v)) return '—'
|
if (v == null || Number.isNaN(v)) return '—'
|
||||||
return Math.round(Number(v)).toLocaleString('fa-IR')
|
return Math.round(Number(v)).toLocaleString('fa-IR')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: TahesabWalletLinkedRow['status']) {
|
||||||
|
if (status === 'same') return 'یکسان'
|
||||||
|
if (status === 'ready') return 'قابل اعمال'
|
||||||
|
if (status === 'missing') return 'بدون مانده'
|
||||||
|
return 'تداخل'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(status: TahesabWalletLinkedRow['status']) {
|
||||||
|
if (status === 'same') return 'green'
|
||||||
|
if (status === 'ready') return 'blue'
|
||||||
|
if (status === 'missing') return 'default'
|
||||||
|
return 'orange'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
listLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getTahesabWalletLinkedList({
|
||||||
|
search: search.value.trim(),
|
||||||
|
page: page.value,
|
||||||
|
per_page: perPage.value,
|
||||||
|
})
|
||||||
|
rows.value = res.rows ?? []
|
||||||
|
listTotal.value = res.total ?? 0
|
||||||
|
page.value = res.page ?? page.value
|
||||||
|
} catch {
|
||||||
|
message.error('بارگذاری لیست مانده ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
listLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
page.value = 1
|
||||||
|
void loadList()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTableChange(pag: { current?: number; pageSize?: number }) {
|
||||||
|
page.value = pag.current ?? 1
|
||||||
|
perPage.value = pag.pageSize ?? perPage.value
|
||||||
|
void loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openReview(customerId: number) {
|
||||||
|
if (syncingId.value || running.value || decidingId.value) return
|
||||||
|
syncingId.value = customerId
|
||||||
|
syncingMode.value = 'ask'
|
||||||
|
try {
|
||||||
|
const res = await previewCustomerWalletFromTahesab(customerId)
|
||||||
|
if (!res.ok && !res.result?.ok) {
|
||||||
|
message.warning(res.message || 'مانده خوانده نشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.needs_decision && !res.result?.needs_decision) {
|
||||||
|
message.info(res.message || 'مانده یکسان است')
|
||||||
|
await loadList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decisionCustomerId.value = customerId
|
||||||
|
decisionResult.value = res.result ?? null
|
||||||
|
decisionVisible.value = true
|
||||||
|
} catch {
|
||||||
|
message.error('خواندن مانده این مشتری ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
syncingId.value = null
|
||||||
|
syncingMode.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReviewDecide(mode: 'overwrite' | 'keep') {
|
||||||
|
const customerId = decisionCustomerId.value
|
||||||
|
decisionVisible.value = false
|
||||||
|
if (!customerId) return
|
||||||
|
await applyDecision(customerId, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadList()
|
||||||
|
})
|
||||||
|
|
||||||
function dismissResult() {
|
function dismissResult() {
|
||||||
resultVisible.value = false
|
resultVisible.value = false
|
||||||
showDetails.value = false
|
showDetails.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyDecision(customerId: number, mode: 'overwrite' | 'keep') {
|
||||||
|
decidingId.value = customerId
|
||||||
|
decidingMode.value = mode
|
||||||
|
try {
|
||||||
|
const res = await syncCustomerWalletFromTahesab(customerId, {
|
||||||
|
overwrite: mode === 'overwrite',
|
||||||
|
keep: mode === 'keep',
|
||||||
|
ask_if_conflict: false,
|
||||||
|
})
|
||||||
|
decisions.value = decisions.value.filter(d => d.id !== customerId)
|
||||||
|
needsDecision.value = Math.max(0, decisions.value.length)
|
||||||
|
if (mode === 'keep') message.info(res.message || 'موجودی اپ حفظ شد')
|
||||||
|
else message.success(res.message || 'مانده تهحساب اعمال شد')
|
||||||
|
await loadList()
|
||||||
|
} catch {
|
||||||
|
message.error('اعمال تصمیم ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
decidingId.value = null
|
||||||
|
decidingMode.value = null
|
||||||
|
decisionCustomerId.value = null
|
||||||
|
decisionResult.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function confirmAndStart(overwrite: boolean) {
|
function confirmAndStart(overwrite: boolean) {
|
||||||
if (running.value) return
|
if (running.value) return
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -140,8 +369,8 @@ function confirmAndStart(overwrite: boolean) {
|
|||||||
: 'همگامسازی ماندهٔ همهٔ مشتریان؟',
|
: 'همگامسازی ماندهٔ همهٔ مشتریان؟',
|
||||||
icon: createVNode(ExclamationCircleOutlined),
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
content: overwrite
|
content: overwrite
|
||||||
? 'موجودی کیف همهٔ مشتریان دارای کد تهحساب با ماندهٔ تهحساب جایگزین میشود — حتی اگر در اپ مانده داشته باشند.'
|
? 'موجودی کیف همهٔ مشتریان دارای کد تهحساب با ماندهٔ تهحساب جایگزین میشود. دو مانده جمع نمیشوند. این کار برگشتپذیر نیست مگر با سند جدید.'
|
||||||
: 'برای مشتریانی که کیف خالی دارند مانده تهحساب مینشیند. اگر اپ مانده داشته باشد و فرق کند، در لیست «نیازمند تصمیم» میآید.',
|
: 'برای مشتریانی که کیف خالی دارند مانده تهحساب مینشیند. اگر اپ مانده داشته باشد و فرق کند، جمع نمیشود و در لیست «نیازمند تصمیم» میآید تا سندها را ببینید و تأیید کنید.',
|
||||||
okText: overwrite ? 'بله، برای همه اعمال کن' : 'شروع همگامسازی',
|
okText: overwrite ? 'بله، برای همه اعمال کن' : 'شروع همگامسازی',
|
||||||
okType: overwrite ? 'danger' : 'primary',
|
okType: overwrite ? 'danger' : 'primary',
|
||||||
cancelText: 'انصراف',
|
cancelText: 'انصراف',
|
||||||
@@ -213,45 +442,55 @@ async function start(overwrite: boolean) {
|
|||||||
} finally {
|
} finally {
|
||||||
running.value = false
|
running.value = false
|
||||||
overwriteMode.value = false
|
overwriteMode.value = false
|
||||||
|
void loadList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tw-bulk {
|
.tw-bulk {
|
||||||
@apply rounded-xl border border-base p-3;
|
@apply flex min-h-0 flex-col gap-2.5 rounded-lg border border-base p-3;
|
||||||
background: var(--admin-surface-muted, var(--admin-bg-muted, #f8fafc));
|
background: var(--admin-bg-surface, var(--admin-bg-elevated));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__head {
|
/* موبایل: ستون — دکمهها زیر توضیح | دسکتاپ: ردیف */
|
||||||
@apply mb-3 flex items-start gap-2.5;
|
.tw-bulk__row {
|
||||||
|
@apply flex flex-col items-stretch gap-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__icon {
|
@media (min-width: 640px) {
|
||||||
@apply flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gray-50;
|
.tw-bulk__row {
|
||||||
|
@apply flex-row items-center gap-3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__title {
|
.tw-bulk__title {
|
||||||
@apply m-0 text-sm font-700;
|
@apply m-0 text-sm font-600;
|
||||||
color: var(--admin-text, inherit);
|
color: var(--admin-text, inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tw-bulk__title-row {
|
||||||
|
@apply flex items-center gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.tw-bulk__hint {
|
.tw-bulk__hint {
|
||||||
@apply m-0 mt-0.5 text-xs leading-5;
|
@apply m-0 mt-0.5 text-xs leading-5;
|
||||||
color: var(--admin-text-secondary, #6b7280);
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__actions {
|
.tw-bulk__actions {
|
||||||
@apply flex flex-col gap-2;
|
@apply flex flex-wrap items-center gap-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__btn {
|
@media (min-width: 640px) {
|
||||||
@apply font-600;
|
.tw-bulk__actions {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__progress {
|
.tw-bulk__progress {
|
||||||
@apply mt-3 rounded-lg border border-base p-2.5 text-xs;
|
@apply mt-2 rounded-md border border-base p-2 text-xs;
|
||||||
background: var(--admin-bg-surface, #fff);
|
background: var(--admin-bg-muted, transparent);
|
||||||
color: var(--admin-text, inherit);
|
color: var(--admin-text, inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,54 +499,100 @@ async function start(overwrite: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__track {
|
.tw-bulk__track {
|
||||||
@apply mt-2 h-1.5 overflow-hidden rounded-full;
|
@apply mt-1.5 h-1 overflow-hidden rounded-full;
|
||||||
background: var(--admin-border, #e5e7eb);
|
background: var(--admin-border, #e5e7eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__track-fill {
|
.tw-bulk__track-fill {
|
||||||
@apply h-full rounded-full transition-all duration-300;
|
@apply h-full rounded-full transition-all duration-300;
|
||||||
background: var(--admin-primary, #10913A);
|
background: var(--admin-primary, #d4af37);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__ok {
|
.tw-bulk__ok {
|
||||||
@apply rounded-full px-2.5 py-0.5 font-600;
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
background: rgba(52, 211, 153, 0.15);
|
background: rgba(52, 211, 153, 0.15);
|
||||||
color: #059669;
|
color: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__changed {
|
.tw-bulk__changed {
|
||||||
@apply rounded-full px-2.5 py-0.5 font-600;
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
background: rgba(14, 165, 233, 0.12);
|
background: rgba(14, 165, 233, 0.12);
|
||||||
color: #0284c7;
|
color: #0284c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__decision {
|
.tw-bulk__decision {
|
||||||
@apply rounded-full px-2.5 py-0.5 font-600;
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
background: rgba(245, 158, 11, 0.15);
|
background: rgba(245, 158, 11, 0.15);
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__fail {
|
.tw-bulk__fail {
|
||||||
@apply rounded-full px-2.5 py-0.5 font-600;
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
background: rgba(239, 68, 68, 0.12);
|
background: rgba(239, 68, 68, 0.12);
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__decisions {
|
.tw-bulk__decisions {
|
||||||
@apply mt-2 max-h-36 overflow-y-auto rounded-lg p-2 text-xs leading-6;
|
@apply mt-1.5 max-h-32 overflow-y-auto rounded-md p-2 text-xs leading-6;
|
||||||
background: rgba(245, 158, 11, 0.08);
|
background: rgba(245, 158, 11, 0.08);
|
||||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__decisions-title {
|
.tw-bulk__decision-row {
|
||||||
@apply mb-1 font-700;
|
@apply mb-2 flex flex-col gap-1.5 border-b pb-2 last:mb-0 last:border-0 last:pb-0;
|
||||||
|
border-color: rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
.tw-bulk__decision-actions {
|
||||||
|
@apply flex flex-wrap gap-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tw-bulk__errors {
|
.tw-bulk__errors {
|
||||||
@apply mt-2 max-h-28 overflow-y-auto rounded-lg p-2 text-xs leading-6;
|
@apply mt-1.5 max-h-24 overflow-y-auto rounded-md p-2 text-xs leading-6;
|
||||||
background: rgba(239, 68, 68, 0.08);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tw-bulk__toolbar {
|
||||||
|
@apply flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tw-bulk__toolbar {
|
||||||
|
@apply flex-row items-center gap-2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__search {
|
||||||
|
@apply w-full min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tw-bulk__search {
|
||||||
|
@apply max-w-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__count {
|
||||||
|
@apply text-xs;
|
||||||
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__table-wrap {
|
||||||
|
@apply min-w-0 overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__cell-name {
|
||||||
|
@apply text-xs font-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__cell-sub {
|
||||||
|
@apply text-xs;
|
||||||
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__row-actions {
|
||||||
|
@apply flex flex-wrap gap-0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
|
|
||||||
<!-- ─── بنر آمار ─── -->
|
<!-- ─── بنر آمار ─── -->
|
||||||
<div class="cs-banner">
|
<div class="cs-banner">
|
||||||
|
<div class="cs-banner__title-row">
|
||||||
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
||||||
|
<AdminFieldHelp :text="accountingZarinCustomerSyncIntro" size="sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- آمار اصلی: ردیف بالا -->
|
<!-- آمار اصلی: ردیف بالا -->
|
||||||
<div class="cs-stats-main">
|
<div class="cs-stats-main">
|
||||||
@@ -16,17 +19,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">مشتریان پنل</div>
|
<div class="cs-stat__label">مشتریان پنل</div>
|
||||||
</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 cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
|
<div class="cs-stat cs-stat--zarin cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
|
||||||
<div class="cs-stat__num">
|
<div class="cs-stat__num">
|
||||||
<template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template>
|
<template v-if="status?.zarin_total != null">{{ status.zarin_total }}</template>
|
||||||
<a-spin v-else-if="loadingZarin" size="small" />
|
<a-spin v-else-if="loadingZarin" size="small" />
|
||||||
<span v-else class="cs-stat__hint">کلیک کنید</span>
|
<span v-else class="cs-stat__hint">کلیک کنید</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">مشتریان زرین</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_with_phone_count != null">{{ status.zarin_with_phone_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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,11 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cs-stat cs-stat--sky cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
|
<div class="cs-stat cs-stat--sky cs-stat--clickable" role="button" tabindex="0" @click="onTabChange('zarin')" @keydown.enter="onTabChange('zarin')">
|
||||||
<div class="cs-stat__num">
|
<div class="cs-stat__num">
|
||||||
<template v-if="status?.zarin_only_count != null">{{ status.zarin_only_count }}</template>
|
<template v-if="status?.zarin_importable_count != null">{{ status.zarin_importable_count }}</template>
|
||||||
<a-spin v-else-if="loadingZarin" size="small" />
|
<a-spin v-else-if="loadingZarin" size="small" />
|
||||||
<span v-else class="cs-stat__hint">کلیک کنید</span>
|
<span v-else class="cs-stat__hint">کلیک کنید</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-stat__label">زرین — بدون پنل</div>
|
<div class="cs-stat__label">قابل وارد کردن</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,8 +74,7 @@
|
|||||||
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
||||||
@click="onTabChange('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>
|
<span v-if="status?.panel_only" class="cs-tab__badge cs-tab__badge--amber">{{ status.panel_only }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -76,9 +82,8 @@
|
|||||||
:class="{ 'cs-tab--active': activeTab === 'zarin' }"
|
:class="{ 'cs-tab--active': activeTab === 'zarin' }"
|
||||||
@click="onTabChange('zarin')"
|
@click="onTabChange('zarin')"
|
||||||
>
|
>
|
||||||
<Icon icon="vuesax-linear:arrow-right" :size="14" />
|
از زرین به پنل
|
||||||
زرین ← پنل
|
<span v-if="status?.zarin_importable_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.zarin_importable_count }}</span>
|
||||||
<span v-if="status?.zarin_only_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.zarin_only_count }}</span>
|
|
||||||
<span v-else-if="!zarinFetched && !loadingZarin" class="cs-tab__hint">بارگذاری</span>
|
<span v-else-if="!zarinFetched && !loadingZarin" class="cs-tab__hint">بارگذاری</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +195,28 @@
|
|||||||
|
|
||||||
<!-- ─── تب ۲: زرین → پنل ─── -->
|
<!-- ─── تب ۲: زرین → پنل ─── -->
|
||||||
<div v-show="activeTab === 'zarin'" class="cs-card">
|
<div v-show="activeTab === 'zarin'" class="cs-card">
|
||||||
|
<div
|
||||||
|
v-if="(status?.phone_match_count || 0) > 0 || (status?.phone_conflict_count || 0) > 0"
|
||||||
|
class="cs-link-banner"
|
||||||
|
>
|
||||||
|
<div class="cs-link-banner__copy">
|
||||||
|
<p v-if="status?.phone_match_count" class="cs-link-banner__title">
|
||||||
|
{{ status.phone_match_count }} مشتری با شماره یکسان در پنل هست — کد زرین روی همان حساب مینشیند.
|
||||||
|
</p>
|
||||||
|
<p v-if="status?.phone_conflict_count" class="cs-link-banner__warn">
|
||||||
|
{{ status.phone_conflict_count }} مورد تداخل کد — از صفحهٔ مشتری تصمیم بگیرید.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
v-if="status?.phone_match_count"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="linkingPhones"
|
||||||
|
@click="confirmLinkPhones"
|
||||||
|
>
|
||||||
|
نشاندن کد روی شمارههای یکسان
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
<div class="cs-card__head">
|
<div class="cs-card__head">
|
||||||
<div class="cs-card__head-right">
|
<div class="cs-card__head-right">
|
||||||
<span class="cs-dot cs-dot--sky" />
|
<span class="cs-dot cs-dot--sky" />
|
||||||
@@ -204,19 +231,19 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
:loading="syncingZarin"
|
:loading="syncingZarin"
|
||||||
:disabled="!status?.zarin_only_count || selectedZarin.size === 0"
|
:disabled="importableSelectedCount === 0"
|
||||||
@click="importSelected"
|
@click="importSelected"
|
||||||
>
|
>
|
||||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
وارد کردن انتخابشدهها ({{ selectedZarin.size }})
|
وارد کردن انتخابشده ({{ importableSelectedCount }})
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
size="small"
|
size="small"
|
||||||
:loading="importStore.active"
|
:loading="importStore.active"
|
||||||
:disabled="!zarinOnlyList.length"
|
:disabled="!importableCount"
|
||||||
@click="importAll"
|
@click="importAll"
|
||||||
>
|
>
|
||||||
{{ importStore.active ? 'در حال وارد کردن…' : `وارد کردن همه (${status?.zarin_only_count ?? 0})` }}
|
{{ importStore.active ? 'در حال وارد کردن…' : `وارد کردن همه (${importableCount})` }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,21 +291,24 @@
|
|||||||
<div
|
<div
|
||||||
v-for="c in pagedZarin" :key="c.code"
|
v-for="c in pagedZarin" :key="c.code"
|
||||||
class="cs-mobile-row"
|
class="cs-mobile-row"
|
||||||
:class="{ 'cs-mobile-row--done': zarinDone.has(c.code) }"
|
:class="{
|
||||||
|
'cs-mobile-row--done': zarinDone.has(c.code),
|
||||||
|
'cs-mobile-row--no-phone': !rowHasPhone(c),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<a-checkbox
|
<a-checkbox
|
||||||
:checked="selectedZarin.has(c.code)"
|
:checked="selectedZarin.has(c.code)"
|
||||||
:disabled="zarinDone.has(c.code)"
|
:disabled="!rowHasPhone(c) || zarinDone.has(c.code)"
|
||||||
class="cs-mobile-row__check"
|
class="cs-mobile-row__check"
|
||||||
@change="toggleZarin(c.code)"
|
@change="toggleZarin(c.code)"
|
||||||
/>
|
/>
|
||||||
<div class="cs-mobile-row__info">
|
<div class="cs-mobile-row__info">
|
||||||
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||||
<div class="cs-mobile-row__mobile">{{ c.tel || '—' }} · کد {{ c.code }}</div>
|
<div class="cs-mobile-row__mobile">{{ c.tel || 'بدون شماره' }} · کد {{ c.code }} · {{ formatMande(c) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
<a-button v-else type="primary" ghost size="small"
|
<span v-else-if="!rowHasPhone(c)" class="cs-skip-badge">شماره لازم است</span>
|
||||||
@click="importOne(c)">
|
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||||
وارد کردن
|
وارد کردن
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,25 +317,30 @@
|
|||||||
<table class="cs-table">
|
<table class="cs-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
||||||
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>کد زرین</th><th>گروه</th><th>عملیات</th>
|
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>مانده</th><th>کد زرین</th><th>گروه</th><th>عملیات</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(c, idx) in pagedZarin" :key="c.code"
|
<tr v-for="(c, idx) in pagedZarin" :key="c.code"
|
||||||
:class="{ 'cs-table__row--done': zarinDone.has(c.code) }">
|
:class="{
|
||||||
|
'cs-table__row--done': zarinDone.has(c.code),
|
||||||
|
'cs-table__row--no-phone': !rowHasPhone(c),
|
||||||
|
}">
|
||||||
<td>
|
<td>
|
||||||
<a-checkbox
|
<a-checkbox
|
||||||
:checked="selectedZarin.has(c.code)"
|
:checked="selectedZarin.has(c.code)"
|
||||||
:disabled="zarinDone.has(c.code)"
|
:disabled="!rowHasPhone(c) || zarinDone.has(c.code)"
|
||||||
@change="toggleZarin(c.code)"
|
@change="toggleZarin(c.code)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="cs-table__num">{{ (zarinPage - 1) * 20 + idx + 1 }}</td>
|
<td class="cs-table__num">{{ (zarinPage - 1) * 20 + idx + 1 }}</td>
|
||||||
<td class="cs-table__name">{{ c.name }}</td>
|
<td class="cs-table__name">{{ c.name }}</td>
|
||||||
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
||||||
|
<td class="cs-table__mande">{{ formatMande(c) }}</td>
|
||||||
<td class="cs-table__mobile">{{ c.code }}</td>
|
<td class="cs-table__mobile">{{ c.code }}</td>
|
||||||
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
||||||
<td class="cs-table__action">
|
<td class="cs-table__action">
|
||||||
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
<span v-if="zarinDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
|
<span v-else-if="!rowHasPhone(c)" class="cs-skip-badge">شماره لازم است</span>
|
||||||
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||||
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
وارد کردن
|
وارد کردن
|
||||||
@@ -336,8 +371,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
import { ref, computed, onMounted, reactive, watch, createVNode } from 'vue'
|
||||||
import { message } from 'ant-design-vue/es'
|
import { message, Modal } from 'ant-design-vue/es'
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
import {
|
import {
|
||||||
@@ -345,6 +381,7 @@ import {
|
|||||||
syncPanelToZarin,
|
syncPanelToZarin,
|
||||||
importFromZarin,
|
importFromZarin,
|
||||||
registerCustomerInZarin,
|
registerCustomerInZarin,
|
||||||
|
linkZarinMatchingPhones,
|
||||||
type CustomerSyncStatus,
|
type CustomerSyncStatus,
|
||||||
type PanelOnlyCustomer,
|
type PanelOnlyCustomer,
|
||||||
type ZarinOnlyCustomer,
|
type ZarinOnlyCustomer,
|
||||||
@@ -352,6 +389,8 @@ import {
|
|||||||
} from '@/api/zarin'
|
} from '@/api/zarin'
|
||||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
import { useCustomerImportStore } from '@/store/customerImportStore'
|
||||||
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
||||||
|
import { accountingZarinCustomerSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
const importStore = useCustomerImportStore()
|
const importStore = useCustomerImportStore()
|
||||||
|
|
||||||
@@ -382,6 +421,40 @@ const zarinDone = reactive(new Set<number>())
|
|||||||
const selectedZarin = reactive(new Set<number>())
|
const selectedZarin = reactive(new Set<number>())
|
||||||
const zarinSyncResult = ref<SyncPanelResult | null>(null)
|
const zarinSyncResult = ref<SyncPanelResult | null>(null)
|
||||||
const showZarinErrors = ref(false)
|
const showZarinErrors = ref(false)
|
||||||
|
const linkingPhones = ref(false)
|
||||||
|
|
||||||
|
function rowHasPhone(c: ZarinOnlyCustomer) {
|
||||||
|
if (c.has_phone === false) return false
|
||||||
|
if (c.has_phone === true) return true
|
||||||
|
const tel = (c.tel || '').replace(/\D/g, '')
|
||||||
|
return tel.length >= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const importableList = computed(() =>
|
||||||
|
zarinOnlyList.value.filter(c => rowHasPhone(c) && !zarinDone.has(c.code)),
|
||||||
|
)
|
||||||
|
const importableCount = computed(() => importableList.value.length)
|
||||||
|
const importableSelectedCount = computed(() =>
|
||||||
|
[...selectedZarin].filter(code => {
|
||||||
|
const c = zarinOnlyList.value.find(x => x.code === code)
|
||||||
|
return c && rowHasPhone(c) && !zarinDone.has(code)
|
||||||
|
}).length,
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatGold(v?: number | null) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return ''
|
||||||
|
return Number(v).toLocaleString('fa-IR', { maximumFractionDigits: 3 })
|
||||||
|
}
|
||||||
|
function formatCash(v?: number | null) {
|
||||||
|
if (v == null || Number.isNaN(Number(v))) return ''
|
||||||
|
return Math.round(Number(v)).toLocaleString('fa-IR')
|
||||||
|
}
|
||||||
|
function formatMande(c: ZarinOnlyCustomer) {
|
||||||
|
const g = formatGold(c.gold)
|
||||||
|
const r = formatCash(c.cash)
|
||||||
|
if (!g && !r) return '—'
|
||||||
|
return [g ? `${g} طلا` : '', r ? `${r} ریال` : ''].filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
// ─── computed ───
|
// ─── computed ───
|
||||||
const registeredPercent = computed(() => {
|
const registeredPercent = computed(() => {
|
||||||
@@ -396,12 +469,12 @@ const pagedZarin = computed(() => {
|
|||||||
return zarinOnlyList.value.slice(s, s + 20)
|
return zarinOnlyList.value.slice(s, s + 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
const allPageSelected = computed(() =>
|
const allPageSelected = computed(() => {
|
||||||
pagedZarin.value.length > 0 &&
|
const importable = pagedZarin.value.filter(c => rowHasPhone(c) && !zarinDone.has(c.code))
|
||||||
pagedZarin.value.every(c => selectedZarin.has(c.code) || zarinDone.has(c.code))
|
return importable.length > 0 && importable.every(c => selectedZarin.has(c.code))
|
||||||
)
|
})
|
||||||
const someSelected = computed(() =>
|
const someSelected = computed(() =>
|
||||||
pagedZarin.value.some(c => selectedZarin.has(c.code)) && !allPageSelected.value
|
pagedZarin.value.some(c => selectedZarin.has(c.code) && rowHasPhone(c)) && !allPageSelected.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── load panel only (سریع — بدون API زرین) ───
|
// ─── load panel only (سریع — بدون API زرین) ───
|
||||||
@@ -443,6 +516,13 @@ async function loadZarin(force = false) {
|
|||||||
if (status.value) {
|
if (status.value) {
|
||||||
status.value.zarin_total = res.zarin_total ?? 0
|
status.value.zarin_total = res.zarin_total ?? 0
|
||||||
status.value.zarin_only_count = res.zarin_only_count ?? 0
|
status.value.zarin_only_count = res.zarin_only_count ?? 0
|
||||||
|
status.value.zarin_with_phone_count = res.zarin_with_phone_count ?? 0
|
||||||
|
status.value.zarin_without_phone_count = res.zarin_without_phone_count ?? 0
|
||||||
|
status.value.zarin_importable_count = res.zarin_importable_count ?? 0
|
||||||
|
status.value.phone_match_count = res.phone_match_count ?? 0
|
||||||
|
status.value.phone_conflict_count = res.phone_conflict_count ?? 0
|
||||||
|
status.value.phone_matches = res.phone_matches ?? []
|
||||||
|
status.value.phone_conflicts = res.phone_conflicts ?? []
|
||||||
status.value.zarin_group_filter = res.zarin_group_filter ?? null
|
status.value.zarin_group_filter = res.zarin_group_filter ?? null
|
||||||
status.value.include_zarin = res.include_zarin ?? false
|
status.value.include_zarin = res.include_zarin ?? false
|
||||||
status.value.zarin_fetch_error = null
|
status.value.zarin_fetch_error = null
|
||||||
@@ -542,31 +622,70 @@ async function syncAllPanel() {
|
|||||||
|
|
||||||
// ─── تب ۲: عملیات ───
|
// ─── تب ۲: عملیات ───
|
||||||
function toggleZarin(code: number) {
|
function toggleZarin(code: number) {
|
||||||
|
const row = zarinOnlyList.value.find(c => c.code === code)
|
||||||
|
if (row && !rowHasPhone(row)) return
|
||||||
if (selectedZarin.has(code)) selectedZarin.delete(code)
|
if (selectedZarin.has(code)) selectedZarin.delete(code)
|
||||||
else selectedZarin.add(code)
|
else selectedZarin.add(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAll() {
|
function toggleAll() {
|
||||||
|
const importable = pagedZarin.value.filter(c => rowHasPhone(c) && !zarinDone.has(c.code))
|
||||||
if (allPageSelected.value) {
|
if (allPageSelected.value) {
|
||||||
pagedZarin.value.forEach(c => selectedZarin.delete(c.code))
|
importable.forEach(c => selectedZarin.delete(c.code))
|
||||||
} else {
|
} else {
|
||||||
pagedZarin.value.forEach(c => { if (!zarinDone.has(c.code)) selectedZarin.add(c.code) })
|
importable.forEach(c => selectedZarin.add(c.code))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importReport(customers: ZarinOnlyCustomer[]) {
|
||||||
|
const withPhone = customers.filter(rowHasPhone)
|
||||||
|
const noPhone = customers.length - withPhone.length
|
||||||
|
return [
|
||||||
|
`${withPhone.length} مشتری با شماره وارد پنل میشوند.`,
|
||||||
|
noPhone ? `${noPhone} بدون شماره وارد نمیشوند.` : '',
|
||||||
|
'مانده زرین روی کیف خالی مینشیند. اگر اپ مانده داشته باشد برای تصمیم شما میماند.',
|
||||||
|
'دو مانده با هم جمع نمیشوند.',
|
||||||
|
].filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmImport(customers: ZarinOnlyCustomer[], onOk: () => void) {
|
||||||
|
const ready = customers.filter(c => rowHasPhone(c) && !zarinDone.has(c.code))
|
||||||
|
if (!ready.length) {
|
||||||
|
message.warning('مشتری دارای شماره برای وارد کردن نیست')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'وارد کردن به پنل؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content: importReport(customers),
|
||||||
|
okText: 'بله، وارد کن',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function doImport(customers: ZarinOnlyCustomer[]) {
|
async function doImport(customers: ZarinOnlyCustomer[]) {
|
||||||
|
const ready = customers.filter(c => rowHasPhone(c) && !zarinDone.has(c.code))
|
||||||
|
if (!ready.length) {
|
||||||
|
message.warning('بدون شماره وارد پنل نمیشوند')
|
||||||
|
return
|
||||||
|
}
|
||||||
syncingZarin.value = true
|
syncingZarin.value = true
|
||||||
zarinSyncResult.value = null
|
zarinSyncResult.value = null
|
||||||
showZarinErrors.value = false
|
showZarinErrors.value = false
|
||||||
try {
|
try {
|
||||||
const res = await importFromZarin(customers)
|
const res = await importFromZarin(ready)
|
||||||
zarinSyncResult.value = res
|
zarinSyncResult.value = res
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
message.success(`${res.success} مشتری وارد پنل شد`)
|
message.success(`${res.success} مشتری وارد پنل شد`)
|
||||||
customers.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) zarinDone.add(c.code) })
|
ready.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) zarinDone.add(c.code) })
|
||||||
if (status.value?.zarin_only_count != null) {
|
if (status.value?.zarin_only_count != null) {
|
||||||
status.value.zarin_only_count = Math.max(0, status.value.zarin_only_count - res.success)
|
status.value.zarin_only_count = Math.max(0, status.value.zarin_only_count - res.success)
|
||||||
}
|
}
|
||||||
|
if (status.value?.zarin_importable_count != null) {
|
||||||
|
status.value.zarin_importable_count = Math.max(0, status.value.zarin_importable_count - res.success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
||||||
await loadPanel(panelPagination.value.page)
|
await loadPanel(panelPagination.value.page)
|
||||||
@@ -575,18 +694,54 @@ async function doImport(customers: ZarinOnlyCustomer[]) {
|
|||||||
finally { syncingZarin.value = false }
|
finally { syncingZarin.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importOne(customer: ZarinOnlyCustomer) { await doImport([customer]) }
|
async function importOne(customer: ZarinOnlyCustomer) {
|
||||||
async function importSelected() { await doImport(zarinOnlyList.value.filter(c => selectedZarin.has(c.code))) }
|
if (!rowHasPhone(customer)) {
|
||||||
|
message.warning('بدون شماره از زرین وارد پنل نمیشود')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmImport([customer], () => doImport([customer]))
|
||||||
|
}
|
||||||
|
async function importSelected() {
|
||||||
|
const rows = zarinOnlyList.value.filter(c => selectedZarin.has(c.code))
|
||||||
|
confirmImport(rows, () => doImport(rows))
|
||||||
|
}
|
||||||
|
|
||||||
// «وارد کردن همه» با پروگرسبار سراسری و قابل مینیمایز (پسزمینهای)
|
|
||||||
async function importAll() {
|
async function importAll() {
|
||||||
const pending = zarinOnlyList.value.filter(c => !zarinDone.has(c.code))
|
const pending = importableList.value
|
||||||
if (!pending.length) return
|
if (!pending.length) return
|
||||||
if (importStore.active) {
|
if (importStore.active) {
|
||||||
importStore.expand()
|
importStore.expand()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await importStore.start(pending, 'zarin')
|
confirmImport(pending, () => importStore.start(pending, 'zarin'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmLinkPhones() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'نشاندن کد زرین روی شماره یکسان؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content: 'کد فقط روی مشتری پنل بدون کد مینشیند. اگر هر دو مانده داشته باشند برای تصمیم میماند.',
|
||||||
|
okText: 'اعمال کد',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk: linkPhones,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkPhones() {
|
||||||
|
linkingPhones.value = true
|
||||||
|
try {
|
||||||
|
const res = await linkZarinMatchingPhones()
|
||||||
|
if (res.linked) message.success(res.message)
|
||||||
|
else message.info(res.message)
|
||||||
|
if (res.conflicts) message.warning(`${res.conflicts} تداخل کد — دستی تصمیم بگیرید`)
|
||||||
|
await loadZarin(true)
|
||||||
|
await loadPanel(panelPagination.value.page)
|
||||||
|
} catch {
|
||||||
|
message.error('لینک شماره یکسان ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
linkingPhones.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
||||||
@@ -634,12 +789,13 @@ onMounted(() => {
|
|||||||
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
||||||
|
|
||||||
&__title { font-size: 14px; font-weight: 700; }
|
&__title { font-size: 14px; font-weight: 700; }
|
||||||
|
&__title-row { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ردیف اصلی: پنل ↔ زرین */
|
/* ردیف اصلی: پنل ↔ زرین */
|
||||||
.cs-stats-main {
|
.cs-stats-main {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,6 +1058,7 @@ onMounted(() => {
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
&--done { opacity: 0.45; }
|
&--done { opacity: 0.45; }
|
||||||
|
&--no-phone { opacity: 0.55; filter: grayscale(0.35); }
|
||||||
|
|
||||||
&__check { flex-shrink: 0; }
|
&__check { flex-shrink: 0; }
|
||||||
&__info { flex: 1; min-width: 0; }
|
&__info { flex: 1; min-width: 0; }
|
||||||
@@ -919,9 +1076,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
thead tr { background: var(--admin-bg-muted); border-bottom: 1px solid var(--admin-border); }
|
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; }
|
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 { border-bottom: 1px solid var(--admin-border); background: var(--admin-bg-surface); transition: background 0.15s; }
|
||||||
|
tbody tr:nth-child(even) { background: var(--admin-bg-surface); }
|
||||||
tbody tr:hover { background: var(--admin-bg-hover); }
|
tbody tr:hover { background: var(--admin-bg-hover); }
|
||||||
tbody tr.cs-table__row--done { opacity: 0.45; }
|
tbody tr.cs-table__row--done { opacity: 0.45; }
|
||||||
|
tbody tr.cs-table__row--no-phone { opacity: 0.55; filter: grayscale(0.35); }
|
||||||
td { padding: 10px 14px; color: var(--admin-text); }
|
td { padding: 10px 14px; color: var(--admin-text); }
|
||||||
|
|
||||||
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
||||||
@@ -929,6 +1088,46 @@ onMounted(() => {
|
|||||||
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
||||||
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
||||||
&__action { text-align: center; white-space: nowrap; }
|
&__action { text-align: center; white-space: nowrap; }
|
||||||
|
&__mande { font-size: 11px; color: var(--admin-text-secondary); white-space: nowrap; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-skip-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(148,163,184,0.18);
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-link-banner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.cs-link-banner {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cs-link-banner__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
.cs-link-banner__warn {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-done-badge {
|
.cs-done-badge {
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tw-bulk">
|
||||||
|
<div class="tw-bulk__row">
|
||||||
|
<div class="tw-bulk__copy min-w-0 flex-1">
|
||||||
|
<div class="tw-bulk__title-row">
|
||||||
|
<p class="tw-bulk__title">همگامسازی مانده زرین</p>
|
||||||
|
<AdminFieldHelp :text="accountingZarinBalanceSyncIntro" size="sm" />
|
||||||
|
</div>
|
||||||
|
<p class="tw-bulk__hint">
|
||||||
|
کیف مشتریان لینکشده با مانده زرین؛ تداخلها برای تصمیم دستی میمانند.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="tw-bulk__actions">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="running && !overwriteMode"
|
||||||
|
:disabled="running"
|
||||||
|
@click="confirmAndStart(false)"
|
||||||
|
>
|
||||||
|
همگامسازی
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
:loading="running && overwriteMode"
|
||||||
|
:disabled="running"
|
||||||
|
@click="confirmAndStart(true)"
|
||||||
|
>
|
||||||
|
اعمال اجباری
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="running || resultVisible" class="tw-bulk__progress">
|
||||||
|
<template v-if="running">
|
||||||
|
<div class="tw-bulk__progress-row">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>
|
||||||
|
در حال همگامسازی…
|
||||||
|
<template v-if="total > 0">{{ processed }} / {{ total }}</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw-bulk__track">
|
||||||
|
<div class="tw-bulk__track-fill" :style="{ width: percent + '%' }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="resultVisible">
|
||||||
|
<div class="tw-bulk__progress-row">
|
||||||
|
<span class="tw-bulk__ok">✓ {{ success }} موفق</span>
|
||||||
|
<span v-if="changed" class="tw-bulk__changed">{{ changed }} تغییر</span>
|
||||||
|
<span v-if="needsDecision" class="tw-bulk__decision">{{ needsDecision }} تصمیم</span>
|
||||||
|
<span v-if="failed" class="tw-bulk__fail">✗ {{ failed }} ناموفق</span>
|
||||||
|
<a-button
|
||||||
|
v-if="errors.length || decisions.length"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="showDetails = !showDetails"
|
||||||
|
>
|
||||||
|
{{ showDetails ? 'پنهان' : 'جزئیات' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="dismissResult">بستن</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDetails && decisions.length" class="tw-bulk__decisions">
|
||||||
|
<div class="tw-bulk__decisions-title">تداخل مانده — کدام را نگه داریم؟ (جمع نمیشود)</div>
|
||||||
|
<div v-for="d in decisions" :key="d.id" class="tw-bulk__decision-row">
|
||||||
|
<div>
|
||||||
|
<b>{{ d.name }}</b>
|
||||||
|
<template v-if="d.mobile"> ({{ d.mobile }})</template>
|
||||||
|
— اپ: {{ formatGold(d.app_gold) }} طلا / {{ formatCash(d.app_cash) }} ریال
|
||||||
|
· زرین: {{ formatGold(d.tahesab_gold) }} طلا / {{ formatCash(d.tahesab_cash) }} ریال
|
||||||
|
</div>
|
||||||
|
<div class="tw-bulk__decision-actions">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:loading="decidingId === d.id && decidingMode === 'overwrite'"
|
||||||
|
:disabled="!!decidingId"
|
||||||
|
@click="decideOne(d.id, 'overwrite')"
|
||||||
|
>
|
||||||
|
اعمال زرین
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
:loading="decidingId === d.id && decidingMode === 'keep'"
|
||||||
|
:disabled="!!decidingId"
|
||||||
|
@click="decideOne(d.id, 'keep')"
|
||||||
|
>
|
||||||
|
حفظ اپ
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDetails && errors.length" class="tw-bulk__errors">
|
||||||
|
<div v-for="e in errors" :key="e.id">
|
||||||
|
<b>{{ e.name }}</b>
|
||||||
|
<template v-if="e.mobile"> ({{ e.mobile }})</template>
|
||||||
|
— {{ e.error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, createVNode, ref } from 'vue'
|
||||||
|
import { Modal, message } from 'ant-design-vue'
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import {
|
||||||
|
syncAllWalletsFromZarinChunk,
|
||||||
|
syncCustomerWalletFromZarin,
|
||||||
|
type ZarinBulkWalletSyncChunk,
|
||||||
|
} from '@/api/zarin'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import { accountingZarinBalanceSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
|
const CHUNK_LIMIT = 20
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
|
||||||
|
const running = ref(false)
|
||||||
|
const overwriteMode = ref(false)
|
||||||
|
const resultVisible = ref(false)
|
||||||
|
const showDetails = ref(false)
|
||||||
|
|
||||||
|
const total = ref(0)
|
||||||
|
const processed = ref(0)
|
||||||
|
const success = ref(0)
|
||||||
|
const changed = ref(0)
|
||||||
|
const needsDecision = ref(0)
|
||||||
|
const failed = ref(0)
|
||||||
|
const errors = ref<ZarinBulkWalletSyncChunk['errors']>([])
|
||||||
|
const decisions = ref<ZarinBulkWalletSyncChunk['decisions']>([])
|
||||||
|
const decidingId = ref<number | null>(null)
|
||||||
|
const decidingMode = ref<'overwrite' | 'keep' | null>(null)
|
||||||
|
|
||||||
|
const percent = computed(() =>
|
||||||
|
total.value > 0 ? Math.min(100, Math.round((processed.value / total.value) * 100)) : 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatGold(v?: number) {
|
||||||
|
if (v == null || Number.isNaN(v)) return '—'
|
||||||
|
return Number(v).toLocaleString('fa-IR', { maximumFractionDigits: 3 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCash(v?: number) {
|
||||||
|
if (v == null || Number.isNaN(v)) return '—'
|
||||||
|
return Math.round(Number(v)).toLocaleString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissResult() {
|
||||||
|
resultVisible.value = false
|
||||||
|
showDetails.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decideOne(customerId: number, mode: 'overwrite' | 'keep') {
|
||||||
|
decidingId.value = customerId
|
||||||
|
decidingMode.value = mode
|
||||||
|
try {
|
||||||
|
const res = await syncCustomerWalletFromZarin(customerId, {
|
||||||
|
overwrite: mode === 'overwrite',
|
||||||
|
keep: mode === 'keep',
|
||||||
|
ask_if_conflict: false,
|
||||||
|
})
|
||||||
|
decisions.value = decisions.value.filter(d => d.id !== customerId)
|
||||||
|
needsDecision.value = Math.max(0, needsDecision.value - 1)
|
||||||
|
if (mode === 'keep') message.info(res.message || 'موجودی اپ حفظ شد')
|
||||||
|
else message.success(res.message || 'مانده زرین اعمال شد')
|
||||||
|
} catch {
|
||||||
|
message.error('اعمال تصمیم ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
decidingId.value = null
|
||||||
|
decidingMode.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAndStart(overwrite: boolean) {
|
||||||
|
if (running.value) return
|
||||||
|
Modal.confirm({
|
||||||
|
title: overwrite
|
||||||
|
? 'اعمال اجباری مانده زرین برای همه؟'
|
||||||
|
: 'همگامسازی ماندهٔ همهٔ مشتریان؟',
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
content: overwrite
|
||||||
|
? 'موجودی کیف همهٔ مشتریان دارای کد زرین با ماندهٔ زرین جایگزین میشود — حتی اگر در اپ مانده داشته باشند.'
|
||||||
|
: 'برای مشتریانی که کیف خالی دارند مانده زرین مینشیند. اگر اپ مانده داشته باشد و فرق کند، در لیست «نیازمند تصمیم» میآید.',
|
||||||
|
okText: overwrite ? 'بله، برای همه اعمال کن' : 'شروع همگامسازی',
|
||||||
|
okType: overwrite ? 'danger' : 'primary',
|
||||||
|
cancelText: 'انصراف',
|
||||||
|
centered: true,
|
||||||
|
onOk: () => start(overwrite),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start(overwrite: boolean) {
|
||||||
|
running.value = true
|
||||||
|
overwriteMode.value = overwrite
|
||||||
|
resultVisible.value = false
|
||||||
|
showDetails.value = false
|
||||||
|
total.value = 0
|
||||||
|
processed.value = 0
|
||||||
|
success.value = 0
|
||||||
|
changed.value = 0
|
||||||
|
needsDecision.value = 0
|
||||||
|
failed.value = 0
|
||||||
|
errors.value = []
|
||||||
|
decisions.value = []
|
||||||
|
|
||||||
|
let offset = 0
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (!done) {
|
||||||
|
const res = await syncAllWalletsFromZarinChunk({
|
||||||
|
offset,
|
||||||
|
limit: CHUNK_LIMIT,
|
||||||
|
overwrite,
|
||||||
|
})
|
||||||
|
|
||||||
|
total.value = res.total
|
||||||
|
success.value += res.success
|
||||||
|
changed.value += res.changed
|
||||||
|
needsDecision.value += res.needs_decision ?? 0
|
||||||
|
failed.value += res.failed
|
||||||
|
if (res.errors?.length) errors.value = [...errors.value, ...res.errors]
|
||||||
|
if (res.decisions?.length) decisions.value = [...decisions.value, ...res.decisions]
|
||||||
|
|
||||||
|
processed.value = Math.min(res.next_offset, res.total)
|
||||||
|
offset = res.next_offset
|
||||||
|
done = res.done || res.processed === 0 || res.total === 0
|
||||||
|
|
||||||
|
if (!done) await sleep(150)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultVisible.value = true
|
||||||
|
if (failed.value === 0 && needsDecision.value === 0) {
|
||||||
|
message.success(
|
||||||
|
total.value === 0
|
||||||
|
? 'مشتری دارای کد زرین یافت نشد'
|
||||||
|
: `همگامسازی ${success.value} مشتری انجام شد`,
|
||||||
|
)
|
||||||
|
} else if (success.value > 0 || needsDecision.value > 0) {
|
||||||
|
message.warning(
|
||||||
|
`${success.value} موفق` +
|
||||||
|
(needsDecision.value ? ` — ${needsDecision.value} نیازمند تصمیم` : '') +
|
||||||
|
(failed.value ? ` — ${failed.value} ناموفق` : ''),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
message.error('همگامسازی ناموفق بود')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error('خطا در ارتباط با سرور')
|
||||||
|
resultVisible.value = processed.value > 0 || success.value > 0 || failed.value > 0
|
||||||
|
} finally {
|
||||||
|
running.value = false
|
||||||
|
overwriteMode.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tw-bulk {
|
||||||
|
@apply rounded-lg border border-base p-3;
|
||||||
|
background: var(--admin-bg-surface, var(--admin-bg-elevated));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* موبایل: ستون — دکمهها زیر توضیح | دسکتاپ: ردیف */
|
||||||
|
.tw-bulk__row {
|
||||||
|
@apply flex flex-col items-stretch gap-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tw-bulk__row {
|
||||||
|
@apply flex-row items-center gap-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__title {
|
||||||
|
@apply m-0 text-sm font-600;
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__title-row {
|
||||||
|
@apply flex items-center gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__hint {
|
||||||
|
@apply m-0 mt-0.5 text-xs leading-5;
|
||||||
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__actions {
|
||||||
|
@apply flex flex-wrap items-center gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tw-bulk__actions {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__progress {
|
||||||
|
@apply mt-2 rounded-md border border-base p-2 text-xs;
|
||||||
|
background: var(--admin-bg-muted, transparent);
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__progress-row {
|
||||||
|
@apply flex flex-wrap items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__track {
|
||||||
|
@apply mt-1.5 h-1 overflow-hidden rounded-full;
|
||||||
|
background: var(--admin-border, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__track-fill {
|
||||||
|
@apply h-full rounded-full transition-all duration-300;
|
||||||
|
background: var(--admin-primary, #d4af37);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__ok {
|
||||||
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
|
background: rgba(52, 211, 153, 0.15);
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__changed {
|
||||||
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
|
background: rgba(14, 165, 233, 0.12);
|
||||||
|
color: #0284c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__decision {
|
||||||
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__fail {
|
||||||
|
@apply rounded-full px-2 py-0.5 font-600;
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__decisions {
|
||||||
|
@apply mt-1.5 max-h-32 overflow-y-auto rounded-md p-2 text-xs leading-6;
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__decision-row {
|
||||||
|
@apply mb-2 flex flex-col gap-1.5 border-b pb-2 last:mb-0 last:border-0 last:pb-0;
|
||||||
|
border-color: rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
.tw-bulk__decision-actions {
|
||||||
|
@apply flex flex-wrap gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tw-bulk__errors {
|
||||||
|
@apply mt-1.5 max-h-24 overflow-y-auto rounded-md p-2 text-xs leading-6;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="اسناد پرگار">
|
<BasicShowContext title="اسناد پرگار" :help="accountingPargarDocsIntro">
|
||||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
@@ -137,6 +137,7 @@ import {
|
|||||||
pargarTypeLabel
|
pargarTypeLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/pargarDisplay'
|
} from '@/pages/menuPages/accounting/utils/pargarDisplay'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { accountingPargarDocsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="حسابداری">
|
<BasicShowContext title="حسابداری" :help="accountingReportsPageIntro">
|
||||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" />
|
<BaseTable v-bind="tblProps" @register="submit" />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,6 +18,7 @@ import AccountingMobileCardList from '@/pages/menuPages/accounting/components/Ac
|
|||||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { accountingReportsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext :title="customerId ? 'اسناد تهحساب این مشتری' : 'اسناد تهحساب'">
|
<BasicShowContext
|
||||||
|
:title="customerId ? 'اسناد تهحساب این مشتری' : 'اسناد تهحساب'"
|
||||||
|
:help="accountingTahesabDocsIntro"
|
||||||
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="(pending?.total ?? 0) > 0"
|
v-if="(pending?.total ?? 0) > 0"
|
||||||
@@ -213,6 +216,7 @@ import {
|
|||||||
rialAmountLabel
|
rialAmountLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { accountingTahesabDocsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customerId?: number
|
customerId?: number
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext :title="customerId ? 'اسناد زرین این مشتری' : 'اسناد زرین'">
|
<BasicShowContext
|
||||||
|
:title="customerId ? 'اسناد زرین این مشتری' : 'اسناد زرین'"
|
||||||
|
:help="accountingZarinDocsIntro"
|
||||||
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="(pending?.total ?? 0) > 0"
|
v-if="(pending?.total ?? 0) > 0"
|
||||||
@@ -212,6 +215,7 @@ import {
|
|||||||
rialAmountLabel
|
rialAmountLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { accountingZarinDocsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customerId?: number
|
customerId?: number
|
||||||
|
|||||||
@@ -3,25 +3,31 @@
|
|||||||
<BasicMainCard>
|
<BasicMainCard>
|
||||||
<div
|
<div
|
||||||
class="flex h-full w-full min-w-0 max-w-full flex-col overflow-x-hidden"
|
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' || activeTab === 'zarin-customer-sync' ? 'overflow-y-auto' : 'overflow-hidden'"
|
:class="activeTab === 'customer-sync' || activeTab === 'pargar-customer-sync' || activeTab === 'zarin-customer-sync' || activeTab === 'tahesab-wallet-sync' || activeTab === 'zarin-wallet-sync' || activeTab === 'pargar-wallet-sync' ? 'overflow-y-auto' : 'overflow-hidden'"
|
||||||
>
|
>
|
||||||
<PargarStuckOutboundAlert v-if="pargarEnabled" class="mb-3 shrink-0" />
|
<PargarStuckOutboundAlert v-if="pargarEnabled" class="mb-3 shrink-0" />
|
||||||
<ATabs v-model:activeKey="activeTab" class="mb-3 shrink-0">
|
<ATabs v-model:activeKey="activeTab" class="mb-3 shrink-0">
|
||||||
<ATabPane key="reports" tab="گزارشات" />
|
<ATabPane key="reports" tab="گزارشات" />
|
||||||
<ATabPane v-if="tahesabEnabled" key="tahesab" tab="اسناد تهحساب" />
|
<ATabPane v-if="tahesabEnabled" key="tahesab" tab="اسناد تهحساب" />
|
||||||
<ATabPane v-if="tahesabEnabled" key="customer-sync" tab="همگام سازی مشتریان در تهحساب" />
|
<ATabPane v-if="tahesabEnabled" key="customer-sync" tab="همگام سازی مشتریان در تهحساب" />
|
||||||
|
<ATabPane v-if="tahesabEnabled" key="tahesab-wallet-sync" tab="مانده کیف تهحساب" />
|
||||||
<ATabPane v-if="zarinEnabled" key="zarin" tab="اسناد زرین" />
|
<ATabPane v-if="zarinEnabled" key="zarin" tab="اسناد زرین" />
|
||||||
<ATabPane v-if="zarinEnabled" key="zarin-customer-sync" tab="همگام سازی مشتریان در زرین" />
|
<ATabPane v-if="zarinEnabled" key="zarin-customer-sync" tab="همگام سازی مشتریان در زرین" />
|
||||||
|
<ATabPane v-if="zarinEnabled" key="zarin-wallet-sync" tab="مانده کیف زرین" />
|
||||||
<ATabPane v-if="pargarEnabled" key="pargar" tab="اسناد پرگار" />
|
<ATabPane v-if="pargarEnabled" key="pargar" tab="اسناد پرگار" />
|
||||||
<ATabPane v-if="pargarEnabled" key="pargar-customer-sync" tab="همگام سازی مشتریان در پرگار" />
|
<ATabPane v-if="pargarEnabled" key="pargar-customer-sync" tab="همگام سازی مشتریان در پرگار" />
|
||||||
|
<ATabPane v-if="pargarEnabled" key="pargar-wallet-sync" tab="مانده کیف پرگار" />
|
||||||
</ATabs>
|
</ATabs>
|
||||||
<tbl v-if="activeTab === 'reports'" ref="table" class="min-h-0 flex-1" />
|
<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" />
|
<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" />
|
<customerSync v-else-if="tahesabEnabled && activeTab === 'customer-sync'" class="shrink-0" />
|
||||||
|
<tahesabWalletSync v-else-if="tahesabEnabled && activeTab === 'tahesab-wallet-sync'" class="min-h-0 flex-1" />
|
||||||
<zarinTbl v-else-if="zarinEnabled && activeTab === 'zarin'" ref="zarinTable" class="min-h-0 flex-1" />
|
<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" />
|
<zarinCustomerSync v-else-if="zarinEnabled && activeTab === 'zarin-customer-sync'" class="shrink-0" />
|
||||||
|
<zarinWalletSync v-else-if="zarinEnabled && activeTab === 'zarin-wallet-sync'" class="shrink-0" />
|
||||||
<pargarTbl v-else-if="pargarEnabled && activeTab === 'pargar'" ref="pargarTable" class="min-h-0 flex-1" />
|
<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" />
|
<pargarCustomerSync v-else-if="pargarEnabled && activeTab === 'pargar-customer-sync'" class="shrink-0" />
|
||||||
|
<pargarWalletSync v-else-if="pargarEnabled && activeTab === 'pargar-wallet-sync'" class="shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</BasicMainCard>
|
</BasicMainCard>
|
||||||
<BasicModal
|
<BasicModal
|
||||||
@@ -148,14 +154,17 @@
|
|||||||
// import BasicInfoModal from '@/components/BasicInfoModal'
|
// import BasicInfoModal from '@/components/BasicInfoModal'
|
||||||
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules'
|
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules'
|
||||||
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import tbl from './components/table.vue'
|
import tbl from './components/table.vue'
|
||||||
import tahesabTbl from './components/tahesabTable.vue'
|
import tahesabTbl from './components/tahesabTable.vue'
|
||||||
import customerSync from './components/TahesabCustomerSync.vue'
|
import customerSync from './components/TahesabCustomerSync.vue'
|
||||||
|
import tahesabWalletSync from './components/TahesabWalletBulkSync.vue'
|
||||||
import zarinTbl from './components/zarinTable.vue'
|
import zarinTbl from './components/zarinTable.vue'
|
||||||
import zarinCustomerSync from './components/ZarinCustomerSync.vue'
|
import zarinCustomerSync from './components/ZarinCustomerSync.vue'
|
||||||
|
import zarinWalletSync from './components/ZarinWalletBulkSync.vue'
|
||||||
import pargarTbl from './components/pargarTable.vue'
|
import pargarTbl from './components/pargarTable.vue'
|
||||||
import pargarCustomerSync from './components/PargarCustomerSync.vue'
|
import pargarCustomerSync from './components/PargarCustomerSync.vue'
|
||||||
|
import pargarWalletSync from './components/PargarWalletBulkSync.vue'
|
||||||
import PargarStuckOutboundAlert from './components/PargarStuckOutboundAlert.vue'
|
import PargarStuckOutboundAlert from './components/PargarStuckOutboundAlert.vue'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
||||||
@@ -179,7 +188,7 @@ import { initAccountingSettingsFromApi, tahesabEnabledSetting, zarinEnabledSetti
|
|||||||
//loading
|
//loading
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const submitError = ref('')
|
const submitError = ref('')
|
||||||
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'zarin' | 'zarin-customer-sync' | 'pargar' | 'pargar-customer-sync'>('reports')
|
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'tahesab-wallet-sync' | 'zarin' | 'zarin-customer-sync' | 'zarin-wallet-sync' | 'pargar' | 'pargar-customer-sync' | 'pargar-wallet-sync'>('reports')
|
||||||
const tahesabEnabled = tahesabEnabledSetting
|
const tahesabEnabled = tahesabEnabledSetting
|
||||||
const zarinEnabled = zarinEnabledSetting
|
const zarinEnabled = zarinEnabledSetting
|
||||||
const pargarEnabled = pargarEnabledSetting
|
const pargarEnabled = pargarEnabledSetting
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="اعلانهای اپ">
|
<BasicShowContext title="اعلانهای اپ" :help="appNotificationsPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<a-radio-group
|
<a-radio-group
|
||||||
@@ -17,9 +17,6 @@
|
|||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</a-radio-button>
|
</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<AdminFieldHelp
|
|
||||||
text="اعلان ادمین را دستی میفرستید؛ اعلان سیستمی خودکار برای خرید/فروش/واریز/برداشت ساخته میشود. از تبها برای جدا کردن لیست استفاده کنید."
|
|
||||||
/>
|
|
||||||
<BaseButton type="primary" size="small" @click="emits('add')">
|
<BaseButton type="primary" size="small" @click="emits('add')">
|
||||||
<span>افزودن اعلان</span>
|
<span>افزودن اعلان</span>
|
||||||
<BaseIcon icon="vuesax-linear:add" size="16" />
|
<BaseIcon icon="vuesax-linear:add" size="16" />
|
||||||
@@ -89,7 +86,7 @@ import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
|||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { cols } from '../hooks/appNotificationsColumns'
|
import { cols } from '../hooks/appNotificationsColumns'
|
||||||
import { getAppNotificationsTable } from '@/api/appNotifications'
|
import { getAppNotificationsTable } from '@/api/appNotifications'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
import { appNotificationsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
import {
|
import {
|
||||||
APP_NOTIFICATION_SOURCE_TABS,
|
APP_NOTIFICATION_SOURCE_TABS,
|
||||||
type AppNotificationSourceFilter,
|
type AppNotificationSourceFilter,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="اتوماسیون">
|
<BasicShowContext title="اتوماسیون" :help="automationsPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
|
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
|
||||||
افزودن
|
افزودن
|
||||||
@@ -103,6 +103,7 @@ import { cols } from '../hooks/automationsColumns'
|
|||||||
import { getAutomationsTable } from '@/api/automations'
|
import { getAutomationsTable } from '@/api/automations'
|
||||||
import type { Automation } from '@/model/automations'
|
import type { Automation } from '@/model/automations'
|
||||||
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
||||||
|
import { automationsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
|
||||||
<SettingsProfileBanners v-if="showSettingsProfileBanners" />
|
<SettingsProfileBanners v-if="showThemeBanners" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -133,7 +133,7 @@ import SettingsProfileBanners from './components/SettingsProfileBanners.vue'
|
|||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
|
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
|
||||||
import { useAppBannersAdmin } from '@/composables/useAppBannersAdmin'
|
import { useAppBannersAdmin } from '@/composables/useAppBannersAdmin'
|
||||||
import { hasAppUiConfig } from '@/utils/config'
|
import { isModularUiVariant } from '@/utils/config'
|
||||||
import {
|
import {
|
||||||
bannersEventHelp,
|
bannersEventHelp,
|
||||||
bannersImageSectionHelp,
|
bannersImageSectionHelp,
|
||||||
@@ -144,12 +144,12 @@ import {
|
|||||||
bannersShareHelp,
|
bannersShareHelp,
|
||||||
bannersShowHelp,
|
bannersShowHelp,
|
||||||
} from '@/utils/adminHelpCopy'
|
} from '@/utils/adminHelpCopy'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const showThemeBanners = isModularUiVariant()
|
||||||
const { state, saving, save, load, saveSoon } = useAppBannersAdmin()
|
const { state, saving, save, load, saveSoon } = useAppBannersAdmin()
|
||||||
/** اگر اسلایدری هست، پنل را باز نگه دار تا خالی بهنظر نرسد */
|
/** اگر اسلایدری هست، پنل را باز نگه دار تا خالی بهنظر نرسد */
|
||||||
const sliderActiveKey = ref<(string | number)[]>(['slider'])
|
const sliderActiveKey = ref<(string | number)[]>(['slider'])
|
||||||
const showSettingsProfileBanners = computed(() => hasAppUiConfig())
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.slider.length,
|
() => state.slider.length,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="capture-sites-page">
|
<div class="capture-sites-page">
|
||||||
<BasicShowContext title="لیست سایتهای ریگیری">
|
<BasicShowContext title="لیست سایتهای ریگیری" :help="captureSitesPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="capture-sites-page__toolbar">
|
<div class="capture-sites-page__toolbar">
|
||||||
<label v-if="enabledRow" class="capture-sites-page__switch">
|
<label v-if="enabledRow" class="capture-sites-page__switch">
|
||||||
@@ -118,6 +118,7 @@ import {
|
|||||||
serializeTrackingSystemsEnabled,
|
serializeTrackingSystemsEnabled,
|
||||||
} from '@/utils/reygiriSites'
|
} from '@/utils/reygiriSites'
|
||||||
import { setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
import { setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
||||||
|
import { captureSitesPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const sites = ref<CaptureSite[]>([])
|
const sites = ref<CaptureSite[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست حساب ها">
|
<BasicShowContext title="لیست حساب ها" :help="cardsPageIntro">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #extraHeader>
|
<template #extraHeader>
|
||||||
@@ -62,6 +62,7 @@ import { changeAccountSts, getAccountTable } from '@/api/card'
|
|||||||
import type { Card } from '@/model/card'
|
import type { Card } from '@/model/card'
|
||||||
import AccountMobileCardList from './AccountMobileCardList.vue'
|
import AccountMobileCardList from './AccountMobileCardList.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { cardsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<section v-if="visible" class="colleague-accounts">
|
<section
|
||||||
<header class="colleague-accounts__head">
|
v-if="visible"
|
||||||
<div>
|
class="colleague-accounts w-full flex flex-col gap-2.5 rounded-lg border border-base p-2.5 sm:grid sm:grid-cols-2 sm:items-end sm:gap-3"
|
||||||
<h3 class="colleague-accounts__title">کد حسابداری معامله با همکار</h3>
|
>
|
||||||
<p class="colleague-accounts__desc">
|
<div class="min-w-0">
|
||||||
برای هر خرید/فروشی که نزد همکار ثبت میشود، سند روی این کد حساب در سیستم حسابداری فعال زده میشود.
|
<h3 class="m-0 inline-flex items-center gap-1.5 text-sm font-600" style="color: var(--admin-text)">
|
||||||
|
کد حسابداری معامله با همکار
|
||||||
|
<AdminFieldHelp :text="colleagueAccountingCodeHelp" size="sm" />
|
||||||
|
</h3>
|
||||||
|
<p class="m-0 mt-0.5 text-xs leading-5" style="color: var(--admin-text-secondary)">
|
||||||
|
سند خرید/فروش پوشششده روی این کد در حسابداری فعال ثبت میشود.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BaseButton type="primary" :loading="saving" @click="save">
|
|
||||||
ذخیره کد
|
|
||||||
</BaseButton>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<label
|
<label v-if="activeField" class="m-0 flex w-full min-w-0 flex-col gap-1">
|
||||||
v-if="activeField"
|
<span class="inline-flex items-center gap-1.5 text-xs font-600" style="color: var(--admin-text)">
|
||||||
class="colleague-accounts__field colleague-accounts__field--active"
|
|
||||||
>
|
|
||||||
<span class="colleague-accounts__label">
|
|
||||||
{{ activeField.label }}
|
{{ activeField.label }}
|
||||||
<span class="colleague-accounts__badge">فعال</span>
|
<span class="colleague-accounts__badge">فعال</span>
|
||||||
</span>
|
</span>
|
||||||
|
<div class="flex w-full min-w-0 items-center gap-2">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="values[activeField.key]"
|
v-model:value="values[activeField.key]"
|
||||||
:placeholder="activeField.placeholder"
|
:placeholder="activeField.placeholder"
|
||||||
allow-clear
|
allow-clear
|
||||||
class="colleague-accounts__input"
|
size="small"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
/>
|
/>
|
||||||
|
<BaseButton type="primary" size="small" class="shrink-0" :loading="saving" @click="save">
|
||||||
|
ذخیره
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,6 +39,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
||||||
import type { Const } from '@/model/configuration'
|
import type { Const } from '@/model/configuration'
|
||||||
|
import { colleagueAccountingCodeHelp } from '@/utils/adminHelpCopy'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
import {
|
import {
|
||||||
accountingProviderSetting,
|
accountingProviderSetting,
|
||||||
initWalletSettingsFromApi,
|
initWalletSettingsFromApi,
|
||||||
@@ -169,73 +175,14 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped>
|
||||||
.colleague-accounts {
|
.colleague-accounts {
|
||||||
margin-bottom: 1rem;
|
background: var(--admin-bg-surface, var(--admin-bg-elevated, rgba(28, 28, 34, 0.92)));
|
||||||
padding: 1rem 1.125rem;
|
|
||||||
border: 1px solid var(--admin-border, rgba(255, 255, 255, 0.12));
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--admin-bg-elevated, rgba(28, 28, 34, 0.92));
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__head {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--admin-text, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__desc {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
max-width: 42rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: var(--admin-text-secondary, rgba(255, 255, 255, 0.65));
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__field {
|
|
||||||
display: flex;
|
|
||||||
max-width: 28rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid color-mix(in srgb, #d4af37 55%, transparent);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: color-mix(in srgb, var(--admin-bg-muted, #1a1a1f) 80%, transparent);
|
|
||||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, #d4af37 18%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--admin-text, #fff);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.colleague-accounts__badge {
|
.colleague-accounts__badge {
|
||||||
display: inline-flex;
|
@apply inline-flex items-center rounded-full px-1.5 text-10px font-700;
|
||||||
align-items: center;
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(212, 175, 55, 0.18);
|
background: rgba(212, 175, 55, 0.18);
|
||||||
color: #d4af37;
|
color: #d4af37;
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colleague-accounts__input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="تراکنش با اپلیکیشن همکار">
|
<BasicShowContext title="تراکنش با اپلیکیشن همکار" :help="colleagueOrdersPageIntro">
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" />
|
<BaseTable v-bind="tblProps" @register="submit" />
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +14,7 @@ import { getColleagueOrderTable } from '@/api/order'
|
|||||||
import { getColleagueOrderColumns } from '@/pages/menuPages/colleagueOrders/hooks/colleagueOrderColumns'
|
import { getColleagueOrderColumns } from '@/pages/menuPages/colleagueOrders/hooks/colleagueOrderColumns'
|
||||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
|
import { colleagueOrdersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="سفارش شرطی">
|
<BasicShowContext title="سفارش شرطی" :help="conditionalOrdersPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<BaseButton type="primary" size="small" @click="emits('add')">
|
<BaseButton type="primary" size="small" @click="emits('add')">
|
||||||
@@ -53,6 +53,7 @@ import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
|||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { cols } from '../hooks/conditionalOrdersColumns'
|
import { cols } from '../hooks/conditionalOrdersColumns'
|
||||||
import { getConditionalOrdersTable } from '@/api/conditionalOrders'
|
import { getConditionalOrdersTable } from '@/api/conditionalOrders'
|
||||||
|
import { conditionalOrdersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0">
|
<div class="h-full min-h-0">
|
||||||
<BasicShowContent :title="pageTitle" class="h-full">
|
<BasicShowContent :title="pageTitle" :help="configurationPageIntro" class="h-full">
|
||||||
<div class="settings-toolbar">
|
<div class="settings-toolbar">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="search"
|
v-model:value="search"
|
||||||
@@ -207,6 +207,7 @@ import { loading } from '@/utils'
|
|||||||
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration'
|
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration'
|
||||||
import SettingValueField from './SettingValueField.vue'
|
import SettingValueField from './SettingValueField.vue'
|
||||||
import SettingCard from './SettingCard.vue'
|
import SettingCard from './SettingCard.vue'
|
||||||
|
import { configurationPageIntro } from '@/utils/adminHelpCopy'
|
||||||
import { refreshAppModeMenu } from '@/router'
|
import { refreshAppModeMenu } from '@/router'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyGoldCreditEnabledFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings, setGoldCreditEnabledFromSettings } from '@/utils/config'
|
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyGoldCreditEnabledFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings, setGoldCreditEnabledFromSettings } from '@/utils/config'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
title: 'عمومی و برند',
|
title: 'عمومی و برند',
|
||||||
icon: 'vuesax-linear:home-2',
|
icon: 'vuesax-linear:home-2',
|
||||||
match: (k) =>
|
match: (k) =>
|
||||||
/^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$|enamad_url|enamad_logo)$/.test(
|
/^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$|enamad_url|enamad_logo|login_hero_)/.test(
|
||||||
k
|
k
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -174,7 +174,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
title: 'عمومی و برند',
|
title: 'عمومی و برند',
|
||||||
icon: 'vuesax-linear:home-2',
|
icon: 'vuesax-linear:home-2',
|
||||||
match: (k) =>
|
match: (k) =>
|
||||||
/^(name_fa|name_en|app_url|admin_url|apk_download_link|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|sms_panel|main_wallet_products|enamad_url|enamad_logo)/.test(
|
/^(name_fa|name_en|app_url|admin_url|apk_download_link|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|sms_panel|main_wallet_products|enamad_url|enamad_logo|login_hero_)/.test(
|
||||||
k
|
k
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,12 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
|||||||
about_software: 'longtext',
|
about_software: 'longtext',
|
||||||
enamad_url: 'text',
|
enamad_url: 'text',
|
||||||
enamad_logo: 'text',
|
enamad_logo: 'text',
|
||||||
|
login_hero_eyebrow: 'text',
|
||||||
|
login_hero_title: 'text',
|
||||||
|
login_hero_feature_1: 'text',
|
||||||
|
login_hero_feature_2: 'text',
|
||||||
|
login_hero_feature_3: 'text',
|
||||||
|
login_hero_feature_4: 'text',
|
||||||
fee_service_charge_percentage: 'number',
|
fee_service_charge_percentage: 'number',
|
||||||
fee_deduct_from_gold: 'switch',
|
fee_deduct_from_gold: 'switch',
|
||||||
require_zero_main_gold_before_other_buy: 'switch',
|
require_zero_main_gold_before_other_buy: 'switch',
|
||||||
@@ -667,8 +673,20 @@ export function parseSwitchHelp(help: string | undefined | null): SwitchHelpPart
|
|||||||
const KEY_HELP_FALLBACK: Record<string, string> = {
|
const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||||
name_fa:
|
name_fa:
|
||||||
'نام فارسی برند که در پیامکها، عنوان صفحات و بخشهای عمومی اپ به مشتری نشان داده میشود.',
|
'نام فارسی برند که در پیامکها، عنوان صفحات و بخشهای عمومی اپ به مشتری نشان داده میشود.',
|
||||||
|
login_hero_eyebrow:
|
||||||
|
'متن بالای عنوان طلا در ستون چپ صفحه ورود دسکتاپ (تم VIP). خالی = پیشفرض «سرمایهگذاری هوشمند در».',
|
||||||
|
login_hero_title:
|
||||||
|
'عنوان طلایی ستون چپ صفحه ورود دسکتاپ (تم VIP). خالی = پیشفرض «طلای جهانی».',
|
||||||
|
login_hero_feature_1:
|
||||||
|
'اولین آیتم لیست ویژگیهای هیرو لاگین دسکتاپ. خالی = «خرید و فروش آنلاین طلا».',
|
||||||
|
login_hero_feature_2:
|
||||||
|
'دومین آیتم لیست ویژگیهای هیرو لاگین دسکتاپ. خالی = «قیمت لحظهای و دقیق».',
|
||||||
|
login_hero_feature_3:
|
||||||
|
'سومین آیتم لیست ویژگیهای هیرو لاگین دسکتاپ. خالی = «امنیت بالا و قابل اطمینان».',
|
||||||
|
login_hero_feature_4:
|
||||||
|
'چهارمین آیتم لیست ویژگیهای هیرو لاگین دسکتاپ. خالی = «پشتیبانی ۲۴/۷».',
|
||||||
banner_enabled:
|
banner_enabled:
|
||||||
'روشن: منوی «بنرها» در ادمین و اسلایدر صفحه اصلی اپ مشتری نمایش داده میشود. خاموش: مخفی است.',
|
'روشن: منوی «بنرها» در ادمین باز میشود (اسلایدر صفحه اصلی + تصاویر تم مثل بنر اشتراکگذاری/کد معرف). خاموش: منو مخفی است.',
|
||||||
permision__enabled:
|
permision__enabled:
|
||||||
'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده میشود. خاموش: مخفی است.',
|
'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده میشود. خاموش: مخفی است.',
|
||||||
open_register:
|
open_register:
|
||||||
@@ -1037,6 +1055,12 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
|||||||
show_product_card_meta: 'نمایش دسته و تحویل و مقدار روی کارت محصول',
|
show_product_card_meta: 'نمایش دسته و تحویل و مقدار روی کارت محصول',
|
||||||
enamad_url: 'آدرس اینماد',
|
enamad_url: 'آدرس اینماد',
|
||||||
enamad_logo: 'لوگوی اینماد',
|
enamad_logo: 'لوگوی اینماد',
|
||||||
|
login_hero_eyebrow: 'خط اول هیرو لاگین',
|
||||||
|
login_hero_title: 'عنوان هیرو لاگین',
|
||||||
|
login_hero_feature_1: 'ویژگی ۱ هیرو لاگین',
|
||||||
|
login_hero_feature_2: 'ویژگی ۲ هیرو لاگین',
|
||||||
|
login_hero_feature_3: 'ویژگی ۳ هیرو لاگین',
|
||||||
|
login_hero_feature_4: 'ویژگی ۴ هیرو لاگین',
|
||||||
fee_single: 'اختلاف پایه خرید/فروش',
|
fee_single: 'اختلاف پایه خرید/فروش',
|
||||||
buy_fee: 'اضافه قیمت خرید',
|
buy_fee: 'اضافه قیمت خرید',
|
||||||
sell_fee: 'کسر قیمت فروش',
|
sell_fee: 'کسر قیمت فروش',
|
||||||
@@ -1268,6 +1292,8 @@ export const SETTING_EDITABLE_KEYS = new Set([
|
|||||||
'do_send_liman',
|
'do_send_liman',
|
||||||
'has_chat',
|
'has_chat',
|
||||||
'text_about', 'text_rule', 'about_software', 'enamad_url', 'enamad_logo',
|
'text_about', 'text_rule', 'about_software', 'enamad_url', 'enamad_logo',
|
||||||
|
'login_hero_eyebrow', 'login_hero_title',
|
||||||
|
'login_hero_feature_1', 'login_hero_feature_2', 'login_hero_feature_3', 'login_hero_feature_4',
|
||||||
'zaarbox_price_key',
|
'zaarbox_price_key',
|
||||||
'price_channel_enabled', 'price_channel_messengers', 'price_channel_diff',
|
'price_channel_enabled', 'price_channel_messengers', 'price_channel_diff',
|
||||||
'price_channel_min_seconds', 'price_channel_footer_id', 'price_channel_footer_phone',
|
'price_channel_min_seconds', 'price_channel_footer_id', 'price_channel_footer_phone',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست مشتریان بایگانی شده">
|
<BasicShowContext title="لیست مشتریان بایگانی شده" :help="customersArchivePageIntro">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
@@ -51,6 +51,7 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
|||||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { customersArchivePageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'charge'])
|
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'charge'])
|
||||||
|
|||||||
@@ -9,7 +9,13 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-3xl font-bold text-gray-800 mb-2">احراز هویت</h2>
|
<h2 class="text-3xl font-bold text-gray-800 mb-2 inline-flex items-center justify-center gap-1.5">
|
||||||
|
احراز هویت
|
||||||
|
<AdminFieldHelp
|
||||||
|
text="با کد ملی و اطلاعات کارت بانکی، هویت فرد را از طریق سرویس استعلام بانکی بررسی میکند. نتیجه برای تایید مشتری در پنل استفاده میشود."
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
<p class="text-gray-500 text-sm">لطفاً اطلاعات کارت بانکی فرد مورد نظر را وارد کنید</p>
|
<p class="text-gray-500 text-sm">لطفاً اطلاعات کارت بانکی فرد مورد نظر را وارد کنید</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,6 +99,7 @@ import type { CheckCardRequest } from "@/model/user"
|
|||||||
import { checkCard } from "@/api/customer"
|
import { checkCard } from "@/api/customer"
|
||||||
import DatePicker from "@core/components/_sample/components/datePicker.vue";
|
import DatePicker from "@core/components/_sample/components/datePicker.vue";
|
||||||
import Icon from "@core/components/BaseIcon/src/Icon.vue";
|
import Icon from "@core/components/BaseIcon/src/Icon.vue";
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
const formState = reactive<CheckCardRequest>({
|
const formState = reactive<CheckCardRequest>({
|
||||||
// birth_date: '',
|
// birth_date: '',
|
||||||
|
|||||||
@@ -27,21 +27,26 @@
|
|||||||
برای پاک کردن کد، فیلد را خالی بگذارید و ذخیره کنید.
|
برای پاک کردن کد، فیلد را خالی بگذارید و ذخیره کنید.
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="provider === 'tahesab'"
|
v-if="provider === 'tahesab' || provider === 'zarin'"
|
||||||
class="m-0 text-xs"
|
class="m-0 text-xs"
|
||||||
style="color: var(--admin-text-secondary)"
|
style="color: var(--admin-text-secondary)"
|
||||||
>
|
>
|
||||||
بعد از ذخیره کد تهحساب، اگر موجودی اپ با مانده تهحساب فرق داشته باشد از شما تصمیم گرفته میشود.
|
قبل از ذخیره، اگر مانده اپ با حسابداری فرق داشته باشد از شما تأیید گرفته میشود؛ بعد اعمال میشود.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|
||||||
<TahesabMandeDecisionModal
|
<TahesabMandeDecisionModal
|
||||||
v-model:visible="decisionVisible"
|
v-model:visible="decisionVisible"
|
||||||
|
:source-label="provider === 'zarin' ? 'زرین' : 'تهحساب'"
|
||||||
:app-gold="decision?.app_gold"
|
:app-gold="decision?.app_gold"
|
||||||
:app-cash="decision?.app_cash"
|
:app-cash="decision?.app_cash"
|
||||||
:tahesab-gold="decision?.tahesab_gold"
|
:tahesab-gold="decision?.tahesab_gold ?? decision?.zarin_gold"
|
||||||
:tahesab-cash="decision?.tahesab_cash"
|
:tahesab-cash="decision?.tahesab_cash ?? decision?.zarin_cash"
|
||||||
|
:gold-delta="decision?.gold_delta"
|
||||||
|
:rial-delta="decision?.rial_delta"
|
||||||
|
:wallet-docs="decision?.wallet_docs"
|
||||||
|
:tahesab-docs="decision?.tahesab_docs"
|
||||||
@decide="onDecide"
|
@decide="onDecide"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,9 +57,14 @@ import { message } from 'ant-design-vue'
|
|||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import { updateCustomer } from '@/api/customer'
|
import { updateCustomer } from '@/api/customer'
|
||||||
import {
|
import {
|
||||||
|
previewCustomerWalletFromTahesab,
|
||||||
syncCustomerWalletFromTahesab,
|
syncCustomerWalletFromTahesab,
|
||||||
type TahesabWalletSyncResult,
|
type TahesabWalletSyncResult,
|
||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
|
import {
|
||||||
|
previewCustomerWalletFromZarin,
|
||||||
|
syncCustomerWalletFromZarin,
|
||||||
|
} from '@/api/zarin'
|
||||||
import type { Customer } from '@/model/customer'
|
import type { Customer } from '@/model/customer'
|
||||||
import type { AccountingProvider } from '@/utils/config'
|
import type { AccountingProvider } from '@/utils/config'
|
||||||
import { displayAccountingCode, normalizeAccountingCode } from '@/utils/accountingCode'
|
import { displayAccountingCode, normalizeAccountingCode } from '@/utils/accountingCode'
|
||||||
@@ -76,7 +86,7 @@ const loading = ref(false)
|
|||||||
const code = ref('')
|
const code = ref('')
|
||||||
const decisionVisible = ref(false)
|
const decisionVisible = ref(false)
|
||||||
const decision = ref<TahesabWalletSyncResult['result'] | null>(null)
|
const decision = ref<TahesabWalletSyncResult['result'] | null>(null)
|
||||||
const savedCustomerId = ref<number | null>(null)
|
const pendingPayload = ref<{ customerId: number; body: Record<string, string>; nextCode: string } | null>(null)
|
||||||
|
|
||||||
const visibleProxy = computed({
|
const visibleProxy = computed({
|
||||||
get: () => props.visible,
|
get: () => props.visible,
|
||||||
@@ -103,47 +113,44 @@ function close() {
|
|||||||
visibleProxy.value = false
|
visibleProxy.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function maybeSyncTahesabWallet(customerId: number, previousCode: string, nextCode: string) {
|
async function saveCode(customerId: number, body: Record<string, string>) {
|
||||||
if (props.provider !== 'tahesab') return
|
await updateCustomer(customerId, body as Customer)
|
||||||
if (!nextCode || nextCode === previousCode) return
|
message.success('کد حسابداری ذخیره شد.')
|
||||||
|
emit('success')
|
||||||
try {
|
close()
|
||||||
const res = await syncCustomerWalletFromTahesab(customerId, { ask_if_conflict: true })
|
|
||||||
if (res.needs_decision || res.result?.needs_decision) {
|
|
||||||
decision.value = res.result ?? null
|
|
||||||
savedCustomerId.value = customerId
|
|
||||||
decisionVisible.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (res.result?.applied) {
|
|
||||||
message.success(res.message || 'مانده تهحساب روی کیف اعمال شد')
|
|
||||||
} else if (res.message) {
|
|
||||||
message.info(res.message)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// خطای همگامسازی مانع ذخیره کد نیست
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDecide(mode: 'overwrite' | 'keep') {
|
async function applyWalletDecision(customerId: number, mode: 'overwrite' | 'keep') {
|
||||||
const customerId = savedCustomerId.value
|
if (props.provider === 'zarin') {
|
||||||
decisionVisible.value = false
|
return syncCustomerWalletFromZarin(customerId, {
|
||||||
if (!customerId) return
|
|
||||||
try {
|
|
||||||
const res = await syncCustomerWalletFromTahesab(customerId, {
|
|
||||||
overwrite: mode === 'overwrite',
|
overwrite: mode === 'overwrite',
|
||||||
keep: mode === 'keep',
|
keep: mode === 'keep',
|
||||||
ask_if_conflict: false,
|
ask_if_conflict: false,
|
||||||
})
|
})
|
||||||
if (mode === 'keep') {
|
|
||||||
message.info(res.message || 'موجودی اپ حفظ شد')
|
|
||||||
} else {
|
|
||||||
message.success(res.message || 'مانده تهحساب روی کیف اعمال شد')
|
|
||||||
}
|
}
|
||||||
|
return syncCustomerWalletFromTahesab(customerId, {
|
||||||
|
overwrite: mode === 'overwrite',
|
||||||
|
keep: mode === 'keep',
|
||||||
|
ask_if_conflict: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDecide(mode: 'overwrite' | 'keep') {
|
||||||
|
const pending = pendingPayload.value
|
||||||
|
decisionVisible.value = false
|
||||||
|
if (!pending) return
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await saveCode(pending.customerId, pending.body)
|
||||||
|
const res = await applyWalletDecision(pending.customerId, mode)
|
||||||
|
if (mode === 'keep') message.info(res.message || 'موجودی اپ حفظ شد')
|
||||||
|
else message.success(res.message || 'مانده حسابداری روی کیف اعمال شد')
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
pendingPayload.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +168,38 @@ async function submit() {
|
|||||||
? { pargar_account_id: normalized }
|
? { pargar_account_id: normalized }
|
||||||
: { moshtari_code: normalized }
|
: { moshtari_code: normalized }
|
||||||
|
|
||||||
|
const needsWalletAsk =
|
||||||
|
(props.provider === 'tahesab' || props.provider === 'zarin')
|
||||||
|
&& !!normalized
|
||||||
|
&& normalized !== previousCode
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await updateCustomer(props.customer.id, payload as Customer)
|
if (needsWalletAsk) {
|
||||||
message.success('کد حسابداری ذخیره شد.')
|
try {
|
||||||
emit('success')
|
const preview = props.provider === 'zarin'
|
||||||
close()
|
? await previewCustomerWalletFromZarin(props.customer.id, normalized)
|
||||||
await maybeSyncTahesabWallet(props.customer.id, previousCode, normalized)
|
: await previewCustomerWalletFromTahesab(props.customer.id, normalized)
|
||||||
|
|
||||||
|
if (preview.needs_decision || preview.result?.needs_decision) {
|
||||||
|
pendingPayload.value = {
|
||||||
|
customerId: props.customer.id,
|
||||||
|
body: payload as Record<string, string>,
|
||||||
|
nextCode: normalized,
|
||||||
|
}
|
||||||
|
decision.value = preview.result ?? null
|
||||||
|
decisionVisible.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (preview.ok === false && preview.message) {
|
||||||
|
message.warning(preview.message + ' — فقط کد ذخیره میشود')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.warning('مانده حسابداری خوانده نشد — فقط کد ذخیره میشود')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveCode(props.customer.id, payload as Record<string, string>)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="لاگها و اسناد پرگار">
|
<BasicShowContext title="لاگها و اسناد پرگار" :help="customerPargarLogsIntro">
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
@@ -110,6 +110,7 @@ import {
|
|||||||
pargarTypeLabel
|
pargarTypeLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/pargarDisplay'
|
} from '@/pages/menuPages/accounting/utils/pargarDisplay'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { customerPargarLogsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customerId?: number
|
customerId?: number
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal
|
<BasicModal
|
||||||
v-model:visible="visibleProxy"
|
v-model:visible="visibleProxy"
|
||||||
:width="isMobile ? '92vw' : '28rem'"
|
:width="isMobile ? '92vw' : '36rem'"
|
||||||
title="تصمیم برای مانده حساب"
|
title="تصمیم برای مانده حساب"
|
||||||
:custom-footer="true"
|
:custom-footer="true"
|
||||||
cancel-text="بعداً"
|
cancel-text="بعداً"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col gap-3 text-sm" style="color: var(--admin-text)">
|
<div class="flex flex-col gap-3 text-sm" style="color: var(--admin-text)">
|
||||||
<p class="m-0" style="color: var(--admin-text-secondary)">
|
<p class="m-0" style="color: var(--admin-text-secondary)">
|
||||||
موجودی اپ با مانده تهحساب فرق دارد. کدام را اعمال کنیم؟
|
موجودی اپ با مانده {{ sourceLabel }} فرق دارد. دو مانده جمع نمیشوند؛ یکی را پس از بررسی سندها تأیید کنید.
|
||||||
</p>
|
</p>
|
||||||
<div class="rounded-lg border border-base p-3 space-y-2" style="background: var(--admin-bg-muted)">
|
<div class="rounded-lg border border-base p-3 space-y-2" style="background: var(--admin-bg-muted)">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
@@ -17,8 +17,12 @@
|
|||||||
<strong dir="ltr">{{ formatGold(appGold) }}</strong>
|
<strong dir="ltr">{{ formatGold(appGold) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<span style="color: var(--admin-text-secondary)">مانده تهحساب (طلا)</span>
|
<span style="color: var(--admin-text-secondary)">مانده {{ sourceLabel }} (طلا)</span>
|
||||||
<strong dir="ltr">{{ formatGold(tahesabGold) }}</strong>
|
<strong dir="ltr">{{ formatGold(sourceGoldValue) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<span style="color: var(--admin-text-secondary)">اختلاف طلا</span>
|
||||||
|
<strong dir="ltr">{{ formatDeltaGold(goldDeltaValue) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px" style="background: var(--admin-border)" />
|
<div class="h-px" style="background: var(--admin-border)" />
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
@@ -26,13 +30,62 @@
|
|||||||
<strong dir="ltr">{{ formatCash(appCash) }}</strong>
|
<strong dir="ltr">{{ formatCash(appCash) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<span style="color: var(--admin-text-secondary)">مانده تهحساب (ریال)</span>
|
<span style="color: var(--admin-text-secondary)">مانده {{ sourceLabel }} (ریال)</span>
|
||||||
<strong dir="ltr">{{ formatCash(tahesabCash) }}</strong>
|
<strong dir="ltr">{{ formatCash(sourceCashValue) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<span style="color: var(--admin-text-secondary)">اختلاف ریال</span>
|
||||||
|
<strong dir="ltr">{{ formatDeltaCash(rialDeltaValue) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showDocs" class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
|
<div class="rounded-lg border border-base p-2.5 min-w-0">
|
||||||
|
<p class="m-0 mb-1.5 text-xs font-600">سندهای کیف اپ</p>
|
||||||
|
<p class="m-0 mb-1.5 text-xs" style="color: var(--admin-text-secondary)">آخرین تراکنشهای کیف اصلی</p>
|
||||||
|
<div v-if="walletDocs.length" class="max-h-32 overflow-y-auto flex flex-col gap-1.5">
|
||||||
|
<div
|
||||||
|
v-for="doc in walletDocs"
|
||||||
|
:key="'w-' + doc.id"
|
||||||
|
class="rounded-md border border-base px-2 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<div class="font-600 truncate">{{ doc.title || 'تراکنش' }}</div>
|
||||||
|
<div class="flex justify-between gap-2 mt-0.5" style="color: var(--admin-text-secondary)">
|
||||||
|
<span>{{ formatDocDate(doc.created_at) }}</span>
|
||||||
|
<span dir="ltr">{{ formatGold(doc.gold) }} طلا · {{ formatCash(doc.cash) }} ریال</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="m-0 text-xs" style="color: var(--admin-text-secondary)">سندی در کیف ثبت نشده</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border border-base p-2.5 min-w-0">
|
||||||
|
<p class="m-0 mb-1.5 text-xs font-600">سندهای {{ sourceLabel }}</p>
|
||||||
|
<p class="m-0 mb-1.5 text-xs" style="color: var(--admin-text-secondary)">آخرین اسناد حسابداری این مشتری</p>
|
||||||
|
<div v-if="sourceDocs.length" class="max-h-32 overflow-y-auto flex flex-col gap-1.5">
|
||||||
|
<div
|
||||||
|
v-for="doc in sourceDocs"
|
||||||
|
:key="'s-' + doc.id"
|
||||||
|
class="rounded-md border border-base px-2 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<div class="font-600 truncate">{{ doc.title || 'سند' }}</div>
|
||||||
|
<div class="flex justify-between gap-2 mt-0.5" style="color: var(--admin-text-secondary)">
|
||||||
|
<span>{{ formatDocDate(doc.date || doc.created_at) }}</span>
|
||||||
|
<span dir="ltr">
|
||||||
|
<template v-if="doc.gold != null || doc.cash != null">
|
||||||
|
{{ formatGold(doc.gold) }} طلا · {{ formatCash(doc.cash) }} ریال
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ formatGold(doc.amount) }} · {{ doc.type || '—' }}</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="m-0 text-xs" style="color: var(--admin-text-secondary)">سندی از حسابداری نیست</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<a-button type="primary" block :loading="loading === 'overwrite'" :disabled="!!loading" @click="choose('overwrite')">
|
<a-button type="primary" block :loading="loading === 'overwrite'" :disabled="!!loading" @click="choose('overwrite')">
|
||||||
اعمال مانده تهحساب روی کیف
|
اعمال مانده {{ sourceLabel }} روی کیف
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button block :loading="loading === 'keep'" :disabled="!!loading" @click="choose('keep')">
|
<a-button block :loading="loading === 'keep'" :disabled="!!loading" @click="choose('keep')">
|
||||||
نگه داشتن موجودی فعلی اپ
|
نگه داشتن موجودی فعلی اپ
|
||||||
@@ -48,6 +101,7 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import type { TahesabPreviewDoc } from '@/api/tahesab'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -55,6 +109,13 @@ const props = defineProps<{
|
|||||||
appCash?: number | null
|
appCash?: number | null
|
||||||
tahesabGold?: number | null
|
tahesabGold?: number | null
|
||||||
tahesabCash?: number | null
|
tahesabCash?: number | null
|
||||||
|
sourceGold?: number | null
|
||||||
|
sourceCash?: number | null
|
||||||
|
goldDelta?: number | null
|
||||||
|
rialDelta?: number | null
|
||||||
|
sourceLabel?: string
|
||||||
|
walletDocs?: TahesabPreviewDoc[]
|
||||||
|
tahesabDocs?: TahesabPreviewDoc[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -63,6 +124,22 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const loading = ref<'overwrite' | 'keep' | null>(null)
|
const loading = ref<'overwrite' | 'keep' | null>(null)
|
||||||
|
const sourceLabel = computed(() => props.sourceLabel || 'تهحساب')
|
||||||
|
const sourceGoldValue = computed(() => props.sourceGold ?? props.tahesabGold)
|
||||||
|
const sourceCashValue = computed(() => props.sourceCash ?? props.tahesabCash)
|
||||||
|
const goldDeltaValue = computed(() => {
|
||||||
|
if (props.goldDelta != null && !Number.isNaN(Number(props.goldDelta))) return Number(props.goldDelta)
|
||||||
|
return Number(sourceGoldValue.value ?? 0) - Number(props.appGold ?? 0)
|
||||||
|
})
|
||||||
|
const rialDeltaValue = computed(() => {
|
||||||
|
if (props.rialDelta != null && !Number.isNaN(Number(props.rialDelta))) return Number(props.rialDelta)
|
||||||
|
return Number(sourceCashValue.value ?? 0) - Number(props.appCash ?? 0)
|
||||||
|
})
|
||||||
|
const walletDocs = computed(() => props.walletDocs ?? [])
|
||||||
|
const sourceDocs = computed(() => props.tahesabDocs ?? [])
|
||||||
|
const showDocs = computed(
|
||||||
|
() => sourceLabel.value === 'تهحساب' || walletDocs.value.length > 0 || sourceDocs.value.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
const visibleProxy = computed({
|
const visibleProxy = computed({
|
||||||
get: () => props.visible,
|
get: () => props.visible,
|
||||||
@@ -86,6 +163,25 @@ function formatCash(v?: number | null) {
|
|||||||
return Math.round(Number(v)).toLocaleString('fa-IR')
|
return Math.round(Number(v)).toLocaleString('fa-IR')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDeltaGold(v: number) {
|
||||||
|
if (Number.isNaN(v)) return '—'
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return sign + formatGold(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDeltaCash(v: number) {
|
||||||
|
if (Number.isNaN(v)) return '—'
|
||||||
|
const sign = v > 0 ? '+' : ''
|
||||||
|
return sign + formatCash(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDocDate(value?: string | null) {
|
||||||
|
if (!value) return '—'
|
||||||
|
const d = new Date(value)
|
||||||
|
if (Number.isNaN(d.getTime())) return value
|
||||||
|
return d.toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
visibleProxy.value = false
|
visibleProxy.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tahesab-wallet-sync">
|
<div class="tahesab-wallet-sync" :class="{ 'tahesab-wallet-sync--row': layout === 'row' }">
|
||||||
<div class="tahesab-wallet-sync__head">
|
<div class="tahesab-wallet-sync__head">
|
||||||
<div class="tahesab-wallet-sync__icon" aria-hidden="true">
|
<div class="tahesab-wallet-sync__icon" aria-hidden="true">
|
||||||
<Icon icon="vuesax-linear:refresh-circle" :size="18" color="var(--admin-primary, #10913A)" />
|
<Icon icon="vuesax-linear:refresh-circle" :size="18" color="var(--admin-primary, #10913A)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tahesab-wallet-sync__copy min-w-0 flex-1">
|
<div class="tahesab-wallet-sync__copy min-w-0 flex-1">
|
||||||
<p class="tahesab-wallet-sync__title">همگامسازی با تهحساب</p>
|
<p class="tahesab-wallet-sync__title">همگامسازی مانده</p>
|
||||||
<p class="tahesab-wallet-sync__hint">
|
<p class="tahesab-wallet-sync__hint">
|
||||||
{{
|
{{
|
||||||
canSync
|
canSync
|
||||||
? 'مانده طلا و ریال تهحساب را با کیف اپ مقایسه میکند؛ اگر تداخل باشد شما تصمیم میگیرید'
|
? 'اول مانده و سندها را میبینید؛ بعد تأیید میکنید. دو مانده جمع نمیشوند'
|
||||||
: 'ابتدا کد حسابداری تهحساب را برای این مشتری ثبت کنید'
|
: 'ابتدا کد حسابداری تهحساب را برای این مشتری ثبت کنید'
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
block
|
|
||||||
size="small"
|
size="small"
|
||||||
class="tahesab-wallet-sync__btn"
|
class="tahesab-wallet-sync__btn"
|
||||||
|
:block="layout !== 'row'"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="!canSync || loading"
|
:disabled="!canSync || loading"
|
||||||
@click="$emit('sync', 'ask')"
|
@click="$emit('sync', 'ask')"
|
||||||
@@ -32,10 +32,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
canSync?: boolean
|
canSync?: boolean
|
||||||
}>()
|
layout?: 'stack' | 'row'
|
||||||
|
}>(),
|
||||||
|
{ layout: 'stack' },
|
||||||
|
)
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'sync', mode: 'ask' | 'overwrite' | 'keep'): void
|
(e: 'sync', mode: 'ask' | 'overwrite' | 'keep'): void
|
||||||
@@ -48,6 +52,24 @@ defineEmits<{
|
|||||||
background: var(--admin-surface-muted, var(--admin-bg-muted, #f8fafc));
|
background: var(--admin-surface-muted, var(--admin-bg-muted, #f8fafc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tahesab-wallet-sync--row {
|
||||||
|
@apply flex flex-col gap-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tahesab-wallet-sync--row .tahesab-wallet-sync__head {
|
||||||
|
@apply mb-0 min-w-0 flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.tahesab-wallet-sync--row {
|
||||||
|
@apply flex-row items-center gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tahesab-wallet-sync--row .tahesab-wallet-sync__btn {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tahesab-wallet-sync__head {
|
.tahesab-wallet-sync__head {
|
||||||
@apply mb-2.5 flex items-start gap-2;
|
@apply mb-2.5 flex items-start gap-2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="zarin-wallet-sync">
|
||||||
|
<div class="zarin-wallet-sync__head">
|
||||||
|
<div class="zarin-wallet-sync__icon" aria-hidden="true">
|
||||||
|
<Icon icon="vuesax-linear:refresh-circle" :size="18" color="var(--admin-primary, #10913A)" />
|
||||||
|
</div>
|
||||||
|
<div class="zarin-wallet-sync__copy min-w-0 flex-1">
|
||||||
|
<p class="zarin-wallet-sync__title">همگامسازی با زرین</p>
|
||||||
|
<p class="zarin-wallet-sync__hint">
|
||||||
|
{{
|
||||||
|
canSync
|
||||||
|
? 'مانده طلا و ریال زرین را با کیف اپ مقایسه میکند؛ اگر تداخل باشد شما تصمیم میگیرید'
|
||||||
|
: 'ابتدا کد حسابداری زرین را برای این مشتری ثبت کنید'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
size="small"
|
||||||
|
class="zarin-wallet-sync__btn"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!canSync || loading"
|
||||||
|
@click="$emit('sync', 'ask')"
|
||||||
|
>
|
||||||
|
بررسی و همگامسازی مانده
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
loading?: boolean
|
||||||
|
canSync?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'sync', mode: 'ask' | 'overwrite' | 'keep'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.zarin-wallet-sync {
|
||||||
|
@apply mb-2.5 rounded-xl border border-base p-2.5;
|
||||||
|
background: var(--admin-surface-muted, var(--admin-bg-muted, #f8fafc));
|
||||||
|
}
|
||||||
|
|
||||||
|
.zarin-wallet-sync__head {
|
||||||
|
@apply mb-2.5 flex items-start gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zarin-wallet-sync__icon {
|
||||||
|
@apply flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zarin-wallet-sync__title {
|
||||||
|
@apply m-0 text-sm font-700;
|
||||||
|
color: var(--admin-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zarin-wallet-sync__hint {
|
||||||
|
@apply m-0 mt-0.5 text-xs leading-5;
|
||||||
|
color: var(--admin-text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zarin-wallet-sync__btn {
|
||||||
|
@apply font-600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="لیست دارایی ها">
|
<BasicShowContext title="لیست دارایی ها" :help="customerFundsIntro">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||||
import BasicShowContext from '@/components/BasicShowContext.vue'
|
import BasicShowContext from '@/components/BasicShowContext.vue'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { customerFundsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const [assetFundsSubmit] = useBaseTable()
|
const [assetFundsSubmit] = useBaseTable()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<BaseIcon icon="vuesax-linear:medal-star" Size="16px" />
|
<BaseIcon icon="vuesax-linear:medal-star" Size="16px" />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
<BasicShowContext title="لیست اقساط">
|
<BasicShowContext title="لیست اقساط" :help="goldCreditInstallmentsIntro">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl" />
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl" />
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,6 +59,7 @@ import type { TableInput } from '@core/type/baseTable'
|
|||||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import { cols } from '@/pages/menuPages/customers/base/hooks/goldCreditColumns'
|
import { cols } from '@/pages/menuPages/customers/base/hooks/goldCreditColumns'
|
||||||
import { getCustomerGoldCreditInstallments, type GoldCredit } from '@/api/goldCredit'
|
import { getCustomerGoldCreditInstallments, type GoldCredit } from '@/api/goldCredit'
|
||||||
|
import { goldCreditInstallmentsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="لیست قلک ها">
|
<BasicShowContext title="لیست قلک ها" :help="customerPiggyFundsIntro">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
@@ -45,6 +45,7 @@ import { computed, ref } from 'vue'
|
|||||||
import { getPiggyFundsTable, getPiggyFundTransactionsTable } from '@/api/user'
|
import { getPiggyFundsTable, getPiggyFundTransactionsTable } from '@/api/user'
|
||||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||||
import BasicShowContext from '@/components/BasicShowContext.vue'
|
import BasicShowContext from '@/components/BasicShowContext.vue'
|
||||||
|
import { customerPiggyFundsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const [txSubmit] = useBaseTable()
|
const [txSubmit] = useBaseTable()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="لیست معرفی کرده ها">
|
<BasicShowContext title="لیست معرفی کرده ها" :help="customerReferralsIntro">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
</BaseTable>
|
</BaseTable>
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
@@ -28,6 +28,7 @@ import {toJalaliNoTime} from '@core/utils/useDataTime'
|
|||||||
import type {Customer} from '@/model/customer'
|
import type {Customer} from '@/model/customer'
|
||||||
import {onMounted} from 'vue'
|
import {onMounted} from 'vue'
|
||||||
import {getReferralsTable} from "@/api/user";
|
import {getReferralsTable} from "@/api/user";
|
||||||
|
import { customerReferralsIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست مشتریان">
|
<BasicShowContext title="لیست مشتریان" :help="customersPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
@@ -273,6 +273,7 @@ import {
|
|||||||
initWalletSettingsFromApi
|
initWalletSettingsFromApi
|
||||||
} from '@/utils/config'
|
} from '@/utils/config'
|
||||||
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
import { displayAccountingCode, isValidAccountingCode } from '@/utils/accountingCode'
|
||||||
|
import { customersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet'])
|
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'chargeWallet'])
|
||||||
|
|||||||
@@ -203,6 +203,12 @@
|
|||||||
:can-sync="tahesabWalletCanSync"
|
:can-sync="tahesabWalletCanSync"
|
||||||
@sync="syncTahesabWallet"
|
@sync="syncTahesabWallet"
|
||||||
/>
|
/>
|
||||||
|
<ZarinWalletSyncAction
|
||||||
|
v-if="zarinWalletSyncVisible"
|
||||||
|
:loading="zarinSyncLoading"
|
||||||
|
:can-sync="zarinWalletCanSync"
|
||||||
|
@sync="syncZarinWallet"
|
||||||
|
/>
|
||||||
<CustomerWalletBalancePanel
|
<CustomerWalletBalancePanel
|
||||||
:key="`wallet-mobile-${walletPanelKey}`"
|
:key="`wallet-mobile-${walletPanelKey}`"
|
||||||
ref="walletPanelMobile"
|
ref="walletPanelMobile"
|
||||||
@@ -230,6 +236,12 @@
|
|||||||
:can-sync="tahesabWalletCanSync"
|
:can-sync="tahesabWalletCanSync"
|
||||||
@sync="syncTahesabWallet"
|
@sync="syncTahesabWallet"
|
||||||
/>
|
/>
|
||||||
|
<ZarinWalletSyncAction
|
||||||
|
v-if="zarinWalletSyncVisible"
|
||||||
|
:loading="zarinSyncLoading"
|
||||||
|
:can-sync="zarinWalletCanSync"
|
||||||
|
@sync="syncZarinWallet"
|
||||||
|
/>
|
||||||
<CustomerWalletBalancePanel
|
<CustomerWalletBalancePanel
|
||||||
:key="`wallet-desktop-${walletPanelKey}`"
|
:key="`wallet-desktop-${walletPanelKey}`"
|
||||||
ref="walletPanelDesktop"
|
ref="walletPanelDesktop"
|
||||||
@@ -262,6 +274,12 @@
|
|||||||
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
|
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane v-if="tahesabDocsVisible" key="اسناد تهحساب" tab="اسناد تهحساب">
|
<a-tab-pane v-if="tahesabDocsVisible" key="اسناد تهحساب" tab="اسناد تهحساب">
|
||||||
|
<TahesabWalletSyncAction
|
||||||
|
layout="row"
|
||||||
|
:loading="tahesabSyncLoading"
|
||||||
|
:can-sync="tahesabWalletCanSync"
|
||||||
|
@sync="syncTahesabWallet"
|
||||||
|
/>
|
||||||
<CustomerTahesabDocsTable ref="tahesabDocsTable" :customer-id="userInfo?.id" />
|
<CustomerTahesabDocsTable ref="tahesabDocsTable" :customer-id="userInfo?.id" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane v-if="zarinDocsVisible" key="اسناد زرین" tab="اسناد زرین">
|
<a-tab-pane v-if="zarinDocsVisible" key="اسناد زرین" tab="اسناد زرین">
|
||||||
@@ -289,10 +307,15 @@
|
|||||||
/>
|
/>
|
||||||
<TahesabMandeDecisionModal
|
<TahesabMandeDecisionModal
|
||||||
v-model:visible="tahesabDecisionVisible"
|
v-model:visible="tahesabDecisionVisible"
|
||||||
|
:source-label="walletDecisionLabel"
|
||||||
:app-gold="tahesabDecision?.app_gold"
|
:app-gold="tahesabDecision?.app_gold"
|
||||||
:app-cash="tahesabDecision?.app_cash"
|
:app-cash="tahesabDecision?.app_cash"
|
||||||
:tahesab-gold="tahesabDecision?.tahesab_gold"
|
:tahesab-gold="tahesabDecision?.tahesab_gold ?? tahesabDecision?.zarin_gold"
|
||||||
:tahesab-cash="tahesabDecision?.tahesab_cash"
|
:tahesab-cash="tahesabDecision?.tahesab_cash ?? tahesabDecision?.zarin_cash"
|
||||||
|
:gold-delta="tahesabDecision?.gold_delta"
|
||||||
|
:rial-delta="tahesabDecision?.rial_delta"
|
||||||
|
:wallet-docs="tahesabDecision?.wallet_docs"
|
||||||
|
:tahesab-docs="tahesabDecision?.tahesab_docs"
|
||||||
@decide="onTahesabMandeDecide"
|
@decide="onTahesabMandeDecide"
|
||||||
/>
|
/>
|
||||||
<BasicInfoModal
|
<BasicInfoModal
|
||||||
@@ -359,11 +382,17 @@ import { displayAccountingCode, isValidAccountingCode } from '@/utils/accounting
|
|||||||
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
import CustomerPresenceBadge from '@/pages/menuPages/customers/base/components/CustomerPresenceBadge.vue'
|
||||||
import { syncCustomerWalletFromPargar, type PargarSyncWalletResult } from '@/api/pargar'
|
import { syncCustomerWalletFromPargar, type PargarSyncWalletResult } from '@/api/pargar'
|
||||||
import {
|
import {
|
||||||
|
previewCustomerWalletFromTahesab,
|
||||||
syncCustomerWalletFromTahesab,
|
syncCustomerWalletFromTahesab,
|
||||||
type TahesabWalletSyncResult,
|
type TahesabWalletSyncResult,
|
||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
|
import {
|
||||||
|
syncCustomerWalletFromZarin,
|
||||||
|
type ZarinWalletSyncResult,
|
||||||
|
} from '@/api/zarin'
|
||||||
import PargarWalletSyncAction from './components/PargarWalletSyncAction.vue'
|
import PargarWalletSyncAction from './components/PargarWalletSyncAction.vue'
|
||||||
import TahesabWalletSyncAction from './components/TahesabWalletSyncAction.vue'
|
import TahesabWalletSyncAction from './components/TahesabWalletSyncAction.vue'
|
||||||
|
import ZarinWalletSyncAction from './components/ZarinWalletSyncAction.vue'
|
||||||
import TahesabMandeDecisionModal from './components/TahesabMandeDecisionModal.vue'
|
import TahesabMandeDecisionModal from './components/TahesabMandeDecisionModal.vue'
|
||||||
|
|
||||||
const fnameHelp: DashboardHelp = {
|
const fnameHelp: DashboardHelp = {
|
||||||
@@ -761,8 +790,11 @@ const accountingCodeValue = computed(() => {
|
|||||||
|
|
||||||
const pargarSyncLoading = ref(false)
|
const pargarSyncLoading = ref(false)
|
||||||
const tahesabSyncLoading = ref(false)
|
const tahesabSyncLoading = ref(false)
|
||||||
|
const zarinSyncLoading = ref(false)
|
||||||
const tahesabDecisionVisible = ref(false)
|
const tahesabDecisionVisible = ref(false)
|
||||||
const tahesabDecision = ref<TahesabWalletSyncResult['result'] | null>(null)
|
const tahesabDecision = ref<(TahesabWalletSyncResult['result'] & { zarin_gold?: number; zarin_cash?: number }) | null>(null)
|
||||||
|
const walletDecisionProvider = ref<'tahesab' | 'zarin'>('tahesab')
|
||||||
|
const walletDecisionLabel = computed(() => walletDecisionProvider.value === 'zarin' ? 'زرین' : 'تهحساب')
|
||||||
const walletPanelDesktop = ref<{ reload?: () => void | Promise<void> } | null>(null)
|
const walletPanelDesktop = ref<{ reload?: () => void | Promise<void> } | null>(null)
|
||||||
const walletPanelMobile = ref<{ reload?: () => void | Promise<void> } | null>(null)
|
const walletPanelMobile = ref<{ reload?: () => void | Promise<void> } | null>(null)
|
||||||
const walletPanelKey = ref(0)
|
const walletPanelKey = ref(0)
|
||||||
@@ -776,6 +808,10 @@ const tahesabWalletSyncVisible = computed(
|
|||||||
() => accountingProviderSetting.value === 'tahesab'
|
() => accountingProviderSetting.value === 'tahesab'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const zarinWalletSyncVisible = computed(
|
||||||
|
() => accountingProviderSetting.value === 'zarin'
|
||||||
|
)
|
||||||
|
|
||||||
const tahesabDocsVisible = computed(() => accountingProviderSetting.value === 'tahesab')
|
const tahesabDocsVisible = computed(() => accountingProviderSetting.value === 'tahesab')
|
||||||
const zarinDocsVisible = computed(() => accountingProviderSetting.value === 'zarin')
|
const zarinDocsVisible = computed(() => accountingProviderSetting.value === 'zarin')
|
||||||
|
|
||||||
@@ -787,6 +823,10 @@ const tahesabWalletCanSync = computed(() =>
|
|||||||
isValidAccountingCode(userInfo.value?.moshtari_code)
|
isValidAccountingCode(userInfo.value?.moshtari_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const zarinWalletCanSync = computed(() =>
|
||||||
|
isValidAccountingCode(userInfo.value?.moshtari_code)
|
||||||
|
)
|
||||||
|
|
||||||
async function refreshWalletPanels() {
|
async function refreshWalletPanels() {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
walletPanelKey.value += 1
|
walletPanelKey.value += 1
|
||||||
@@ -797,7 +837,8 @@ async function refreshWalletPanels() {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTahesabDecision(result?: TahesabWalletSyncResult['result']) {
|
function openTahesabDecision(result?: TahesabWalletSyncResult['result'], provider: 'tahesab' | 'zarin' = 'tahesab') {
|
||||||
|
walletDecisionProvider.value = provider
|
||||||
tahesabDecision.value = result ?? null
|
tahesabDecision.value = result ?? null
|
||||||
tahesabDecisionVisible.value = true
|
tahesabDecisionVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -806,14 +847,28 @@ async function syncTahesabWallet(mode: 'ask' | 'overwrite' | 'keep' = 'ask') {
|
|||||||
if (!userInfo.value?.id || !tahesabWalletCanSync.value || tahesabSyncLoading.value) return
|
if (!userInfo.value?.id || !tahesabWalletCanSync.value || tahesabSyncLoading.value) return
|
||||||
tahesabSyncLoading.value = true
|
tahesabSyncLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
if (mode === 'ask') {
|
||||||
|
const preview = await previewCustomerWalletFromTahesab(userInfo.value.id)
|
||||||
|
if (!preview.ok && !preview.result?.ok) {
|
||||||
|
message.warning(preview.message || 'مانده از تهحساب خوانده نشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!preview.needs_decision && !preview.result?.needs_decision) {
|
||||||
|
message.info(preview.message || 'مانده با تهحساب یکسان است')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openTahesabDecision(preview.result, 'tahesab')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const res = await syncCustomerWalletFromTahesab(userInfo.value.id, {
|
const res = await syncCustomerWalletFromTahesab(userInfo.value.id, {
|
||||||
ask_if_conflict: mode === 'ask',
|
ask_if_conflict: false,
|
||||||
overwrite: mode === 'overwrite',
|
overwrite: mode === 'overwrite',
|
||||||
keep: mode === 'keep',
|
keep: mode === 'keep',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.needs_decision || res.result?.needs_decision) {
|
if (res.needs_decision || res.result?.needs_decision) {
|
||||||
openTahesabDecision(res.result)
|
openTahesabDecision(res.result, 'tahesab')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,9 +901,55 @@ async function syncTahesabWallet(mode: 'ask' | 'overwrite' | 'keep' = 'ask') {
|
|||||||
|
|
||||||
async function onTahesabMandeDecide(mode: 'overwrite' | 'keep') {
|
async function onTahesabMandeDecide(mode: 'overwrite' | 'keep') {
|
||||||
tahesabDecisionVisible.value = false
|
tahesabDecisionVisible.value = false
|
||||||
|
if (walletDecisionProvider.value === 'zarin') {
|
||||||
|
await syncZarinWallet(mode)
|
||||||
|
return
|
||||||
|
}
|
||||||
await syncTahesabWallet(mode)
|
await syncTahesabWallet(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncZarinWallet(mode: 'ask' | 'overwrite' | 'keep' = 'ask') {
|
||||||
|
if (!userInfo.value?.id || !zarinWalletCanSync.value || zarinSyncLoading.value) return
|
||||||
|
zarinSyncLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await syncCustomerWalletFromZarin(userInfo.value.id, {
|
||||||
|
ask_if_conflict: mode === 'ask',
|
||||||
|
overwrite: mode === 'overwrite',
|
||||||
|
keep: mode === 'keep',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.needs_decision || res.result?.needs_decision) {
|
||||||
|
openTahesabDecision(res.result, 'zarin')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.result?.applied || res.ok) {
|
||||||
|
message.success(res.message || 'مانده از زرین روی کیف اعمال شد')
|
||||||
|
} else {
|
||||||
|
message.info(res.message || 'تغییری برای اعمال نبود')
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshWalletPanels()
|
||||||
|
refreshTransactionTable()
|
||||||
|
refreshTable()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const data = (e as { response?: { data?: ZarinWalletSyncResult } })?.response?.data
|
||||||
|
const status = (e as { response?: { status?: number } })?.response?.status
|
||||||
|
|
||||||
|
await refreshWalletPanels()
|
||||||
|
refreshTransactionTable()
|
||||||
|
refreshTable()
|
||||||
|
|
||||||
|
if (data?.result?.applied || data?.ok) {
|
||||||
|
message.success(data?.message || 'مانده از زرین روی کیف اعمال شد')
|
||||||
|
} else if (status !== 422) {
|
||||||
|
message.warning(data?.message || 'اعمال مانده از زرین ناموفق بود')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
zarinSyncLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function syncPargarWallet(force = false) {
|
async function syncPargarWallet(force = false) {
|
||||||
if (!userInfo.value?.id || !pargarWalletCanSync.value || pargarSyncLoading.value) return
|
if (!userInfo.value?.id || !pargarWalletCanSync.value || pargarSyncLoading.value) return
|
||||||
pargarSyncLoading.value = true
|
pargarSyncLoading.value = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 w-full">
|
<div class="h-full min-h-0 w-full">
|
||||||
<BasicShowContext title="اعتبار">
|
<BasicShowContext title="اعتبار" :help="goldCreditPageIntro">
|
||||||
<a-tabs v-model:activeKey="activeTab" class="gold-credit-tabs h-full" destroy-inactive-tab-pane>
|
<a-tabs v-model:activeKey="activeTab" class="gold-credit-tabs h-full" destroy-inactive-tab-pane>
|
||||||
<a-tab-pane key="requests" tab="درخواستها">
|
<a-tab-pane key="requests" tab="درخواستها">
|
||||||
<GoldCreditRequestsTab />
|
<GoldCreditRequestsTab />
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import GoldCreditRequestsTab from './requests/index.vue'
|
import GoldCreditRequestsTab from './requests/index.vue'
|
||||||
import GoldCreditActiveTab from './active/index.vue'
|
import GoldCreditActiveTab from './active/index.vue'
|
||||||
|
import { goldCreditPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const activeTab = ref('requests')
|
const activeTab = ref('requests')
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<div class="online-page__hero shrink-0 mb-4">
|
<div class="online-page__hero shrink-0 mb-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="online-page__title m-0">کاربران آنلاین</h2>
|
<h2 class="online-page__title m-0 inline-flex items-center gap-1.5">
|
||||||
|
کاربران آنلاین
|
||||||
|
<AdminFieldHelp :text="customersOnlinePageIntro" size="sm" />
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="online-page__count">
|
<div class="online-page__count">
|
||||||
@@ -297,6 +300,8 @@ import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
|
|||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import { customersOnlinePageIntro } from '@/utils/adminHelpCopy'
|
||||||
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
|
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
|
||||||
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
import { inputLenRules, inputRules } from '@/utils/inputRules'
|
||||||
import CustomerMobileDetailSheet from '@/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue'
|
import CustomerMobileDetailSheet from '@/pages/menuPages/customers/base/components/CustomerMobileDetailSheet.vue'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="پکیجهای قیمت ویژه">
|
<BasicShowContext title="پکیجهای قیمت ویژه" :help="specialPricePageIntro">
|
||||||
<template v-if="isMobile" #extra>
|
<template v-if="isMobile" #extra>
|
||||||
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
|
<BaseButton type="primary" size="small" icon="vuesax-linear:add" @click="emits('add')">
|
||||||
افزودن
|
افزودن
|
||||||
@@ -55,6 +55,7 @@ import { getCustomerSpecialPricePackageTable } from '@/api/customerSpecialPrice'
|
|||||||
import { getSpecialPricePackageColumns } from '../hooks/specialPriceColumns'
|
import { getSpecialPricePackageColumns } from '../hooks/specialPriceColumns'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { specialPricePageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: 'add'): void
|
(e: 'add'): void
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="لیست تخفیفات">
|
<BasicShowContext title="لیست تخفیفات" :help="discountPageIntro">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #extraHeader>
|
<template #extraHeader>
|
||||||
<BaseButton type="primary" size="small" class="flex" @click="emits('add')">
|
<BaseButton type="primary" size="small" class="flex" @click="emits('add')">
|
||||||
@@ -29,6 +29,7 @@ import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
|||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { cols } from '../hooks/discountColumns'
|
import { cols } from '../hooks/discountColumns'
|
||||||
import { getDiscountTable } from '@/api/discount'
|
import { getDiscountTable } from '@/api/discount'
|
||||||
|
import { discountPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست کارت های هدیه">
|
<BasicShowContext title="لیست کارت های هدیه" :help="giftCardsPageIntro">
|
||||||
<template v-if="!customerId && isMobile" #extra>
|
<template v-if="!customerId && isMobile" #extra>
|
||||||
<BaseButton type="primary" size="small" icon="vuesax-linear:receipt-add" @click="emits('add', 'gift')">
|
<BaseButton type="primary" size="small" icon="vuesax-linear:receipt-add" @click="emits('add', 'gift')">
|
||||||
افزودن
|
افزودن
|
||||||
@@ -65,6 +65,7 @@ import { getGiftCardTable } from '@/api/giftCard'
|
|||||||
import GiftCardMobileCardList from './GiftCardMobileCardList.vue'
|
import GiftCardMobileCardList from './GiftCardMobileCardList.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import { isStoreApp } from '@/utils/config'
|
import { isStoreApp } from '@/utils/config'
|
||||||
|
import { giftCardsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
customerId?: number
|
customerId?: number
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست سفارشات">
|
<BasicShowContext title="لیست سفارشات" :help="ordersPageIntro">
|
||||||
<template v-if="!customerId && isMobile" #extra>
|
<template v-if="!customerId && isMobile" #extra>
|
||||||
<BaseButton type="primary" size="small" icon="vuesax-linear:receipt-add" @click="emits('add')">
|
<BaseButton type="primary" size="small" icon="vuesax-linear:receipt-add" @click="emits('add')">
|
||||||
افزودن
|
افزودن
|
||||||
@@ -49,6 +49,7 @@ import { message } from 'ant-design-vue'
|
|||||||
import { getOrderTable } from '@/api/order'
|
import { getOrderTable } from '@/api/order'
|
||||||
import OrderMobileCardList from './OrderMobileCardList.vue'
|
import OrderMobileCardList from './OrderMobileCardList.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { ordersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست برداشت فیزیکی">
|
<BasicShowContext title="لیست برداشت فیزیکی" :help="physicalWithdrawPageIntro">
|
||||||
<template #extra>
|
|
||||||
<AdminFieldHelp :text="physicalWithdrawPageIntro" />
|
|
||||||
</template>
|
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<BaseTable class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #extraHeader>
|
<template #extraHeader>
|
||||||
@@ -89,7 +86,6 @@ import { updateSts } from '@/api/wallet'
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Withdraw } from '@/model/wallet'
|
import type { Withdraw } from '@/model/wallet'
|
||||||
import { status as physicalStatusMap } from '@/pages/menuPages/withdraw/information/hooks/status'
|
import { status as physicalStatusMap } from '@/pages/menuPages/withdraw/information/hooks/status'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
|
||||||
import PhysicalWithdrawAdjustModal from './PhysicalWithdrawAdjustModal.vue'
|
import PhysicalWithdrawAdjustModal from './PhysicalWithdrawAdjustModal.vue'
|
||||||
import {
|
import {
|
||||||
physicalWithdrawApproveHelp,
|
physicalWithdrawApproveHelp,
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicShowContext title="هشدار قیمت">
|
<BasicShowContext title="هشدار قیمت" :help="priceTriggersPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<AdminFieldHelp :text="priceTriggersPageIntro" />
|
|
||||||
<BaseButton type="primary" size="small" @click="emits('add')">
|
<BaseButton type="primary" size="small" @click="emits('add')">
|
||||||
<span>افزودن هشدار</span>
|
<span>افزودن هشدار</span>
|
||||||
<BaseIcon icon="vuesax-linear:add" size="16" />
|
<BaseIcon icon="vuesax-linear:add" size="16" />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
@@ -54,7 +51,6 @@ import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
|||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { cols } from '../hooks/priceTriggersColumns'
|
import { cols } from '../hooks/priceTriggersColumns'
|
||||||
import { getPriceTriggersTable } from '@/api/priceTriggers'
|
import { getPriceTriggersTable } from '@/api/priceTriggers'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
|
||||||
import { priceTriggersPageIntro } from '@/utils/adminHelpCopy'
|
import { priceTriggersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="تاریخچه قیمت">
|
<BasicShowContext title="تاریخچه قیمت" :help="pricingPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<AdminFieldHelp :text="pricingPageIntro" />
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -14,7 +12,6 @@
|
|||||||
>
|
>
|
||||||
بهروزرسانی قیمت
|
بهروزرسانی قیمت
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
@@ -63,7 +60,6 @@ import { storePriceColumns } from '../hooks/storePriceColumns'
|
|||||||
import { usePrefix } from '@core/composable/usePrefix'
|
import { usePrefix } from '@core/composable/usePrefix'
|
||||||
import { getPriceSyncLogTable } from '@/api/price'
|
import { getPriceSyncLogTable } from '@/api/price'
|
||||||
import PriceMobileCardList from './PriceMobileCardList.vue'
|
import PriceMobileCardList from './PriceMobileCardList.vue'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
|
||||||
import { pricingPageIntro } from '@/utils/adminHelpCopy'
|
import { pricingPageIntro } from '@/utils/adminHelpCopy'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BasicShowContext title="لیست محصولات" class="min-h-0 flex-1">
|
<BasicShowContext title="لیست محصولات" class="min-h-0 flex-1" :help="productsPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
@@ -148,6 +148,7 @@ import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
|||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
import type { Product } from '@/model/product'
|
import type { Product } from '@/model/product'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { productsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="category-page flex h-full min-h-0 flex-col gap-4 p-4">
|
<div class="category-page flex h-full min-h-0 flex-col gap-4 p-4">
|
||||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-sm font-600" style="color: var(--admin-text)">
|
||||||
|
دستهبندی محصولات
|
||||||
|
<AdminFieldHelp :text="productsCategoriesPageIntro" size="sm" />
|
||||||
|
</span>
|
||||||
<BaseButton :loading="syncing" @click="handleSync">
|
<BaseButton :loading="syncing" @click="handleSync">
|
||||||
همگامسازی از محصولات
|
همگامسازی از محصولات
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
@@ -65,6 +69,8 @@ import { debounce } from 'lodash'
|
|||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category'
|
import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category'
|
||||||
import type { Category } from '@/model/category'
|
import type { Category } from '@/model/category'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import { productsCategoriesPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const syncing = ref(false)
|
const syncing = ref(false)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست پیامک ها">
|
<BasicShowContext title="لیست پیامک ها" :help="smsPageIntro">
|
||||||
<template v-if="groupSmsEnabled && !mobile && isMobileScreen" #extra>
|
<template v-if="groupSmsEnabled && !mobile && isMobileScreen" #extra>
|
||||||
<BaseButton type="primary" size="small" icon="vuesax-linear:sms" @click="emits('add')">
|
<BaseButton type="primary" size="small" icon="vuesax-linear:sms" @click="emits('add')">
|
||||||
ارسال پیامک
|
ارسال پیامک
|
||||||
@@ -40,6 +40,7 @@ import { getSMSTable } from '@/api/sms'
|
|||||||
import SmsMobileCardList from './SmsMobileCardList.vue'
|
import SmsMobileCardList from './SmsMobileCardList.vue'
|
||||||
import { isMobile as isMobileScreen } from '@core/utils/is'
|
import { isMobile as isMobileScreen } from '@core/utils/is'
|
||||||
import { groupSmsEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
import { groupSmsEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||||
|
import { smsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
mobile?: number | string
|
mobile?: number | string
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<div class="sms-panel-page h-full min-h-0 w-full min-w-0">
|
<div class="sms-panel-page h-full min-h-0 w-full min-w-0">
|
||||||
<div class="sms-panel-page__scroll h-full min-h-0 overflow-y-auto overflow-x-hidden px-2 pb-8 md:px-4">
|
<div class="sms-panel-page__scroll h-full min-h-0 overflow-y-auto overflow-x-hidden px-2 pb-8 md:px-4">
|
||||||
<header class="mb-6 md:mb-8">
|
<header class="mb-6 md:mb-8">
|
||||||
<h1 class="sms-panel-heading text-xl font-bold md:text-2xl">پنل پیامک</h1>
|
<h1 class="sms-panel-heading inline-flex items-center gap-1.5 text-xl font-bold md:text-2xl">
|
||||||
|
پنل پیامک
|
||||||
|
<AdminFieldHelp :text="smsPanelPageIntro" size="sm" />
|
||||||
|
</h1>
|
||||||
<p class="sms-panel-subtitle mt-1 text-sm">مدیریت کیف پول، تعرفه و گزارش ارسال پیامک</p>
|
<p class="sms-panel-subtitle mt-1 text-sm">مدیریت کیف پول، تعرفه و گزارش ارسال پیامک</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -628,6 +631,8 @@ import BasicModal from '@/components/BasicModal.vue'
|
|||||||
import BaseDatePicker from '@core/components/BaseDatePicker'
|
import BaseDatePicker from '@core/components/BaseDatePicker'
|
||||||
import BaseButton from '@core/components/BaseButton'
|
import BaseButton from '@core/components/BaseButton'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import { smsPanelPageIntro } from '@/utils/adminHelpCopy'
|
||||||
import SmsPanelLogsTable from './components/SmsPanelLogsTable.vue'
|
import SmsPanelLogsTable from './components/SmsPanelLogsTable.vue'
|
||||||
import SmsPanelLedgerTable from './components/SmsPanelLedgerTable.vue'
|
import SmsPanelLedgerTable from './components/SmsPanelLedgerTable.vue'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست گزارشات">
|
<BasicShowContext title="لیست گزارشات" :help="transactionsPageIntro">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<!-- <template #extraHeader>
|
<!-- <template #extraHeader>
|
||||||
@@ -75,6 +75,7 @@ import {getWalletTable} from '@/api/wallet'
|
|||||||
import { getTransactionTable } from '@/api/transaction'
|
import { getTransactionTable } from '@/api/transaction'
|
||||||
import TransactionMobileCardList from './TransactionMobileCardList.vue'
|
import TransactionMobileCardList from './TransactionMobileCardList.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { transactionsPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
customerId?: number
|
customerId?: number
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست مدیران">
|
<BasicShowContext title="لیست مدیران" :help="usersPageIntro">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
@@ -65,6 +65,7 @@ import { getUserTable } from '@/api/user'
|
|||||||
import { cols } from '../hooks/usersColumns'
|
import { cols } from '../hooks/usersColumns'
|
||||||
import ManagerMobileCardList from './ManagerMobileCardList.vue'
|
import ManagerMobileCardList from './ManagerMobileCardList.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { usersPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0">
|
<div class="h-full min-h-0">
|
||||||
<BasicShowContext title="لیست کیف پول">
|
<BasicShowContext title="لیست کیف پول" :help="walletPageIntro">
|
||||||
<div class="flex h-full min-h-0 flex-col gap-3">
|
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||||
<div class="hidden shrink-0 md:grid md:grid-cols-1 md:gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
<div class="hidden shrink-0 md:grid md:grid-cols-1 md:gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
<div
|
<div
|
||||||
@@ -81,6 +81,7 @@ import { isMobile } from '@core/utils/is'
|
|||||||
import { totalMetalWalletTitle } from '@/utils/adminMetalLabels'
|
import { totalMetalWalletTitle } from '@/utils/adminMetalLabels'
|
||||||
import type { WalletTableSums } from '@/model/wallet'
|
import type { WalletTableSums } from '@/model/wallet'
|
||||||
import { formatMetalDisplayValue, metalDisplayUnit } from '@/utils/metalWeight'
|
import { formatMetalDisplayValue, metalDisplayUnit } from '@/utils/metalWeight'
|
||||||
|
import { walletPageIntro } from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||||
<BasicShowContext title="لیست برداشت و واریز">
|
<BasicShowContext title="لیست برداشت و واریز" :help="withdrawPageIntro">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<BaseTable v-if="!isMobile" :key="flowType" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-if="!isMobile" :key="flowType" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||||
<template #extraHeader>
|
<template #extraHeader>
|
||||||
@@ -241,6 +241,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
|
import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
|
||||||
import {toNumber} from "lodash";
|
import {toNumber} from "lodash";
|
||||||
|
import { withdrawPageIntro } from '@/utils/adminHelpCopy'
|
||||||
const withdrawId = ref(null)
|
const withdrawId = ref(null)
|
||||||
const sts = ref()
|
const sts = ref()
|
||||||
const statusList = computed(() => {
|
const statusList = computed(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { importFromTahesab, type TahesabOnlyCustomer } from '@/api/tahesab'
|
import { importFromTahesab, type TahesabOnlyCustomer } from '@/api/tahesab'
|
||||||
import { importFromPargar, type PargarOnlyCustomer } from '@/api/pargar'
|
import { importFromPargar, type PargarOnlyCustomer } from '@/api/pargar'
|
||||||
|
import { importFromZarin, type ZarinOnlyCustomer } from '@/api/zarin'
|
||||||
|
|
||||||
export interface ImportError {
|
export interface ImportError {
|
||||||
code: number
|
code: number
|
||||||
@@ -10,12 +11,12 @@ export interface ImportError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ImportStatus = 'idle' | 'running' | 'done' | 'canceled'
|
export type ImportStatus = 'idle' | 'running' | 'done' | 'canceled'
|
||||||
export type ImportProvider = 'tahesab' | 'pargar'
|
export type ImportProvider = 'tahesab' | 'pargar' | 'zarin'
|
||||||
|
|
||||||
/** نتیجهٔ نهایی برای نمایش آیکن/متن مناسب */
|
/** نتیجهٔ نهایی برای نمایش آیکن/متن مناسب */
|
||||||
export type ImportOutcome = 'running' | 'success' | 'error' | 'partial' | 'canceled'
|
export type ImportOutcome = 'running' | 'success' | 'error' | 'partial' | 'canceled'
|
||||||
|
|
||||||
type ImportCustomer = TahesabOnlyCustomer | PargarOnlyCustomer
|
type ImportCustomer = TahesabOnlyCustomer | PargarOnlyCustomer | ZarinOnlyCustomer
|
||||||
|
|
||||||
/** تعداد مشتری در هر درخواست (برای جلوگیری از محدودیت نرخ سرور) */
|
/** تعداد مشتری در هر درخواست (برای جلوگیری از محدودیت نرخ سرور) */
|
||||||
const CHUNK_SIZE = 50
|
const CHUNK_SIZE = 50
|
||||||
@@ -120,6 +121,9 @@ export const useCustomerImportStore = defineStore('customerImport', () => {
|
|||||||
if (provider.value === 'pargar') {
|
if (provider.value === 'pargar') {
|
||||||
return await importFromPargar(chunk as PargarOnlyCustomer[])
|
return await importFromPargar(chunk as PargarOnlyCustomer[])
|
||||||
}
|
}
|
||||||
|
if (provider.value === 'zarin') {
|
||||||
|
return await importFromZarin(chunk as ZarinOnlyCustomer[])
|
||||||
|
}
|
||||||
return await importFromTahesab(chunk as TahesabOnlyCustomer[])
|
return await importFromTahesab(chunk as TahesabOnlyCustomer[])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (attempt < 2 && !cancelRequested) {
|
if (attempt < 2 && !cancelRequested) {
|
||||||
|
|||||||
@@ -170,3 +170,120 @@ export const automationStatusHelp =
|
|||||||
|
|
||||||
export const tradingHoursPageIntro =
|
export const tradingHoursPageIntro =
|
||||||
'ساعت باز و بسته بودن فروشگاه و رفتار نمایش قیمت در اپ مشتری. میتوانید برای همه روزها یک ساعت بگذارید یا برای هر روز هفته جداگانه تنظیم کنید.'
|
'ساعت باز و بسته بودن فروشگاه و رفتار نمایش قیمت در اپ مشتری. میتوانید برای همه روزها یک ساعت بگذارید یا برای هر روز هفته جداگانه تنظیم کنید.'
|
||||||
|
|
||||||
|
export const customersPageIntro =
|
||||||
|
'لیست مشتریان ثبتشده در اپ. از اینجا میتوانید مشتری جدید بسازید، ویرایش کنید، موجودی و سفارشها را ببینید، یا مشتری را بایگانی کنید.'
|
||||||
|
|
||||||
|
export const customersArchivePageIntro =
|
||||||
|
'مشتریانی که بایگانی شدهاند اینجا هستند؛ در اپ فعال نیستند. میتوانید دوباره از بایگانی خارجشان کنید.'
|
||||||
|
|
||||||
|
export const customersOnlinePageIntro =
|
||||||
|
'مشتریانی که الان اپ را باز دارند. با بستن اپ از لیست حذف میشوند. برای جزئیات روی کارت بزنید.'
|
||||||
|
|
||||||
|
export const goldCreditPageIntro =
|
||||||
|
'اعتبار طلایی مشتریان: درخواستهای جدید و اعتبارهای فعال. از تبها بین درخواست و فعال جابهجا شوید.'
|
||||||
|
|
||||||
|
export const goldCreditInstallmentsIntro =
|
||||||
|
'اقساط مربوط به اعتبار طلایی همین مشتری. وضعیت پرداخت هر قسط را اینجا میبینید.'
|
||||||
|
|
||||||
|
export const specialPricePageIntro =
|
||||||
|
'پکیج قیمت ویژه برای گروهی از مشتریان. با پکیج میتوانید خرید/فروش را با قیمت یا کارمزد متفاوت از بقیه تنظیم کنید.'
|
||||||
|
|
||||||
|
export const productsPageIntro =
|
||||||
|
'محصولات قابل معامله در اپ. افزودن، ویرایش، قیمتگذاری، موجودی و بایگانی از همینجا انجام میشود.'
|
||||||
|
|
||||||
|
export const productsCategoriesPageIntro =
|
||||||
|
'دستهبندی محصولات برای فیلتر تبهای اپ. ترتیب را با کشیدن عوض کنید؛ «همگامسازی از محصولات» دستههای گمشده را از روی محصولات میسازد.'
|
||||||
|
|
||||||
|
export const ordersPageIntro =
|
||||||
|
'سفارشهای خرید و فروش مشتریان. میتوانید سفارش جدید ثبت کنید یا جزئیات و وضعیت هر سفارش را ببینید.'
|
||||||
|
|
||||||
|
export const conditionalOrdersPageIntro =
|
||||||
|
'سفارش شرطی: وقتی قیمت به هدف برسد سیستم خودکار سفارش میزند. فعال/غیرفعال و ویرایش از همین لیست.'
|
||||||
|
|
||||||
|
export const colleagueOrdersPageIntro =
|
||||||
|
'تراکنشهای پوشششده با اپ همکار. برای ثبت درست سند حسابداری، کد حساب همکار را هم در همین صفحه تنظیم کنید.'
|
||||||
|
|
||||||
|
export const colleagueAccountingCodeHelp =
|
||||||
|
'کدی که سند خرید/فروش پوشششدهٔ همکار روی آن در سیستم حسابداری فعال ثبت میشود. فقط فیلد مربوط به حسابداری فعال را پر کنید.'
|
||||||
|
|
||||||
|
export const transactionsPageIntro =
|
||||||
|
'گزارش تراکنشهای کیف پول و عملیات مالی مشتریان. برای بررسی جزئیات هر ردیف را باز کنید.'
|
||||||
|
|
||||||
|
export const walletPageIntro =
|
||||||
|
'موجودی کیفهای مشتریان (طلا، ریال، محصول، قلک). خلاصه و جزئیات هر کیف را اینجا میبینید.'
|
||||||
|
|
||||||
|
export const withdrawPageIntro =
|
||||||
|
'درخواستهای واریز و برداشت ریالی مشتریان. تایید، رد یا تعیین وزن (در صورت نیاز) از همین لیست.'
|
||||||
|
|
||||||
|
export const cardsPageIntro =
|
||||||
|
'حسابها و کارتهای بانکی ثبتشده برای واریز/برداشت. افزودن، ویرایش یا حذف از همینجا.'
|
||||||
|
|
||||||
|
export const giftCardsPageIntro =
|
||||||
|
'ساخت و مدیریت کارت هدیه (ریالی یا طلا). هر پویش چند کد یکتا میسازد؛ وضعیت مصرف و انقضا را اینجا ببینید.'
|
||||||
|
|
||||||
|
export const discountPageIntro =
|
||||||
|
'پویشهای کد تخفیف. درصد، تعداد کد و تاریخ انقضا را تنظیم کنید؛ هر کد معمولاً یکبار مصرف است.'
|
||||||
|
|
||||||
|
export const smsPageIntro =
|
||||||
|
'تاریخچه پیامکهای ارسالشده به مشتریان. در صورت فعال بودن پیامک گروهی، از همینجا هم میتوانید پیامک بفرستید.'
|
||||||
|
|
||||||
|
export const smsPanelPageIntro =
|
||||||
|
'پنل شارژ و وضعیت سرویس پیامک. موجودی، خرید بسته و تنظیمات ارسال پیامک در این صفحه است.'
|
||||||
|
|
||||||
|
export const appNotificationsPageIntro =
|
||||||
|
'اعلان ادمین را دستی میفرستید؛ اعلان سیستمی خودکار برای خرید/فروش/واریز/برداشت ساخته میشود. از تبها برای جدا کردن لیست استفاده کنید.'
|
||||||
|
|
||||||
|
export const automationsPageIntro =
|
||||||
|
'قوانین پیام خودکار (زماندار، رویداد یا قیمت). مخاطب، متن، کانال پیامک/اعلان و سقف ارسال را اینجا تنظیم کنید.'
|
||||||
|
|
||||||
|
export const usersPageIntro =
|
||||||
|
'مدیران پنل ادمین. افزودن مدیر، ویرایش دسترسی و غیرفعال کردن از همین لیست.'
|
||||||
|
|
||||||
|
export const captureSitesPageIntro =
|
||||||
|
'سایتهای منبع قیمت برای ریگیری. روشن/خاموش کردن ریگیری و مدیریت آدرسها از همین صفحه.'
|
||||||
|
|
||||||
|
export const configurationPageIntro =
|
||||||
|
'تنظیمات کلی سامانه (کلید–مقدار). با جستجو و فیلتر نوع فیلد، مورد را پیدا کنید؛ بعد از تغییر ذخیره کنید. روی آیکون i کنار هر تنظیم توضیح همان فیلد را ببینید.'
|
||||||
|
|
||||||
|
export const accountingReportsPageIntro =
|
||||||
|
'گزارشهای حسابداری داخلی پنل. برای اسناد و همگامسازی مشتریان از تبهای تهحساب / زرین / پرگار استفاده کنید.'
|
||||||
|
|
||||||
|
export const accountingTahesabDocsIntro =
|
||||||
|
'اسناد ارسالشده یا در صف تهحساب. میتوانید وضعیت، خطا و جزئیات هر سند را ببینید و در صورت نیاز همگام کنید.'
|
||||||
|
|
||||||
|
export const accountingZarinDocsIntro =
|
||||||
|
'اسناد ارسالشده یا در صف زرین. وضعیت، خطا و جزئیات هر سند را اینجا پیگیری کنید.'
|
||||||
|
|
||||||
|
export const accountingPargarDocsIntro =
|
||||||
|
'اسناد پرگار. وضعیت ارسال، خطا و جزئیات هر سند را اینجا ببینید.'
|
||||||
|
|
||||||
|
export const accountingTahesabCustomerSyncIntro =
|
||||||
|
'لینک کردن مشتریان پنل با کد تهحساب. تب «پنل ← تهحساب» کسانی که فقط در پنلاند؛ تب برعکس کسانی که فقط در تهحساباند.'
|
||||||
|
|
||||||
|
export const accountingZarinCustomerSyncIntro =
|
||||||
|
'لینک کردن مشتریان پنل با کد زرین. مشتریان بدون لینک را از دو طرف پیدا و ثبت کنید.'
|
||||||
|
|
||||||
|
export const accountingPargarCustomerSyncIntro =
|
||||||
|
'لینک کردن مشتریان پنل با کد پرگار. مشتریان بدون لینک را از دو طرف پیدا و ثبت کنید.'
|
||||||
|
|
||||||
|
export const accountingTahesabBalanceSyncIntro =
|
||||||
|
'جدول مشتریان لینکشده ماندهٔ کیف را کنار ماندهٔ تهحساب و سندها نشان میدهد. همگام تکی اول بررسی و تأیید است. «همگامسازی» همه فقط کیف خالی را مینشاند و تداخل را نگه میدارد؛ «اعمال اجباری» مانده تهحساب را برای همه جایگزین میکند. دو مانده جمع نمیشوند.'
|
||||||
|
|
||||||
|
export const accountingZarinBalanceSyncIntro =
|
||||||
|
'مانده طلا و ریال زرین را روی کیف اصلی مشتریان لینکشده مینشاند. همگامسازی دستی است؛ تداخل برای تصمیم شما میماند. دو مانده با هم جمع نمیشوند.'
|
||||||
|
|
||||||
|
export const accountingPargarBalanceSyncIntro =
|
||||||
|
'مانده پرگار را روی کیف مشتریان لینکشده مینشاند. قبل از اجرا مطمئن شوید کد پرگار مشتریان درست است.'
|
||||||
|
|
||||||
|
export const customerFundsIntro =
|
||||||
|
'داراییهای مرتبط با همین مشتری (محصول و موجودی). جزئیات و تراکنش هر دارایی را از اکشنها باز کنید.'
|
||||||
|
|
||||||
|
export const customerPiggyFundsIntro =
|
||||||
|
'قلکهای همین مشتری و موجودی هرکدام.'
|
||||||
|
|
||||||
|
export const customerReferralsIntro =
|
||||||
|
'مشتریانی که با کد معرف همین نفر ثبتنام کردهاند.'
|
||||||
|
|
||||||
|
export const customerPargarLogsIntro =
|
||||||
|
'لاگ و اسناد پرگار مربوط به همین مشتری.'
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ export function hasAppUiConfig(): boolean {
|
|||||||
return Boolean(cfg)
|
return Boolean(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** true اگر get_user → ui_variant برابر modular باشد (بنرهای تم اپ) */
|
||||||
|
export function isModularUiVariant(): boolean {
|
||||||
|
return String(getConfigApp('ui_variant') ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase() === 'modular'
|
||||||
|
}
|
||||||
|
|
||||||
let storeModeFromSettings: boolean | null = null
|
let storeModeFromSettings: boolean | null = null
|
||||||
let settingsBootstrapped = false
|
let settingsBootstrapped = false
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user