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
@@ -1,7 +1,7 @@
<template>
<BasicModal
v-model:visible="visibleProxy"
:width="isMobile ? '92vw' : '28rem'"
:width="isMobile ? '92vw' : '36rem'"
title="تصمیم برای مانده حساب"
:custom-footer="true"
cancel-text="بعداً"
@@ -9,7 +9,7 @@
>
<div class="flex flex-col gap-3 text-sm" style="color: var(--admin-text)">
<p class="m-0" style="color: var(--admin-text-secondary)">
موجودی اپ با مانده ته‌حساب فرق دارد. کدام را اعمال کنیم؟
موجودی اپ با مانده {{ sourceLabel }} فرق دارد. دو مانده جمع نمی‌شوند؛ یکی را پس از بررسی سندها تأیید کنید.
</p>
<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">
@@ -17,8 +17,12 @@
<strong dir="ltr">{{ formatGold(appGold) }}</strong>
</div>
<div class="flex justify-between gap-2">
<span style="color: var(--admin-text-secondary)">مانده ته‌حساب (طلا)</span>
<strong dir="ltr">{{ formatGold(tahesabGold) }}</strong>
<span style="color: var(--admin-text-secondary)">مانده {{ sourceLabel }} (طلا)</span>
<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 class="h-px" style="background: var(--admin-border)" />
<div class="flex justify-between gap-2">
@@ -26,13 +30,62 @@
<strong dir="ltr">{{ formatCash(appCash) }}</strong>
</div>
<div class="flex justify-between gap-2">
<span style="color: var(--admin-text-secondary)">مانده ته‌حساب (ریال)</span>
<strong dir="ltr">{{ formatCash(tahesabCash) }}</strong>
<span style="color: var(--admin-text-secondary)">مانده {{ sourceLabel }} (ریال)</span>
<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 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">
<a-button type="primary" block :loading="loading === 'overwrite'" :disabled="!!loading" @click="choose('overwrite')">
اعمال مانده ته‌حساب روی کیف
اعمال مانده {{ sourceLabel }} روی کیف
</a-button>
<a-button block :loading="loading === 'keep'" :disabled="!!loading" @click="choose('keep')">
نگه داشتن موجودی فعلی اپ
@@ -48,6 +101,7 @@
import { computed, ref, watch } from 'vue'
import BasicModal from '@/components/BasicModal.vue'
import { isMobile } from '@core/utils/is'
import type { TahesabPreviewDoc } from '@/api/tahesab'
const props = defineProps<{
visible: boolean
@@ -55,6 +109,13 @@ const props = defineProps<{
appCash?: number | null
tahesabGold?: 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<{
@@ -63,6 +124,22 @@ const emit = defineEmits<{
}>()
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({
get: () => props.visible,
@@ -86,6 +163,25 @@ function formatCash(v?: number | null) {
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() {
visibleProxy.value = false
}