diff --git a/src/App.vue b/src/App.vue
index ad52768..8595286 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,6 +16,7 @@ import CustomerImportProgress from '@/components/CustomerImportProgress.vue'
import TahesabDocSyncProgress from '@/components/TahesabDocSyncProgress.vue'
import GroupSmsSendProgress from '@/components/GroupSmsSendProgress.vue'
import { useUserStore } from '@/store/userStore'
+import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
import { bootstrapAdminChat, useAdminChatService } from '@/utils/adminChat'
import { supportChatMenuEnabledSetting } from '@/utils/config'
@@ -66,7 +67,15 @@ watch(latestIncomingNotification, (payload) => {
onMounted(async () => {
await config.getConfigData()
+ void useTahesabDocSyncStore().hydrate()
})
+
+watch(
+ () => userStore.userInfo?.id,
+ (id) => {
+ if (id) void useTahesabDocSyncStore().hydrate()
+ },
+)
diff --git a/src/api/tahesab.ts b/src/api/tahesab.ts
index 0a2f943..6fde2af 100644
--- a/src/api/tahesab.ts
+++ b/src/api/tahesab.ts
@@ -38,6 +38,7 @@ export interface TahesabDocSyncState {
running: boolean
finished: boolean
stopped: boolean
+ stage?: 'docs' | 'customers_out' | 'customers_in'
percent: number
total: number
processed: number
@@ -45,6 +46,9 @@ export interface TahesabDocSyncState {
failed: number
current: string
errors: { label?: string; error?: string }[]
+ doc_total?: number
+ doc_success?: number
+ doc_failed?: number
started_at?: string | null
finished_at?: string | null
}
@@ -97,6 +101,16 @@ export const stopTahesabDocSync = async () =>
'/admin/tahesab/doc-sync/stop'
)
+export const startTahesabCustomerOutbound = async () =>
+ await useAxios.post<{ ok: boolean; message: string; doc_sync: TahesabDocSyncState }>(
+ '/admin/tahesab/customer-sync/outbound/start'
+ )
+
+export const startTahesabCustomerInbound = async () =>
+ await useAxios.post<{ ok: boolean; message: string; doc_sync: TahesabDocSyncState }>(
+ '/admin/tahesab/customer-sync/inbound/start'
+ )
+
export const registerCustomerInTahesab = async (customerId: number) =>
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
`/admin/customer/${customerId}/tahesab/register`
diff --git a/src/components/TahesabDocSyncProgress.vue b/src/components/TahesabDocSyncProgress.vue
index 469b7be..953c303 100644
--- a/src/components/TahesabDocSyncProgress.vue
+++ b/src/components/TahesabDocSyncProgress.vue
@@ -33,7 +33,7 @@
/
{{ store.state.total }}
-
در حال ارسال سندها به تهحساب…
+ {{ runningLabel }}
@@ -59,14 +59,12 @@
{{ outcomeInfo.text }}
-
- از {{ store.state.total }} سند، {{ store.state.processed }} مورد پردازش شد
-
+ {{ reportSubtitle }}
{{ store.state.success }}
-
ارسال شد
+
{{ doneCardLabel }}
@@ -121,7 +119,7 @@
{{ outcomeInfo.text }}
- ارسال سند به تهحساب
+ {{ runningMiniTitle }}
@@ -145,21 +143,71 @@ import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
const store = useTahesabDocSyncStore()
-const OUTCOME_MAP = {
- success: { icon: 'vuesax-linear:tick-circle', text: 'ارسال سندها تمام شد', tone: 'ok' as const },
- error: { icon: 'vuesax-linear:close-circle', text: 'ارسال سندها ناموفق بود', tone: 'fail' as const },
- partial: { icon: 'vuesax-linear:danger', text: 'بعضی سندها به خطا خورد', tone: 'warn' as const },
- canceled: { icon: 'vuesax-linear:info-circle', text: 'ارسال متوقف شد', tone: 'warn' as const },
- running: { icon: 'vuesax-linear:document-upload', text: 'ارسال سند به تهحساب', tone: 'ok' as const },
+const stage = computed(() => store.stage)
+
+function outcomeMap() {
+ if (stage.value === 'customers_in') {
+ return {
+ success: { icon: 'vuesax-linear:tick-circle', text: 'ورود مشتریان تمام شد', tone: 'ok' as const },
+ error: { icon: 'vuesax-linear:close-circle', text: 'ورود مشتریان ناموفق بود', tone: 'fail' as const },
+ partial: { icon: 'vuesax-linear:danger', text: 'بعضی مشتریان وارد نشدند', tone: 'warn' as const },
+ canceled: { icon: 'vuesax-linear:info-circle', text: 'ورود متوقف شد', tone: 'warn' as const },
+ running: { icon: 'vuesax-linear:import', text: 'ورود از تهحساب', tone: 'ok' as const },
+ }
+ }
+ if (stage.value === 'customers_out') {
+ return {
+ success: { icon: 'vuesax-linear:tick-circle', text: 'ثبت مشتریان تمام شد', tone: 'ok' as const },
+ error: { icon: 'vuesax-linear:close-circle', text: 'ثبت مشتریان ناموفق بود', tone: 'fail' as const },
+ partial: { icon: 'vuesax-linear:danger', text: 'بعضی مشتریان ثبت نشدند', tone: 'warn' as const },
+ canceled: { icon: 'vuesax-linear:info-circle', text: 'ثبت متوقف شد', tone: 'warn' as const },
+ running: { icon: 'vuesax-linear:profile-2user', text: 'ثبت در تهحساب', tone: 'ok' as const },
+ }
+ }
+ return {
+ success: { icon: 'vuesax-linear:tick-circle', text: 'ارسال سندها تمام شد', tone: 'ok' as const },
+ error: { icon: 'vuesax-linear:close-circle', text: 'ارسال سندها ناموفق بود', tone: 'fail' as const },
+ partial: { icon: 'vuesax-linear:danger', text: 'بعضی سندها به خطا خورد', tone: 'warn' as const },
+ canceled: { icon: 'vuesax-linear:info-circle', text: 'ارسال متوقف شد', tone: 'warn' as const },
+ running: { icon: 'vuesax-linear:document-upload', text: 'ارسال سند به تهحساب', tone: 'ok' as const },
+ }
}
-const outcomeInfo = computed(() => OUTCOME_MAP[store.outcome])
-const headIcon = computed(() =>
- store.finished ? outcomeInfo.value.icon : 'vuesax-linear:document-upload',
-)
-const headTitle = computed(() =>
- store.finished ? 'گزارش ارسال سندها' : 'ارسال سندها به تهحساب',
-)
+const outcomeInfo = computed(() => outcomeMap()[store.outcome])
+const runningIcon = computed(() => {
+ if (stage.value === 'customers_in') return 'vuesax-linear:import'
+ if (stage.value === 'customers_out') return 'vuesax-linear:profile-2user'
+ return 'vuesax-linear:document-upload'
+})
+const headIcon = computed(() => (store.finished ? outcomeInfo.value.icon : runningIcon.value))
+const headTitle = computed(() => {
+ if (store.finished) {
+ if (stage.value === 'customers_in') return 'گزارش ورود مشتریان'
+ if (stage.value === 'customers_out') return 'گزارش ثبت مشتریان'
+ return 'گزارش ارسال سندها'
+ }
+ if (stage.value === 'customers_in') return 'ورود مشتریان از تهحساب'
+ if (stage.value === 'customers_out') return 'ثبت مشتریان در تهحساب'
+ return 'ارسال سندها به تهحساب'
+})
+const runningLabel = computed(() => {
+ if (stage.value === 'customers_in') return 'در حال وارد کردن مشتریان از تهحساب…'
+ if (stage.value === 'customers_out') return 'در حال ثبت مشتریان پنل در تهحساب…'
+ return 'در حال ارسال سندها به تهحساب…'
+})
+const runningMiniTitle = computed(() => {
+ if (stage.value === 'customers_in') return 'ورود از تهحساب'
+ if (stage.value === 'customers_out') return 'ثبت مشتری در تهحساب'
+ return 'ارسال سند به تهحساب'
+})
+const unit = computed(() => (stage.value === 'docs' ? 'سند' : 'مشتری'))
+const doneCardLabel = computed(() => (stage.value === 'docs' ? 'ارسال شد' : 'ثبت شد'))
+const reportSubtitle = computed(() => {
+ const docs = store.state.doc_total
+ ? `سندها: ${store.state.doc_success} موفق` + (store.state.doc_failed ? `، ${store.state.doc_failed} خطا. ` : '. ')
+ : ''
+ return `${docs}از ${store.state.total} ${unit.value}، ${store.state.processed} مورد پردازش شد`
+})
const RING_CIRC = 2 * Math.PI * 15.5
const ringStyle = computed(() => ({
strokeDasharray: `${RING_CIRC}`,
diff --git a/src/pages/menuPages/accounting/components/TahesabCustomerSync.vue b/src/pages/menuPages/accounting/components/TahesabCustomerSync.vue
index c3f08a0..5604c43 100644
--- a/src/pages/menuPages/accounting/components/TahesabCustomerSync.vue
+++ b/src/pages/menuPages/accounting/components/TahesabCustomerSync.vue
@@ -105,8 +105,8 @@
@@ -243,11 +243,15 @@
- {{ importStore.active ? 'در حال…' : `وارد کردن همه (${importableCount})` }}
+ {{
+ docSyncStore.active && docSyncStore.stage === 'customers_in'
+ ? 'در حال…'
+ : `وارد کردن همه (${importableCount})`
+ }}
@@ -364,7 +368,6 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { toJalali } from '@core/utils/useDataTime'
import {
getCustomerSyncStatus,
- syncPanelToTahesab,
importFromTahesab,
registerCustomerInTahesab,
linkTahesabMatchingPhones,
@@ -374,14 +377,12 @@ import {
type TahesabOnlyCustomer,
type SyncPanelResult,
} from '@/api/tahesab'
-import { useCustomerImportStore } from '@/store/customerImportStore'
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
import TahesabDocSyncBox from '@/pages/menuPages/accounting/components/TahesabDocSyncBox.vue'
import { accountingTahesabCustomerSyncIntro } from '@/utils/adminHelpCopy'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
-const importStore = useCustomerImportStore()
const docSyncStore = useTahesabDocSyncStore()
// ─── state ───
@@ -600,18 +601,15 @@ async function registerOnePanel(customer: PanelOnlyCustomer) {
}
async function syncAllPanel() {
- syncingPanel.value = true
- panelSyncResult.value = null
- showPanelErrors.value = false
+ if (docSyncStore.active) {
+ docSyncStore.expand()
+ return
+ }
try {
- const res = await syncPanelToTahesab()
- panelSyncResult.value = res
- if (res.success) message.success(`${res.success} مشتری ثبت شد`)
- if (res.failed) message.warning(`${res.failed} مشتری ثبت نشد`)
- await load(1)
- panelDone.clear()
- } catch { message.error('خطا در ارتباط با سرور') }
- finally { syncingPanel.value = false }
+ await docSyncStore.startOutbound()
+ } catch (e: any) {
+ message.error(e?.response?.data?.message || e?.message || 'شروع ثبت ناموفق بود')
+ }
}
// ─── تب ۲: عملیات ───
@@ -703,11 +701,15 @@ async function importSelected() {
async function importAll() {
const pending = importableList.value
if (!pending.length) return
- if (importStore.active) {
- importStore.expand()
+ if (docSyncStore.active) {
+ docSyncStore.expand()
return
}
- confirmImport(pending, () => importStore.start(pending, 'tahesab'))
+ confirmImport(pending, () => {
+ void docSyncStore.startInbound().catch((e: any) => {
+ message.error(e?.response?.data?.message || e?.message || 'شروع ورود ناموفق بود')
+ })
+ })
}
async function confirmLinkPhones() {
@@ -771,26 +773,21 @@ async function enrichPageMande() {
watch([tahesabPage, tahesabSearch], () => { void enrichPageMande() })
-// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
-watch(() => importStore.doneCodes.length, () => {
- importStore.doneCodes.forEach(code => tahesabDone.add(code))
-})
-
-// پس از پایان کامل، آمار و لیست را تازه کن
-watch(() => importStore.status, async (s, prev) => {
- if (prev === 'running' && (s === 'done' || s === 'canceled')) {
- selectedTahesab.clear()
- if (status.value?.tahesab_only_count != null) {
- status.value.tahesab_only_count = Math.max(0, status.value.tahesab_only_count - importStore.success)
+watch(
+ () => docSyncStore.finished,
+ async (done, wasDone) => {
+ if (!done || wasDone) return
+ await load(1)
+ panelDone.clear()
+ if (docSyncStore.stage === 'customers_in') {
+ tahesabFetched.value = false
+ if (activeTab.value === 'tahesab') await loadTahesab()
}
- await loadPanel(panelPagination.value.page)
- }
-})
+ },
+)
onMounted(() => {
load(1)
- // اگر عملیات سراسری در حال اجراست، ردیفهای انجامشده را همگام کن
- importStore.doneCodes.forEach(code => tahesabDone.add(code))
})
diff --git a/src/pages/menuPages/accounting/components/TahesabDocSyncBox.vue b/src/pages/menuPages/accounting/components/TahesabDocSyncBox.vue
index 14f0db5..d30d794 100644
--- a/src/pages/menuPages/accounting/components/TahesabDocSyncBox.vue
+++ b/src/pages/menuPages/accounting/components/TahesabDocSyncBox.vue
@@ -6,7 +6,7 @@
ارسال سندها به تهحساب
با یک کلیک همهٔ سفارشها و شارژهای جاافتاده یکییکی و با فاصله به تهحساب میروند.
- سندهایی که قبلاً رفتهاند دوباره ارسال نمیشوند؛ میتوانید پیشرفت را کوچک کنید.
+ بعد از سندها، مشتریان پنلِ بدون کد هم همانجا ثبت میشوند. پیشرفت را میتوانید کوچک کنید.
{
const active = computed(() => state.value.running)
const finished = computed(() => state.value.finished && !state.value.running)
const percent = computed(() => state.value.percent)
+ const stage = computed(() => state.value.stage || 'docs')
const outcome = computed(() => {
if (state.value.running) return 'running'
if (state.value.stopped) return 'canceled'
@@ -45,13 +53,18 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
return 'partial'
})
+ function pollMs() {
+ const s = state.value.stage
+ return s === 'customers_out' || s === 'customers_in' ? POLL_CUSTOMERS_MS : POLL_DOCS_MS
+ }
+
function apply(snapshot?: TahesabDocSyncState | null) {
if (!snapshot) return
const wasVisible = visible.value
state.value = { ...emptyState(), ...snapshot }
if (snapshot.running) {
visible.value = true
- startPoll()
+ restartPoll()
return
}
stopPoll()
@@ -64,7 +77,12 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
if (timer) return
timer = setInterval(() => {
void hydrate()
- }, POLL_MS)
+ }, pollMs())
+ }
+
+ function restartPoll() {
+ stopPoll()
+ startPoll()
}
function stopPoll() {
@@ -74,12 +92,16 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
}
}
- async function hydrate() {
+ async function hydrate(retries = 2) {
try {
const summary = await getTahesabPendingSummary()
apply(summary.doc_sync)
} catch {
- /* ignore */
+ if (retries > 0) {
+ window.setTimeout(() => {
+ void hydrate(retries - 1)
+ }, 1500)
+ }
}
}
@@ -88,7 +110,7 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
if (state.value.running) {
visible.value = true
minimized.value = false
- startPoll()
+ restartPoll()
return
}
starting.value = true
@@ -103,6 +125,46 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
}
}
+ async function startOutbound() {
+ if (starting.value) return
+ if (state.value.running) {
+ visible.value = true
+ minimized.value = false
+ restartPoll()
+ return
+ }
+ starting.value = true
+ try {
+ const res = await startTahesabCustomerOutbound()
+ message.value = res.message || ''
+ apply(res.doc_sync)
+ visible.value = true
+ minimized.value = false
+ } finally {
+ starting.value = false
+ }
+ }
+
+ async function startInbound() {
+ if (starting.value) return
+ if (state.value.running) {
+ visible.value = true
+ minimized.value = false
+ restartPoll()
+ return
+ }
+ starting.value = true
+ try {
+ const res = await startTahesabCustomerInbound()
+ message.value = res.message || ''
+ apply(res.doc_sync)
+ visible.value = true
+ minimized.value = false
+ } finally {
+ starting.value = false
+ }
+ }
+
async function stop() {
const res = await stopTahesabDocSync()
message.value = res.message || ''
@@ -135,10 +197,13 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
active,
finished,
percent,
+ stage,
outcome,
apply,
hydrate,
start,
+ startOutbound,
+ startInbound,
stop,
minimize,
expand,