Show remaining invoice balance while adding and reviewing checks.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,7 +8,9 @@ import CurrencyInput from '@/components/ui/CurrencyInput.vue'
|
||||
import PersianDateInput from '@/components/ui/PersianDateInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { parseFormattedNumber } from '@/utils/format'
|
||||
import IconCash from '@/components/icons/IconCash.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import { formatCurrency, formatNumber, parseFormattedNumber } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
@@ -26,7 +28,7 @@ import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const { orderId } = useRequirePricedOrder()
|
||||
const { orderId, order } = useRequirePricedOrder()
|
||||
const isFull = computed(() => orderStore.draftCheckMode === 'full')
|
||||
|
||||
const amount = ref('')
|
||||
@@ -43,6 +45,40 @@ const backImage = ref('')
|
||||
const notes = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
const suggestedAmountApplied = ref(false)
|
||||
|
||||
const invoiceAmount = computed(() => order.value?.finalAmount ?? 0)
|
||||
const previousChecksTotal = computed(() =>
|
||||
orderStore.draftChecks.reduce((sum, check) => sum + check.amount, 0),
|
||||
)
|
||||
const currentCheckAmount = computed(() => parseFormattedNumber(amount.value) || 0)
|
||||
const remainingBeforeCurrent = computed(() =>
|
||||
Math.max(0, invoiceAmount.value - previousChecksTotal.value),
|
||||
)
|
||||
const remainingAfterCurrent = computed(() =>
|
||||
Math.max(0, invoiceAmount.value - previousChecksTotal.value - currentCheckAmount.value),
|
||||
)
|
||||
const isCovered = computed(
|
||||
() =>
|
||||
invoiceAmount.value > 0 &&
|
||||
previousChecksTotal.value + currentCheckAmount.value >= invoiceAmount.value,
|
||||
)
|
||||
const isOverInvoice = computed(
|
||||
() =>
|
||||
invoiceAmount.value > 0 &&
|
||||
previousChecksTotal.value + currentCheckAmount.value > invoiceAmount.value,
|
||||
)
|
||||
|
||||
watch(
|
||||
[invoiceAmount, remainingBeforeCurrent],
|
||||
() => {
|
||||
if (suggestedAmountApplied.value || amount.value) return
|
||||
if (remainingBeforeCurrent.value <= 0) return
|
||||
amount.value = String(remainingBeforeCurrent.value)
|
||||
suggestedAmountApplied.value = true
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(amount, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'amount')
|
||||
@@ -80,6 +116,11 @@ async function handleImageUpload(event: Event, target: 'front' | 'back') {
|
||||
else backImage.value = base64
|
||||
}
|
||||
|
||||
function fillRemainingAmount() {
|
||||
if (remainingBeforeCurrent.value <= 0) return
|
||||
amount.value = String(remainingBeforeCurrent.value)
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
@@ -157,6 +198,7 @@ function resetForm() {
|
||||
notes.value = ''
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
suggestedAmountApplied.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -171,6 +213,72 @@ function resetForm() {
|
||||
<div class="page__scroll checkout-page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div
|
||||
v-if="invoiceAmount > 0"
|
||||
class="check-balance mb-4 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4"
|
||||
:class="{
|
||||
'check-balance--ok': isCovered && !isOverInvoice,
|
||||
'check-balance--warn': isOverInvoice,
|
||||
}"
|
||||
>
|
||||
<div class="mb-3 flex items-start gap-3">
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[var(--color-primary-soft)] text-[var(--color-primary)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<IconCash :size="22" />
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="m-0 text-sm font-semibold text-[var(--color-text)]">مانده فاکتور</p>
|
||||
<p class="m-0 mt-1 text-xs leading-relaxed text-[var(--color-text-muted)]">
|
||||
با هر چک، مبلغ باقیمانده بهروز میشود تا پوشش کامل فاکتور را از دست ندهید.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-3 text-sm">
|
||||
<span class="text-[var(--color-text-muted)]">مبلغ فاکتور</span>
|
||||
<strong class="font-tabular text-[var(--color-text)]">{{ formatCurrency(invoiceAmount) }}</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 text-sm">
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--color-text-muted)]">
|
||||
<IconCheckBook :size="14" />
|
||||
چکهای ثبتشده
|
||||
<span v-if="orderStore.draftChecks.length" class="text-xs">
|
||||
({{ formatNumber(orderStore.draftChecks.length) }})
|
||||
</span>
|
||||
</span>
|
||||
<strong class="font-tabular text-[var(--color-text)]">
|
||||
{{ formatCurrency(previousChecksTotal) }}
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 flex items-center justify-between gap-3 border-t border-[var(--color-border)] pt-3"
|
||||
>
|
||||
<span class="text-sm font-semibold text-[var(--color-text)]">باقیمانده</span>
|
||||
<strong
|
||||
class="font-tabular text-base"
|
||||
:class="
|
||||
isCovered && !isOverInvoice
|
||||
? 'text-[var(--color-success)]'
|
||||
: isOverInvoice
|
||||
? 'text-[var(--color-danger)]'
|
||||
: 'text-[var(--color-primary)]'
|
||||
"
|
||||
>
|
||||
{{
|
||||
isOverInvoice
|
||||
? `${formatCurrency(previousChecksTotal + currentCheckAmount - invoiceAmount)} بیش از فاکتور`
|
||||
: isCovered
|
||||
? 'پوشش کامل شد'
|
||||
: formatCurrency(remainingAfterCurrent)
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<span class="step active">۱. اطلاعات چک</span>
|
||||
<span class="step">۲. چک بعدی</span>
|
||||
@@ -184,6 +292,14 @@ function resetForm() {
|
||||
placeholder="مبلغ"
|
||||
:error="fieldErrors.amount"
|
||||
/>
|
||||
<button
|
||||
v-if="remainingBeforeCurrent > 0 && currentCheckAmount !== remainingBeforeCurrent"
|
||||
type="button"
|
||||
class="check-fill-remaining mb-3 text-sm font-medium text-[var(--color-primary)]"
|
||||
@click="fillRemainingAmount"
|
||||
>
|
||||
پر کردن با باقیمانده ({{ formatCurrency(remainingBeforeCurrent) }})
|
||||
</button>
|
||||
<PersianDateInput v-model="dueDate" label="تاریخ سررسید" :error="fieldErrors.dueDate" />
|
||||
|
||||
<template v-if="isFull">
|
||||
@@ -262,6 +378,32 @@ function resetForm() {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.check-balance--ok {
|
||||
border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border));
|
||||
background: var(--color-success-soft);
|
||||
}
|
||||
|
||||
.check-balance--warn {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 35%, var(--color-border));
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
|
||||
.check-fill-remaining {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.check-fill-remaining:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.font-tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -60,10 +60,18 @@ const rass = computed(() => {
|
||||
|
||||
const invoiceAmount = computed(() => order.value?.finalAmount ?? 0)
|
||||
|
||||
const remainingAmount = computed(() =>
|
||||
Math.max(0, invoiceAmount.value - rass.value.totalAmount),
|
||||
)
|
||||
|
||||
const isAmountComplete = computed(
|
||||
() => invoiceAmount.value > 0 && rass.value.totalAmount >= invoiceAmount.value,
|
||||
)
|
||||
|
||||
const isOverInvoice = computed(
|
||||
() => invoiceAmount.value > 0 && rass.value.totalAmount > invoiceAmount.value,
|
||||
)
|
||||
|
||||
const openMenuId = computed({
|
||||
get: () => (route.query.menu as string) ?? '',
|
||||
set: (id: string) => {
|
||||
@@ -150,10 +158,33 @@ async function submitChecks() {
|
||||
<span class="summary-card__label">مبلغ فاکتور</span>
|
||||
<strong class="summary-card__value">{{ formatNumber(invoiceAmount) }}</strong>
|
||||
<span class="summary-card__unit">تومان</span>
|
||||
<p v-if="isAmountComplete" class="summary-card__status">
|
||||
<p v-if="isAmountComplete && !isOverInvoice" class="summary-card__status">
|
||||
<span class="summary-card__status-icon">✓</span>
|
||||
مبلغ کامل است
|
||||
</p>
|
||||
<p v-else-if="isOverInvoice" class="summary-card__status summary-card__status--warn">
|
||||
بیش از مبلغ فاکتور
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="summary-card summary-card--remain"
|
||||
:class="{
|
||||
'summary-card--remain-ok': isAmountComplete && !isOverInvoice,
|
||||
'summary-card--remain-warn': isOverInvoice,
|
||||
}"
|
||||
>
|
||||
<span class="summary-card__label">باقیمانده</span>
|
||||
<strong class="summary-card__value">
|
||||
{{
|
||||
isOverInvoice
|
||||
? formatNumber(rass.totalAmount - invoiceAmount)
|
||||
: formatNumber(remainingAmount)
|
||||
}}
|
||||
</strong>
|
||||
<span class="summary-card__unit">تومان</span>
|
||||
<p v-if="!isAmountComplete" class="summary-card__hint">
|
||||
هنوز باید چک ثبت کنید
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -282,6 +313,24 @@ async function submitChecks() {
|
||||
<span>مبلغ فاکتور</span>
|
||||
<strong>{{ formatNumber(invoiceAmount) }} تومان</strong>
|
||||
</div>
|
||||
<div
|
||||
class="desktop-summary-panel__row desktop-summary-panel__row--remain"
|
||||
:class="{
|
||||
'desktop-summary-panel__row--ok': isAmountComplete && !isOverInvoice,
|
||||
'desktop-summary-panel__row--warn': isOverInvoice,
|
||||
}"
|
||||
>
|
||||
<span>باقیمانده</span>
|
||||
<strong>
|
||||
{{
|
||||
isOverInvoice
|
||||
? `${formatNumber(rass.totalAmount - invoiceAmount)} بیش از فاکتور`
|
||||
: isAmountComplete
|
||||
? 'پوشش کامل'
|
||||
: `${formatNumber(remainingAmount)} تومان`
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>رأس چکها</span>
|
||||
<strong>{{ toPersianDigits(String(rass.rassDays)) }} روز</strong>
|
||||
@@ -348,6 +397,18 @@ async function submitChecks() {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row--remain strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row--ok strong {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row--warn strong {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.check-list-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -428,6 +489,22 @@ async function submitChecks() {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.summary-card--remain {
|
||||
grid-column: 1 / -1;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.summary-card--remain-ok {
|
||||
border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border));
|
||||
background: var(--color-success-soft);
|
||||
}
|
||||
|
||||
.summary-card--remain-warn {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 35%, var(--color-border));
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
|
||||
.summary-card__label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
@@ -461,6 +538,17 @@ async function submitChecks() {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.summary-card__status--warn {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.summary-card__hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.summary-card__status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
Reference in New Issue
Block a user