Surface paid vs pending installments and imported transaction counts after moving transactions off the payment document.
227 lines
10 KiB
Vue
227 lines
10 KiB
Vue
<!-- /src/views/payments/PaymentDetailView.vue -->
|
||
<template>
|
||
<div class="payment-detail-view" v-if="payment">
|
||
<PageHeader :title="$t('payments.paymentDetail')" :subtitle="`کد صورتحساب: ${payment._id || payment.id}`">
|
||
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/payments')" />
|
||
</PageHeader>
|
||
|
||
<div class="grid mb-4">
|
||
<!-- Summary Card -->
|
||
<div class="col-12 lg:col-4">
|
||
<div class="surface-card p-4 border-round border-1 border-color shadow-sm mb-4">
|
||
<h3 class="text-lg font-bold mb-3">خلاصه صورتحساب</h3>
|
||
<div class="flex flex-column gap-3">
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">نام کاربر / دانشجو</span>
|
||
<span class="font-bold text-color text-base">{{ payment.user?.name || payment.userName || 'کاربر' }}</span>
|
||
</div>
|
||
<div v-if="payment.classes && payment.classes.length">
|
||
<span class="text-muted text-xs block mb-1">کلاسهای مربوطه</span>
|
||
<div class="flex flex-wrap gap-1 mt-1">
|
||
<Tag v-for="c in payment.classes" :key="c._id || c" :value="c.name || 'کلاس'" severity="info" class="text-xs" />
|
||
</div>
|
||
</div>
|
||
<div v-else-if="payment.course">
|
||
<span class="text-muted text-xs block mb-1">دوره آموزشی</span>
|
||
<span class="font-semibold text-color text-sm">{{ payment.course?.title }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">مبلغ کل صورتحساب</span>
|
||
<span class="font-bold text-color text-xl">{{ toPersianDigits((payment.amount || 0).toLocaleString()) }} تومان</span>
|
||
</div>
|
||
<div v-if="payment.discount">
|
||
<span class="text-muted text-xs block mb-1">تخفیف</span>
|
||
<span class="font-semibold text-orange-500 text-lg">{{ toPersianDigits((payment.discount || 0).toLocaleString()) }} تومان</span>
|
||
</div>
|
||
<div v-if="payment.discount">
|
||
<span class="text-muted text-xs block mb-1">مبلغ قابل پرداخت</span>
|
||
<span class="font-bold text-color text-xl">{{ toPersianDigits(payableAmount.toLocaleString()) }} تومان</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">مبلغ کل دریافتی</span>
|
||
<span class="font-bold text-green-600 text-lg">{{ toPersianDigits((payment.paidAmount || 0).toLocaleString()) }} تومان</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">باقیمانده</span>
|
||
<span class="font-bold text-red-500 text-lg">{{ toPersianDigits(remainingAmount.toLocaleString()) }} تومان</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">وضعیت پرداخت</span>
|
||
<StatusTag :status="payment.status" type="payment" />
|
||
</div>
|
||
<div>
|
||
<span class="text-muted text-xs block mb-1">یادداشت</span>
|
||
<p class="text-sm text-color m-0 white-space-pre-wrap line-height-3">{{ payment.notes || '—' }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Transaction History & Form -->
|
||
<div class="col-12 lg:col-8">
|
||
<div class="surface-card p-4 border-round border-1 border-color shadow-sm mb-4">
|
||
<div class="flex align-items-center justify-content-between mb-3">
|
||
<h3 class="text-lg font-bold m-0">تاریخچه تراکنشهای دریافتی</h3>
|
||
<PermissionGate permission="payments:update">
|
||
<Button label="ثبت تراکنش جدید" icon="pi pi-plus" size="small" severity="success" @click="showTransactionModal = true" />
|
||
</PermissionGate>
|
||
</div>
|
||
|
||
<DataTable :value="payment.transactions || []" class="p-datatable-sm text-sm">
|
||
<Column field="amount" header="مبلغ تراکنش">
|
||
<template #body="{ data }">
|
||
{{ toPersianDigits(data.amount?.toLocaleString()) }} تومان
|
||
</template>
|
||
</Column>
|
||
<Column field="status" header="وضعیت">
|
||
<template #body="{ data }">
|
||
<StatusTag :status="data.status || (data.date ? 'paid' : 'pending')" type="payment" />
|
||
</template>
|
||
</Column>
|
||
<Column field="method" header="روش پرداخت">
|
||
<template #body="{ data }">
|
||
<Tag
|
||
v-if="data.method"
|
||
:value="data.method === 'online' ? 'درگاه آنلاین' : (data.method === 'card' ? 'کارت به کارت' : 'نقدی')"
|
||
severity="info"
|
||
/>
|
||
<span v-else class="text-muted">—</span>
|
||
</template>
|
||
</Column>
|
||
<Column field="receiptNumber" header="شماره فیش / پیگیری">
|
||
<template #body="{ data }">
|
||
<span dir="ltr">{{ toPersianDigits(data.receiptNumber || '-') }}</span>
|
||
</template>
|
||
</Column>
|
||
<Column field="dueDate" header="سررسید">
|
||
<template #body="{ data }">
|
||
{{ formatJalali(data.dueDate) }}
|
||
</template>
|
||
</Column>
|
||
<Column field="date" header="تاریخ پرداخت">
|
||
<template #body="{ data }">
|
||
{{ data.date ? formatJalali(data.date) : '—' }}
|
||
</template>
|
||
</Column>
|
||
<Column field="notes" header="یادداشت">
|
||
<template #body="{ data }">
|
||
<span class="white-space-pre-wrap">{{ data.notes || '—' }}</span>
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Record New Transaction Modal -->
|
||
<Dialog v-model:visible="showTransactionModal" header="ثبت تراکنش جدید" modal :style="{ width: '450px' }">
|
||
<div class="flex flex-column gap-3 py-2">
|
||
<div class="flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">مبلغ واریزی (تومان) *</label>
|
||
<InputNumber v-model="trxForm.amount" class="w-full text-sm" suffix=" تومان" />
|
||
</div>
|
||
<div class="flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">روش پرداخت *</label>
|
||
<Dropdown v-model="trxForm.method" :options="methodOptions" optionLabel="label" optionValue="value" class="w-full text-sm" />
|
||
</div>
|
||
<div class="flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">شماره فیش / پیگیری *</label>
|
||
<InputText v-model.trim="trxForm.receiptNumber" class="w-full text-sm" dir="ltr" />
|
||
</div>
|
||
<div class="flex flex-column gap-2">
|
||
<label class="font-semibold text-sm" for="trx-notes">یادداشت</label>
|
||
<Textarea
|
||
id="trx-notes"
|
||
v-model="trxForm.notes"
|
||
rows="3"
|
||
class="w-full text-sm"
|
||
maxlength="5000"
|
||
placeholder="یادداشت داخلی درباره این تراکنش…"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<Button label="انصراف" text severity="secondary" @click="showTransactionModal = false" />
|
||
<Button label="ثبت تراکنش" icon="pi pi-check" severity="success" :loading="isSubmittingTrx" @click="handleRecordTransaction" />
|
||
</template>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import { useRoute } from 'vue-router';
|
||
import { paymentApi } from '@/api/paymentApi';
|
||
import { usePersianDate } from '@/composables/usePersianDate';
|
||
import { useToast } from '@/composables/useToast';
|
||
import { getPayableAmount } from '@/utils/paymentAmount';
|
||
import PageHeader from '@/components/common/PageHeader.vue';
|
||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||
import StatusTag from '@/components/common/StatusTag.vue';
|
||
import Button from 'primevue/button';
|
||
import Tag from 'primevue/tag';
|
||
import DataTable from 'primevue/datatable';
|
||
import Column from 'primevue/column';
|
||
import Dialog from 'primevue/dialog';
|
||
import Dropdown from 'primevue/select';
|
||
import InputText from 'primevue/inputtext';
|
||
import InputNumber from 'primevue/inputnumber';
|
||
import Textarea from 'primevue/textarea';
|
||
|
||
const route = useRoute();
|
||
const paymentId = route.params.id;
|
||
const { toPersianDigits, formatJalali } = usePersianDate();
|
||
const { showSuccess, showError } = useToast();
|
||
|
||
const payment = ref(null);
|
||
const showTransactionModal = ref(false);
|
||
const isSubmittingTrx = ref(false);
|
||
|
||
const methodOptions = [
|
||
{ label: 'کارت به کارت', value: 'card' },
|
||
{ label: 'درگاه آنلاین', value: 'online' },
|
||
{ label: 'نقدی / شبا', value: 'cash' }
|
||
];
|
||
|
||
const trxForm = reactive({
|
||
amount: 500000,
|
||
method: 'card',
|
||
receiptNumber: '',
|
||
notes: ''
|
||
});
|
||
|
||
const payableAmount = computed(() => getPayableAmount(payment.value || {}));
|
||
const remainingAmount = computed(() => Math.max(0, payableAmount.value - (payment.value?.paidAmount || 0)));
|
||
|
||
const fetchPayment = async () => {
|
||
try {
|
||
const res = await paymentApi.getOne(paymentId);
|
||
payment.value = res.data || res;
|
||
} catch (err) {
|
||
showError(err);
|
||
}
|
||
};
|
||
|
||
const handleRecordTransaction = async () => {
|
||
if (!trxForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
|
||
if (!trxForm.receiptNumber) { showError('لطفا شماره فیش یا پیگیری را وارد کنید'); return; }
|
||
|
||
isSubmittingTrx.value = true;
|
||
try {
|
||
await paymentApi.recordTransaction(paymentId, trxForm);
|
||
showSuccess('تراکنش جدید با موفقیت ثبت شد');
|
||
showTransactionModal.value = false;
|
||
trxForm.notes = '';
|
||
fetchPayment();
|
||
} catch (err) {
|
||
showError(err);
|
||
} finally {
|
||
isSubmittingTrx.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchPayment();
|
||
});
|
||
</script>
|