Files
gameno-dashboard/src/views/payments/PaymentDetailView.vue
T
kavehhn 047344e2ed Refetch payment after transaction edit to show updated amount.
Coerce edited amount to a number and reload invoice data after save.
2026-08-16 22:36:12 +03:30

386 lines
17 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- /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>
<Column header="عملیات" style="width: 5rem">
<template #body="{ data }">
<PermissionGate permission="payments:update">
<Button icon="pi pi-pencil" text rounded severity="secondary" size="small" @click="openEditTransaction(data)" />
</PermissionGate>
</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=" تومان" :min="1" />
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm">وضعیت تراکنش *</label>
<Dropdown v-model="trxForm.status" :options="statusOptions" optionLabel="label" optionValue="value" class="w-full text-sm" />
</div>
<div v-if="trxForm.status === 'paid'" 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>
<DatePicker v-model="trxForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
</div>
<div v-if="trxForm.status === 'paid'" class="flex flex-column gap-2">
<label class="font-semibold text-sm">تاریخ پرداخت</label>
<DatePicker v-model="trxForm.date" class="w-full text-sm" placeholder="1403/01/01" />
</div>
<div v-if="trxForm.status === 'paid'" 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>
<!-- Edit Transaction Modal -->
<Dialog v-model:visible="showEditModal" 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="editForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm">وضعیت تراکنش *</label>
<Dropdown v-model="editForm.status" :options="statusOptions" optionLabel="label" optionValue="value" class="w-full text-sm" />
</div>
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
<label class="font-semibold text-sm">روش پرداخت</label>
<Dropdown v-model="editForm.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>
<DatePicker v-model="editForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
</div>
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
<label class="font-semibold text-sm">تاریخ پرداخت</label>
<DatePicker v-model="editForm.date" class="w-full text-sm" placeholder="1403/01/01" />
</div>
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
<label class="font-semibold text-sm">شماره فیش / پیگیری</label>
<InputText v-model.trim="editForm.receiptNumber" class="w-full text-sm" dir="ltr" />
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm" for="edit-trx-notes">یادداشت</label>
<Textarea
id="edit-trx-notes"
v-model="editForm.notes"
rows="3"
class="w-full text-sm"
maxlength="5000"
placeholder="یادداشت داخلی درباره این تراکنش…"
/>
</div>
</div>
<template #footer>
<Button label="انصراف" text severity="secondary" @click="showEditModal = false" />
<Button label="ذخیره تغییرات" icon="pi pi-check" severity="success" :loading="isSubmittingEdit" @click="handleUpdateTransaction" />
</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';
import DatePicker from 'vue3-persian-datetime-picker';
const route = useRoute();
const paymentId = route.params.id;
const { toPersianDigits, formatJalali, toJalaliPickerValue, toGregorianIso } = usePersianDate();
const { showSuccess, showError } = useToast();
const payment = ref(null);
const showTransactionModal = ref(false);
const showEditModal = ref(false);
const isSubmittingTrx = ref(false);
const isSubmittingEdit = ref(false);
const editingTransactionId = ref(null);
const methodOptions = [
{ label: 'کارت به کارت', value: 'card' },
{ label: 'درگاه آنلاین', value: 'online' },
{ label: 'نقدی / شبا', value: 'cash' }
];
const statusOptions = [
{ label: 'پرداخت شده', value: 'paid' },
{ label: 'در انتظار پرداخت', value: 'pending' }
];
const defaultTrxForm = () => ({
amount: 500000,
status: 'paid',
method: 'card',
receiptNumber: '',
dueDate: '',
date: '',
notes: ''
});
const trxForm = reactive(defaultTrxForm());
const editForm = reactive({
amount: 0,
status: 'paid',
method: 'card',
receiptNumber: '',
dueDate: '',
date: '',
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.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
const dueDate = toGregorianIso(trxForm.dueDate);
if (!dueDate) { showError('تاریخ سررسید نامعتبر است'); return; }
if (trxForm.status === 'paid' && !trxForm.receiptNumber) {
showError('لطفا شماره فیش یا پیگیری را وارد کنید');
return;
}
isSubmittingTrx.value = true;
try {
const payload = {
amount: trxForm.amount,
status: trxForm.status,
dueDate,
notes: trxForm.notes || undefined
};
if (trxForm.status === 'paid') {
payload.method = trxForm.method;
payload.receiptNumber = trxForm.receiptNumber;
const paymentDate = toGregorianIso(trxForm.date);
if (paymentDate) payload.date = paymentDate;
}
const res = await paymentApi.recordTransaction(paymentId, payload);
payment.value = res.data || res;
showSuccess('تراکنش جدید با موفقیت ثبت شد');
showTransactionModal.value = false;
Object.assign(trxForm, defaultTrxForm());
} catch (err) {
showError(err);
} finally {
isSubmittingTrx.value = false;
}
};
const openEditTransaction = (trx) => {
editingTransactionId.value = trx._id || trx.id;
editForm.amount = trx.amount || 0;
editForm.status = trx.status || (trx.date ? 'paid' : 'pending');
editForm.method = trx.method || 'card';
editForm.receiptNumber = trx.receiptNumber || '';
editForm.dueDate = toJalaliPickerValue(trx.dueDate);
editForm.date = toJalaliPickerValue(trx.date);
editForm.notes = trx.notes || '';
showEditModal.value = true;
};
const handleUpdateTransaction = async () => {
if (!editForm.amount && editForm.amount !== 0) {
showError('لطفا مبلغ را وارد کنید');
return;
}
if (!editForm.dueDate) {
showError('لطفا تاریخ سررسید را وارد کنید');
return;
}
const dueDate = toGregorianIso(editForm.dueDate);
if (!dueDate) {
showError('تاریخ سررسید نامعتبر است');
return;
}
isSubmittingEdit.value = true;
try {
const payload = {
amount: Number(editForm.amount),
status: editForm.status,
dueDate,
notes: editForm.notes
};
if (editForm.status === 'paid') {
payload.method = editForm.method;
payload.receiptNumber = editForm.receiptNumber;
payload.date = toGregorianIso(editForm.date);
}
await paymentApi.updateTransaction(editingTransactionId.value, payload);
await fetchPayment();
showSuccess('تراکنش با موفقیت به‌روزرسانی شد');
showEditModal.value = false;
} catch (err) {
showError(err);
} finally {
isSubmittingEdit.value = false;
}
};
onMounted(() => {
fetchPayment();
});
</script>