feat(tahesab): poll customer sync from any page and reuse the progress modal
Start outbound/inbound on the server; hydrate after refresh so the transfer modal returns without opening accounting. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import CustomerImportProgress from '@/components/CustomerImportProgress.vue'
|
|||||||
import TahesabDocSyncProgress from '@/components/TahesabDocSyncProgress.vue'
|
import TahesabDocSyncProgress from '@/components/TahesabDocSyncProgress.vue'
|
||||||
import GroupSmsSendProgress from '@/components/GroupSmsSendProgress.vue'
|
import GroupSmsSendProgress from '@/components/GroupSmsSendProgress.vue'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
||||||
import { bootstrapAdminChat, useAdminChatService } from '@/utils/adminChat'
|
import { bootstrapAdminChat, useAdminChatService } from '@/utils/adminChat'
|
||||||
import { supportChatMenuEnabledSetting } from '@/utils/config'
|
import { supportChatMenuEnabledSetting } from '@/utils/config'
|
||||||
|
|
||||||
@@ -66,7 +67,15 @@ watch(latestIncomingNotification, (payload) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await config.getConfigData()
|
await config.getConfigData()
|
||||||
|
void useTahesabDocSyncStore().hydrate()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => userStore.userInfo?.id,
|
||||||
|
(id) => {
|
||||||
|
if (id) void useTahesabDocSyncStore().hydrate()
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less"></style>
|
<style lang="less"></style>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface TahesabDocSyncState {
|
|||||||
running: boolean
|
running: boolean
|
||||||
finished: boolean
|
finished: boolean
|
||||||
stopped: boolean
|
stopped: boolean
|
||||||
|
stage?: 'docs' | 'customers_out' | 'customers_in'
|
||||||
percent: number
|
percent: number
|
||||||
total: number
|
total: number
|
||||||
processed: number
|
processed: number
|
||||||
@@ -45,6 +46,9 @@ export interface TahesabDocSyncState {
|
|||||||
failed: number
|
failed: number
|
||||||
current: string
|
current: string
|
||||||
errors: { label?: string; error?: string }[]
|
errors: { label?: string; error?: string }[]
|
||||||
|
doc_total?: number
|
||||||
|
doc_success?: number
|
||||||
|
doc_failed?: number
|
||||||
started_at?: string | null
|
started_at?: string | null
|
||||||
finished_at?: string | null
|
finished_at?: string | null
|
||||||
}
|
}
|
||||||
@@ -97,6 +101,16 @@ export const stopTahesabDocSync = async () =>
|
|||||||
'/admin/tahesab/doc-sync/stop'
|
'/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) =>
|
export const registerCustomerInTahesab = async (customerId: number) =>
|
||||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||||
`/admin/customer/${customerId}/tahesab/register`
|
`/admin/customer/${customerId}/tahesab/register`
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<span class="cip-counter__sep">/</span>
|
<span class="cip-counter__sep">/</span>
|
||||||
<span class="cip-counter__total">{{ store.state.total }}</span>
|
<span class="cip-counter__total">{{ store.state.total }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cip-counter__label">در حال ارسال سندها به تهحساب…</div>
|
<div class="cip-counter__label">{{ runningLabel }}</div>
|
||||||
<div class="cip-track">
|
<div class="cip-track">
|
||||||
<div class="cip-track__fill" :style="{ width: store.percent + '%' }" />
|
<div class="cip-track__fill" :style="{ width: store.percent + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
@@ -59,14 +59,12 @@
|
|||||||
<Icon :icon="outcomeInfo.icon" :size="34" />
|
<Icon :icon="outcomeInfo.icon" :size="34" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cip-report__title">{{ outcomeInfo.text }}</div>
|
<div class="cip-report__title">{{ outcomeInfo.text }}</div>
|
||||||
<div class="cip-report__subtitle">
|
<div class="cip-report__subtitle">{{ reportSubtitle }}</div>
|
||||||
از {{ store.state.total }} سند، {{ store.state.processed }} مورد پردازش شد
|
|
||||||
</div>
|
|
||||||
<div class="cip-report__cards">
|
<div class="cip-report__cards">
|
||||||
<div class="cip-rcard cip-rcard--ok">
|
<div class="cip-rcard cip-rcard--ok">
|
||||||
<Icon icon="vuesax-linear:tick-circle" :size="20" />
|
<Icon icon="vuesax-linear:tick-circle" :size="20" />
|
||||||
<div class="cip-rcard__num">{{ store.state.success }}</div>
|
<div class="cip-rcard__num">{{ store.state.success }}</div>
|
||||||
<div class="cip-rcard__label">ارسال شد</div>
|
<div class="cip-rcard__label">{{ doneCardLabel }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cip-rcard" :class="store.state.failed ? 'cip-rcard--fail' : 'cip-rcard--muted'">
|
<div class="cip-rcard" :class="store.state.failed ? 'cip-rcard--fail' : 'cip-rcard--muted'">
|
||||||
<Icon icon="vuesax-linear:close-circle" :size="20" />
|
<Icon icon="vuesax-linear:close-circle" :size="20" />
|
||||||
@@ -121,7 +119,7 @@
|
|||||||
<span class="cip-mini__text">
|
<span class="cip-mini__text">
|
||||||
<span class="cip-mini__title">
|
<span class="cip-mini__title">
|
||||||
<template v-if="store.finished">{{ outcomeInfo.text }}</template>
|
<template v-if="store.finished">{{ outcomeInfo.text }}</template>
|
||||||
<template v-else>ارسال سند به تهحساب</template>
|
<template v-else>{{ runningMiniTitle }}</template>
|
||||||
</span>
|
</span>
|
||||||
<span class="cip-mini__count">
|
<span class="cip-mini__count">
|
||||||
<template v-if="store.finished">
|
<template v-if="store.finished">
|
||||||
@@ -145,21 +143,71 @@ import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
|||||||
|
|
||||||
const store = useTahesabDocSyncStore()
|
const store = useTahesabDocSyncStore()
|
||||||
|
|
||||||
const OUTCOME_MAP = {
|
const stage = computed(() => store.stage)
|
||||||
success: { icon: 'vuesax-linear:tick-circle', text: 'ارسال سندها تمام شد', tone: 'ok' as const },
|
|
||||||
error: { icon: 'vuesax-linear:close-circle', text: 'ارسال سندها ناموفق بود', tone: 'fail' as const },
|
function outcomeMap() {
|
||||||
partial: { icon: 'vuesax-linear:danger', text: 'بعضی سندها به خطا خورد', tone: 'warn' as const },
|
if (stage.value === 'customers_in') {
|
||||||
canceled: { icon: 'vuesax-linear:info-circle', text: 'ارسال متوقف شد', tone: 'warn' as const },
|
return {
|
||||||
running: { icon: 'vuesax-linear:document-upload', text: 'ارسال سند به تهحساب', tone: 'ok' as const },
|
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 outcomeInfo = computed(() => outcomeMap()[store.outcome])
|
||||||
const headIcon = computed(() =>
|
const runningIcon = computed(() => {
|
||||||
store.finished ? outcomeInfo.value.icon : 'vuesax-linear:document-upload',
|
if (stage.value === 'customers_in') return 'vuesax-linear:import'
|
||||||
)
|
if (stage.value === 'customers_out') return 'vuesax-linear:profile-2user'
|
||||||
const headTitle = computed(() =>
|
return 'vuesax-linear:document-upload'
|
||||||
store.finished ? 'گزارش ارسال سندها' : 'ارسال سندها به تهحساب',
|
})
|
||||||
)
|
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 RING_CIRC = 2 * Math.PI * 15.5
|
||||||
const ringStyle = computed(() => ({
|
const ringStyle = computed(() => ({
|
||||||
strokeDasharray: `${RING_CIRC}`,
|
strokeDasharray: `${RING_CIRC}`,
|
||||||
|
|||||||
@@ -105,8 +105,8 @@
|
|||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
:loading="syncingPanel"
|
:loading="docSyncStore.starting || (docSyncStore.active && docSyncStore.stage === 'customers_out')"
|
||||||
:disabled="!status?.panel_only"
|
:disabled="!status?.panel_only && !docSyncStore.active"
|
||||||
@click="syncAllPanel"
|
@click="syncAllPanel"
|
||||||
>
|
>
|
||||||
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
||||||
@@ -243,11 +243,15 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
size="small"
|
size="small"
|
||||||
:loading="importStore.active"
|
:loading="docSyncStore.starting || (docSyncStore.active && docSyncStore.stage === 'customers_in')"
|
||||||
:disabled="!importableCount"
|
:disabled="!importableCount && !docSyncStore.active"
|
||||||
@click="importAll"
|
@click="importAll"
|
||||||
>
|
>
|
||||||
{{ importStore.active ? 'در حال…' : `وارد کردن همه (${importableCount})` }}
|
{{
|
||||||
|
docSyncStore.active && docSyncStore.stage === 'customers_in'
|
||||||
|
? 'در حال…'
|
||||||
|
: `وارد کردن همه (${importableCount})`
|
||||||
|
}}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,7 +368,6 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
|||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
import {
|
import {
|
||||||
getCustomerSyncStatus,
|
getCustomerSyncStatus,
|
||||||
syncPanelToTahesab,
|
|
||||||
importFromTahesab,
|
importFromTahesab,
|
||||||
registerCustomerInTahesab,
|
registerCustomerInTahesab,
|
||||||
linkTahesabMatchingPhones,
|
linkTahesabMatchingPhones,
|
||||||
@@ -374,14 +377,12 @@ import {
|
|||||||
type TahesabOnlyCustomer,
|
type TahesabOnlyCustomer,
|
||||||
type SyncPanelResult,
|
type SyncPanelResult,
|
||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
import { useCustomerImportStore } from '@/store/customerImportStore'
|
|
||||||
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
||||||
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
import ColleagueAccountingAccounts from '@/pages/menuPages/colleagueOrders/components/ColleagueAccountingAccounts.vue'
|
||||||
import TahesabDocSyncBox from '@/pages/menuPages/accounting/components/TahesabDocSyncBox.vue'
|
import TahesabDocSyncBox from '@/pages/menuPages/accounting/components/TahesabDocSyncBox.vue'
|
||||||
import { accountingTahesabCustomerSyncIntro } from '@/utils/adminHelpCopy'
|
import { accountingTahesabCustomerSyncIntro } from '@/utils/adminHelpCopy'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
|
||||||
const importStore = useCustomerImportStore()
|
|
||||||
const docSyncStore = useTahesabDocSyncStore()
|
const docSyncStore = useTahesabDocSyncStore()
|
||||||
|
|
||||||
// ─── state ───
|
// ─── state ───
|
||||||
@@ -600,18 +601,15 @@ async function registerOnePanel(customer: PanelOnlyCustomer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncAllPanel() {
|
async function syncAllPanel() {
|
||||||
syncingPanel.value = true
|
if (docSyncStore.active) {
|
||||||
panelSyncResult.value = null
|
docSyncStore.expand()
|
||||||
showPanelErrors.value = false
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await syncPanelToTahesab()
|
await docSyncStore.startOutbound()
|
||||||
panelSyncResult.value = res
|
} catch (e: any) {
|
||||||
if (res.success) message.success(`${res.success} مشتری ثبت شد`)
|
message.error(e?.response?.data?.message || e?.message || 'شروع ثبت ناموفق بود')
|
||||||
if (res.failed) message.warning(`${res.failed} مشتری ثبت نشد`)
|
}
|
||||||
await load(1)
|
|
||||||
panelDone.clear()
|
|
||||||
} catch { message.error('خطا در ارتباط با سرور') }
|
|
||||||
finally { syncingPanel.value = false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── تب ۲: عملیات ───
|
// ─── تب ۲: عملیات ───
|
||||||
@@ -703,11 +701,15 @@ async function importSelected() {
|
|||||||
async function importAll() {
|
async function importAll() {
|
||||||
const pending = importableList.value
|
const pending = importableList.value
|
||||||
if (!pending.length) return
|
if (!pending.length) return
|
||||||
if (importStore.active) {
|
if (docSyncStore.active) {
|
||||||
importStore.expand()
|
docSyncStore.expand()
|
||||||
return
|
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() {
|
async function confirmLinkPhones() {
|
||||||
@@ -771,26 +773,21 @@ async function enrichPageMande() {
|
|||||||
|
|
||||||
watch([tahesabPage, tahesabSearch], () => { void enrichPageMande() })
|
watch([tahesabPage, tahesabSearch], () => { void enrichPageMande() })
|
||||||
|
|
||||||
// با پیشرفت عملیات سراسری، ردیفهای وارد شده را همینجا هم تیک بزن
|
watch(
|
||||||
watch(() => importStore.doneCodes.length, () => {
|
() => docSyncStore.finished,
|
||||||
importStore.doneCodes.forEach(code => tahesabDone.add(code))
|
async (done, wasDone) => {
|
||||||
})
|
if (!done || wasDone) return
|
||||||
|
await load(1)
|
||||||
// پس از پایان کامل، آمار و لیست را تازه کن
|
panelDone.clear()
|
||||||
watch(() => importStore.status, async (s, prev) => {
|
if (docSyncStore.stage === 'customers_in') {
|
||||||
if (prev === 'running' && (s === 'done' || s === 'canceled')) {
|
tahesabFetched.value = false
|
||||||
selectedTahesab.clear()
|
if (activeTab.value === 'tahesab') await loadTahesab()
|
||||||
if (status.value?.tahesab_only_count != null) {
|
|
||||||
status.value.tahesab_only_count = Math.max(0, status.value.tahesab_only_count - importStore.success)
|
|
||||||
}
|
}
|
||||||
await loadPanel(panelPagination.value.page)
|
},
|
||||||
}
|
)
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
load(1)
|
load(1)
|
||||||
// اگر عملیات سراسری در حال اجراست، ردیفهای انجامشده را همگام کن
|
|
||||||
importStore.doneCodes.forEach(code => tahesabDone.add(code))
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<p class="m-0 text-sm font-800 text-gray-800">ارسال سندها به تهحساب</p>
|
<p class="m-0 text-sm font-800 text-gray-800">ارسال سندها به تهحساب</p>
|
||||||
<p class="mb-0 mt-1 text-xs leading-relaxed text-gray-500">
|
<p class="mb-0 mt-1 text-xs leading-relaxed text-gray-500">
|
||||||
با یک کلیک همهٔ سفارشها و شارژهای جاافتاده یکییکی و با فاصله به تهحساب میروند.
|
با یک کلیک همهٔ سفارشها و شارژهای جاافتاده یکییکی و با فاصله به تهحساب میروند.
|
||||||
سندهایی که قبلاً رفتهاند دوباره ارسال نمیشوند؛ میتوانید پیشرفت را کوچک کنید.
|
بعد از سندها، مشتریان پنلِ بدون کد هم همانجا ثبت میشوند. پیشرفت را میتوانید کوچک کنید.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a-button
|
<a-button
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ import { defineStore } from 'pinia'
|
|||||||
import {
|
import {
|
||||||
getTahesabPendingSummary,
|
getTahesabPendingSummary,
|
||||||
startTahesabDocSync,
|
startTahesabDocSync,
|
||||||
|
startTahesabCustomerInbound,
|
||||||
|
startTahesabCustomerOutbound,
|
||||||
stopTahesabDocSync,
|
stopTahesabDocSync,
|
||||||
type TahesabDocSyncState,
|
type TahesabDocSyncState,
|
||||||
} from '@/api/tahesab'
|
} from '@/api/tahesab'
|
||||||
|
|
||||||
export type DocSyncOutcome = 'running' | 'success' | 'error' | 'partial' | 'canceled'
|
export type DocSyncOutcome = 'running' | 'success' | 'error' | 'partial' | 'canceled'
|
||||||
|
|
||||||
const POLL_MS = 10_000
|
const POLL_DOCS_MS = 10_000
|
||||||
|
const POLL_CUSTOMERS_MS = 3_000
|
||||||
|
|
||||||
function emptyState(): TahesabDocSyncState {
|
function emptyState(): TahesabDocSyncState {
|
||||||
return {
|
return {
|
||||||
running: false,
|
running: false,
|
||||||
finished: false,
|
finished: false,
|
||||||
stopped: false,
|
stopped: false,
|
||||||
|
stage: 'docs',
|
||||||
percent: 0,
|
percent: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
processed: 0,
|
processed: 0,
|
||||||
@@ -23,6 +27,9 @@ function emptyState(): TahesabDocSyncState {
|
|||||||
failed: 0,
|
failed: 0,
|
||||||
current: '',
|
current: '',
|
||||||
errors: [],
|
errors: [],
|
||||||
|
doc_total: 0,
|
||||||
|
doc_success: 0,
|
||||||
|
doc_failed: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +44,7 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
|
|||||||
const active = computed(() => state.value.running)
|
const active = computed(() => state.value.running)
|
||||||
const finished = computed(() => state.value.finished && !state.value.running)
|
const finished = computed(() => state.value.finished && !state.value.running)
|
||||||
const percent = computed(() => state.value.percent)
|
const percent = computed(() => state.value.percent)
|
||||||
|
const stage = computed(() => state.value.stage || 'docs')
|
||||||
const outcome = computed<DocSyncOutcome>(() => {
|
const outcome = computed<DocSyncOutcome>(() => {
|
||||||
if (state.value.running) return 'running'
|
if (state.value.running) return 'running'
|
||||||
if (state.value.stopped) return 'canceled'
|
if (state.value.stopped) return 'canceled'
|
||||||
@@ -45,13 +53,18 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
|
|||||||
return 'partial'
|
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) {
|
function apply(snapshot?: TahesabDocSyncState | null) {
|
||||||
if (!snapshot) return
|
if (!snapshot) return
|
||||||
const wasVisible = visible.value
|
const wasVisible = visible.value
|
||||||
state.value = { ...emptyState(), ...snapshot }
|
state.value = { ...emptyState(), ...snapshot }
|
||||||
if (snapshot.running) {
|
if (snapshot.running) {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
startPoll()
|
restartPoll()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stopPoll()
|
stopPoll()
|
||||||
@@ -64,7 +77,12 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
|
|||||||
if (timer) return
|
if (timer) return
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
void hydrate()
|
void hydrate()
|
||||||
}, POLL_MS)
|
}, pollMs())
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartPoll() {
|
||||||
|
stopPoll()
|
||||||
|
startPoll()
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPoll() {
|
function stopPoll() {
|
||||||
@@ -74,12 +92,16 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrate() {
|
async function hydrate(retries = 2) {
|
||||||
try {
|
try {
|
||||||
const summary = await getTahesabPendingSummary()
|
const summary = await getTahesabPendingSummary()
|
||||||
apply(summary.doc_sync)
|
apply(summary.doc_sync)
|
||||||
} catch {
|
} 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) {
|
if (state.value.running) {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
minimized.value = false
|
minimized.value = false
|
||||||
startPoll()
|
restartPoll()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
starting.value = true
|
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() {
|
async function stop() {
|
||||||
const res = await stopTahesabDocSync()
|
const res = await stopTahesabDocSync()
|
||||||
message.value = res.message || ''
|
message.value = res.message || ''
|
||||||
@@ -135,10 +197,13 @@ export const useTahesabDocSyncStore = defineStore('tahesabDocSync', () => {
|
|||||||
active,
|
active,
|
||||||
finished,
|
finished,
|
||||||
percent,
|
percent,
|
||||||
|
stage,
|
||||||
outcome,
|
outcome,
|
||||||
apply,
|
apply,
|
||||||
hydrate,
|
hydrate,
|
||||||
start,
|
start,
|
||||||
|
startOutbound,
|
||||||
|
startInbound,
|
||||||
stop,
|
stop,
|
||||||
minimize,
|
minimize,
|
||||||
expand,
|
expand,
|
||||||
|
|||||||
Reference in New Issue
Block a user