2026-08-30 14:16:36 +03:30
|
|
|
|
<template>
|
|
|
|
|
|
<BasicModal
|
|
|
|
|
|
v-model:visible="visibleProxy"
|
2026-09-05 22:30:58 +03:30
|
|
|
|
:width="isMobile ? '92vw' : '40rem'"
|
2026-08-30 14:16:36 +03:30
|
|
|
|
title="تصمیم برای مانده حساب"
|
|
|
|
|
|
:custom-footer="true"
|
|
|
|
|
|
cancel-text="بعداً"
|
|
|
|
|
|
@close="close"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex flex-col gap-3 text-sm" style="color: var(--admin-text)">
|
2026-09-05 22:30:58 +03:30
|
|
|
|
<p class="m-0 leading-6" style="color: var(--admin-text-secondary)">
|
|
|
|
|
|
موجودی اپ با مانده {{ sourceLabel }} فرق دارد. دو مانده جمع نمیشوند — یکی از سه مسیر زیر را انتخاب کنید.
|
2026-08-30 14:16:36 +03:30
|
|
|
|
</p>
|
2026-09-05 22:30:58 +03:30
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
<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">
|
2026-08-30 21:18:26 +03:30
|
|
|
|
<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>
|
2026-08-30 14:16:36 +03:30
|
|
|
|
</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">
|
2026-08-30 21:18:26 +03:30
|
|
|
|
<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>
|
2026-08-30 14:16:36 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-30 21:18:26 +03:30
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
<div class="flex flex-col gap-2">
|
2026-09-05 22:30:58 +03:30
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
<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'
|
2026-08-30 21:18:26 +03:30
|
|
|
|
import type { TahesabPreviewDoc } from '@/api/tahesab'
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
2026-09-05 22:30:58 +03:30
|
|
|
|
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 }
|
|
|
|
|
|
)
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'update:visible', value: boolean): void
|
2026-09-05 22:30:58 +03:30
|
|
|
|
(e: 'decide', mode: MandeDecisionMode): void
|
2026-08-30 14:16:36 +03:30
|
|
|
|
}>()
|
|
|
|
|
|
|
2026-09-05 22:30:58 +03:30
|
|
|
|
const loading = ref<MandeDecisionMode | null>(null)
|
2026-08-30 21:18:26 +03:30
|
|
|
|
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
|
|
|
|
|
|
)
|
2026-09-05 22:30:58 +03:30
|
|
|
|
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
|
|
|
|
|
|
})
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 21:18:26 +03:30
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
function close() {
|
|
|
|
|
|
visibleProxy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 22:30:58 +03:30
|
|
|
|
function choose(mode: MandeDecisionMode) {
|
2026-08-30 14:16:36 +03:30
|
|
|
|
loading.value = mode
|
|
|
|
|
|
emit('decide', mode)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-09-05 22:30:58 +03:30
|
|
|
|
|
|
|
|
|
|
<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>
|