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:
2026-08-30 21:18:26 +03:30
co-authored by Cursor
parent 04cc03fe25
commit 1c4b2d4435
60 changed files with 2464 additions and 552 deletions
@@ -27,21 +27,26 @@
برای پاک کردن کد، فیلد را خالی بگذارید و ذخیره کنید.
</p>
<p
v-if="provider === 'tahesab'"
v-if="provider === 'tahesab' || provider === 'zarin'"
class="m-0 text-xs"
style="color: var(--admin-text-secondary)"
>
بعد از ذخیره کد ته‌حساب، اگر موجودی اپ با مانده ته‌حساب فرق داشته باشد از شما تصمیم گرفته می‌شود.
قبل از ذخیره، اگر مانده اپ با حسابداری فرق داشته باشد از شما تأیید گرفته می‌شود؛ بعد اعمال می‌شود.
</p>
</div>
</BasicModal>
<TahesabMandeDecisionModal
v-model:visible="decisionVisible"
:source-label="provider === 'zarin' ? 'زرین' : 'ته‌حساب'"
:app-gold="decision?.app_gold"
:app-cash="decision?.app_cash"
:tahesab-gold="decision?.tahesab_gold"
:tahesab-cash="decision?.tahesab_cash"
:tahesab-gold="decision?.tahesab_gold ?? decision?.zarin_gold"
: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"
/>
</template>
@@ -52,9 +57,14 @@ import { message } from 'ant-design-vue'
import BasicModal from '@/components/BasicModal.vue'
import { updateCustomer } from '@/api/customer'
import {
previewCustomerWalletFromTahesab,
syncCustomerWalletFromTahesab,
type TahesabWalletSyncResult,
} from '@/api/tahesab'
import {
previewCustomerWalletFromZarin,
syncCustomerWalletFromZarin,
} from '@/api/zarin'
import type { Customer } from '@/model/customer'
import type { AccountingProvider } from '@/utils/config'
import { displayAccountingCode, normalizeAccountingCode } from '@/utils/accountingCode'
@@ -76,7 +86,7 @@ const loading = ref(false)
const code = ref('')
const decisionVisible = ref(false)
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({
get: () => props.visible,
@@ -103,47 +113,44 @@ function close() {
visibleProxy.value = false
}
async function maybeSyncTahesabWallet(customerId: number, previousCode: string, nextCode: string) {
if (props.provider !== 'tahesab') return
if (!nextCode || nextCode === previousCode) return
try {
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 saveCode(customerId: number, body: Record<string, string>) {
await updateCustomer(customerId, body as Customer)
message.success('کد حسابداری ذخیره شد.')
emit('success')
close()
}
async function onDecide(mode: 'overwrite' | 'keep') {
const customerId = savedCustomerId.value
decisionVisible.value = false
if (!customerId) return
try {
const res = await syncCustomerWalletFromTahesab(customerId, {
async function applyWalletDecision(customerId: number, mode: 'overwrite' | 'keep') {
if (props.provider === 'zarin') {
return syncCustomerWalletFromZarin(customerId, {
overwrite: mode === 'overwrite',
keep: mode === 'keep',
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')
} catch (e) {
console.error(e)
} finally {
loading.value = false
pendingPayload.value = null
}
}
@@ -161,13 +168,38 @@ async function submit() {
? { pargar_account_id: normalized }
: { moshtari_code: normalized }
const needsWalletAsk =
(props.provider === 'tahesab' || props.provider === 'zarin')
&& !!normalized
&& normalized !== previousCode
try {
loading.value = true
await updateCustomer(props.customer.id, payload as Customer)
message.success('کد حسابداری ذخیره شد.')
emit('success')
close()
await maybeSyncTahesabWallet(props.customer.id, previousCode, normalized)
if (needsWalletAsk) {
try {
const preview = props.provider === 'zarin'
? await previewCustomerWalletFromZarin(props.customer.id, 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) {
console.error(e)
} finally {