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>
327 lines
12 KiB
Vue
327 lines
12 KiB
Vue
<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>
|