Files
boursecu_admin/src/pages/menuPages/customers/base/components/TahesabMandeDecisionModal.vue
T
mohmdsdgandCursor 9ef381f00e feat(tahesab): three-way mande decision UI including push to Tahesab
Clarify overwrite vs keep vs push-to-tahesab, warn when app balance is zero, and update colleague wait-callback help text.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:30:58 +03:30

327 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<BasicModal
v-model:visible="visibleProxy"
:width="isMobile ? '92vw' : '40rem'"
title="تصمیم برای مانده حساب"
:custom-footer="true"
cancel-text="بعداً"
@close="close"
>
<div class="flex flex-col gap-3 text-sm" style="color: var(--admin-text)">
<p class="m-0 leading-6" style="color: var(--admin-text-secondary)">
موجودی اپ با مانده {{ sourceLabel }} فرق دارد. دو مانده جمع نمی‌شوند — یکی از سه مسیر زیر را انتخاب کنید.
</p>
<div
v-if="appIsEmpty && allowPush"
class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-900"
>
موجودی اپ صفر است. اگر «اعمال مانده اپ روی ته‌حساب» را بزنید، مانده ته‌حساب با سند تعدیل هم‌تراز صفر اپ می‌شود.
</div>
<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">
<span style="color: var(--admin-text-secondary)">موجودی اپ (طلا)</span>
<strong dir="ltr">{{ formatGold(appGold) }}</strong>
</div>
<div class="flex justify-between gap-2">
<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">
<span style="color: var(--admin-text-secondary)">موجودی اپ (ریال)</span>
<strong dir="ltr">{{ formatCash(appCash) }}</strong>
</div>
<div class="flex justify-between gap-2">
<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">
<button
type="button"
class="mande-choice mande-choice--primary"
:disabled="!!loading"
@click="choose('overwrite')"
>
<span class="mande-choice__title">
<a-spin v-if="loading === 'overwrite'" size="small" class="me-1" />
اعمال مانده {{ sourceLabel }} روی کیف اپ
</span>
<span class="mande-choice__hint">
موجودی اپ مثل {{ sourceLabel }} می‌شود. سند تعدیل روی کیف اپ ثبت می‌شود.
</span>
</button>
<button
v-if="allowPush"
type="button"
class="mande-choice"
:class="appIsEmpty ? 'mande-choice--warn' : 'mande-choice--secondary'"
:disabled="!!loading"
@click="choose('push')"
>
<span class="mande-choice__title">
<a-spin v-if="loading === 'push'" size="small" class="me-1" />
اعمال مانده اپ روی {{ sourceLabel }}
<span v-if="appIsEmpty" class="mande-choice__badge">اپ صفر</span>
</span>
<span class="mande-choice__hint">
{{
appIsEmpty
? `مانده ${sourceLabel} با سند تعدیل هم‌تراز صفر اپ می‌شود. کیف اپ تغییر نمی‌کند.`
: `مانده ${sourceLabel} مثل موجودی اپ می‌شود. کیف اپ تغییر نمی‌کند.`
}}
</span>
</button>
<button
type="button"
class="mande-choice"
:disabled="!!loading"
@click="choose('keep')"
>
<span class="mande-choice__title">
<a-spin v-if="loading === 'keep'" size="small" class="me-1" />
نگه داشتن وضعیت فعلی
</span>
<span class="mande-choice__hint">
نه کیف اپ عوض می‌شود نه {{ sourceLabel }}؛ اختلاف برای بعد می‌ماند.
</span>
</button>
<a-button type="link" block :disabled="!!loading" @click="close">بعداً</a-button>
</div>
</div>
<template #footer><span /></template>
</BasicModal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import BasicModal from '@/components/BasicModal.vue'
import { isMobile } from '@core/utils/is'
import type { TahesabPreviewDoc } from '@/api/tahesab'
export type MandeDecisionMode = 'overwrite' | 'keep' | 'push'
const props = withDefaults(
defineProps<{
visible: boolean
appGold?: number | null
appCash?: number | null
tahesabGold?: number | null
tahesabCash?: number | null
sourceGold?: number | null
sourceCash?: number | null
goldDelta?: number | null
rialDelta?: number | null
sourceLabel?: string
/** فقط ته‌حساب: اجازهٔ نوشتن مانده اپ روی حسابداری */
allowPush?: boolean
walletDocs?: TahesabPreviewDoc[]
tahesabDocs?: TahesabPreviewDoc[]
}>(),
{ allowPush: false }
)
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'decide', mode: MandeDecisionMode): void
}>()
const loading = ref<MandeDecisionMode | 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 appIsEmpty = computed(() => {
const g = Math.abs(Number(props.appGold ?? 0))
const c = Math.abs(Number(props.appCash ?? 0))
return g < 0.0005 && c < 1
})
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value),
})
watch(
() => props.visible,
(v) => {
if (!v) loading.value = null
}
)
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 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
}
function choose(mode: MandeDecisionMode) {
loading.value = mode
emit('decide', mode)
}
</script>
<style scoped>
.mande-choice {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.25rem;
width: 100%;
text-align: right;
border-radius: 0.75rem;
border: 1px solid var(--admin-border, #e5e7eb);
background: var(--admin-surface, #fff);
padding: 0.75rem 0.875rem;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.mande-choice:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.mande-choice:not(:disabled):hover {
border-color: var(--admin-primary, #10913a);
}
.mande-choice--primary {
border-color: var(--admin-primary, #10913a);
background: color-mix(in srgb, var(--admin-primary, #10913a) 8%, white);
}
.mande-choice--secondary:not(:disabled):hover {
background: var(--admin-bg-muted, #f8fafc);
}
.mande-choice--warn {
border-color: #f59e0b;
background: #fffbeb;
}
.mande-choice__title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
font-size: 0.875rem;
font-weight: 700;
color: var(--admin-text, #111827);
}
.mande-choice__hint {
font-size: 0.75rem;
line-height: 1.5;
color: var(--admin-text-secondary, #6b7280);
}
.mande-choice__badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
background: #fef3c7;
color: #92400e;
font-size: 0.6875rem;
font-weight: 700;
padding: 0.1rem 0.45rem;
}
</style>