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>
This commit is contained in:
2026-09-05 22:30:58 +03:30
co-authored by Cursor
parent e0b89223ac
commit 9ef381f00e
6 changed files with 208 additions and 42 deletions
@@ -1,16 +1,24 @@
<template>
<BasicModal
v-model:visible="visibleProxy"
:width="isMobile ? '92vw' : '36rem'"
: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" style="color: var(--admin-text-secondary)">
موجودی اپ با مانده {{ sourceLabel }} فرق دارد. دو مانده جمع نمی‌شوند؛ یکی را پس از بررسی سندها تأیید کنید.
<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>
@@ -84,12 +92,58 @@
</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')">
نگه داشتن موجودی فعلی اپ
</a-button>
<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>
@@ -103,27 +157,34 @@ import BasicModal from '@/components/BasicModal.vue'
import { isMobile } from '@core/utils/is'
import type { TahesabPreviewDoc } from '@/api/tahesab'
const props = 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
walletDocs?: TahesabPreviewDoc[]
tahesabDocs?: TahesabPreviewDoc[]
}>()
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: 'overwrite' | 'keep'): void
(e: 'decide', mode: MandeDecisionMode): void
}>()
const loading = ref<'overwrite' | 'keep' | null>(null)
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)
@@ -140,6 +201,11 @@ 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,
@@ -186,8 +252,75 @@ function close() {
visibleProxy.value = false
}
function choose(mode: 'overwrite' | 'keep') {
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>